From bb6d04be3b6e9320cb162280fed843ee6d2d12c1 Mon Sep 17 00:00:00 2001 From: Andrii Landiak Date: Fri, 8 Sep 2023 14:54:02 +0300 Subject: [PATCH 001/345] Logic for handling rule chain updates from edge, including metadata --- .../service/edge/rpc/EdgeGrpcSession.java | 14 +- .../service/edge/rpc/EdgeSyncCursor.java | 2 +- .../edge/rpc/processor/BaseEdgeProcessor.java | 3 + .../rule/BaseRuleChainProcessor.java | 127 ++++++++++++++++++ .../rule/RuleChainEdgeProcessor.java | 102 +++++++++++++- .../server/edge/AbstractEdgeTest.java | 46 ++----- .../server/edge/AssetEdgeTest.java | 26 ++++ .../server/edge/DashboardEdgeTest.java | 23 ++++ .../server/edge/DeviceEdgeTest.java | 31 +++++ .../server/edge/RuleChainEdgeTest.java | 72 +++++----- .../server/dao/rule/RuleChainService.java | 2 + common/edge-api/src/main/proto/edge.proto | 5 +- .../server/dao/rule/BaseRuleChainService.java | 15 ++- 13 files changed, 390 insertions(+), 78 deletions(-) create mode 100644 application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/rule/BaseRuleChainProcessor.java 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 cfb92d0bfd..538a12bd08 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 @@ -65,6 +65,8 @@ import org.thingsboard.server.gen.edge.v1.RequestMsg; import org.thingsboard.server.gen.edge.v1.RequestMsgType; import org.thingsboard.server.gen.edge.v1.ResponseMsg; import org.thingsboard.server.gen.edge.v1.RuleChainMetadataRequestMsg; +import org.thingsboard.server.gen.edge.v1.RuleChainMetadataUpdateMsg; +import org.thingsboard.server.gen.edge.v1.RuleChainUpdateMsg; import org.thingsboard.server.gen.edge.v1.SyncCompletedMsg; import org.thingsboard.server.gen.edge.v1.UplinkMsg; import org.thingsboard.server.gen.edge.v1.UplinkResponseMsg; @@ -628,7 +630,7 @@ public final class EdgeGrpcSession implements Closeable { case CUSTOMER: return ctx.getCustomerProcessor().convertCustomerEventToDownlink(edgeEvent); case RULE_CHAIN: - return ctx.getRuleChainProcessor().convertRuleChainEventToDownlink(edgeEvent); + return ctx.getRuleChainProcessor().convertRuleChainEventToDownlink(edgeEvent, this.edgeVersion); case RULE_CHAIN_METADATA: return ctx.getRuleChainProcessor().convertRuleChainMetadataEventToDownlink(edgeEvent, this.edgeVersion); case ALARM: @@ -690,6 +692,16 @@ public final class EdgeGrpcSession implements Closeable { result.add(ctx.getAssetProcessor().processAssetMsgFromEdge(edge.getTenantId(), edge, assetUpdateMsg)); } } + if (uplinkMsg.getRuleChainUpdateMsgCount() > 0) { + for (RuleChainUpdateMsg ruleChainUpdateMsg : uplinkMsg.getRuleChainUpdateMsgList()) { + result.add(ctx.getRuleChainProcessor().processRuleChainMsgFromEdge(edge.getTenantId(), edge, ruleChainUpdateMsg)); + } + } + if (uplinkMsg.getRuleChainMetadataUpdateMsgCount() > 0) { + for (RuleChainMetadataUpdateMsg ruleChainMetadataUpdateMsg : uplinkMsg.getRuleChainMetadataUpdateMsgList()) { + result.add(ctx.getRuleChainProcessor().processRuleChainMetadataMsgFromEdge(edge.getTenantId(), edge, ruleChainMetadataUpdateMsg)); + } + } if (uplinkMsg.getAlarmUpdateMsgCount() > 0) { for (AlarmUpdateMsg alarmUpdateMsg : uplinkMsg.getAlarmUpdateMsgList()) { result.add(ctx.getAlarmProcessor().processAlarmMsg(edge.getTenantId(), alarmUpdateMsg)); diff --git a/application/src/main/java/org/thingsboard/server/service/edge/rpc/EdgeSyncCursor.java b/application/src/main/java/org/thingsboard/server/service/edge/rpc/EdgeSyncCursor.java index a39dae3b54..586e1b403a 100644 --- a/application/src/main/java/org/thingsboard/server/service/edge/rpc/EdgeSyncCursor.java +++ b/application/src/main/java/org/thingsboard/server/service/edge/rpc/EdgeSyncCursor.java @@ -53,7 +53,6 @@ public class EdgeSyncCursor { public EdgeSyncCursor(EdgeContextComponent ctx, Edge edge, boolean fullSync) { if (fullSync) { fetchers.add(new QueuesEdgeEventFetcher(ctx.getQueueService())); - fetchers.add(new RuleChainsEdgeEventFetcher(ctx.getRuleChainService())); fetchers.add(new AdminSettingsEdgeEventFetcher(ctx.getAdminSettingsService(), ctx.getFreemarkerConfig())); fetchers.add(new TenantEdgeEventFetcher(ctx.getTenantService())); fetchers.add(new TenantAdminUsersEdgeEventFetcher(ctx.getUserService())); @@ -64,6 +63,7 @@ public class EdgeSyncCursor { fetchers.add(new CustomerUsersEdgeEventFetcher(ctx.getUserService(), edge.getCustomerId())); } } + fetchers.add(new RuleChainsEdgeEventFetcher(ctx.getRuleChainService())); fetchers.add(new DashboardsEdgeEventFetcher(ctx.getDashboardService())); fetchers.add(new DefaultProfilesEdgeEventFetcher(ctx.getDeviceProfileService(), ctx.getAssetProfileService())); fetchers.add(new DeviceProfilesEdgeEventFetcher(ctx.getDeviceProfileService())); diff --git a/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/BaseEdgeProcessor.java b/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/BaseEdgeProcessor.java index 328a64efe2..78bd56d90c 100644 --- a/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/BaseEdgeProcessor.java +++ b/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/BaseEdgeProcessor.java @@ -233,6 +233,9 @@ public abstract class BaseEdgeProcessor { @Autowired protected DataValidator entityViewValidator; + @Autowired + protected DataValidator ruleChainValidator; + @Autowired protected EdgeMsgConstructor edgeMsgConstructor; diff --git a/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/rule/BaseRuleChainProcessor.java b/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/rule/BaseRuleChainProcessor.java new file mode 100644 index 0000000000..dc9fa6e14d --- /dev/null +++ b/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/rule/BaseRuleChainProcessor.java @@ -0,0 +1,127 @@ +/** + * Copyright © 2016-2023 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.service.edge.rpc.processor.rule; + +import com.datastax.oss.driver.api.core.uuid.Uuids; +import lombok.extern.slf4j.Slf4j; +import org.thingsboard.common.util.JacksonUtil; +import org.thingsboard.server.common.data.id.RuleChainId; +import org.thingsboard.server.common.data.id.RuleNodeId; +import org.thingsboard.server.common.data.id.TenantId; +import org.thingsboard.server.common.data.rule.NodeConnectionInfo; +import org.thingsboard.server.common.data.rule.RuleChain; +import org.thingsboard.server.common.data.rule.RuleChainConnectionInfo; +import org.thingsboard.server.common.data.rule.RuleChainMetaData; +import org.thingsboard.server.common.data.rule.RuleChainType; +import org.thingsboard.server.common.data.rule.RuleNode; +import org.thingsboard.server.gen.edge.v1.RuleChainMetadataUpdateMsg; +import org.thingsboard.server.gen.edge.v1.RuleChainUpdateMsg; +import org.thingsboard.server.gen.edge.v1.RuleNodeProto; +import org.thingsboard.server.service.edge.rpc.processor.BaseEdgeProcessor; + +import java.io.IOException; +import java.util.ArrayList; +import java.util.List; +import java.util.UUID; +import java.util.function.Function; + +@Slf4j +public class BaseRuleChainProcessor extends BaseEdgeProcessor { + + protected boolean saveOrUpdateRuleChain(TenantId tenantId, RuleChainId ruleChainId, RuleChainUpdateMsg ruleChainUpdateMsg) { + boolean created = false; + RuleChain ruleChain = ruleChainService.findRuleChainById(tenantId, ruleChainId); + if (ruleChain == null) { + created = true; + ruleChain = new RuleChain(); + ruleChain.setTenantId(tenantId); + ruleChain.setCreatedTime(Uuids.unixTimestamp(ruleChainId.getId())); + } + ruleChain.setName(ruleChainUpdateMsg.getName()); + ruleChain.setType(RuleChainType.EDGE); + ruleChain.setDebugMode(ruleChainUpdateMsg.getDebugMode()); + ruleChain.setConfiguration(JacksonUtil.toJsonNode(ruleChainUpdateMsg.getConfiguration())); + + UUID firstRuleNodeUUID = safeGetUUID(ruleChainUpdateMsg.getFirstRuleNodeIdMSB(), ruleChainUpdateMsg.getFirstRuleNodeIdLSB()); + ruleChain.setFirstRuleNodeId(firstRuleNodeUUID != null ? new RuleNodeId(firstRuleNodeUUID) : null); + + ruleChainValidator.validate(ruleChain, RuleChain::getTenantId); + if (created) { + ruleChain.setId(ruleChainId); + } + ruleChainService.saveRuleChain(ruleChain, false); + return created; + } + + protected boolean saveOrUpdateRuleChainMetadata(TenantId tenantId, RuleChainId ruleChainId, RuleChainMetadataUpdateMsg ruleChainMetadataUpdateMsg) throws IOException { + RuleChainMetaData ruleChainMetadata = new RuleChainMetaData(); + ruleChainMetadata.setRuleChainId(ruleChainId); + ruleChainMetadata.setNodes(parseNodeProtos(ruleChainId, ruleChainMetadataUpdateMsg.getNodesList())); + ruleChainMetadata.setConnections(parseConnectionProtos(ruleChainMetadataUpdateMsg.getConnectionsList())); + ruleChainMetadata.setRuleChainConnections(parseRuleChainConnectionProtos(ruleChainMetadataUpdateMsg.getRuleChainConnectionsList())); + if (ruleChainMetadataUpdateMsg.getFirstNodeIndex() != -1) { + ruleChainMetadata.setFirstNodeIndex(ruleChainMetadataUpdateMsg.getFirstNodeIndex()); + } + if (ruleChainMetadata.getNodes().size() > 0) { + ruleChainService.saveRuleChainMetaData(tenantId, ruleChainMetadata, Function.identity()); + return true; + } + return false; + } + + private List parseNodeProtos(RuleChainId ruleChainId, List nodesList) throws IOException { + List result = new ArrayList<>(); + for (RuleNodeProto proto : nodesList) { + RuleNode ruleNode = new RuleNode(); + RuleNodeId ruleNodeId = new RuleNodeId(new UUID(proto.getIdMSB(), proto.getIdLSB())); + ruleNode.setId(ruleNodeId); + ruleNode.setCreatedTime(Uuids.unixTimestamp(ruleNodeId.getId())); + ruleNode.setRuleChainId(ruleChainId); + ruleNode.setType(proto.getType()); + ruleNode.setName(proto.getName()); + ruleNode.setDebugMode(proto.getDebugMode()); + ruleNode.setConfiguration(JacksonUtil.OBJECT_MAPPER.readTree(proto.getConfiguration())); + ruleNode.setAdditionalInfo(JacksonUtil.OBJECT_MAPPER.readTree(proto.getAdditionalInfo())); + result.add(ruleNode); + } + return result; + } + + private List parseConnectionProtos(List connectionsList) { + List result = new ArrayList<>(); + for (org.thingsboard.server.gen.edge.v1.NodeConnectionInfoProto proto : connectionsList) { + NodeConnectionInfo info = new NodeConnectionInfo(); + info.setFromIndex(proto.getFromIndex()); + info.setToIndex(proto.getToIndex()); + info.setType(proto.getType()); + result.add(info); + } + return result; + } + + private List parseRuleChainConnectionProtos(List ruleChainConnectionsList) throws IOException { + List result = new ArrayList<>(); + for (org.thingsboard.server.gen.edge.v1.RuleChainConnectionInfoProto proto : ruleChainConnectionsList) { + RuleChainConnectionInfo info = new RuleChainConnectionInfo(); + info.setFromIndex(proto.getFromIndex()); + info.setTargetRuleChainId(new RuleChainId(new UUID(proto.getTargetRuleChainIdMSB(), proto.getTargetRuleChainIdLSB()))); + info.setType(proto.getType()); + info.setAdditionalInfo(JacksonUtil.OBJECT_MAPPER.readTree(proto.getAdditionalInfo())); + result.add(info); + } + return result; + } +} 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 ba6fc1ef97..ae5e158ba7 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 @@ -15,29 +15,120 @@ */ package org.thingsboard.server.service.edge.rpc.processor.rule; +import com.google.common.util.concurrent.Futures; +import com.google.common.util.concurrent.ListenableFuture; 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.edge.Edge; import org.thingsboard.server.common.data.edge.EdgeEvent; import org.thingsboard.server.common.data.id.RuleChainId; +import org.thingsboard.server.common.data.id.TenantId; +import org.thingsboard.server.common.data.msg.TbMsgType; import org.thingsboard.server.common.data.rule.RuleChain; import org.thingsboard.server.common.data.rule.RuleChainMetaData; +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; import org.thingsboard.server.queue.util.TbCoreComponent; -import org.thingsboard.server.service.edge.rpc.processor.BaseEdgeProcessor; + +import java.util.UUID; import static org.thingsboard.server.service.edge.DefaultEdgeNotificationService.EDGE_IS_ROOT_BODY_KEY; @Component @Slf4j @TbCoreComponent -public class RuleChainEdgeProcessor extends BaseEdgeProcessor { +public class RuleChainEdgeProcessor extends BaseRuleChainProcessor { + + public ListenableFuture processRuleChainMsgFromEdge(TenantId tenantId, Edge edge, RuleChainUpdateMsg ruleChainUpdateMsg) { + log.trace("[{}] executing processRuleChainMsgFromEdge [{}] from edge [{}]", tenantId, ruleChainUpdateMsg, edge.getName()); + RuleChainId ruleChainId = new RuleChainId(new UUID(ruleChainUpdateMsg.getIdMSB(), ruleChainUpdateMsg.getIdLSB())); + try { + edgeSynchronizationManager.getSync().set(true); + + switch (ruleChainUpdateMsg.getMsgType()) { + case ENTITY_CREATED_RPC_MESSAGE: + case ENTITY_UPDATED_RPC_MESSAGE: + saveOrUpdateRuleChain(tenantId, ruleChainId, ruleChainUpdateMsg, edge); + return Futures.immediateFuture(null); + case ENTITY_DELETED_RPC_MESSAGE: + RuleChain ruleChainToDelete = ruleChainService.findRuleChainById(tenantId, ruleChainId); + if (ruleChainToDelete != null) { + ruleChainService.unassignRuleChainFromEdge(tenantId, ruleChainId, edge.getId(), false); + } + return Futures.immediateFuture(null); + case UNRECOGNIZED: + default: + return handleUnsupportedMsgType(ruleChainUpdateMsg.getMsgType()); + } + } catch (DataValidationException e) { + if (e.getMessage().contains("limit reached")) { + log.warn("[{}] Number of allowed rule chains violated {}", tenantId, ruleChainUpdateMsg, e); + return Futures.immediateFuture(null); + } else { + return Futures.immediateFailedFuture(e); + } + } finally { + edgeSynchronizationManager.getSync().remove(); + } + } + + + public ListenableFuture processRuleChainMetadataMsgFromEdge(TenantId tenantId, Edge edge, RuleChainMetadataUpdateMsg ruleChainMetadataUpdateMsg) { + log.trace("[{}] executing processRuleChainMetadataMsgFromEdge [{}] from edge [{}]", tenantId, ruleChainMetadataUpdateMsg, edge.getName()); + RuleChainId ruleChainId = new RuleChainId(new UUID(ruleChainMetadataUpdateMsg.getRuleChainIdMSB(), ruleChainMetadataUpdateMsg.getRuleChainIdLSB())); + try { + edgeSynchronizationManager.getSync().set(true); + + switch (ruleChainMetadataUpdateMsg.getMsgType()) { + case ENTITY_CREATED_RPC_MESSAGE: + case ENTITY_UPDATED_RPC_MESSAGE: + saveOrUpdateRuleChainMetadata(tenantId, ruleChainId, ruleChainMetadataUpdateMsg); + return Futures.immediateFuture(null); + case UNRECOGNIZED: + default: + return handleUnsupportedMsgType(ruleChainMetadataUpdateMsg.getMsgType()); + } + } catch (Exception e) { + String errMsg = String.format("Can't process rule chain metadata update msg %s", ruleChainMetadataUpdateMsg); + log.error(errMsg, e); + return Futures.immediateFailedFuture(new RuntimeException(errMsg, e)); + } finally { + edgeSynchronizationManager.getSync().remove(); + } + } + + private void saveOrUpdateRuleChain(TenantId tenantId, RuleChainId ruleChainId, RuleChainUpdateMsg ruleChainUpdateMsg, Edge edge) { + boolean created = super.saveOrUpdateRuleChain(tenantId, ruleChainId, ruleChainUpdateMsg); + if (created) { + createRelationFromEdge(tenantId, edge.getId(), ruleChainId); + pushRuleChainCreatedEventToRuleEngine(tenantId, edge, ruleChainId); + ruleChainService.assignRuleChainToEdge(tenantId, ruleChainId, edge.getId()); + } + if (ruleChainUpdateMsg.getRoot()) { + edge.setRootRuleChainId(ruleChainId); + edgeService.saveEdge(edge); + } + } + + private void pushRuleChainCreatedEventToRuleEngine(TenantId tenantId, Edge edge, RuleChainId ruleChainId) { + try { + RuleChain ruleChain = ruleChainService.findRuleChainById(tenantId, ruleChainId); + String ruleChainAsString = JacksonUtil.toString(ruleChain); + TbMsgMetaData msgMetaData = getEdgeActionTbMsgMetaData(edge, null); + pushEntityEventToRuleEngine(tenantId, ruleChainId, null, TbMsgType.ENTITY_CREATED, ruleChainAsString, msgMetaData); + } catch (Exception e) { + log.warn("[{}][{}] Failed to push rule chain action to rule engine: {}", tenantId, ruleChainId, TbMsgType.ENTITY_CREATED.name(), e); + } + } - public DownlinkMsg convertRuleChainEventToDownlink(EdgeEvent edgeEvent) { + public DownlinkMsg convertRuleChainEventToDownlink(EdgeEvent edgeEvent, EdgeVersion edgeVersion) { RuleChainId ruleChainId = new RuleChainId(edgeEvent.getEntityId()); DownlinkMsg downlinkMsg = null; switch (edgeEvent.getAction()) { @@ -55,10 +146,13 @@ public class RuleChainEdgeProcessor extends BaseEdgeProcessor { UpdateMsgType msgType = getUpdateMsgType(edgeEvent.getAction()); RuleChainUpdateMsg ruleChainUpdateMsg = ruleChainMsgConstructor.constructRuleChainUpdatedMsg(msgType, ruleChain, isRoot); + RuleChainMetaData ruleChainMetaData = ruleChainService.loadRuleChainMetaData(edgeEvent.getTenantId(), ruleChainId); + RuleChainMetadataUpdateMsg ruleChainMetadataUpdateMsg = + ruleChainMsgConstructor.constructRuleChainMetadataUpdatedMsg(edgeEvent.getTenantId(), msgType, ruleChainMetaData, edgeVersion); downlinkMsg = DownlinkMsg.newBuilder() .setDownlinkMsgId(EdgeUtils.nextPositiveInt()) .addRuleChainUpdateMsg(ruleChainUpdateMsg) - .build(); + .addRuleChainMetadataUpdateMsg(ruleChainMetadataUpdateMsg).build(); } break; case DELETED: diff --git a/application/src/test/java/org/thingsboard/server/edge/AbstractEdgeTest.java b/application/src/test/java/org/thingsboard/server/edge/AbstractEdgeTest.java index 410348762f..dc1015ef4a 100644 --- a/application/src/test/java/org/thingsboard/server/edge/AbstractEdgeTest.java +++ b/application/src/test/java/org/thingsboard/server/edge/AbstractEdgeTest.java @@ -81,14 +81,12 @@ import org.thingsboard.server.gen.edge.v1.DeviceProfileUpdateMsg; import org.thingsboard.server.gen.edge.v1.DeviceUpdateMsg; import org.thingsboard.server.gen.edge.v1.EdgeConfiguration; import org.thingsboard.server.gen.edge.v1.QueueUpdateMsg; -import org.thingsboard.server.gen.edge.v1.RuleChainMetadataRequestMsg; import org.thingsboard.server.gen.edge.v1.RuleChainMetadataUpdateMsg; import org.thingsboard.server.gen.edge.v1.RuleChainUpdateMsg; import org.thingsboard.server.gen.edge.v1.SyncCompletedMsg; import org.thingsboard.server.gen.edge.v1.TenantProfileUpdateMsg; import org.thingsboard.server.gen.edge.v1.TenantUpdateMsg; import org.thingsboard.server.gen.edge.v1.UpdateMsgType; -import org.thingsboard.server.gen.edge.v1.UplinkMsg; import org.thingsboard.server.gen.edge.v1.UserUpdateMsg; import org.thingsboard.server.queue.util.DataDecodingEncodingService; @@ -129,36 +127,12 @@ abstract public class AbstractEdgeTest extends AbstractControllerTest { installation(); edgeImitator = new EdgeImitator("localhost", 7070, edge.getRoutingKey(), edge.getSecret()); - edgeImitator.expectMessageAmount(26); + edgeImitator.expectMessageAmount(27); edgeImitator.connect(); - requestEdgeRuleChainMetadata(); - verifyEdgeConnectionAndInitialData(); } - private void requestEdgeRuleChainMetadata() throws Exception { - RuleChainId rootRuleChainId = getEdgeRootRuleChainId(); - RuleChainMetadataRequestMsg.Builder builder = RuleChainMetadataRequestMsg.newBuilder() - .setRuleChainIdMSB(rootRuleChainId.getId().getMostSignificantBits()) - .setRuleChainIdLSB(rootRuleChainId.getId().getLeastSignificantBits()); - testAutoGeneratedCodeByProtobuf(builder); - UplinkMsg.Builder uplinkMsgBuilder = UplinkMsg.newBuilder() - .addRuleChainMetadataRequestMsg(builder.build()); - edgeImitator.sendUplinkMsg(uplinkMsgBuilder.build()); - } - - private RuleChainId getEdgeRootRuleChainId() throws Exception { - List edgeRuleChains = doGetTypedWithPageLink("/api/edge/" + edge.getUuidId() + "/ruleChains?", - new TypeReference>() {}, new PageLink(100)).getData(); - for (RuleChain edgeRuleChain : edgeRuleChains) { - if (edgeRuleChain.isRoot()) { - return edgeRuleChain.getId(); - } - } - throw new RuntimeException("Root rule chain not found"); - } - @After public void teardownEdgeTest() { try { @@ -230,7 +204,7 @@ abstract public class AbstractEdgeTest extends AbstractControllerTest { // 2 messages - 1 from rule chain fetcher and 1 from rule chain controller UUID ruleChainUUID = validateRuleChains(); - // 1 from request message + // 2 messages - 1 from rule chain fetcher and 1 from rule chain controller (it goes along with RuleChainUpdateMsg) validateRuleChainMetadataUpdates(ruleChainUUID); // 4 messages - 4 messages from fetcher - 2 from system level ('mail', 'mailTemplates') and 2 from admin level ('mail', 'mailTemplates') @@ -385,11 +359,17 @@ abstract public class AbstractEdgeTest extends AbstractControllerTest { } private void validateRuleChainMetadataUpdates(UUID expectedRuleChainUUID) { - Optional ruleChainMetadataUpdateOpt = edgeImitator.findMessageByType(RuleChainMetadataUpdateMsg.class); - Assert.assertTrue(ruleChainMetadataUpdateOpt.isPresent()); - RuleChainMetadataUpdateMsg ruleChainMetadataUpdateMsg = ruleChainMetadataUpdateOpt.get(); - Assert.assertEquals(UpdateMsgType.ENTITY_CREATED_RPC_MESSAGE, ruleChainMetadataUpdateMsg.getMsgType()); - UUID ruleChainUUID = new UUID(ruleChainMetadataUpdateMsg.getRuleChainIdMSB(), ruleChainMetadataUpdateMsg.getRuleChainIdLSB()); + List ruleChainMetadataUpdateMsgList = edgeImitator.findAllMessagesByType(RuleChainMetadataUpdateMsg.class); + Assert.assertEquals(2, ruleChainMetadataUpdateMsgList.size()); + // metadata create msg + RuleChainMetadataUpdateMsg ruleChainMetadataUpdateMsgCreated = ruleChainMetadataUpdateMsgList.get(0); + Assert.assertEquals(UpdateMsgType.ENTITY_CREATED_RPC_MESSAGE, ruleChainMetadataUpdateMsgCreated.getMsgType()); + UUID ruleChainUUID = new UUID(ruleChainMetadataUpdateMsgCreated.getRuleChainIdMSB(), ruleChainMetadataUpdateMsgCreated.getRuleChainIdLSB()); + Assert.assertEquals(expectedRuleChainUUID, ruleChainUUID); + // metadata update msg + RuleChainMetadataUpdateMsg ruleChainMetadataUpdateMsgUpdated = ruleChainMetadataUpdateMsgList.get(1); + Assert.assertEquals(UpdateMsgType.ENTITY_UPDATED_RPC_MESSAGE, ruleChainMetadataUpdateMsgUpdated.getMsgType()); + ruleChainUUID = new UUID(ruleChainMetadataUpdateMsgUpdated.getRuleChainIdMSB(), ruleChainMetadataUpdateMsgUpdated.getRuleChainIdLSB()); Assert.assertEquals(expectedRuleChainUUID, ruleChainUUID); } diff --git a/application/src/test/java/org/thingsboard/server/edge/AssetEdgeTest.java b/application/src/test/java/org/thingsboard/server/edge/AssetEdgeTest.java index faf0896b81..e3ad8aaa25 100644 --- a/application/src/test/java/org/thingsboard/server/edge/AssetEdgeTest.java +++ b/application/src/test/java/org/thingsboard/server/edge/AssetEdgeTest.java @@ -168,6 +168,7 @@ public class AssetEdgeTest extends AbstractEdgeTest { public void testSendAssetToCloud() throws Exception { UUID uuid = Uuids.timeBased(); + // created asset on edge UplinkMsg.Builder uplinkMsgBuilder = UplinkMsg.newBuilder(); AssetUpdateMsg.Builder assetUpdateMsgBuilder = AssetUpdateMsg.newBuilder(); assetUpdateMsgBuilder.setIdMSB(uuid.getMostSignificantBits()); @@ -191,6 +192,31 @@ public class AssetEdgeTest extends AbstractEdgeTest { Asset asset = doGet("/api/asset/" + uuid, Asset.class); Assert.assertNotNull(asset); Assert.assertEquals("Asset Edge 2", asset.getName()); + + // updated asset on edge + uplinkMsgBuilder = UplinkMsg.newBuilder(); + assetUpdateMsgBuilder = AssetUpdateMsg.newBuilder(); + assetUpdateMsgBuilder.setIdMSB(uuid.getMostSignificantBits()); + assetUpdateMsgBuilder.setIdLSB(uuid.getLeastSignificantBits()); + assetUpdateMsgBuilder.setName("Asset Edge 2 Updated"); + assetUpdateMsgBuilder.setType("test"); + assetUpdateMsgBuilder.setMsgType(UpdateMsgType.ENTITY_UPDATED_RPC_MESSAGE); + testAutoGeneratedCodeByProtobuf(assetUpdateMsgBuilder); + uplinkMsgBuilder.addAssetUpdateMsg(assetUpdateMsgBuilder.build()); + + testAutoGeneratedCodeByProtobuf(uplinkMsgBuilder); + + edgeImitator.expectResponsesAmount(1); + edgeImitator.sendUplinkMsg(uplinkMsgBuilder.build()); + + Assert.assertTrue(edgeImitator.waitForResponses()); + + latestResponseMsg = edgeImitator.getLatestResponseMsg(); + Assert.assertTrue(latestResponseMsg.getSuccess()); + + asset = doGet("/api/asset/" + uuid, Asset.class); + Assert.assertNotNull(asset); + Assert.assertEquals("Asset Edge 2 Updated", asset.getName()); } @Test diff --git a/application/src/test/java/org/thingsboard/server/edge/DashboardEdgeTest.java b/application/src/test/java/org/thingsboard/server/edge/DashboardEdgeTest.java index 2d21d30174..7ef3da1fa0 100644 --- a/application/src/test/java/org/thingsboard/server/edge/DashboardEdgeTest.java +++ b/application/src/test/java/org/thingsboard/server/edge/DashboardEdgeTest.java @@ -171,6 +171,7 @@ public class DashboardEdgeTest extends AbstractEdgeTest { public void testSendDashboardToCloud() throws Exception { UUID uuid = Uuids.timeBased(); + // create dashboard on edge UplinkMsg.Builder uplinkMsgBuilder = UplinkMsg.newBuilder(); DashboardUpdateMsg.Builder dashboardUpdateMsgBuilder = DashboardUpdateMsg.newBuilder(); dashboardUpdateMsgBuilder.setIdMSB(uuid.getMostSignificantBits()); @@ -191,6 +192,28 @@ public class DashboardEdgeTest extends AbstractEdgeTest { Dashboard dashboard = doGet("/api/dashboard/" + uuid, Dashboard.class); Assert.assertNotNull(dashboard); Assert.assertEquals("Edge Test Dashboard", dashboard.getName()); + + // update dashboard on edge + uplinkMsgBuilder = UplinkMsg.newBuilder(); + dashboardUpdateMsgBuilder = DashboardUpdateMsg.newBuilder(); + dashboardUpdateMsgBuilder.setIdMSB(uuid.getMostSignificantBits()); + dashboardUpdateMsgBuilder.setIdLSB(uuid.getLeastSignificantBits()); + dashboardUpdateMsgBuilder.setTitle("Edge Test Dashboard Updated"); + dashboardUpdateMsgBuilder.setConfiguration(""); + dashboardUpdateMsgBuilder.setMsgType(UpdateMsgType.ENTITY_UPDATED_RPC_MESSAGE); + testAutoGeneratedCodeByProtobuf(dashboardUpdateMsgBuilder); + uplinkMsgBuilder.addDashboardUpdateMsg(dashboardUpdateMsgBuilder.build()); + + testAutoGeneratedCodeByProtobuf(uplinkMsgBuilder); + + edgeImitator.expectResponsesAmount(1); + edgeImitator.sendUplinkMsg(uplinkMsgBuilder.build()); + + Assert.assertTrue(edgeImitator.waitForResponses()); + + dashboard = doGet("/api/dashboard/" + uuid, Dashboard.class); + Assert.assertNotNull(dashboard); + Assert.assertEquals("Edge Test Dashboard Updated", dashboard.getName()); } @Test diff --git a/application/src/test/java/org/thingsboard/server/edge/DeviceEdgeTest.java b/application/src/test/java/org/thingsboard/server/edge/DeviceEdgeTest.java index 15b15c0be0..42f8d1d272 100644 --- a/application/src/test/java/org/thingsboard/server/edge/DeviceEdgeTest.java +++ b/application/src/test/java/org/thingsboard/server/edge/DeviceEdgeTest.java @@ -538,6 +538,7 @@ public class DeviceEdgeTest extends AbstractEdgeTest { public void testSendDeviceToCloud() throws Exception { UUID uuid = Uuids.timeBased(); + // create device on edge UplinkMsg.Builder uplinkMsgBuilder = UplinkMsg.newBuilder(); DeviceUpdateMsg.Builder deviceUpdateMsgBuilder = DeviceUpdateMsg.newBuilder(); deviceUpdateMsgBuilder.setIdMSB(uuid.getMostSignificantBits()); @@ -566,6 +567,36 @@ public class DeviceEdgeTest extends AbstractEdgeTest { Device device = doGet("/api/device/" + newDeviceId, Device.class); Assert.assertNotNull(device); Assert.assertEquals("Edge Device 2", device.getName()); + + // update device on edge + uplinkMsgBuilder = UplinkMsg.newBuilder(); + deviceUpdateMsgBuilder = DeviceUpdateMsg.newBuilder(); + deviceUpdateMsgBuilder.setIdMSB(uuid.getMostSignificantBits()); + deviceUpdateMsgBuilder.setIdLSB(uuid.getLeastSignificantBits()); + deviceUpdateMsgBuilder.setName("Edge Device 2 Updated"); + deviceUpdateMsgBuilder.setType("test"); + deviceUpdateMsgBuilder.setMsgType(UpdateMsgType.ENTITY_CREATED_RPC_MESSAGE); + uplinkMsgBuilder.addDeviceUpdateMsg(deviceUpdateMsgBuilder.build()); + + edgeImitator.expectResponsesAmount(1); + edgeImitator.expectMessageAmount(1); + + edgeImitator.sendUplinkMsg(uplinkMsgBuilder.build()); + + Assert.assertTrue(edgeImitator.waitForResponses()); + Assert.assertTrue(edgeImitator.waitForMessages()); + + latestMessage = edgeImitator.getLatestMessage(); + Assert.assertTrue(latestMessage instanceof DeviceCredentialsRequestMsg); + latestDeviceCredentialsRequestMsg = (DeviceCredentialsRequestMsg) latestMessage; + Assert.assertEquals(uuid.getMostSignificantBits(), latestDeviceCredentialsRequestMsg.getDeviceIdMSB()); + Assert.assertEquals(uuid.getLeastSignificantBits(), latestDeviceCredentialsRequestMsg.getDeviceIdLSB()); + + newDeviceId = new UUID(latestDeviceCredentialsRequestMsg.getDeviceIdMSB(), latestDeviceCredentialsRequestMsg.getDeviceIdLSB()); + + device = doGet("/api/device/" + newDeviceId, Device.class); + Assert.assertNotNull(device); + Assert.assertEquals("Edge Device 2 Updated", device.getName()); } @Test diff --git a/application/src/test/java/org/thingsboard/server/edge/RuleChainEdgeTest.java b/application/src/test/java/org/thingsboard/server/edge/RuleChainEdgeTest.java index 83b2370cf0..4be82e7d8b 100644 --- a/application/src/test/java/org/thingsboard/server/edge/RuleChainEdgeTest.java +++ b/application/src/test/java/org/thingsboard/server/edge/RuleChainEdgeTest.java @@ -15,7 +15,7 @@ */ package org.thingsboard.server.edge; -import com.google.protobuf.AbstractMessage; +import com.datastax.oss.driver.api.core.uuid.Uuids; import org.junit.Assert; import org.junit.Test; import org.thingsboard.common.util.JacksonUtil; @@ -29,11 +29,10 @@ import org.thingsboard.server.common.data.rule.RuleChainMetaData; import org.thingsboard.server.common.data.rule.RuleChainType; import org.thingsboard.server.common.data.rule.RuleNode; import org.thingsboard.server.dao.service.DaoSqlTest; -import org.thingsboard.server.gen.edge.v1.RuleChainMetadataRequestMsg; -import org.thingsboard.server.gen.edge.v1.RuleChainMetadataUpdateMsg; import org.thingsboard.server.gen.edge.v1.RuleChainUpdateMsg; import org.thingsboard.server.gen.edge.v1.UpdateMsgType; import org.thingsboard.server.gen.edge.v1.UplinkMsg; +import org.thingsboard.server.gen.edge.v1.UplinkResponseMsg; import java.util.ArrayList; import java.util.Collections; @@ -49,7 +48,7 @@ public class RuleChainEdgeTest extends AbstractEdgeTest { @Test public void testRuleChains() throws Exception { // create rule chain - edgeImitator.expectMessageAmount(2); + edgeImitator.expectMessageAmount(4); RuleChain ruleChain = new RuleChain(); ruleChain.setName("Edge Test Rule Chain"); ruleChain.setType(RuleChainType.EDGE); @@ -67,8 +66,6 @@ public class RuleChainEdgeTest extends AbstractEdgeTest { Assert.assertEquals(ruleChainUpdateMsg.getIdLSB(), savedRuleChain.getUuidId().getLeastSignificantBits()); Assert.assertEquals(ruleChainUpdateMsg.getName(), savedRuleChain.getName()); - testRuleChainMetadataRequestMsg(savedRuleChain.getId()); - // unassign rule chain from edge edgeImitator.expectMessageAmount(1); doDelete("/api/edge/" + edge.getUuidId() @@ -89,55 +86,56 @@ public class RuleChainEdgeTest extends AbstractEdgeTest { } @Test - public void testSendRuleChainMetadataRequestToCloud() throws Exception { - RuleChainId edgeRootRuleChainId = edge.getRootRuleChainId(); + public void testRuleChainToCloud() throws Exception { + UUID uuid = Uuids.timeBased(); + // create rule chain on edge UplinkMsg.Builder uplinkMsgBuilder = UplinkMsg.newBuilder(); - RuleChainMetadataRequestMsg.Builder ruleChainMetadataRequestMsgBuilder = RuleChainMetadataRequestMsg.newBuilder(); - ruleChainMetadataRequestMsgBuilder.setRuleChainIdMSB(edgeRootRuleChainId.getId().getMostSignificantBits()); - ruleChainMetadataRequestMsgBuilder.setRuleChainIdLSB(edgeRootRuleChainId.getId().getLeastSignificantBits()); - testAutoGeneratedCodeByProtobuf(ruleChainMetadataRequestMsgBuilder); - uplinkMsgBuilder.addRuleChainMetadataRequestMsg(ruleChainMetadataRequestMsgBuilder.build()); + RuleChainUpdateMsg.Builder ruleChainUpdateMsgBuilder = RuleChainUpdateMsg.newBuilder(); + ruleChainUpdateMsgBuilder.setIdMSB(uuid.getMostSignificantBits()); + ruleChainUpdateMsgBuilder.setIdLSB(uuid.getLeastSignificantBits()); + ruleChainUpdateMsgBuilder.setName("Rule Chain Edge"); + ruleChainUpdateMsgBuilder.setMsgType(UpdateMsgType.ENTITY_CREATED_RPC_MESSAGE); + testAutoGeneratedCodeByProtobuf(ruleChainUpdateMsgBuilder); + uplinkMsgBuilder.addRuleChainUpdateMsg(ruleChainUpdateMsgBuilder.build()); testAutoGeneratedCodeByProtobuf(uplinkMsgBuilder); edgeImitator.expectResponsesAmount(1); - edgeImitator.expectMessageAmount(1); edgeImitator.sendUplinkMsg(uplinkMsgBuilder.build()); + Assert.assertTrue(edgeImitator.waitForResponses()); - Assert.assertTrue(edgeImitator.waitForMessages()); - AbstractMessage latestMessage = edgeImitator.getLatestMessage(); - Assert.assertTrue(latestMessage instanceof RuleChainMetadataUpdateMsg); - RuleChainMetadataUpdateMsg ruleChainMetadataUpdateMsg = (RuleChainMetadataUpdateMsg) latestMessage; - Assert.assertEquals(ruleChainMetadataUpdateMsg.getRuleChainIdMSB(), edgeRootRuleChainId.getId().getMostSignificantBits()); - Assert.assertEquals(ruleChainMetadataUpdateMsg.getRuleChainIdLSB(), edgeRootRuleChainId.getId().getLeastSignificantBits()); + UplinkResponseMsg latestResponseMsg = edgeImitator.getLatestResponseMsg(); + Assert.assertTrue(latestResponseMsg.getSuccess()); - testAutoGeneratedCodeByProtobuf(ruleChainMetadataUpdateMsg); - } + RuleChain ruleChain = doGet("/api/ruleChain/" + uuid, RuleChain.class); + Assert.assertNotNull(ruleChain); + Assert.assertEquals("Rule Chain Edge", ruleChain.getName()); - private void testRuleChainMetadataRequestMsg(RuleChainId ruleChainId) throws Exception { - RuleChainMetadataRequestMsg.Builder ruleChainMetadataRequestMsgBuilder = RuleChainMetadataRequestMsg.newBuilder() - .setRuleChainIdMSB(ruleChainId.getId().getMostSignificantBits()) - .setRuleChainIdLSB(ruleChainId.getId().getLeastSignificantBits()); - testAutoGeneratedCodeByProtobuf(ruleChainMetadataRequestMsgBuilder); + // update rule chain on edge + uplinkMsgBuilder = UplinkMsg.newBuilder(); + ruleChainUpdateMsgBuilder = RuleChainUpdateMsg.newBuilder(); + ruleChainUpdateMsgBuilder.setIdMSB(uuid.getMostSignificantBits()); + ruleChainUpdateMsgBuilder.setIdLSB(uuid.getLeastSignificantBits()); + ruleChainUpdateMsgBuilder.setName("Rule Chain Edge Updated"); + ruleChainUpdateMsgBuilder.setMsgType(UpdateMsgType.ENTITY_UPDATED_RPC_MESSAGE); + testAutoGeneratedCodeByProtobuf(ruleChainUpdateMsgBuilder); + uplinkMsgBuilder.addRuleChainUpdateMsg(ruleChainUpdateMsgBuilder.build()); - UplinkMsg.Builder uplinkMsgBuilder = UplinkMsg.newBuilder() - .addRuleChainMetadataRequestMsg(ruleChainMetadataRequestMsgBuilder.build()); testAutoGeneratedCodeByProtobuf(uplinkMsgBuilder); edgeImitator.expectResponsesAmount(1); - edgeImitator.expectMessageAmount(1); edgeImitator.sendUplinkMsg(uplinkMsgBuilder.build()); + Assert.assertTrue(edgeImitator.waitForResponses()); - Assert.assertTrue(edgeImitator.waitForMessages()); - AbstractMessage latestMessage = edgeImitator.getLatestMessage(); - Assert.assertTrue(latestMessage instanceof RuleChainMetadataUpdateMsg); - RuleChainMetadataUpdateMsg ruleChainMetadataUpdateMsg = (RuleChainMetadataUpdateMsg) latestMessage; - RuleChainId receivedRuleChainId = - new RuleChainId(new UUID(ruleChainMetadataUpdateMsg.getRuleChainIdMSB(), ruleChainMetadataUpdateMsg.getRuleChainIdLSB())); - Assert.assertEquals(ruleChainId, receivedRuleChainId); + latestResponseMsg = edgeImitator.getLatestResponseMsg(); + Assert.assertTrue(latestResponseMsg.getSuccess()); + + ruleChain = doGet("/api/ruleChain/" + uuid, RuleChain.class); + Assert.assertNotNull(ruleChain); + Assert.assertEquals("Rule Chain Edge Updated", ruleChain.getName()); } private void createRuleChainMetadata(RuleChain ruleChain) { diff --git a/common/dao-api/src/main/java/org/thingsboard/server/dao/rule/RuleChainService.java b/common/dao-api/src/main/java/org/thingsboard/server/dao/rule/RuleChainService.java index 95cdc0e02d..94e0979ef5 100644 --- a/common/dao-api/src/main/java/org/thingsboard/server/dao/rule/RuleChainService.java +++ b/common/dao-api/src/main/java/org/thingsboard/server/dao/rule/RuleChainService.java @@ -44,6 +44,8 @@ public interface RuleChainService extends EntityDaoService { RuleChain saveRuleChain(RuleChain ruleChain); + RuleChain saveRuleChain(RuleChain ruleChain, boolean doValidate); + boolean setRootRuleChain(TenantId tenantId, RuleChainId ruleChainId); RuleChainUpdateResult saveRuleChainMetaData(TenantId tenantId, RuleChainMetaData ruleChainMetaData, Function ruleNodeUpdater); diff --git a/common/edge-api/src/main/proto/edge.proto b/common/edge-api/src/main/proto/edge.proto index d9da289001..f7d5b5d771 100644 --- a/common/edge-api/src/main/proto/edge.proto +++ b/common/edge-api/src/main/proto/edge.proto @@ -420,6 +420,7 @@ message TenantProfileUpdateMsg { bytes profileDataBytes = 8; } +// deprecated message RuleChainMetadataRequestMsg { int64 ruleChainIdMSB = 1; int64 ruleChainIdLSB = 2; @@ -557,7 +558,7 @@ message UplinkMsg { repeated DeviceCredentialsUpdateMsg deviceCredentialsUpdateMsg = 4; repeated AlarmUpdateMsg alarmUpdateMsg = 5; repeated RelationUpdateMsg relationUpdateMsg = 6; - repeated RuleChainMetadataRequestMsg ruleChainMetadataRequestMsg = 7; + repeated RuleChainMetadataRequestMsg ruleChainMetadataRequestMsg = 7; // deprecated repeated AttributesRequestMsg attributesRequestMsg = 8; repeated RelationRequestMsg relationRequestMsg = 9; repeated UserCredentialsRequestMsg userCredentialsRequestMsg = 10; @@ -571,6 +572,8 @@ message UplinkMsg { repeated EntityViewUpdateMsg entityViewUpdateMsg = 18; repeated AssetProfileUpdateMsg assetProfileUpdateMsg = 19; repeated DeviceProfileUpdateMsg deviceProfileUpdateMsg = 20; + repeated RuleChainUpdateMsg ruleChainUpdateMsg = 21; + repeated RuleChainMetadataUpdateMsg ruleChainMetadataUpdateMsg = 22; } message UplinkResponseMsg { diff --git a/dao/src/main/java/org/thingsboard/server/dao/rule/BaseRuleChainService.java b/dao/src/main/java/org/thingsboard/server/dao/rule/BaseRuleChainService.java index 5e6b1e8009..387fc00c6c 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/rule/BaseRuleChainService.java +++ b/dao/src/main/java/org/thingsboard/server/dao/rule/BaseRuleChainService.java @@ -108,7 +108,20 @@ public class BaseRuleChainService extends AbstractEntityService implements RuleC @Override @Transactional public RuleChain saveRuleChain(RuleChain ruleChain) { - ruleChainValidator.validate(ruleChain, RuleChain::getTenantId); + return doSaveRuleChain(ruleChain, true); + } + + @Override + @Transactional + public RuleChain saveRuleChain(RuleChain ruleChain, boolean doValidate) { + return doSaveRuleChain(ruleChain, doValidate); + } + + private RuleChain doSaveRuleChain(RuleChain ruleChain, boolean doValidate) { + log.trace("Executing doSaveRuleChain [{}]", ruleChain); + if (doValidate) { + ruleChainValidator.validate(ruleChain, RuleChain::getTenantId); + } try { RuleChain savedRuleChain = ruleChainDao.save(ruleChain.getTenantId(), ruleChain); if (ruleChain.getId() == null) { From 2ef441d115fe861dc5ba8ea31ccd6c9780811375 Mon Sep 17 00:00:00 2001 From: Andrii Landiak Date: Fri, 8 Sep 2023 15:27:49 +0300 Subject: [PATCH 002/345] Tmp check test logic for rule chain --- .../thingsboard/server/dao/rule/BaseRuleChainService.java | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/dao/src/main/java/org/thingsboard/server/dao/rule/BaseRuleChainService.java b/dao/src/main/java/org/thingsboard/server/dao/rule/BaseRuleChainService.java index 387fc00c6c..7e9ae12035 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/rule/BaseRuleChainService.java +++ b/dao/src/main/java/org/thingsboard/server/dao/rule/BaseRuleChainService.java @@ -178,11 +178,15 @@ public class BaseRuleChainService extends AbstractEntityService implements RuleC if (nodes != null) { for (RuleNode node : nodes) { setSingletonMode(node); + /* TODO: voba - merge comment if (node.getId() != null) { ruleNodeIndexMap.put(node.getId(), nodes.indexOf(node)); } else { toAddOrUpdate.add(node); } + */ + ruleNodeIndexMap.put(node.getId(), nodes.indexOf(node)); + toAddOrUpdate.add(node); } } @@ -193,8 +197,8 @@ public class BaseRuleChainService extends AbstractEntityService implements RuleC Integer index = ruleNodeIndexMap.get(existingNode.getId()); RuleNode newRuleNode = null; if (index != null) { - newRuleNode = ruleChainMetaData.getNodes().get(index); - toAddOrUpdate.add(newRuleNode); +// newRuleNode = ruleChainMetaData.getNodes().get(index); +// toAddOrUpdate.add(newRuleNode); } else { updatedRuleNodes.add(new RuleNodeUpdateResult(existingNode, null)); toDelete.add(existingNode); From 055fab9ad54a789ecb6451692e6a57c85b2e02bd Mon Sep 17 00:00:00 2001 From: Andrii Landiak Date: Fri, 8 Sep 2023 17:09:06 +0300 Subject: [PATCH 003/345] Fix test: missing bean and improve safe for rule chain --- .../edge/rpc/processor/rule/BaseRuleChainProcessor.java | 4 ++-- .../edge/rpc/processor/rule/RuleChainEdgeProcessor.java | 3 ++- .../service/edge/rpc/processor/BaseEdgeProcessorTest.java | 4 ++++ 3 files changed, 8 insertions(+), 3 deletions(-) diff --git a/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/rule/BaseRuleChainProcessor.java b/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/rule/BaseRuleChainProcessor.java index dc9fa6e14d..66a471e3a0 100644 --- a/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/rule/BaseRuleChainProcessor.java +++ b/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/rule/BaseRuleChainProcessor.java @@ -41,7 +41,7 @@ import java.util.function.Function; @Slf4j public class BaseRuleChainProcessor extends BaseEdgeProcessor { - protected boolean saveOrUpdateRuleChain(TenantId tenantId, RuleChainId ruleChainId, RuleChainUpdateMsg ruleChainUpdateMsg) { + protected boolean saveOrUpdateRuleChain(TenantId tenantId, RuleChainId ruleChainId, RuleChainUpdateMsg ruleChainUpdateMsg, RuleChainType ruleChainType) { boolean created = false; RuleChain ruleChain = ruleChainService.findRuleChainById(tenantId, ruleChainId); if (ruleChain == null) { @@ -51,7 +51,7 @@ public class BaseRuleChainProcessor extends BaseEdgeProcessor { ruleChain.setCreatedTime(Uuids.unixTimestamp(ruleChainId.getId())); } ruleChain.setName(ruleChainUpdateMsg.getName()); - ruleChain.setType(RuleChainType.EDGE); + ruleChain.setType(ruleChainType); ruleChain.setDebugMode(ruleChainUpdateMsg.getDebugMode()); ruleChain.setConfiguration(JacksonUtil.toJsonNode(ruleChainUpdateMsg.getConfiguration())); 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 ae5e158ba7..e2ce1fe651 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 @@ -28,6 +28,7 @@ import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.common.data.msg.TbMsgType; import org.thingsboard.server.common.data.rule.RuleChain; import org.thingsboard.server.common.data.rule.RuleChainMetaData; +import org.thingsboard.server.common.data.rule.RuleChainType; import org.thingsboard.server.common.msg.TbMsgMetaData; import org.thingsboard.server.dao.exception.DataValidationException; import org.thingsboard.server.gen.edge.v1.DownlinkMsg; @@ -105,7 +106,7 @@ public class RuleChainEdgeProcessor extends BaseRuleChainProcessor { } private void saveOrUpdateRuleChain(TenantId tenantId, RuleChainId ruleChainId, RuleChainUpdateMsg ruleChainUpdateMsg, Edge edge) { - boolean created = super.saveOrUpdateRuleChain(tenantId, ruleChainId, ruleChainUpdateMsg); + boolean created = super.saveOrUpdateRuleChain(tenantId, ruleChainId, ruleChainUpdateMsg, RuleChainType.EDGE); if (created) { createRelationFromEdge(tenantId, edge.getId(), ruleChainId); pushRuleChainCreatedEventToRuleEngine(tenantId, edge, ruleChainId); diff --git a/application/src/test/java/org/thingsboard/server/service/edge/rpc/processor/BaseEdgeProcessorTest.java b/application/src/test/java/org/thingsboard/server/service/edge/rpc/processor/BaseEdgeProcessorTest.java index 1c4b481302..9519362305 100644 --- a/application/src/test/java/org/thingsboard/server/service/edge/rpc/processor/BaseEdgeProcessorTest.java +++ b/application/src/test/java/org/thingsboard/server/service/edge/rpc/processor/BaseEdgeProcessorTest.java @@ -31,6 +31,7 @@ import org.thingsboard.server.common.data.id.DashboardId; import org.thingsboard.server.common.data.id.EdgeId; import org.thingsboard.server.common.data.id.RuleChainId; import org.thingsboard.server.common.data.id.TenantId; +import org.thingsboard.server.common.data.rule.RuleChain; import org.thingsboard.server.dao.alarm.AlarmService; import org.thingsboard.server.dao.asset.AssetProfileService; import org.thingsboard.server.dao.asset.AssetService; @@ -196,6 +197,9 @@ public abstract class BaseEdgeProcessorTest { @MockBean protected DataValidator entityViewValidator; + @MockBean + protected DataValidator ruleChainValidator; + @MockBean protected EdgeMsgConstructor edgeMsgConstructor; From eaafdf1f874d6ad6b81257841b24f4e87d34f696 Mon Sep 17 00:00:00 2001 From: YevhenBondarenko Date: Wed, 4 Dec 2024 17:05:37 +0100 Subject: [PATCH 004/345] implemented sending api usages in pack --- .../DefaultTbApiUsageStateService.java | 12 +++-- .../apiusage/TbApiUsageStateService.java | 4 +- .../queue/DefaultTbCoreConsumerService.java | 14 +++--- .../src/main/resources/thingsboard.yml | 2 + .../server/common/util/ProtoUtils.java | 35 +++++++++++++++ common/proto/src/main/proto/queue.proto | 22 ++++++++- .../provider/AwsSqsMonolithQueueFactory.java | 8 ++-- .../provider/AwsSqsTbCoreQueueFactory.java | 8 ++-- .../AwsSqsTbRuleEngineQueueFactory.java | 4 +- .../AwsSqsTbVersionControlQueueFactory.java | 2 +- .../provider/AwsSqsTransportQueueFactory.java | 2 +- .../InMemoryMonolithQueueFactory.java | 4 +- .../InMemoryTbTransportQueueFactory.java | 2 +- .../provider/KafkaMonolithQueueFactory.java | 12 ++--- .../provider/KafkaTbCoreQueueFactory.java | 12 ++--- .../KafkaTbRuleEngineQueueFactory.java | 6 +-- .../KafkaTbTransportQueueFactory.java | 6 +-- .../KafkaTbVersionControlQueueFactory.java | 6 +-- .../provider/PubSubMonolithQueueFactory.java | 8 ++-- .../provider/PubSubTbCoreQueueFactory.java | 8 ++-- .../PubSubTbRuleEngineQueueFactory.java | 4 +- .../PubSubTbVersionControlQueueFactory.java | 2 +- .../provider/PubSubTransportQueueFactory.java | 4 +- .../RabbitMqMonolithQueueFactory.java | 8 ++-- .../provider/RabbitMqTbCoreQueueFactory.java | 8 ++-- .../RabbitMqTbRuleEngineQueueFactory.java | 4 +- .../RabbitMqTbVersionControlQueueFactory.java | 2 +- .../RabbitMqTransportQueueFactory.java | 4 +- .../ServiceBusMonolithQueueFactory.java | 8 ++-- .../ServiceBusTbCoreQueueFactory.java | 8 ++-- .../ServiceBusTbRuleEngineQueueFactory.java | 4 +- ...erviceBusTbVersionControlQueueFactory.java | 2 +- .../ServiceBusTransportQueueFactory.java | 4 +- .../queue/provider/TbCoreQueueFactory.java | 4 +- .../provider/TbCoreQueueProducerProvider.java | 6 +-- .../provider/TbQueueProducerProvider.java | 4 +- .../TbRuleEngineProducerProvider.java | 6 +-- .../TbTransportQueueProducerProvider.java | 6 +-- .../TbUsageStatsClientQueueFactory.java | 4 +- .../TbVersionControlProducerProvider.java | 6 +-- .../DefaultTbApiUsageReportClient.java | 45 +++++++++++++++---- .../src/main/resources/tb-vc-executor.yml | 2 + .../src/main/resources/tb-coap-transport.yml | 2 + .../src/main/resources/tb-http-transport.yml | 2 + .../src/main/resources/tb-lwm2m-transport.yml | 2 + .../src/main/resources/tb-mqtt-transport.yml | 2 + .../src/main/resources/tb-snmp-transport.yml | 2 + 47 files changed, 216 insertions(+), 116 deletions(-) diff --git a/application/src/main/java/org/thingsboard/server/service/apiusage/DefaultTbApiUsageStateService.java b/application/src/main/java/org/thingsboard/server/service/apiusage/DefaultTbApiUsageStateService.java index 60bf529ace..82413013a3 100644 --- a/application/src/main/java/org/thingsboard/server/service/apiusage/DefaultTbApiUsageStateService.java +++ b/application/src/main/java/org/thingsboard/server/service/apiusage/DefaultTbApiUsageStateService.java @@ -55,11 +55,13 @@ import org.thingsboard.server.common.msg.queue.ServiceType; import org.thingsboard.server.common.msg.queue.TbCallback; import org.thingsboard.server.common.msg.queue.TopicPartitionInfo; import org.thingsboard.server.common.msg.tools.SchedulerUtils; +import org.thingsboard.server.common.util.ProtoUtils; import org.thingsboard.server.dao.tenant.TbTenantProfileCache; import org.thingsboard.server.dao.tenant.TenantService; import org.thingsboard.server.dao.timeseries.TimeseriesService; import org.thingsboard.server.dao.usagerecord.ApiUsageStateService; import org.thingsboard.server.gen.transport.TransportProtos.ToUsageStatsServiceMsg; +import org.thingsboard.server.gen.transport.TransportProtos.ToUsageStatsServiceMsgPack; import org.thingsboard.server.gen.transport.TransportProtos.UsageStatsKVProto; import org.thingsboard.server.queue.common.TbProtoQueueMsg; import org.thingsboard.server.queue.discovery.PartitionService; @@ -153,9 +155,12 @@ public class DefaultTbApiUsageStateService extends AbstractPartitionBasedService } @Override - public void process(TbProtoQueueMsg msg, TbCallback callback) { - ToUsageStatsServiceMsg statsMsg = msg.getValue(); + public void process(TbProtoQueueMsg msgPack, TbCallback callback) { + msgPack.getValue().getMsgsList().forEach(this::process); + callback.onSuccess(); + } + private void process(ToUsageStatsServiceMsg statsMsg) { TenantId tenantId = TenantId.fromUUID(new UUID(statsMsg.getTenantIdMSB(), statsMsg.getTenantIdLSB())); EntityId ownerId; if (statsMsg.getCustomerIdMSB() != 0 && statsMsg.getCustomerIdLSB() != 0) { @@ -165,7 +170,6 @@ public class DefaultTbApiUsageStateService extends AbstractPartitionBasedService } processEntityUsageStats(tenantId, ownerId, statsMsg.getValuesList(), statsMsg.getServiceId()); - callback.onSuccess(); } private void processEntityUsageStats(TenantId tenantId, EntityId ownerId, List values, String serviceId) { @@ -190,7 +194,7 @@ public class DefaultTbApiUsageStateService extends AbstractPartitionBasedService updatedEntries = new ArrayList<>(ApiUsageRecordKey.values().length); Set apiFeatures = new HashSet<>(); for (UsageStatsKVProto statsItem : values) { - ApiUsageRecordKey recordKey = ApiUsageRecordKey.valueOf(statsItem.getKey()); + ApiUsageRecordKey recordKey = ProtoUtils.fromProto(statsItem.getKey()); StatsCalculationResult calculationResult = usageState.calculate(recordKey, statsItem.getValue(), serviceId); if (calculationResult.isValueChanged()) { diff --git a/application/src/main/java/org/thingsboard/server/service/apiusage/TbApiUsageStateService.java b/application/src/main/java/org/thingsboard/server/service/apiusage/TbApiUsageStateService.java index 5b5b1b5225..27673ccc92 100644 --- a/application/src/main/java/org/thingsboard/server/service/apiusage/TbApiUsageStateService.java +++ b/application/src/main/java/org/thingsboard/server/service/apiusage/TbApiUsageStateService.java @@ -22,13 +22,13 @@ import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.common.data.id.TenantProfileId; import org.thingsboard.server.common.msg.queue.TbCallback; import org.thingsboard.server.common.stats.TbApiUsageStateClient; -import org.thingsboard.server.gen.transport.TransportProtos.ToUsageStatsServiceMsg; +import org.thingsboard.server.gen.transport.TransportProtos.ToUsageStatsServiceMsgPack; import org.thingsboard.server.queue.common.TbProtoQueueMsg; import org.thingsboard.server.queue.discovery.event.PartitionChangeEvent; public interface TbApiUsageStateService extends TbApiUsageStateClient, RuleEngineApiUsageStateService, ApplicationListener { - void process(TbProtoQueueMsg msg, TbCallback callback); + void process(TbProtoQueueMsg msg, TbCallback callback); void onTenantProfileUpdate(TenantProfileId tenantProfileId); diff --git a/application/src/main/java/org/thingsboard/server/service/queue/DefaultTbCoreConsumerService.java b/application/src/main/java/org/thingsboard/server/service/queue/DefaultTbCoreConsumerService.java index 3aaa36b068..6d4a108107 100644 --- a/application/src/main/java/org/thingsboard/server/service/queue/DefaultTbCoreConsumerService.java +++ b/application/src/main/java/org/thingsboard/server/service/queue/DefaultTbCoreConsumerService.java @@ -74,7 +74,7 @@ import org.thingsboard.server.gen.transport.TransportProtos.TbTimeSeriesUpdatePr import org.thingsboard.server.gen.transport.TransportProtos.ToCoreMsg; import org.thingsboard.server.gen.transport.TransportProtos.ToCoreNotificationMsg; import org.thingsboard.server.gen.transport.TransportProtos.ToOtaPackageStateServiceMsg; -import org.thingsboard.server.gen.transport.TransportProtos.ToUsageStatsServiceMsg; +import org.thingsboard.server.gen.transport.TransportProtos.ToUsageStatsServiceMsgPack; import org.thingsboard.server.gen.transport.TransportProtos.TransportToDeviceActorMsg; import org.thingsboard.server.queue.TbQueueConsumer; import org.thingsboard.server.queue.common.TbProtoQueueMsg; @@ -150,7 +150,7 @@ public class DefaultTbCoreConsumerService extends AbstractConsumerService, CoreQueueConfig> mainConsumer; - private QueueConsumerManager> usageStatsConsumer; + private QueueConsumerManager> usageStatsConsumer; private QueueConsumerManager> firmwareStatesConsumer; private volatile ListeningExecutorService deviceActivityEventsExecutor; @@ -207,7 +207,7 @@ public class DefaultTbCoreConsumerService extends AbstractConsumerService>builder() + this.usageStatsConsumer = QueueConsumerManager.>builder() .name("TB Usage Stats") .msgPackProcessor(this::processUsageStatsMsg) .pollInterval(pollInterval) @@ -402,11 +402,11 @@ public class DefaultTbCoreConsumerService extends AbstractConsumerService> msgs, TbQueueConsumer> consumer) throws Exception { - ConcurrentMap> pendingMap = msgs.stream().collect( + private void processUsageStatsMsg(List> msgs, TbQueueConsumer> consumer) throws Exception { + ConcurrentMap> pendingMap = msgs.stream().collect( Collectors.toConcurrentMap(s -> UUID.randomUUID(), Function.identity())); CountDownLatch processingTimeoutLatch = new CountDownLatch(1); - TbPackProcessingContext> ctx = new TbPackProcessingContext<>( + TbPackProcessingContext> ctx = new TbPackProcessingContext<>( processingTimeoutLatch, pendingMap, new ConcurrentHashMap<>()); pendingMap.forEach((id, msg) -> { log.trace("[{}] Creating usage stats callback for message: {}", id, msg.getValue()); @@ -453,7 +453,7 @@ public class DefaultTbCoreConsumerService extends AbstractConsumerService msg, TbCallback callback) { + private void handleUsageStats(TbProtoQueueMsg msg, TbCallback callback) { statsService.process(msg, callback); } diff --git a/application/src/main/resources/thingsboard.yml b/application/src/main/resources/thingsboard.yml index f642d7fea5..163a87046d 100644 --- a/application/src/main/resources/thingsboard.yml +++ b/application/src/main/resources/thingsboard.yml @@ -188,6 +188,8 @@ usage: enabled_per_customer: "${USAGE_STATS_REPORT_PER_CUSTOMER_ENABLED:false}" # Statistics reporting interval, set to send summarized data every 10 seconds by default interval: "${USAGE_STATS_REPORT_INTERVAL:10}" + # Amount of statistic messages in pack + pack_size: "${USAGE_STATS_REPORT_PACK_SIZE:1024}" check: # Interval of checking the start of the next cycle and re-enabling the blocked tenants/customers cycle: "${USAGE_STATS_CHECK_CYCLE:60000}" diff --git a/common/proto/src/main/java/org/thingsboard/server/common/util/ProtoUtils.java b/common/proto/src/main/java/org/thingsboard/server/common/util/ProtoUtils.java index 264b23f118..62a7d266d3 100644 --- a/common/proto/src/main/java/org/thingsboard/server/common/util/ProtoUtils.java +++ b/common/proto/src/main/java/org/thingsboard/server/common/util/ProtoUtils.java @@ -20,6 +20,7 @@ import com.fasterxml.jackson.databind.JsonNode; import com.google.protobuf.ByteString; import lombok.extern.slf4j.Slf4j; import org.thingsboard.common.util.JacksonUtil; +import org.thingsboard.server.common.data.ApiUsageRecordKey; import org.thingsboard.server.common.data.ApiUsageState; import org.thingsboard.server.common.data.ApiUsageStateValue; import org.thingsboard.server.common.data.Device; @@ -88,6 +89,7 @@ import org.thingsboard.server.common.msg.rule.engine.DeviceDeleteMsg; import org.thingsboard.server.common.msg.rule.engine.DeviceEdgeUpdateMsg; import org.thingsboard.server.common.msg.rule.engine.DeviceNameOrTypeUpdateMsg; import org.thingsboard.server.gen.transport.TransportProtos; +import org.thingsboard.server.gen.transport.TransportProtos.ApiUsageRecordKeyProto; import java.util.ArrayList; import java.util.Arrays; @@ -399,6 +401,39 @@ public class ProtoUtils { return builder.build(); } + public static ApiUsageRecordKeyProto toProto(ApiUsageRecordKey apiUsageRecordKey) { + return switch (apiUsageRecordKey) { + case TRANSPORT_MSG_COUNT -> ApiUsageRecordKeyProto.TRANSPORT_MSG_COUNT; + case TRANSPORT_DP_COUNT -> ApiUsageRecordKeyProto.TRANSPORT_DP_COUNT; + case STORAGE_DP_COUNT -> ApiUsageRecordKeyProto.STORAGE_DP_COUNT; + case RE_EXEC_COUNT -> ApiUsageRecordKeyProto.RE_EXEC_COUNT; + case JS_EXEC_COUNT -> ApiUsageRecordKeyProto.JS_EXEC_COUNT; + case TBEL_EXEC_COUNT -> ApiUsageRecordKeyProto.TBEL_EXEC_COUNT; + case EMAIL_EXEC_COUNT -> ApiUsageRecordKeyProto.EMAIL_EXEC_COUNT; + case SMS_EXEC_COUNT -> ApiUsageRecordKeyProto.SMS_EXEC_COUNT; + case CREATED_ALARMS_COUNT -> ApiUsageRecordKeyProto.CREATED_ALARMS_COUNT; + case ACTIVE_DEVICES -> ApiUsageRecordKeyProto.ACTIVE_DEVICES; + case INACTIVE_DEVICES -> ApiUsageRecordKeyProto.INACTIVE_DEVICES; + }; + } + + public static ApiUsageRecordKey fromProto(ApiUsageRecordKeyProto proto) { + return switch (proto) { + case UNRECOGNIZED -> null; + case TRANSPORT_MSG_COUNT -> ApiUsageRecordKey.TRANSPORT_MSG_COUNT; + case TRANSPORT_DP_COUNT -> ApiUsageRecordKey.TRANSPORT_DP_COUNT; + case STORAGE_DP_COUNT -> ApiUsageRecordKey.STORAGE_DP_COUNT; + case RE_EXEC_COUNT -> ApiUsageRecordKey.RE_EXEC_COUNT; + case JS_EXEC_COUNT -> ApiUsageRecordKey.JS_EXEC_COUNT; + case TBEL_EXEC_COUNT -> ApiUsageRecordKey.TBEL_EXEC_COUNT; + case EMAIL_EXEC_COUNT -> ApiUsageRecordKey.EMAIL_EXEC_COUNT; + case SMS_EXEC_COUNT -> ApiUsageRecordKey.SMS_EXEC_COUNT; + case CREATED_ALARMS_COUNT -> ApiUsageRecordKey.CREATED_ALARMS_COUNT; + case ACTIVE_DEVICES -> ApiUsageRecordKey.ACTIVE_DEVICES; + case INACTIVE_DEVICES -> ApiUsageRecordKey.INACTIVE_DEVICES; + }; + } + private static ToDeviceActorNotificationMsg fromProto(TransportProtos.DeviceAttributesEventMsgProto proto) { return new DeviceAttributesEventNotificationMsg( TenantId.fromUUID(new UUID(proto.getTenantIdMSB(), proto.getTenantIdLSB())), diff --git a/common/proto/src/main/proto/queue.proto b/common/proto/src/main/proto/queue.proto index 228a4039d2..3541654b73 100644 --- a/common/proto/src/main/proto/queue.proto +++ b/common/proto/src/main/proto/queue.proto @@ -61,6 +61,20 @@ enum EntityTypeProto { MOBILE_APP_BUNDLE = 38; } +enum ApiUsageRecordKeyProto { + TRANSPORT_MSG_COUNT = 0; + TRANSPORT_DP_COUNT = 1; + STORAGE_DP_COUNT = 2; + RE_EXEC_COUNT = 3; + JS_EXEC_COUNT = 4; + TBEL_EXEC_COUNT = 5; + EMAIL_EXEC_COUNT = 6; + SMS_EXEC_COUNT = 7; + CREATED_ALARMS_COUNT = 8; + ACTIVE_DEVICES = 9; + INACTIVE_DEVICES = 10; +} + /** * Service Discovery Data Structures; */ @@ -1588,8 +1602,8 @@ message ToTransportMsg { repeated QueueDeleteMsg queueDeleteMsgs = 16; } -message UsageStatsKVProto{ - string key = 1; +message UsageStatsKVProto { + ApiUsageRecordKeyProto key = 1; int64 value = 2; } @@ -1604,6 +1618,10 @@ message ToUsageStatsServiceMsg { string serviceId = 8; } +message ToUsageStatsServiceMsgPack { + repeated ToUsageStatsServiceMsg msgs = 1; +} + message ToOtaPackageStateServiceMsg { int64 ts = 1; int64 tenantIdMSB = 2; diff --git a/common/queue/src/main/java/org/thingsboard/server/queue/provider/AwsSqsMonolithQueueFactory.java b/common/queue/src/main/java/org/thingsboard/server/queue/provider/AwsSqsMonolithQueueFactory.java index 310cc1d1c5..8efc66f2c1 100644 --- a/common/queue/src/main/java/org/thingsboard/server/queue/provider/AwsSqsMonolithQueueFactory.java +++ b/common/queue/src/main/java/org/thingsboard/server/queue/provider/AwsSqsMonolithQueueFactory.java @@ -34,7 +34,7 @@ import org.thingsboard.server.gen.transport.TransportProtos.ToOtaPackageStateSer import org.thingsboard.server.gen.transport.TransportProtos.ToRuleEngineMsg; import org.thingsboard.server.gen.transport.TransportProtos.ToRuleEngineNotificationMsg; import org.thingsboard.server.gen.transport.TransportProtos.ToTransportMsg; -import org.thingsboard.server.gen.transport.TransportProtos.ToUsageStatsServiceMsg; +import org.thingsboard.server.gen.transport.TransportProtos.ToUsageStatsServiceMsgPack; import org.thingsboard.server.gen.transport.TransportProtos.ToVersionControlServiceMsg; import org.thingsboard.server.gen.transport.TransportProtos.TransportApiRequestMsg; import org.thingsboard.server.gen.transport.TransportProtos.TransportApiResponseMsg; @@ -211,14 +211,14 @@ public class AwsSqsMonolithQueueFactory implements TbCoreQueueFactory, TbRuleEng } @Override - public TbQueueProducer> createToUsageStatsServiceMsgProducer() { + public TbQueueProducer> createToUsageStatsServiceMsgProducer() { return new TbAwsSqsProducerTemplate<>(coreAdmin, sqsSettings, topicService.buildTopicName(coreSettings.getUsageStatsTopic())); } @Override - public TbQueueConsumer> createToUsageStatsServiceMsgConsumer() { + public TbQueueConsumer> createToUsageStatsServiceMsgConsumer() { return new TbAwsSqsConsumerTemplate<>(coreAdmin, sqsSettings, topicService.buildTopicName(coreSettings.getUsageStatsTopic()), - msg -> new TbProtoQueueMsg<>(msg.getKey(), ToUsageStatsServiceMsg.parseFrom(msg.getData()), msg.getHeaders())); + msg -> new TbProtoQueueMsg<>(msg.getKey(), ToUsageStatsServiceMsgPack.parseFrom(msg.getData()), msg.getHeaders())); } @Override diff --git a/common/queue/src/main/java/org/thingsboard/server/queue/provider/AwsSqsTbCoreQueueFactory.java b/common/queue/src/main/java/org/thingsboard/server/queue/provider/AwsSqsTbCoreQueueFactory.java index 515c1bba44..29676deb5e 100644 --- a/common/queue/src/main/java/org/thingsboard/server/queue/provider/AwsSqsTbCoreQueueFactory.java +++ b/common/queue/src/main/java/org/thingsboard/server/queue/provider/AwsSqsTbCoreQueueFactory.java @@ -31,7 +31,7 @@ import org.thingsboard.server.gen.transport.TransportProtos.ToOtaPackageStateSer import org.thingsboard.server.gen.transport.TransportProtos.ToRuleEngineMsg; import org.thingsboard.server.gen.transport.TransportProtos.ToRuleEngineNotificationMsg; import org.thingsboard.server.gen.transport.TransportProtos.ToTransportMsg; -import org.thingsboard.server.gen.transport.TransportProtos.ToUsageStatsServiceMsg; +import org.thingsboard.server.gen.transport.TransportProtos.ToUsageStatsServiceMsgPack; import org.thingsboard.server.gen.transport.TransportProtos.ToVersionControlServiceMsg; import org.thingsboard.server.gen.transport.TransportProtos.TransportApiRequestMsg; import org.thingsboard.server.gen.transport.TransportProtos.TransportApiResponseMsg; @@ -189,14 +189,14 @@ public class AwsSqsTbCoreQueueFactory implements TbCoreQueueFactory { } @Override - public TbQueueProducer> createToUsageStatsServiceMsgProducer() { + public TbQueueProducer> createToUsageStatsServiceMsgProducer() { return new TbAwsSqsProducerTemplate<>(coreAdmin, sqsSettings, topicService.buildTopicName(coreSettings.getUsageStatsTopic())); } @Override - public TbQueueConsumer> createToUsageStatsServiceMsgConsumer() { + public TbQueueConsumer> createToUsageStatsServiceMsgConsumer() { return new TbAwsSqsConsumerTemplate<>(coreAdmin, sqsSettings, topicService.buildTopicName(coreSettings.getUsageStatsTopic()), - msg -> new TbProtoQueueMsg<>(msg.getKey(), ToUsageStatsServiceMsg.parseFrom(msg.getData()), msg.getHeaders())); + msg -> new TbProtoQueueMsg<>(msg.getKey(), ToUsageStatsServiceMsgPack.parseFrom(msg.getData()), msg.getHeaders())); } @Override diff --git a/common/queue/src/main/java/org/thingsboard/server/queue/provider/AwsSqsTbRuleEngineQueueFactory.java b/common/queue/src/main/java/org/thingsboard/server/queue/provider/AwsSqsTbRuleEngineQueueFactory.java index a93aba2764..31dde9228d 100644 --- a/common/queue/src/main/java/org/thingsboard/server/queue/provider/AwsSqsTbRuleEngineQueueFactory.java +++ b/common/queue/src/main/java/org/thingsboard/server/queue/provider/AwsSqsTbRuleEngineQueueFactory.java @@ -32,7 +32,7 @@ import org.thingsboard.server.gen.transport.TransportProtos.ToOtaPackageStateSer import org.thingsboard.server.gen.transport.TransportProtos.ToRuleEngineMsg; import org.thingsboard.server.gen.transport.TransportProtos.ToRuleEngineNotificationMsg; import org.thingsboard.server.gen.transport.TransportProtos.ToTransportMsg; -import org.thingsboard.server.gen.transport.TransportProtos.ToUsageStatsServiceMsg; +import org.thingsboard.server.gen.transport.TransportProtos.ToUsageStatsServiceMsgPack; import org.thingsboard.server.queue.TbQueueAdmin; import org.thingsboard.server.queue.TbQueueConsumer; import org.thingsboard.server.queue.TbQueueProducer; @@ -172,7 +172,7 @@ public class AwsSqsTbRuleEngineQueueFactory implements TbRuleEngineQueueFactory } @Override - public TbQueueProducer> createToUsageStatsServiceMsgProducer() { + public TbQueueProducer> createToUsageStatsServiceMsgProducer() { return new TbAwsSqsProducerTemplate<>(coreAdmin, sqsSettings, topicService.buildTopicName(coreSettings.getUsageStatsTopic())); } diff --git a/common/queue/src/main/java/org/thingsboard/server/queue/provider/AwsSqsTbVersionControlQueueFactory.java b/common/queue/src/main/java/org/thingsboard/server/queue/provider/AwsSqsTbVersionControlQueueFactory.java index 67ff0393bd..58dc8b7c88 100644 --- a/common/queue/src/main/java/org/thingsboard/server/queue/provider/AwsSqsTbVersionControlQueueFactory.java +++ b/common/queue/src/main/java/org/thingsboard/server/queue/provider/AwsSqsTbVersionControlQueueFactory.java @@ -62,7 +62,7 @@ public class AwsSqsTbVersionControlQueueFactory implements TbVersionControlQueue } @Override - public TbQueueProducer> createToUsageStatsServiceMsgProducer() { + public TbQueueProducer> createToUsageStatsServiceMsgProducer() { return new TbAwsSqsProducerTemplate<>(coreAdmin, sqsSettings, topicService.buildTopicName(coreSettings.getUsageStatsTopic())); } diff --git a/common/queue/src/main/java/org/thingsboard/server/queue/provider/AwsSqsTransportQueueFactory.java b/common/queue/src/main/java/org/thingsboard/server/queue/provider/AwsSqsTransportQueueFactory.java index 407ef86a99..7c649d879a 100644 --- a/common/queue/src/main/java/org/thingsboard/server/queue/provider/AwsSqsTransportQueueFactory.java +++ b/common/queue/src/main/java/org/thingsboard/server/queue/provider/AwsSqsTransportQueueFactory.java @@ -126,7 +126,7 @@ public class AwsSqsTransportQueueFactory implements TbTransportQueueFactory { } @Override - public TbQueueProducer> createToUsageStatsServiceMsgProducer() { + public TbQueueProducer> createToUsageStatsServiceMsgProducer() { return new TbAwsSqsProducerTemplate<>(coreAdmin, sqsSettings, topicService.buildTopicName(coreSettings.getUsageStatsTopic())); } diff --git a/common/queue/src/main/java/org/thingsboard/server/queue/provider/InMemoryMonolithQueueFactory.java b/common/queue/src/main/java/org/thingsboard/server/queue/provider/InMemoryMonolithQueueFactory.java index d70cad159b..d4ca701b51 100644 --- a/common/queue/src/main/java/org/thingsboard/server/queue/provider/InMemoryMonolithQueueFactory.java +++ b/common/queue/src/main/java/org/thingsboard/server/queue/provider/InMemoryMonolithQueueFactory.java @@ -140,7 +140,7 @@ public class InMemoryMonolithQueueFactory implements TbCoreQueueFactory, TbRuleE } @Override - public TbQueueConsumer> createToUsageStatsServiceMsgConsumer() { + public TbQueueConsumer> createToUsageStatsServiceMsgConsumer() { return new InMemoryTbQueueConsumer<>(storage, topicService.buildTopicName(coreSettings.getUsageStatsTopic())); } @@ -155,7 +155,7 @@ public class InMemoryMonolithQueueFactory implements TbCoreQueueFactory, TbRuleE } @Override - public TbQueueProducer> createToUsageStatsServiceMsgProducer() { + public TbQueueProducer> createToUsageStatsServiceMsgProducer() { return new InMemoryTbQueueProducer<>(storage, topicService.buildTopicName(coreSettings.getUsageStatsTopic())); } diff --git a/common/queue/src/main/java/org/thingsboard/server/queue/provider/InMemoryTbTransportQueueFactory.java b/common/queue/src/main/java/org/thingsboard/server/queue/provider/InMemoryTbTransportQueueFactory.java index 4ee79aef60..2d8e124172 100644 --- a/common/queue/src/main/java/org/thingsboard/server/queue/provider/InMemoryTbTransportQueueFactory.java +++ b/common/queue/src/main/java/org/thingsboard/server/queue/provider/InMemoryTbTransportQueueFactory.java @@ -116,7 +116,7 @@ public class InMemoryTbTransportQueueFactory implements TbTransportQueueFactory } @Override - public TbQueueProducer> createToUsageStatsServiceMsgProducer() { + public TbQueueProducer> createToUsageStatsServiceMsgProducer() { return new InMemoryTbQueueProducer<>(storage, topicService.buildTopicName(coreSettings.getUsageStatsTopic())); } diff --git a/common/queue/src/main/java/org/thingsboard/server/queue/provider/KafkaMonolithQueueFactory.java b/common/queue/src/main/java/org/thingsboard/server/queue/provider/KafkaMonolithQueueFactory.java index dd5d61e834..18d1a3af00 100644 --- a/common/queue/src/main/java/org/thingsboard/server/queue/provider/KafkaMonolithQueueFactory.java +++ b/common/queue/src/main/java/org/thingsboard/server/queue/provider/KafkaMonolithQueueFactory.java @@ -35,7 +35,7 @@ import org.thingsboard.server.gen.transport.TransportProtos.ToOtaPackageStateSer import org.thingsboard.server.gen.transport.TransportProtos.ToRuleEngineMsg; import org.thingsboard.server.gen.transport.TransportProtos.ToRuleEngineNotificationMsg; import org.thingsboard.server.gen.transport.TransportProtos.ToTransportMsg; -import org.thingsboard.server.gen.transport.TransportProtos.ToUsageStatsServiceMsg; +import org.thingsboard.server.gen.transport.TransportProtos.ToUsageStatsServiceMsgPack; import org.thingsboard.server.gen.transport.TransportProtos.ToVersionControlServiceMsg; import org.thingsboard.server.gen.transport.TransportProtos.TransportApiRequestMsg; import org.thingsboard.server.gen.transport.TransportProtos.TransportApiResponseMsg; @@ -320,13 +320,13 @@ public class KafkaMonolithQueueFactory implements TbCoreQueueFactory, TbRuleEngi } @Override - public TbQueueConsumer> createToUsageStatsServiceMsgConsumer() { - TbKafkaConsumerTemplate.TbKafkaConsumerTemplateBuilder> consumerBuilder = TbKafkaConsumerTemplate.builder(); + public TbQueueConsumer> createToUsageStatsServiceMsgConsumer() { + TbKafkaConsumerTemplate.TbKafkaConsumerTemplateBuilder> consumerBuilder = TbKafkaConsumerTemplate.builder(); consumerBuilder.settings(kafkaSettings); consumerBuilder.topic(topicService.buildTopicName(coreSettings.getUsageStatsTopic())); consumerBuilder.clientId("monolith-us-consumer-" + serviceInfoProvider.getServiceId()); consumerBuilder.groupId(topicService.buildTopicName("monolith-us-consumer")); - consumerBuilder.decoder(msg -> new TbProtoQueueMsg<>(msg.getKey(), ToUsageStatsServiceMsg.parseFrom(msg.getData()), msg.getHeaders())); + consumerBuilder.decoder(msg -> new TbProtoQueueMsg<>(msg.getKey(), ToUsageStatsServiceMsgPack.parseFrom(msg.getData()), msg.getHeaders())); consumerBuilder.admin(coreAdmin); consumerBuilder.statsService(consumerStatsService); return consumerBuilder.build(); @@ -356,8 +356,8 @@ public class KafkaMonolithQueueFactory implements TbCoreQueueFactory, TbRuleEngi } @Override - public TbQueueProducer> createToUsageStatsServiceMsgProducer() { - TbKafkaProducerTemplate.TbKafkaProducerTemplateBuilder> requestBuilder = TbKafkaProducerTemplate.builder(); + public TbQueueProducer> createToUsageStatsServiceMsgProducer() { + TbKafkaProducerTemplate.TbKafkaProducerTemplateBuilder> requestBuilder = TbKafkaProducerTemplate.builder(); requestBuilder.settings(kafkaSettings); requestBuilder.clientId("monolith-us-producer-" + serviceInfoProvider.getServiceId()); requestBuilder.defaultTopic(topicService.buildTopicName(coreSettings.getUsageStatsTopic())); diff --git a/common/queue/src/main/java/org/thingsboard/server/queue/provider/KafkaTbCoreQueueFactory.java b/common/queue/src/main/java/org/thingsboard/server/queue/provider/KafkaTbCoreQueueFactory.java index cc0e044917..b6a2e252ff 100644 --- a/common/queue/src/main/java/org/thingsboard/server/queue/provider/KafkaTbCoreQueueFactory.java +++ b/common/queue/src/main/java/org/thingsboard/server/queue/provider/KafkaTbCoreQueueFactory.java @@ -34,7 +34,7 @@ import org.thingsboard.server.gen.transport.TransportProtos.ToOtaPackageStateSer import org.thingsboard.server.gen.transport.TransportProtos.ToRuleEngineMsg; import org.thingsboard.server.gen.transport.TransportProtos.ToRuleEngineNotificationMsg; import org.thingsboard.server.gen.transport.TransportProtos.ToTransportMsg; -import org.thingsboard.server.gen.transport.TransportProtos.ToUsageStatsServiceMsg; +import org.thingsboard.server.gen.transport.TransportProtos.ToUsageStatsServiceMsgPack; import org.thingsboard.server.gen.transport.TransportProtos.ToVersionControlServiceMsg; import org.thingsboard.server.gen.transport.TransportProtos.TransportApiRequestMsg; import org.thingsboard.server.gen.transport.TransportProtos.TransportApiResponseMsg; @@ -269,13 +269,13 @@ public class KafkaTbCoreQueueFactory implements TbCoreQueueFactory { } @Override - public TbQueueConsumer> createToUsageStatsServiceMsgConsumer() { - TbKafkaConsumerTemplate.TbKafkaConsumerTemplateBuilder> consumerBuilder = TbKafkaConsumerTemplate.builder(); + public TbQueueConsumer> createToUsageStatsServiceMsgConsumer() { + TbKafkaConsumerTemplate.TbKafkaConsumerTemplateBuilder> consumerBuilder = TbKafkaConsumerTemplate.builder(); consumerBuilder.settings(kafkaSettings); consumerBuilder.topic(topicService.buildTopicName(coreSettings.getUsageStatsTopic())); consumerBuilder.clientId("tb-core-us-consumer-" + serviceInfoProvider.getServiceId()); consumerBuilder.groupId(topicService.buildTopicName("tb-core-us-consumer")); - consumerBuilder.decoder(msg -> new TbProtoQueueMsg<>(msg.getKey(), ToUsageStatsServiceMsg.parseFrom(msg.getData()), msg.getHeaders())); + consumerBuilder.decoder(msg -> new TbProtoQueueMsg<>(msg.getKey(), ToUsageStatsServiceMsgPack.parseFrom(msg.getData()), msg.getHeaders())); consumerBuilder.admin(coreAdmin); consumerBuilder.statsService(consumerStatsService); return consumerBuilder.build(); @@ -305,8 +305,8 @@ public class KafkaTbCoreQueueFactory implements TbCoreQueueFactory { } @Override - public TbQueueProducer> createToUsageStatsServiceMsgProducer() { - TbKafkaProducerTemplate.TbKafkaProducerTemplateBuilder> requestBuilder = TbKafkaProducerTemplate.builder(); + public TbQueueProducer> createToUsageStatsServiceMsgProducer() { + TbKafkaProducerTemplate.TbKafkaProducerTemplateBuilder> requestBuilder = TbKafkaProducerTemplate.builder(); requestBuilder.settings(kafkaSettings); requestBuilder.clientId("tb-core-us-producer-" + serviceInfoProvider.getServiceId()); requestBuilder.defaultTopic(topicService.buildTopicName(coreSettings.getUsageStatsTopic())); diff --git a/common/queue/src/main/java/org/thingsboard/server/queue/provider/KafkaTbRuleEngineQueueFactory.java b/common/queue/src/main/java/org/thingsboard/server/queue/provider/KafkaTbRuleEngineQueueFactory.java index 87a1a69c2e..68f5fee9c2 100644 --- a/common/queue/src/main/java/org/thingsboard/server/queue/provider/KafkaTbRuleEngineQueueFactory.java +++ b/common/queue/src/main/java/org/thingsboard/server/queue/provider/KafkaTbRuleEngineQueueFactory.java @@ -33,7 +33,7 @@ import org.thingsboard.server.gen.transport.TransportProtos.ToOtaPackageStateSer import org.thingsboard.server.gen.transport.TransportProtos.ToRuleEngineMsg; import org.thingsboard.server.gen.transport.TransportProtos.ToRuleEngineNotificationMsg; import org.thingsboard.server.gen.transport.TransportProtos.ToTransportMsg; -import org.thingsboard.server.gen.transport.TransportProtos.ToUsageStatsServiceMsg; +import org.thingsboard.server.gen.transport.TransportProtos.ToUsageStatsServiceMsgPack; import org.thingsboard.server.queue.TbQueueAdmin; import org.thingsboard.server.queue.TbQueueConsumer; import org.thingsboard.server.queue.TbQueueProducer; @@ -274,8 +274,8 @@ public class KafkaTbRuleEngineQueueFactory implements TbRuleEngineQueueFactory { } @Override - public TbQueueProducer> createToUsageStatsServiceMsgProducer() { - TbKafkaProducerTemplate.TbKafkaProducerTemplateBuilder> requestBuilder = TbKafkaProducerTemplate.builder(); + public TbQueueProducer> createToUsageStatsServiceMsgProducer() { + TbKafkaProducerTemplate.TbKafkaProducerTemplateBuilder> requestBuilder = TbKafkaProducerTemplate.builder(); requestBuilder.settings(kafkaSettings); requestBuilder.clientId("tb-rule-engine-us-producer-" + serviceInfoProvider.getServiceId()); requestBuilder.defaultTopic(topicService.buildTopicName(coreSettings.getUsageStatsTopic())); diff --git a/common/queue/src/main/java/org/thingsboard/server/queue/provider/KafkaTbTransportQueueFactory.java b/common/queue/src/main/java/org/thingsboard/server/queue/provider/KafkaTbTransportQueueFactory.java index dd260840f5..daa78a08f6 100644 --- a/common/queue/src/main/java/org/thingsboard/server/queue/provider/KafkaTbTransportQueueFactory.java +++ b/common/queue/src/main/java/org/thingsboard/server/queue/provider/KafkaTbTransportQueueFactory.java @@ -24,7 +24,7 @@ import org.thingsboard.server.gen.transport.TransportProtos.ToCoreNotificationMs import org.thingsboard.server.gen.transport.TransportProtos.ToHousekeeperServiceMsg; import org.thingsboard.server.gen.transport.TransportProtos.ToRuleEngineMsg; import org.thingsboard.server.gen.transport.TransportProtos.ToTransportMsg; -import org.thingsboard.server.gen.transport.TransportProtos.ToUsageStatsServiceMsg; +import org.thingsboard.server.gen.transport.TransportProtos.ToUsageStatsServiceMsgPack; import org.thingsboard.server.gen.transport.TransportProtos.TransportApiRequestMsg; import org.thingsboard.server.gen.transport.TransportProtos.TransportApiResponseMsg; import org.thingsboard.server.queue.TbQueueAdmin; @@ -165,8 +165,8 @@ public class KafkaTbTransportQueueFactory implements TbTransportQueueFactory { } @Override - public TbQueueProducer> createToUsageStatsServiceMsgProducer() { - TbKafkaProducerTemplate.TbKafkaProducerTemplateBuilder> requestBuilder = TbKafkaProducerTemplate.builder(); + public TbQueueProducer> createToUsageStatsServiceMsgProducer() { + TbKafkaProducerTemplate.TbKafkaProducerTemplateBuilder> requestBuilder = TbKafkaProducerTemplate.builder(); requestBuilder.settings(kafkaSettings); requestBuilder.clientId("transport-node-us-producer-" + serviceInfoProvider.getServiceId()); requestBuilder.defaultTopic(topicService.buildTopicName(coreSettings.getUsageStatsTopic())); diff --git a/common/queue/src/main/java/org/thingsboard/server/queue/provider/KafkaTbVersionControlQueueFactory.java b/common/queue/src/main/java/org/thingsboard/server/queue/provider/KafkaTbVersionControlQueueFactory.java index aacebb8579..7e6c05dcae 100644 --- a/common/queue/src/main/java/org/thingsboard/server/queue/provider/KafkaTbVersionControlQueueFactory.java +++ b/common/queue/src/main/java/org/thingsboard/server/queue/provider/KafkaTbVersionControlQueueFactory.java @@ -20,7 +20,7 @@ import org.springframework.boot.autoconfigure.condition.ConditionalOnExpression; import org.springframework.stereotype.Component; import org.thingsboard.server.gen.transport.TransportProtos.ToCoreNotificationMsg; import org.thingsboard.server.gen.transport.TransportProtos.ToHousekeeperServiceMsg; -import org.thingsboard.server.gen.transport.TransportProtos.ToUsageStatsServiceMsg; +import org.thingsboard.server.gen.transport.TransportProtos.ToUsageStatsServiceMsgPack; import org.thingsboard.server.gen.transport.TransportProtos.ToVersionControlServiceMsg; import org.thingsboard.server.queue.TbQueueAdmin; import org.thingsboard.server.queue.TbQueueConsumer; @@ -98,8 +98,8 @@ public class KafkaTbVersionControlQueueFactory implements TbVersionControlQueueF } @Override - public TbQueueProducer> createToUsageStatsServiceMsgProducer() { - TbKafkaProducerTemplate.TbKafkaProducerTemplateBuilder> requestBuilder = TbKafkaProducerTemplate.builder(); + public TbQueueProducer> createToUsageStatsServiceMsgProducer() { + TbKafkaProducerTemplate.TbKafkaProducerTemplateBuilder> requestBuilder = TbKafkaProducerTemplate.builder(); requestBuilder.settings(kafkaSettings); requestBuilder.clientId("tb-vc-us-producer-" + serviceInfoProvider.getServiceId()); requestBuilder.defaultTopic(topicService.buildTopicName(coreSettings.getUsageStatsTopic())); diff --git a/common/queue/src/main/java/org/thingsboard/server/queue/provider/PubSubMonolithQueueFactory.java b/common/queue/src/main/java/org/thingsboard/server/queue/provider/PubSubMonolithQueueFactory.java index 05bef819b7..d72fde8441 100644 --- a/common/queue/src/main/java/org/thingsboard/server/queue/provider/PubSubMonolithQueueFactory.java +++ b/common/queue/src/main/java/org/thingsboard/server/queue/provider/PubSubMonolithQueueFactory.java @@ -34,7 +34,7 @@ import org.thingsboard.server.gen.transport.TransportProtos.ToOtaPackageStateSer import org.thingsboard.server.gen.transport.TransportProtos.ToRuleEngineMsg; import org.thingsboard.server.gen.transport.TransportProtos.ToRuleEngineNotificationMsg; import org.thingsboard.server.gen.transport.TransportProtos.ToTransportMsg; -import org.thingsboard.server.gen.transport.TransportProtos.ToUsageStatsServiceMsg; +import org.thingsboard.server.gen.transport.TransportProtos.ToUsageStatsServiceMsgPack; import org.thingsboard.server.gen.transport.TransportProtos.ToVersionControlServiceMsg; import org.thingsboard.server.gen.transport.TransportProtos.TransportApiRequestMsg; import org.thingsboard.server.gen.transport.TransportProtos.TransportApiResponseMsg; @@ -211,9 +211,9 @@ public class PubSubMonolithQueueFactory implements TbCoreQueueFactory, TbRuleEng } @Override - public TbQueueConsumer> createToUsageStatsServiceMsgConsumer() { + public TbQueueConsumer> createToUsageStatsServiceMsgConsumer() { return new TbPubSubConsumerTemplate<>(coreAdmin, pubSubSettings, topicService.buildTopicName(coreSettings.getUsageStatsTopic()), - msg -> new TbProtoQueueMsg<>(msg.getKey(), ToUsageStatsServiceMsg.parseFrom(msg.getData()), msg.getHeaders())); + msg -> new TbProtoQueueMsg<>(msg.getKey(), ToUsageStatsServiceMsgPack.parseFrom(msg.getData()), msg.getHeaders())); } @Override @@ -228,7 +228,7 @@ public class PubSubMonolithQueueFactory implements TbCoreQueueFactory, TbRuleEng } @Override - public TbQueueProducer> createToUsageStatsServiceMsgProducer() { + public TbQueueProducer> createToUsageStatsServiceMsgProducer() { return new TbPubSubProducerTemplate<>(coreAdmin, pubSubSettings, topicService.buildTopicName(coreSettings.getUsageStatsTopic())); } diff --git a/common/queue/src/main/java/org/thingsboard/server/queue/provider/PubSubTbCoreQueueFactory.java b/common/queue/src/main/java/org/thingsboard/server/queue/provider/PubSubTbCoreQueueFactory.java index 919d895a97..ae7c1ab7f0 100644 --- a/common/queue/src/main/java/org/thingsboard/server/queue/provider/PubSubTbCoreQueueFactory.java +++ b/common/queue/src/main/java/org/thingsboard/server/queue/provider/PubSubTbCoreQueueFactory.java @@ -31,7 +31,7 @@ import org.thingsboard.server.gen.transport.TransportProtos.ToOtaPackageStateSer import org.thingsboard.server.gen.transport.TransportProtos.ToRuleEngineMsg; import org.thingsboard.server.gen.transport.TransportProtos.ToRuleEngineNotificationMsg; import org.thingsboard.server.gen.transport.TransportProtos.ToTransportMsg; -import org.thingsboard.server.gen.transport.TransportProtos.ToUsageStatsServiceMsg; +import org.thingsboard.server.gen.transport.TransportProtos.ToUsageStatsServiceMsgPack; import org.thingsboard.server.gen.transport.TransportProtos.ToVersionControlServiceMsg; import org.thingsboard.server.gen.transport.TransportProtos.TransportApiRequestMsg; import org.thingsboard.server.gen.transport.TransportProtos.TransportApiResponseMsg; @@ -181,9 +181,9 @@ public class PubSubTbCoreQueueFactory implements TbCoreQueueFactory { } @Override - public TbQueueConsumer> createToUsageStatsServiceMsgConsumer() { + public TbQueueConsumer> createToUsageStatsServiceMsgConsumer() { return new TbPubSubConsumerTemplate<>(coreAdmin, pubSubSettings, topicService.buildTopicName(coreSettings.getUsageStatsTopic()), - msg -> new TbProtoQueueMsg<>(msg.getKey(), ToUsageStatsServiceMsg.parseFrom(msg.getData()), msg.getHeaders())); + msg -> new TbProtoQueueMsg<>(msg.getKey(), ToUsageStatsServiceMsgPack.parseFrom(msg.getData()), msg.getHeaders())); } @Override @@ -198,7 +198,7 @@ public class PubSubTbCoreQueueFactory implements TbCoreQueueFactory { } @Override - public TbQueueProducer> createToUsageStatsServiceMsgProducer() { + public TbQueueProducer> createToUsageStatsServiceMsgProducer() { return new TbPubSubProducerTemplate<>(coreAdmin, pubSubSettings, topicService.buildTopicName(coreSettings.getUsageStatsTopic())); } diff --git a/common/queue/src/main/java/org/thingsboard/server/queue/provider/PubSubTbRuleEngineQueueFactory.java b/common/queue/src/main/java/org/thingsboard/server/queue/provider/PubSubTbRuleEngineQueueFactory.java index 671da15f20..edf304057d 100644 --- a/common/queue/src/main/java/org/thingsboard/server/queue/provider/PubSubTbRuleEngineQueueFactory.java +++ b/common/queue/src/main/java/org/thingsboard/server/queue/provider/PubSubTbRuleEngineQueueFactory.java @@ -33,7 +33,7 @@ import org.thingsboard.server.gen.transport.TransportProtos.ToOtaPackageStateSer import org.thingsboard.server.gen.transport.TransportProtos.ToRuleEngineMsg; import org.thingsboard.server.gen.transport.TransportProtos.ToRuleEngineNotificationMsg; import org.thingsboard.server.gen.transport.TransportProtos.ToTransportMsg; -import org.thingsboard.server.gen.transport.TransportProtos.ToUsageStatsServiceMsg; +import org.thingsboard.server.gen.transport.TransportProtos.ToUsageStatsServiceMsgPack; import org.thingsboard.server.queue.TbQueueAdmin; import org.thingsboard.server.queue.TbQueueConsumer; import org.thingsboard.server.queue.TbQueueProducer; @@ -177,7 +177,7 @@ public class PubSubTbRuleEngineQueueFactory implements TbRuleEngineQueueFactory } @Override - public TbQueueProducer> createToUsageStatsServiceMsgProducer() { + public TbQueueProducer> createToUsageStatsServiceMsgProducer() { return new TbPubSubProducerTemplate<>(coreAdmin, pubSubSettings, topicService.buildTopicName(coreSettings.getUsageStatsTopic())); } diff --git a/common/queue/src/main/java/org/thingsboard/server/queue/provider/PubSubTbVersionControlQueueFactory.java b/common/queue/src/main/java/org/thingsboard/server/queue/provider/PubSubTbVersionControlQueueFactory.java index b6da4e2973..3261ad8dfd 100644 --- a/common/queue/src/main/java/org/thingsboard/server/queue/provider/PubSubTbVersionControlQueueFactory.java +++ b/common/queue/src/main/java/org/thingsboard/server/queue/provider/PubSubTbVersionControlQueueFactory.java @@ -62,7 +62,7 @@ public class PubSubTbVersionControlQueueFactory implements TbVersionControlQueue } @Override - public TbQueueProducer> createToUsageStatsServiceMsgProducer() { + public TbQueueProducer> createToUsageStatsServiceMsgProducer() { return new TbPubSubProducerTemplate<>(coreAdmin, pubSubSettings, topicService.buildTopicName(coreSettings.getUsageStatsTopic())); } diff --git a/common/queue/src/main/java/org/thingsboard/server/queue/provider/PubSubTransportQueueFactory.java b/common/queue/src/main/java/org/thingsboard/server/queue/provider/PubSubTransportQueueFactory.java index a7500d2083..fa2f7793e7 100644 --- a/common/queue/src/main/java/org/thingsboard/server/queue/provider/PubSubTransportQueueFactory.java +++ b/common/queue/src/main/java/org/thingsboard/server/queue/provider/PubSubTransportQueueFactory.java @@ -24,7 +24,7 @@ import org.thingsboard.server.gen.transport.TransportProtos.ToCoreNotificationMs import org.thingsboard.server.gen.transport.TransportProtos.ToHousekeeperServiceMsg; import org.thingsboard.server.gen.transport.TransportProtos.ToRuleEngineMsg; import org.thingsboard.server.gen.transport.TransportProtos.ToTransportMsg; -import org.thingsboard.server.gen.transport.TransportProtos.ToUsageStatsServiceMsg; +import org.thingsboard.server.gen.transport.TransportProtos.ToUsageStatsServiceMsgPack; import org.thingsboard.server.gen.transport.TransportProtos.TransportApiRequestMsg; import org.thingsboard.server.gen.transport.TransportProtos.TransportApiResponseMsg; import org.thingsboard.server.queue.TbQueueAdmin; @@ -126,7 +126,7 @@ public class PubSubTransportQueueFactory implements TbTransportQueueFactory { } @Override - public TbQueueProducer> createToUsageStatsServiceMsgProducer() { + public TbQueueProducer> createToUsageStatsServiceMsgProducer() { return new TbPubSubProducerTemplate<>(coreAdmin, pubSubSettings, topicService.buildTopicName(coreSettings.getUsageStatsTopic())); } diff --git a/common/queue/src/main/java/org/thingsboard/server/queue/provider/RabbitMqMonolithQueueFactory.java b/common/queue/src/main/java/org/thingsboard/server/queue/provider/RabbitMqMonolithQueueFactory.java index 3c59923def..e5cdeb1741 100644 --- a/common/queue/src/main/java/org/thingsboard/server/queue/provider/RabbitMqMonolithQueueFactory.java +++ b/common/queue/src/main/java/org/thingsboard/server/queue/provider/RabbitMqMonolithQueueFactory.java @@ -34,7 +34,7 @@ import org.thingsboard.server.gen.transport.TransportProtos.ToOtaPackageStateSer import org.thingsboard.server.gen.transport.TransportProtos.ToRuleEngineMsg; import org.thingsboard.server.gen.transport.TransportProtos.ToRuleEngineNotificationMsg; import org.thingsboard.server.gen.transport.TransportProtos.ToTransportMsg; -import org.thingsboard.server.gen.transport.TransportProtos.ToUsageStatsServiceMsg; +import org.thingsboard.server.gen.transport.TransportProtos.ToUsageStatsServiceMsgPack; import org.thingsboard.server.gen.transport.TransportProtos.ToVersionControlServiceMsg; import org.thingsboard.server.gen.transport.TransportProtos.TransportApiRequestMsg; import org.thingsboard.server.gen.transport.TransportProtos.TransportApiResponseMsg; @@ -209,9 +209,9 @@ public class RabbitMqMonolithQueueFactory implements TbCoreQueueFactory, TbRuleE } @Override - public TbQueueConsumer> createToUsageStatsServiceMsgConsumer() { + public TbQueueConsumer> createToUsageStatsServiceMsgConsumer() { return new TbRabbitMqConsumerTemplate<>(coreAdmin, rabbitMqSettings, topicService.buildTopicName(coreSettings.getUsageStatsTopic()), - msg -> new TbProtoQueueMsg<>(msg.getKey(), ToUsageStatsServiceMsg.parseFrom(msg.getData()), msg.getHeaders())); + msg -> new TbProtoQueueMsg<>(msg.getKey(), ToUsageStatsServiceMsgPack.parseFrom(msg.getData()), msg.getHeaders())); } @Override @@ -226,7 +226,7 @@ public class RabbitMqMonolithQueueFactory implements TbCoreQueueFactory, TbRuleE } @Override - public TbQueueProducer> createToUsageStatsServiceMsgProducer() { + public TbQueueProducer> createToUsageStatsServiceMsgProducer() { return new TbRabbitMqProducerTemplate<>(coreAdmin, rabbitMqSettings, topicService.buildTopicName(coreSettings.getUsageStatsTopic())); } diff --git a/common/queue/src/main/java/org/thingsboard/server/queue/provider/RabbitMqTbCoreQueueFactory.java b/common/queue/src/main/java/org/thingsboard/server/queue/provider/RabbitMqTbCoreQueueFactory.java index 16f9838384..2fcd7936c1 100644 --- a/common/queue/src/main/java/org/thingsboard/server/queue/provider/RabbitMqTbCoreQueueFactory.java +++ b/common/queue/src/main/java/org/thingsboard/server/queue/provider/RabbitMqTbCoreQueueFactory.java @@ -31,7 +31,7 @@ import org.thingsboard.server.gen.transport.TransportProtos.ToOtaPackageStateSer import org.thingsboard.server.gen.transport.TransportProtos.ToRuleEngineMsg; import org.thingsboard.server.gen.transport.TransportProtos.ToRuleEngineNotificationMsg; import org.thingsboard.server.gen.transport.TransportProtos.ToTransportMsg; -import org.thingsboard.server.gen.transport.TransportProtos.ToUsageStatsServiceMsg; +import org.thingsboard.server.gen.transport.TransportProtos.ToUsageStatsServiceMsgPack; import org.thingsboard.server.gen.transport.TransportProtos.ToVersionControlServiceMsg; import org.thingsboard.server.gen.transport.TransportProtos.TransportApiRequestMsg; import org.thingsboard.server.gen.transport.TransportProtos.TransportApiResponseMsg; @@ -211,9 +211,9 @@ public class RabbitMqTbCoreQueueFactory implements TbCoreQueueFactory { } @Override - public TbQueueConsumer> createToUsageStatsServiceMsgConsumer() { + public TbQueueConsumer> createToUsageStatsServiceMsgConsumer() { return new TbRabbitMqConsumerTemplate<>(coreAdmin, rabbitMqSettings, topicService.buildTopicName(coreSettings.getUsageStatsTopic()), - msg -> new TbProtoQueueMsg<>(msg.getKey(), ToUsageStatsServiceMsg.parseFrom(msg.getData()), msg.getHeaders())); + msg -> new TbProtoQueueMsg<>(msg.getKey(), ToUsageStatsServiceMsgPack.parseFrom(msg.getData()), msg.getHeaders())); } @Override @@ -228,7 +228,7 @@ public class RabbitMqTbCoreQueueFactory implements TbCoreQueueFactory { } @Override - public TbQueueProducer> createToUsageStatsServiceMsgProducer() { + public TbQueueProducer> createToUsageStatsServiceMsgProducer() { return new TbRabbitMqProducerTemplate<>(coreAdmin, rabbitMqSettings, topicService.buildTopicName(coreSettings.getUsageStatsTopic())); } diff --git a/common/queue/src/main/java/org/thingsboard/server/queue/provider/RabbitMqTbRuleEngineQueueFactory.java b/common/queue/src/main/java/org/thingsboard/server/queue/provider/RabbitMqTbRuleEngineQueueFactory.java index a7af729e1f..773b1644de 100644 --- a/common/queue/src/main/java/org/thingsboard/server/queue/provider/RabbitMqTbRuleEngineQueueFactory.java +++ b/common/queue/src/main/java/org/thingsboard/server/queue/provider/RabbitMqTbRuleEngineQueueFactory.java @@ -32,7 +32,7 @@ import org.thingsboard.server.gen.transport.TransportProtos.ToOtaPackageStateSer import org.thingsboard.server.gen.transport.TransportProtos.ToRuleEngineMsg; import org.thingsboard.server.gen.transport.TransportProtos.ToRuleEngineNotificationMsg; import org.thingsboard.server.gen.transport.TransportProtos.ToTransportMsg; -import org.thingsboard.server.gen.transport.TransportProtos.ToUsageStatsServiceMsg; +import org.thingsboard.server.gen.transport.TransportProtos.ToUsageStatsServiceMsgPack; import org.thingsboard.server.queue.TbQueueAdmin; import org.thingsboard.server.queue.TbQueueConsumer; import org.thingsboard.server.queue.TbQueueProducer; @@ -170,7 +170,7 @@ public class RabbitMqTbRuleEngineQueueFactory implements TbRuleEngineQueueFactor } @Override - public TbQueueProducer> createToUsageStatsServiceMsgProducer() { + public TbQueueProducer> createToUsageStatsServiceMsgProducer() { return new TbRabbitMqProducerTemplate<>(coreAdmin, rabbitMqSettings, topicService.buildTopicName(coreSettings.getUsageStatsTopic())); } diff --git a/common/queue/src/main/java/org/thingsboard/server/queue/provider/RabbitMqTbVersionControlQueueFactory.java b/common/queue/src/main/java/org/thingsboard/server/queue/provider/RabbitMqTbVersionControlQueueFactory.java index 604955be2c..56b1ea40f5 100644 --- a/common/queue/src/main/java/org/thingsboard/server/queue/provider/RabbitMqTbVersionControlQueueFactory.java +++ b/common/queue/src/main/java/org/thingsboard/server/queue/provider/RabbitMqTbVersionControlQueueFactory.java @@ -62,7 +62,7 @@ public class RabbitMqTbVersionControlQueueFactory implements TbVersionControlQue } @Override - public TbQueueProducer> createToUsageStatsServiceMsgProducer() { + public TbQueueProducer> createToUsageStatsServiceMsgProducer() { return new TbRabbitMqProducerTemplate<>(coreAdmin, rabbitMqSettings, topicService.buildTopicName(coreSettings.getUsageStatsTopic())); } diff --git a/common/queue/src/main/java/org/thingsboard/server/queue/provider/RabbitMqTransportQueueFactory.java b/common/queue/src/main/java/org/thingsboard/server/queue/provider/RabbitMqTransportQueueFactory.java index ea922557bd..f3bc3094a4 100644 --- a/common/queue/src/main/java/org/thingsboard/server/queue/provider/RabbitMqTransportQueueFactory.java +++ b/common/queue/src/main/java/org/thingsboard/server/queue/provider/RabbitMqTransportQueueFactory.java @@ -24,7 +24,7 @@ import org.thingsboard.server.gen.transport.TransportProtos.ToCoreNotificationMs import org.thingsboard.server.gen.transport.TransportProtos.ToHousekeeperServiceMsg; import org.thingsboard.server.gen.transport.TransportProtos.ToRuleEngineMsg; import org.thingsboard.server.gen.transport.TransportProtos.ToTransportMsg; -import org.thingsboard.server.gen.transport.TransportProtos.ToUsageStatsServiceMsg; +import org.thingsboard.server.gen.transport.TransportProtos.ToUsageStatsServiceMsgPack; import org.thingsboard.server.gen.transport.TransportProtos.TransportApiRequestMsg; import org.thingsboard.server.gen.transport.TransportProtos.TransportApiResponseMsg; import org.thingsboard.server.queue.TbQueueAdmin; @@ -127,7 +127,7 @@ public class RabbitMqTransportQueueFactory implements TbTransportQueueFactory { } @Override - public TbQueueProducer> createToUsageStatsServiceMsgProducer() { + public TbQueueProducer> createToUsageStatsServiceMsgProducer() { return new TbRabbitMqProducerTemplate<>(coreAdmin, rabbitMqSettings, topicService.buildTopicName(coreSettings.getUsageStatsTopic())); } diff --git a/common/queue/src/main/java/org/thingsboard/server/queue/provider/ServiceBusMonolithQueueFactory.java b/common/queue/src/main/java/org/thingsboard/server/queue/provider/ServiceBusMonolithQueueFactory.java index 932bbd21e5..c0e6977b35 100644 --- a/common/queue/src/main/java/org/thingsboard/server/queue/provider/ServiceBusMonolithQueueFactory.java +++ b/common/queue/src/main/java/org/thingsboard/server/queue/provider/ServiceBusMonolithQueueFactory.java @@ -33,7 +33,7 @@ import org.thingsboard.server.gen.transport.TransportProtos.ToOtaPackageStateSer import org.thingsboard.server.gen.transport.TransportProtos.ToRuleEngineMsg; import org.thingsboard.server.gen.transport.TransportProtos.ToRuleEngineNotificationMsg; import org.thingsboard.server.gen.transport.TransportProtos.ToTransportMsg; -import org.thingsboard.server.gen.transport.TransportProtos.ToUsageStatsServiceMsg; +import org.thingsboard.server.gen.transport.TransportProtos.ToUsageStatsServiceMsgPack; import org.thingsboard.server.gen.transport.TransportProtos.ToVersionControlServiceMsg; import org.thingsboard.server.gen.transport.TransportProtos.TransportApiRequestMsg; import org.thingsboard.server.gen.transport.TransportProtos.TransportApiResponseMsg; @@ -208,9 +208,9 @@ public class ServiceBusMonolithQueueFactory implements TbCoreQueueFactory, TbRul } @Override - public TbQueueConsumer> createToUsageStatsServiceMsgConsumer() { + public TbQueueConsumer> createToUsageStatsServiceMsgConsumer() { return new TbServiceBusConsumerTemplate<>(coreAdmin, serviceBusSettings, topicService.buildTopicName(coreSettings.getUsageStatsTopic()), - msg -> new TbProtoQueueMsg<>(msg.getKey(), ToUsageStatsServiceMsg.parseFrom(msg.getData()), msg.getHeaders())); + msg -> new TbProtoQueueMsg<>(msg.getKey(), ToUsageStatsServiceMsgPack.parseFrom(msg.getData()), msg.getHeaders())); } @Override @@ -225,7 +225,7 @@ public class ServiceBusMonolithQueueFactory implements TbCoreQueueFactory, TbRul } @Override - public TbQueueProducer> createToUsageStatsServiceMsgProducer() { + public TbQueueProducer> createToUsageStatsServiceMsgProducer() { return new TbServiceBusProducerTemplate<>(coreAdmin, serviceBusSettings, topicService.buildTopicName(coreSettings.getUsageStatsTopic())); } diff --git a/common/queue/src/main/java/org/thingsboard/server/queue/provider/ServiceBusTbCoreQueueFactory.java b/common/queue/src/main/java/org/thingsboard/server/queue/provider/ServiceBusTbCoreQueueFactory.java index 71a7efe50b..7cc1183c80 100644 --- a/common/queue/src/main/java/org/thingsboard/server/queue/provider/ServiceBusTbCoreQueueFactory.java +++ b/common/queue/src/main/java/org/thingsboard/server/queue/provider/ServiceBusTbCoreQueueFactory.java @@ -31,7 +31,7 @@ import org.thingsboard.server.gen.transport.TransportProtos.ToOtaPackageStateSer import org.thingsboard.server.gen.transport.TransportProtos.ToRuleEngineMsg; import org.thingsboard.server.gen.transport.TransportProtos.ToRuleEngineNotificationMsg; import org.thingsboard.server.gen.transport.TransportProtos.ToTransportMsg; -import org.thingsboard.server.gen.transport.TransportProtos.ToUsageStatsServiceMsg; +import org.thingsboard.server.gen.transport.TransportProtos.ToUsageStatsServiceMsgPack; import org.thingsboard.server.gen.transport.TransportProtos.ToVersionControlServiceMsg; import org.thingsboard.server.gen.transport.TransportProtos.TransportApiRequestMsg; import org.thingsboard.server.gen.transport.TransportProtos.TransportApiResponseMsg; @@ -181,9 +181,9 @@ public class ServiceBusTbCoreQueueFactory implements TbCoreQueueFactory { } @Override - public TbQueueConsumer> createToUsageStatsServiceMsgConsumer() { + public TbQueueConsumer> createToUsageStatsServiceMsgConsumer() { return new TbServiceBusConsumerTemplate<>(coreAdmin, serviceBusSettings, topicService.buildTopicName(coreSettings.getUsageStatsTopic()), - msg -> new TbProtoQueueMsg<>(msg.getKey(), ToUsageStatsServiceMsg.parseFrom(msg.getData()), msg.getHeaders())); + msg -> new TbProtoQueueMsg<>(msg.getKey(), ToUsageStatsServiceMsgPack.parseFrom(msg.getData()), msg.getHeaders())); } @Override @@ -198,7 +198,7 @@ public class ServiceBusTbCoreQueueFactory implements TbCoreQueueFactory { } @Override - public TbQueueProducer> createToUsageStatsServiceMsgProducer() { + public TbQueueProducer> createToUsageStatsServiceMsgProducer() { return new TbServiceBusProducerTemplate<>(coreAdmin, serviceBusSettings, topicService.buildTopicName(coreSettings.getUsageStatsTopic())); } diff --git a/common/queue/src/main/java/org/thingsboard/server/queue/provider/ServiceBusTbRuleEngineQueueFactory.java b/common/queue/src/main/java/org/thingsboard/server/queue/provider/ServiceBusTbRuleEngineQueueFactory.java index 54661f695c..33fd286704 100644 --- a/common/queue/src/main/java/org/thingsboard/server/queue/provider/ServiceBusTbRuleEngineQueueFactory.java +++ b/common/queue/src/main/java/org/thingsboard/server/queue/provider/ServiceBusTbRuleEngineQueueFactory.java @@ -32,7 +32,7 @@ import org.thingsboard.server.gen.transport.TransportProtos.ToOtaPackageStateSer import org.thingsboard.server.gen.transport.TransportProtos.ToRuleEngineMsg; import org.thingsboard.server.gen.transport.TransportProtos.ToRuleEngineNotificationMsg; import org.thingsboard.server.gen.transport.TransportProtos.ToTransportMsg; -import org.thingsboard.server.gen.transport.TransportProtos.ToUsageStatsServiceMsg; +import org.thingsboard.server.gen.transport.TransportProtos.ToUsageStatsServiceMsgPack; import org.thingsboard.server.queue.TbQueueAdmin; import org.thingsboard.server.queue.TbQueueConsumer; import org.thingsboard.server.queue.TbQueueProducer; @@ -170,7 +170,7 @@ public class ServiceBusTbRuleEngineQueueFactory implements TbRuleEngineQueueFact } @Override - public TbQueueProducer> createToUsageStatsServiceMsgProducer() { + public TbQueueProducer> createToUsageStatsServiceMsgProducer() { return new TbServiceBusProducerTemplate<>(coreAdmin, serviceBusSettings, topicService.buildTopicName(coreSettings.getUsageStatsTopic())); } diff --git a/common/queue/src/main/java/org/thingsboard/server/queue/provider/ServiceBusTbVersionControlQueueFactory.java b/common/queue/src/main/java/org/thingsboard/server/queue/provider/ServiceBusTbVersionControlQueueFactory.java index 0c363488ce..2f02f530c1 100644 --- a/common/queue/src/main/java/org/thingsboard/server/queue/provider/ServiceBusTbVersionControlQueueFactory.java +++ b/common/queue/src/main/java/org/thingsboard/server/queue/provider/ServiceBusTbVersionControlQueueFactory.java @@ -62,7 +62,7 @@ public class ServiceBusTbVersionControlQueueFactory implements TbVersionControlQ } @Override - public TbQueueProducer> createToUsageStatsServiceMsgProducer() { + public TbQueueProducer> createToUsageStatsServiceMsgProducer() { return new TbServiceBusProducerTemplate<>(coreAdmin, serviceBusSettings, topicService.buildTopicName(coreSettings.getUsageStatsTopic())); } diff --git a/common/queue/src/main/java/org/thingsboard/server/queue/provider/ServiceBusTransportQueueFactory.java b/common/queue/src/main/java/org/thingsboard/server/queue/provider/ServiceBusTransportQueueFactory.java index 7f3e246eec..1481ec58ac 100644 --- a/common/queue/src/main/java/org/thingsboard/server/queue/provider/ServiceBusTransportQueueFactory.java +++ b/common/queue/src/main/java/org/thingsboard/server/queue/provider/ServiceBusTransportQueueFactory.java @@ -24,7 +24,7 @@ import org.thingsboard.server.gen.transport.TransportProtos.ToCoreNotificationMs import org.thingsboard.server.gen.transport.TransportProtos.ToHousekeeperServiceMsg; import org.thingsboard.server.gen.transport.TransportProtos.ToRuleEngineMsg; import org.thingsboard.server.gen.transport.TransportProtos.ToTransportMsg; -import org.thingsboard.server.gen.transport.TransportProtos.ToUsageStatsServiceMsg; +import org.thingsboard.server.gen.transport.TransportProtos.ToUsageStatsServiceMsgPack; import org.thingsboard.server.gen.transport.TransportProtos.TransportApiRequestMsg; import org.thingsboard.server.gen.transport.TransportProtos.TransportApiResponseMsg; import org.thingsboard.server.queue.TbQueueAdmin; @@ -128,7 +128,7 @@ public class ServiceBusTransportQueueFactory implements TbTransportQueueFactory } @Override - public TbQueueProducer> createToUsageStatsServiceMsgProducer() { + public TbQueueProducer> createToUsageStatsServiceMsgProducer() { return new TbServiceBusProducerTemplate<>(coreAdmin, serviceBusSettings, topicService.buildTopicName(coreSettings.getUsageStatsTopic())); } diff --git a/common/queue/src/main/java/org/thingsboard/server/queue/provider/TbCoreQueueFactory.java b/common/queue/src/main/java/org/thingsboard/server/queue/provider/TbCoreQueueFactory.java index c4002f4d3e..b9ccfd1866 100644 --- a/common/queue/src/main/java/org/thingsboard/server/queue/provider/TbCoreQueueFactory.java +++ b/common/queue/src/main/java/org/thingsboard/server/queue/provider/TbCoreQueueFactory.java @@ -28,7 +28,7 @@ import org.thingsboard.server.gen.transport.TransportProtos.ToOtaPackageStateSer import org.thingsboard.server.gen.transport.TransportProtos.ToRuleEngineMsg; import org.thingsboard.server.gen.transport.TransportProtos.ToRuleEngineNotificationMsg; import org.thingsboard.server.gen.transport.TransportProtos.ToTransportMsg; -import org.thingsboard.server.gen.transport.TransportProtos.ToUsageStatsServiceMsg; +import org.thingsboard.server.gen.transport.TransportProtos.ToUsageStatsServiceMsgPack; import org.thingsboard.server.gen.transport.TransportProtos.ToVersionControlServiceMsg; import org.thingsboard.server.gen.transport.TransportProtos.TransportApiRequestMsg; import org.thingsboard.server.gen.transport.TransportProtos.TransportApiResponseMsg; @@ -91,7 +91,7 @@ public interface TbCoreQueueFactory extends TbUsageStatsClientQueueFactory, Hous * * @return */ - TbQueueConsumer> createToUsageStatsServiceMsgConsumer(); + TbQueueConsumer> createToUsageStatsServiceMsgConsumer(); /** * Used to consume messages about firmware update notifications by TB Core Service diff --git a/common/queue/src/main/java/org/thingsboard/server/queue/provider/TbCoreQueueProducerProvider.java b/common/queue/src/main/java/org/thingsboard/server/queue/provider/TbCoreQueueProducerProvider.java index 9cf18e6cb4..386a525b28 100644 --- a/common/queue/src/main/java/org/thingsboard/server/queue/provider/TbCoreQueueProducerProvider.java +++ b/common/queue/src/main/java/org/thingsboard/server/queue/provider/TbCoreQueueProducerProvider.java @@ -26,7 +26,7 @@ import org.thingsboard.server.gen.transport.TransportProtos.ToHousekeeperService import org.thingsboard.server.gen.transport.TransportProtos.ToRuleEngineMsg; import org.thingsboard.server.gen.transport.TransportProtos.ToRuleEngineNotificationMsg; import org.thingsboard.server.gen.transport.TransportProtos.ToTransportMsg; -import org.thingsboard.server.gen.transport.TransportProtos.ToUsageStatsServiceMsg; +import org.thingsboard.server.gen.transport.TransportProtos.ToUsageStatsServiceMsgPack; import org.thingsboard.server.gen.transport.TransportProtos.ToVersionControlServiceMsg; import org.thingsboard.server.queue.TbQueueProducer; import org.thingsboard.server.queue.common.TbProtoQueueMsg; @@ -45,7 +45,7 @@ public class TbCoreQueueProducerProvider implements TbQueueProducerProvider { private TbQueueProducer> toEdge; private TbQueueProducer> toEdgeNotifications; private TbQueueProducer> toEdgeEvents; - private TbQueueProducer> toUsageStats; + private TbQueueProducer> toUsageStats; private TbQueueProducer> toVersionControl; private TbQueueProducer> toHousekeeper; @@ -94,7 +94,7 @@ public class TbCoreQueueProducerProvider implements TbQueueProducerProvider { } @Override - public TbQueueProducer> getTbUsageStatsMsgProducer() { + public TbQueueProducer> getTbUsageStatsMsgProducer() { return toUsageStats; } diff --git a/common/queue/src/main/java/org/thingsboard/server/queue/provider/TbQueueProducerProvider.java b/common/queue/src/main/java/org/thingsboard/server/queue/provider/TbQueueProducerProvider.java index ec31763baa..d5c011c7b9 100644 --- a/common/queue/src/main/java/org/thingsboard/server/queue/provider/TbQueueProducerProvider.java +++ b/common/queue/src/main/java/org/thingsboard/server/queue/provider/TbQueueProducerProvider.java @@ -24,7 +24,7 @@ import org.thingsboard.server.gen.transport.TransportProtos.ToHousekeeperService import org.thingsboard.server.gen.transport.TransportProtos.ToRuleEngineMsg; import org.thingsboard.server.gen.transport.TransportProtos.ToRuleEngineNotificationMsg; import org.thingsboard.server.gen.transport.TransportProtos.ToTransportMsg; -import org.thingsboard.server.gen.transport.TransportProtos.ToUsageStatsServiceMsg; +import org.thingsboard.server.gen.transport.TransportProtos.ToUsageStatsServiceMsgPack; import org.thingsboard.server.gen.transport.TransportProtos.ToVersionControlServiceMsg; import org.thingsboard.server.queue.TbQueueProducer; import org.thingsboard.server.queue.common.TbProtoQueueMsg; @@ -74,7 +74,7 @@ public interface TbQueueProducerProvider { * * @return */ - TbQueueProducer> getTbUsageStatsMsgProducer(); + TbQueueProducer> getTbUsageStatsMsgProducer(); /** * Used to push messages to other instances of TB Core Service diff --git a/common/queue/src/main/java/org/thingsboard/server/queue/provider/TbRuleEngineProducerProvider.java b/common/queue/src/main/java/org/thingsboard/server/queue/provider/TbRuleEngineProducerProvider.java index e9f7773a26..835b02ec8e 100644 --- a/common/queue/src/main/java/org/thingsboard/server/queue/provider/TbRuleEngineProducerProvider.java +++ b/common/queue/src/main/java/org/thingsboard/server/queue/provider/TbRuleEngineProducerProvider.java @@ -27,7 +27,7 @@ import org.thingsboard.server.gen.transport.TransportProtos.ToHousekeeperService import org.thingsboard.server.gen.transport.TransportProtos.ToRuleEngineMsg; import org.thingsboard.server.gen.transport.TransportProtos.ToRuleEngineNotificationMsg; import org.thingsboard.server.gen.transport.TransportProtos.ToTransportMsg; -import org.thingsboard.server.gen.transport.TransportProtos.ToUsageStatsServiceMsg; +import org.thingsboard.server.gen.transport.TransportProtos.ToUsageStatsServiceMsgPack; import org.thingsboard.server.gen.transport.TransportProtos.ToVersionControlServiceMsg; import org.thingsboard.server.queue.TbQueueProducer; import org.thingsboard.server.queue.common.TbProtoQueueMsg; @@ -42,7 +42,7 @@ public class TbRuleEngineProducerProvider implements TbQueueProducerProvider { private TbQueueProducer> toTbCore; private TbQueueProducer> toRuleEngineNotifications; private TbQueueProducer> toTbCoreNotifications; - private TbQueueProducer> toUsageStats; + private TbQueueProducer> toUsageStats; private TbQueueProducer> toHousekeeper; private TbQueueProducer> toEdge; private TbQueueProducer> toEdgeNotifications; @@ -107,7 +107,7 @@ public class TbRuleEngineProducerProvider implements TbQueueProducerProvider { } @Override - public TbQueueProducer> getTbUsageStatsMsgProducer() { + public TbQueueProducer> getTbUsageStatsMsgProducer() { return toUsageStats; } diff --git a/common/queue/src/main/java/org/thingsboard/server/queue/provider/TbTransportQueueProducerProvider.java b/common/queue/src/main/java/org/thingsboard/server/queue/provider/TbTransportQueueProducerProvider.java index 7960cbcf32..689d236de7 100644 --- a/common/queue/src/main/java/org/thingsboard/server/queue/provider/TbTransportQueueProducerProvider.java +++ b/common/queue/src/main/java/org/thingsboard/server/queue/provider/TbTransportQueueProducerProvider.java @@ -27,7 +27,7 @@ import org.thingsboard.server.gen.transport.TransportProtos.ToHousekeeperService import org.thingsboard.server.gen.transport.TransportProtos.ToRuleEngineMsg; import org.thingsboard.server.gen.transport.TransportProtos.ToRuleEngineNotificationMsg; import org.thingsboard.server.gen.transport.TransportProtos.ToTransportMsg; -import org.thingsboard.server.gen.transport.TransportProtos.ToUsageStatsServiceMsg; +import org.thingsboard.server.gen.transport.TransportProtos.ToUsageStatsServiceMsgPack; import org.thingsboard.server.gen.transport.TransportProtos.ToVersionControlServiceMsg; import org.thingsboard.server.queue.TbQueueProducer; import org.thingsboard.server.queue.common.TbProtoQueueMsg; @@ -40,7 +40,7 @@ public class TbTransportQueueProducerProvider implements TbQueueProducerProvider private TbQueueProducer> toRuleEngine; private TbQueueProducer> toTbCore; private TbQueueProducer> toTbCoreNotifications; - private TbQueueProducer> toUsageStats; + private TbQueueProducer> toUsageStats; private TbQueueProducer> toHousekeeper; public TbTransportQueueProducerProvider(TbTransportQueueFactory tbQueueProvider) { @@ -102,7 +102,7 @@ public class TbTransportQueueProducerProvider implements TbQueueProducerProvider } @Override - public TbQueueProducer> getTbUsageStatsMsgProducer() { + public TbQueueProducer> getTbUsageStatsMsgProducer() { return toUsageStats; } diff --git a/common/queue/src/main/java/org/thingsboard/server/queue/provider/TbUsageStatsClientQueueFactory.java b/common/queue/src/main/java/org/thingsboard/server/queue/provider/TbUsageStatsClientQueueFactory.java index 0c551fc998..c4c5f5215a 100644 --- a/common/queue/src/main/java/org/thingsboard/server/queue/provider/TbUsageStatsClientQueueFactory.java +++ b/common/queue/src/main/java/org/thingsboard/server/queue/provider/TbUsageStatsClientQueueFactory.java @@ -15,12 +15,12 @@ */ package org.thingsboard.server.queue.provider; -import org.thingsboard.server.gen.transport.TransportProtos.ToUsageStatsServiceMsg; +import org.thingsboard.server.gen.transport.TransportProtos.ToUsageStatsServiceMsgPack; import org.thingsboard.server.queue.TbQueueProducer; import org.thingsboard.server.queue.common.TbProtoQueueMsg; public interface TbUsageStatsClientQueueFactory { - TbQueueProducer> createToUsageStatsServiceMsgProducer(); + TbQueueProducer> createToUsageStatsServiceMsgProducer(); } diff --git a/common/queue/src/main/java/org/thingsboard/server/queue/provider/TbVersionControlProducerProvider.java b/common/queue/src/main/java/org/thingsboard/server/queue/provider/TbVersionControlProducerProvider.java index cd4fa12df0..400ccfc085 100644 --- a/common/queue/src/main/java/org/thingsboard/server/queue/provider/TbVersionControlProducerProvider.java +++ b/common/queue/src/main/java/org/thingsboard/server/queue/provider/TbVersionControlProducerProvider.java @@ -27,7 +27,7 @@ import org.thingsboard.server.gen.transport.TransportProtos.ToHousekeeperService import org.thingsboard.server.gen.transport.TransportProtos.ToRuleEngineMsg; import org.thingsboard.server.gen.transport.TransportProtos.ToRuleEngineNotificationMsg; import org.thingsboard.server.gen.transport.TransportProtos.ToTransportMsg; -import org.thingsboard.server.gen.transport.TransportProtos.ToUsageStatsServiceMsg; +import org.thingsboard.server.gen.transport.TransportProtos.ToUsageStatsServiceMsgPack; import org.thingsboard.server.gen.transport.TransportProtos.ToVersionControlServiceMsg; import org.thingsboard.server.queue.TbQueueProducer; import org.thingsboard.server.queue.common.TbProtoQueueMsg; @@ -38,7 +38,7 @@ public class TbVersionControlProducerProvider implements TbQueueProducerProvider private final TbVersionControlQueueFactory tbQueueProvider; private TbQueueProducer> toTbCoreNotifications; - private TbQueueProducer> toUsageStats; + private TbQueueProducer> toUsageStats; private TbQueueProducer> toHousekeeper; public TbVersionControlProducerProvider(TbVersionControlQueueFactory tbQueueProvider) { @@ -98,7 +98,7 @@ public class TbVersionControlProducerProvider implements TbQueueProducerProvider } @Override - public TbQueueProducer> getTbUsageStatsMsgProducer() { + public TbQueueProducer> getTbUsageStatsMsgProducer() { return toUsageStats; } diff --git a/common/queue/src/main/java/org/thingsboard/server/queue/usagestats/DefaultTbApiUsageReportClient.java b/common/queue/src/main/java/org/thingsboard/server/queue/usagestats/DefaultTbApiUsageReportClient.java index e8502145ec..10ea5b13a0 100644 --- a/common/queue/src/main/java/org/thingsboard/server/queue/usagestats/DefaultTbApiUsageReportClient.java +++ b/common/queue/src/main/java/org/thingsboard/server/queue/usagestats/DefaultTbApiUsageReportClient.java @@ -29,7 +29,10 @@ import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.common.msg.queue.ServiceType; import org.thingsboard.server.common.msg.queue.TopicPartitionInfo; import org.thingsboard.server.common.stats.TbApiUsageReportClient; +import org.thingsboard.server.common.util.ProtoUtils; +import org.thingsboard.server.gen.transport.TransportProtos; import org.thingsboard.server.gen.transport.TransportProtos.ToUsageStatsServiceMsg; +import org.thingsboard.server.gen.transport.TransportProtos.ToUsageStatsServiceMsgPack; import org.thingsboard.server.gen.transport.TransportProtos.UsageStatsKVProto; import org.thingsboard.server.queue.TbQueueProducer; import org.thingsboard.server.queue.common.TbProtoQueueMsg; @@ -38,13 +41,18 @@ import org.thingsboard.server.queue.discovery.TbServiceInfoProvider; import org.thingsboard.server.queue.provider.TbQueueProducerProvider; import org.thingsboard.server.queue.scheduler.SchedulerComponent; +import java.util.ArrayList; import java.util.EnumMap; +import java.util.HashMap; +import java.util.List; +import java.util.Map; import java.util.Random; import java.util.UUID; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.ConcurrentMap; import java.util.concurrent.TimeUnit; import java.util.concurrent.atomic.AtomicLong; +import java.util.stream.IntStream; @Component @Slf4j @@ -57,6 +65,8 @@ public class DefaultTbApiUsageReportClient implements TbApiUsageReportClient { private boolean enabledPerCustomer; @Value("${usage.stats.report.interval:10}") private int interval; + @Value("${usage.stats.report.pack_size:1024}") + private int packSize; private final EnumMap> stats = new EnumMap<>(ApiUsageRecordKey.class); @@ -64,7 +74,7 @@ public class DefaultTbApiUsageReportClient implements TbApiUsageReportClient { private final TbServiceInfoProvider serviceInfoProvider; private final SchedulerComponent scheduler; private final TbQueueProducerProvider producerProvider; - private TbQueueProducer> msgProducer; + private TbQueueProducer> msgProducer; @PostConstruct private void init() { @@ -110,31 +120,50 @@ public class DefaultTbApiUsageReportClient implements TbApiUsageReportClient { }); UsageStatsKVProto.Builder statsItem = UsageStatsKVProto.newBuilder() - .setKey(key.name()) + .setKey(ProtoUtils.toProto(key)) .setValue(value); statsMsg.addValues(statsItem.build()); }); statsForKey.clear(); } - report.forEach(((parent, statsMsg) -> { - //TODO: figure out how to minimize messages into the queue. Maybe group by 100s of messages? + Map> reportStatsPerTpi = new HashMap<>(); + + report.forEach((parent, statsMsg) -> { try { TopicPartitionInfo tpi = partitionService.resolve(ServiceType.TB_CORE, parent.getTenantId(), parent.getId()) .newByTopic(msgProducer.getDefaultTopic()); - msgProducer.send(tpi, new TbProtoQueueMsg<>(UUID.randomUUID(), statsMsg.build()), null); + reportStatsPerTpi.computeIfAbsent(tpi, k -> new ArrayList<>()).add(statsMsg.build()); } catch (TenantNotFoundException e) { log.debug("Couldn't report usage stats for non-existing tenant: {}", e.getTenantId()); - } catch (Exception e) { - log.warn("Failed to report usage stats for tenant {}", parent.getTenantId(), e); } - })); + }); + + reportStatsPerTpi.forEach((tpi, statsList) -> { + toMsgPack(statsList).forEach(pack -> { + try { + msgProducer.send(tpi, new TbProtoQueueMsg<>(UUID.randomUUID(), pack), null); + } catch (Exception e) { + log.warn("Failed to report usage stats pack to TPI {}", tpi, e); + } + }); + }); if (!report.isEmpty()) { log.debug("Reporting API usage statistics for {} tenants and customers", report.size()); } } + private List toMsgPack(List list) { + return IntStream.range(0, (list.size() + packSize - 1) / packSize) + .mapToObj(i -> { + var packList = list.subList(i * packSize, Math.min((i + 1) * packSize, list.size())); + var pack = ToUsageStatsServiceMsgPack.newBuilder(); + pack.addAllMsgs(packList); + return pack.build(); + }).toList(); + } + @Override public void report(TenantId tenantId, CustomerId customerId, ApiUsageRecordKey key, long value) { if (!enabled) return; diff --git a/msa/vc-executor/src/main/resources/tb-vc-executor.yml b/msa/vc-executor/src/main/resources/tb-vc-executor.yml index 6b79f2d101..32cdb6085f 100644 --- a/msa/vc-executor/src/main/resources/tb-vc-executor.yml +++ b/msa/vc-executor/src/main/resources/tb-vc-executor.yml @@ -289,6 +289,8 @@ usage: enabled_per_customer: "${USAGE_STATS_REPORT_PER_CUSTOMER_ENABLED:false}" # Interval of reporting the statistics. By default, the summarized statistics are sent every 10 seconds interval: "${USAGE_STATS_REPORT_INTERVAL:10}" + # Amount of statistic messages in pack + pack_size: "${USAGE_STATS_REPORT_PACK_SIZE:1024}" # Metrics parameters metrics: diff --git a/transport/coap/src/main/resources/tb-coap-transport.yml b/transport/coap/src/main/resources/tb-coap-transport.yml index 2c1c0550e1..23ddc2efc5 100644 --- a/transport/coap/src/main/resources/tb-coap-transport.yml +++ b/transport/coap/src/main/resources/tb-coap-transport.yml @@ -478,6 +478,8 @@ usage: enabled_per_customer: "${USAGE_STATS_REPORT_PER_CUSTOMER_ENABLED:false}" # Interval of reporting the statistics. By default, the summarized statistics are sent every 10 seconds interval: "${USAGE_STATS_REPORT_INTERVAL:10}" + # Amount of statistic messages in pack + pack_size: "${USAGE_STATS_REPORT_PACK_SIZE:1024}" # Metrics parameters metrics: diff --git a/transport/http/src/main/resources/tb-http-transport.yml b/transport/http/src/main/resources/tb-http-transport.yml index 3c28c6ddf6..bc0cea209e 100644 --- a/transport/http/src/main/resources/tb-http-transport.yml +++ b/transport/http/src/main/resources/tb-http-transport.yml @@ -448,6 +448,8 @@ usage: enabled_per_customer: "${USAGE_STATS_REPORT_PER_CUSTOMER_ENABLED:false}" # Interval of reporting the statistics. By default, the summarized statistics are sent every 10 seconds interval: "${USAGE_STATS_REPORT_INTERVAL:10}" + # Amount of statistic messages in pack + pack_size: "${USAGE_STATS_REPORT_PACK_SIZE:1024}" # Metrics parameters metrics: diff --git a/transport/lwm2m/src/main/resources/tb-lwm2m-transport.yml b/transport/lwm2m/src/main/resources/tb-lwm2m-transport.yml index bafe77ee92..c2c0bf8e93 100644 --- a/transport/lwm2m/src/main/resources/tb-lwm2m-transport.yml +++ b/transport/lwm2m/src/main/resources/tb-lwm2m-transport.yml @@ -548,6 +548,8 @@ usage: enabled_per_customer: "${USAGE_STATS_REPORT_PER_CUSTOMER_ENABLED:false}" # Interval of reporting the statistics. By default, the summarized statistics are sent every 10 seconds interval: "${USAGE_STATS_REPORT_INTERVAL:10}" + # Amount of statistic messages in pack + pack_size: "${USAGE_STATS_REPORT_PACK_SIZE:1024}" # Metrics parameters metrics: diff --git a/transport/mqtt/src/main/resources/tb-mqtt-transport.yml b/transport/mqtt/src/main/resources/tb-mqtt-transport.yml index 51a2c173a7..88bd34280d 100644 --- a/transport/mqtt/src/main/resources/tb-mqtt-transport.yml +++ b/transport/mqtt/src/main/resources/tb-mqtt-transport.yml @@ -481,6 +481,8 @@ usage: enabled_per_customer: "${USAGE_STATS_REPORT_PER_CUSTOMER_ENABLED:false}" # Interval of reporting the statistics. By default, the summarized statistics are sent every 10 seconds interval: "${USAGE_STATS_REPORT_INTERVAL:10}" + # Amount of statistic messages in pack + pack_size: "${USAGE_STATS_REPORT_PACK_SIZE:1024}" # Metrics parameters metrics: diff --git a/transport/snmp/src/main/resources/tb-snmp-transport.yml b/transport/snmp/src/main/resources/tb-snmp-transport.yml index df222fe09f..0e587b6028 100644 --- a/transport/snmp/src/main/resources/tb-snmp-transport.yml +++ b/transport/snmp/src/main/resources/tb-snmp-transport.yml @@ -434,6 +434,8 @@ usage: enabled_per_customer: "${USAGE_STATS_REPORT_PER_CUSTOMER_ENABLED:false}" # Interval of reporting the statistics. By default, the summarized statistics are sent every 10 seconds interval: "${USAGE_STATS_REPORT_INTERVAL:10}" + # Amount of statistic messages in pack + pack_size: "${USAGE_STATS_REPORT_PACK_SIZE:1024}" # Metrics parameters metrics: From 0cc55ab9898f0a1dbadf08ae53158ffc81ab25dd Mon Sep 17 00:00:00 2001 From: YevhenBondarenko Date: Tue, 10 Dec 2024 12:26:44 +0100 Subject: [PATCH 005/345] added tbel metrics, refactored JS stats --- .../server/actors/ActorSystemContext.java | 16 --- .../actors/ruleChain/DefaultTbContext.java | 21 ---- .../service/stats/DefaultJsInvokeStats.java | 83 ---------------- .../src/main/resources/thingsboard.yml | 2 - .../server/actors/JsInvokeStats.java | 44 --------- .../api/AbstractScriptInvokeService.java | 54 +++++++--- .../script/api/ScriptStatCallback.java | 14 +-- .../api/js/AbstractJsInvokeService.java | 5 + .../api/tbel/DefaultTbelInvokeService.java | 6 ++ .../server/common/stats/DefaultCounter.java | 4 + .../server/common/stats/StatsType.java | 1 + .../dashboards/core_and_js_metrics.json | 98 ++++++++++++++++++- .../rule/engine/api/TbContext.java | 6 -- .../rule/engine/action/TbClearAlarmNode.java | 2 - .../rule/engine/action/TbCreateAlarmNode.java | 8 -- .../rule/engine/action/TbLogNode.java | 3 - .../rule/engine/debug/TbMsgGeneratorNode.java | 2 - .../rule/engine/filter/TbJsFilterNode.java | 3 - .../rule/engine/filter/TbJsSwitchNode.java | 3 - .../engine/transform/TbTransformMsgNode.java | 2 - .../engine/action/TbCreateAlarmNodeTest.java | 20 +--- 21 files changed, 162 insertions(+), 235 deletions(-) delete mode 100644 application/src/main/java/org/thingsboard/server/service/stats/DefaultJsInvokeStats.java delete mode 100644 common/actor/src/main/java/org/thingsboard/server/actors/JsInvokeStats.java diff --git a/application/src/main/java/org/thingsboard/server/actors/ActorSystemContext.java b/application/src/main/java/org/thingsboard/server/actors/ActorSystemContext.java index 1c18a31f68..28f743d1b5 100644 --- a/application/src/main/java/org/thingsboard/server/actors/ActorSystemContext.java +++ b/application/src/main/java/org/thingsboard/server/actors/ActorSystemContext.java @@ -28,7 +28,6 @@ import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Value; import org.springframework.context.annotation.Lazy; import org.springframework.data.redis.core.RedisTemplate; -import org.springframework.scheduling.annotation.Scheduled; import org.springframework.stereotype.Component; import org.thingsboard.common.util.JacksonUtil; import org.thingsboard.rule.engine.api.MailService; @@ -399,10 +398,6 @@ public class ActorSystemContext { @Getter private ClaimDevicesService claimDevicesService; - @Autowired - @Getter - private JsInvokeStats jsInvokeStats; - //TODO: separate context for TbCore and TbRuleEngine @Autowired(required = false) @Getter @@ -527,17 +522,6 @@ public class ActorSystemContext { this.localCacheType = "caffeine".equals(cacheType); } - @Scheduled(fixedDelayString = "${actors.statistics.js_print_interval_ms}") - public void printStats() { - if (statisticsEnabled) { - if (jsInvokeStats.getRequests() > 0 || jsInvokeStats.getResponses() > 0 || jsInvokeStats.getFailures() > 0) { - log.info("Rule Engine JS Invoke Stats: requests [{}] responses [{}] failures [{}]", - jsInvokeStats.getRequests(), jsInvokeStats.getResponses(), jsInvokeStats.getFailures()); - jsInvokeStats.reset(); - } - } - } - @Value("${actors.tenant.create_components_on_init:true}") @Getter private boolean tenantComponentsInitEnabled; diff --git a/application/src/main/java/org/thingsboard/server/actors/ruleChain/DefaultTbContext.java b/application/src/main/java/org/thingsboard/server/actors/ruleChain/DefaultTbContext.java index 0070eba4a1..2be01c57b7 100644 --- a/application/src/main/java/org/thingsboard/server/actors/ruleChain/DefaultTbContext.java +++ b/application/src/main/java/org/thingsboard/server/actors/ruleChain/DefaultTbContext.java @@ -608,27 +608,6 @@ public class DefaultTbContext implements TbContext { } } - @Override - public void logJsEvalRequest() { - if (mainCtx.isStatisticsEnabled()) { - mainCtx.getJsInvokeStats().incrementRequests(); - } - } - - @Override - public void logJsEvalResponse() { - if (mainCtx.isStatisticsEnabled()) { - mainCtx.getJsInvokeStats().incrementResponses(); - } - } - - @Override - public void logJsEvalFailure() { - if (mainCtx.isStatisticsEnabled()) { - mainCtx.getJsInvokeStats().incrementFailures(); - } - } - @Override public String getServiceId() { return mainCtx.getServiceInfoProvider().getServiceId(); diff --git a/application/src/main/java/org/thingsboard/server/service/stats/DefaultJsInvokeStats.java b/application/src/main/java/org/thingsboard/server/service/stats/DefaultJsInvokeStats.java deleted file mode 100644 index 3fc389b3d7..0000000000 --- a/application/src/main/java/org/thingsboard/server/service/stats/DefaultJsInvokeStats.java +++ /dev/null @@ -1,83 +0,0 @@ -/** - * Copyright © 2016-2024 The Thingsboard Authors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package org.thingsboard.server.service.stats; - -import jakarta.annotation.PostConstruct; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.stereotype.Service; -import org.thingsboard.server.actors.JsInvokeStats; -import org.thingsboard.server.common.stats.StatsCounter; -import org.thingsboard.server.common.stats.StatsFactory; -import org.thingsboard.server.common.stats.StatsType; - -@Service -public class DefaultJsInvokeStats implements JsInvokeStats { - private static final String REQUESTS = "requests"; - private static final String RESPONSES = "responses"; - private static final String FAILURES = "failures"; - - private StatsCounter requestsCounter; - private StatsCounter responsesCounter; - private StatsCounter failuresCounter; - - @Autowired - private StatsFactory statsFactory; - - @PostConstruct - public void init() { - String key = StatsType.JS_INVOKE.getName(); - this.requestsCounter = statsFactory.createStatsCounter(key, REQUESTS); - this.responsesCounter = statsFactory.createStatsCounter(key, RESPONSES); - this.failuresCounter = statsFactory.createStatsCounter(key, FAILURES); - } - - @Override - public void incrementRequests(int amount) { - requestsCounter.add(amount); - } - - @Override - public void incrementResponses(int amount) { - responsesCounter.add(amount); - } - - @Override - public void incrementFailures(int amount) { - failuresCounter.add(amount); - } - - @Override - public int getRequests() { - return requestsCounter.get(); - } - - @Override - public int getResponses() { - return responsesCounter.get(); - } - - @Override - public int getFailures() { - return failuresCounter.get(); - } - - @Override - public void reset() { - requestsCounter.clear(); - responsesCounter.clear(); - failuresCounter.clear(); - } -} diff --git a/application/src/main/resources/thingsboard.yml b/application/src/main/resources/thingsboard.yml index ef6d2204d8..fcd61599d3 100644 --- a/application/src/main/resources/thingsboard.yml +++ b/application/src/main/resources/thingsboard.yml @@ -500,8 +500,6 @@ actors: statistics: # Enable/disable actor statistics enabled: "${ACTORS_STATISTICS_ENABLED:true}" - # Frequency of printing the JS executor statistics - js_print_interval_ms: "${ACTORS_JS_STATISTICS_PRINT_INTERVAL_MS:10000}" # Actors statistic persistence frequency in milliseconds persist_frequency: "${ACTORS_STATISTICS_PERSIST_FREQUENCY:3600000}" diff --git a/common/actor/src/main/java/org/thingsboard/server/actors/JsInvokeStats.java b/common/actor/src/main/java/org/thingsboard/server/actors/JsInvokeStats.java deleted file mode 100644 index 5a175ed761..0000000000 --- a/common/actor/src/main/java/org/thingsboard/server/actors/JsInvokeStats.java +++ /dev/null @@ -1,44 +0,0 @@ -/** - * Copyright © 2016-2024 The Thingsboard Authors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package org.thingsboard.server.actors; - -public interface JsInvokeStats { - default void incrementRequests() { - incrementRequests(1); - } - - void incrementRequests(int amount); - - default void incrementResponses() { - incrementResponses(1); - } - - void incrementResponses(int amount); - - default void incrementFailures() { - incrementFailures(1); - } - - void incrementFailures(int amount); - - int getRequests(); - - int getResponses(); - - int getFailures(); - - void reset(); -} diff --git a/common/script/script-api/src/main/java/org/thingsboard/script/api/AbstractScriptInvokeService.java b/common/script/script-api/src/main/java/org/thingsboard/script/api/AbstractScriptInvokeService.java index 342801f9a8..e778201ee3 100644 --- a/common/script/script-api/src/main/java/org/thingsboard/script/api/AbstractScriptInvokeService.java +++ b/common/script/script-api/src/main/java/org/thingsboard/script/api/AbstractScriptInvokeService.java @@ -20,10 +20,14 @@ import com.google.common.util.concurrent.Futures; import com.google.common.util.concurrent.ListenableFuture; import com.google.common.util.concurrent.MoreExecutors; import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Autowired; import org.thingsboard.common.util.JacksonUtil; import org.thingsboard.common.util.ThingsBoardExecutors; import org.thingsboard.server.common.data.id.CustomerId; import org.thingsboard.server.common.data.id.TenantId; +import org.thingsboard.server.common.stats.StatsCounter; +import org.thingsboard.server.common.stats.StatsFactory; +import org.thingsboard.server.common.stats.StatsType; import java.util.Map; import java.util.UUID; @@ -32,22 +36,31 @@ import java.util.concurrent.Executor; import java.util.concurrent.ScheduledExecutorService; import java.util.concurrent.TimeUnit; import java.util.concurrent.TimeoutException; -import java.util.concurrent.atomic.AtomicInteger; import static java.lang.String.format; @Slf4j public abstract class AbstractScriptInvokeService implements ScriptInvokeService { + private static final String REQUESTS = "requests"; + private static final String INVOKE_RESPONSES = "invoke_responses"; + private static final String EVAL_RESPONSES = "eval_responses"; + private static final String FAILURES = "failures"; + private static final String TIMEOUTS = "timeouts"; + protected final Map disabledScripts = new ConcurrentHashMap<>(); - private final AtomicInteger pushedMsgs = new AtomicInteger(0); - private final AtomicInteger invokeMsgs = new AtomicInteger(0); - private final AtomicInteger evalMsgs = new AtomicInteger(0); - protected final AtomicInteger failedMsgs = new AtomicInteger(0); - protected final AtomicInteger timeoutMsgs = new AtomicInteger(0); - private final FutureCallback evalCallback = new ScriptStatCallback<>(evalMsgs, timeoutMsgs, failedMsgs); - private final FutureCallback invokeCallback = new ScriptStatCallback<>(invokeMsgs, timeoutMsgs, failedMsgs); + private StatsCounter requestsCounter; + private StatsCounter invokeResponsesCounter; + private StatsCounter evalResponsesCounter; + private StatsCounter failuresCounter; + private StatsCounter timeoutsCounter; + + private FutureCallback evalCallback; + private FutureCallback invokeCallback; + + @Autowired + private StatsFactory statsFactory; protected ScheduledExecutorService timeoutExecutorService; @@ -76,6 +89,7 @@ public abstract class AbstractScriptInvokeService implements ScriptInvokeService protected abstract boolean isScriptPresent(UUID scriptId); protected abstract boolean isExecEnabled(TenantId tenantId); + protected abstract void reportExecution(TenantId tenantId, CustomerId customerId); protected abstract ListenableFuture doEvalScript(TenantId tenantId, ScriptType scriptType, String scriptBody, UUID scriptId, String[] argNames); @@ -85,6 +99,14 @@ public abstract class AbstractScriptInvokeService implements ScriptInvokeService protected abstract void doRelease(UUID scriptId) throws Exception; public void init() { + String key = getStatsType().getName(); + this.requestsCounter = statsFactory.createStatsCounter(key, REQUESTS); + this.invokeResponsesCounter = statsFactory.createStatsCounter(key, INVOKE_RESPONSES); + this.evalResponsesCounter = statsFactory.createStatsCounter(key, EVAL_RESPONSES); + this.failuresCounter = statsFactory.createStatsCounter(key, FAILURES); + this.timeoutsCounter = statsFactory.createStatsCounter(key, TIMEOUTS); + this.evalCallback = new ScriptStatCallback<>(evalResponsesCounter, timeoutsCounter, failuresCounter); + this.invokeCallback = new ScriptStatCallback<>(invokeResponsesCounter, timeoutsCounter, failuresCounter); if (getMaxEvalRequestsTimeout() > 0 || getMaxInvokeRequestsTimeout() > 0) { timeoutExecutorService = ThingsBoardExecutors.newSingleThreadScheduledExecutor("script-timeout"); } @@ -98,11 +120,11 @@ public abstract class AbstractScriptInvokeService implements ScriptInvokeService public void printStats() { if (isStatsEnabled()) { - int pushed = pushedMsgs.getAndSet(0); - int invoked = invokeMsgs.getAndSet(0); - int evaluated = evalMsgs.getAndSet(0); - int failed = failedMsgs.getAndSet(0); - int timedOut = timeoutMsgs.getAndSet(0); + int pushed = requestsCounter.getAndClear(); + int invoked = invokeResponsesCounter.getAndClear(); + int evaluated = evalResponsesCounter.getAndClear(); + int failed = failuresCounter.getAndClear(); + int timedOut = timeoutsCounter.getAndClear(); if (pushed > 0 || invoked > 0 || evaluated > 0 || failed > 0 || timedOut > 0) { log.info("{}: pushed [{}] received [{}] invoke [{}] eval [{}] failed [{}] timedOut [{}]", getStatsName(), pushed, invoked + evaluated, invoked, evaluated, failed, timedOut); @@ -117,7 +139,7 @@ public abstract class AbstractScriptInvokeService implements ScriptInvokeService return error(format("Script body exceeds maximum allowed size of %s symbols", getMaxScriptBodySize())); } UUID scriptId = UUID.randomUUID(); - pushedMsgs.incrementAndGet(); + requestsCounter.increment(); return withTimeoutAndStatsCallback(scriptId, null, doEvalScript(tenantId, scriptType, scriptBody, scriptId, argNames), evalCallback, getMaxEvalRequestsTimeout()); } else { @@ -139,7 +161,7 @@ public abstract class AbstractScriptInvokeService implements ScriptInvokeService return Futures.immediateFailedFuture(handleScriptException(scriptId, null, t)); } reportExecution(tenantId, customerId); - pushedMsgs.incrementAndGet(); + requestsCounter.increment(); log.trace("[{}] InvokeScript uuid {} with timeout {}ms", tenantId, scriptId, getMaxInvokeRequestsTimeout()); var task = doInvokeFunction(scriptId, args); @@ -274,4 +296,6 @@ public abstract class AbstractScriptInvokeService implements ScriptInvokeService private ListenableFuture error(String message) { return Futures.immediateFailedFuture(new RuntimeException(message)); } + + protected abstract StatsType getStatsType(); } diff --git a/common/script/script-api/src/main/java/org/thingsboard/script/api/ScriptStatCallback.java b/common/script/script-api/src/main/java/org/thingsboard/script/api/ScriptStatCallback.java index 0e5fb94a89..308a6858ec 100644 --- a/common/script/script-api/src/main/java/org/thingsboard/script/api/ScriptStatCallback.java +++ b/common/script/script-api/src/main/java/org/thingsboard/script/api/ScriptStatCallback.java @@ -19,29 +19,29 @@ import com.google.common.util.concurrent.FutureCallback; import jakarta.annotation.Nullable; import lombok.AllArgsConstructor; import lombok.extern.slf4j.Slf4j; +import org.thingsboard.server.common.stats.StatsCounter; import java.util.concurrent.TimeoutException; -import java.util.concurrent.atomic.AtomicInteger; @Slf4j @AllArgsConstructor public class ScriptStatCallback implements FutureCallback { - private final AtomicInteger successMsgs; - private final AtomicInteger timeoutMsgs; - private final AtomicInteger failedMsgs; + private final StatsCounter successMsgs; + private final StatsCounter timeoutMsgs; + private final StatsCounter failedMsgs; @Override public void onSuccess(@Nullable T result) { - successMsgs.incrementAndGet(); + successMsgs.increment(); } @Override public void onFailure(Throwable t) { if (t instanceof TimeoutException || (t.getCause() != null && t.getCause() instanceof TimeoutException)) { - timeoutMsgs.incrementAndGet(); + timeoutMsgs.increment(); } else { - failedMsgs.incrementAndGet(); + failedMsgs.increment(); } } } diff --git a/common/script/script-api/src/main/java/org/thingsboard/script/api/js/AbstractJsInvokeService.java b/common/script/script-api/src/main/java/org/thingsboard/script/api/js/AbstractJsInvokeService.java index 6849f9d1fa..6ef699378e 100644 --- a/common/script/script-api/src/main/java/org/thingsboard/script/api/js/AbstractJsInvokeService.java +++ b/common/script/script-api/src/main/java/org/thingsboard/script/api/js/AbstractJsInvokeService.java @@ -26,6 +26,7 @@ import org.thingsboard.script.api.ScriptType; import org.thingsboard.server.common.data.ApiUsageRecordKey; import org.thingsboard.server.common.data.id.CustomerId; import org.thingsboard.server.common.data.id.TenantId; +import org.thingsboard.server.common.stats.StatsType; import org.thingsboard.server.common.stats.TbApiUsageReportClient; import org.thingsboard.server.common.stats.TbApiUsageStateClient; @@ -117,4 +118,8 @@ public abstract class AbstractJsInvokeService extends AbstractScriptInvokeServic .hash().toString(); } + @Override + protected StatsType getStatsType() { + return StatsType.JS_INVOKE; + } } diff --git a/common/script/script-api/src/main/java/org/thingsboard/script/api/tbel/DefaultTbelInvokeService.java b/common/script/script-api/src/main/java/org/thingsboard/script/api/tbel/DefaultTbelInvokeService.java index 74d6a4e6f5..479f84591e 100644 --- a/common/script/script-api/src/main/java/org/thingsboard/script/api/tbel/DefaultTbelInvokeService.java +++ b/common/script/script-api/src/main/java/org/thingsboard/script/api/tbel/DefaultTbelInvokeService.java @@ -44,6 +44,7 @@ import org.thingsboard.script.api.TbScriptException; import org.thingsboard.server.common.data.ApiUsageRecordKey; import org.thingsboard.server.common.data.id.CustomerId; import org.thingsboard.server.common.data.id.TenantId; +import org.thingsboard.server.common.stats.StatsType; import org.thingsboard.server.common.stats.TbApiUsageReportClient; import org.thingsboard.server.common.stats.TbApiUsageStateClient; @@ -255,4 +256,9 @@ public class DefaultTbelInvokeService extends AbstractScriptInvokeService implem protected long getMaxEvalRequestsTimeout() { return maxInvokeRequestsTimeout * 2; } + + @Override + protected StatsType getStatsType() { + return StatsType.TBEL_INVOKE; + } } diff --git a/common/stats/src/main/java/org/thingsboard/server/common/stats/DefaultCounter.java b/common/stats/src/main/java/org/thingsboard/server/common/stats/DefaultCounter.java index 3334f2aabb..cd69ec04fe 100644 --- a/common/stats/src/main/java/org/thingsboard/server/common/stats/DefaultCounter.java +++ b/common/stats/src/main/java/org/thingsboard/server/common/stats/DefaultCounter.java @@ -41,6 +41,10 @@ public class DefaultCounter { return aiCounter.get(); } + public int getAndClear() { + return aiCounter.getAndSet(0); + } + public void add(int delta){ aiCounter.addAndGet(delta); micrometerCounter.increment(delta); diff --git a/common/stats/src/main/java/org/thingsboard/server/common/stats/StatsType.java b/common/stats/src/main/java/org/thingsboard/server/common/stats/StatsType.java index 3833155c05..b58a4ec608 100644 --- a/common/stats/src/main/java/org/thingsboard/server/common/stats/StatsType.java +++ b/common/stats/src/main/java/org/thingsboard/server/common/stats/StatsType.java @@ -20,6 +20,7 @@ public enum StatsType { CORE("core"), TRANSPORT("transport"), JS_INVOKE("jsInvoke"), + TBEL_INVOKE("tbelInvoke"), RATE_EXECUTOR("rateExecutor"), HOUSEKEEPER("housekeeper"), EDGE("edge"); diff --git a/docker/monitoring/grafana/provisioning/dashboards/core_and_js_metrics.json b/docker/monitoring/grafana/provisioning/dashboards/core_and_js_metrics.json index e04a5c2d84..15ff06ab8c 100644 --- a/docker/monitoring/grafana/provisioning/dashboards/core_and_js_metrics.json +++ b/docker/monitoring/grafana/provisioning/dashboards/core_and_js_metrics.json @@ -223,8 +223,8 @@ "fill": 1, "fillGradient": 0, "gridPos": { - "h": 12, - "w": 24, + "h": 10, + "w": 12, "x": 0, "y": 10 }, @@ -303,6 +303,100 @@ "align": false, "alignLevel": null } + }, + { + "aliasColors": {}, + "bars": false, + "dashLength": 10, + "dashes": false, + "datasource": null, + "fieldConfig": { + "defaults": {}, + "overrides": [] + }, + "fill": 1, + "fillGradient": 0, + "gridPos": { + "h": 10, + "w": 12, + "x": 12, + "y": 10 + }, + "hiddenSeries": false, + "id": 19, + "legend": { + "avg": false, + "current": false, + "max": false, + "min": false, + "show": true, + "total": false, + "values": false + }, + "lines": true, + "linewidth": 1, + "nullPointMode": "null", + "options": { + "alertThreshold": true + }, + "percentage": false, + "pluginVersion": "7.5.4", + "pointradius": 2, + "points": false, + "renderer": "flot", + "seriesOverrides": [], + "spaceLength": 10, + "stack": false, + "steppedLine": false, + "targets": [ + { + "exemplar": true, + "expr": "sum by(statsName) (increase(tbelInvoke_total[1m]))", + "interval": "", + "legendFormat": "{{statsName}}", + "refId": "A" + } + ], + "thresholds": [], + "timeFrom": null, + "timeRegions": [], + "timeShift": null, + "title": "TbelInvoke Stats", + "tooltip": { + "shared": true, + "sort": 0, + "value_type": "individual" + }, + "type": "graph", + "xaxis": { + "buckets": null, + "mode": "time", + "name": null, + "show": true, + "values": [] + }, + "yaxes": [ + { + "format": "short", + "label": null, + "logBase": 1, + "max": null, + "min": null, + "show": true + }, + { + "format": "short", + "label": null, + "logBase": 1, + "max": null, + "min": null, + "show": true + } + ], + "yaxis": { + "align": false, + "alignLevel": null + } } ], "schemaVersion": 27, diff --git a/rule-engine/rule-engine-api/src/main/java/org/thingsboard/rule/engine/api/TbContext.java b/rule-engine/rule-engine-api/src/main/java/org/thingsboard/rule/engine/api/TbContext.java index b517bb0051..b78f52892a 100644 --- a/rule-engine/rule-engine-api/src/main/java/org/thingsboard/rule/engine/api/TbContext.java +++ b/rule-engine/rule-engine-api/src/main/java/org/thingsboard/rule/engine/api/TbContext.java @@ -373,12 +373,6 @@ public interface TbContext { ScriptEngine createScriptEngine(ScriptLanguage scriptLang, String script, String... argNames); - void logJsEvalRequest(); - - void logJsEvalResponse(); - - void logJsEvalFailure(); - String getServiceId(); EventLoopGroup getSharedEventLoop(); diff --git a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/action/TbClearAlarmNode.java b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/action/TbClearAlarmNode.java index 622448f494..84091b3a52 100644 --- a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/action/TbClearAlarmNode.java +++ b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/action/TbClearAlarmNode.java @@ -70,10 +70,8 @@ public class TbClearAlarmNode extends TbAbstractAlarmNode clearAlarm(TbContext ctx, TbMsg msg, Alarm alarm) { - ctx.logJsEvalRequest(); ListenableFuture asyncDetails = buildAlarmDetails(msg, alarm.getDetails()); return Futures.transform(asyncDetails, details -> { - ctx.logJsEvalResponse(); AlarmApiCallResult result = ctx.getAlarmService().clearAlarm(ctx.getTenantId(), alarm.getId(), System.currentTimeMillis(), details); if (result.isSuccessful()) { return new TbAlarmResult(false, false, result.isCleared(), result.getAlarm()); diff --git a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/action/TbCreateAlarmNode.java b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/action/TbCreateAlarmNode.java index 7e535596cd..4e821163c7 100644 --- a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/action/TbCreateAlarmNode.java +++ b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/action/TbCreateAlarmNode.java @@ -119,15 +119,11 @@ public class TbCreateAlarmNode extends TbAbstractAlarmNode asyncDetails; boolean buildDetails = !config.isUseMessageAlarmData() || config.isOverwriteAlarmDetails(); if (buildDetails) { - ctx.logJsEvalRequest(); asyncDetails = buildAlarmDetails(msg, null); } else { asyncDetails = Futures.immediateFuture(null); } ListenableFuture asyncAlarm = Futures.transform(asyncDetails, details -> { - if (buildDetails) { - ctx.logJsEvalResponse(); - } Alarm newAlarm; if (msgAlarm != null) { newAlarm = msgAlarm; @@ -148,15 +144,11 @@ public class TbCreateAlarmNode extends TbAbstractAlarmNode asyncDetails; boolean buildDetails = !config.isUseMessageAlarmData() || config.isOverwriteAlarmDetails(); if (buildDetails) { - ctx.logJsEvalRequest(); asyncDetails = buildAlarmDetails(msg, existingAlarm.getDetails()); } else { asyncDetails = Futures.immediateFuture(null); } ListenableFuture asyncUpdated = Futures.transform(asyncDetails, details -> { - if (buildDetails) { - ctx.logJsEvalResponse(); - } if (msgAlarm != null) { existingAlarm.setSeverity(msgAlarm.getSeverity()); existingAlarm.setPropagate(msgAlarm.isPropagate()); diff --git a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/action/TbLogNode.java b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/action/TbLogNode.java index 6b07d41a42..8c21018762 100644 --- a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/action/TbLogNode.java +++ b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/action/TbLogNode.java @@ -76,18 +76,15 @@ public class TbLogNode implements TbNode { return; } - ctx.logJsEvalRequest(); Futures.addCallback(scriptEngine.executeToStringAsync(msg), new FutureCallback() { @Override public void onSuccess(@Nullable String result) { - ctx.logJsEvalResponse(); log.info(result); ctx.tellSuccess(msg); } @Override public void onFailure(Throwable t) { - ctx.logJsEvalResponse(); ctx.tellFailure(msg, t); } }, MoreExecutors.directExecutor()); //usually js responses runs on js callback executor diff --git a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/debug/TbMsgGeneratorNode.java b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/debug/TbMsgGeneratorNode.java index c7bf1423f8..f303653093 100644 --- a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/debug/TbMsgGeneratorNode.java +++ b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/debug/TbMsgGeneratorNode.java @@ -161,10 +161,8 @@ public class TbMsgGeneratorNode implements TbNode { prevMsg = ctx.newMsg(queueName, TbMsg.EMPTY_STRING, originatorId, msg.getCustomerId(), TbMsgMetaData.EMPTY, TbMsg.EMPTY_JSON_OBJECT); } if (initialized.get()) { - ctx.logJsEvalRequest(); return Futures.transformAsync(scriptEngine.executeGenerateAsync(prevMsg), generated -> { log.trace("generate process response, generated {}, config {}", generated, config); - ctx.logJsEvalResponse(); prevMsg = ctx.newMsg(queueName, generated.getType(), originatorId, msg.getCustomerId(), generated.getMetaData(), generated.getData()); return Futures.immediateFuture(prevMsg); }, MoreExecutors.directExecutor()); //usually it runs on js-executor-remote-callback thread pool diff --git a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/filter/TbJsFilterNode.java b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/filter/TbJsFilterNode.java index 6d5aaa6477..8f8c68aa62 100644 --- a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/filter/TbJsFilterNode.java +++ b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/filter/TbJsFilterNode.java @@ -61,15 +61,12 @@ public class TbJsFilterNode implements TbNode { @Override public void onMsg(TbContext ctx, TbMsg msg) { - ctx.logJsEvalRequest(); withCallback(scriptEngine.executeFilterAsync(msg), filterResult -> { - ctx.logJsEvalResponse(); ctx.tellNext(msg, filterResult ? TbNodeConnectionType.TRUE : TbNodeConnectionType.FALSE); }, t -> { ctx.tellFailure(msg, t); - ctx.logJsEvalFailure(); }, ctx.getDbCallbackExecutor()); } diff --git a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/filter/TbJsSwitchNode.java b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/filter/TbJsSwitchNode.java index e866f05bd0..a3c3df642b 100644 --- a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/filter/TbJsSwitchNode.java +++ b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/filter/TbJsSwitchNode.java @@ -62,17 +62,14 @@ public class TbJsSwitchNode implements TbNode { @Override public void onMsg(TbContext ctx, TbMsg msg) { - ctx.logJsEvalRequest(); Futures.addCallback(scriptEngine.executeSwitchAsync(msg), new FutureCallback<>() { @Override public void onSuccess(@Nullable Set result) { - ctx.logJsEvalResponse(); processSwitch(ctx, msg, result); } @Override public void onFailure(Throwable t) { - ctx.logJsEvalFailure(); ctx.tellFailure(msg, t); } }, MoreExecutors.directExecutor()); //usually runs in a callbackExecutor diff --git a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/transform/TbTransformMsgNode.java b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/transform/TbTransformMsgNode.java index 3227c0c52c..61444fae93 100644 --- a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/transform/TbTransformMsgNode.java +++ b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/transform/TbTransformMsgNode.java @@ -58,13 +58,11 @@ public class TbTransformMsgNode extends TbAbstractTransformNode> transform(TbContext ctx, TbMsg msg) { - ctx.logJsEvalRequest(); return scriptEngine.executeUpdateAsync(msg); } @Override protected void transformFailure(TbContext ctx, TbMsg msg, Throwable t) { - ctx.logJsEvalFailure(); super.transformFailure(ctx, msg, t); } diff --git a/rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/action/TbCreateAlarmNodeTest.java b/rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/action/TbCreateAlarmNodeTest.java index c0e27231c4..d1bf5a0840 100644 --- a/rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/action/TbCreateAlarmNodeTest.java +++ b/rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/action/TbCreateAlarmNodeTest.java @@ -119,8 +119,8 @@ class TbCreateAlarmNodeTest { delete metadata.prevAlarmDetails; //now metadata is the same as it comes IN this rule node } - - + + return details;"""); assertThat(config.getAlarmDetailsBuildTbel()).isEqualTo(""" \ @@ -131,8 +131,8 @@ class TbCreateAlarmNodeTest { metadata.remove('prevAlarmDetails'); //now metadata is the same as it comes IN this rule node } - - + + return details;"""); assertThat(config.getSeverity()).isEqualTo(AlarmSeverity.CRITICAL.name()); assertThat(config.isPropagate()).isFalse(); @@ -242,9 +242,7 @@ class TbCreateAlarmNodeTest { // THEN // verify alarm details script evaluation - then(ctxMock).should().logJsEvalRequest(); then(alarmDetailsScriptMock).should().executeJsonAsync(incomingMsg); - then(ctxMock).should().logJsEvalResponse(); // verify we called createAlarm() with correct AlarmCreateOrUpdateActiveRequest then(alarmServiceMock).should().createAlarm(expectedCreateAlarmRequest); @@ -411,9 +409,7 @@ class TbCreateAlarmNodeTest { // THEN // verify alarm details script evaluation - then(ctxMock).should().logJsEvalRequest(); then(alarmDetailsScriptMock).should().executeJsonAsync(incomingMsg); - then(ctxMock).should().logJsEvalResponse(); // verify we called createAlarm() with correct AlarmCreateOrUpdateActiveRequest then(alarmServiceMock).should().createAlarm(expectedCreateAlarmRequest); @@ -601,14 +597,12 @@ class TbCreateAlarmNodeTest { // THEN // verify alarm details script evaluation - then(ctxMock).should().logJsEvalRequest(); var dummyMsgCaptor = ArgumentCaptor.forClass(TbMsg.class); then(alarmDetailsScriptMock).should().executeJsonAsync(dummyMsgCaptor.capture()); TbMsg actualDummyMsg = dummyMsgCaptor.getValue(); assertThat(actualDummyMsg.getType()).isEqualTo(incomingMsg.getType()); assertThat(actualDummyMsg.getData()).isEqualTo(incomingMsg.getData()); assertThat(actualDummyMsg.getMetaData().getData()).containsEntry("prevAlarmDetails", JacksonUtil.toString(oldAlarmDetails)); - then(ctxMock).should().logJsEvalResponse(); // verify we called updateAlarm() with correct AlarmUpdateRequest then(alarmServiceMock).should().updateAlarm(expectedUpdateAlarmRequest); @@ -773,9 +767,7 @@ class TbCreateAlarmNodeTest { // THEN // verify alarm details script was not evaluated - then(ctxMock).should(never()).logJsEvalRequest(); then(alarmDetailsScriptMock).should(never()).executeJsonAsync(any()); - then(ctxMock).should(never()).logJsEvalResponse(); // verify we called createAlarm() with correct AlarmCreateOrUpdateActiveRequest then(alarmServiceMock).should().createAlarm(expectedCreateAlarmRequest); @@ -960,14 +952,12 @@ class TbCreateAlarmNodeTest { // THEN // verify alarm details script evaluation - then(ctxMock).should().logJsEvalRequest(); var dummyMsgCaptor = ArgumentCaptor.forClass(TbMsg.class); then(alarmDetailsScriptMock).should().executeJsonAsync(dummyMsgCaptor.capture()); TbMsg actualDummyMsg = dummyMsgCaptor.getValue(); assertThat(actualDummyMsg.getType()).isEqualTo(incomingMsg.getType()); assertThat(actualDummyMsg.getData()).isEqualTo(incomingMsg.getData()); assertThat(actualDummyMsg.getMetaData().getData()).containsEntry("prevAlarmDetails", JacksonUtil.toString(oldAlarmDetails)); - then(ctxMock).should().logJsEvalResponse(); // verify we called updateAlarm() with correct AlarmUpdateRequest then(alarmServiceMock).should().updateAlarm(expectedUpdateAlarmRequest); @@ -1141,14 +1131,12 @@ class TbCreateAlarmNodeTest { // THEN // verify alarm details script evaluation - then(ctxMock).should().logJsEvalRequest(); var dummyMsgCaptor = ArgumentCaptor.forClass(TbMsg.class); then(alarmDetailsScriptMock).should().executeJsonAsync(dummyMsgCaptor.capture()); TbMsg actualDummyMsg = dummyMsgCaptor.getValue(); assertThat(actualDummyMsg.getType()).isEqualTo(incomingMsg.getType()); assertThat(actualDummyMsg.getData()).isEqualTo(incomingMsg.getData()); assertThat(actualDummyMsg.getMetaData().getData()).containsEntry("prevAlarmDetails", JacksonUtil.toString(alarmDetails)); - then(ctxMock).should().logJsEvalResponse(); // verify we called updateAlarm() with correct AlarmUpdateRequest then(alarmServiceMock).should().updateAlarm(expectedUpdateAlarmRequest); From 2eda229b21a38fe14132aee1ee5d9354b5f9173c Mon Sep 17 00:00:00 2001 From: YevhenBondarenko Date: Tue, 10 Dec 2024 13:21:22 +0100 Subject: [PATCH 006/345] fixed tests --- .../server/service/script/RemoteJsInvokeServiceTest.java | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/application/src/test/java/org/thingsboard/server/service/script/RemoteJsInvokeServiceTest.java b/application/src/test/java/org/thingsboard/server/service/script/RemoteJsInvokeServiceTest.java index a0951723e2..fb0161885d 100644 --- a/application/src/test/java/org/thingsboard/server/service/script/RemoteJsInvokeServiceTest.java +++ b/application/src/test/java/org/thingsboard/server/service/script/RemoteJsInvokeServiceTest.java @@ -21,9 +21,13 @@ import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.mockito.ArgumentCaptor; +import org.springframework.test.util.ReflectionTestUtils; import org.thingsboard.script.api.ScriptType; import org.thingsboard.server.common.data.ApiUsageState; import org.thingsboard.server.common.data.id.TenantId; +import org.thingsboard.server.common.stats.DefaultStatsFactory; +import org.thingsboard.server.common.stats.StatsCounter; +import org.thingsboard.server.common.stats.StatsFactory; import org.thingsboard.server.common.stats.TbApiUsageReportClient; import org.thingsboard.server.common.stats.TbApiUsageStateClient; import org.thingsboard.server.gen.js.JsInvokeProtos; @@ -68,6 +72,10 @@ class RemoteJsInvokeServiceTest { remoteJsInvokeService = new RemoteJsInvokeService(Optional.of(apiUsageStateClient), Optional.of(apiUsageReportClient)); jsRequestTemplate = mock(TbQueueRequestTemplate.class); remoteJsInvokeService.requestTemplate = jsRequestTemplate; + StatsFactory statsFactory = mock(StatsFactory.class); + when(statsFactory.createStatsCounter(any(), any())).thenReturn(mock(StatsCounter.class)); + ReflectionTestUtils.setField(remoteJsInvokeService, "statsFactory",statsFactory); + remoteJsInvokeService.init(); } @AfterEach From dcf4eb27930942412d418135565a48c70a770a33 Mon Sep 17 00:00:00 2001 From: YevhenBondarenko Date: Tue, 10 Dec 2024 16:04:11 +0100 Subject: [PATCH 007/345] refactored due to comments --- .../DefaultTbApiUsageReportClient.java | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/common/queue/src/main/java/org/thingsboard/server/queue/usagestats/DefaultTbApiUsageReportClient.java b/common/queue/src/main/java/org/thingsboard/server/queue/usagestats/DefaultTbApiUsageReportClient.java index 10ea5b13a0..e667256e06 100644 --- a/common/queue/src/main/java/org/thingsboard/server/queue/usagestats/DefaultTbApiUsageReportClient.java +++ b/common/queue/src/main/java/org/thingsboard/server/queue/usagestats/DefaultTbApiUsageReportClient.java @@ -15,6 +15,7 @@ */ package org.thingsboard.server.queue.usagestats; +import com.google.common.collect.Lists; import jakarta.annotation.PostConstruct; import lombok.Data; import lombok.RequiredArgsConstructor; @@ -52,7 +53,6 @@ import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.ConcurrentMap; import java.util.concurrent.TimeUnit; import java.util.concurrent.atomic.AtomicLong; -import java.util.stream.IntStream; @Component @Slf4j @@ -155,13 +155,13 @@ public class DefaultTbApiUsageReportClient implements TbApiUsageReportClient { } private List toMsgPack(List list) { - return IntStream.range(0, (list.size() + packSize - 1) / packSize) - .mapToObj(i -> { - var packList = list.subList(i * packSize, Math.min((i + 1) * packSize, list.size())); - var pack = ToUsageStatsServiceMsgPack.newBuilder(); - pack.addAllMsgs(packList); - return pack.build(); - }).toList(); + return Lists.partition(list, packSize) + .stream() + .map(partition -> + ToUsageStatsServiceMsgPack.newBuilder() + .addAllMsgs(partition) + .build()) + .toList(); } @Override From 7f4a0ef48f3f3a7ab4070b76c43f1dbfcb069f20 Mon Sep 17 00:00:00 2001 From: Igor Kulikov Date: Fri, 27 Dec 2024 15:26:19 +0200 Subject: [PATCH 008/345] UI: New maps V3 --- ui-ngx/angular.json | 3 +- .../lib/{maps => maps-legacy}/circle.ts | 8 +- .../common-maps-utils.ts | 0 .../select-entity-dialog.component.html | 0 .../select-entity-dialog.component.scss | 0 .../dialogs/select-entity-dialog.component.ts | 0 .../lib/{maps => maps-legacy}/leaflet-map.ts | 6 +- .../lib/{maps => maps-legacy}/map-models.ts | 0 .../map-widget.interface.ts | 2 +- .../lib/{maps => maps-legacy}/map-widget2.ts | 2 +- .../lib/{maps => maps-legacy}/maps-utils.ts | 2 +- .../lib/{maps => maps-legacy}/markers.scss | 0 .../lib/{maps => maps-legacy}/markers.ts | 0 .../lib/{maps => maps-legacy}/polygon.ts | 2 +- .../lib/{maps => maps-legacy}/polyline.ts | 2 +- .../providers/google-map.ts | 0 .../providers/here-map.ts | 0 .../providers/image-map.ts | 2 +- .../providers/openstreet-map.ts | 0 .../providers/public-api.ts | 2 +- .../providers/tencent-map.ts | 0 .../widget/lib/maps/leaflet/leaflet-tb.ts | 0 .../components/widget/lib/maps/map-layer.ts | 90 +++++ .../widget/lib/maps/map-widget.component.html | 25 ++ .../widget/lib/maps/map-widget.component.scss | 38 +++ .../widget/lib/maps/map-widget.component.ts | 97 ++++++ .../widget/lib/maps/map-widget.models.ts | 38 +++ .../components/widget/lib/maps/map.models.ts | 310 ++++++++++++++++++ .../home/components/widget/lib/maps/map.scss | 19 ++ .../home/components/widget/lib/maps/map.ts | 196 +++++++++++ .../settings/map/circle-settings.component.ts | 2 +- .../map/common-map-settings.component.ts | 2 +- .../google-map-provider-settings.component.ts | 2 +- .../here-map-provider-settings.component.ts | 2 +- .../image-map-provider-settings.component.ts | 2 +- .../map/map-editor-settings.component.ts | 2 +- .../map/map-provider-settings.component.ts | 2 +- .../settings/map/map-settings.component.ts | 2 +- .../map/map-widget-settings.component.ts | 2 +- .../marker-clustering-settings.component.ts | 2 +- .../map/markers-settings.component.ts | 2 +- ...nstreet-map-provider-settings.component.ts | 2 +- .../map/polygon-settings.component.ts | 2 +- .../map/route-map-settings.component.ts | 2 +- .../route-map-widget-settings.component.ts | 2 +- ...tencent-map-provider-settings.component.ts | 2 +- ...rip-animation-common-settings.component.ts | 2 +- ...rip-animation-marker-settings.component.ts | 2 +- .../trip-animation-path-settings.component.ts | 2 +- ...trip-animation-point-settings.component.ts | 2 +- ...rip-animation-widget-settings.component.ts | 2 +- .../trip-animation.component.ts | 10 +- .../widget/widget-component.service.ts | 2 +- .../widget/widget-components.module.ts | 9 +- .../history-selector.component.ts | 2 +- ui-ngx/src/typings/leaflet-extend-tb.d.ts | 3 + 56 files changed, 866 insertions(+), 46 deletions(-) rename ui-ngx/src/app/modules/home/components/widget/lib/{maps => maps-legacy}/circle.ts (96%) rename ui-ngx/src/app/modules/home/components/widget/lib/{maps => maps-legacy}/common-maps-utils.ts (100%) rename ui-ngx/src/app/modules/home/components/widget/lib/{maps => maps-legacy}/dialogs/select-entity-dialog.component.html (100%) rename ui-ngx/src/app/modules/home/components/widget/lib/{maps => maps-legacy}/dialogs/select-entity-dialog.component.scss (100%) rename ui-ngx/src/app/modules/home/components/widget/lib/{maps => maps-legacy}/dialogs/select-entity-dialog.component.ts (100%) rename ui-ngx/src/app/modules/home/components/widget/lib/{maps => maps-legacy}/leaflet-map.ts (99%) rename ui-ngx/src/app/modules/home/components/widget/lib/{maps => maps-legacy}/map-models.ts (100%) rename ui-ngx/src/app/modules/home/components/widget/lib/{maps => maps-legacy}/map-widget.interface.ts (91%) rename ui-ngx/src/app/modules/home/components/widget/lib/{maps => maps-legacy}/map-widget2.ts (99%) rename ui-ngx/src/app/modules/home/components/widget/lib/{maps => maps-legacy}/maps-utils.ts (99%) rename ui-ngx/src/app/modules/home/components/widget/lib/{maps => maps-legacy}/markers.scss (100%) rename ui-ngx/src/app/modules/home/components/widget/lib/{maps => maps-legacy}/markers.ts (100%) rename ui-ngx/src/app/modules/home/components/widget/lib/{maps => maps-legacy}/polygon.ts (99%) rename ui-ngx/src/app/modules/home/components/widget/lib/{maps => maps-legacy}/polyline.ts (98%) rename ui-ngx/src/app/modules/home/components/widget/lib/{maps => maps-legacy}/providers/google-map.ts (100%) rename ui-ngx/src/app/modules/home/components/widget/lib/{maps => maps-legacy}/providers/here-map.ts (100%) rename ui-ngx/src/app/modules/home/components/widget/lib/{maps => maps-legacy}/providers/image-map.ts (99%) rename ui-ngx/src/app/modules/home/components/widget/lib/{maps => maps-legacy}/providers/openstreet-map.ts (100%) rename ui-ngx/src/app/modules/home/components/widget/lib/{maps => maps-legacy}/providers/public-api.ts (93%) rename ui-ngx/src/app/modules/home/components/widget/lib/{maps => maps-legacy}/providers/tencent-map.ts (100%) create mode 100644 ui-ngx/src/app/modules/home/components/widget/lib/maps/leaflet/leaflet-tb.ts create mode 100644 ui-ngx/src/app/modules/home/components/widget/lib/maps/map-layer.ts create mode 100644 ui-ngx/src/app/modules/home/components/widget/lib/maps/map-widget.component.html create mode 100644 ui-ngx/src/app/modules/home/components/widget/lib/maps/map-widget.component.scss create mode 100644 ui-ngx/src/app/modules/home/components/widget/lib/maps/map-widget.component.ts create mode 100644 ui-ngx/src/app/modules/home/components/widget/lib/maps/map-widget.models.ts create mode 100644 ui-ngx/src/app/modules/home/components/widget/lib/maps/map.models.ts create mode 100644 ui-ngx/src/app/modules/home/components/widget/lib/maps/map.scss create mode 100644 ui-ngx/src/app/modules/home/components/widget/lib/maps/map.ts diff --git a/ui-ngx/angular.json b/ui-ngx/angular.json index f605bfee8a..2803459f10 100644 --- a/ui-ngx/angular.json +++ b/ui-ngx/angular.json @@ -101,7 +101,8 @@ "node_modules/tooltipster/dist/css/plugins/tooltipster/sideTip/themes/tooltipster-sideTip-shadow.min.css", "node_modules/jstree-bootstrap-theme/dist/themes/proton/style.min.css", "node_modules/leaflet/dist/leaflet.css", - "src/app/modules/home/components/widget/lib/maps/markers.scss", + "src/app/modules/home/components/widget/lib/maps/map.scss", + "src/app/modules/home/components/widget/lib/maps-legacy/markers.scss", "src/app/modules/home/components/widget/lib/home-page/home-page.scss", "node_modules/leaflet.markercluster/dist/MarkerCluster.css", "node_modules/leaflet.markercluster/dist/MarkerCluster.Default.css", diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/maps/circle.ts b/ui-ngx/src/app/modules/home/components/widget/lib/maps-legacy/circle.ts similarity index 96% rename from ui-ngx/src/app/modules/home/components/widget/lib/maps/circle.ts rename to ui-ngx/src/app/modules/home/components/widget/lib/maps-legacy/circle.ts index 680f3faca6..67e8325f61 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/maps/circle.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/maps-legacy/circle.ts @@ -15,10 +15,10 @@ /// import L, { LeafletMouseEvent } from 'leaflet'; -import { CircleData, WidgetCircleSettings } from '@home/components/widget/lib/maps/map-models'; -import { functionValueCalculator, parseWithTranslation } from '@home/components/widget/lib/maps/common-maps-utils'; -import LeafletMap from '@home/components/widget/lib/maps/leaflet-map'; -import { createTooltip } from '@home/components/widget/lib/maps/maps-utils'; +import { CircleData, WidgetCircleSettings } from '@home/components/widget/lib/maps-legacy/map-models'; +import { functionValueCalculator, parseWithTranslation } from '@home/components/widget/lib/maps-legacy/common-maps-utils'; +import LeafletMap from '@home/components/widget/lib/maps-legacy/leaflet-map'; +import { createTooltip } from '@home/components/widget/lib/maps-legacy/maps-utils'; import { FormattedData } from '@shared/models/widget.models'; import { fillDataPattern, processDataPattern, safeExecuteTbFunction } from '@core/utils'; diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/maps/common-maps-utils.ts b/ui-ngx/src/app/modules/home/components/widget/lib/maps-legacy/common-maps-utils.ts similarity index 100% rename from ui-ngx/src/app/modules/home/components/widget/lib/maps/common-maps-utils.ts rename to ui-ngx/src/app/modules/home/components/widget/lib/maps-legacy/common-maps-utils.ts diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/maps/dialogs/select-entity-dialog.component.html b/ui-ngx/src/app/modules/home/components/widget/lib/maps-legacy/dialogs/select-entity-dialog.component.html similarity index 100% rename from ui-ngx/src/app/modules/home/components/widget/lib/maps/dialogs/select-entity-dialog.component.html rename to ui-ngx/src/app/modules/home/components/widget/lib/maps-legacy/dialogs/select-entity-dialog.component.html diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/maps/dialogs/select-entity-dialog.component.scss b/ui-ngx/src/app/modules/home/components/widget/lib/maps-legacy/dialogs/select-entity-dialog.component.scss similarity index 100% rename from ui-ngx/src/app/modules/home/components/widget/lib/maps/dialogs/select-entity-dialog.component.scss rename to ui-ngx/src/app/modules/home/components/widget/lib/maps-legacy/dialogs/select-entity-dialog.component.scss diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/maps/dialogs/select-entity-dialog.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/maps-legacy/dialogs/select-entity-dialog.component.ts similarity index 100% rename from ui-ngx/src/app/modules/home/components/widget/lib/maps/dialogs/select-entity-dialog.component.ts rename to ui-ngx/src/app/modules/home/components/widget/lib/maps-legacy/dialogs/select-entity-dialog.component.ts diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/maps/leaflet-map.ts b/ui-ngx/src/app/modules/home/components/widget/lib/maps-legacy/leaflet-map.ts similarity index 99% rename from ui-ngx/src/app/modules/home/components/widget/lib/maps/leaflet-map.ts rename to ui-ngx/src/app/modules/home/components/widget/lib/maps-legacy/leaflet-map.ts index ee5af0bfb5..2a390a39fc 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/maps/leaflet-map.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/maps-legacy/leaflet-map.ts @@ -43,8 +43,8 @@ import { isJSON, isValidLatLng, LabelSettings -} from '@home/components/widget/lib/maps/maps-utils'; -import { checkLngLat, createLoadingDiv } from '@home/components/widget/lib/maps/common-maps-utils'; +} from '@home/components/widget/lib/maps-legacy/maps-utils'; +import { checkLngLat, createLoadingDiv } from '@home/components/widget/lib/maps-legacy/common-maps-utils'; import { WidgetContext } from '@home/models/widget-component.models'; import { deepClone, @@ -60,7 +60,7 @@ import { TranslateService } from '@ngx-translate/core'; import { SelectEntityDialogComponent, SelectEntityDialogData -} from '@home/components/widget/lib/maps/dialogs/select-entity-dialog.component'; +} from '@home/components/widget/lib/maps-legacy/dialogs/select-entity-dialog.component'; import { MatDialog } from '@angular/material/dialog'; import { FormattedData, ReplaceInfo } from '@shared/models/widget.models'; import { ImagePipe } from '@shared/pipe/image.pipe'; diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/maps/map-models.ts b/ui-ngx/src/app/modules/home/components/widget/lib/maps-legacy/map-models.ts similarity index 100% rename from ui-ngx/src/app/modules/home/components/widget/lib/maps/map-models.ts rename to ui-ngx/src/app/modules/home/components/widget/lib/maps-legacy/map-models.ts diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/maps/map-widget.interface.ts b/ui-ngx/src/app/modules/home/components/widget/lib/maps-legacy/map-widget.interface.ts similarity index 91% rename from ui-ngx/src/app/modules/home/components/widget/lib/maps/map-widget.interface.ts rename to ui-ngx/src/app/modules/home/components/widget/lib/maps-legacy/map-widget.interface.ts index 761584c98f..05e1f2b7c0 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/maps/map-widget.interface.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/maps-legacy/map-widget.interface.ts @@ -14,7 +14,7 @@ /// limitations under the License. /// -import LeafletMap from '@home/components/widget/lib/maps/leaflet-map'; +import LeafletMap from '@home/components/widget/lib/maps-legacy/leaflet-map'; export interface MapWidgetInterface { map?: LeafletMap; diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/maps/map-widget2.ts b/ui-ngx/src/app/modules/home/components/widget/lib/maps-legacy/map-widget2.ts similarity index 99% rename from ui-ngx/src/app/modules/home/components/widget/lib/maps/map-widget2.ts rename to ui-ngx/src/app/modules/home/components/widget/lib/maps-legacy/map-widget2.ts index 4ca5d4302a..950329d7a5 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/maps/map-widget2.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/maps-legacy/map-widget2.ts @@ -23,7 +23,7 @@ import { Datasource, DatasourceData, FormattedData, WidgetActionDescriptor } fro import { TranslateService } from '@ngx-translate/core'; import { UtilsService } from '@core/services/utils.service'; import { EntityDataPageLink } from '@shared/models/query/query.models'; -import { providerClass } from '@home/components/widget/lib/maps/providers/public-api'; +import { providerClass } from '@home/components/widget/lib/maps-legacy/providers/public-api'; import { isDefined, isDefinedAndNotNull, parseTbFunction } from '@core/utils'; import L from 'leaflet'; import { firstValueFrom, forkJoin, from, Observable, of } from 'rxjs'; diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/maps/maps-utils.ts b/ui-ngx/src/app/modules/home/components/widget/lib/maps-legacy/maps-utils.ts similarity index 99% rename from ui-ngx/src/app/modules/home/components/widget/lib/maps/maps-utils.ts rename to ui-ngx/src/app/modules/home/components/widget/lib/maps-legacy/maps-utils.ts index d47ed8f110..97132628cd 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/maps/maps-utils.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/maps-legacy/maps-utils.ts @@ -18,7 +18,7 @@ import L from 'leaflet'; import { GenericFunction, ShowTooltipAction, WidgetToolipSettings } from './map-models'; import { Datasource, FormattedData } from '@app/shared/models/widget.models'; import { fillDataPattern, isDefinedAndNotNull, isString, processDataPattern, safeExecuteTbFunction } from '@core/utils'; -import { parseWithTranslation } from '@home/components/widget/lib/maps/common-maps-utils'; +import { parseWithTranslation } from '@home/components/widget/lib/maps-legacy/common-maps-utils'; import { CompiledTbFunction } from '@shared/models/js-function.models'; export function createTooltip(target: L.Layer, diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/maps/markers.scss b/ui-ngx/src/app/modules/home/components/widget/lib/maps-legacy/markers.scss similarity index 100% rename from ui-ngx/src/app/modules/home/components/widget/lib/maps/markers.scss rename to ui-ngx/src/app/modules/home/components/widget/lib/maps-legacy/markers.scss diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/maps/markers.ts b/ui-ngx/src/app/modules/home/components/widget/lib/maps-legacy/markers.ts similarity index 100% rename from ui-ngx/src/app/modules/home/components/widget/lib/maps/markers.ts rename to ui-ngx/src/app/modules/home/components/widget/lib/maps-legacy/markers.ts diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/maps/polygon.ts b/ui-ngx/src/app/modules/home/components/widget/lib/maps-legacy/polygon.ts similarity index 99% rename from ui-ngx/src/app/modules/home/components/widget/lib/maps/polygon.ts rename to ui-ngx/src/app/modules/home/components/widget/lib/maps-legacy/polygon.ts index cb9ba7c35f..1b51a1989e 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/maps/polygon.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/maps-legacy/polygon.ts @@ -20,7 +20,7 @@ import { functionValueCalculator, parseWithTranslation } from './common-maps-uti import { WidgetPolygonSettings } from './map-models'; import { FormattedData } from '@shared/models/widget.models'; import { fillDataPattern, processDataPattern, safeExecuteTbFunction } from '@core/utils'; -import LeafletMap from '@home/components/widget/lib/maps/leaflet-map'; +import LeafletMap from '@home/components/widget/lib/maps-legacy/leaflet-map'; export class Polygon { diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/maps/polyline.ts b/ui-ngx/src/app/modules/home/components/widget/lib/maps-legacy/polyline.ts similarity index 98% rename from ui-ngx/src/app/modules/home/components/widget/lib/maps/polyline.ts rename to ui-ngx/src/app/modules/home/components/widget/lib/maps-legacy/polyline.ts index 347782d210..fc842b6f3d 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/maps/polyline.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/maps-legacy/polyline.ts @@ -19,7 +19,7 @@ import L, { PolylineDecorator, PolylineDecoratorOptions, Symbol } from 'leaflet' import 'leaflet-polylinedecorator'; import { WidgetPolylineSettings } from './map-models'; -import { functionValueCalculator } from '@home/components/widget/lib/maps/common-maps-utils'; +import { functionValueCalculator } from '@home/components/widget/lib/maps-legacy/common-maps-utils'; import { FormattedData } from '@shared/models/widget.models'; export class Polyline { diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/maps/providers/google-map.ts b/ui-ngx/src/app/modules/home/components/widget/lib/maps-legacy/providers/google-map.ts similarity index 100% rename from ui-ngx/src/app/modules/home/components/widget/lib/maps/providers/google-map.ts rename to ui-ngx/src/app/modules/home/components/widget/lib/maps-legacy/providers/google-map.ts diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/maps/providers/here-map.ts b/ui-ngx/src/app/modules/home/components/widget/lib/maps-legacy/providers/here-map.ts similarity index 100% rename from ui-ngx/src/app/modules/home/components/widget/lib/maps/providers/here-map.ts rename to ui-ngx/src/app/modules/home/components/widget/lib/maps-legacy/providers/here-map.ts diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/maps/providers/image-map.ts b/ui-ngx/src/app/modules/home/components/widget/lib/maps-legacy/providers/image-map.ts similarity index 99% rename from ui-ngx/src/app/modules/home/components/widget/lib/maps/providers/image-map.ts rename to ui-ngx/src/app/modules/home/components/widget/lib/maps-legacy/providers/image-map.ts index d91a84d3b2..7d3dfdf3f4 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/maps/providers/image-map.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/maps-legacy/providers/image-map.ts @@ -25,7 +25,7 @@ import { } from '../map-models'; import { combineLatest, Observable, of, ReplaySubject, switchMap } from 'rxjs'; import { catchError } from 'rxjs/operators'; -import { calculateNewPointCoordinate, loadImageWithAspect } from '@home/components/widget/lib/maps/common-maps-utils'; +import { calculateNewPointCoordinate, loadImageWithAspect } from '@home/components/widget/lib/maps-legacy/common-maps-utils'; import { WidgetContext } from '@home/models/widget-component.models'; import { DataSet, DatasourceType, FormattedData, widgetType } from '@shared/models/widget.models'; import { DataKeyType } from '@shared/models/telemetry/telemetry.models'; diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/maps/providers/openstreet-map.ts b/ui-ngx/src/app/modules/home/components/widget/lib/maps-legacy/providers/openstreet-map.ts similarity index 100% rename from ui-ngx/src/app/modules/home/components/widget/lib/maps/providers/openstreet-map.ts rename to ui-ngx/src/app/modules/home/components/widget/lib/maps-legacy/providers/openstreet-map.ts diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/maps/providers/public-api.ts b/ui-ngx/src/app/modules/home/components/widget/lib/maps-legacy/providers/public-api.ts similarity index 93% rename from ui-ngx/src/app/modules/home/components/widget/lib/maps/providers/public-api.ts rename to ui-ngx/src/app/modules/home/components/widget/lib/maps-legacy/providers/public-api.ts index 4915002d43..80938e5839 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/maps/providers/public-api.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/maps-legacy/providers/public-api.ts @@ -20,7 +20,7 @@ import { GoogleMap } from './google-map'; import { HEREMap } from './here-map'; import { ImageMap } from './image-map'; import { Type } from '@angular/core'; -import LeafletMap from '@home/components/widget/lib/maps/leaflet-map'; +import LeafletMap from '@home/components/widget/lib/maps-legacy/leaflet-map'; export const providerClass: { [key: string]: Type } = { 'openstreet-map': OpenStreetMap, diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/maps/providers/tencent-map.ts b/ui-ngx/src/app/modules/home/components/widget/lib/maps-legacy/providers/tencent-map.ts similarity index 100% rename from ui-ngx/src/app/modules/home/components/widget/lib/maps/providers/tencent-map.ts rename to ui-ngx/src/app/modules/home/components/widget/lib/maps-legacy/providers/tencent-map.ts diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/maps/leaflet/leaflet-tb.ts b/ui-ngx/src/app/modules/home/components/widget/lib/maps/leaflet/leaflet-tb.ts new file mode 100644 index 0000000000..e69de29bb2 diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/maps/map-layer.ts b/ui-ngx/src/app/modules/home/components/widget/lib/maps/map-layer.ts new file mode 100644 index 0000000000..12952bf0d5 --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/widget/lib/maps/map-layer.ts @@ -0,0 +1,90 @@ +import { + defaultOpenStreetMapLayerSettings, + MapLayerSettings, + MapProvider, OpenStreetLayerType, + OpenStreetMapLayerSettings, openStreetMapLayerTranslationMap +} from '@home/components/widget/lib/maps/map.models'; +import { WidgetContext } from '@home/models/widget-component.models'; +import { DeepPartial } from '@shared/models/common'; +import { mergeDeep } from '@core/utils'; +import { Observable, of } from 'rxjs'; +import { CustomTranslatePipe } from '@shared/pipe/custom-translate.pipe'; +import L from 'leaflet'; +import { map } from 'rxjs/operators'; + +export abstract class TbMapLayer { + + static fromSettings(ctx: WidgetContext, + inputSettings: DeepPartial) { + + switch (inputSettings.provider) { + case MapProvider.google: + break; + case MapProvider.openstreet: + return new TbOpenStreetMapLayer(ctx, inputSettings); + case MapProvider.tencent: + break; + case MapProvider.here: + break; + case MapProvider.custom: + break; + } + } + + protected settings: S; + + protected constructor(protected ctx: WidgetContext, + protected inputSettings: DeepPartial) { + this.settings = mergeDeep({} as S, this.defaultSettings(), this.inputSettings as S); + } + + protected abstract defaultSettings(): S; + + protected title(): string { + const customTranslate = this.ctx.$injector.get(CustomTranslatePipe); + if (this.settings.label) { + return customTranslate.transform(this.settings.label); + } else { + return this.generateTitle(); + } + } + + protected abstract generateTitle(): string; + + protected abstract createLayer(): Observable; + + public loadLayer(): Observable<{title: string, layer: L.Layer}> { + return this.createLayer().pipe( + map((layer) => { + return { + title: this.title(), + layer + }; + }) + ); + } + +} + +class TbOpenStreetMapLayer extends TbMapLayer { + + constructor(protected ctx: WidgetContext, + protected inputSettings: DeepPartial) { + super(ctx, inputSettings); + } + + protected defaultSettings(): OpenStreetMapLayerSettings { + return defaultOpenStreetMapLayerSettings; + } + + protected generateTitle(): string { + const layerType = OpenStreetLayerType[this.settings.layerType]; + return this.ctx.translate.instant(openStreetMapLayerTranslationMap.get(layerType)); + } + + protected createLayer(): Observable { + const layer = L.tileLayer.provider(OpenStreetLayerType[this.settings.layerType]); + return of(layer); + } + +} diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/maps/map-widget.component.html b/ui-ngx/src/app/modules/home/components/widget/lib/maps/map-widget.component.html new file mode 100644 index 0000000000..09c44e1841 --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/widget/lib/maps/map-widget.component.html @@ -0,0 +1,25 @@ + +
+
+ + + +
+
+
diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/maps/map-widget.component.scss b/ui-ngx/src/app/modules/home/components/widget/lib/maps/map-widget.component.scss new file mode 100644 index 0000000000..a47b5eb578 --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/widget/lib/maps/map-widget.component.scss @@ -0,0 +1,38 @@ +/** + * Copyright © 2016-2024 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +.tb-map-panel { + width: 100%; + height: 100%; + position: relative; + display: flex; + flex-direction: column; + gap: 8px; + padding: 20px 24px 24px 24px; + > div:not(.tb-map-overlay) { + z-index: 1; + } + .tb-map-overlay { + position: absolute; + top: 12px; + left: 12px; + bottom: 12px; + right: 12px; + } + div.tb-widget-title { + padding: 0; + } +} diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/maps/map-widget.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/maps/map-widget.component.ts new file mode 100644 index 0000000000..58d4d4fcc0 --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/widget/lib/maps/map-widget.component.ts @@ -0,0 +1,97 @@ +/// +/// Copyright © 2016-2024 The Thingsboard Authors +/// +/// Licensed under the Apache License, Version 2.0 (the "License"); +/// you may not use this file except in compliance with the License. +/// You may obtain a copy of the License at +/// +/// http://www.apache.org/licenses/LICENSE-2.0 +/// +/// Unless required by applicable law or agreed to in writing, software +/// distributed under the License is distributed on an "AS IS" BASIS, +/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +/// See the License for the specific language governing permissions and +/// limitations under the License. +/// + +import { + AfterViewInit, + ChangeDetectorRef, + Component, + ElementRef, + Input, + OnDestroy, + OnInit, + Renderer2, + TemplateRef, + ViewChild, + ViewEncapsulation +} from '@angular/core'; +import { mapWidgetDefaultSettings, MapWidgetSettings } from '@home/components/widget/lib/maps/map-widget.models'; +import { WidgetContext } from '@home/models/widget-component.models'; +import { Observable } from 'rxjs'; +import { backgroundStyle, ComponentStyle, overlayStyle } from '@shared/models/widget-settings.models'; +import { TbMap } from '@home/components/widget/lib/maps/map'; +import { MapSetting } from '@home/components/widget/lib/maps/map.models'; +import { WidgetComponent } from '@home/components/widget/widget.component'; +import { ImagePipe } from '@shared/pipe/image.pipe'; +import { DomSanitizer } from '@angular/platform-browser'; + +@Component({ + selector: 'tb-map-widget', + templateUrl: './map-widget.component.html', + styleUrls: ['./map-widget.component.scss'], + encapsulation: ViewEncapsulation.None +}) +export class MapWidgetComponent implements OnInit, OnDestroy, AfterViewInit { + + @ViewChild('mapElement', {static: false}) + mapElement: ElementRef; + + settings: MapWidgetSettings; + + @Input() + ctx: WidgetContext; + + @Input() + widgetTitlePanel: TemplateRef; + + backgroundStyle$: Observable; + overlayStyle: ComponentStyle = {}; + padding: string; + + private map: TbMap; + + constructor(public widgetComponent: WidgetComponent, + private imagePipe: ImagePipe, + private sanitizer: DomSanitizer, + private renderer: Renderer2, + private cd: ChangeDetectorRef) { + } + + ngOnInit(): void { + this.ctx.$scope.mapWidget = this; + this.settings = {...mapWidgetDefaultSettings, ...this.ctx.settings}; + + this.backgroundStyle$ = backgroundStyle(this.settings.background, this.imagePipe, this.sanitizer); + this.overlayStyle = overlayStyle(this.settings.background.overlay); + this.padding = this.settings.background.overlay.enabled ? undefined : this.settings.padding; + } + + ngAfterViewInit() { + this.map = TbMap.fromSettings(this.ctx, this.settings, this.mapElement.nativeElement); + } + + ngOnDestroy() { + if (this.map) { + this.map.destroy(); + } + } + + public onInit() { + const borderRadius = this.ctx.$widgetElement.css('borderRadius'); + this.overlayStyle = {...this.overlayStyle, ...{borderRadius}}; + this.cd.detectChanges(); + } + +} diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/maps/map-widget.models.ts b/ui-ngx/src/app/modules/home/components/widget/lib/maps/map-widget.models.ts new file mode 100644 index 0000000000..1d7f5a86e3 --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/widget/lib/maps/map-widget.models.ts @@ -0,0 +1,38 @@ +/// +/// Copyright © 2016-2024 The Thingsboard Authors +/// +/// Licensed under the Apache License, Version 2.0 (the "License"); +/// you may not use this file except in compliance with the License. +/// You may obtain a copy of the License at +/// +/// http://www.apache.org/licenses/LICENSE-2.0 +/// +/// Unless required by applicable law or agreed to in writing, software +/// distributed under the License is distributed on an "AS IS" BASIS, +/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +/// See the License for the specific language governing permissions and +/// limitations under the License. +/// + +import { defaultMapSettings, MapSetting } from '@home/components/widget/lib/maps/map.models'; +import { BackgroundSettings, BackgroundType } from '@shared/models/widget-settings.models'; +import { mergeDeep } from '@core/utils'; + +export interface MapWidgetSettings extends MapSetting { + background: BackgroundSettings; + padding: string; +} + +export const mapWidgetDefaultSettings: MapWidgetSettings = + mergeDeep({} as MapWidgetSettings, defaultMapSettings as MapWidgetSettings, { + background: { + type: BackgroundType.color, + color: '#fff', + overlay: { + enabled: false, + color: 'rgba(255,255,255,0.72)', + blur: 3 + } + }, + padding: '12px' +} as MapWidgetSettings); diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/maps/map.models.ts b/ui-ngx/src/app/modules/home/components/widget/lib/maps/map.models.ts new file mode 100644 index 0000000000..d618240d7a --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/widget/lib/maps/map.models.ts @@ -0,0 +1,310 @@ +/// +/// Copyright © 2016-2024 The Thingsboard Authors +/// +/// Licensed under the Apache License, Version 2.0 (the "License"); +/// you may not use this file except in compliance with the License. +/// You may obtain a copy of the License at +/// +/// http://www.apache.org/licenses/LICENSE-2.0 +/// +/// Unless required by applicable law or agreed to in writing, software +/// distributed under the License is distributed on an "AS IS" BASIS, +/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +/// See the License for the specific language governing permissions and +/// limitations under the License. +/// + +import { DataKey, DatasourceType } from '@shared/models/widget.models'; +import { EntityType } from '@shared/models/entity-type.models'; +import { DataKeyType } from '@shared/models/telemetry/telemetry.models'; +import { mergeDeep } from '@core/utils'; + +export enum MapType { + geoMap = 'geoMap', + image = 'image' +} + +export interface MapDataSourceSettings { + dsType: DatasourceType; + dsEntityType?: EntityType; + dsEntityId?: string; + dsEntityAliasId?: string; + dsFilterId?: string; +} + +export interface MapDataLayerSettings extends MapDataSourceSettings { + additionalDataKeys?: DataKey[]; + group?: string; +} + +export interface MarkersDataLayerSettings extends MapDataLayerSettings { + xKey: DataKey; + yKey: DataKey; +} + +export const defaultMarkersDataLayerSettings = (mapType: MapType): MarkersDataLayerSettings => ({ + dsType: DatasourceType.entity, + xKey: { + name: MapType.geoMap === mapType ? 'latitude' : 'xPos', + label: MapType.geoMap === mapType ? 'latitude' : 'xPos', + type: DataKeyType.attribute + }, + yKey: { + name: MapType.geoMap === mapType ? 'longitude' : 'yPos', + label: MapType.geoMap === mapType ? 'longitude' : 'yPos', + type: DataKeyType.attribute + } +}); + +export interface PolygonsDataLayerSettings extends MapDataLayerSettings { + polygonKey: DataKey; +} + +export const defaultPolygonsDataLayerSettings: PolygonsDataLayerSettings = { + dsType: DatasourceType.entity, + polygonKey: { + name: 'perimeter', + label: 'perimeter', + type: DataKeyType.attribute + } +}; + +export interface CirclesDataLayerSettings extends MapDataLayerSettings { + circleKey: DataKey; +} + +export const defaultCirclesDataLayerSettings: CirclesDataLayerSettings = { + dsType: DatasourceType.entity, + circleKey: { + name: 'perimeter', + label: 'perimeter', + type: DataKeyType.attribute + } +}; + +export interface AdditionalMapDataSourceSettings extends MapDataSourceSettings { + dataKeys: DataKey[]; +} + +export enum MapControlsPosition { + topleft = 'topleft', + topright = 'topright', + bottomleft = 'bottomleft', + bottomright = 'bottomright' +} + +export enum MapZoomAction { + scroll = 'scroll', + doubleClick = 'doubleClick', + controlButtons = 'controlButtons' +} + +export interface BaseMapSettings { + mapType: MapType; + markers: MarkersDataLayerSettings[]; + polygons: PolygonsDataLayerSettings[]; + circles: CirclesDataLayerSettings[]; + additionalDataSources: AdditionalMapDataSourceSettings[]; + controlsPosition: MapControlsPosition; + zoomActions: MapZoomAction[]; + fitMapBounds: boolean; + useDefaultCenterPosition: boolean; + defaultCenterPosition?: string; + defaultZoomLevel: number; + mapPageSize: number; +} + +export const DEFAULT_MAP_PAGE_SIZE = 16384; +export const DEFAULT_ZOOM_LEVEL = 8; + +export const defaultBaseMapSettings: BaseMapSettings = { + mapType: MapType.geoMap, + markers: [], + polygons: [], + circles: [], + additionalDataSources: [], + controlsPosition: MapControlsPosition.topleft, + zoomActions: [MapZoomAction.scroll, MapZoomAction.doubleClick, MapZoomAction.controlButtons], + fitMapBounds: true, + useDefaultCenterPosition: false, + defaultCenterPosition: '0,0', + defaultZoomLevel: null, + mapPageSize: DEFAULT_MAP_PAGE_SIZE +}; + +export enum MapProvider { + google = 'google-map', + openstreet = 'openstreet-map', + here = 'here', + tencent = 'tencent-map', + custom = 'custom' +} + +export interface MapLayerSettings { + label?: string; + provider: MapProvider; +} + +export enum GoogleLayerType { + roadmap = 'roadmap', + satellite = 'satellite', + hybrid = 'hybrid', + terrain = 'terrain' +} + +export interface GoogleMapLayerSettings extends MapLayerSettings { + provider: MapProvider.google; + layerType: GoogleLayerType; + apiKey: string; +} + +export const defaultGoogleMapLayerSettings: GoogleMapLayerSettings = { + provider: MapProvider.google, + layerType: GoogleLayerType.roadmap, + apiKey: 'AIzaSyDoEx2kaGz3PxwbI9T7ccTSg5xjdw8Nw8Q' +}; + +export enum OpenStreetLayerType { + openStreetMapnik = 'OpenStreetMap.Mapnik', + openStreetHot = 'OpenStreetMap.HOT', + esriWorldStreetMap = 'Esri.WorldStreetMap', + esriWorldTopoMap = 'Esri.WorldTopoMap', + esriWorldImagery = 'Esri.WorldImagery', + cartoDbPositron = 'CartoDB.Positron', + cartoDbDarkMatter = 'CartoDB.DarkMatter' +} + +export const openStreetMapLayerTranslationMap = new Map( + [ + [OpenStreetLayerType.openStreetMapnik, 'widgets.maps.openstreet-provider-mapnik'], + [OpenStreetLayerType.openStreetHot, 'widgets.maps.openstreet-provider-hot'], + [OpenStreetLayerType.esriWorldStreetMap, 'widgets.maps.openstreet-provider-esri-street'], + [OpenStreetLayerType.esriWorldTopoMap, 'widgets.maps.openstreet-provider-esri-topo'], + [OpenStreetLayerType.esriWorldImagery, 'widgets.maps.openstreet-provider-esri-imagery'], + [OpenStreetLayerType.cartoDbPositron, 'widgets.maps.openstreet-provider-cartodb-positron'], + [OpenStreetLayerType.cartoDbDarkMatter, 'widgets.maps.openstreet-provider-cartodb-dark-matter'] + ] +); + +export interface OpenStreetMapLayerSettings extends MapLayerSettings { + provider: MapProvider.openstreet; + layerType: OpenStreetLayerType; +} + +export const defaultOpenStreetMapLayerSettings: OpenStreetMapLayerSettings = { + provider: MapProvider.openstreet, + layerType: OpenStreetLayerType.openStreetMapnik +} + +export enum HereLayerType { + hereNormalDay = 'HERE.normalDay', + hereNormalNight = 'HERE.normalNight', + hereHybridDay = 'HERE.hybridDay', + hereTerrainDay = 'HERE.terrainDay' +} + +export interface HereMapLayerSettings extends MapLayerSettings { + provider: MapProvider.here; + layerType: HereLayerType; + apiKey: string; +} + +export const defaultHereMapLayerSettings: HereMapLayerSettings = { + provider: MapProvider.here, + layerType: HereLayerType.hereNormalDay, + apiKey: 'kVXykxAfZ6LS4EbCTO02soFVfjA7HoBzNVVH9u7nzoE' +} + +export enum TencentLayerType { + roadmap = 'roadmap', + satellite = 'satellite', + hybrid = 'hybrid' +} + +export interface TencentMapLayerSettings extends MapLayerSettings { + provider: MapProvider.tencent; + layerType: TencentLayerType + apiKey: string; +} + +export const defaultTencentMapLayerSettings: TencentMapLayerSettings = { + provider: MapProvider.tencent, + layerType: TencentLayerType.roadmap, + apiKey: '84d6d83e0e51e481e50454ccbe8986b' +} + +export interface CustomMapLayerSettings extends MapLayerSettings { + provider: MapProvider.custom; + tileUrl: string; +} + +export const defaultCustomMapLayerSettings: CustomMapLayerSettings = { + provider: MapProvider.custom, + tileUrl: 'https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png' +} + +export const defaultMapLayerSettings = (provider: MapProvider): MapLayerSettings => { + switch (provider) { + case MapProvider.google: + return defaultGoogleMapLayerSettings; + case MapProvider.openstreet: + return defaultOpenStreetMapLayerSettings; + case MapProvider.here: + return defaultHereMapLayerSettings; + case MapProvider.tencent: + return defaultTencentMapLayerSettings; + case MapProvider.custom: + return defaultCustomMapLayerSettings; + } +}; + +export const defaultMapLayers: MapLayerSettings[] = (Object.keys(OpenStreetLayerType) as OpenStreetLayerType[]).map(type => ({ + provider: MapProvider.openstreet, + layerType: type +})); + +export interface GeoMapSettings extends BaseMapSettings { + layers?: MapLayerSettings[]; +} + +export const defaultGeoMapSettings: GeoMapSettings = { + mapType: MapType.geoMap, + layers: mergeDeep([], defaultMapLayers), + ...mergeDeep({} as BaseMapSettings, defaultBaseMapSettings) +}; + +export enum ImageSourceType { + image = 'image', + attribute = 'attribute' +} + +export interface ImageMapSettings extends BaseMapSettings { + imageSourceType?: ImageSourceType; + imageUrl?: string; + imageEntityAlias?: string; + imageUrlAttribute?: string; +} + +export const defaultImageMapSettings: ImageMapSettings = { + mapType: MapType.image, + imageSourceType: ImageSourceType.image, + imageUrl: 'data:image/svg+xml;base64,PHN2ZyBpZD0ic3ZnMiIgeG1sbnM6cmRmPSJodHRwOi8vd3d3LnczLm9yZy8xOTk5LzAyLzIyLXJkZi1zeW50YXgtbnMjIiB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIGhlaWdodD0iMTAwIiB3aWR0aD0iMTAwIiB2ZXJzaW9uPSIxLjEiIHhtbG5zOmNjPSJodHRwOi8vY3JlYXRpdmVjb21tb25zLm9yZy9ucyMiIHhtbG5zOmRjPSJodHRwOi8vcHVybC5vcmcvZGMvZWxlbWVudHMvMS4xLyIgdmlld0JveD0iMCAwIDEwMCAxMDAiPgogPGcgaWQ9ImxheWVyMSIgdHJhbnNmb3JtPSJ0cmFuc2xhdGUoMCAtOTUyLjM2KSI+CiAgPHJlY3QgaWQ9InJlY3Q0Njg0IiBzdHJva2UtbGluZWpvaW49InJvdW5kIiBoZWlnaHQ9Ijk5LjAxIiB3aWR0aD0iOTkuMDEiIHN0cm9rZT0iIzAwMCIgc3Ryb2tlLWxpbmVjYXA9InJvdW5kIiB5PSI5NTIuODYiIHg9Ii40OTUwNSIgc3Ryb2tlLXdpZHRoPSIuOTkwMTAiIGZpbGw9IiNlZWUiLz4KICA8dGV4dCBpZD0idGV4dDQ2ODYiIHN0eWxlPSJ3b3JkLXNwYWNpbmc6MHB4O2xldHRlci1zcGFjaW5nOjBweDt0ZXh0LWFuY2hvcjptaWRkbGU7dGV4dC1hbGlnbjpjZW50ZXIiIGZvbnQtd2VpZ2h0PSJib2xkIiB4bWw6c3BhY2U9InByZXNlcnZlIiBmb250LXNpemU9IjEwcHgiIGxpbmUtaGVpZ2h0PSIxMjUlIiB5PSI5NzAuNzI4MDkiIHg9IjQ5LjM5NjQ3NyIgZm9udC1mYW1pbHk9IlJvYm90byIgZmlsbD0iIzY2NjY2NiI+PHRzcGFuIGlkPSJ0c3BhbjQ2OTAiIHg9IjUwLjY0NjQ3NyIgeT0iOTcwLjcyODA5Ij5JbWFnZSBiYWNrZ3JvdW5kIDwvdHNwYW4+PHRzcGFuIGlkPSJ0c3BhbjQ2OTIiIHg9IjQ5LjM5NjQ3NyIgeT0iOTgzLjIyODA5Ij5pcyBub3QgY29uZmlndXJlZDwvdHNwYW4+PC90ZXh0PgogIDxyZWN0IGlkPSJyZWN0NDY5NCIgc3Ryb2tlLWxpbmVqb2luPSJyb3VuZCIgaGVpZ2h0PSIxOS4zNiIgd2lkdGg9IjY5LjM2IiBzdHJva2U9IiMwMDAiIHN0cm9rZS1saW5lY2FwPSJyb3VuZCIgeT0iOTkyLjY4IiB4PSIxNS4zMiIgc3Ryb2tlLXdpZHRoPSIuNjM5ODYiIGZpbGw9Im5vbmUiLz4KIDwvZz4KPC9zdmc+Cg==', + ...mergeDeep({} as BaseMapSettings, defaultBaseMapSettings) +} + +export type MapSetting = GeoMapSettings & ImageMapSettings; + +export const defaultMapSettings: MapSetting = defaultGeoMapSettings; + +export function parseCenterPosition(position: string | [number, number]): [number, number] { + if (typeof (position) === 'string') { + const parts = position.split(','); + if (parts.length === 2) { + return [Number(parts[0]), Number(parts[1])]; + } + } + if (typeof (position) === 'object') { + return position; + } + return [0, 0]; +} diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/maps/map.scss b/ui-ngx/src/app/modules/home/components/widget/lib/maps/map.scss new file mode 100644 index 0000000000..c01ca9e3fc --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/widget/lib/maps/map.scss @@ -0,0 +1,19 @@ +.tb-map-layout { + display: flex; + width: 100%; + height: 100%; + min-width: 0; + min-height: 0; + flex: 1; + &.tb-sidebar-left { + flex-direction: row-reverse; + } + &.tb-sidebar-right { + flex-direction: row; + } + .tb-map { + flex: 1; + } + .tb-map-sidebar { + } +} 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 new file mode 100644 index 0000000000..f4fd0baaac --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/widget/lib/maps/map.ts @@ -0,0 +1,196 @@ +/// +/// Copyright © 2016-2024 The Thingsboard Authors +/// +/// Licensed under the Apache License, Version 2.0 (the "License"); +/// you may not use this file except in compliance with the License. +/// You may obtain a copy of the License at +/// +/// http://www.apache.org/licenses/LICENSE-2.0 +/// +/// Unless required by applicable law or agreed to in writing, software +/// distributed under the License is distributed on an "AS IS" BASIS, +/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +/// See the License for the specific language governing permissions and +/// limitations under the License. +/// + +import { + BaseMapSettings, + DEFAULT_ZOOM_LEVEL, + defaultGeoMapSettings, + defaultImageMapSettings, + GeoMapSettings, + ImageMapSettings, + MapSetting, + MapType, + MapZoomAction, + parseCenterPosition +} from '@home/components/widget/lib/maps/map.models'; +import { WidgetContext } from '@home/models/widget-component.models'; +import { mergeDeep } from '@core/utils'; +import { DeepPartial } from '@shared/models/common'; +import L from 'leaflet'; +import { forkJoin, Observable, of } from 'rxjs'; +import { TbMapLayer } from '@home/components/widget/lib/maps/map-layer'; +import { map } from 'rxjs/operators'; + +export abstract class TbMap { + + static fromSettings(ctx: WidgetContext, + inputSettings: DeepPartial, + mapElement: HTMLElement): TbMap { + switch (inputSettings.mapType) { + case MapType.geoMap: + return new TbGeoMap(ctx, inputSettings, mapElement); + case MapType.image: + return new TbImageMap(ctx, inputSettings, mapElement); + } + } + + protected settings: S; + protected map: L.Map; + + protected defaultCenterPosition: [number, number]; + protected bounds: L.LatLngBounds; + protected layerControl: L.Control.Layers; + + protected mapElement: HTMLElement; + protected sidebarElement: HTMLElement; + + protected constructor(protected ctx: WidgetContext, + protected inputSettings: DeepPartial, + protected containerElement: HTMLElement) { + this.settings = mergeDeep({} as S, this.defaultSettings(), this.inputSettings as S); + $(containerElement).empty(); + $(containerElement).addClass('tb-map-layout'); + if (this.settings.controlsPosition.endsWith('left')) { + $(containerElement).addClass('tb-sidebar-left'); + } else { + $(containerElement).addClass('tb-sidebar-right'); + } + const mapElement = $('
'); + const sidebarElement = $('
'); + $(containerElement).append(mapElement); + $(containerElement).append(sidebarElement); + + this.mapElement = mapElement[0]; + this.sidebarElement = sidebarElement[0]; + + this.defaultCenterPosition = parseCenterPosition(this.settings.defaultCenterPosition); + + this.layerControl = L.control.layers({}, {}, {position: this.settings.controlsPosition, collapsed: true}); + this.createMap().subscribe((map) => { + this.map = map; + this.initMap(); + }); + L.TB = { + sidebar: (s) => { return null;} + }; + } + + private initMap() { + this.map.zoomControl.setPosition(this.settings.controlsPosition); + this.layerControl.addTo(this.map); + this.map.on('move', () => { + this.ctx.updatePopoverPositions(); + }); + this.map.on('zoomstart', () => { + this.ctx.setPopoversHidden(true); + }); + this.map.on('zoomend', () => { + this.ctx.setPopoversHidden(false); + this.ctx.updatePopoverPositions(); + setTimeout(() => { + this.ctx.updatePopoverPositions(); + }); + }); + if (this.settings.useDefaultCenterPosition) { + this.map.panTo(this.defaultCenterPosition); + this.bounds = this.map.getBounds(); + } else { + this.bounds = new L.LatLngBounds(null, null); + } + } + + protected abstract defaultSettings(): S; + + protected abstract createMap(): Observable; + + public destroy() { + if (this.map) { + this.map.remove(); + } + } + +} + +class TbGeoMap extends TbMap { + + constructor(protected ctx: WidgetContext, + protected inputSettings: DeepPartial, + protected containerElement: HTMLElement) { + super(ctx, inputSettings, containerElement); + } + + protected defaultSettings(): GeoMapSettings { + return defaultGeoMapSettings; + } + + protected createMap(): Observable { + const theMap = L.map(this.mapElement, { + scrollWheelZoom: this.settings.zoomActions.includes(MapZoomAction.scroll), + doubleClickZoom: this.settings.zoomActions.includes(MapZoomAction.doubleClick), + zoomControl: this.settings.zoomActions.includes(MapZoomAction.controlButtons) + }).setView(this.defaultCenterPosition, this.settings.defaultZoomLevel || DEFAULT_ZOOM_LEVEL); + return this.loadLayers().pipe( + map((layers) => { + if (layers.length) { + const layer = layers[0]; + layer.layer.addTo(theMap); + if (layers.length > 1) { + layers.forEach(l => { + this.layerControl.addBaseLayer(l.layer, l.title); + }); + } + } + return theMap; + }) + ); + } + + private loadLayers(): Observable<{title: string, layer: L.Layer}[]> { + const layers = this.settings.layers.map(settings => TbMapLayer.fromSettings(this.ctx, settings)); + return forkJoin(layers.map(layer => layer.loadLayer())); + } + + +} + +class TbImageMap extends TbMap { + + private maxZoom = 4; + + constructor(protected ctx: WidgetContext, + protected inputSettings: DeepPartial, + protected mapElement: HTMLElement) { + super(ctx, inputSettings, mapElement); + } + + protected defaultSettings(): ImageMapSettings { + return defaultImageMapSettings; + } + + protected createMap(): Observable { + const map = L.map(this.mapElement, { + scrollWheelZoom: this.settings.zoomActions.includes(MapZoomAction.scroll), + doubleClickZoom: this.settings.zoomActions.includes(MapZoomAction.doubleClick), + zoomControl: this.settings.zoomActions.includes(MapZoomAction.controlButtons), + minZoom: 1, + maxZoom: this.maxZoom, + zoom: 1, + crs: L.CRS.Simple, + attributionControl: false + }).setView(this.defaultCenterPosition, this.settings.defaultZoomLevel || DEFAULT_ZOOM_LEVEL); + return of(map); + } +} diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/map/circle-settings.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/settings/map/circle-settings.component.ts index 704f2b3b5b..5708293748 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/settings/map/circle-settings.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/map/circle-settings.component.ts @@ -33,7 +33,7 @@ import { CircleSettings, ShowTooltipAction, showTooltipActionTranslationMap -} from '@home/components/widget/lib/maps/map-models'; +} from '@home/components/widget/lib/maps-legacy/map-models'; import { WidgetService } from '@core/http/widget.service'; import { Widget } from '@shared/models/widget.models'; import { takeUntilDestroyed } from '@angular/core/rxjs-interop'; diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/map/common-map-settings.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/settings/map/common-map-settings.component.ts index 09df84593d..2e9fa0dc9f 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/settings/map/common-map-settings.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/map/common-map-settings.component.ts @@ -29,7 +29,7 @@ import { PageComponent } from '@shared/components/page.component'; import { Store } from '@ngrx/store'; import { AppState } from '@core/core.state'; import { TranslateService } from '@ngx-translate/core'; -import { CommonMapSettings, MapProviders } from '@home/components/widget/lib/maps/map-models'; +import { CommonMapSettings, MapProviders } from '@home/components/widget/lib/maps-legacy/map-models'; import { Widget } from '@shared/models/widget.models'; import { takeUntilDestroyed } from '@angular/core/rxjs-interop'; diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/map/google-map-provider-settings.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/settings/map/google-map-provider-settings.component.ts index dde7175b1e..e42849112a 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/settings/map/google-map-provider-settings.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/map/google-map-provider-settings.component.ts @@ -33,7 +33,7 @@ import { GoogleMapProviderSettings, GoogleMapType, googleMapTypeProviderTranslationMap -} from '@home/components/widget/lib/maps/map-models'; +} from '@home/components/widget/lib/maps-legacy/map-models'; import { takeUntilDestroyed } from '@angular/core/rxjs-interop'; @Component({ diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/map/here-map-provider-settings.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/settings/map/here-map-provider-settings.component.ts index a5b63b26ee..2ee44ec2c1 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/settings/map/here-map-provider-settings.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/map/here-map-provider-settings.component.ts @@ -33,7 +33,7 @@ import { HereMapProvider, HereMapProviderSettings, hereMapProviderTranslationMap -} from '@home/components/widget/lib/maps/map-models'; +} from '@home/components/widget/lib/maps-legacy/map-models'; import { isDefinedAndNotNull } from '@core/utils'; import { takeUntilDestroyed } from '@angular/core/rxjs-interop'; diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/map/image-map-provider-settings.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/settings/map/image-map-provider-settings.component.ts index 9c655137a0..0117e233f7 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/settings/map/image-map-provider-settings.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/map/image-map-provider-settings.component.ts @@ -28,7 +28,7 @@ import { PageComponent } from '@shared/components/page.component'; import { Store } from '@ngrx/store'; import { AppState } from '@core/core.state'; import { TranslateService } from '@ngx-translate/core'; -import { ImageMapProviderSettings } from '@home/components/widget/lib/maps/map-models'; +import { ImageMapProviderSettings } from '@home/components/widget/lib/maps-legacy/map-models'; import { IAliasController } from '@core/api/widget-api.models'; import { Observable, of } from 'rxjs'; import { catchError, map, mergeMap, publishReplay, refCount, startWith, tap } from 'rxjs/operators'; diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/map/map-editor-settings.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/settings/map/map-editor-settings.component.ts index 7fed20ba9e..e448a5c1a2 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/settings/map/map-editor-settings.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/map/map-editor-settings.component.ts @@ -28,7 +28,7 @@ import { PageComponent } from '@shared/components/page.component'; import { Store } from '@ngrx/store'; import { AppState } from '@core/core.state'; import { TranslateService } from '@ngx-translate/core'; -import { MapEditorSettings } from '@home/components/widget/lib/maps/map-models'; +import { MapEditorSettings } from '@home/components/widget/lib/maps-legacy/map-models'; import { WidgetService } from '@core/http/widget.service'; import { takeUntilDestroyed } from '@angular/core/rxjs-interop'; diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/map/map-provider-settings.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/settings/map/map-provider-settings.component.ts index 2bc7b93f76..fffe4aff7d 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/settings/map/map-provider-settings.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/map/map-provider-settings.component.ts @@ -41,7 +41,7 @@ import { mapProviderTranslationMap, OpenStreetMapProviderSettings, TencentMapProviderSettings -} from '@home/components/widget/lib/maps/map-models'; +} from '@home/components/widget/lib/maps-legacy/map-models'; import { extractType } from '@core/utils'; import { IAliasController } from '@core/api/widget-api.models'; import { takeUntilDestroyed } from '@angular/core/rxjs-interop'; diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/map/map-settings.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/settings/map/map-settings.component.ts index ad4ead491a..bb9da1b3ce 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/settings/map/map-settings.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/map/map-settings.component.ts @@ -46,7 +46,7 @@ import { PolygonSettings, PolylineSettings, UnitedMapSettings -} from '@home/components/widget/lib/maps/map-models'; +} from '@home/components/widget/lib/maps-legacy/map-models'; import { extractType } from '@core/utils'; import { IAliasController } from '@core/api/widget-api.models'; import { Widget } from '@shared/models/widget.models'; diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/map/map-widget-settings.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/settings/map/map-widget-settings.component.ts index 34980009c3..1ff63ff246 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/settings/map/map-widget-settings.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/map/map-widget-settings.component.ts @@ -19,7 +19,7 @@ import { WidgetSettings, WidgetSettingsComponent } from '@shared/models/widget.m import { UntypedFormBuilder, UntypedFormGroup } from '@angular/forms'; import { Store } from '@ngrx/store'; import { AppState } from '@core/core.state'; -import { defaultMapSettings } from 'src/app/modules/home/components/widget/lib/maps/map-models'; +import { defaultMapSettings } from 'src/app/modules/home/components/widget/lib/maps-legacy/map-models'; @Component({ selector: 'tb-map-widget-settings', diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/map/marker-clustering-settings.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/settings/map/marker-clustering-settings.component.ts index d648f951d9..c95f70b1d4 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/settings/map/marker-clustering-settings.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/map/marker-clustering-settings.component.ts @@ -29,7 +29,7 @@ import { PageComponent } from '@shared/components/page.component'; import { Store } from '@ngrx/store'; import { AppState } from '@core/core.state'; import { TranslateService } from '@ngx-translate/core'; -import { MarkerClusteringSettings } from '@home/components/widget/lib/maps/map-models'; +import { MarkerClusteringSettings } from '@home/components/widget/lib/maps-legacy/map-models'; import { WidgetService } from '@core/http/widget.service'; import { takeUntilDestroyed } from '@angular/core/rxjs-interop'; diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/map/markers-settings.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/settings/map/markers-settings.component.ts index 33ef1526ed..be9296270c 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/settings/map/markers-settings.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/map/markers-settings.component.ts @@ -31,7 +31,7 @@ import { TranslateService } from '@ngx-translate/core'; import { MapProviders, MarkersSettings, ShowTooltipAction, showTooltipActionTranslationMap -} from '@home/components/widget/lib/maps/map-models'; +} from '@home/components/widget/lib/maps-legacy/map-models'; import { WidgetService } from '@core/http/widget.service'; import { takeUntilDestroyed } from '@angular/core/rxjs-interop'; diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/map/openstreet-map-provider-settings.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/settings/map/openstreet-map-provider-settings.component.ts index 14675fb5ef..8006ad5e28 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/settings/map/openstreet-map-provider-settings.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/map/openstreet-map-provider-settings.component.ts @@ -33,7 +33,7 @@ import { OpenStreetMapProvider, OpenStreetMapProviderSettings, openStreetMapProviderTranslationMap -} from '@home/components/widget/lib/maps/map-models'; +} from '@home/components/widget/lib/maps-legacy/map-models'; import { takeUntilDestroyed } from '@angular/core/rxjs-interop'; @Component({ diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/map/polygon-settings.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/settings/map/polygon-settings.component.ts index ae0fd0c1f1..7cc8b9052c 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/settings/map/polygon-settings.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/map/polygon-settings.component.ts @@ -33,7 +33,7 @@ import { PolygonSettings, ShowTooltipAction, showTooltipActionTranslationMap -} from '@home/components/widget/lib/maps/map-models'; +} from '@home/components/widget/lib/maps-legacy/map-models'; import { WidgetService } from '@core/http/widget.service'; import { Widget } from '@shared/models/widget.models'; import { takeUntilDestroyed } from '@angular/core/rxjs-interop'; diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/map/route-map-settings.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/settings/map/route-map-settings.component.ts index 3ccbf9211c..f2f6442e89 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/settings/map/route-map-settings.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/map/route-map-settings.component.ts @@ -28,7 +28,7 @@ import { PageComponent } from '@shared/components/page.component'; import { Store } from '@ngrx/store'; import { AppState } from '@core/core.state'; import { TranslateService } from '@ngx-translate/core'; -import { PolylineSettings } from '@home/components/widget/lib/maps/map-models'; +import { PolylineSettings } from '@home/components/widget/lib/maps-legacy/map-models'; import { WidgetService } from '@core/http/widget.service'; import { takeUntilDestroyed } from '@angular/core/rxjs-interop'; diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/map/route-map-widget-settings.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/settings/map/route-map-widget-settings.component.ts index 8d5e640d59..6a72fc67e5 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/settings/map/route-map-widget-settings.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/map/route-map-widget-settings.component.ts @@ -19,7 +19,7 @@ import { WidgetSettings, WidgetSettingsComponent } from '@shared/models/widget.m import { UntypedFormBuilder, UntypedFormGroup } from '@angular/forms'; import { Store } from '@ngrx/store'; import { AppState } from '@core/core.state'; -import { defaultMapSettings } from 'src/app/modules/home/components/widget/lib/maps/map-models'; +import { defaultMapSettings } from 'src/app/modules/home/components/widget/lib/maps-legacy/map-models'; @Component({ selector: 'tb-route-map-widget-settings', diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/map/tencent-map-provider-settings.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/settings/map/tencent-map-provider-settings.component.ts index 868af4968c..a7b65cf451 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/settings/map/tencent-map-provider-settings.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/map/tencent-map-provider-settings.component.ts @@ -33,7 +33,7 @@ import { TencentMapProviderSettings, TencentMapType, tencentMapTypeProviderTranslationMap -} from '@home/components/widget/lib/maps/map-models'; +} from '@home/components/widget/lib/maps-legacy/map-models'; import { takeUntilDestroyed } from '@angular/core/rxjs-interop'; @Component({ diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/map/trip-animation-common-settings.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/settings/map/trip-animation-common-settings.component.ts index 1e7c484bdb..4a3b1cf6f9 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/settings/map/trip-animation-common-settings.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/map/trip-animation-common-settings.component.ts @@ -29,7 +29,7 @@ import { PageComponent } from '@shared/components/page.component'; import { Store } from '@ngrx/store'; import { AppState } from '@core/core.state'; import { TranslateService } from '@ngx-translate/core'; -import { TripAnimationCommonSettings } from '@home/components/widget/lib/maps/map-models'; +import { TripAnimationCommonSettings } from '@home/components/widget/lib/maps-legacy/map-models'; import { Widget } from '@shared/models/widget.models'; import { WidgetService } from '@core/http/widget.service'; import { takeUntilDestroyed } from '@angular/core/rxjs-interop'; diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/map/trip-animation-marker-settings.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/settings/map/trip-animation-marker-settings.component.ts index 422bcd3a2c..d8d065fe92 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/settings/map/trip-animation-marker-settings.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/map/trip-animation-marker-settings.component.ts @@ -29,7 +29,7 @@ import { PageComponent } from '@shared/components/page.component'; import { Store } from '@ngrx/store'; import { AppState } from '@core/core.state'; import { TranslateService } from '@ngx-translate/core'; -import { TripAnimationMarkerSettings } from '@home/components/widget/lib/maps/map-models'; +import { TripAnimationMarkerSettings } from '@home/components/widget/lib/maps-legacy/map-models'; import { WidgetService } from '@core/http/widget.service'; import { takeUntilDestroyed } from '@angular/core/rxjs-interop'; diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/map/trip-animation-path-settings.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/settings/map/trip-animation-path-settings.component.ts index e8fd9b64c5..9340428838 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/settings/map/trip-animation-path-settings.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/map/trip-animation-path-settings.component.ts @@ -32,7 +32,7 @@ import { PolylineDecoratorSymbol, polylineDecoratorSymbolTranslationMap, PolylineSettings -} from '@home/components/widget/lib/maps/map-models'; +} from '@home/components/widget/lib/maps-legacy/map-models'; import { WidgetService } from '@core/http/widget.service'; import { takeUntilDestroyed } from '@angular/core/rxjs-interop'; diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/map/trip-animation-point-settings.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/settings/map/trip-animation-point-settings.component.ts index 91ef694292..0317379954 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/settings/map/trip-animation-point-settings.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/map/trip-animation-point-settings.component.ts @@ -33,7 +33,7 @@ import { PolylineDecoratorSymbol, polylineDecoratorSymbolTranslationMap, PolylineSettings -} from '@home/components/widget/lib/maps/map-models'; +} from '@home/components/widget/lib/maps-legacy/map-models'; import { WidgetService } from '@core/http/widget.service'; import { takeUntilDestroyed } from '@angular/core/rxjs-interop'; diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/map/trip-animation-widget-settings.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/settings/map/trip-animation-widget-settings.component.ts index ab4ab92e17..336c776cd4 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/settings/map/trip-animation-widget-settings.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/map/trip-animation-widget-settings.component.ts @@ -35,7 +35,7 @@ import { PointsSettings, PolygonSettings, PolylineSettings -} from 'src/app/modules/home/components/widget/lib/maps/map-models'; +} from 'src/app/modules/home/components/widget/lib/maps-legacy/map-models'; import { extractType } from '@core/utils'; @Component({ diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/trip-animation/trip-animation.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/trip-animation/trip-animation.component.ts index 2fbfbc1968..3463141e2b 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/trip-animation/trip-animation.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/trip-animation/trip-animation.component.ts @@ -31,7 +31,7 @@ import { defaultTripAnimationSettings, MapProviders, WidgetUnitedTripAnimationSettings -} from '@home/components/widget/lib/maps/map-models'; +} from '@home/components/widget/lib/maps-legacy/map-models'; import { DomSanitizer, SafeHtml } from '@angular/platform-browser'; import { WidgetContext } from '@app/modules/home/models/widget-component.models'; import { @@ -39,7 +39,7 @@ import { getRatio, interpolateOnLineSegment, parseWithTranslation -} from '@home/components/widget/lib/maps/common-maps-utils'; +} from '@home/components/widget/lib/maps-legacy/common-maps-utils'; import { FormattedData, WidgetConfig } from '@shared/models/widget.models'; import moment from 'moment'; import { @@ -51,7 +51,7 @@ import { parseTbFunction, safeExecuteTbFunction } from '@core/utils'; -import { MapWidgetInterface } from '@home/components/widget/lib/maps/map-widget.interface'; +import { MapWidgetInterface } from '@home/components/widget/lib/maps-legacy/map-widget.interface'; import { firstValueFrom, from } from 'rxjs'; interface DataMap { @@ -123,7 +123,7 @@ export class TripAnimationComponent implements OnInit, AfterViewInit, OnDestroy } ngAfterViewInit() { - import('@home/components/widget/lib/maps/map-widget2').then( + import('@home/components/widget/lib/maps-legacy/map-widget2').then( (mod) => { this.mapWidget = new mod.MapWidgetController(MapProviders.openstreet, false, this.ctx, this.mapContainer.nativeElement, false, () => { @@ -378,4 +378,4 @@ export class TripAnimationComponent implements OnInit, AfterViewInit, OnDestroy } } -export let TbTripAnimationWidget = TripAnimationComponent; +export const TbTripAnimationWidget = TripAnimationComponent; diff --git a/ui-ngx/src/app/modules/home/components/widget/widget-component.service.ts b/ui-ngx/src/app/modules/home/components/widget/widget-component.service.ts index d9b18d896a..37cc09d93d 100644 --- a/ui-ngx/src/app/modules/home/components/widget/widget-component.service.ts +++ b/ui-ngx/src/app/modules/home/components/widget/widget-component.service.ts @@ -186,7 +186,7 @@ export class WidgetComponentService { (window as any).TbCanvasDigitalGauge = mod.TbCanvasDigitalGauge; })) ); - widgetModulesTasks.push(from(import('@home/components/widget/lib/maps/map-widget2')).pipe( + widgetModulesTasks.push(from(import('@home/components/widget/lib/maps-legacy/map-widget2')).pipe( tap((mod) => { (window as any).TbMapWidgetV2 = mod.TbMapWidgetV2; })) diff --git a/ui-ngx/src/app/modules/home/components/widget/widget-components.module.ts b/ui-ngx/src/app/modules/home/components/widget/widget-components.module.ts index 21f2d89dc6..1530561924 100644 --- a/ui-ngx/src/app/modules/home/components/widget/widget-components.module.ts +++ b/ui-ngx/src/app/modules/home/components/widget/widget-components.module.ts @@ -39,7 +39,7 @@ import { EdgesOverviewWidgetComponent } from '@home/components/widget/lib/edges- import { JsonInputWidgetComponent } from '@home/components/widget/lib/json-input-widget.component'; import { QrCodeWidgetComponent } from '@home/components/widget/lib/qrcode-widget.component'; import { MarkdownWidgetComponent } from '@home/components/widget/lib/markdown-widget.component'; -import { SelectEntityDialogComponent } from '@home/components/widget/lib/maps/dialogs/select-entity-dialog.component'; +import { SelectEntityDialogComponent } from '@home/components/widget/lib/maps-legacy/dialogs/select-entity-dialog.component'; import { HomePageWidgetsModule } from '@home/components/widget/lib/home-page/home-page-widgets.module'; import { WIDGET_COMPONENTS_MODULE_TOKEN } from '@home/components/tokens'; import { FlotWidgetComponent } from '@home/components/widget/lib/flot-widget.component'; @@ -88,6 +88,7 @@ import { import { EllipsisChipListDirective } from '@shared/directives/ellipsis-chip-list.directive'; import { ScadaSymbolWidgetComponent } from '@home/components/widget/lib/scada/scada-symbol-widget.component'; import { TwoSegmentButtonWidgetComponent } from '@home/components/widget/lib/button/two-segment-button-widget.component'; +import { MapWidgetComponent } from '@home/components/widget/lib/maps/map-widget.component'; @NgModule({ declarations: [ @@ -141,7 +142,8 @@ import { TwoSegmentButtonWidgetComponent } from '@home/components/widget/lib/but LabelValueCardWidgetComponent, UnreadNotificationWidgetComponent, NotificationTypeFilterPanelComponent, - ScadaSymbolWidgetComponent + ScadaSymbolWidgetComponent, + MapWidgetComponent ], imports: [ CommonModule, @@ -202,7 +204,8 @@ import { TwoSegmentButtonWidgetComponent } from '@home/components/widget/lib/but LabelValueCardWidgetComponent, UnreadNotificationWidgetComponent, NotificationTypeFilterPanelComponent, - ScadaSymbolWidgetComponent + ScadaSymbolWidgetComponent, + MapWidgetComponent ], providers: [ {provide: WIDGET_COMPONENTS_MODULE_TOKEN, useValue: WidgetComponentsModule}, diff --git a/ui-ngx/src/app/shared/components/time/history-selector/history-selector.component.ts b/ui-ngx/src/app/shared/components/time/history-selector/history-selector.component.ts index 5486e9b0a2..61322338f1 100644 --- a/ui-ngx/src/app/shared/components/time/history-selector/history-selector.component.ts +++ b/ui-ngx/src/app/shared/components/time/history-selector/history-selector.component.ts @@ -17,7 +17,7 @@ import { ChangeDetectorRef, Component, EventEmitter, Input, OnChanges, Output } from '@angular/core'; import { interval } from 'rxjs'; import { filter } from 'rxjs/operators'; -import { HistorySelectSettings } from '@app/modules/home/components/widget/lib/maps/map-models'; +import { HistorySelectSettings } from '@app/modules/home/components/widget/lib/maps-legacy/map-models'; @Component({ selector: 'tb-history-selector', diff --git a/ui-ngx/src/typings/leaflet-extend-tb.d.ts b/ui-ngx/src/typings/leaflet-extend-tb.d.ts index 5a02c4de08..6ed43edefa 100644 --- a/ui-ngx/src/typings/leaflet-extend-tb.d.ts +++ b/ui-ngx/src/typings/leaflet-extend-tb.d.ts @@ -21,4 +21,7 @@ declare module 'leaflet' { interface MarkerOptions { tbMarkerData?: FormattedData; } + namespace TB { + function sidebar(selector: string): Control; + } } From 0bbb55b5cd893889072e6b533f5b6934b3995922 Mon Sep 17 00:00:00 2001 From: Igor Kulikov Date: Mon, 30 Dec 2024 19:29:15 +0200 Subject: [PATCH 009/345] UI: Implement map layers sidebar. --- .../widget/lib/maps/leaflet/leaflet-tb.ts | 235 ++++++++++++++++++ .../components/widget/lib/maps/map-layer.ts | 35 ++- .../widget/lib/maps/map-widget.models.ts | 2 +- .../components/widget/lib/maps/map.models.ts | 2 +- .../home/components/widget/lib/maps/map.scss | 124 +++++++++ .../home/components/widget/lib/maps/map.ts | 88 +++++-- .../assets/locale/locale.constant-en_US.json | 2 + ui-ngx/src/typings/leaflet-extend-tb.d.ts | 44 +++- 8 files changed, 495 insertions(+), 37 deletions(-) diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/maps/leaflet/leaflet-tb.ts b/ui-ngx/src/app/modules/home/components/widget/lib/maps/leaflet/leaflet-tb.ts index e69de29bb2..e458ed8ebc 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/maps/leaflet/leaflet-tb.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/maps/leaflet/leaflet-tb.ts @@ -0,0 +1,235 @@ +/// +/// Copyright © 2016-2024 The Thingsboard Authors +/// +/// Licensed under the Apache License, Version 2.0 (the "License"); +/// you may not use this file except in compliance with the License. +/// You may obtain a copy of the License at +/// +/// http://www.apache.org/licenses/LICENSE-2.0 +/// +/// Unless required by applicable law or agreed to in writing, software +/// distributed under the License is distributed on an "AS IS" BASIS, +/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +/// See the License for the specific language governing permissions and +/// limitations under the License. +/// + +import L, { TB } from 'leaflet'; + +class SidebarControl extends L.Control { + + private readonly sidebar: JQuery; + + private current = $(); + private currentButton = $(); + + private map: L.Map; + + constructor(options: TB.SidebarControlOptions) { + super(options); + this.sidebar = $('
'); + this.options.container.append(this.sidebar); + const position = options?.position || 'topleft'; + if (['topleft', 'bottomleft'].includes(position)) { + this.options.container.addClass('tb-sidebar-left'); + } else { + this.options.container.addClass('tb-sidebar-right'); + } + } + + addPane(pane: JQuery): this { + pane.hide().appendTo(this.sidebar); + return this; + } + + togglePane(pane: JQuery, button: JQuery) { + const paneWidth = this.options?.paneWidth || 220; + const position = this.options?.position || 'topleft'; + + this.current.hide().trigger('hide'); + this.currentButton.removeClass('active'); + + if (this.current === pane) { + if (['topleft', 'bottomleft'].includes(position)) { + this.map.panBy([-paneWidth, 0], { animate: false }); + } + this.sidebar.hide(); + this.current = this.currentButton = $(); + } else { + this.sidebar.show(); + this.current = pane; + this.currentButton = button || $(); + if (['topleft', 'bottomleft'].includes(position)) { + this.map.panBy([paneWidth, 0], { animate: false }); + } + } + this.map.invalidateSize({ pan: false, animate: false }); + this.current.show().trigger('show'); + this.currentButton.addClass('active'); + } + + addTo(map: L.Map): this { + this.map = map; + return this; + } +} + +class SidebarPaneControl extends L.Control { + + private button: JQuery; + private $ui: JQuery; + + constructor(options: O) { + super(options); + } + + onAdd(map: L.Map): HTMLElement { + const $container = $("
") + .attr('class', 'leaflet-bar'); + + this.button = $("") + .attr('class', 'tb-control-button') + .attr('href', '#') + .html('
') + .on('click', (e) => { + this.toggle(e); + }); + if (this.options.buttonTitle) { + this.button.attr('title', this.options.buttonTitle); + } + this.button.appendTo($container); + + this.$ui = $('
') + .attr('class', this.options.uiClass); + + $('
') + .appendTo(this.$ui) + .append($('
') + .text(this.options.paneTitle)) + .append($('
') + .append($('') + .attr('aria-label', 'Close') + .bind('click', (e) => { + this.toggle(e); + }))); + + this.options.sidebar.addPane(this.$ui); + + this.onAddPane(map, this.button, this.$ui, (e) => { + this.toggle(e); + }); + + return $container[0]; + } + + public onAddPane(map: L.Map, button: JQuery, $ui: JQuery, toggle: (e: JQuery.MouseEventBase) => void) {} + + private toggle(e: JQuery.MouseEventBase) { + e.stopPropagation(); + e.preventDefault(); + if (!this.button.hasClass("disabled")) { + this.options.sidebar.togglePane(this.$ui, this.button); + } + //$(".leaflet-control .control-button").tooltip("hide"); + } +} + +class LayersControl extends SidebarPaneControl { + constructor(options: TB.LayersControlOptions) { + super(options); + } + + public onAddPane(map: L.Map, button: JQuery, $ui: JQuery, toggle: (e: JQuery.MouseEventBase) => void) { + const layers = this.options.layers; + const baseSection = $("
") + .attr('class', 'tb-layers-container') + .appendTo($ui); + + layers.forEach((layerData, i) => { + const id = 'map-ui-layer-' + i; + const buttonContainer = $('
') + .appendTo(baseSection); + const mapContainer = $('
') + .appendTo(buttonContainer); + const input = $('') + .prop('id', id) + .prop('checked', map.hasLayer(layerData.layer)) + .appendTo(buttonContainer); + + const item = $('
diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/map/map-data-layer-dialog.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/map/map-data-layer-dialog.component.ts index 5244e7e266..9751e87621 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/map/map-data-layer-dialog.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/map/map-data-layer-dialog.component.ts @@ -18,12 +18,13 @@ import { Component, DestroyRef, Inject, ViewEncapsulation } from '@angular/core' import { DialogComponent } from '@shared/components/dialog.component'; import { CirclesDataLayerSettings, - DataLayerEditAction, dataLayerEditActions, + DataLayerEditAction, + dataLayerEditActions, dataLayerEditActionTranslationMap, defaultBaseMapDataLayerSettings, MapDataLayerSettings, MapDataLayerType, - MapType, mapZoomActions, mapZoomActionTranslationMap, + MapType, MarkersDataLayerSettings, MarkerType, PolygonsDataLayerSettings, @@ -40,7 +41,6 @@ import { takeUntilDestroyed } from '@angular/core/rxjs-interop'; import { EntityType } from '@shared/models/entity-type.models'; import { MapSettingsContext } from '@home/components/widget/lib/settings/common/map/map-settings.component.models'; import { genNextLabelForDataKeys, mergeDeepIgnoreArray } from '@core/utils'; -import { MapProviders } from '@home/components/widget/lib/maps-legacy/map-models'; import { WidgetService } from '@core/http/widget.service'; export interface MapDataLayerDialogData { @@ -318,8 +318,4 @@ export class MapDataLayerDialogComponent extends DialogComponent +
+
widgets.maps.data-layer.tooltip-tag-actions
+
+ + +
+
+
+ + {{ action.name }} + +
+
+ + +
+
+
+ +
+
diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/map/map-tooltip-tag-actions.component.scss b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/map/map-tooltip-tag-actions.component.scss new file mode 100644 index 0000000000..bb18ee4012 --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/map/map-tooltip-tag-actions.component.scss @@ -0,0 +1,49 @@ +/** + * Copyright © 2016-2024 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +.mat-mdc-chip.mat-mdc-standard-chip.tb-tag-action-chip { + overflow: hidden; + line-height: 20px; + height: 32px; + + &.mdc-evolution-chip--with-trailing-action { + .mdc-evolution-chip__action--primary { + padding-left: 4px; + padding-right: 12px; + } + } + + .tb-chip-labels { + display: flex; + flex-direction: row; + align-items: center; + min-width: 0; + padding: 2px 10px; + .tb-chip-label { + font-weight: normal; + font-size: 14px; + line-height: 20px; + &.tb-chip-text { + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; + } + } + } + .mat-mdc-chip-remove.mat-mdc-icon-button { + color: inherit; + opacity: inherit; + } +} diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/map/map-tooltip-tag-actions.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/map/map-tooltip-tag-actions.component.ts new file mode 100644 index 0000000000..fb3d690fe6 --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/map/map-tooltip-tag-actions.component.ts @@ -0,0 +1,176 @@ +/// +/// Copyright © 2016-2024 The Thingsboard Authors +/// +/// Licensed under the Apache License, Version 2.0 (the "License"); +/// you may not use this file except in compliance with the License. +/// You may obtain a copy of the License at +/// +/// http://www.apache.org/licenses/LICENSE-2.0 +/// +/// Unless required by applicable law or agreed to in writing, software +/// distributed under the License is distributed on an "AS IS" BASIS, +/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +/// See the License for the specific language governing permissions and +/// limitations under the License. +/// + +import { + Component, + DestroyRef, + forwardRef, + Input, + OnInit, + Renderer2, + ViewContainerRef, + ViewEncapsulation +} from '@angular/core'; +import { ControlValueAccessor, NG_VALUE_ACCESSOR, UntypedFormBuilder, UntypedFormGroup } from '@angular/forms'; +import { WidgetAction, WidgetActionType, widgetType } from '@shared/models/widget.models'; +import { takeUntilDestroyed } from '@angular/core/rxjs-interop'; +import { TbPopoverService } from '@shared/components/popover.service'; +import { MapSettingsContext } from '@home/components/widget/lib/settings/common/map/map-settings.component.models'; +import { + WidgetActionSettingsPanelComponent +} from '@home/components/widget/lib/settings/common/action/widget-action-settings-panel.component'; +import { MatButton } from '@angular/material/button'; +import { TranslateService } from '@ngx-translate/core'; +import { deepClone } from '@core/utils'; + +@Component({ + selector: 'tb-map-tooltip-tag-actions-panel', + templateUrl: './map-tooltip-tag-actions.component.html', + styleUrls: ['./map-tooltip-tag-actions.component.scss'], + providers: [ + { + provide: NG_VALUE_ACCESSOR, + useExisting: forwardRef(() => MapTooltipTagActionsComponent), + multi: true + } + ], + encapsulation: ViewEncapsulation.None +}) +export class MapTooltipTagActionsComponent implements ControlValueAccessor, OnInit { + + @Input() + disabled: boolean; + + @Input() + context: MapSettingsContext; + + actionsFormGroup: UntypedFormGroup; + + private propagateChange = (_val: any) => {}; + + constructor(private fb: UntypedFormBuilder, + private popoverService: TbPopoverService, + private renderer: Renderer2, + private viewContainerRef: ViewContainerRef, + private translate: TranslateService, + private destroyRef: DestroyRef) { + } + + ngOnInit() { + this.actionsFormGroup = this.fb.group({ + actions: [null, []] + }); + this.actionsFormGroup.get('actions').valueChanges.pipe( + takeUntilDestroyed(this.destroyRef) + ).subscribe( + (val) => this.propagateChange(val) + ); + } + + writeValue(actions?: WidgetAction[]): void { + this.actionsFormGroup.get('actions').patchValue(actions || [], {emitEvent: false}); + } + + registerOnChange(fn: any): void { + this.propagateChange = fn; + } + + registerOnTouched(fn: any): void { + } + + setDisabledState(isDisabled: boolean): void { + this.disabled = isDisabled; + if (this.disabled) { + this.actionsFormGroup.disable({emitEvent: false}); + } else { + this.actionsFormGroup.enable({emitEvent: false}); + } + } + + removeAction(index: number): void { + const actions: WidgetAction[] = this.actionsFormGroup.get('actions').value; + if (actions[index]) { + actions.splice(index, 1); + this.actionsFormGroup.get('actions').patchValue(actions); + } + } + + addAction($event: Event, matButton: MatButton): void { + if ($event) { + $event.stopPropagation(); + } + const action: WidgetAction = { + name: '', + type: WidgetActionType.doNothing + }; + const trigger = matButton._elementRef.nativeElement; + const actionNames = (this.actionsFormGroup.get('actions').value as WidgetAction[] || []).map(action => action.name); + this.openActionSettingsPopup(trigger, action, actionNames, true, (added) => { + if (added) { + const actions: WidgetAction[] = this.actionsFormGroup.get('actions').value || []; + actions.push(added); + this.actionsFormGroup.get('actions').patchValue(actions); + } + }); + } + + editAction($event: Event, matButton: MatButton, index: number): void { + if ($event) { + $event.stopPropagation(); + } + const actions: WidgetAction[] = this.actionsFormGroup.get('actions').value; + if (actions[index]) { + const action = deepClone(actions[index]); + const trigger = matButton._elementRef.nativeElement; + const actionNames = actions.filter((_action, current) => current !== index).map(action => action.name); + this.openActionSettingsPopup(trigger, action, actionNames, false, (updated) => { + if (updated) { + actions[index] = updated; + this.actionsFormGroup.get('actions').patchValue(actions); + } + }); + } + } + + private openActionSettingsPopup(trigger: Element, action: WidgetAction, actionNames: string[], isAdd: boolean, callback: (action?: WidgetAction) => void) { + if (this.popoverService.hasPopover(trigger)) { + this.popoverService.hidePopover(trigger); + } else { + const title = this.translate.instant(isAdd ? 'widgets.maps.data-layer.add-tooltip-tag-action' : 'widgets.maps.data-layer.edit-tooltip-tag-action'); + const applyTitle = this.translate.instant(isAdd ? 'action.add' : 'action.apply'); + const ctx: any = { + widgetAction: action, + withName: true, + actionNames, + panelTitle: title, + applyTitle, + widgetType: widgetType.latest, + callbacks: this.context.callbacks + }; + const widgetActionSettingsPanelPopover = this.popoverService.displayPopover(trigger, this.renderer, + this.viewContainerRef, WidgetActionSettingsPanelComponent, + ['leftTopOnly', 'leftOnly', 'leftBottomOnly'], true, null, + ctx, + {}, + {}, {}, true); + widgetActionSettingsPanelPopover.tbComponentRef.instance.widgetActionApplied.subscribe((widgetAction) => { + widgetActionSettingsPanelPopover.hide(); + callback(widgetAction); + }); + } + } + +} 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 8dd52cb1fe..351c203d3b 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 @@ -233,6 +233,9 @@ import { import { ImageMapSourceSettingsComponent } from '@home/components/widget/lib/settings/common/map/image-map-source-settings.component'; +import { + MapTooltipTagActionsComponent +} from '@home/components/widget/lib/settings/common/map/map-tooltip-tag-actions.component'; @NgModule({ declarations: [ @@ -309,6 +312,7 @@ import { ImageMapSourceSettingsComponent, DataLayerColorSettingsComponent, DataLayerColorSettingsPanelComponent, + MapTooltipTagActionsComponent, DataLayerPatternSettingsComponent, MarkerShapeSettingsComponent, MarkerShapesComponent, diff --git a/ui-ngx/src/app/shared/models/widget.models.ts b/ui-ngx/src/app/shared/models/widget.models.ts index 8b6558a11f..36d8966c6a 100644 --- a/ui-ngx/src/app/shared/models/widget.models.ts +++ b/ui-ngx/src/app/shared/models/widget.models.ts @@ -692,6 +692,7 @@ export interface CustomActionDescriptor { } export interface WidgetAction extends CustomActionDescriptor { + name?: string; type: WidgetActionType; targetDashboardId?: string; targetDashboardStateId?: string; 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 b6085c2a40..bb1f184a45 100644 --- a/ui-ngx/src/assets/locale/locale.constant-en_US.json +++ b/ui-ngx/src/assets/locale/locale.constant-en_US.json @@ -7667,6 +7667,10 @@ "tooltip-offset": "Tooltip offset", "tooltip-offset-horizontal": "Horizontal", "tooltip-offset-vertical": "Vertical", + "tooltip-tag-actions": "Tag actions", + "add-tooltip-tag-action": "Add tag action", + "edit-tooltip-tag-action": "Edit tag action", + "remove-tooltip-tag-action": "Remove tag action", "action-add": "Add", "action-edit": "Edit", "action-move": "Move", From 80e83e0ff9b4787a7525f45e244095de992d1c51 Mon Sep 17 00:00:00 2001 From: Igor Kulikov Date: Fri, 31 Jan 2025 19:32:18 +0200 Subject: [PATCH 046/345] UI: Maps top toolbar. --- .../widget/lib/maps/leaflet/leaflet-tb.ts | 76 ++++++++++++++++--- .../home/components/widget/lib/maps/map.scss | 2 +- .../home/components/widget/lib/maps/map.ts | 22 +++++- ui-ngx/src/typings/leaflet-extend-tb.d.ts | 30 +++++++- 4 files changed, 113 insertions(+), 17 deletions(-) diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/maps/leaflet/leaflet-tb.ts b/ui-ngx/src/app/modules/home/components/widget/lib/maps/leaflet/leaflet-tb.ts index 9418d3fcba..5994d25fca 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/maps/leaflet/leaflet-tb.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/maps/leaflet/leaflet-tb.ts @@ -276,16 +276,49 @@ class GroupsControl extends SidebarPaneControl { } } -class ToolbarButton extends L.Control { +class TopToolbarButton { + private readonly button: JQuery; + private _onClick: (e: MouseEvent) => void; + + constructor(private readonly options: TB.TopToolbarButtonOptions) { + const iconElement = $('
'); + this.button = $("
") + .attr('class', 'tb-control-button tb-control-text-button') + .attr('href', '#') + .attr('role', 'button'); + this.button.append(iconElement); + this.button.append(`
${this.options.title}
`); + this.loadIcon(iconElement); + this.button.on('click', (e) => { + e.stopPropagation(); + e.preventDefault(); + if (this._onClick) { + this._onClick(e.originalEvent); + } + }); + } + + onClick(onClick: (e: MouseEvent) => void): void { + this._onClick = onClick; + } + + private loadIcon(iconElement: JQuery) { + // this.options.icon + } + + getButtonElement(): JQuery { + return this.button; + } +} + +class ToolbarButton { private readonly id: string; private readonly button: JQuery; private active = false; private disabled = false; - constructor(options: TB.ToolbarButtonOptions) { - super(options); + constructor(private readonly options: TB.ToolbarButtonOptions) { this.id = options.id; - const buttonText = this.options.showText ? this.options.title : null; this.button = $("
") .attr('class', 'tb-control-button') @@ -346,6 +379,25 @@ class ToolbarButton extends L.Control { } } +class TopToolbarControl { + + private readonly toolbarElement: JQuery; + + constructor(private readonly options: TB.TopToolbarControlOptions) { + const controlContainer = $('.leaflet-control-container', options.mapElement); + this.toolbarElement = $('
'); + this.toolbarElement.appendTo(controlContainer); + } + + toolbarButton(options: TB.TopToolbarButtonOptions): TopToolbarButton { + const button = new TopToolbarButton(options); + const buttonContainer = $('
'); + button.getButtonElement().appendTo(buttonContainer); + buttonContainer.appendTo(this.toolbarElement); + return button; + } +} + class ToolbarControl extends L.Control { private buttonContainer: JQuery; @@ -372,15 +424,14 @@ class ToolbarControl extends L.Control { } -class BottomToolbarControl extends L.Control { +class BottomToolbarControl { private readonly buttonContainer: JQuery; private toolbarButtons: ToolbarButton[] = []; container: HTMLElement; - constructor(options: TB.BottomToolbarControlOptions) { - super(options); + constructor(private readonly options: TB.BottomToolbarControlOptions) { const controlContainer = $('.leaflet-control-container', options.mapElement); const toolbar = $('
'); toolbar.appendTo(controlContainer); @@ -389,10 +440,6 @@ class BottomToolbarControl extends L.Control { this.container = this.buttonContainer[0]; } - addTo(map: L.Map): this { - return this; - } - getButton(id: string): ToolbarButton { return this.toolbarButtons.find(b => b.getId() === id); } @@ -452,6 +499,10 @@ const groups = (options: TB.GroupsControlOptions): GroupsControl => { return new GroupsControl(options); } +const topToolbar = (options: TB.TopToolbarControlOptions): TopToolbarControl => { + return new TopToolbarControl(options); +} + const toolbar = (options: L.ControlOptions): ToolbarControl => { return new ToolbarControl(options); } @@ -515,6 +566,8 @@ L.TB = L.TB || { SidebarPaneControl, LayersControl, GroupsControl, + TopToolbarButton, + TopToolbarControl, ToolbarButton, ToolbarControl, BottomToolbarControl, @@ -522,6 +575,7 @@ L.TB = L.TB || { sidebarPane, layers, groups, + topToolbar, toolbar, bottomToolbar, TileLayer: { diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/maps/map.scss b/ui-ngx/src/app/modules/home/components/widget/lib/maps/map.scss index 270732d25b..3cb3c2cb35 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/maps/map.scss +++ b/ui-ngx/src/app/modules/home/components/widget/lib/maps/map.scss @@ -141,7 +141,7 @@ } } } - .tb-map-bottom-toolbar { + .tb-map-bottom-toolbar, .tb-map-top-toolbar { left: 0; right: 0; display: flex; 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 1e7165a85f..21518a699e 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 @@ -85,6 +85,7 @@ export abstract class TbMap { protected sidebar: L.TB.SidebarControl; + protected customActionsToolbar: L.TB.TopToolbarControl; protected editToolbar: L.TB.BottomToolbarControl; protected addMarkerButton: L.TB.ToolbarButton; @@ -166,6 +167,7 @@ export abstract class TbMap { } this.setupDataLayers(); this.setupEditMode(); + this.setupCustomActions(); this.createdControlButtonTooltip(this.mapElement, ['topleft', 'bottomleft'].includes(this.settings.controlsPosition) ? 'right' : 'left'); } @@ -270,7 +272,7 @@ export abstract class TbMap { onClose: () => { return this.deselectItem(true); } - }).addTo(this.map); + }); this.map.on('click', () => { this.deselectItem(); @@ -349,6 +351,24 @@ export abstract class TbMap { } } + private setupCustomActions() { + this.customActionsToolbar = L.TB.topToolbar({ + mapElement: $(this.mapElement) + }); + + /*const customButton = this.customActionsToolbar.toolbarButton({ + title: 'Super button', + icon: 'add' + }); + this.customActionsToolbar.toolbarButton({ + title: 'Super button 2', + icon: 'add' + }); + customButton.onClick(e => { + console.log("Called!"); + });*/ + } + private placeMarker(e: MouseEvent, button: L.TB.ToolbarButton): void { this.placeItem(e, button, this.addMarkerDataLayers, (entity) => { diff --git a/ui-ngx/src/typings/leaflet-extend-tb.d.ts b/ui-ngx/src/typings/leaflet-extend-tb.d.ts index e24c632a3b..26084fabb5 100644 --- a/ui-ngx/src/typings/leaflet-extend-tb.d.ts +++ b/ui-ngx/src/typings/leaflet-extend-tb.d.ts @@ -89,7 +89,27 @@ declare module 'leaflet' { constructor(options: GroupsControlOptions); } - interface ToolbarButtonOptions extends ControlOptions { + interface TopToolbarButtonOptions { + icon: string; + iconColor?: string; + title: string; + } + + class TopToolbarButton { + constructor(options: TopToolbarButtonOptions); + onClick(onClick: (e: MouseEvent) => void): void; + } + + interface TopToolbarControlOptions { + mapElement: JQuery; + } + + class TopToolbarControl { + constructor(options: TopToolbarControlOptions); + toolbarButton(options: TopToolbarButtonOptions): TopToolbarButton; + } + + interface ToolbarButtonOptions { id: string; title: string; click: (e: MouseEvent, button: ToolbarButton) => void; @@ -97,7 +117,7 @@ declare module 'leaflet' { showText?: boolean; } - class ToolbarButton extends Control{ + class ToolbarButton { constructor(options: ToolbarButtonOptions); setActive(active: boolean): void; isActive(): boolean; @@ -110,13 +130,13 @@ declare module 'leaflet' { toolbarButton(options: ToolbarButtonOptions): ToolbarButton; } - interface BottomToolbarControlOptions extends ControlOptions { + interface BottomToolbarControlOptions { mapElement: JQuery; closeTitle: string; onClose: () => boolean; } - class BottomToolbarControl extends Control { + class BottomToolbarControl { constructor(options: BottomToolbarControlOptions); getButton(id: string): ToolbarButton | undefined; open(buttons: ToolbarButtonOptions[], showCloseButton?: boolean): void; @@ -132,6 +152,8 @@ declare module 'leaflet' { function groups(options: GroupsControlOptions): GroupsControl; + function topToolbar(options: TopToolbarControlOptions): TopToolbarControl; + function toolbar(options: ControlOptions): ToolbarControl; function bottomToolbar(options: BottomToolbarControlOptions): BottomToolbarControl; From 0e9d0b3ba3b456b71fd732a7504d714ac0abc58c Mon Sep 17 00:00:00 2001 From: dashevchenko Date: Mon, 3 Feb 2025 18:27:36 +0200 Subject: [PATCH 047/345] fixed EntityServiceTest --- .../server/service/edqs/EdqsSyncService.java | 95 ++----- .../EdqsEntityQueryControllerTest.java | 10 +- .../controller/EntityQueryControllerTest.java | 36 +-- .../entitiy/EdqsEntityServiceTest.java | 11 +- .../service/entitiy/EntityServiceTest.java | 244 +----------------- .../server/common/data/ObjectType.java | 17 +- .../data/edqs/fields/DashboardFields.java | 32 ++- .../common/data/edqs/fields/EntityFields.java | 6 + .../data/edqs/fields/EntityViewFields.java | 7 + .../common/data/edqs/fields/FieldsUtil.java | 2 - .../common/data/edqs/query/EdqsRequest.java | 4 - .../common/data/edqs/query/QueryResult.java | 4 +- .../common/data/permission/QueryContext.java | 3 + .../server/common/data/queue/QueueStats.java | 8 +- common/edqs/pom.xml | 4 +- .../server/edqs/data/ApiUsageStateData.java | 2 +- .../server/edqs/data/AssetData.java | 2 +- .../server/edqs/data/BaseEntityData.java | 2 +- .../server/edqs/data/CustomerData.java | 2 +- .../server/edqs/data/DeviceData.java | 2 +- .../server/edqs/data/EntityData.java | 2 +- .../server/edqs/data/EntityGroupData.java | 58 ----- .../server/edqs/data/EntityProfileData.java | 2 +- .../server/edqs/data/GenericData.java | 2 +- .../server/edqs/data/ProfileAwareData.java | 2 +- .../server/edqs/data/RelationData.java | 2 +- .../server/edqs/data/RelationInfo.java | 2 +- .../server/edqs/data/RelationsRepo.java | 2 +- .../server/edqs/data/TenantData.java | 2 +- .../edqs/data/dp/AbstractDataPoint.java | 2 +- .../server/edqs/data/dp/BoolDataPoint.java | 2 +- .../edqs/data/dp/CompressedJsonDataPoint.java | 2 +- .../data/dp/CompressedStringDataPoint.java | 2 +- .../server/edqs/data/dp/DataPoint.java | 2 +- .../server/edqs/data/dp/DoubleDataPoint.java | 2 +- .../server/edqs/data/dp/JsonDataPoint.java | 2 +- .../server/edqs/data/dp/LongDataPoint.java | 2 +- .../server/edqs/data/dp/StringDataPoint.java | 2 +- .../server/edqs/load/TenantRepoLoader.java | 2 +- .../server/edqs/processor/EdqsConverter.java | 2 +- .../server/edqs/processor/EdqsProcessor.java | 9 +- .../server/edqs/processor/EdqsProducer.java | 2 +- .../server/edqs/query/DataKey.java | 2 +- .../server/edqs/query/EdqsCountQuery.java | 2 +- .../server/edqs/query/EdqsDataQuery.java | 2 +- .../server/edqs/query/EdqsFilter.java | 2 +- .../server/edqs/query/EdqsQuery.java | 2 +- .../server/edqs/query/SortableEntityData.java | 4 +- .../AbstractEntityGroupQueryProcessor.java | 75 ------ ...stractEntityProfileNameQueryProcessor.java | 2 +- .../AbstractEntityProfileQueryProcessor.java | 2 +- .../AbstractEntitySearchQueryProcessor.java | 4 +- .../processor/AbstractQueryProcessor.java | 71 +---- .../AbstractRelationQueryProcessor.java | 111 +------- .../AbstractSimpleQueryProcessor.java | 53 +--- ...bstractSingleEntityTypeQueryProcessor.java | 116 +-------- .../ApiUsageStateQueryProcessor.java | 35 +-- .../processor/AssetSearchQueryProcessor.java | 2 +- .../processor/AssetTypeQueryProcessor.java | 2 +- .../query/processor/CombinedPermissions.java | 25 -- .../processor/DeviceSearchQueryProcessor.java | 2 +- .../processor/DeviceTypeQueryProcessor.java | 2 +- .../processor/EdgeTypeQueryProcessor.java | 2 +- .../EdgeTypeSearchQueryProcessor.java | 2 +- .../EntitiesByGroupNameQueryProcessor.java | 121 --------- .../EntitiesByGroupQueryProcessor.java | 96 ------- .../EntityGroupListQueryProcessor.java | 84 ------ .../EntityGroupNameQueryProcessor.java | 83 ------ .../processor/EntityListQueryProcessor.java | 34 +-- .../processor/EntityNameQueryProcessor.java | 2 +- .../query/processor/EntityQueryProcessor.java | 2 +- .../EntityQueryProcessorFactory.java | 8 +- .../processor/EntityTypeQueryProcessor.java | 2 +- .../EntityViewSearchQueryProcessor.java | 2 +- .../EntityViewTypeQueryProcessor.java | 2 +- .../query/processor/GroupPermissions.java | 29 --- .../edqs/query/processor/Permissions.java | 24 -- .../processor/RelationQueryPermissions.java | 33 --- .../processor/RelationQueryProcessor.java | 2 +- .../SchedulerEventQueryProcessor.java | 37 --- .../processor/SingleEntityQueryProcessor.java | 36 +-- .../StateEntityOwnerQueryProcessor.java | 91 ------- .../server/edqs/repo/EdqRepository.java | 7 +- .../edqs/repo/InMemoryEdqRepository.java | 22 +- .../server/edqs/repo/KeyDictionary.java | 2 +- .../server/edqs/repo/TbBytePool.java | 2 +- .../server/edqs/repo/TbStringPool.java | 2 +- .../server/edqs/repo/TenantRepo.java | 142 ++-------- .../server/edqs/state/EdqsStateService.java | 2 +- .../edqs/state/KafkaEdqsStateService.java | 2 +- .../edqs/state/LocalEdqsStateService.java | 2 +- .../server/edqs/stats/EdqsStatsService.java | 2 +- .../edqs/util/EdqsPartitionService.java | 2 +- .../server/edqs/util/EdqsRocksDb.java | 2 +- .../server/edqs/util/RepositoryUtils.java | 39 +-- .../server/edqs/util/TbRocksDb.java | 2 +- .../server/edqs/util/VersionsStore.java | 2 +- .../server/queue/discovery/QueueKey.java | 6 + .../java/org/thingsboard/server/dao/Dao.java | 5 +- .../dao/customer/CustomerServiceImpl.java | 2 +- .../server/dao/entity/BaseEntityService.java | 25 +- .../dao/sql/asset/AssetProfileRepository.java | 5 +- .../server/dao/sql/asset/AssetRepository.java | 5 +- .../server/dao/sql/asset/JpaAssetDao.java | 5 +- .../dao/sql/asset/JpaAssetProfileDao.java | 5 +- .../dao/sql/customer/CustomerRepository.java | 9 +- .../dao/sql/customer/JpaCustomerDao.java | 6 +- .../sql/dashboard/DashboardRepository.java | 6 +- .../dao/sql/dashboard/JpaDashboardDao.java | 5 +- .../sql/device/DeviceProfileRepository.java | 6 +- .../dao/sql/device/DeviceRepository.java | 5 +- .../server/dao/sql/device/JpaDeviceDao.java | 5 +- .../dao/sql/device/JpaDeviceProfileDao.java | 5 +- .../server/dao/sql/edge/EdgeRepository.java | 6 +- .../server/dao/sql/edge/JpaEdgeDao.java | 5 +- .../sql/entityview/EntityViewRepository.java | 7 +- .../dao/sql/entityview/JpaEntityViewDao.java | 7 +- .../query/DefaultAlarmQueryRepository.java | 3 - .../dao/sql/queue/JpaQueueStatsDao.java | 5 +- .../dao/sql/queue/QueueStatsRepository.java | 5 +- .../server/dao/sql/rule/JpaRuleChainDao.java | 6 +- .../dao/sql/rule/RuleChainRepository.java | 5 +- .../server/dao/sql/tenant/JpaTenantDao.java | 11 +- .../dao/sql/tenant/JpaTenantProfileDao.java | 5 +- .../sql/tenant/TenantProfileRepository.java | 5 +- .../dao/sql/tenant/TenantRepository.java | 5 +- .../usagerecord/ApiUsageStateRepository.java | 8 +- .../sql/usagerecord/JpaApiUsageStateDao.java | 6 +- .../server/dao/sql/user/JpaUserDao.java | 5 +- .../server/dao/sql/user/UserRepository.java | 7 +- .../dao/sql/widget/JpaWidgetTypeDao.java | 6 +- .../dao/sql/widget/JpaWidgetsBundleDao.java | 7 +- .../dao/sql/widget/WidgetTypeRepository.java | 6 +- .../sql/widget/WidgetsBundleRepository.java | 6 +- edqs/pom.xml | 5 +- .../edqs/DummyQueueRoutingInfoService.java | 2 +- .../edqs/DummyTenantRoutingInfoService.java | 2 +- .../edqs/ThingsboardEdqsApplication.java | 4 +- edqs/src/main/resources/edqs.yml | 2 +- edqs/src/main/resources/logback.xml | 2 +- .../server/edqs/repo/AbstractEDQTest.java | 51 +--- .../edqs/repo/ApiUsageStateFilterTest.java | 5 +- .../edqs/repo/AssetSearchQueryFilterTest.java | 114 ++------ .../server/edqs/repo/AssetTypeFilterTest.java | 41 ++- .../repo/DeviceSearchQueryFilterTest.java | 133 ++-------- .../edqs/repo/DeviceTypeFilterTest.java | 15 +- .../edqs/repo/EdgeSearchQueryFilterTest.java | 81 ++---- .../server/edqs/repo/EdgeTypeFilterTest.java | 25 +- .../repo/EntitiesByGroupIdFilterTest.java | 161 ------------ .../repo/EntitiesByGroupNameFilterTest.java | 140 ---------- .../edqs/repo/EntityGroupListFilterTest.java | 189 -------------- .../edqs/repo/EntityGroupNameFilterTest.java | 186 ------------- .../edqs/repo/EntityListFilterTest.java | 71 +---- .../edqs/repo/EntityNameFilterTest.java | 15 +- .../edqs/repo/EntityTypeFilterTest.java | 15 +- .../repo/EntityViewSearchQueryFilterTest.java | 131 ++-------- .../edqs/repo/EntityViewTypeFilterTest.java | 25 +- .../edqs/repo/RelationsQueryFilterTest.java | 91 +------ .../server/edqs/repo/RepositoryUtilsTest.java | 2 +- .../edqs/repo/SchedulerEventFilterTest.java | 129 --------- .../edqs/repo/SingleEntityFilterTest.java | 55 +--- .../edqs/repo/StateEntityOwnerFilterTest.java | 81 ------ pom.xml | 1 + 163 files changed, 557 insertions(+), 3488 deletions(-) delete mode 100644 common/edqs/src/main/java/org/thingsboard/server/edqs/data/EntityGroupData.java delete mode 100644 common/edqs/src/main/java/org/thingsboard/server/edqs/query/processor/AbstractEntityGroupQueryProcessor.java delete mode 100644 common/edqs/src/main/java/org/thingsboard/server/edqs/query/processor/CombinedPermissions.java delete mode 100644 common/edqs/src/main/java/org/thingsboard/server/edqs/query/processor/EntitiesByGroupNameQueryProcessor.java delete mode 100644 common/edqs/src/main/java/org/thingsboard/server/edqs/query/processor/EntitiesByGroupQueryProcessor.java delete mode 100644 common/edqs/src/main/java/org/thingsboard/server/edqs/query/processor/EntityGroupListQueryProcessor.java delete mode 100644 common/edqs/src/main/java/org/thingsboard/server/edqs/query/processor/EntityGroupNameQueryProcessor.java delete mode 100644 common/edqs/src/main/java/org/thingsboard/server/edqs/query/processor/GroupPermissions.java delete mode 100644 common/edqs/src/main/java/org/thingsboard/server/edqs/query/processor/Permissions.java delete mode 100644 common/edqs/src/main/java/org/thingsboard/server/edqs/query/processor/RelationQueryPermissions.java delete mode 100644 common/edqs/src/main/java/org/thingsboard/server/edqs/query/processor/SchedulerEventQueryProcessor.java delete mode 100644 common/edqs/src/main/java/org/thingsboard/server/edqs/query/processor/StateEntityOwnerQueryProcessor.java delete mode 100644 edqs/src/test/java/org/thingsboard/server/edqs/repo/EntitiesByGroupIdFilterTest.java delete mode 100644 edqs/src/test/java/org/thingsboard/server/edqs/repo/EntitiesByGroupNameFilterTest.java delete mode 100644 edqs/src/test/java/org/thingsboard/server/edqs/repo/EntityGroupListFilterTest.java delete mode 100644 edqs/src/test/java/org/thingsboard/server/edqs/repo/EntityGroupNameFilterTest.java delete mode 100644 edqs/src/test/java/org/thingsboard/server/edqs/repo/SchedulerEventFilterTest.java delete mode 100644 edqs/src/test/java/org/thingsboard/server/edqs/repo/StateEntityOwnerFilterTest.java diff --git a/application/src/main/java/org/thingsboard/server/service/edqs/EdqsSyncService.java b/application/src/main/java/org/thingsboard/server/service/edqs/EdqsSyncService.java index bf8e096059..04633892e5 100644 --- a/application/src/main/java/org/thingsboard/server/service/edqs/EdqsSyncService.java +++ b/application/src/main/java/org/thingsboard/server/service/edqs/EdqsSyncService.java @@ -27,7 +27,6 @@ import org.thingsboard.server.common.data.edqs.EdqsObject; import org.thingsboard.server.common.data.edqs.Entity; import org.thingsboard.server.common.data.edqs.LatestTsKv; import org.thingsboard.server.common.data.edqs.fields.EntityFields; -import org.thingsboard.server.common.data.edqs.fields.TenantFields; import org.thingsboard.server.common.data.id.EntityId; import org.thingsboard.server.common.data.id.EntityIdFactory; import org.thingsboard.server.common.data.id.TenantId; @@ -37,7 +36,6 @@ import org.thingsboard.server.dao.Dao; import org.thingsboard.server.dao.attributes.AttributesDao; import org.thingsboard.server.dao.dictionary.KeyDictionaryDao; import org.thingsboard.server.dao.entity.EntityDaoRegistry; -import org.thingsboard.server.dao.group.EntityGroupDao; import org.thingsboard.server.dao.model.sql.AttributeKvEntity; import org.thingsboard.server.dao.model.sql.RelationEntity; import org.thingsboard.server.dao.model.sqlts.dictionary.KeyDictionaryEntry; @@ -46,39 +44,16 @@ import org.thingsboard.server.dao.sql.relation.RelationRepository; import org.thingsboard.server.dao.sqlts.latest.TsKvLatestRepository; import org.thingsboard.server.dao.tenant.TenantDao; -import java.util.EnumSet; import java.util.List; import java.util.Map; -import java.util.Set; import java.util.UUID; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.atomic.AtomicInteger; -import static org.thingsboard.server.common.data.ObjectType.API_USAGE_STATE; -import static org.thingsboard.server.common.data.ObjectType.ASSET; -import static org.thingsboard.server.common.data.ObjectType.ASSET_PROFILE; import static org.thingsboard.server.common.data.ObjectType.ATTRIBUTE_KV; -import static org.thingsboard.server.common.data.ObjectType.BLOB_ENTITY; -import static org.thingsboard.server.common.data.ObjectType.CONVERTER; -import static org.thingsboard.server.common.data.ObjectType.CUSTOMER; -import static org.thingsboard.server.common.data.ObjectType.DASHBOARD; -import static org.thingsboard.server.common.data.ObjectType.DEVICE; -import static org.thingsboard.server.common.data.ObjectType.DEVICE_PROFILE; -import static org.thingsboard.server.common.data.ObjectType.EDGE; -import static org.thingsboard.server.common.data.ObjectType.ENTITY_GROUP; -import static org.thingsboard.server.common.data.ObjectType.ENTITY_VIEW; -import static org.thingsboard.server.common.data.ObjectType.INTEGRATION; import static org.thingsboard.server.common.data.ObjectType.LATEST_TS_KV; -import static org.thingsboard.server.common.data.ObjectType.QUEUE_STATS; import static org.thingsboard.server.common.data.ObjectType.RELATION; -import static org.thingsboard.server.common.data.ObjectType.ROLE; -import static org.thingsboard.server.common.data.ObjectType.RULE_CHAIN; -import static org.thingsboard.server.common.data.ObjectType.SCHEDULER_EVENT; -import static org.thingsboard.server.common.data.ObjectType.TENANT; -import static org.thingsboard.server.common.data.ObjectType.TENANT_PROFILE; -import static org.thingsboard.server.common.data.ObjectType.USER; -import static org.thingsboard.server.common.data.ObjectType.WIDGETS_BUNDLE; -import static org.thingsboard.server.common.data.ObjectType.WIDGET_TYPE; +import static org.thingsboard.server.common.data.ObjectType.edqsTenantTypes; @Slf4j public abstract class EdqsSyncService { @@ -94,8 +69,6 @@ public abstract class EdqsSyncService { @Autowired private RelationRepository relationRepository; @Autowired - private EntityGroupDao entityGroupDao; - @Autowired private TsKvLatestRepository tsKvLatestRepository; @Autowired @Lazy @@ -106,12 +79,6 @@ public abstract class EdqsSyncService { private final Map counters = new ConcurrentHashMap<>(); - public static final Set edqsTenantTypes = EnumSet.of( - TENANT_PROFILE, CUSTOMER, DEVICE_PROFILE, DEVICE, ASSET_PROFILE, ASSET, EDGE, ENTITY_VIEW, USER, DASHBOARD, - RULE_CHAIN, WIDGET_TYPE, WIDGETS_BUNDLE, CONVERTER, INTEGRATION, SCHEDULER_EVENT, ROLE, - BLOB_ENTITY, API_USAGE_STATE, QUEUE_STATS - ); - public abstract boolean isSyncNeeded(); public void sync() { @@ -119,9 +86,7 @@ public abstract class EdqsSyncService { long startTs = System.currentTimeMillis(); counters.clear(); - syncTenants(); syncTenantEntities(); - syncEntityGroups(); syncRelations(); loadKeyDictionary(); syncAttributes(); @@ -139,49 +104,28 @@ public abstract class EdqsSyncService { edqsService.processEvent(tenantId, type, EdqsEventType.UPDATED, object); } - private void syncTenants() { - log.info("Synchronizing tenants to EDQS"); - long ts = System.currentTimeMillis(); - var tenants = new PageDataIterable<>(tenantDao::findAllFields, 10000); - for (EntityFields entityFields : tenants) { - TenantId tenantId = TenantId.fromUUID(entityFields.getId()); - entityInfoMap.put(entityFields.getId(), new EntityIdInfo(EntityType.TENANT, tenantId)); - process(tenantId, TENANT, new Entity(EntityType.TENANT, entityFields)); - } - process(TenantId.SYS_TENANT_ID, TENANT, new Entity(EntityType.TENANT, new TenantFields(TenantId.SYS_TENANT_ID.getId(), Long.MAX_VALUE))); - log.info("Finished synchronizing tenants to EDQS in {} ms", (System.currentTimeMillis() - ts)); - } - private void syncTenantEntities() { for (ObjectType type : edqsTenantTypes) { - log.info("Synchronizing tenant {} entities to EDQS", type); + log.info("Synchronizing {} entities to EDQS", type); long ts = System.currentTimeMillis(); EntityType entityType = type.toEntityType(); Dao dao = entityDaoRegistry.getDao(entityType); - var entities = new PageDataIterable<>(dao::findAllFields, 10000); - for (EntityFields entityFields : entities) { - TenantId tenantId = TenantId.fromUUID(entityFields.getTenantId()); - entityInfoMap.put(entityFields.getId(), new EntityIdInfo(entityType, tenantId)); - process(tenantId, type, new Entity(type.toEntityType(), entityFields)); - } - log.info("Finished synchronizing tenant {} entities to EDQS in {} ms", type, (System.currentTimeMillis() - ts)); - } - } - - private void syncEntityGroups() { - log.info("Synchronizing entity groups to EDQS"); - long ts = System.currentTimeMillis(); - var entityGroups = new PageDataIterable<>(entityGroupDao::findAllFields, 30000); - for (EntityFields groupFields : entityGroups) { - EntityIdInfo entityIdInfo = entityInfoMap.get(groupFields.getOwnerId()); - if (entityIdInfo != null) { - entityInfoMap.put(groupFields.getId(), new EntityIdInfo(EntityType.ENTITY_GROUP, entityIdInfo.tenantId())); - process(entityIdInfo.tenantId(), ENTITY_GROUP, new Entity(EntityType.ENTITY_GROUP, groupFields)); - } else { - log.info("Entity group owner not found: " + groupFields.getOwnerId()); + UUID lastFromEntityId = UUID.fromString("00000000-0000-0000-0000-000000000000"); + while (true) { + var batch = dao.findNextBatch(lastFromEntityId, 10000); + if (batch.isEmpty()) { + break; + } + for (EntityFields entityFields : batch) { + TenantId tenantId = TenantId.fromUUID(entityFields.getTenantId()); + entityInfoMap.put(entityFields.getId(), new EntityIdInfo(entityType, tenantId)); + process(tenantId, type, new Entity(entityType, entityFields)); + } + EntityFields lastRecord = batch.get(batch.size() - 1); + lastFromEntityId = lastRecord.getId(); } + log.info("Finished synchronizing {} entities to EDQS in {} ms", type, (System.currentTimeMillis() - ts)); } - log.info("Finished synchronizing entity groups to EDQS in {} ms", (System.currentTimeMillis() - ts)); } private void syncRelations() { @@ -215,7 +159,7 @@ public abstract class EdqsSyncService { private void processRelationBatch(List relations) { for (RelationEntity relation : relations) { - if (RelationTypeGroup.COMMON.name().equals(relation.getRelationTypeGroup()) || (RelationTypeGroup.FROM_ENTITY_GROUP.name().equals(relation.getRelationTypeGroup()))) { + if (RelationTypeGroup.COMMON.name().equals(relation.getRelationTypeGroup())) { EntityIdInfo entityIdInfo = entityInfoMap.get(relation.getFromId()); if (entityIdInfo != null) { process(entityIdInfo.tenantId(), RELATION, relation.toData()); @@ -284,7 +228,7 @@ public abstract class EdqsSyncService { int lastKey = Integer.MIN_VALUE; while (true) { - List batch = tsKvLatestRepository.findNextBatch(lastEntityId, lastKey, 10000); + List batch = tsKvLatestRepository.findNextBatch(lastEntityId, lastKey, 10000); if (batch.isEmpty()) { break; } @@ -332,6 +276,7 @@ public abstract class EdqsSyncService { return strKey; } - public record EntityIdInfo(EntityType entityType, TenantId tenantId) {} + public record EntityIdInfo(EntityType entityType, TenantId tenantId) { + } } diff --git a/application/src/test/java/org/thingsboard/server/controller/EdqsEntityQueryControllerTest.java b/application/src/test/java/org/thingsboard/server/controller/EdqsEntityQueryControllerTest.java index daa1c92c1c..a75461faaa 100644 --- a/application/src/test/java/org/thingsboard/server/controller/EdqsEntityQueryControllerTest.java +++ b/application/src/test/java/org/thingsboard/server/controller/EdqsEntityQueryControllerTest.java @@ -28,13 +28,14 @@ import org.thingsboard.server.dao.service.DaoSqlTest; import org.thingsboard.server.edqs.util.EdqsRocksDb; import java.util.concurrent.TimeUnit; +import java.util.function.BiPredicate; import static org.awaitility.Awaitility.await; @DaoSqlTest @TestPropertySource(properties = { - "queue.type=kafka", // uncomment to use Kafka - "queue.kafka.bootstrap.servers=10.7.1.254:9092", +// "queue.type=kafka", // uncomment to use Kafka +// "queue.kafka.bootstrap.servers=10.7.1.254:9092", "queue.edqs.sync_enabled=true", "queue.edqs.api_enabled=true", "queue.edqs.mode=local" @@ -64,4 +65,9 @@ public class EdqsEntityQueryControllerTest extends EntityQueryControllerTest { result -> result == expectedResult); } + @Override + protected Long countByQueryAndCheck(EntityCountQuery query, long expectedResult, BiPredicate condition) { + return await().atMost(TIMEOUT, TimeUnit.SECONDS).until(() -> countByQuery(query), + result -> condition.test(result, expectedResult)); + } } 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 ad554f11ba..3941f16b88 100644 --- a/application/src/test/java/org/thingsboard/server/controller/EntityQueryControllerTest.java +++ b/application/src/test/java/org/thingsboard/server/controller/EntityQueryControllerTest.java @@ -73,6 +73,7 @@ import java.util.Arrays; import java.util.Collections; import java.util.List; import java.util.concurrent.TimeUnit; +import java.util.function.BiPredicate; import java.util.stream.Collectors; import static org.assertj.core.api.Assertions.assertThat; @@ -80,7 +81,7 @@ import static org.awaitility.Awaitility.await; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; @DaoSqlTest -public class EntityQueryControllerTest extends AbstractControllerTest { +public abstract class EntityQueryControllerTest extends AbstractControllerTest { private static final String CUSTOMER_USER_EMAIL = "entityQueryCustomer@thingsboard.org"; private static final String TENANT_PASSWORD = "testPassword1"; @@ -157,17 +158,10 @@ public class EntityQueryControllerTest extends AbstractControllerTest { @Test public void testSysAdminCountEntitiesByQuery() throws Exception { - loginSysAdmin(); - - EntityTypeFilter allDeviceFilter = new EntityTypeFilter(); - allDeviceFilter.setEntityType(EntityType.DEVICE); - EntityCountQuery query = new EntityCountQuery(allDeviceFilter); - Long initialCount = countByQuery(query); - loginTenantAdmin(); List devices = new ArrayList<>(); - String devicePrefix = "Device" + RandomStringUtils.random(5); + String devicePrefix = "Device" + RandomStringUtils.randomAlphabetic(5); for (int i = 0; i < 97; i++) { Device device = new Device(); device.setName(devicePrefix + i); @@ -177,18 +171,18 @@ public class EntityQueryControllerTest extends AbstractControllerTest { Thread.sleep(1); } DeviceTypeFilter filter = new DeviceTypeFilter(); - filter.setDeviceType("default"); + filter.setDeviceTypes(List.of("default")); filter.setDeviceNameFilter(""); loginSysAdmin(); EntityCountQuery countQuery = new EntityCountQuery(filter); - countByQueryAndCheck(countQuery, initialCount + 97); + countByQueryAndCheck(countQuery, 97, (actual, expected) -> actual >= expected); - filter.setDeviceType("unknown"); + filter.setDeviceTypes(List.of("unknown")); countByQueryAndCheck(countQuery, 0); - filter.setDeviceType("default"); + filter.setDeviceTypes(List.of("default")); filter.setDeviceNameFilter(devicePrefix + "1"); countByQueryAndCheck(countQuery, 11); @@ -199,7 +193,7 @@ public class EntityQueryControllerTest extends AbstractControllerTest { countQuery = new EntityCountQuery(entityListFilter); countByQueryAndCheck(countQuery, 97); - countByQueryAndCheck(query, initialCount + 97); + countByQueryAndCheck(countQuery, 97, (actual, expected) -> actual >= expected); } @Test @@ -847,9 +841,9 @@ public class EntityQueryControllerTest extends AbstractControllerTest { var loadedEntities = new ArrayList<>(data.getData()); return loadedEntities.size() == expectedNumOfDevices; }); - if (expectedNumOfDevices == 0) { - return; - } + if (expectedNumOfDevices == 0) { + return; + } var data = findByQuery(query); var loadedEntities = new ArrayList<>(data.getData()); @@ -870,7 +864,8 @@ public class EntityQueryControllerTest extends AbstractControllerTest { } protected PageData findByQuery(EntityDataQuery query) throws Exception { - return doPostWithTypedResponse("/api/entitiesQuery/find", query, new TypeReference<>() {}); + return doPostWithTypedResponse("/api/entitiesQuery/find", query, new TypeReference<>() { + }); } protected PageData findByQueryAndCheck(EntityDataQuery query, int expectedResultSize) throws Exception { @@ -911,4 +906,9 @@ public class EntityQueryControllerTest extends AbstractControllerTest { return numericFilter; } + protected Long countByQueryAndCheck(EntityCountQuery query, long expectedResult, BiPredicate condition) throws Exception { + Long result = countByQuery(query); + assertThat(condition.test(result, expectedResult)).isTrue(); + return result; + } } diff --git a/application/src/test/java/org/thingsboard/server/service/entitiy/EdqsEntityServiceTest.java b/application/src/test/java/org/thingsboard/server/service/entitiy/EdqsEntityServiceTest.java index eea7e0eb7f..cf24deb7e4 100644 --- a/application/src/test/java/org/thingsboard/server/service/entitiy/EdqsEntityServiceTest.java +++ b/application/src/test/java/org/thingsboard/server/service/entitiy/EdqsEntityServiceTest.java @@ -21,7 +21,6 @@ import org.springframework.boot.test.mock.mockito.MockBean; import org.springframework.test.context.TestPropertySource; import org.thingsboard.server.common.data.id.CustomerId; import org.thingsboard.server.common.data.page.PageData; -import org.thingsboard.server.common.data.permission.MergedUserPermissions; import org.thingsboard.server.common.data.query.EntityCountQuery; import org.thingsboard.server.common.data.query.EntityData; import org.thingsboard.server.common.data.query.EntityDataQuery; @@ -53,19 +52,19 @@ public class EdqsEntityServiceTest extends EntityServiceTest { } @Override - protected PageData findByQueryAndCheck(CustomerId customerId, MergedUserPermissions permissions, EntityDataQuery query, long expectedResultSize) { - return await().atMost(15, TimeUnit.SECONDS).until(() -> findByQuery(customerId, permissions, query), + protected PageData findByQueryAndCheck(CustomerId customerId, EntityDataQuery query, long expectedResultSize) { + return await().atMost(15, TimeUnit.SECONDS).until(() -> findByQuery(customerId, query), result -> result.getTotalElements() == expectedResultSize); } @Override protected long countByQueryAndCheck(EntityCountQuery countQuery, int expectedResult) { - return countByQueryAndCheck(new CustomerId(CustomerId.NULL_UUID), mergedUserPermissionsPE, countQuery, expectedResult); + return countByQueryAndCheck(new CustomerId(CustomerId.NULL_UUID), countQuery, expectedResult); } @Override - protected long countByQueryAndCheck(CustomerId customerId, MergedUserPermissions permissions, EntityCountQuery query, int expectedResult) { - return await().atMost(15, TimeUnit.SECONDS).until(() -> countByQuery(customerId, permissions, query), + protected long countByQueryAndCheck(CustomerId customerId, EntityCountQuery query, int expectedResult) { + return await().atMost(15, TimeUnit.SECONDS).until(() -> countByQuery(customerId, query), result -> result == expectedResult); } 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 04bad95be7..fab4cba3f5 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 @@ -489,7 +489,7 @@ public class EntityServiceTest extends AbstractControllerTest { filter.setFilters(Lists.newArrayList( new RelationEntityTypeFilter("buildingToApt", Collections.singletonList(EntityType.ASSET)), new RelationEntityTypeFilter("AptToEnergy", Collections.singletonList(EntityType.DEVICE)))); - countByQueryAndCheck(countQuery, 9); + countByQueryAndCheck(countQuery, 3); deviceService.deleteDevicesByTenantId(tenantId); assetService.deleteAssetsByTenantId(tenantId); @@ -1444,30 +1444,16 @@ public class EntityServiceTest extends AbstractControllerTest { assertThat(deviceName).isEqualTo(customerDevices.get(0).getName()); // find by customer user with generic permission - MergedUserPermissions mergedGenericPermission = new MergedUserPermissions(Map.of(Resource.DEVICE, Set.of(Operation.READ)), Collections.emptyMap()); - PageData customerResults = findByQueryAndCheck(customerId, mergedGenericPermission, query, 1); + PageData customerResults = findByQueryAndCheck(customerId, query, 1); String cutomerDeviceName = customerResults.getData().get(0).getLatest().get(EntityKeyType.ENTITY_FIELD).get("name").getValue(); assertThat(cutomerDeviceName).isEqualTo(customerDevices.get(0).getName()); - // find by customer user with group permission - MergedUserPermissions mergedGroupOnlyPermission = new MergedUserPermissions(Collections.emptyMap(), Map.of(customerDeviceGroup.getId(), new MergedGroupPermissionInfo(EntityType.DEVICE, Set.of(Operation.READ)))); - PageData result2 = findByQueryAndCheck(customerId, mergedGroupOnlyPermission, query, 1); - - String resultDeviceName2 = result2.getData().get(0).getLatest().get(EntityKeyType.ENTITY_FIELD).get("name").getValue(); - assertThat(resultDeviceName2).isEqualTo(customerDevices.get(0).getName()); - // try to find tenant device by customer user SingleEntityFilter tenantDeviceFilter = new SingleEntityFilter(); tenantDeviceFilter.setSingleEntity(tenantDevices.get(0).getId()); EntityDataQuery customerQuery2 = new EntityDataQuery(tenantDeviceFilter, pageLink, entityFields, null, null); - findByQueryAndCheck(customerId, mergedGenericPermission, customerQuery2, 0); - - // find by tenant user with group permission - PageData results3 = findByQueryAndCheck(new CustomerId(EntityId.NULL_UUID), mergedGroupOnlyPermission, query, 1); - - String deviceName3 = results3.getData().get(0).getLatest().get(EntityKeyType.ENTITY_FIELD).get("name").getValue(); - assertThat(deviceName3).isEqualTo(customerDevices.get(0).getName()); + findByQueryAndCheck(customerId, customerQuery2, 0); } private List getResultDeviceIds(PageData result) { @@ -1634,230 +1620,6 @@ public class EntityServiceTest extends AbstractControllerTest { deviceService.deleteDevicesByTenantId(tenantId); } - @Test - public void testFindEntityDataByRelationQuery_blobEntity_customerLevel() { - final int deviceCnt = 2; - final int relationsCnt = 3; - final int blobEntitiesCnt = deviceCnt * relationsCnt; - - Customer customer = new Customer(); - customer.setTenantId(tenantId); - customer.setTitle("Customer Relation Query"); - customer = customerService.saveCustomer(customer); - - List devices = new ArrayList<>(); - for (int i = 0; i < deviceCnt; i++) { - Device device = new Device(); - device.setTenantId(tenantId); - device.setName("Device relation query " + i); - device.setCustomerId(customer.getId()); - device.setType("default"); - devices.add(deviceService.saveDevice(device)); - } - - List blobEntities = new ArrayList<>(); - for (int i = 0; i < blobEntitiesCnt; i++) { - BlobEntity blobEntity = new BlobEntity(); - blobEntity.setName("Blob relation query " + i); - blobEntity.setTenantId(tenantId); - blobEntity.setContentType("image/png"); - blobEntity.setData(ByteBuffer.allocate(1024)); - blobEntity.setCustomerId(customer.getId()); - blobEntity.setType("Report"); - blobEntities.add(blobEntityService.saveBlobEntity(blobEntity)); - } - - for (int i = 0; i < deviceCnt; i++) { - for (int j = 0; j < relationsCnt; j++) { - EntityRelation relationEntity = new EntityRelation(); - relationEntity.setFrom(devices.get(i).getId()); - relationEntity.setTo(blobEntities.get(j + (i * relationsCnt)).getId()); - relationEntity.setTypeGroup(RelationTypeGroup.COMMON); - relationEntity.setType("fileAttached"); - relationService.saveRelation(tenantId, relationEntity); - } - } - - MergedUserPermissions mergedUserPermissions = new MergedUserPermissions(Map.of(ALL, Set.of(Operation.ALL)), Collections.emptyMap()); - - RelationEntityTypeFilter relationEntityTypeFilter = new RelationEntityTypeFilter("fileAttached", Collections.singletonList(EntityType.BLOB_ENTITY)); - RelationsQueryFilter filter = new RelationsQueryFilter(); - filter.setFilters(Collections.singletonList(relationEntityTypeFilter)); - filter.setDirection(EntitySearchDirection.FROM); - EntityDataPageLink pageLink = new EntityDataPageLink(10, 0, null, null); - - for (Device device : devices) { - filter.setRootEntity(device.getId()); - - EntityDataQuery query = new EntityDataQuery(filter, pageLink, Collections.emptyList(), Collections.emptyList(), Collections.emptyList()); - findByQueryAndCheck(customer.getId(), mergedUserPermissions, query, relationsCnt); - countByQueryAndCheck(customer.getId(), mergedUserPermissions, query, relationsCnt); - /* - In order to be careful with updating Relation Query while adding new Entity Type, - this checkup will help to find place, where you could check the correctness of building query - */ - Assert.assertEquals(38, EntityType.values().length); - } - } - - @Test - public void testFindEntitiesByRelationEntityTypeFilterWithTenantGroupPermission() { - final int assetCount = 2; - final int relationsCnt = 4; - final int deviceEntitiesCnt = assetCount * relationsCnt; - - EntityGroup deviceGroup = new EntityGroup(); - deviceGroup.setName("Device Tenant Level Group"); - deviceGroup.setOwnerId(tenantId); - deviceGroup.setTenantId(tenantId); - deviceGroup.setType(EntityType.DEVICE); - deviceGroup = entityGroupService.saveEntityGroup(tenantId, tenantId, deviceGroup); - - List assets = new ArrayList<>(); - for (int i = 0; i < assetCount; i++) { - Asset building = new Asset(); - building.setTenantId(tenantId); - building.setName("Building _" + i); - building.setType("building"); - building = assetService.saveAsset(building); - assets.add(building); - } - - List devices = new ArrayList<>(); - for (int i = 0; i < deviceEntitiesCnt; i++) { - Device device = new Device(); - device.setTenantId(tenantId); - device.setName("Test device " + i); - device.setType("default"); - Device savedDevice = deviceService.saveDevice(device); - devices.add(savedDevice); - if (i % 2 == 0) { - entityGroupService.addEntityToEntityGroup(tenantId, deviceGroup.getId(), savedDevice.getId()); - } - } - - for (int i = 0; i < assetCount; i++) { - for (int j = 0; j < relationsCnt; j++) { - EntityRelation relationEntity = new EntityRelation(); - relationEntity.setFrom(assets.get(i).getId()); - relationEntity.setTo(devices.get(j + (i * relationsCnt)).getId()); - relationEntity.setTypeGroup(RelationTypeGroup.COMMON); - relationEntity.setType("contains"); - relationService.saveRelation(tenantId, relationEntity); - } - } - - MergedUserPermissions groupOnlyPermission = new MergedUserPermissions(Collections.emptyMap(), - Map.of(deviceGroup.getId(), new MergedGroupPermissionInfo(EntityType.DEVICE, Set.of(Operation.READ)))); - - RelationEntityTypeFilter relationEntityTypeFilter = new RelationEntityTypeFilter("contains", Collections.singletonList(EntityType.DEVICE)); - RelationsQueryFilter filter = new RelationsQueryFilter(); - filter.setFilters(Collections.singletonList(relationEntityTypeFilter)); - filter.setDirection(EntitySearchDirection.FROM); - EntityDataPageLink pageLink = new EntityDataPageLink(10, 0, null, null); - List keyFiltersEqualString = createStringKeyFilters("name", EntityKeyType.ENTITY_FIELD, StringOperation.STARTS_WITH, "Test device "); - - for (Asset asset : assets) { - filter.setRootEntity(asset.getId()); - - EntityDataQuery query = new EntityDataQuery(filter, pageLink, Collections.emptyList(), Collections.emptyList(), keyFiltersEqualString); - findByQueryAndCheck(new CustomerId(EntityId.NULL_UUID), groupOnlyPermission, query, relationsCnt / 2); - countByQueryAndCheck(new CustomerId(EntityId.NULL_UUID), groupOnlyPermission, query, relationsCnt / 2); - } - } - - @Test - public void testFindEntitiesWithRelationEntityTypeFilterByCustomerUser() { - Customer customer = new Customer(); - customer.setTenantId(tenantId); - customer.setTitle("Customer Relation Query"); - customer = customerService.saveCustomer(customer); - - final int assetCount = 2; - final int relationsCnt = 4; - final int deviceEntitiesCnt = assetCount * relationsCnt; - - EntityGroup deviceGroup = new EntityGroup(); - deviceGroup.setName("Device Tenant Level Group"); - deviceGroup.setOwnerId(customer.getId()); - deviceGroup.setTenantId(tenantId); - deviceGroup.setType(EntityType.DEVICE); - deviceGroup = entityGroupService.saveEntityGroup(tenantId, tenantId, deviceGroup); - - List assets = new ArrayList<>(); - for (int i = 0; i < assetCount; i++) { - Asset building = new Asset(); - building.setTenantId(tenantId); - building.setCustomerId(customer.getId()); - building.setName("Building _" + i); - building.setType("building"); - building = assetService.saveAsset(building); - assets.add(building); - } - - List devices = new ArrayList<>(); - for (int i = 0; i < deviceEntitiesCnt; i++) { - Device device = new Device(); - device.setTenantId(tenantId); - device.setCustomerId(customer.getId()); - device.setName("Test device " + i); - device.setType("default"); - Device savedDevice = deviceService.saveDevice(device); - devices.add(savedDevice); - if (i % 2 == 0) { - entityGroupService.addEntityToEntityGroup(tenantId, deviceGroup.getId(), savedDevice.getId()); - } - } - - for (int i = 0; i < assetCount; i++) { - for (int j = 0; j < relationsCnt; j++) { - EntityRelation relationEntity = new EntityRelation(); - relationEntity.setFrom(assets.get(i).getId()); - relationEntity.setTo(devices.get(j + (i * relationsCnt)).getId()); - relationEntity.setTypeGroup(RelationTypeGroup.COMMON); - relationEntity.setType("contains"); - relationService.saveRelation(tenantId, relationEntity); - } - } - - MergedUserPermissions mergedGroupOnlyPermission = new MergedUserPermissions(Collections.emptyMap(), Map.of(deviceGroup.getId(), new MergedGroupPermissionInfo(EntityType.DEVICE, Set.of(Operation.ALL)))); - MergedUserPermissions mergedGenericOnlyPermission = new MergedUserPermissions(Map.of(Resource.ALL, Set.of(Operation.ALL)), Collections.emptyMap()); - MergedUserPermissions mergedGenericAndGroupPermission = new MergedUserPermissions(Map.of(Resource.ALL, Set.of(Operation.ALL)), Map.of(deviceGroup.getId(), new MergedGroupPermissionInfo(EntityType.DEVICE, Set.of(Operation.ALL)))); - RelationEntityTypeFilter relationEntityTypeFilter = new RelationEntityTypeFilter("contains", Collections.singletonList(EntityType.DEVICE)); - RelationsQueryFilter filter = new RelationsQueryFilter(); - filter.setFilters(Collections.singletonList(relationEntityTypeFilter)); - filter.setDirection(EntitySearchDirection.FROM); - EntityDataPageLink pageLink = new EntityDataPageLink(10, 0, null, null); - List keyFiltersEqualString = createStringKeyFilters("name", EntityKeyType.ENTITY_FIELD, StringOperation.STARTS_WITH, "Test device "); - - EntityDataQuery query = new EntityDataQuery(filter, pageLink, Collections.emptyList(), Collections.emptyList(), keyFiltersEqualString); - - for (Asset asset : assets) { - filter.setRootEntity(asset.getId()); - - //check by user with generic permission - PageData relationsResult = findByQueryAndCheck(customer.getId(), mergedGenericOnlyPermission, query, relationsCnt); - countByQueryAndCheck(customer.getId(), mergedGenericOnlyPermission, query, relationsCnt); - - //check by user with generic and group permission - PageData relationsResult1 = findByQueryAndCheck(customer.getId(), mergedGenericAndGroupPermission, query, relationsCnt); - countByQueryAndCheck(customer.getId(), mergedGenericAndGroupPermission, query, relationsCnt); - - //check by other customer user with group only permission - PageData relationsResult2 = findByQueryAndCheck(otherCustomerId, mergedGroupOnlyPermission, query, relationsCnt / 2); - long relationsResultCnt2 = countByQueryAndCheck(otherCustomerId, mergedGroupOnlyPermission, query, relationsCnt / 2); - - Assert.assertEquals(relationsCnt / 2, relationsResult2.getData().size()); - Assert.assertEquals(relationsCnt / 2, relationsResultCnt2); - - //check by other customer user with generic and group only permission - PageData relationsResult3 = findByQueryAndCheck(otherCustomerId, mergedGenericAndGroupPermission, query, relationsCnt / 2); - long relationsResultCnt3 = countByQueryAndCheck(otherCustomerId, mergedGenericAndGroupPermission, query, relationsCnt / 2); - - Assert.assertEquals(relationsCnt / 2, relationsResult3.getData().size()); - Assert.assertEquals(relationsCnt / 2, relationsResultCnt3); - } - } @Test public void testBuildNumericPredicateQueryOperations() throws ExecutionException, InterruptedException { diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/ObjectType.java b/common/data/src/main/java/org/thingsboard/server/common/data/ObjectType.java index c78998cd6a..d3aa547bf0 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/ObjectType.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/ObjectType.java @@ -30,16 +30,8 @@ public enum ObjectType { RULE_CHAIN, OTA_PACKAGE, RESOURCE, - ROLE, - ENTITY_GROUP, - DEVICE_GROUP_OTA_PACKAGE, - GROUP_PERMISSION, - BLOB_ENTITY, - SCHEDULER_EVENT, EVENT, RULE_NODE, - CONVERTER, - INTEGRATION, USER, USER_CREDENTIALS, USER_AUTH_SETTINGS, @@ -62,8 +54,6 @@ public enum ObjectType { NOTIFICATION_TARGET, NOTIFICATION_TEMPLATE, NOTIFICATION_RULE, - WHITE_LABELING, - CUSTOM_TRANSLATION, ALARM_COMMENT, ALARM_TYPE, API_USAGE_STATE, @@ -75,16 +65,15 @@ public enum ObjectType { LATEST_TS_KV; public static final Set edqsTenantTypes = EnumSet.of( - TENANT_PROFILE, CUSTOMER, DEVICE_PROFILE, DEVICE, ASSET_PROFILE, ASSET, EDGE, ENTITY_VIEW, USER, DASHBOARD, - RULE_CHAIN, WIDGET_TYPE, WIDGETS_BUNDLE, CONVERTER, INTEGRATION, SCHEDULER_EVENT, ROLE, - BLOB_ENTITY, API_USAGE_STATE, QUEUE_STATS + TENANT, TENANT_PROFILE, CUSTOMER, DEVICE_PROFILE, DEVICE, ASSET_PROFILE, ASSET, EDGE, ENTITY_VIEW, USER, DASHBOARD, + RULE_CHAIN, WIDGET_TYPE, WIDGETS_BUNDLE, API_USAGE_STATE, QUEUE_STATS ); public static final Set edqsTypes = new HashSet<>(edqsTenantTypes); public static final Set edqsSystemTypes = EnumSet.of(TENANT, TENANT_PROFILE, USER, DASHBOARD, API_USAGE_STATE, ATTRIBUTE_KV, LATEST_TS_KV); static { - edqsTypes.addAll(Arrays.asList(TENANT, ENTITY_GROUP, RELATION, ATTRIBUTE_KV, LATEST_TS_KV)); + edqsTypes.addAll(Arrays.asList(TENANT, RELATION, ATTRIBUTE_KV, LATEST_TS_KV)); } public EntityType toEntityType() { diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/edqs/fields/DashboardFields.java b/common/data/src/main/java/org/thingsboard/server/common/data/edqs/fields/DashboardFields.java index af1640b7be..fa57f9d0f4 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/edqs/fields/DashboardFields.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/edqs/fields/DashboardFields.java @@ -15,10 +15,15 @@ */ package org.thingsboard.server.common.data.edqs.fields; +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.ObjectMapper; import lombok.Data; import lombok.NoArgsConstructor; import lombok.experimental.SuperBuilder; +import java.util.ArrayList; +import java.util.List; import java.util.UUID; @@ -27,7 +32,30 @@ import java.util.UUID; @SuperBuilder public class DashboardFields extends AbstractEntityFields { - public DashboardFields(UUID id, long createdTime, UUID tenantId, UUID customerId, String name, Long version) { - super(id, createdTime, tenantId, customerId, name, version); + private static ObjectMapper objectMapper = new ObjectMapper(); + private List assignedCustomerIds; + + public DashboardFields(UUID id, long createdTime, UUID tenantId, String assignedCustomers, String name, Long version) { + super(id, createdTime, tenantId, name, version); + this.assignedCustomerIds = getCustomerIds(assignedCustomers); + } + + private static List getCustomerIds(String assignedCustomers) { + List ids = new ArrayList<>(); + if (assignedCustomers == null || assignedCustomers.isEmpty()) { + return ids; + } + try { + JsonNode rootNode = objectMapper.readTree(assignedCustomers); + for (JsonNode node : rootNode) { + String idStr = node.path("customerId").path("id").asText(); + if (!idStr.isEmpty()) { + ids.add(UUID.fromString(idStr)); + } + } + } catch (JsonProcessingException e) { + throw new RuntimeException(e); + } + return ids; } } diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/edqs/fields/EntityFields.java b/common/data/src/main/java/org/thingsboard/server/common/data/edqs/fields/EntityFields.java index 7536586f57..ba9f3b0e9b 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/edqs/fields/EntityFields.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/edqs/fields/EntityFields.java @@ -19,6 +19,8 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.thingsboard.server.common.data.id.EntityId; +import java.util.Collections; +import java.util.List; import java.util.UUID; public interface EntityFields { @@ -37,6 +39,10 @@ public interface EntityFields { return null; } + default List getAssignedCustomerIds() { + return Collections.emptyList(); + } + default long getCreatedTime() { return 0; } diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/edqs/fields/EntityViewFields.java b/common/data/src/main/java/org/thingsboard/server/common/data/edqs/fields/EntityViewFields.java index 5635566cc4..01c32f820e 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/edqs/fields/EntityViewFields.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/edqs/fields/EntityViewFields.java @@ -19,6 +19,8 @@ import lombok.Data; import lombok.NoArgsConstructor; import lombok.experimental.SuperBuilder; +import java.util.UUID; + @Data @NoArgsConstructor @SuperBuilder @@ -27,4 +29,9 @@ public class EntityViewFields extends AbstractEntityFields { private String type; private String additionalInfo; + public EntityViewFields(UUID id, long createdTime, UUID tenantId, UUID customerId, String name, String type, String additionalInfo, Long version) { + super(id, createdTime, tenantId, customerId, name, version); + this.type = type; + this.additionalInfo = additionalInfo; + } } diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/edqs/fields/FieldsUtil.java b/common/data/src/main/java/org/thingsboard/server/common/data/edqs/fields/FieldsUtil.java index f1cf51bf8b..6543a6d779 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/edqs/fields/FieldsUtil.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/edqs/fields/FieldsUtil.java @@ -85,7 +85,6 @@ public class FieldsUtil { return CustomerFields.builder() .id(entity.getUuidId()) .createdTime(entity.getCreatedTime()) - .customerId(getCustomerId(entity.getCustomerId())) .name(entity.getTitle()) .additionalInfo(getText(entity.getAdditionalInfo())) .email(entity.getEmail()) @@ -199,7 +198,6 @@ public class FieldsUtil { return DashboardFields.builder() .id(entity.getUuidId()) .createdTime(entity.getCreatedTime()) - .customerId(getCustomerId(entity.getCustomerId())) .name(entity.getTitle()) .version(entity.getVersion()) .build(); diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/edqs/query/EdqsRequest.java b/common/data/src/main/java/org/thingsboard/server/common/data/edqs/query/EdqsRequest.java index 135061a842..b776d18551 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/edqs/query/EdqsRequest.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/edqs/query/EdqsRequest.java @@ -15,12 +15,10 @@ */ package org.thingsboard.server.common.data.edqs.query; -import com.fasterxml.jackson.annotation.JsonIncludeProperties; import lombok.AllArgsConstructor; import lombok.Builder; import lombok.Data; import lombok.NoArgsConstructor; -import org.thingsboard.server.common.data.permission.MergedUserPermissions; import org.thingsboard.server.common.data.query.EntityCountQuery; import org.thingsboard.server.common.data.query.EntityDataQuery; @@ -32,7 +30,5 @@ public class EdqsRequest { private EntityDataQuery entityDataQuery; private EntityCountQuery entityCountQuery; - @JsonIncludeProperties({"genericPermissions", "groupPermissions"}) - private MergedUserPermissions userPermissions; } diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/edqs/query/QueryResult.java b/common/data/src/main/java/org/thingsboard/server/common/data/edqs/query/QueryResult.java index 0c45812690..70e25ba14e 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/edqs/query/QueryResult.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/edqs/query/QueryResult.java @@ -30,12 +30,10 @@ import java.util.Map; public class QueryResult { private final EntityId entityId; - private final boolean readAttrs; - private final boolean readTs; private final Map> latest; public EntityData toOldEntityData() { - return new EntityData(entityId, readAttrs, readTs, latest, Collections.emptyMap(), Collections.emptyMap()); + return new EntityData(entityId, latest, Collections.emptyMap(), Collections.emptyMap()); } } diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/permission/QueryContext.java b/common/data/src/main/java/org/thingsboard/server/common/data/permission/QueryContext.java index b377d63ee4..b8b3f51aec 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/permission/QueryContext.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/permission/QueryContext.java @@ -44,4 +44,7 @@ public class QueryContext { this(tenantId, customerId, entityType, false); } + public boolean isTenantUser() { + return customerId == null || customerId.isNullUid(); + } } \ No newline at end of file diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/queue/QueueStats.java b/common/data/src/main/java/org/thingsboard/server/common/data/queue/QueueStats.java index 9c3536dea1..6c648daf02 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/queue/QueueStats.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/queue/QueueStats.java @@ -18,15 +18,13 @@ package org.thingsboard.server.common.data.queue; import lombok.Data; import lombok.EqualsAndHashCode; import org.thingsboard.server.common.data.BaseData; -import org.thingsboard.server.common.data.EntityType; -import org.thingsboard.server.common.data.HasEntityType; import org.thingsboard.server.common.data.HasTenantId; import org.thingsboard.server.common.data.id.QueueStatsId; import org.thingsboard.server.common.data.id.TenantId; @EqualsAndHashCode(callSuper = true) @Data -public class QueueStats extends BaseData implements HasTenantId, HasEntityType { +public class QueueStats extends BaseData implements HasTenantId { private TenantId tenantId; private String queueName; private String serviceId; @@ -38,8 +36,4 @@ public class QueueStats extends BaseData implements HasTenantId, H super(id); } - @Override - public EntityType getEntityType() { - return EntityType.QUEUE_STATS; - } } diff --git a/common/edqs/pom.xml b/common/edqs/pom.xml index 40e89acee6..e58c7c97a0 100644 --- a/common/edqs/pom.xml +++ b/common/edqs/pom.xml @@ -1,6 +1,6 @@ A2, A1 --Contains--> D1. A1 --Manages--> D2. - createRelation(EntityType.ASSET, ta1, EntityType.ASSET, ta2, "Contains"); - createRelation(EntityType.ASSET, ta1, EntityType.DEVICE, da1, "Contains"); - createRelation(EntityType.ASSET, ta1, EntityType.DEVICE, da2, "Manages"); - - MergedUserPermissions readGroupPermissions = new MergedUserPermissions(Collections.emptyMap(), Collections.singletonMap(new EntityGroupId(eg1), - new MergedGroupPermissionInfo(EntityType.DEVICE, new HashSet<>(Arrays.asList(Operation.READ, Operation.READ_ATTRIBUTES, Operation.READ_TELEMETRY))))); - - PageData relationsResult = filter(readGroupPermissions, new AssetId(ta1), new RelationEntityTypeFilter("Contains", Arrays.asList(EntityType.DEVICE, EntityType.ASSET))); - Assert.assertEquals(1, relationsResult.getData().size()); - Assert.assertTrue(checkContains(relationsResult, da1)); - - relationsResult = filter(readGroupPermissions, new AssetId(ta1), new RelationEntityTypeFilter("Manages", Arrays.asList(EntityType.DEVICE, EntityType.ASSET))); - Assert.assertEquals(0, relationsResult.getData().size()); - } - @Test public void testFindCustomerDevices() { UUID ta1 = createAsset("T A1"); @@ -169,55 +136,15 @@ public class RelationsQueryFilterTest extends AbstractEDQTest { Assert.assertEquals(0, relationsResult.getData().size()); } - @Test - public void testFindCustomerDevicesGroupsOnly() { - UUID ta1 = createAsset("T A1"); - UUID ta2 = createAsset("T A2"); - UUID da1 = createDevice(customerId, "T D1"); - UUID da2 = createDevice(customerId, "T D2"); - UUID da3 = createDevice(customerId, "T D3"); - - UUID eg1 = createGroup(EntityType.DEVICE, "Group A"); - createRelation(EntityType.ENTITY_GROUP, eg1, EntityType.DEVICE, da1, RelationTypeGroup.FROM_ENTITY_GROUP, "Contains"); - createRelation(EntityType.ENTITY_GROUP, eg1, EntityType.DEVICE, da2, RelationTypeGroup.FROM_ENTITY_GROUP, "Contains"); - createRelation(EntityType.ENTITY_GROUP, eg1, EntityType.DEVICE, da3, RelationTypeGroup.FROM_ENTITY_GROUP, "Contains"); - - // A1 --Contains--> A2, A1 --Contains--> D1. A1 --Manages--> D2. - createRelation(EntityType.ASSET, ta1, EntityType.ASSET, ta2, "Contains"); - createRelation(EntityType.ASSET, ta1, EntityType.DEVICE, da1, "Contains"); - createRelation(EntityType.ASSET, ta1, EntityType.DEVICE, da2, "Manages"); - createRelation(EntityType.DEVICE, da2, EntityType.DEVICE, da3, "Contains"); - - MergedUserPermissions readGroupPermissions = new MergedUserPermissions(Collections.emptyMap(), Collections.singletonMap(new EntityGroupId(eg1), - new MergedGroupPermissionInfo(EntityType.DEVICE, new HashSet<>(Arrays.asList(Operation.READ, Operation.READ_ATTRIBUTES, Operation.READ_TELEMETRY))))); - - PageData relationsResult = filter(readGroupPermissions, customerId, new AssetId(ta1), new RelationEntityTypeFilter("Contains", Arrays.asList(EntityType.DEVICE, EntityType.ASSET))); - Assert.assertEquals(2, relationsResult.getData().size()); - Assert.assertTrue(checkContains(relationsResult, da1)); - Assert.assertTrue(checkContains(relationsResult, da3)); - - relationsResult = filter(readGroupPermissions, customerId, new AssetId(ta1), new RelationEntityTypeFilter("Manages", Arrays.asList(EntityType.DEVICE, EntityType.ASSET))); - Assert.assertEquals(1, relationsResult.getData().size()); - Assert.assertTrue(checkContains(relationsResult, da2)); - } - private PageData filter(EntityId rootId, RelationEntityTypeFilter... relationEntityTypeFilters) { - return filter(RepositoryUtils.ALL_READ_PERMISSIONS, rootId, relationEntityTypeFilters); - } - - private PageData filter(MergedUserPermissions permissions, EntityId rootId, RelationEntityTypeFilter... relationEntityTypeFilters) { - return filter(permissions, null, rootId, relationEntityTypeFilters); + return filter(null, rootId, relationEntityTypeFilters); } private PageData filter(CustomerId customerId, EntityId rootId, RelationEntityTypeFilter... relationEntityTypeFilters) { - return filter(RepositoryUtils.ALL_READ_PERMISSIONS, customerId, rootId, relationEntityTypeFilters); - } - - private PageData filter(MergedUserPermissions permissions, CustomerId customerId, EntityId rootId, RelationEntityTypeFilter... relationEntityTypeFilters) { - return filter(permissions, customerId, rootId, 3, false, relationEntityTypeFilters); + return filter(customerId, rootId, 3, false, relationEntityTypeFilters); } - private PageData filter(MergedUserPermissions permissions, CustomerId customerId, EntityId rootId, int maxLevel, boolean lastLevelOnly, RelationEntityTypeFilter... relationEntityTypeFilters) { + private PageData filter(CustomerId customerId, EntityId rootId, int maxLevel, boolean lastLevelOnly, RelationEntityTypeFilter... relationEntityTypeFilters) { RelationsQueryFilter filter = new RelationsQueryFilter(); filter.setRootEntity(rootId); filter.setFilters(Arrays.asList(relationEntityTypeFilters)); @@ -227,7 +154,7 @@ public class RelationsQueryFilterTest extends AbstractEDQTest { EntityDataPageLink pageLink = new EntityDataPageLink(10, 0, null, null); List keyFiltersEqualString = createStringKeyFilters("name", EntityKeyType.ENTITY_FIELD, StringFilterPredicate.StringOperation.STARTS_WITH, "T"); EntityDataQuery query = new EntityDataQuery(filter, pageLink, Collections.emptyList(), Collections.emptyList(), keyFiltersEqualString); - return repository.findEntityDataByQuery(tenantId, customerId, permissions, query, false); + return repository.findEntityDataByQuery(tenantId, customerId, query, false); } } diff --git a/edqs/src/test/java/org/thingsboard/server/edqs/repo/RepositoryUtilsTest.java b/edqs/src/test/java/org/thingsboard/server/edqs/repo/RepositoryUtilsTest.java index 42706a796a..48a72a9cc6 100644 --- a/edqs/src/test/java/org/thingsboard/server/edqs/repo/RepositoryUtilsTest.java +++ b/edqs/src/test/java/org/thingsboard/server/edqs/repo/RepositoryUtilsTest.java @@ -1,5 +1,5 @@ /** - * Copyright © 2016-2024 ThingsBoard, Inc. + * Copyright © 2016-2024 The Thingsboard Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/edqs/src/test/java/org/thingsboard/server/edqs/repo/SchedulerEventFilterTest.java b/edqs/src/test/java/org/thingsboard/server/edqs/repo/SchedulerEventFilterTest.java deleted file mode 100644 index 8ebbff534d..0000000000 --- a/edqs/src/test/java/org/thingsboard/server/edqs/repo/SchedulerEventFilterTest.java +++ /dev/null @@ -1,129 +0,0 @@ -/** - * Copyright © 2016-2024 ThingsBoard, Inc. - * - * 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.edqs.repo; - -import org.junit.After; -import org.junit.Assert; -import org.junit.Before; -import org.junit.Test; -import org.thingsboard.server.common.data.id.DashboardId; -import org.thingsboard.server.common.data.id.DeviceId; -import org.thingsboard.server.common.data.id.EntityId; -import org.thingsboard.server.common.data.query.EntityDataPageLink; -import org.thingsboard.server.common.data.query.EntityDataQuery; -import org.thingsboard.server.common.data.query.EntityDataSortOrder; -import org.thingsboard.server.common.data.query.EntityKey; -import org.thingsboard.server.common.data.query.EntityKeyType; -import org.thingsboard.server.common.data.query.EntityKeyValueType; -import org.thingsboard.server.common.data.query.FilterPredicateValue; -import org.thingsboard.server.common.data.query.KeyFilter; -import org.thingsboard.server.common.data.query.SchedulerEventFilter; -import org.thingsboard.server.common.data.query.StringFilterPredicate; -import org.thingsboard.server.edqs.util.RepositoryUtils; - -import java.util.Arrays; -import java.util.List; -import java.util.UUID; - -public class SchedulerEventFilterTest extends AbstractEDQTest { - - @Before - public void setUp() { - } - - @After - public void tearDown() { - } - - @Test - public void testFindTenantSchedulerEvents() { - UUID dashboardId = createDashboard("test dashboard"); - UUID deviceId = createDevice("test device"); - - UUID eventId1 = createSchedulerEvent("Update attributes", new DeviceId(deviceId), "Turn off device"); - UUID eventId2 = createSchedulerEvent("Generate report", new DashboardId(dashboardId), "Generate morning report"); - UUID eventId3 = createSchedulerEvent("Generate report", new DashboardId(dashboardId), "Generate evening report"); - - // find all scheduler events with type "Generate report" - var result = repository.findEntityDataByQuery(tenantId, null, RepositoryUtils.ALL_READ_PERMISSIONS, getSchedulerEventQuery("Generate report", null, null), false); - Assert.assertEquals(2, result.getTotalElements()); - Assert.assertTrue(checkContains(result, eventId2)); - Assert.assertTrue(checkContains(result, eventId3)); - - // find all scheduler events for device originator - result = repository.findEntityDataByQuery(tenantId, null, RepositoryUtils.ALL_READ_PERMISSIONS, getSchedulerEventQuery(null, new DeviceId(deviceId), null), false); - Assert.assertEquals(1, result.getTotalElements()); - Assert.assertTrue(checkContains(result, eventId1)); - - // find all scheduler events with name "%morning%" - KeyFilter containsNameFilter = getSchedulerEventNameKeyFilter(StringFilterPredicate.StringOperation.CONTAINS, "morning", true); - result = repository.findEntityDataByQuery(tenantId, null, RepositoryUtils.ALL_READ_PERMISSIONS, getSchedulerEventQuery(null, null, List.of(containsNameFilter)), false); - Assert.assertEquals(1, result.getTotalElements()); - Assert.assertTrue(checkContains(result, eventId2)); - } - - @Test - public void testFindCustomerEdges() { - UUID dashboardId = createDashboard( "test dashboard"); - UUID deviceId = createDevice("test device"); - - UUID eventId1 = createSchedulerEvent(customerId.getId(), "Update attributes", new DeviceId(deviceId), "Turn off device"); - UUID eventId2 = createSchedulerEvent(customerId.getId(), "Generate report", new DashboardId(dashboardId), "Generate morning report"); - UUID eventId3 = createSchedulerEvent(customerId.getId(), "Generate report", new DashboardId(dashboardId), "Generate evening report"); - - // find all scheduler events with type "Generate report" - var result = repository.findEntityDataByQuery(tenantId, customerId, RepositoryUtils.ALL_READ_PERMISSIONS, getSchedulerEventQuery("Generate report", null, null), false); - Assert.assertEquals(2, result.getTotalElements()); - Assert.assertTrue(checkContains(result, eventId2)); - Assert.assertTrue(checkContains(result, eventId3)); - - // find all scheduler events for device originator - result = repository.findEntityDataByQuery(tenantId, customerId, RepositoryUtils.ALL_READ_PERMISSIONS, getSchedulerEventQuery(null, new DeviceId(deviceId), null), false); - Assert.assertEquals(1, result.getTotalElements()); - Assert.assertTrue(checkContains(result, eventId1)); - - // find all scheduler events with name "%morning%" - KeyFilter containsNameFilter = getSchedulerEventNameKeyFilter(StringFilterPredicate.StringOperation.CONTAINS, "morning", true); - result = repository.findEntityDataByQuery(tenantId, customerId, RepositoryUtils.ALL_READ_PERMISSIONS, getSchedulerEventQuery(null, null, List.of(containsNameFilter)), false); - Assert.assertEquals(1, result.getTotalElements()); - Assert.assertTrue(checkContains(result, eventId2)); - } - - private static EntityDataQuery getSchedulerEventQuery(String eventType, EntityId entityId, List keyFilters) { - SchedulerEventFilter filter = new SchedulerEventFilter(); - filter.setEventType(eventType); - filter.setOriginator(entityId); - var pageLink = new EntityDataPageLink(20, 0, null, new EntityDataSortOrder(new EntityKey(EntityKeyType.ENTITY_FIELD, "createdTime"), EntityDataSortOrder.Direction.DESC), false); - - var entityFields = Arrays.asList(new EntityKey(EntityKeyType.ENTITY_FIELD, "name"), new EntityKey(EntityKeyType.ENTITY_FIELD, "createdTime")); - var latestValues = Arrays.asList(new EntityKey(EntityKeyType.TIME_SERIES, "state")); - - return new EntityDataQuery(filter, pageLink, entityFields, latestValues, keyFilters); - } - - private static KeyFilter getSchedulerEventNameKeyFilter(StringFilterPredicate.StringOperation operation, String predicateValue, boolean ignoreCase) { - KeyFilter nameFilter = new KeyFilter(); - nameFilter.setKey(new EntityKey(EntityKeyType.ENTITY_FIELD, "name")); - var predicate = new StringFilterPredicate(); - predicate.setIgnoreCase(ignoreCase); - predicate.setOperation(operation); - predicate.setValue(new FilterPredicateValue<>(predicateValue)); - nameFilter.setPredicate(predicate); - nameFilter.setValueType(EntityKeyValueType.STRING); - return nameFilter; - } - -} diff --git a/edqs/src/test/java/org/thingsboard/server/edqs/repo/SingleEntityFilterTest.java b/edqs/src/test/java/org/thingsboard/server/edqs/repo/SingleEntityFilterTest.java index 8755191e39..c7de17e33c 100644 --- a/edqs/src/test/java/org/thingsboard/server/edqs/repo/SingleEntityFilterTest.java +++ b/edqs/src/test/java/org/thingsboard/server/edqs/repo/SingleEntityFilterTest.java @@ -1,5 +1,5 @@ /** - * Copyright © 2016-2024 ThingsBoard, Inc. + * Copyright © 2016-2024 The Thingsboard Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -22,16 +22,10 @@ import org.junit.Test; import org.thingsboard.server.common.data.Device; import org.thingsboard.server.common.data.EntityType; import org.thingsboard.server.common.data.edqs.LatestTsKv; -import org.thingsboard.server.common.data.edqs.query.QueryResult; import org.thingsboard.server.common.data.id.DeviceId; import org.thingsboard.server.common.data.id.DeviceProfileId; -import org.thingsboard.server.common.data.id.EntityGroupId; import org.thingsboard.server.common.data.kv.BasicTsKvEntry; import org.thingsboard.server.common.data.kv.StringDataEntry; -import org.thingsboard.server.common.data.permission.MergedGroupPermissionInfo; -import org.thingsboard.server.common.data.permission.MergedUserPermissions; -import org.thingsboard.server.common.data.permission.Operation; -import org.thingsboard.server.common.data.permission.Resource; import org.thingsboard.server.common.data.query.EntityDataPageLink; import org.thingsboard.server.common.data.query.EntityDataQuery; import org.thingsboard.server.common.data.query.EntityDataSortOrder; @@ -42,12 +36,8 @@ import org.thingsboard.server.common.data.query.FilterPredicateValue; import org.thingsboard.server.common.data.query.KeyFilter; import org.thingsboard.server.common.data.query.SingleEntityFilter; import org.thingsboard.server.common.data.query.StringFilterPredicate; -import org.thingsboard.server.common.data.relation.RelationTypeGroup; -import org.thingsboard.server.edqs.util.RepositoryUtils; import java.util.Arrays; -import java.util.Map; -import java.util.Set; import java.util.UUID; public class SingleEntityFilterTest extends AbstractEDQTest { @@ -72,7 +62,7 @@ public class SingleEntityFilterTest extends AbstractEDQTest { addOrUpdate(EntityType.DEVICE, device); addOrUpdate(new LatestTsKv(deviceId, new BasicTsKvEntry(43, new StringDataEntry("state", "TEST")), 0L)); - var result = repository.findEntityDataByQuery(tenantId, null, RepositoryUtils.ALL_READ_PERMISSIONS, getEntityDataQuery(device.getId()), false); + var result = repository.findEntityDataByQuery(tenantId, null, getEntityDataQuery(device.getId()), false); Assert.assertEquals(1, result.getTotalElements()); var first = result.getData().get(0); @@ -80,13 +70,13 @@ public class SingleEntityFilterTest extends AbstractEDQTest { Assert.assertEquals("LoRa-1", first.getLatest().get(EntityKeyType.ENTITY_FIELD).get("name").getValue()); Assert.assertEquals("42", first.getLatest().get(EntityKeyType.ENTITY_FIELD).get("createdTime").getValue()); - result = repository.findEntityDataByQuery(tenantId, null, RepositoryUtils.ALL_READ_PERMISSIONS, getEntityDataQuery(new DeviceId(UUID.randomUUID())), false); + result = repository.findEntityDataByQuery(tenantId, null, getEntityDataQuery(new DeviceId(UUID.randomUUID())), false); Assert.assertEquals(0, result.getTotalElements()); device.setCustomerId(customerId); addOrUpdate(EntityType.DEVICE, device); - result = repository.findEntityDataByQuery(tenantId, null, RepositoryUtils.ALL_READ_PERMISSIONS, getEntityDataQuery(device.getId()), false); + result = repository.findEntityDataByQuery(tenantId, null, getEntityDataQuery(device.getId()), false); Assert.assertEquals(1, result.getTotalElements()); first = result.getData().get(0); Assert.assertEquals(deviceId, first.getEntityId()); @@ -94,39 +84,6 @@ public class SingleEntityFilterTest extends AbstractEDQTest { Assert.assertEquals("42", first.getLatest().get(EntityKeyType.ENTITY_FIELD).get("createdTime").getValue()); } - @Test - public void testFindTenantDeviceWithGenericAndGroupPermission() { - UUID deviceId = createDevice(customerId, "LoRa-customer-1"); - UUID deviceId2 = createDevice(customerId, "LoRa-customer-2"); - UUID deviceId3 = createDevice(customerId, "LoRa-customer-3"); - - // add device and device 2 to Group A - UUID groupAId = createGroup(customerId.getId(), EntityType.DEVICE, "Group A"); - createRelation(EntityType.ENTITY_GROUP, groupAId, EntityType.DEVICE, deviceId, RelationTypeGroup.FROM_ENTITY_GROUP, "Contains"); - createRelation(EntityType.ENTITY_GROUP, groupAId, EntityType.DEVICE, deviceId2, RelationTypeGroup.FROM_ENTITY_GROUP, "Contains"); - - // add device and device 2 to Group A - UUID groupBId = createGroup(customerId.getId(), EntityType.DEVICE, "Group B"); - createRelation(EntityType.ENTITY_GROUP, groupAId, EntityType.DEVICE, deviceId3, RelationTypeGroup.FROM_ENTITY_GROUP, "Contains"); - - MergedUserPermissions genericAndGroupAPermission = new MergedUserPermissions( - Map.of(Resource.ALL, Set.of(Operation.ALL)), Map.of(new EntityGroupId(groupAId), new MergedGroupPermissionInfo(EntityType.DEVICE, Set.of(Operation.ALL)))); - var result = repository.findEntityDataByQuery(tenantId, null, genericAndGroupAPermission, getEntityDataQuery(new DeviceId(deviceId2)), false); - Assert.assertEquals(1, result.getTotalElements()); - QueryResult queryResult = result.getData().get(0); - Assert.assertEquals(deviceId2, queryResult.getEntityId().getId()); - Assert.assertEquals("LoRa-customer-2", queryResult.getLatest().get(EntityKeyType.ENTITY_FIELD).get("name").getValue()); - - // find device without permission - MergedUserPermissions genericAndGroupBPermission = new MergedUserPermissions( - Map.of(Resource.ALL, Set.of(Operation.ALL)), Map.of(new EntityGroupId(groupAId), new MergedGroupPermissionInfo(EntityType.DEVICE, Set.of(Operation.ALL)))); - result = repository.findEntityDataByQuery(tenantId, null, genericAndGroupBPermission, getEntityDataQuery(new DeviceId(deviceId3)), false); - Assert.assertEquals(1, result.getTotalElements()); - queryResult = result.getData().get(0); - Assert.assertEquals(deviceId3, queryResult.getEntityId().getId()); - Assert.assertEquals("LoRa-customer-3", queryResult.getLatest().get(EntityKeyType.ENTITY_FIELD).get("name").getValue()); - } - @Test public void testFindCustomerDevice() { DeviceId deviceId = new DeviceId(UUID.randomUUID()); @@ -139,13 +96,13 @@ public class SingleEntityFilterTest extends AbstractEDQTest { addOrUpdate(EntityType.DEVICE, device); addOrUpdate(new LatestTsKv(deviceId, new BasicTsKvEntry(43, new StringDataEntry("state", "TEST")), 0L)); - var result = repository.findEntityDataByQuery(tenantId, customerId, RepositoryUtils.ALL_READ_PERMISSIONS, getEntityDataQuery(device.getId()), false); + var result = repository.findEntityDataByQuery(tenantId, customerId, getEntityDataQuery(device.getId()), false); Assert.assertEquals(0, result.getTotalElements()); device.setCustomerId(customerId); addOrUpdate(EntityType.DEVICE, device); - result = repository.findEntityDataByQuery(tenantId, customerId, RepositoryUtils.ALL_READ_PERMISSIONS, getEntityDataQuery(device.getId()), false); + result = repository.findEntityDataByQuery(tenantId, customerId, getEntityDataQuery(device.getId()), false); Assert.assertEquals(1, result.getTotalElements()); var first = result.getData().get(0); diff --git a/edqs/src/test/java/org/thingsboard/server/edqs/repo/StateEntityOwnerFilterTest.java b/edqs/src/test/java/org/thingsboard/server/edqs/repo/StateEntityOwnerFilterTest.java deleted file mode 100644 index f0bb4c4e0b..0000000000 --- a/edqs/src/test/java/org/thingsboard/server/edqs/repo/StateEntityOwnerFilterTest.java +++ /dev/null @@ -1,81 +0,0 @@ -/** - * Copyright © 2016-2024 ThingsBoard, Inc. - * - * 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.edqs.repo; - -import org.junit.After; -import org.junit.Assert; -import org.junit.Before; -import org.junit.Test; -import org.thingsboard.server.common.data.id.CustomerId; -import org.thingsboard.server.common.data.id.DeviceId; -import org.thingsboard.server.common.data.query.EntityDataPageLink; -import org.thingsboard.server.common.data.query.EntityDataQuery; -import org.thingsboard.server.common.data.query.EntityDataSortOrder; -import org.thingsboard.server.common.data.query.EntityKey; -import org.thingsboard.server.common.data.query.EntityKeyType; -import org.thingsboard.server.common.data.query.EntityKeyValueType; -import org.thingsboard.server.common.data.query.FilterPredicateValue; -import org.thingsboard.server.common.data.query.KeyFilter; -import org.thingsboard.server.common.data.query.StateEntityOwnerFilter; -import org.thingsboard.server.common.data.query.StringFilterPredicate; -import org.thingsboard.server.edqs.util.RepositoryUtils; - -import java.util.Arrays; -import java.util.UUID; - -public class StateEntityOwnerFilterTest extends AbstractEDQTest { - - @Before - public void setUp() { - } - - @After - public void tearDown() { - } - - @Test - public void testFindCustomerDeviceOwner() { - UUID customerId = UUID.randomUUID(); - createCustomer(customerId, null, "Customer A"); - UUID deviceId = createDevice(new CustomerId(customerId), "LoRa-1"); - - var result = repository.findEntityDataByQuery(tenantId, null, RepositoryUtils.ALL_READ_PERMISSIONS, getEntityDataQuery(new DeviceId(deviceId)), false); - - Assert.assertEquals(1, result.getTotalElements()); - var customer = result.getData().get(0); - Assert.assertEquals(customerId, customer.getEntityId().getId()); - Assert.assertEquals("Customer A", customer.getLatest().get(EntityKeyType.ENTITY_FIELD).get("name").getValue()); - } - - private static EntityDataQuery getEntityDataQuery(DeviceId deviceId) { - StateEntityOwnerFilter filter = new StateEntityOwnerFilter(); - filter.setSingleEntity(deviceId); - var pageLink = new EntityDataPageLink(20, 0, null, new EntityDataSortOrder(new EntityKey(EntityKeyType.TIME_SERIES, "name"), EntityDataSortOrder.Direction.DESC), false); - - var entityFields = Arrays.asList(new EntityKey(EntityKeyType.ENTITY_FIELD, "name"), new EntityKey(EntityKeyType.ENTITY_FIELD, "createdTime")); - KeyFilter nameFilter = new KeyFilter(); - nameFilter.setKey(new EntityKey(EntityKeyType.ENTITY_FIELD, "name")); - var predicate = new StringFilterPredicate(); - predicate.setIgnoreCase(false); - predicate.setOperation(StringFilterPredicate.StringOperation.CONTAINS); - predicate.setValue(new FilterPredicateValue<>("LoRa-")); - nameFilter.setPredicate(predicate); - nameFilter.setValueType(EntityKeyValueType.STRING); - - return new EntityDataQuery(filter, pageLink, entityFields, null, Arrays.asList(nameFilter)); - } - -} diff --git a/pom.xml b/pom.xml index 42747bae46..c809651aa2 100755 --- a/pom.xml +++ b/pom.xml @@ -74,6 +74,7 @@ 1.7.0 4.4.0 2.2.14 + 0.6.12 3.12.1 2.0.0-M15 2.10.1 From 60736e12377d2f6411e873c2e9f7f827f71b8c42 Mon Sep 17 00:00:00 2001 From: dashevchenko Date: Tue, 4 Feb 2025 12:23:35 +0200 Subject: [PATCH 048/345] fixed entity removal from CustomerData --- .../server/service/edqs/EdqsSyncService.java | 6 ++-- .../EdqsEntityQueryControllerTest.java | 5 --- .../controller/EntityQueryControllerTest.java | 18 +++++----- .../server/common/data/ObjectType.java | 2 +- .../processor/AbstractQueryProcessor.java | 2 +- .../AbstractRelationQueryProcessor.java | 5 ++- .../processor/EntityListQueryProcessor.java | 2 +- .../processor/SingleEntityQueryProcessor.java | 2 +- .../server/edqs/repo/TenantRepo.java | 35 ++++++++++--------- .../dao/queue/BaseQueueStatsService.java | 2 +- .../server/dao/queue/QueueStatsDao.java | 7 ++-- .../dao/sql/queue/JpaQueueStatsDao.java | 8 ++++- 12 files changed, 48 insertions(+), 46 deletions(-) diff --git a/application/src/main/java/org/thingsboard/server/service/edqs/EdqsSyncService.java b/application/src/main/java/org/thingsboard/server/service/edqs/EdqsSyncService.java index 04633892e5..c2750ae840 100644 --- a/application/src/main/java/org/thingsboard/server/service/edqs/EdqsSyncService.java +++ b/application/src/main/java/org/thingsboard/server/service/edqs/EdqsSyncService.java @@ -110,9 +110,9 @@ public abstract class EdqsSyncService { long ts = System.currentTimeMillis(); EntityType entityType = type.toEntityType(); Dao dao = entityDaoRegistry.getDao(entityType); - UUID lastFromEntityId = UUID.fromString("00000000-0000-0000-0000-000000000000"); + UUID lastId = UUID.fromString("00000000-0000-0000-0000-000000000000"); while (true) { - var batch = dao.findNextBatch(lastFromEntityId, 10000); + var batch = dao.findNextBatch(lastId, 10000); if (batch.isEmpty()) { break; } @@ -122,7 +122,7 @@ public abstract class EdqsSyncService { process(tenantId, type, new Entity(entityType, entityFields)); } EntityFields lastRecord = batch.get(batch.size() - 1); - lastFromEntityId = lastRecord.getId(); + lastId = lastRecord.getId(); } log.info("Finished synchronizing {} entities to EDQS in {} ms", type, (System.currentTimeMillis() - ts)); } diff --git a/application/src/test/java/org/thingsboard/server/controller/EdqsEntityQueryControllerTest.java b/application/src/test/java/org/thingsboard/server/controller/EdqsEntityQueryControllerTest.java index a75461faaa..baf9f98993 100644 --- a/application/src/test/java/org/thingsboard/server/controller/EdqsEntityQueryControllerTest.java +++ b/application/src/test/java/org/thingsboard/server/controller/EdqsEntityQueryControllerTest.java @@ -65,9 +65,4 @@ public class EdqsEntityQueryControllerTest extends EntityQueryControllerTest { result -> result == expectedResult); } - @Override - protected Long countByQueryAndCheck(EntityCountQuery query, long expectedResult, BiPredicate condition) { - return await().atMost(TIMEOUT, TimeUnit.SECONDS).until(() -> countByQuery(query), - result -> condition.test(result, expectedResult)); - } } 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 3941f16b88..508f66e584 100644 --- a/application/src/test/java/org/thingsboard/server/controller/EntityQueryControllerTest.java +++ b/application/src/test/java/org/thingsboard/server/controller/EntityQueryControllerTest.java @@ -81,7 +81,7 @@ import static org.awaitility.Awaitility.await; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; @DaoSqlTest -public abstract class EntityQueryControllerTest extends AbstractControllerTest { +public class EntityQueryControllerTest extends AbstractControllerTest { private static final String CUSTOMER_USER_EMAIL = "entityQueryCustomer@thingsboard.org"; private static final String TENANT_PASSWORD = "testPassword1"; @@ -158,6 +158,13 @@ public abstract class EntityQueryControllerTest extends AbstractControllerTest { @Test public void testSysAdminCountEntitiesByQuery() throws Exception { + loginSysAdmin(); + + EntityTypeFilter allDeviceFilter = new EntityTypeFilter(); + allDeviceFilter.setEntityType(EntityType.DEVICE); + EntityCountQuery query = new EntityCountQuery(allDeviceFilter); + countByQueryAndCheck(query, 0); + loginTenantAdmin(); List devices = new ArrayList<>(); @@ -177,7 +184,7 @@ public abstract class EntityQueryControllerTest extends AbstractControllerTest { loginSysAdmin(); EntityCountQuery countQuery = new EntityCountQuery(filter); - countByQueryAndCheck(countQuery, 97, (actual, expected) -> actual >= expected); + countByQueryAndCheck(countQuery, 97); filter.setDeviceTypes(List.of("unknown")); countByQueryAndCheck(countQuery, 0); @@ -193,7 +200,7 @@ public abstract class EntityQueryControllerTest extends AbstractControllerTest { countQuery = new EntityCountQuery(entityListFilter); countByQueryAndCheck(countQuery, 97); - countByQueryAndCheck(countQuery, 97, (actual, expected) -> actual >= expected); + countByQueryAndCheck(countQuery, 97); } @Test @@ -906,9 +913,4 @@ public abstract class EntityQueryControllerTest extends AbstractControllerTest { return numericFilter; } - protected Long countByQueryAndCheck(EntityCountQuery query, long expectedResult, BiPredicate condition) throws Exception { - Long result = countByQuery(query); - assertThat(condition.test(result, expectedResult)).isTrue(); - return result; - } } diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/ObjectType.java b/common/data/src/main/java/org/thingsboard/server/common/data/ObjectType.java index d3aa547bf0..bc5ec58213 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/ObjectType.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/ObjectType.java @@ -73,7 +73,7 @@ public enum ObjectType { API_USAGE_STATE, ATTRIBUTE_KV, LATEST_TS_KV); static { - edqsTypes.addAll(Arrays.asList(TENANT, RELATION, ATTRIBUTE_KV, LATEST_TS_KV)); + edqsTypes.addAll(Arrays.asList(RELATION, ATTRIBUTE_KV, LATEST_TS_KV)); } public EntityType toEntityType() { diff --git a/common/edqs/src/main/java/org/thingsboard/server/edqs/query/processor/AbstractQueryProcessor.java b/common/edqs/src/main/java/org/thingsboard/server/edqs/query/processor/AbstractQueryProcessor.java index 29626dad1f..b795af08db 100644 --- a/common/edqs/src/main/java/org/thingsboard/server/edqs/query/processor/AbstractQueryProcessor.java +++ b/common/edqs/src/main/java/org/thingsboard/server/edqs/query/processor/AbstractQueryProcessor.java @@ -62,7 +62,7 @@ public abstract class AbstractQueryProcessor implements } } - protected static boolean checkCustomer(UUID customerId, EntityData ed) { + protected static boolean checkCustomerId(UUID customerId, EntityData ed) { return customerId.equals(ed.getCustomerId()) || (ed.getEntityType() == EntityType.DASHBOARD && ed.getFields().getAssignedCustomerIds().contains(customerId)); } diff --git a/common/edqs/src/main/java/org/thingsboard/server/edqs/query/processor/AbstractRelationQueryProcessor.java b/common/edqs/src/main/java/org/thingsboard/server/edqs/query/processor/AbstractRelationQueryProcessor.java index b5eaf26e6e..2a4e72ff0b 100644 --- a/common/edqs/src/main/java/org/thingsboard/server/edqs/query/processor/AbstractRelationQueryProcessor.java +++ b/common/edqs/src/main/java/org/thingsboard/server/edqs/query/processor/AbstractRelationQueryProcessor.java @@ -16,7 +16,6 @@ package org.thingsboard.server.edqs.query.processor; import lombok.RequiredArgsConstructor; -import org.thingsboard.server.common.data.EntityType; import org.thingsboard.server.common.data.permission.QueryContext; import org.thingsboard.server.common.data.query.EntityFilter; import org.thingsboard.server.common.data.relation.EntitySearchDirection; @@ -79,7 +78,7 @@ public abstract class AbstractRelationQueryProcessor ext } else { var customerId = ctx.getCustomerId().getId(); for (EntityData ed : entities) { - if (checkCustomer(customerId, ed)) { + if (checkCustomerId(customerId, ed)) { result++; } } @@ -97,7 +96,7 @@ public abstract class AbstractRelationQueryProcessor ext var customerId = ctx.getCustomerId().getId(); List result = new ArrayList<>(); for (EntityData ed : entities) { - if (checkCustomer(customerId, ed)) { + if (checkCustomerId(customerId, ed)) { result.add(toSortData(ed)); } } diff --git a/common/edqs/src/main/java/org/thingsboard/server/edqs/query/processor/EntityListQueryProcessor.java b/common/edqs/src/main/java/org/thingsboard/server/edqs/query/processor/EntityListQueryProcessor.java index 36016009bd..b319a4e13a 100644 --- a/common/edqs/src/main/java/org/thingsboard/server/edqs/query/processor/EntityListQueryProcessor.java +++ b/common/edqs/src/main/java/org/thingsboard/server/edqs/query/processor/EntityListQueryProcessor.java @@ -41,7 +41,7 @@ public class EntityListQueryProcessor extends AbstractSingleEntityTypeQueryProce @Override protected void processCustomerQuery(UUID customerId, Consumer> processor) { processAll(ed -> { - if (checkCustomer(customerId, ed)) { + if (checkCustomerId(customerId, ed)) { processor.accept(ed); } }); diff --git a/common/edqs/src/main/java/org/thingsboard/server/edqs/query/processor/SingleEntityQueryProcessor.java b/common/edqs/src/main/java/org/thingsboard/server/edqs/query/processor/SingleEntityQueryProcessor.java index 19b32b5a93..55464b4529 100644 --- a/common/edqs/src/main/java/org/thingsboard/server/edqs/query/processor/SingleEntityQueryProcessor.java +++ b/common/edqs/src/main/java/org/thingsboard/server/edqs/query/processor/SingleEntityQueryProcessor.java @@ -39,7 +39,7 @@ public class SingleEntityQueryProcessor extends AbstractSingleEntityTypeQueryPro @Override protected void processCustomerQuery(UUID customerId, Consumer> processor) { processAll(ed -> { - if (checkCustomer(customerId, ed)) { + if (checkCustomerId(customerId, ed)) { processor.accept(ed); } }); diff --git a/common/edqs/src/main/java/org/thingsboard/server/edqs/repo/TenantRepo.java b/common/edqs/src/main/java/org/thingsboard/server/edqs/repo/TenantRepo.java index 7773c760f0..68e1c42589 100644 --- a/common/edqs/src/main/java/org/thingsboard/server/edqs/repo/TenantRepo.java +++ b/common/edqs/src/main/java/org/thingsboard/server/edqs/repo/TenantRepo.java @@ -195,24 +195,20 @@ public class TenantRepo { processFields(fields); entityData.setFields(entity.getFields()); - switch (entity.getType()) { - default -> { - UUID newCustomerId = fields.getCustomerId(); - UUID oldCustomerId = entityData.getCustomerId(); - entityData.setCustomerId(newCustomerId); - if (entityIdMismatch(oldCustomerId, newCustomerId)) { - if (oldCustomerId != null) { - CustomerData old = (CustomerData) getEntityMap(EntityType.CUSTOMER).get(oldCustomerId); - if (old != null) { - old.remove(entityData); - } - } - if (newCustomerId != null) { - CustomerData newData = (CustomerData) getEntityMap(EntityType.CUSTOMER).computeIfAbsent(newCustomerId, CustomerData::new); - newData.addOrUpdate(entityData); - } + UUID newCustomerId = fields.getCustomerId(); + UUID oldCustomerId = entityData.getCustomerId(); + entityData.setCustomerId(newCustomerId); + if (entityIdMismatch(oldCustomerId, newCustomerId)) { + if (oldCustomerId != null) { + CustomerData old = (CustomerData) getEntityMap(EntityType.CUSTOMER).get(oldCustomerId); + if (old != null) { + old.remove(entityData); } } + if (newCustomerId != null) { + CustomerData newData = (CustomerData) getEntityMap(EntityType.CUSTOMER).computeIfAbsent(newCustomerId, CustomerData::new); + newData.addOrUpdate(entityData); + } } } finally { entityUpdateLock.unlock(); @@ -228,6 +224,13 @@ public class TenantRepo { if (removed != null) { getEntitySet(entityType).remove(removed); edqsStatsService.ifPresent(statService -> statService.reportTenantEdqsObject(tenantId, ObjectType.fromEntityType(entityType), EdqsEventType.DELETED)); + UUID customerId = removed.getCustomerId(); + if (customerId != null) { + CustomerData customerData = (CustomerData) getEntityMap(EntityType.CUSTOMER).get(customerId); + if (customerData != null) { + customerData.remove(removed); + } + } } } finally { entityUpdateLock.unlock(); diff --git a/dao/src/main/java/org/thingsboard/server/dao/queue/BaseQueueStatsService.java b/dao/src/main/java/org/thingsboard/server/dao/queue/BaseQueueStatsService.java index efc9dab41b..e3e6edad94 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/queue/BaseQueueStatsService.java +++ b/dao/src/main/java/org/thingsboard/server/dao/queue/BaseQueueStatsService.java @@ -85,7 +85,7 @@ public class BaseQueueStatsService extends AbstractEntityService implements Queu public PageData findByTenantId(TenantId tenantId, PageLink pageLink) { log.trace("Executing findByTenantId, tenantId: [{}]", tenantId); Validator.validatePageLink(pageLink); - return queueStatsDao.findByTenantId(tenantId, pageLink); + return queueStatsDao.findAllByTenantId(tenantId, pageLink); } @Override diff --git a/dao/src/main/java/org/thingsboard/server/dao/queue/QueueStatsDao.java b/dao/src/main/java/org/thingsboard/server/dao/queue/QueueStatsDao.java index 5466a8afcc..018eb04d67 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/queue/QueueStatsDao.java +++ b/dao/src/main/java/org/thingsboard/server/dao/queue/QueueStatsDao.java @@ -17,19 +17,16 @@ package org.thingsboard.server.dao.queue; import org.thingsboard.server.common.data.id.QueueStatsId; import org.thingsboard.server.common.data.id.TenantId; -import org.thingsboard.server.common.data.page.PageData; -import org.thingsboard.server.common.data.page.PageLink; import org.thingsboard.server.common.data.queue.QueueStats; import org.thingsboard.server.dao.Dao; +import org.thingsboard.server.dao.TenantEntityDao; import java.util.List; -public interface QueueStatsDao extends Dao { +public interface QueueStatsDao extends Dao, TenantEntityDao { QueueStats findByTenantIdQueueNameAndServiceId(TenantId tenantId, String queueName, String serviceId); - PageData findByTenantId(TenantId tenantId, PageLink pageLink); - void deleteByTenantId(TenantId tenantId); List findByIds(TenantId tenantId, List queueStatsIds); diff --git a/dao/src/main/java/org/thingsboard/server/dao/sql/queue/JpaQueueStatsDao.java b/dao/src/main/java/org/thingsboard/server/dao/sql/queue/JpaQueueStatsDao.java index 97fedd693a..41e2de02a6 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/sql/queue/JpaQueueStatsDao.java +++ b/dao/src/main/java/org/thingsboard/server/dao/sql/queue/JpaQueueStatsDao.java @@ -21,6 +21,7 @@ import org.springframework.data.domain.Limit; import org.springframework.data.jpa.repository.JpaRepository; import org.springframework.stereotype.Component; import org.thingsboard.server.common.data.EntityType; +import org.thingsboard.server.common.data.ObjectType; import org.thingsboard.server.common.data.edqs.fields.QueueStatsFields; import org.thingsboard.server.common.data.id.QueueStatsId; import org.thingsboard.server.common.data.id.TenantId; @@ -62,10 +63,15 @@ public class JpaQueueStatsDao extends JpaAbstractDao findByTenantId(TenantId tenantId, PageLink pageLink) { + public PageData findAllByTenantId(TenantId tenantId, PageLink pageLink) { return DaoUtil.toPageData(queueStatsRepository.findByTenantId(tenantId.getId(), pageLink.getTextSearch(), DaoUtil.toPageable(pageLink))); } + @Override + public ObjectType getType() { + return ObjectType.QUEUE_STATS; + } + @Override public void deleteByTenantId(TenantId tenantId) { queueStatsRepository.deleteByTenantId(tenantId.getId()); From e3a72fec09ca0f82584123b2ee2050ec41824db2 Mon Sep 17 00:00:00 2001 From: Andrii Landiak Date: Tue, 4 Feb 2025 12:39:06 +0200 Subject: [PATCH 049/345] Refactor TopicService. Change ymls to be able to configure notification topic names --- .../src/main/resources/thingsboard.yml | 7 +++- .../server/queue/discovery/TopicService.java | 41 +++++++++++++------ .../src/main/resources/tb-vc-executor.yml | 2 + .../src/main/resources/tb-coap-transport.yml | 4 ++ .../src/main/resources/tb-http-transport.yml | 4 ++ .../src/main/resources/tb-lwm2m-transport.yml | 4 ++ .../src/main/resources/tb-mqtt-transport.yml | 4 ++ .../src/main/resources/tb-snmp-transport.yml | 4 ++ 8 files changed, 57 insertions(+), 13 deletions(-) diff --git a/application/src/main/resources/thingsboard.yml b/application/src/main/resources/thingsboard.yml index f29cc55197..4ee08c7095 100644 --- a/application/src/main/resources/thingsboard.yml +++ b/application/src/main/resources/thingsboard.yml @@ -1644,6 +1644,8 @@ queue: core: # Default topic name topic: "${TB_QUEUE_CORE_TOPIC:tb_core}" + # For high-priority notifications that require minimum latency and processing time + notifications_topic: "${TB_QUEUE_CORE_NOTIFICATIONS_TOPIC:tb_core.notifications}" # Interval in milliseconds to poll messages by Core microservices poll-interval: "${TB_QUEUE_CORE_POLL_INTERVAL_MS:25}" # Amount of partitions used by Core microservices @@ -1688,7 +1690,6 @@ queue: enabled: "${TB_HOUSEKEEPER_STATS_ENABLED:true}" # Statistics printing interval for Housekeeper print-interval-ms: "${TB_HOUSEKEEPER_STATS_PRINT_INTERVAL_MS:60000}" - vc: # Default topic name topic: "${TB_QUEUE_VC_TOPIC:tb_version_control}" @@ -1720,6 +1721,8 @@ queue: rule-engine: # Deprecated. It will be removed in the nearest releases topic: "${TB_QUEUE_RULE_ENGINE_TOPIC:tb_rule_engine}" + # For high-priority notifications that require minimum latency and processing time + notifications_topic: "${TB_QUEUE_RULE_ENGINE_NOTIFICATIONS_TOPIC:tb_rule_engine.notifications}" # Interval in milliseconds to poll messages by Rule Engine poll-interval: "${TB_QUEUE_RULE_ENGINE_POLL_INTERVAL_MS:25}" # Timeout for processing a message pack of Rule Engine @@ -1743,6 +1746,8 @@ queue: edge: # Default topic name topic: "${TB_QUEUE_EDGE_TOPIC:tb_edge}" + # For high-priority notifications that require minimum latency and processing time + notifications_topic: "${TB_QUEUE_EDGE_NOTIFICATIONS_TOPIC:tb_edge.notifications}" # Amount of partitions used by Edge services partitions: "${TB_QUEUE_EDGE_PARTITIONS:10}" # Poll interval for topics related to Edge services 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 927c311a2d..254d0a95d6 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 @@ -32,6 +32,18 @@ public class TopicService { @Value("${queue.prefix:}") private String prefix; + @Value("${queue.core.notifications-topic:tb_core.notifications}") + private String tbCoreNotificationsTopic; + + @Value("${queue.rule-engine.notifications-topic:tb_rule_engine.notifications}") + private String tbRuleEngineNotificationsTopic; + + @Value("${queue.transport.notifications-topics:tb_transport.notifications}") + private String tbTransportNotificationsTopic; + + @Value("${queue.edge.notifications-topic:tb_edge.notifications}") + private String tbEdgeNotificationsTopic; + private final ConcurrentMap tbCoreNotificationTopics = new ConcurrentHashMap<>(); private final ConcurrentMap tbRuleEngineNotificationTopics = new ConcurrentHashMap<>(); private final ConcurrentMap tbEdgeNotificationTopics = new ConcurrentHashMap<>(); @@ -47,19 +59,28 @@ public class TopicService { public TopicPartitionInfo getNotificationsTopic(ServiceType serviceType, String serviceId) { return switch (serviceType) { case TB_CORE -> tbCoreNotificationTopics.computeIfAbsent(serviceId, - id -> buildNotificationsTopicPartitionInfo(serviceType, serviceId)); + id -> buildNotificationsTopicPartitionInfo(tbCoreNotificationsTopic, serviceId)); case TB_RULE_ENGINE -> tbRuleEngineNotificationTopics.computeIfAbsent(serviceId, - id -> buildNotificationsTopicPartitionInfo(serviceType, serviceId)); - default -> buildNotificationsTopicPartitionInfo(serviceType, serviceId); + id -> buildNotificationsTopicPartitionInfo(tbRuleEngineNotificationsTopic, serviceId)); + case TB_TRANSPORT -> buildNotificationsTopicPartitionInfo(tbTransportNotificationsTopic, serviceId); + default -> throw new IllegalStateException("Unexpected service type: " + serviceType); }; } + private TopicPartitionInfo buildNotificationsTopicPartitionInfo(String topic, String serviceId) { + return buildTopicPartitionInfo(buildNotificationTopicName(topic, serviceId), null, null, false); + } + + public TopicPartitionInfo buildTopicPartitionInfo(String topic, TenantId tenantId, Integer partition, boolean myPartition) { + return new TopicPartitionInfo(buildTopicName(topic), tenantId, partition, myPartition); + } + public TopicPartitionInfo getEdgeNotificationsTopic(String serviceId) { return tbEdgeNotificationTopics.computeIfAbsent(serviceId, id -> buildEdgeNotificationsTopicPartitionInfo(serviceId)); } private TopicPartitionInfo buildEdgeNotificationsTopicPartitionInfo(String serviceId) { - return buildTopicPartitionInfo("tb_edge.notifications." + serviceId, null, null, false); + return buildTopicPartitionInfo(buildNotificationTopicName(tbEdgeNotificationsTopic, serviceId), null, null, false); } public TopicPartitionInfo getEdgeEventNotificationsTopic(TenantId tenantId, EdgeId edgeId) { @@ -70,18 +91,14 @@ public class TopicService { return buildTopicPartitionInfo("tb_edge_event.notifications." + tenantId + "." + edgeId, null, null, false); } - private TopicPartitionInfo buildNotificationsTopicPartitionInfo(ServiceType serviceType, String serviceId) { - return buildTopicPartitionInfo(serviceType.name().toLowerCase() + ".notifications." + serviceId, null, null, false); - } - - public TopicPartitionInfo buildTopicPartitionInfo(String topic, TenantId tenantId, Integer partition, boolean myPartition) { - return new TopicPartitionInfo(buildTopicName(topic), tenantId, partition, myPartition); - } - public String buildTopicName(String topic) { return prefix.isBlank() ? topic : prefix + "." + topic; } + private String buildNotificationTopicName(String topic, String serviceId) { + return topic + "." + serviceId; + } + public String buildConsumerGroupId(String servicePrefix, TenantId tenantId, String queueName, Integer partitionId) { return this.buildTopicName( servicePrefix + queueName diff --git a/msa/vc-executor/src/main/resources/tb-vc-executor.yml b/msa/vc-executor/src/main/resources/tb-vc-executor.yml index cba83e5ff7..595e514907 100644 --- a/msa/vc-executor/src/main/resources/tb-vc-executor.yml +++ b/msa/vc-executor/src/main/resources/tb-vc-executor.yml @@ -151,6 +151,8 @@ queue: core: # Default topic name topic: "${TB_QUEUE_CORE_TOPIC:tb_core}" + # For high-priority notifications that require minimum latency and processing time + notifications_topic: "${TB_QUEUE_CORE_NOTIFICATIONS_TOPIC:tb_core.notifications}" # Interval in milliseconds to poll messages by Core microservices poll-interval: "${TB_QUEUE_CORE_POLL_INTERVAL_MS:25}" # Amount of partitions used by Core microservices diff --git a/transport/coap/src/main/resources/tb-coap-transport.yml b/transport/coap/src/main/resources/tb-coap-transport.yml index 66ab703a54..794eaed047 100644 --- a/transport/coap/src/main/resources/tb-coap-transport.yml +++ b/transport/coap/src/main/resources/tb-coap-transport.yml @@ -351,6 +351,8 @@ queue: core: # Default topic name topic: "${TB_QUEUE_CORE_TOPIC:tb_core}" + # For high-priority notifications that require minimum latency and processing time + notifications_topic: "${TB_QUEUE_CORE_NOTIFICATIONS_TOPIC:tb_core.notifications}" # Interval in milliseconds to poll messages by Core microservices poll-interval: "${TB_QUEUE_CORE_POLL_INTERVAL_MS:25}" # Amount of partitions used by Core microservices @@ -381,6 +383,8 @@ queue: rule-engine: # Deprecated. It will be removed in the nearest releases topic: "${TB_QUEUE_RULE_ENGINE_TOPIC:tb_rule_engine}" + # For high-priority notifications that require minimum latency and processing time + notifications_topic: "${TB_QUEUE_RULE_ENGINE_NOTIFICATIONS_TOPIC:tb_rule_engine.notifications}" # Interval in milliseconds to poll messages by Rule Engine poll-interval: "${TB_QUEUE_RULE_ENGINE_POLL_INTERVAL_MS:25}" # Timeout for processing a message pack of Rule Engine diff --git a/transport/http/src/main/resources/tb-http-transport.yml b/transport/http/src/main/resources/tb-http-transport.yml index be91e4fe67..860059b1ee 100644 --- a/transport/http/src/main/resources/tb-http-transport.yml +++ b/transport/http/src/main/resources/tb-http-transport.yml @@ -296,6 +296,8 @@ queue: core: # Default topic name topic: "${TB_QUEUE_CORE_TOPIC:tb_core}" + # For high-priority notifications that require minimum latency and processing time + notifications_topic: "${TB_QUEUE_CORE_NOTIFICATIONS_TOPIC:tb_core.notifications}" # Interval in milliseconds to poll messages by Core microservices poll-interval: "${TB_QUEUE_CORE_POLL_INTERVAL_MS:25}" # Amount of partitions used by Core microservices @@ -326,6 +328,8 @@ queue: rule-engine: # Deprecated. It will be removed in the nearest releases topic: "${TB_QUEUE_RULE_ENGINE_TOPIC:tb_rule_engine}" + # For high-priority notifications that require minimum latency and processing time + notifications_topic: "${TB_QUEUE_RULE_ENGINE_NOTIFICATIONS_TOPIC:tb_rule_engine.notifications}" # Interval in milliseconds to poll messages by Rule Engine poll-interval: "${TB_QUEUE_RULE_ENGINE_POLL_INTERVAL_MS:25}" # Timeout for processing a message pack of Rule Engine diff --git a/transport/lwm2m/src/main/resources/tb-lwm2m-transport.yml b/transport/lwm2m/src/main/resources/tb-lwm2m-transport.yml index 7bdd68baf1..824d998836 100644 --- a/transport/lwm2m/src/main/resources/tb-lwm2m-transport.yml +++ b/transport/lwm2m/src/main/resources/tb-lwm2m-transport.yml @@ -397,6 +397,8 @@ queue: core: # Default topic name topic: "${TB_QUEUE_CORE_TOPIC:tb_core}" + # For high-priority notifications that require minimum latency and processing time + notifications_topic: "${TB_QUEUE_CORE_NOTIFICATIONS_TOPIC:tb_core.notifications}" # Interval in milliseconds to poll messages by Core microservices poll-interval: "${TB_QUEUE_CORE_POLL_INTERVAL_MS:25}" # Amount of partitions used by Core microservices @@ -427,6 +429,8 @@ queue: rule-engine: # Deprecated. It will be removed in the nearest releases topic: "${TB_QUEUE_RULE_ENGINE_TOPIC:tb_rule_engine}" + # For high-priority notifications that require minimum latency and processing time + notifications_topic: "${TB_QUEUE_RULE_ENGINE_NOTIFICATIONS_TOPIC:tb_rule_engine.notifications}" # Interval in milliseconds to poll messages by Rule Engine poll-interval: "${TB_QUEUE_RULE_ENGINE_POLL_INTERVAL_MS:25}" # Timeout for processing a message pack of Rule Engine diff --git a/transport/mqtt/src/main/resources/tb-mqtt-transport.yml b/transport/mqtt/src/main/resources/tb-mqtt-transport.yml index 8d9a60a319..4b0adab7d1 100644 --- a/transport/mqtt/src/main/resources/tb-mqtt-transport.yml +++ b/transport/mqtt/src/main/resources/tb-mqtt-transport.yml @@ -330,6 +330,8 @@ queue: core: # Default topic name topic: "${TB_QUEUE_CORE_TOPIC:tb_core}" + # For high-priority notifications that require minimum latency and processing time + notifications_topic: "${TB_QUEUE_CORE_NOTIFICATIONS_TOPIC:tb_core.notifications}" # Interval in milliseconds to poll messages by Core microservices poll-interval: "${TB_QUEUE_CORE_POLL_INTERVAL_MS:25}" # Amount of partitions used by Core microservices @@ -360,6 +362,8 @@ queue: rule-engine: # Deprecated. It will be removed in the nearest releases topic: "${TB_QUEUE_RULE_ENGINE_TOPIC:tb_rule_engine}" + # For high-priority notifications that require minimum latency and processing time + notifications_topic: "${TB_QUEUE_RULE_ENGINE_NOTIFICATIONS_TOPIC:tb_rule_engine.notifications}" # Interval in milliseconds to poll messages by Rule Engine poll-interval: "${TB_QUEUE_RULE_ENGINE_POLL_INTERVAL_MS:25}" # Timeout for processing a message pack of Rule Engine diff --git a/transport/snmp/src/main/resources/tb-snmp-transport.yml b/transport/snmp/src/main/resources/tb-snmp-transport.yml index 4c11bd0018..e6fe253e79 100644 --- a/transport/snmp/src/main/resources/tb-snmp-transport.yml +++ b/transport/snmp/src/main/resources/tb-snmp-transport.yml @@ -283,6 +283,8 @@ queue: core: # Default topic name topic: "${TB_QUEUE_CORE_TOPIC:tb_core}" + # For high-priority notifications that require minimum latency and processing time + notifications_topic: "${TB_QUEUE_CORE_NOTIFICATIONS_TOPIC:tb_core.notifications}" # Interval in milliseconds to poll messages by Core microservices poll-interval: "${TB_QUEUE_CORE_POLL_INTERVAL_MS:25}" # Amount of partitions used by Core microservices @@ -313,6 +315,8 @@ queue: rule-engine: # Deprecated. It will be removed in the nearest releases topic: "${TB_QUEUE_RULE_ENGINE_TOPIC:tb_rule_engine}" + # For high-priority notifications that require minimum latency and processing time + notifications_topic: "${TB_QUEUE_RULE_ENGINE_NOTIFICATIONS_TOPIC:tb_rule_engine.notifications}" # Interval in milliseconds to poll messages by Rule Engine poll-interval: "${TB_QUEUE_RULE_ENGINE_POLL_INTERVAL_MS:25}" # Timeout for processing a message pack of Rule Engine From b2bf5b9f6535bc55c233d4ba7ba13edb156bc08b Mon Sep 17 00:00:00 2001 From: dashevchenko Date: Wed, 5 Feb 2025 12:20:23 +0200 Subject: [PATCH 050/345] configurable edqs sync batch sizes --- .../server/service/edqs/DefaultEdqsService.java | 2 +- .../server/service/edqs/EdqsListener.java | 2 +- .../server/service/edqs/EdqsSyncService.java | 16 +++++++++------- .../service/edqs/KafkaEdqsSyncService.java | 2 +- .../service/edqs/LocalEdqsSyncService.java | 2 +- application/src/main/resources/thingsboard.yml | 5 ++++- .../EdqsEntityQueryControllerTest.java | 2 +- .../service/entitiy/EdqsEntityServiceTest.java | 2 +- .../test/resources/application-test.properties | 2 +- .../server/queue/edqs/EdqsComponent.java | 2 +- .../server/queue/edqs/InMemoryEdqsComponent.java | 2 +- .../server/queue/edqs/KafkaEdqsComponent.java | 2 +- .../server/dao/sql/query/DummyEdqsService.java | 2 +- 13 files changed, 24 insertions(+), 19 deletions(-) diff --git a/application/src/main/java/org/thingsboard/server/service/edqs/DefaultEdqsService.java b/application/src/main/java/org/thingsboard/server/service/edqs/DefaultEdqsService.java index 160d4bb565..e7243ed3bc 100644 --- a/application/src/main/java/org/thingsboard/server/service/edqs/DefaultEdqsService.java +++ b/application/src/main/java/org/thingsboard/server/service/edqs/DefaultEdqsService.java @@ -81,7 +81,7 @@ import java.util.concurrent.TimeUnit; @Service @RequiredArgsConstructor @Slf4j -@ConditionalOnProperty(value = "queue.edqs.sync_enabled", havingValue = "true") +@ConditionalOnProperty(value = "queue.edqs.sync.enabled", havingValue = "true") public class DefaultEdqsService implements EdqsService { private final EdqsClientQueueFactory queueFactory; diff --git a/application/src/main/java/org/thingsboard/server/service/edqs/EdqsListener.java b/application/src/main/java/org/thingsboard/server/service/edqs/EdqsListener.java index c4ce0c7a7e..dc8d4bf36d 100644 --- a/application/src/main/java/org/thingsboard/server/service/edqs/EdqsListener.java +++ b/application/src/main/java/org/thingsboard/server/service/edqs/EdqsListener.java @@ -28,7 +28,7 @@ import org.thingsboard.server.dao.eventsourcing.SaveEntityEvent; @Service @RequiredArgsConstructor -@ConditionalOnProperty(value = "queue.edqs.sync_enabled", havingValue = "true") +@ConditionalOnProperty(value = "queue.edqs.sync.enabled", havingValue = "true") public class EdqsListener { private final EdqsService edqsService; diff --git a/application/src/main/java/org/thingsboard/server/service/edqs/EdqsSyncService.java b/application/src/main/java/org/thingsboard/server/service/edqs/EdqsSyncService.java index c2750ae840..80aeee8635 100644 --- a/application/src/main/java/org/thingsboard/server/service/edqs/EdqsSyncService.java +++ b/application/src/main/java/org/thingsboard/server/service/edqs/EdqsSyncService.java @@ -17,6 +17,7 @@ package org.thingsboard.server.service.edqs; import lombok.extern.slf4j.Slf4j; import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.annotation.Value; import org.springframework.context.annotation.Lazy; import org.thingsboard.server.common.data.AttributeScope; import org.thingsboard.server.common.data.EntityType; @@ -42,7 +43,6 @@ import org.thingsboard.server.dao.model.sqlts.dictionary.KeyDictionaryEntry; import org.thingsboard.server.dao.model.sqlts.latest.TsKvLatestEntity; import org.thingsboard.server.dao.sql.relation.RelationRepository; import org.thingsboard.server.dao.sqlts.latest.TsKvLatestRepository; -import org.thingsboard.server.dao.tenant.TenantDao; import java.util.List; import java.util.Map; @@ -58,11 +58,13 @@ import static org.thingsboard.server.common.data.ObjectType.edqsTenantTypes; @Slf4j public abstract class EdqsSyncService { + @Value("${queue.edqs.sync.entity_batch_size:10000}") + private int entityBatchSize; + @Value("${queue.edqs.sync.ts_batch_size:10000}") + private int tsBatchSize; @Autowired private EntityDaoRegistry entityDaoRegistry; @Autowired - private TenantDao tenantDao; - @Autowired private AttributesDao attributesDao; @Autowired private KeyDictionaryDao keyDictionaryDao; @@ -112,7 +114,7 @@ public abstract class EdqsSyncService { Dao dao = entityDaoRegistry.getDao(entityType); UUID lastId = UUID.fromString("00000000-0000-0000-0000-000000000000"); while (true) { - var batch = dao.findNextBatch(lastId, 10000); + var batch = dao.findNextBatch(lastId, entityBatchSize); if (batch.isEmpty()) { break; } @@ -140,7 +142,7 @@ public abstract class EdqsSyncService { while (true) { List batch = relationRepository.findNextBatch(lastFromEntityId, lastFromEntityType, lastRelationTypeGroup, - lastRelationType, lastToEntityId, lastToEntityType, 10000); + lastRelationType, lastToEntityId, lastToEntityType, entityBatchSize); if (batch.isEmpty()) { break; } @@ -189,7 +191,7 @@ public abstract class EdqsSyncService { int lastAttributeKey = Integer.MIN_VALUE; while (true) { - List batch = attributesDao.findNextBatch(lastEntityId, lastAttributeType, lastAttributeKey, 10000); + List batch = attributesDao.findNextBatch(lastEntityId, lastAttributeType, lastAttributeKey, tsBatchSize); if (batch.isEmpty()) { break; } @@ -228,7 +230,7 @@ public abstract class EdqsSyncService { int lastKey = Integer.MIN_VALUE; while (true) { - List batch = tsKvLatestRepository.findNextBatch(lastEntityId, lastKey, 10000); + List batch = tsKvLatestRepository.findNextBatch(lastEntityId, lastKey, tsBatchSize); if (batch.isEmpty()) { break; } diff --git a/application/src/main/java/org/thingsboard/server/service/edqs/KafkaEdqsSyncService.java b/application/src/main/java/org/thingsboard/server/service/edqs/KafkaEdqsSyncService.java index f4e9a02b45..0c6f948770 100644 --- a/application/src/main/java/org/thingsboard/server/service/edqs/KafkaEdqsSyncService.java +++ b/application/src/main/java/org/thingsboard/server/service/edqs/KafkaEdqsSyncService.java @@ -27,7 +27,7 @@ import java.util.Collections; @Service @RequiredArgsConstructor -@ConditionalOnExpression("'${queue.edqs.sync_enabled:true}' == 'true' && '${queue.type:null}' == 'kafka'") +@ConditionalOnExpression("'${queue.edqs.sync.enabled:true}' == 'true' && '${queue.type:null}' == 'kafka'") public class KafkaEdqsSyncService extends EdqsSyncService { private final TbKafkaSettings kafkaSettings; diff --git a/application/src/main/java/org/thingsboard/server/service/edqs/LocalEdqsSyncService.java b/application/src/main/java/org/thingsboard/server/service/edqs/LocalEdqsSyncService.java index 924bf94830..11d5894307 100644 --- a/application/src/main/java/org/thingsboard/server/service/edqs/LocalEdqsSyncService.java +++ b/application/src/main/java/org/thingsboard/server/service/edqs/LocalEdqsSyncService.java @@ -22,7 +22,7 @@ import org.thingsboard.server.edqs.util.EdqsRocksDb; @Service @RequiredArgsConstructor -@ConditionalOnExpression("'${queue.edqs.sync_enabled:true}' == 'true' && '${queue.type:null}' == 'in-memory'") +@ConditionalOnExpression("'${queue.edqs.sync.enabled:true}' == 'true' && '${queue.type:null}' == 'in-memory'") public class LocalEdqsSyncService extends EdqsSyncService { private final EdqsRocksDb db; diff --git a/application/src/main/resources/thingsboard.yml b/application/src/main/resources/thingsboard.yml index dd0e5948c7..e41d0c2aed 100644 --- a/application/src/main/resources/thingsboard.yml +++ b/application/src/main/resources/thingsboard.yml @@ -1695,7 +1695,10 @@ queue: # Statistics printing interval for Housekeeper print-interval-ms: "${TB_HOUSEKEEPER_STATS_PRINT_INTERVAL_MS:60000}" edqs: - sync_enabled: "${TB_EDQS_SYNC_ENABLED:true}" # FIXME: disable by default before release + sync: + enabled: "${TB_EDQS_SYNC_ENABLED:true}" # Enable/disable EDQS synchronization with postgres db FIXME: disable by default before release + entity_batch_size: "${TB_EDQS_SYNC_ENTITY_BATCH_SIZE:10000}" # batch size of entities being synced with EDQS + ts_batch_size: "${TB_EDQS_SYNC_TS_BATCH_SIZE:10000}" # batch size of timeseries data being synced with EDQS api_enabled: "${TB_EDQS_API_ENABLED:true}" # FIXME: disable by default before release mode: "${TB_EDQS_MODE:local}" # local or remote local: diff --git a/application/src/test/java/org/thingsboard/server/controller/EdqsEntityQueryControllerTest.java b/application/src/test/java/org/thingsboard/server/controller/EdqsEntityQueryControllerTest.java index baf9f98993..9bb0bbe30a 100644 --- a/application/src/test/java/org/thingsboard/server/controller/EdqsEntityQueryControllerTest.java +++ b/application/src/test/java/org/thingsboard/server/controller/EdqsEntityQueryControllerTest.java @@ -36,7 +36,7 @@ import static org.awaitility.Awaitility.await; @TestPropertySource(properties = { // "queue.type=kafka", // uncomment to use Kafka // "queue.kafka.bootstrap.servers=10.7.1.254:9092", - "queue.edqs.sync_enabled=true", + "queue.edqs.sync.enabled=true", "queue.edqs.api_enabled=true", "queue.edqs.mode=local" }) diff --git a/application/src/test/java/org/thingsboard/server/service/entitiy/EdqsEntityServiceTest.java b/application/src/test/java/org/thingsboard/server/service/entitiy/EdqsEntityServiceTest.java index cf24deb7e4..cb377e0431 100644 --- a/application/src/test/java/org/thingsboard/server/service/entitiy/EdqsEntityServiceTest.java +++ b/application/src/test/java/org/thingsboard/server/service/entitiy/EdqsEntityServiceTest.java @@ -34,7 +34,7 @@ import static org.awaitility.Awaitility.await; @DaoSqlTest @TestPropertySource(properties = { - "queue.edqs.sync_enabled=true", + "queue.edqs.sync.enabled=true", "queue.edqs.api_enabled=true", "queue.edqs.mode=local" }) diff --git a/application/src/test/resources/application-test.properties b/application/src/test/resources/application-test.properties index ba6863c705..8933c36db5 100644 --- a/application/src/test/resources/application-test.properties +++ b/application/src/test/resources/application-test.properties @@ -57,5 +57,5 @@ server.log_controller_error_stack_trace=true transport.gateway.dashboard.sync.enabled=false -queue.edqs.sync_enabled=false +queue.edqs.sync.enabled=false queue.edqs.api_enabled=false diff --git a/common/queue/src/main/java/org/thingsboard/server/queue/edqs/EdqsComponent.java b/common/queue/src/main/java/org/thingsboard/server/queue/edqs/EdqsComponent.java index 838f9b4aa2..27a1e09f79 100644 --- a/common/queue/src/main/java/org/thingsboard/server/queue/edqs/EdqsComponent.java +++ b/common/queue/src/main/java/org/thingsboard/server/queue/edqs/EdqsComponent.java @@ -22,7 +22,7 @@ import java.lang.annotation.RetentionPolicy; @Retention(RetentionPolicy.RUNTIME) // TODO: tb-core ? -@ConditionalOnExpression("'${queue.edqs.sync_enabled:true}'=='true' && ('${service.type:null}'=='edqs' || " + +@ConditionalOnExpression("'${queue.edqs.sync.enabled:true}'=='true' && ('${service.type:null}'=='edqs' || " + "(('${service.type:null}'=='monolith' || '${service.type:null}'=='tb-core') && " + "'${queue.edqs.mode:null}'=='local'))") public @interface EdqsComponent { diff --git a/common/queue/src/main/java/org/thingsboard/server/queue/edqs/InMemoryEdqsComponent.java b/common/queue/src/main/java/org/thingsboard/server/queue/edqs/InMemoryEdqsComponent.java index 5055787fde..3e04dda3b5 100644 --- a/common/queue/src/main/java/org/thingsboard/server/queue/edqs/InMemoryEdqsComponent.java +++ b/common/queue/src/main/java/org/thingsboard/server/queue/edqs/InMemoryEdqsComponent.java @@ -21,6 +21,6 @@ import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; @Retention(RetentionPolicy.RUNTIME) -@ConditionalOnExpression("'${queue.edqs.sync_enabled:true}'=='true' && '${service.type:null}'=='monolith' && '${queue.edqs.mode:null}'=='local' && '${queue.type:null}'=='in-memory'") +@ConditionalOnExpression("'${queue.edqs.sync.enabled:true}'=='true' && '${service.type:null}'=='monolith' && '${queue.edqs.mode:null}'=='local' && '${queue.type:null}'=='in-memory'") public @interface InMemoryEdqsComponent { } diff --git a/common/queue/src/main/java/org/thingsboard/server/queue/edqs/KafkaEdqsComponent.java b/common/queue/src/main/java/org/thingsboard/server/queue/edqs/KafkaEdqsComponent.java index 9f112e7f59..9963972e9c 100644 --- a/common/queue/src/main/java/org/thingsboard/server/queue/edqs/KafkaEdqsComponent.java +++ b/common/queue/src/main/java/org/thingsboard/server/queue/edqs/KafkaEdqsComponent.java @@ -21,7 +21,7 @@ import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; @Retention(RetentionPolicy.RUNTIME) -@ConditionalOnExpression("'${queue.edqs.sync_enabled:true}'=='true' && ('${service.type:null}'=='edqs' || " + +@ConditionalOnExpression("'${queue.edqs.sync.enabled:true}'=='true' && ('${service.type:null}'=='edqs' || " + "(('${service.type:null}'=='monolith' || '${service.type:null}'=='tb-core') && " + "'${queue.edqs.mode:null}'=='local' && '${queue.type:null}'=='kafka'))") public @interface KafkaEdqsComponent { diff --git a/dao/src/main/java/org/thingsboard/server/dao/sql/query/DummyEdqsService.java b/dao/src/main/java/org/thingsboard/server/dao/sql/query/DummyEdqsService.java index 91934293a6..b69aa52ec5 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/sql/query/DummyEdqsService.java +++ b/dao/src/main/java/org/thingsboard/server/dao/sql/query/DummyEdqsService.java @@ -30,7 +30,7 @@ import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.common.msg.edqs.EdqsService; @Service -@ConditionalOnProperty(value = "queue.edqs.sync_enabled", havingValue = "false", matchIfMissing = true) +@ConditionalOnProperty(value = "queue.edqs.sync.enabled", havingValue = "false", matchIfMissing = true) public class DummyEdqsService implements EdqsService { @Override From 3d9897f5af21ae26f197009cd6e985e5316f10ee Mon Sep 17 00:00:00 2001 From: dashevchenko Date: Wed, 5 Feb 2025 13:30:42 +0200 Subject: [PATCH 051/345] fixed NullPointer exception in CachedAttributesService --- .../server/dao/attributes/CachedAttributesService.java | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/dao/src/main/java/org/thingsboard/server/dao/attributes/CachedAttributesService.java b/dao/src/main/java/org/thingsboard/server/dao/attributes/CachedAttributesService.java index c5ac50c9f1..3c12ae3209 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/attributes/CachedAttributesService.java +++ b/dao/src/main/java/org/thingsboard/server/dao/attributes/CachedAttributesService.java @@ -266,7 +266,9 @@ public class CachedAttributesService implements AttributesService { String key = keyVersionPair.getFirst(); Long version = keyVersionPair.getSecond(); cache.evict(new AttributeCacheKey(scope, entityId, key), version); - edqsService.onDelete(tenantId, ObjectType.ATTRIBUTE_KV, new AttributeKv(entityId, scope, key, version)); + if (version != null) { + edqsService.onDelete(tenantId, ObjectType.ATTRIBUTE_KV, new AttributeKv(entityId, scope, key, version)); + } return key; }, cacheExecutor)).collect(Collectors.toList())); } From 09bd3de984d15c7596bdc7d5152b3c5bc91ec342 Mon Sep 17 00:00:00 2001 From: dashevchenko Date: Wed, 5 Feb 2025 16:42:31 +0200 Subject: [PATCH 052/345] test fixes --- .../controller/EntityQueryControllerTest.java | 16 ++-------------- .../discovery/HashPartitionServiceTest.java | 1 + .../state/DefaultDeviceStateServiceTest.java | 18 +++++++++--------- .../update/330/device_profile_001_out.json | 6 ++++-- 4 files changed, 16 insertions(+), 25 deletions(-) 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 508f66e584..d9652946ab 100644 --- a/application/src/test/java/org/thingsboard/server/controller/EntityQueryControllerTest.java +++ b/application/src/test/java/org/thingsboard/server/controller/EntityQueryControllerTest.java @@ -817,26 +817,14 @@ public class EntityQueryControllerTest extends AbstractControllerTest { EntityDataQuery query = new EntityDataQuery(filter, pageLink, entityFields, null, null); - PageData data = - doPostWithTypedResponse("/api/entitiesQuery/find", query, new TypeReference>() { - }); - - Assert.assertEquals(1, data.getTotalElements()); - Assert.assertEquals(1, data.getTotalPages()); - Assert.assertEquals(1, data.getData().size()); + findByQueryAndCheck(query, 1); // unnassign dashboard login(TENANT_EMAIL, TENANT_PASSWORD); doDelete("/api/customer/" + savedCustomer.getId().getId().toString() + "/dashboard/" + savedDashboard.getId().getId().toString(), Dashboard.class); login(CUSTOMER_USER_EMAIL, CUSTOMER_USER_PASSWORD); - PageData dataAfterUnassign = - doPostWithTypedResponse("/api/entitiesQuery/find", query, new TypeReference>() { - }); - - Assert.assertEquals(0, dataAfterUnassign.getTotalElements()); - Assert.assertEquals(0, dataAfterUnassign.getTotalPages()); - Assert.assertEquals(0, dataAfterUnassign.getData().size()); + findByQueryAndCheck(query, 0); } private void checkEntitiesByQuery(EntityDataQuery query, int expectedNumOfDevices, String expectedOwnerName, String expectedOwnerType) throws Exception { diff --git a/application/src/test/java/org/thingsboard/server/queue/discovery/HashPartitionServiceTest.java b/application/src/test/java/org/thingsboard/server/queue/discovery/HashPartitionServiceTest.java index 0a4009e8b2..0d3ae093aa 100644 --- a/application/src/test/java/org/thingsboard/server/queue/discovery/HashPartitionServiceTest.java +++ b/application/src/test/java/org/thingsboard/server/queue/discovery/HashPartitionServiceTest.java @@ -431,6 +431,7 @@ public class HashPartitionServiceTest { ReflectionTestUtils.setField(partitionService, "hashFunctionName", hashFunctionName); ReflectionTestUtils.setField(partitionService, "edgeTopic", "tb.edge"); ReflectionTestUtils.setField(partitionService, "edgePartitions", 10); + ReflectionTestUtils.setField(partitionService, "edqsPartitions", 12); partitionService.init(); partitionService.partitionsInit(); return partitionService; diff --git a/application/src/test/java/org/thingsboard/server/service/state/DefaultDeviceStateServiceTest.java b/application/src/test/java/org/thingsboard/server/service/state/DefaultDeviceStateServiceTest.java index 9880ec964b..3c3fe2716c 100644 --- a/application/src/test/java/org/thingsboard/server/service/state/DefaultDeviceStateServiceTest.java +++ b/application/src/test/java/org/thingsboard/server/service/state/DefaultDeviceStateServiceTest.java @@ -211,7 +211,7 @@ public class DefaultDeviceStateServiceTest { // THEN then(telemetrySubscriptionService).should().saveAttributes(argThat(request -> - request.getTenantId().equals(TenantId.SYS_TENANT_ID) && request.getEntityId().equals(deviceId) && + request.getTenantId().equals(tenantId) && request.getEntityId().equals(deviceId) && request.getScope().equals(AttributeScope.SERVER_SCOPE) && request.getEntries().get(0).getKey().equals(LAST_CONNECT_TIME) && request.getEntries().get(0).getValue().equals(lastConnectTime) @@ -298,7 +298,7 @@ public class DefaultDeviceStateServiceTest { // THEN then(telemetrySubscriptionService).should().saveAttributes(argThat(request -> - request.getTenantId().equals(TenantId.SYS_TENANT_ID) && request.getEntityId().equals(deviceId) && + request.getTenantId().equals(tenantId) && request.getEntityId().equals(deviceId) && request.getScope().equals(AttributeScope.SERVER_SCOPE) && request.getEntries().get(0).getKey().equals(LAST_DISCONNECT_TIME) && request.getEntries().get(0).getValue().equals(lastDisconnectTime) @@ -421,13 +421,13 @@ public class DefaultDeviceStateServiceTest { // THEN then(telemetrySubscriptionService).should().saveAttributes(argThat(request -> - request.getTenantId().equals(TenantId.SYS_TENANT_ID) && request.getEntityId().equals(deviceId) && + request.getTenantId().equals(tenantId) && request.getEntityId().equals(deviceId) && request.getScope().equals(AttributeScope.SERVER_SCOPE) && request.getEntries().get(0).getKey().equals(INACTIVITY_ALARM_TIME) && request.getEntries().get(0).getValue().equals(lastInactivityTime) )); then(telemetrySubscriptionService).should().saveAttributes(argThat(request -> - request.getTenantId().equals(TenantId.SYS_TENANT_ID) && request.getEntityId().equals(deviceId) && + request.getTenantId().equals(tenantId) && request.getEntityId().equals(deviceId) && request.getScope().equals(AttributeScope.SERVER_SCOPE) && request.getEntries().get(0).getKey().equals(ACTIVITY_STATE) && request.getEntries().get(0).getValue().equals(false) @@ -465,12 +465,12 @@ public class DefaultDeviceStateServiceTest { // THEN then(telemetrySubscriptionService).should().saveAttributes(argThat(request -> - request.getTenantId().equals(TenantId.SYS_TENANT_ID) && request.getEntityId().equals(deviceId) && + request.getTenantId().equals(tenantId) && request.getEntityId().equals(deviceId) && request.getScope().equals(AttributeScope.SERVER_SCOPE) && request.getEntries().get(0).getKey().equals(INACTIVITY_ALARM_TIME) )); then(telemetrySubscriptionService).should().saveAttributes(argThat(request -> - request.getTenantId().equals(TenantId.SYS_TENANT_ID) && request.getEntityId().equals(deviceId) && + request.getTenantId().equals(tenantId) && request.getEntityId().equals(deviceId) && request.getScope().equals(AttributeScope.SERVER_SCOPE) && request.getEntries().get(0).getKey().equals(ACTIVITY_STATE) && request.getEntries().get(0).getValue().equals(false) @@ -1002,7 +1002,7 @@ public class DefaultDeviceStateServiceTest { assertThat(actualNotification.isActive()).isFalse(); then(telemetrySubscriptionService).should().saveAttributes(argThat(request -> - request.getTenantId().equals(TenantId.SYS_TENANT_ID) && request.getEntityId().equals(deviceId) && + request.getTenantId().equals(tenantId) && request.getEntityId().equals(deviceId) && request.getScope().equals(AttributeScope.SERVER_SCOPE) && request.getEntries().get(0).getKey().equals(INACTIVITY_ALARM_TIME) && request.getEntries().get(0).getValue().equals(expectedLastInactivityAlarmTime) @@ -1170,7 +1170,7 @@ public class DefaultDeviceStateServiceTest { assertThat(attributeRequestCaptor.getAllValues()).hasSize(2) .anySatisfy(request -> { - assertThat(request.getTenantId()).isEqualTo(TenantId.SYS_TENANT_ID); + assertThat(request.getTenantId()).isEqualTo(tenantId); assertThat(request.getEntityId()).isEqualTo(deviceId); assertThat(request.getScope()).isEqualTo(AttributeScope.SERVER_SCOPE); assertThat(request.getEntries()).singleElement().satisfies(attributeKvEntry -> { @@ -1179,7 +1179,7 @@ public class DefaultDeviceStateServiceTest { }); }) .anySatisfy(request -> { - assertThat(request.getTenantId()).isEqualTo(TenantId.SYS_TENANT_ID); + assertThat(request.getTenantId()).isEqualTo(tenantId); assertThat(request.getEntityId()).isEqualTo(deviceId); assertThat(request.getScope()).isEqualTo(AttributeScope.SERVER_SCOPE); assertThat(request.getEntries()).singleElement().satisfies(attributeKvEntry -> { diff --git a/application/src/test/resources/update/330/device_profile_001_out.json b/application/src/test/resources/update/330/device_profile_001_out.json index 9a349c6638..29e2241ee9 100644 --- a/application/src/test/resources/update/330/device_profile_001_out.json +++ b/application/src/test/resources/update/330/device_profile_001_out.json @@ -64,7 +64,8 @@ "dynamicValue": { "sourceType": null, "sourceAttribute": null, - "inherit": false + "inherit": false, + "resolvedValue" : null } } } @@ -103,7 +104,8 @@ "dynamicValue": { "sourceType": null, "sourceAttribute": null, - "inherit": false + "inherit": false, + "resolvedValue" : null } } } From f128f49d44edbdafbe6927b3137a3106cdc33bf7 Mon Sep 17 00:00:00 2001 From: dashevchenko Date: Wed, 5 Feb 2025 18:57:23 +0200 Subject: [PATCH 053/345] test fixes --- .../service/entitiy/EntityServiceTest.java | 18 +++++------------- 1 file changed, 5 insertions(+), 13 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 fab4cba3f5..a559eca6ce 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 @@ -1178,8 +1178,7 @@ public class EntityServiceTest extends AbstractControllerTest { EntityDataQuery query = new EntityDataQuery(singleEntityFilter, pageLink, entityFields, null, null); - PageData result = searchEntities(query); - assertEquals(1, result.getTotalElements()); + PageData result = findByQueryAndCheck(query, 1); String deviceName = result.getData().get(0).getLatest().get(EntityKeyType.ENTITY_FIELD).get("name").getValue(); assertThat(deviceName).isEqualTo(devices.get(0).getName()); @@ -1240,11 +1239,8 @@ public class EntityServiceTest extends AbstractControllerTest { filter.setRootEntity(asset.getId()); EntityDataQuery query = new EntityDataQuery(filter, pageLink, Collections.emptyList(), Collections.emptyList(), keyFiltersEqualString); - PageData relationsResult = entityService.findEntityDataByQuery(tenantId, customer.getId(), query); - long relationsResultCnt = entityService.countEntitiesByQuery(tenantId, customer.getId(), query); - - Assert.assertEquals(relationsCnt, relationsResult.getData().size()); - Assert.assertEquals(relationsCnt, relationsResultCnt); + findByQueryAndCheck(customer.getId(), query, relationsCnt); + countByQueryAndCheck(customer.getId(), query, relationsCnt); } } @@ -1497,9 +1493,6 @@ public class EntityServiceTest extends AbstractControllerTest { assertThat(tenantResultName).isEqualTo(TEST_CUSTOMER_NAME); } - private PageData searchEntities(EntityDataQuery query) { - return entityService.findEntityDataByQuery(tenantId, new CustomerId(CustomerId.NULL_UUID), query); - } private EntityDataQuery createDeviceSearchQuery(String deviceField, StringOperation operation, String searchQuery) { DeviceTypeFilter deviceTypeFilter = new DeviceTypeFilter(); @@ -1593,7 +1586,7 @@ public class EntityServiceTest extends AbstractControllerTest { .getLatest().get(currentAttributeKeyType).get("temperature").getValue()); } List deviceTemperatures = temperatures.stream().map(aLong -> Long.toString(aLong)).collect(Collectors.toList()); - Assert.assertEquals(deviceTemperatures, loadedTemperatures); + assertThat(loadedTemperatures).containsExactlyInAnyOrderElementsOf(deviceTemperatures); pageLink = new EntityDataPageLink(10, 0, null, sortOrder); KeyFilter highTemperatureFilter = createNumericKeyFilter("temperature", currentAttributeKeyType, NumericFilterPredicate.NumericOperation.GREATER, 45); @@ -1614,8 +1607,7 @@ public class EntityServiceTest extends AbstractControllerTest { entityData.getLatest().get(currentAttributeKeyType).get("temperature").getValue()).collect(Collectors.toList()); List deviceHighTemperatures = highTemperatures.stream().map(aLong -> Long.toString(aLong)).collect(Collectors.toList()); - Assert.assertEquals(deviceHighTemperatures, loadedHighTemperatures); - + assertThat(loadedHighTemperatures).containsExactlyInAnyOrderElementsOf(deviceHighTemperatures); } deviceService.deleteDevicesByTenantId(tenantId); } From 8de861f8977d1da48b85959dd520983aaa6a0876 Mon Sep 17 00:00:00 2001 From: Andrii Landiak Date: Fri, 7 Feb 2025 16:11:33 +0200 Subject: [PATCH 054/345] Improve customization for edge-event topic --- application/src/main/resources/thingsboard.yml | 2 ++ .../org/thingsboard/server/queue/discovery/TopicService.java | 5 ++++- 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/application/src/main/resources/thingsboard.yml b/application/src/main/resources/thingsboard.yml index 4ee08c7095..7b1df1d9d7 100644 --- a/application/src/main/resources/thingsboard.yml +++ b/application/src/main/resources/thingsboard.yml @@ -1748,6 +1748,8 @@ queue: topic: "${TB_QUEUE_EDGE_TOPIC:tb_edge}" # For high-priority notifications that require minimum latency and processing time notifications_topic: "${TB_QUEUE_EDGE_NOTIFICATIONS_TOPIC:tb_edge.notifications}" + # For edge events messages + event_notifications_topic: "${TB_QUEUE_EDGE_EVENT_NOTIFICATIONS_TOPIC:tb_edge_event.notifications}" # Amount of partitions used by Edge services partitions: "${TB_QUEUE_EDGE_PARTITIONS:10}" # Poll interval for topics related to Edge services 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 254d0a95d6..bab55eece8 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 @@ -44,6 +44,9 @@ public class TopicService { @Value("${queue.edge.notifications-topic:tb_edge.notifications}") private String tbEdgeNotificationsTopic; + @Value("${queue.edge.event-notifications-topic:tb_edge.notifications}") + private String tbEdgeEventNotificationsTopic; + private final ConcurrentMap tbCoreNotificationTopics = new ConcurrentHashMap<>(); private final ConcurrentMap tbRuleEngineNotificationTopics = new ConcurrentHashMap<>(); private final ConcurrentMap tbEdgeNotificationTopics = new ConcurrentHashMap<>(); @@ -88,7 +91,7 @@ public class TopicService { } public TopicPartitionInfo buildEdgeEventNotificationsTopicPartitionInfo(TenantId tenantId, EdgeId edgeId) { - return buildTopicPartitionInfo("tb_edge_event.notifications." + tenantId + "." + edgeId, null, null, false); + return buildTopicPartitionInfo(tbEdgeEventNotificationsTopic + "." + tenantId + "." + edgeId, null, null, false); } public String buildTopicName(String topic) { From b54e0cd59c0d70181c509279a80bf985e61fd719 Mon Sep 17 00:00:00 2001 From: Andrii Landiak Date: Fri, 7 Feb 2025 16:12:53 +0200 Subject: [PATCH 055/345] Rename tbEdgeEventNotificationsTopic --- .../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 bab55eece8..ca080f9481 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 @@ -44,7 +44,7 @@ public class TopicService { @Value("${queue.edge.notifications-topic:tb_edge.notifications}") private String tbEdgeNotificationsTopic; - @Value("${queue.edge.event-notifications-topic:tb_edge.notifications}") + @Value("${queue.edge.event-notifications-topic:tb_edge_event.notifications}") private String tbEdgeEventNotificationsTopic; private final ConcurrentMap tbCoreNotificationTopics = new ConcurrentHashMap<>(); From 5563da7c0d9e48fc707626e23db0082c6bf68faa Mon Sep 17 00:00:00 2001 From: dashevchenko Date: Mon, 10 Feb 2025 16:28:59 +0200 Subject: [PATCH 056/345] implemented filters for AlarmCountQuery --- ...efaultTbEntityDataSubscriptionService.java | 8 +- .../subscription/TbAlarmCountSubCtx.java | 37 +++++- .../server/controller/WebsocketApiTest.java | 114 +++++++++++++++++- .../server/dao/alarm/AlarmService.java | 2 + .../common/data/query/AlarmCountQuery.java | 7 ++ .../common/data/query/EntityCountQuery.java | 2 +- .../server/dao/alarm/AlarmDao.java | 2 +- .../server/dao/alarm/BaseAlarmService.java | 8 +- .../server/dao/sql/alarm/JpaAlarmDao.java | 4 +- .../dao/sql/query/AlarmQueryRepository.java | 2 +- .../query/DefaultAlarmQueryRepository.java | 11 +- .../server/dao/service/AlarmServiceTest.java | 51 ++++++++ 12 files changed, 230 insertions(+), 18 deletions(-) diff --git a/application/src/main/java/org/thingsboard/server/service/subscription/DefaultTbEntityDataSubscriptionService.java b/application/src/main/java/org/thingsboard/server/service/subscription/DefaultTbEntityDataSubscriptionService.java index 946a425a71..9812872bf1 100644 --- a/application/src/main/java/org/thingsboard/server/service/subscription/DefaultTbEntityDataSubscriptionService.java +++ b/application/src/main/java/org/thingsboard/server/service/subscription/DefaultTbEntityDataSubscriptionService.java @@ -33,7 +33,6 @@ import org.springframework.stereotype.Service; import org.springframework.web.socket.CloseStatus; import org.thingsboard.common.util.ThingsBoardExecutors; import org.thingsboard.common.util.ThingsBoardThreadFactory; -import org.thingsboard.server.common.data.alarm.AlarmInfo; import org.thingsboard.server.common.data.kv.BaseReadTsKvQuery; import org.thingsboard.server.common.data.kv.ReadTsKvQuery; import org.thingsboard.server.common.data.kv.ReadTsKvQueryResult; @@ -41,7 +40,6 @@ import org.thingsboard.server.common.data.kv.TsKvEntry; import org.thingsboard.server.common.data.page.PageData; import org.thingsboard.server.common.data.query.AlarmDataQuery; import org.thingsboard.server.common.data.query.ComparisonTsValue; -import org.thingsboard.server.common.data.query.OriginatorAlarmFilter; import org.thingsboard.server.common.data.query.EntityData; import org.thingsboard.server.common.data.query.EntityDataQuery; import org.thingsboard.server.common.data.query.EntityKey; @@ -55,7 +53,6 @@ import org.thingsboard.server.dao.timeseries.TimeseriesService; import org.thingsboard.server.queue.discovery.TbServiceInfoProvider; import org.thingsboard.server.queue.util.TbCoreComponent; import org.thingsboard.server.service.executors.DbCallbackExecutorService; -import org.thingsboard.server.service.security.model.SecurityUser; import org.thingsboard.server.service.ws.WebSocketService; import org.thingsboard.server.service.ws.WebSocketSessionRef; import org.thingsboard.server.service.ws.telemetry.cmd.v2.AggHistoryCmd; @@ -65,7 +62,6 @@ import org.thingsboard.server.service.ws.telemetry.cmd.v2.AlarmCountCmd; import org.thingsboard.server.service.ws.telemetry.cmd.v2.AlarmDataCmd; import org.thingsboard.server.service.ws.telemetry.cmd.v2.AlarmDataUpdate; import org.thingsboard.server.service.ws.telemetry.cmd.v2.AlarmStatusCmd; -import org.thingsboard.server.service.ws.telemetry.cmd.v2.CmdUpdate; import org.thingsboard.server.service.ws.telemetry.cmd.v2.EntityCountCmd; import org.thingsboard.server.service.ws.telemetry.cmd.v2.EntityDataCmd; import org.thingsboard.server.service.ws.telemetry.cmd.v2.EntityDataUpdate; @@ -74,7 +70,6 @@ import org.thingsboard.server.service.ws.telemetry.cmd.v2.GetTsCmd; 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.cmd.v2.UnsubscribeCmd; -import org.thingsboard.server.service.ws.telemetry.sub.AlarmSubscriptionUpdate; import java.util.ArrayList; import java.util.Arrays; @@ -83,7 +78,6 @@ import java.util.LinkedHashSet; import java.util.List; import java.util.Map; import java.util.Set; -import java.util.UUID; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.ConcurrentMap; import java.util.concurrent.ExecutionException; @@ -555,7 +549,7 @@ public class DefaultTbEntityDataSubscriptionService implements TbEntityDataSubsc private TbAlarmCountSubCtx createSubCtx(WebSocketSessionRef sessionRef, AlarmCountCmd cmd) { Map sessionSubs = subscriptionsBySessionId.computeIfAbsent(sessionRef.getSessionId(), k -> new ConcurrentHashMap<>()); TbAlarmCountSubCtx ctx = new TbAlarmCountSubCtx(serviceId, wsService, entityService, localSubscriptionService, - attributesService, stats, alarmService, sessionRef, cmd.getCmdId()); + attributesService, stats, alarmService, sessionRef, cmd.getCmdId(), maxEntitiesPerAlarmSubscription); if (cmd.getQuery() != null) { ctx.setAndResolveQuery(cmd.getQuery()); } diff --git a/application/src/main/java/org/thingsboard/server/service/subscription/TbAlarmCountSubCtx.java b/application/src/main/java/org/thingsboard/server/service/subscription/TbAlarmCountSubCtx.java index af7cfcfcfe..8425b2a57a 100644 --- a/application/src/main/java/org/thingsboard/server/service/subscription/TbAlarmCountSubCtx.java +++ b/application/src/main/java/org/thingsboard/server/service/subscription/TbAlarmCountSubCtx.java @@ -19,20 +19,33 @@ import lombok.Getter; import lombok.Setter; import lombok.ToString; import lombok.extern.slf4j.Slf4j; +import org.thingsboard.server.common.data.id.EntityId; +import org.thingsboard.server.common.data.page.PageData; import org.thingsboard.server.common.data.query.AlarmCountQuery; +import org.thingsboard.server.common.data.query.EntityData; +import org.thingsboard.server.common.data.query.EntityDataPageLink; +import org.thingsboard.server.common.data.query.EntityDataQuery; +import org.thingsboard.server.common.data.query.EntityDataSortOrder; +import org.thingsboard.server.common.data.query.EntityKey; +import org.thingsboard.server.common.data.query.EntityKeyType; import org.thingsboard.server.dao.alarm.AlarmService; import org.thingsboard.server.dao.attributes.AttributesService; import org.thingsboard.server.dao.entity.EntityService; +import org.thingsboard.server.dao.model.ModelConstants; import org.thingsboard.server.service.ws.WebSocketService; import org.thingsboard.server.service.ws.WebSocketSessionRef; import org.thingsboard.server.service.ws.telemetry.cmd.v2.AlarmCountUpdate; +import java.util.List; + @Slf4j @ToString(callSuper = true) public class TbAlarmCountSubCtx extends TbAbstractEntityQuerySubCtx { private final AlarmService alarmService; + private final int maxEntitiesPerAlarmSubscription; + @Getter @Setter private volatile int result; @@ -40,20 +53,21 @@ public class TbAlarmCountSubCtx extends TbAbstractEntityQuerySubCtx data = entityService.findEntityDataByQuery(getTenantId(), getCustomerId(), buildEntityDataQuery()); + List entityIds = data.getData().stream().map(EntityData::getEntityId).toList(); + if (entityIds.isEmpty()) { + return 0; + } else { + return (int) alarmService.countAlarmsByQuery(getTenantId(), getCustomerId(), query, entityIds); + } + + } + + private EntityDataQuery buildEntityDataQuery() { + EntityDataPageLink edpl = new EntityDataPageLink(maxEntitiesPerAlarmSubscription, 0, null, + new EntityDataSortOrder(new EntityKey(EntityKeyType.ENTITY_FIELD, ModelConstants.CREATED_TIME_PROPERTY))); + return new EntityDataQuery(query.getEntityFilter(), edpl, null, null, null); + } } 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 b64c87ccbd..e72e1dc806 100644 --- a/application/src/test/java/org/thingsboard/server/controller/WebsocketApiTest.java +++ b/application/src/test/java/org/thingsboard/server/controller/WebsocketApiTest.java @@ -83,7 +83,8 @@ import static org.springframework.test.web.servlet.result.MockMvcResultMatchers. @Slf4j @DaoSqlTest @TestPropertySource(properties = { - "server.ws.alarms_per_alarm_status_subscription_cache_size=5" + "server.ws.alarms_per_alarm_status_subscription_cache_size=5", + "server.ws.dynamic_page_link.refresh_interval=2" }) public class WebsocketApiTest extends AbstractControllerTest { @Autowired @@ -324,6 +325,117 @@ public class WebsocketApiTest extends AbstractControllerTest { Assert.assertEquals(1, update.getCount()); } + @Test + public void testAlarmCountWsCmdWithSingleEntityFilter() throws Exception { + loginTenantAdmin(); + + SingleEntityFilter singleEntityFilter = new SingleEntityFilter(); + singleEntityFilter.setSingleEntity(tenantId); + AlarmCountQuery alarmCountQuery = new AlarmCountQuery(singleEntityFilter); + AlarmCountCmd cmd1 = new AlarmCountCmd(1, alarmCountQuery); + + getWsClient().send(cmd1); + + AlarmCountUpdate update = getWsClient().parseAlarmCountReply(getWsClient().waitForReply()); + Assert.assertEquals(1, update.getCmdId()); + Assert.assertEquals(0, update.getCount()); + + Alarm alarm = new Alarm(); + alarm.setOriginator(tenantId); + alarm.setType("TEST ALARM"); + alarm.setSeverity(AlarmSeverity.WARNING); + + alarm = doPost("/api/alarm", alarm, Alarm.class); + + AlarmCountCmd cmd2 = new AlarmCountCmd(2, alarmCountQuery); + + getWsClient().send(cmd2); + + update = getWsClient().parseAlarmCountReply(getWsClient().waitForReply()); + Assert.assertEquals(2, update.getCmdId()); + Assert.assertEquals(1, update.getCount()); + + singleEntityFilter.setSingleEntity(tenantAdminUserId); + AlarmCountCmd cmd3 = new AlarmCountCmd(3, alarmCountQuery); + + getWsClient().send(cmd3); + + update = getWsClient().parseAlarmCountReply(getWsClient().waitForReply()); + Assert.assertEquals(3, update.getCmdId()); + Assert.assertEquals(0, update.getCount()); + + alarm.setAssigneeId(tenantAdminUserId); + alarm = doPost("/api/alarm", alarm, Alarm.class); + + singleEntityFilter.setSingleEntity(tenantId); + alarmCountQuery.setAssigneeId(tenantAdminUserId); + AlarmCountCmd cmd4 = new AlarmCountCmd(4, alarmCountQuery); + + getWsClient().send(cmd4); + + update = getWsClient().parseAlarmCountReply(getWsClient().waitForReply()); + Assert.assertEquals(4, update.getCmdId()); + Assert.assertEquals(1, update.getCount()); + + alarmCountQuery.setSeverityList(Collections.singletonList(AlarmSeverity.CRITICAL)); + AlarmCountCmd cmd5 = new AlarmCountCmd(5, alarmCountQuery); + + getWsClient().send(cmd5); + + update = getWsClient().parseAlarmCountReply(getWsClient().waitForReply()); + Assert.assertEquals(5, update.getCmdId()); + Assert.assertEquals(0, update.getCount()); + + alarm.setSeverity(AlarmSeverity.CRITICAL); + doPost("/api/alarm", alarm, Alarm.class); + + AlarmCountCmd cmd6 = new AlarmCountCmd(6, alarmCountQuery); + + getWsClient().send(cmd6); + + update = getWsClient().parseAlarmCountReply(getWsClient().waitForReply()); + Assert.assertEquals(6, update.getCmdId()); + Assert.assertEquals(1, update.getCount()); + } + + @Test + public void testAlarmCountWsCmdWithDeviceType() throws Exception { + loginTenantAdmin(); + + DeviceTypeFilter deviceTypeFilter = new DeviceTypeFilter(); + deviceTypeFilter.setDeviceTypes(List.of("default")); + AlarmCountQuery alarmCountQuery = new AlarmCountQuery(deviceTypeFilter); + AlarmCountCmd cmd1 = new AlarmCountCmd(1, alarmCountQuery); + + getWsClient().send(cmd1); + + AlarmCountUpdate update = getWsClient().parseAlarmCountReply(getWsClient().waitForReply()); + Assert.assertEquals(1, update.getCmdId()); + Assert.assertEquals(0, update.getCount()); + + getWsClient().registerWaitForUpdate(); + + Alarm alarm = new Alarm(); + alarm.setOriginator(device.getId()); + alarm.setType("TEST ALARM"); + alarm.setSeverity(AlarmSeverity.WARNING); + + alarm = doPost("/api/alarm", alarm, Alarm.class); + + update = getWsClient().parseAlarmCountReply(getWsClient().waitForUpdate(3000)); + Assert.assertEquals(1, update.getCmdId()); + Assert.assertEquals(1, update.getCount()); + + deviceTypeFilter.setDeviceTypes(List.of("non-existing")); + AlarmCountCmd cmd3 = new AlarmCountCmd(3, alarmCountQuery); + + getWsClient().send(cmd3); + + update = getWsClient().parseAlarmCountReply(getWsClient().waitForReply()); + Assert.assertEquals(3, update.getCmdId()); + Assert.assertEquals(0, update.getCount()); + } + @Test public void testAlarmStatusWsCmd() throws Exception { loginTenantAdmin(); diff --git a/common/dao-api/src/main/java/org/thingsboard/server/dao/alarm/AlarmService.java b/common/dao-api/src/main/java/org/thingsboard/server/dao/alarm/AlarmService.java index ce040595cc..11252b59a5 100644 --- a/common/dao-api/src/main/java/org/thingsboard/server/dao/alarm/AlarmService.java +++ b/common/dao-api/src/main/java/org/thingsboard/server/dao/alarm/AlarmService.java @@ -118,6 +118,8 @@ public interface AlarmService extends EntityDaoService { long countAlarmsByQuery(TenantId tenantId, CustomerId customerId, AlarmCountQuery query); + long countAlarmsByQuery(TenantId tenantId, CustomerId customerId, AlarmCountQuery query, Collection orderedEntityIds); + PageData findAlarmTypesByTenantId(TenantId tenantId, PageLink pageLink); List findActiveOriginatorAlarms(TenantId tenantId, OriginatorAlarmFilter originatorAlarmFilter, int limit); diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/query/AlarmCountQuery.java b/common/data/src/main/java/org/thingsboard/server/common/data/query/AlarmCountQuery.java index 19cb445380..bb969a28ce 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/query/AlarmCountQuery.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/query/AlarmCountQuery.java @@ -19,6 +19,7 @@ import lombok.AllArgsConstructor; import lombok.Builder; import lombok.Getter; import lombok.NoArgsConstructor; +import lombok.Setter; import lombok.ToString; import org.thingsboard.server.common.data.alarm.AlarmSearchStatus; import org.thingsboard.server.common.data.alarm.AlarmSeverity; @@ -30,6 +31,7 @@ import java.util.List; @NoArgsConstructor @AllArgsConstructor @Getter +@Setter @ToString public class AlarmCountQuery extends EntityCountQuery { private long startTs; @@ -40,4 +42,9 @@ public class AlarmCountQuery extends EntityCountQuery { private List severityList; private boolean searchPropagatedAlarms; private UserId assigneeId; + + public AlarmCountQuery(EntityFilter entityFilter) { + super(entityFilter); + } + } diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/query/EntityCountQuery.java b/common/data/src/main/java/org/thingsboard/server/common/data/query/EntityCountQuery.java index 3dd24147e7..be9fa4c664 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/query/EntityCountQuery.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/query/EntityCountQuery.java @@ -27,7 +27,7 @@ import java.util.List; public class EntityCountQuery { @Getter - private EntityFilter entityFilter; + protected EntityFilter entityFilter; @Getter protected List keyFilters; diff --git a/dao/src/main/java/org/thingsboard/server/dao/alarm/AlarmDao.java b/dao/src/main/java/org/thingsboard/server/dao/alarm/AlarmDao.java index 4b4ee0ae4f..2432bb4bbe 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/alarm/AlarmDao.java +++ b/dao/src/main/java/org/thingsboard/server/dao/alarm/AlarmDao.java @@ -106,7 +106,7 @@ public interface AlarmDao extends Dao { AlarmApiCallResult unassignAlarm(TenantId tenantId, AlarmId alarmId, long unassignTime); - long countAlarmsByQuery(TenantId tenantId, CustomerId customerId, AlarmCountQuery query); + long countAlarmsByQuery(TenantId tenantId, CustomerId customerId, AlarmCountQuery query, Collection orderedEntityIds); PageData findTenantAlarmTypes(UUID tenantId, PageLink pageLink); diff --git a/dao/src/main/java/org/thingsboard/server/dao/alarm/BaseAlarmService.java b/dao/src/main/java/org/thingsboard/server/dao/alarm/BaseAlarmService.java index bf08621d14..14cd65185d 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/alarm/BaseAlarmService.java +++ b/dao/src/main/java/org/thingsboard/server/dao/alarm/BaseAlarmService.java @@ -352,7 +352,13 @@ public class BaseAlarmService extends AbstractCachedEntityService INCORRECT_TENANT_ID + id); - return alarmDao.countAlarmsByQuery(tenantId, customerId, query); + return alarmDao.countAlarmsByQuery(tenantId, customerId, query, Collections.emptyList()); + } + + @Override + public long countAlarmsByQuery(TenantId tenantId, CustomerId customerId, AlarmCountQuery query, Collection orderedEntityIds) { + validateId(tenantId, id -> INCORRECT_TENANT_ID + id); + return alarmDao.countAlarmsByQuery(tenantId, customerId, query, orderedEntityIds); } @Override diff --git a/dao/src/main/java/org/thingsboard/server/dao/sql/alarm/JpaAlarmDao.java b/dao/src/main/java/org/thingsboard/server/dao/sql/alarm/JpaAlarmDao.java index e433dfbd86..a3e02d5461 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/sql/alarm/JpaAlarmDao.java +++ b/dao/src/main/java/org/thingsboard/server/dao/sql/alarm/JpaAlarmDao.java @@ -415,8 +415,8 @@ public class JpaAlarmDao extends JpaAbstractDao implements A } @Override - public long countAlarmsByQuery(TenantId tenantId, CustomerId customerId, AlarmCountQuery query) { - return alarmQueryRepository.countAlarmsByQuery(tenantId, customerId, query); + public long countAlarmsByQuery(TenantId tenantId, CustomerId customerId, AlarmCountQuery query, Collection orderedEntityIds) { + return alarmQueryRepository.countAlarmsByQuery(tenantId, customerId, query, orderedEntityIds); } @Override diff --git a/dao/src/main/java/org/thingsboard/server/dao/sql/query/AlarmQueryRepository.java b/dao/src/main/java/org/thingsboard/server/dao/sql/query/AlarmQueryRepository.java index 49aa6c7276..2a26d62376 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/sql/query/AlarmQueryRepository.java +++ b/dao/src/main/java/org/thingsboard/server/dao/sql/query/AlarmQueryRepository.java @@ -30,6 +30,6 @@ public interface AlarmQueryRepository { PageData findAlarmDataByQueryForEntities(TenantId tenantId, AlarmDataQuery query, Collection orderedEntityIds); - long countAlarmsByQuery(TenantId tenantId, CustomerId customerId, AlarmCountQuery query); + long countAlarmsByQuery(TenantId tenantId, CustomerId customerId, AlarmCountQuery query, Collection orderedEntityIds); } diff --git a/dao/src/main/java/org/thingsboard/server/dao/sql/query/DefaultAlarmQueryRepository.java b/dao/src/main/java/org/thingsboard/server/dao/sql/query/DefaultAlarmQueryRepository.java index 05da22f9a5..b4cc40fc6d 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/sql/query/DefaultAlarmQueryRepository.java +++ b/dao/src/main/java/org/thingsboard/server/dao/sql/query/DefaultAlarmQueryRepository.java @@ -44,6 +44,7 @@ import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.Objects; +import java.util.UUID; import java.util.stream.Collectors; @Repository @@ -314,7 +315,7 @@ public class DefaultAlarmQueryRepository implements AlarmQueryRepository { } @Override - public long countAlarmsByQuery(TenantId tenantId, CustomerId customerId, AlarmCountQuery query) { + public long countAlarmsByQuery(TenantId tenantId, CustomerId customerId, AlarmCountQuery query, Collection orderedEntityIds) { QueryContext ctx = new QueryContext(new QuerySecurityContext(tenantId, null, EntityType.ALARM)); if (query.isSearchPropagatedAlarms()) { @@ -326,6 +327,10 @@ public class DefaultAlarmQueryRepository implements AlarmQueryRepository { ctx.append(" and a.customer_id = :customerId and ea.customer_id = :customerId"); ctx.addUuidParameter("customerId", customerId.getId()); } + if (!orderedEntityIds.isEmpty()) { + ctx.addUuidListParameter("entity_filter_entity_ids", orderedEntityIds.stream().map(EntityId::getId).collect(Collectors.toList())); + ctx.append(" and ea.entity_id in (:entity_filter_entity_ids)"); + } } else { ctx.append("select count(id) from alarm_info a "); ctx.append("where a.tenant_id = :tenantId"); @@ -334,6 +339,10 @@ public class DefaultAlarmQueryRepository implements AlarmQueryRepository { ctx.append(" and a.customer_id = :customerId"); ctx.addUuidParameter("customerId", customerId.getId()); } + if (!orderedEntityIds.isEmpty()) { + ctx.addUuidListParameter("entity_filter_entity_ids", orderedEntityIds.stream().map(EntityId::getId).collect(Collectors.toList())); + ctx.append(" and a.originator_id in (:entity_filter_entity_ids)"); + } } long startTs; diff --git a/dao/src/test/java/org/thingsboard/server/dao/service/AlarmServiceTest.java b/dao/src/test/java/org/thingsboard/server/dao/service/AlarmServiceTest.java index aa1c96d84b..d12765717a 100644 --- a/dao/src/test/java/org/thingsboard/server/dao/service/AlarmServiceTest.java +++ b/dao/src/test/java/org/thingsboard/server/dao/service/AlarmServiceTest.java @@ -22,6 +22,7 @@ import org.springframework.beans.factory.annotation.Autowired; import org.thingsboard.common.util.JacksonUtil; import org.thingsboard.server.common.data.Customer; import org.thingsboard.server.common.data.Device; +import org.thingsboard.server.common.data.EntityType; import org.thingsboard.server.common.data.User; import org.thingsboard.server.common.data.alarm.Alarm; import org.thingsboard.server.common.data.alarm.AlarmApiCallResult; @@ -48,6 +49,7 @@ import org.thingsboard.server.common.data.query.DeviceTypeFilter; import org.thingsboard.server.common.data.query.EntityDataSortOrder; import org.thingsboard.server.common.data.query.EntityKey; import org.thingsboard.server.common.data.query.EntityKeyType; +import org.thingsboard.server.common.data.query.EntityListFilter; import org.thingsboard.server.common.data.relation.EntityRelation; import org.thingsboard.server.common.data.relation.RelationTypeGroup; import org.thingsboard.server.common.data.security.Authority; @@ -936,4 +938,53 @@ public class AlarmServiceTest extends AbstractServiceTest { Assert.assertEquals(0, alarms.getData().size()); } + @Test + public void testCountAlarmsForEntities() throws ExecutionException, InterruptedException { + AssetId parentId = new AssetId(Uuids.timeBased()); + AssetId childId = new AssetId(Uuids.timeBased()); + + EntityRelation relation = new EntityRelation(parentId, childId, EntityRelation.CONTAINS_TYPE); + + Assert.assertTrue(relationService.saveRelationAsync(tenantId, relation).get()); + + long ts = System.currentTimeMillis(); + AlarmApiCallResult result = alarmService.createAlarm(AlarmCreateOrUpdateActiveRequest.builder() + .tenantId(tenantId) + .originator(childId) + .type(TEST_ALARM) + .severity(AlarmSeverity.CRITICAL) + .startTs(ts).build()); + AlarmInfo created = result.getAlarm(); + created.setPropagate(true); + result = alarmService.updateAlarm(AlarmUpdateRequest.fromAlarm(created)); + created = result.getAlarm(); + + EntityListFilter entityListFilter = new EntityListFilter(); + entityListFilter.setEntityList(List.of(childId.getId().toString(), parentId.getId().toString())); + entityListFilter.setEntityType(EntityType.ASSET); + AlarmCountQuery countQuery = new AlarmCountQuery(entityListFilter); + countQuery.setStartTs(0L); + countQuery.setEndTs(System.currentTimeMillis()); + + long alarmsCount = alarmService.countAlarmsByQuery(tenantId, null, countQuery, List.of(childId)); + Assert.assertEquals(1, alarmsCount); + + countQuery.setSearchPropagatedAlarms(true); + + alarmsCount = alarmService.countAlarmsByQuery(tenantId, null, countQuery, List.of(parentId)); + Assert.assertEquals(1, alarmsCount); + + created = alarmService.acknowledgeAlarm(tenantId, created.getId(), System.currentTimeMillis()).getAlarm(); + + countQuery.setStatusList(List.of(AlarmSearchStatus.UNACK)); + alarmsCount = alarmService.countAlarmsByQuery(tenantId, null, countQuery, List.of(childId)); + Assert.assertEquals(0, alarmsCount); + + alarmService.clearAlarm(tenantId, created.getId(), System.currentTimeMillis(), null); + + countQuery.setStatusList(List.of(AlarmSearchStatus.CLEARED)); + alarmsCount = alarmService.countAlarmsByQuery(tenantId, null, countQuery, List.of(childId)); + Assert.assertEquals(1, alarmsCount); + } + } From 33eac2f77868c4b79c1beb2a99a02fa0047f756c Mon Sep 17 00:00:00 2001 From: dashevchenko Date: Mon, 10 Feb 2025 17:34:12 +0200 Subject: [PATCH 057/345] fixed tests --- .../service/subscription/TbAlarmCountSubCtx.java | 15 ++++++++------- .../server/common/data/query/AlarmCountQuery.java | 6 ++---- .../common/data/query/EntityCountQuery.java | 2 +- .../server/dao/alarm/BaseAlarmService.java | 3 +-- .../sql/query/DefaultAlarmQueryRepository.java | 4 ++-- 5 files changed, 14 insertions(+), 16 deletions(-) diff --git a/application/src/main/java/org/thingsboard/server/service/subscription/TbAlarmCountSubCtx.java b/application/src/main/java/org/thingsboard/server/service/subscription/TbAlarmCountSubCtx.java index 8425b2a57a..4771bad716 100644 --- a/application/src/main/java/org/thingsboard/server/service/subscription/TbAlarmCountSubCtx.java +++ b/application/src/main/java/org/thingsboard/server/service/subscription/TbAlarmCountSubCtx.java @@ -80,14 +80,15 @@ public class TbAlarmCountSubCtx extends TbAbstractEntityQuerySubCtx data = entityService.findEntityDataByQuery(getTenantId(), getCustomerId(), buildEntityDataQuery()); - List entityIds = data.getData().stream().map(EntityData::getEntityId).toList(); - if (entityIds.isEmpty()) { - return 0; - } else { - return (int) alarmService.countAlarmsByQuery(getTenantId(), getCustomerId(), query, entityIds); + List entityIds = null; + if (query.getEntityFilter() != null) { + PageData data = entityService.findEntityDataByQuery(getTenantId(), getCustomerId(), buildEntityDataQuery()); + if (data.getData().isEmpty()) { + return 0; + } + entityIds = data.getData().stream().map(EntityData::getEntityId).toList(); } - + return (int) alarmService.countAlarmsByQuery(getTenantId(), getCustomerId(), query, entityIds); } private EntityDataQuery buildEntityDataQuery() { diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/query/AlarmCountQuery.java b/common/data/src/main/java/org/thingsboard/server/common/data/query/AlarmCountQuery.java index bb969a28ce..1bf19a970f 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/query/AlarmCountQuery.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/query/AlarmCountQuery.java @@ -17,9 +17,8 @@ package org.thingsboard.server.common.data.query; import lombok.AllArgsConstructor; import lombok.Builder; -import lombok.Getter; +import lombok.Data; import lombok.NoArgsConstructor; -import lombok.Setter; import lombok.ToString; import org.thingsboard.server.common.data.alarm.AlarmSearchStatus; import org.thingsboard.server.common.data.alarm.AlarmSeverity; @@ -30,8 +29,7 @@ import java.util.List; @Builder @NoArgsConstructor @AllArgsConstructor -@Getter -@Setter +@Data @ToString public class AlarmCountQuery extends EntityCountQuery { private long startTs; diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/query/EntityCountQuery.java b/common/data/src/main/java/org/thingsboard/server/common/data/query/EntityCountQuery.java index be9fa4c664..3dd24147e7 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/query/EntityCountQuery.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/query/EntityCountQuery.java @@ -27,7 +27,7 @@ import java.util.List; public class EntityCountQuery { @Getter - protected EntityFilter entityFilter; + private EntityFilter entityFilter; @Getter protected List keyFilters; diff --git a/dao/src/main/java/org/thingsboard/server/dao/alarm/BaseAlarmService.java b/dao/src/main/java/org/thingsboard/server/dao/alarm/BaseAlarmService.java index 14cd65185d..b8a57fdcff 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/alarm/BaseAlarmService.java +++ b/dao/src/main/java/org/thingsboard/server/dao/alarm/BaseAlarmService.java @@ -351,8 +351,7 @@ public class BaseAlarmService extends AbstractCachedEntityService INCORRECT_TENANT_ID + id); - return alarmDao.countAlarmsByQuery(tenantId, customerId, query, Collections.emptyList()); + return countAlarmsByQuery(tenantId, customerId, query, null); } @Override diff --git a/dao/src/main/java/org/thingsboard/server/dao/sql/query/DefaultAlarmQueryRepository.java b/dao/src/main/java/org/thingsboard/server/dao/sql/query/DefaultAlarmQueryRepository.java index b4cc40fc6d..216fd0d2d4 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/sql/query/DefaultAlarmQueryRepository.java +++ b/dao/src/main/java/org/thingsboard/server/dao/sql/query/DefaultAlarmQueryRepository.java @@ -327,7 +327,7 @@ public class DefaultAlarmQueryRepository implements AlarmQueryRepository { ctx.append(" and a.customer_id = :customerId and ea.customer_id = :customerId"); ctx.addUuidParameter("customerId", customerId.getId()); } - if (!orderedEntityIds.isEmpty()) { + if (orderedEntityIds != null) { ctx.addUuidListParameter("entity_filter_entity_ids", orderedEntityIds.stream().map(EntityId::getId).collect(Collectors.toList())); ctx.append(" and ea.entity_id in (:entity_filter_entity_ids)"); } @@ -339,7 +339,7 @@ public class DefaultAlarmQueryRepository implements AlarmQueryRepository { ctx.append(" and a.customer_id = :customerId"); ctx.addUuidParameter("customerId", customerId.getId()); } - if (!orderedEntityIds.isEmpty()) { + if (orderedEntityIds != null) { ctx.addUuidListParameter("entity_filter_entity_ids", orderedEntityIds.stream().map(EntityId::getId).collect(Collectors.toList())); ctx.append(" and a.originator_id in (:entity_filter_entity_ids)"); } From 7b6bb8a6d61546a82305c51862ee087116b10827 Mon Sep 17 00:00:00 2001 From: Vladyslav_Prykhodko Date: Wed, 12 Feb 2025 12:34:56 +0200 Subject: [PATCH 058/345] UI: Add support for entity alias filtering in alarm count data sources --- .../server/service/subscription/TbAlarmCountSubCtx.java | 2 +- .../basic/alarm/alarm-count-basic-config.component.html | 9 +++++++++ .../basic/alarm/alarm-count-basic-config.component.ts | 2 ++ .../components/widget/config/datasource.component.html | 8 ++++---- .../components/widget/config/datasource.component.ts | 9 +++++++++ .../components/widget/config/datasources.component.ts | 4 ++++ 6 files changed, 29 insertions(+), 5 deletions(-) diff --git a/application/src/main/java/org/thingsboard/server/service/subscription/TbAlarmCountSubCtx.java b/application/src/main/java/org/thingsboard/server/service/subscription/TbAlarmCountSubCtx.java index 4771bad716..2a5dea70ac 100644 --- a/application/src/main/java/org/thingsboard/server/service/subscription/TbAlarmCountSubCtx.java +++ b/application/src/main/java/org/thingsboard/server/service/subscription/TbAlarmCountSubCtx.java @@ -94,6 +94,6 @@ public class TbAlarmCountSubCtx extends TbAbstractEntityQuerySubCtx + +
alarm.filter
diff --git a/ui-ngx/src/app/modules/home/components/widget/config/basic/alarm/alarm-count-basic-config.component.ts b/ui-ngx/src/app/modules/home/components/widget/config/basic/alarm/alarm-count-basic-config.component.ts index 4ad664fae3..d365ed3f2d 100644 --- a/ui-ngx/src/app/modules/home/components/widget/config/basic/alarm/alarm-count-basic-config.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/config/basic/alarm/alarm-count-basic-config.component.ts @@ -69,6 +69,7 @@ export class AlarmCountBasicConfigComponent extends BasicWidgetConfigComponent { const settings: CountWidgetSettings = {...countDefaultSettings(true), ...(configData.config.settings || {})}; this.alarmCountWidgetConfigForm = this.fb.group({ alarmFilterConfig: [getAlarmFilterConfig(configData.config.datasources), []], + datasources: [configData.config.datasources, []], settings: [settings, []], @@ -81,6 +82,7 @@ export class AlarmCountBasicConfigComponent extends BasicWidgetConfigComponent { } protected prepareOutputConfig(config: any): WidgetConfigComponentData { + this.widgetConfig.config.datasources = config.datasources; setAlarmFilterConfig(config.alarmFilterConfig, this.widgetConfig.config.datasources); this.widgetConfig.config.settings = {...(this.widgetConfig.config.settings || {}), ...config.settings}; diff --git a/ui-ngx/src/app/modules/home/components/widget/config/datasource.component.html b/ui-ngx/src/app/modules/home/components/widget/config/datasource.component.html index 8af7753525..6c5c5cf083 100644 --- a/ui-ngx/src/app/modules/home/components/widget/config/datasource.component.html +++ b/ui-ngx/src/app/modules/home/components/widget/config/datasource.component.html @@ -36,7 +36,7 @@ datasourceFormGroup.get('type').value === datasourceType.entity || datasourceFormGroup.get('type').value === datasourceType.entityCount || datasourceFormGroup.get('type').value === datasourceType.alarmCount ? datasourceFormGroup.get('type').value : ''"> - @@ -98,7 +98,7 @@ Date: Wed, 12 Feb 2025 15:38:25 +0200 Subject: [PATCH 059/345] fetch entities only on subs creation --- ...efaultTbEntityDataSubscriptionService.java | 10 +- .../subscription/TbAlarmCountSubCtx.java | 106 ++++++++++++++---- .../server/controller/WebsocketApiTest.java | 4 +- .../query/DefaultAlarmQueryRepository.java | 36 +++--- 4 files changed, 117 insertions(+), 39 deletions(-) diff --git a/application/src/main/java/org/thingsboard/server/service/subscription/DefaultTbEntityDataSubscriptionService.java b/application/src/main/java/org/thingsboard/server/service/subscription/DefaultTbEntityDataSubscriptionService.java index 9812872bf1..b09f8350f1 100644 --- a/application/src/main/java/org/thingsboard/server/service/subscription/DefaultTbEntityDataSubscriptionService.java +++ b/application/src/main/java/org/thingsboard/server/service/subscription/DefaultTbEntityDataSubscriptionService.java @@ -424,8 +424,12 @@ public class DefaultTbEntityDataSubscriptionService implements TbEntityDataSubsc long start = System.currentTimeMillis(); ctx.fetchData(); long end = System.currentTimeMillis(); - stats.getAlarmQueryInvocationCnt().incrementAndGet(); - stats.getAlarmQueryTimeSpent().addAndGet(end - start); + stats.getRegularQueryInvocationCnt().incrementAndGet(); + stats.getRegularQueryTimeSpent().addAndGet(end - start); + ctx.cancelTasks(); + ctx.clearAlarmSubscriptions(); + ctx.fetchAlarmCount(); + ctx.createAlarmSubscriptions(); TbAlarmCountSubCtx finalCtx = ctx; ScheduledFuture task = scheduler.scheduleWithFixedDelay( () -> refreshDynamicQuery(finalCtx), @@ -549,7 +553,7 @@ public class DefaultTbEntityDataSubscriptionService implements TbEntityDataSubsc private TbAlarmCountSubCtx createSubCtx(WebSocketSessionRef sessionRef, AlarmCountCmd cmd) { Map sessionSubs = subscriptionsBySessionId.computeIfAbsent(sessionRef.getSessionId(), k -> new ConcurrentHashMap<>()); TbAlarmCountSubCtx ctx = new TbAlarmCountSubCtx(serviceId, wsService, entityService, localSubscriptionService, - attributesService, stats, alarmService, sessionRef, cmd.getCmdId(), maxEntitiesPerAlarmSubscription); + attributesService, stats, alarmService, sessionRef, cmd.getCmdId(), maxEntitiesPerAlarmSubscription, maxAlarmQueriesPerRefreshInterval); if (cmd.getQuery() != null) { ctx.setAndResolveQuery(cmd.getQuery()); } diff --git a/application/src/main/java/org/thingsboard/server/service/subscription/TbAlarmCountSubCtx.java b/application/src/main/java/org/thingsboard/server/service/subscription/TbAlarmCountSubCtx.java index 2a5dea70ac..712fc5c378 100644 --- a/application/src/main/java/org/thingsboard/server/service/subscription/TbAlarmCountSubCtx.java +++ b/application/src/main/java/org/thingsboard/server/service/subscription/TbAlarmCountSubCtx.java @@ -36,7 +36,9 @@ import org.thingsboard.server.service.ws.WebSocketService; import org.thingsboard.server.service.ws.WebSocketSessionRef; import org.thingsboard.server.service.ws.telemetry.cmd.v2.AlarmCountUpdate; -import java.util.List; +import java.util.LinkedHashSet; +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; @Slf4j @ToString(callSuper = true) @@ -44,56 +46,120 @@ public class TbAlarmCountSubCtx extends TbAbstractEntityQuerySubCtx subToEntityIdMap; + + private LinkedHashSet entitiesIds; + private final int maxEntitiesPerAlarmSubscription; + private final int maxAlarmQueriesPerRefreshInterval; + @Getter @Setter private volatile int result; + @Getter + @Setter + private boolean tooManyEntities; + + private int alarmCountInvocationAttempts; + public TbAlarmCountSubCtx(String serviceId, WebSocketService wsService, EntityService entityService, TbLocalSubscriptionService localSubscriptionService, AttributesService attributesService, SubscriptionServiceStatistics stats, AlarmService alarmService, - WebSocketSessionRef sessionRef, int cmdId, int maxEntitiesPerAlarmSubscription) { + WebSocketSessionRef sessionRef, int cmdId, int maxEntitiesPerAlarmSubscription, int maxAlarmQueriesPerRefreshInterval) { super(serviceId, wsService, entityService, localSubscriptionService, attributesService, stats, sessionRef, cmdId); this.alarmService = alarmService; + this.subToEntityIdMap = new ConcurrentHashMap<>(); this.maxEntitiesPerAlarmSubscription = maxEntitiesPerAlarmSubscription; + this.maxAlarmQueriesPerRefreshInterval = maxAlarmQueriesPerRefreshInterval; + this.entitiesIds = null; + } + + @Override + public void clearSubscriptions() { + clearAlarmSubscriptions(); } @Override public void fetchData() { - result = countAlarms(); - sendWsMsg(new AlarmCountUpdate(cmdId, result)); + resetInvocationCounter(); + if (query.getEntityFilter() != null) { + entitiesIds = new LinkedHashSet<>(); + log.trace("[{}] Fetching data: {}", cmdId, alarmCountInvocationAttempts); + PageData data = entityService.findEntityDataByQuery(getTenantId(), getCustomerId(), buildEntityDataQuery()); + entitiesIds.clear(); + tooManyEntities = data.hasNext(); + for (EntityData entityData : data.getData()) { + entitiesIds.add(entityData.getEntityId()); + } + } } @Override protected void update() { - int newCount = countAlarms(); - if (newCount != result) { - result = newCount; - sendWsMsg(new AlarmCountUpdate(cmdId, result)); + resetInvocationCounter(); + fetchAlarmCount(); + } + + public void fetchAlarmCount() { + alarmCountInvocationAttempts++; + log.trace("[{}] Fetching alarms: {}", cmdId, alarmCountInvocationAttempts); + if (alarmCountInvocationAttempts <= maxAlarmQueriesPerRefreshInterval) { + doFetchAlarmCount(); + } else { + log.trace("[{}] Ignore alarm count fetch due to rate limit: [{}] of maximum [{}]", cmdId, alarmCountInvocationAttempts, maxAlarmQueriesPerRefreshInterval); } } + private void doFetchAlarmCount() { + result = (int) alarmService.countAlarmsByQuery(getTenantId(), getCustomerId(), query, entitiesIds); + sendWsMsg(new AlarmCountUpdate(cmdId, result)); + } + @Override public boolean isDynamic() { return true; } - private int countAlarms() { - List entityIds = null; - if (query.getEntityFilter() != null) { - PageData data = entityService.findEntityDataByQuery(getTenantId(), getCustomerId(), buildEntityDataQuery()); - if (data.getData().isEmpty()) { - return 0; - } - entityIds = data.getData().stream().map(EntityData::getEntityId).toList(); - } - return (int) alarmService.countAlarmsByQuery(getTenantId(), getCustomerId(), query, entityIds); - } - private EntityDataQuery buildEntityDataQuery() { EntityDataPageLink edpl = new EntityDataPageLink(maxEntitiesPerAlarmSubscription, 0, null, new EntityDataSortOrder(new EntityKey(EntityKeyType.ENTITY_FIELD, ModelConstants.CREATED_TIME_PROPERTY))); return new EntityDataQuery(query.getEntityFilter(), edpl, null, null, query.getKeyFilters()); } + + private void resetInvocationCounter() { + alarmCountInvocationAttempts = 0; + } + + public void createAlarmSubscriptions() { + for (EntityId entityId : entitiesIds) { + createAlarmSubscriptionForEntity(entityId); + } + } + + private void createAlarmSubscriptionForEntity(EntityId entityId) { + int subIdx = sessionRef.getSessionSubIdSeq().incrementAndGet(); + subToEntityIdMap.put(subIdx, entityId); + log.trace("[{}][{}][{}] Creating alarms subscription for [{}] ", serviceId, cmdId, subIdx, entityId); + TbAlarmsSubscription subscription = TbAlarmsSubscription.builder() + .serviceId(serviceId) + .sessionId(sessionRef.getSessionId()) + .subscriptionId(subIdx) + .tenantId(sessionRef.getSecurityCtx().getTenantId()) + .entityId(entityId) + .updateProcessor((sub, update) -> fetchAlarmCount()) + .build(); + localSubscriptionService.addSubscription(subscription, sessionRef); + } + + public void clearAlarmSubscriptions() { + if (subToEntityIdMap != null) { + for (Integer subId : subToEntityIdMap.keySet()) { + localSubscriptionService.cancelSubscription(getTenantId(), getSessionId(), subId); + } + subToEntityIdMap.clear(); + } + } + } 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 e72e1dc806..d1b8bc0430 100644 --- a/application/src/test/java/org/thingsboard/server/controller/WebsocketApiTest.java +++ b/application/src/test/java/org/thingsboard/server/controller/WebsocketApiTest.java @@ -84,7 +84,7 @@ import static org.springframework.test.web.servlet.result.MockMvcResultMatchers. @DaoSqlTest @TestPropertySource(properties = { "server.ws.alarms_per_alarm_status_subscription_cache_size=5", - "server.ws.dynamic_page_link.refresh_interval=2" + "server.ws.dynamic_page_link.refresh_interval=3" }) public class WebsocketApiTest extends AbstractControllerTest { @Autowired @@ -422,7 +422,7 @@ public class WebsocketApiTest extends AbstractControllerTest { alarm = doPost("/api/alarm", alarm, Alarm.class); - update = getWsClient().parseAlarmCountReply(getWsClient().waitForUpdate(3000)); + update = getWsClient().parseAlarmCountReply(getWsClient().waitForUpdate(4000)); Assert.assertEquals(1, update.getCmdId()); Assert.assertEquals(1, update.getCount()); diff --git a/dao/src/main/java/org/thingsboard/server/dao/sql/query/DefaultAlarmQueryRepository.java b/dao/src/main/java/org/thingsboard/server/dao/sql/query/DefaultAlarmQueryRepository.java index 216fd0d2d4..db8c8ea552 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/sql/query/DefaultAlarmQueryRepository.java +++ b/dao/src/main/java/org/thingsboard/server/dao/sql/query/DefaultAlarmQueryRepository.java @@ -321,27 +321,35 @@ public class DefaultAlarmQueryRepository implements AlarmQueryRepository { if (query.isSearchPropagatedAlarms()) { ctx.append("select count(distinct(a.id)) from alarm_info a "); ctx.append(JOIN_ENTITY_ALARMS); - ctx.append("where a.tenant_id = :tenantId and ea.tenant_id = :tenantId"); - ctx.addUuidParameter("tenantId", tenantId.getId()); - if (customerId != null && !customerId.isNullUid()) { - ctx.append(" and a.customer_id = :customerId and ea.customer_id = :customerId"); - ctx.addUuidParameter("customerId", customerId.getId()); - } if (orderedEntityIds != null) { + if (orderedEntityIds.isEmpty()) { + return 0; + } ctx.addUuidListParameter("entity_filter_entity_ids", orderedEntityIds.stream().map(EntityId::getId).collect(Collectors.toList())); - ctx.append(" and ea.entity_id in (:entity_filter_entity_ids)"); + ctx.append("where ea.entity_id in (:entity_filter_entity_ids)"); + } else { + ctx.append("where a.tenant_id = :tenantId and ea.tenant_id = :tenantId"); + ctx.addUuidParameter("tenantId", tenantId.getId()); + if (customerId != null && !customerId.isNullUid()) { + ctx.append(" and a.customer_id = :customerId and ea.customer_id = :customerId"); + ctx.addUuidParameter("customerId", customerId.getId()); + } } } else { ctx.append("select count(id) from alarm_info a "); - ctx.append("where a.tenant_id = :tenantId"); - ctx.addUuidParameter("tenantId", tenantId.getId()); - if (customerId != null && !customerId.isNullUid()) { - ctx.append(" and a.customer_id = :customerId"); - ctx.addUuidParameter("customerId", customerId.getId()); - } if (orderedEntityIds != null) { + if (orderedEntityIds.isEmpty()) { + return 0; + } ctx.addUuidListParameter("entity_filter_entity_ids", orderedEntityIds.stream().map(EntityId::getId).collect(Collectors.toList())); - ctx.append(" and a.originator_id in (:entity_filter_entity_ids)"); + ctx.append("where a.originator_id in (:entity_filter_entity_ids)"); + } else { + ctx.append("where a.tenant_id = :tenantId"); + ctx.addUuidParameter("tenantId", tenantId.getId()); + if (customerId != null && !customerId.isNullUid()) { + ctx.append(" and a.customer_id = :customerId"); + ctx.addUuidParameter("customerId", customerId.getId()); + } } } From ca1185de541197d0276c5478f3abc59743aa32ae Mon Sep 17 00:00:00 2001 From: dashevchenko Date: Wed, 12 Feb 2025 17:02:49 +0200 Subject: [PATCH 060/345] do not send update if count was not changed --- ...efaultTbEntityDataSubscriptionService.java | 22 +++++++++++++------ .../subscription/TbAlarmCountSubCtx.java | 19 ++++++++++------ 2 files changed, 27 insertions(+), 14 deletions(-) diff --git a/application/src/main/java/org/thingsboard/server/service/subscription/DefaultTbEntityDataSubscriptionService.java b/application/src/main/java/org/thingsboard/server/service/subscription/DefaultTbEntityDataSubscriptionService.java index b09f8350f1..968711fa55 100644 --- a/application/src/main/java/org/thingsboard/server/service/subscription/DefaultTbEntityDataSubscriptionService.java +++ b/application/src/main/java/org/thingsboard/server/service/subscription/DefaultTbEntityDataSubscriptionService.java @@ -33,6 +33,7 @@ import org.springframework.stereotype.Service; import org.springframework.web.socket.CloseStatus; import org.thingsboard.common.util.ThingsBoardExecutors; import org.thingsboard.common.util.ThingsBoardThreadFactory; +import org.thingsboard.server.common.data.id.EntityId; import org.thingsboard.server.common.data.kv.BaseReadTsKvQuery; import org.thingsboard.server.common.data.kv.ReadTsKvQuery; import org.thingsboard.server.common.data.kv.ReadTsKvQueryResult; @@ -59,6 +60,7 @@ import org.thingsboard.server.service.ws.telemetry.cmd.v2.AggHistoryCmd; import org.thingsboard.server.service.ws.telemetry.cmd.v2.AggKey; import org.thingsboard.server.service.ws.telemetry.cmd.v2.AggTimeSeriesCmd; import org.thingsboard.server.service.ws.telemetry.cmd.v2.AlarmCountCmd; +import org.thingsboard.server.service.ws.telemetry.cmd.v2.AlarmCountUpdate; import org.thingsboard.server.service.ws.telemetry.cmd.v2.AlarmDataCmd; import org.thingsboard.server.service.ws.telemetry.cmd.v2.AlarmDataUpdate; import org.thingsboard.server.service.ws.telemetry.cmd.v2.AlarmStatusCmd; @@ -426,15 +428,21 @@ public class DefaultTbEntityDataSubscriptionService implements TbEntityDataSubsc long end = System.currentTimeMillis(); stats.getRegularQueryInvocationCnt().incrementAndGet(); stats.getRegularQueryTimeSpent().addAndGet(end - start); + Set entitiesIds = ctx.getEntitiesIds(); ctx.cancelTasks(); ctx.clearAlarmSubscriptions(); - ctx.fetchAlarmCount(); - ctx.createAlarmSubscriptions(); - TbAlarmCountSubCtx finalCtx = ctx; - ScheduledFuture task = scheduler.scheduleWithFixedDelay( - () -> refreshDynamicQuery(finalCtx), - dynamicPageLinkRefreshInterval, dynamicPageLinkRefreshInterval, TimeUnit.SECONDS); - finalCtx.setRefreshTask(task); + if (entitiesIds != null && entitiesIds.isEmpty()) { + AlarmCountUpdate update = new AlarmCountUpdate(cmd.getCmdId(), 0); + ctx.sendWsMsg(update); + } else { + ctx.doFetchAlarmCount(); + ctx.createAlarmSubscriptions(); + TbAlarmCountSubCtx finalCtx = ctx; + ScheduledFuture task = scheduler.scheduleWithFixedDelay( + () -> refreshDynamicQuery(finalCtx), + dynamicPageLinkRefreshInterval, dynamicPageLinkRefreshInterval, TimeUnit.SECONDS); + finalCtx.setRefreshTask(task); + } } else { log.debug("[{}][{}] Received duplicate command: {}", session.getSessionId(), cmd.getCmdId(), cmd); } diff --git a/application/src/main/java/org/thingsboard/server/service/subscription/TbAlarmCountSubCtx.java b/application/src/main/java/org/thingsboard/server/service/subscription/TbAlarmCountSubCtx.java index 712fc5c378..d0144b8787 100644 --- a/application/src/main/java/org/thingsboard/server/service/subscription/TbAlarmCountSubCtx.java +++ b/application/src/main/java/org/thingsboard/server/service/subscription/TbAlarmCountSubCtx.java @@ -48,6 +48,7 @@ public class TbAlarmCountSubCtx extends TbAbstractEntityQuerySubCtx subToEntityIdMap; + @Getter private LinkedHashSet entitiesIds; private final int maxEntitiesPerAlarmSubscription; @@ -102,26 +103,30 @@ public class TbAlarmCountSubCtx extends TbAbstractEntityQuerySubCtx Date: Thu, 13 Feb 2025 11:28:42 +0200 Subject: [PATCH 061/345] fixed flaky tests --- .../server/controller/WebsocketApiTest.java | 45 +++---------------- 1 file changed, 7 insertions(+), 38 deletions(-) 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 d1b8bc0430..f8a9db43d0 100644 --- a/application/src/test/java/org/thingsboard/server/controller/WebsocketApiTest.java +++ b/application/src/test/java/org/thingsboard/server/controller/WebsocketApiTest.java @@ -83,8 +83,7 @@ import static org.springframework.test.web.servlet.result.MockMvcResultMatchers. @Slf4j @DaoSqlTest @TestPropertySource(properties = { - "server.ws.alarms_per_alarm_status_subscription_cache_size=5", - "server.ws.dynamic_page_link.refresh_interval=3" + "server.ws.alarms_per_alarm_status_subscription_cache_size=5" }) public class WebsocketApiTest extends AbstractControllerTest { @Autowired @@ -340,6 +339,7 @@ public class WebsocketApiTest extends AbstractControllerTest { Assert.assertEquals(1, update.getCmdId()); Assert.assertEquals(0, update.getCount()); + //create alarm, check count = 1 Alarm alarm = new Alarm(); alarm.setOriginator(tenantId); alarm.setType("TEST ALARM"); @@ -355,6 +355,7 @@ public class WebsocketApiTest extends AbstractControllerTest { Assert.assertEquals(2, update.getCmdId()); Assert.assertEquals(1, update.getCount()); + // set wrong entity id in filter, check count = 0 singleEntityFilter.setSingleEntity(tenantAdminUserId); AlarmCountCmd cmd3 = new AlarmCountCmd(3, alarmCountQuery); @@ -363,39 +364,6 @@ public class WebsocketApiTest extends AbstractControllerTest { update = getWsClient().parseAlarmCountReply(getWsClient().waitForReply()); Assert.assertEquals(3, update.getCmdId()); Assert.assertEquals(0, update.getCount()); - - alarm.setAssigneeId(tenantAdminUserId); - alarm = doPost("/api/alarm", alarm, Alarm.class); - - singleEntityFilter.setSingleEntity(tenantId); - alarmCountQuery.setAssigneeId(tenantAdminUserId); - AlarmCountCmd cmd4 = new AlarmCountCmd(4, alarmCountQuery); - - getWsClient().send(cmd4); - - update = getWsClient().parseAlarmCountReply(getWsClient().waitForReply()); - Assert.assertEquals(4, update.getCmdId()); - Assert.assertEquals(1, update.getCount()); - - alarmCountQuery.setSeverityList(Collections.singletonList(AlarmSeverity.CRITICAL)); - AlarmCountCmd cmd5 = new AlarmCountCmd(5, alarmCountQuery); - - getWsClient().send(cmd5); - - update = getWsClient().parseAlarmCountReply(getWsClient().waitForReply()); - Assert.assertEquals(5, update.getCmdId()); - Assert.assertEquals(0, update.getCount()); - - alarm.setSeverity(AlarmSeverity.CRITICAL); - doPost("/api/alarm", alarm, Alarm.class); - - AlarmCountCmd cmd6 = new AlarmCountCmd(6, alarmCountQuery); - - getWsClient().send(cmd6); - - update = getWsClient().parseAlarmCountReply(getWsClient().waitForReply()); - Assert.assertEquals(6, update.getCmdId()); - Assert.assertEquals(1, update.getCount()); } @Test @@ -422,7 +390,7 @@ public class WebsocketApiTest extends AbstractControllerTest { alarm = doPost("/api/alarm", alarm, Alarm.class); - update = getWsClient().parseAlarmCountReply(getWsClient().waitForUpdate(4000)); + update = getWsClient().parseAlarmCountReply(getWsClient().waitForUpdate()); Assert.assertEquals(1, update.getCmdId()); Assert.assertEquals(1, update.getCount()); @@ -484,17 +452,18 @@ public class WebsocketApiTest extends AbstractControllerTest { doPost("/api/alarm", alarm2, Alarm.class); - AlarmStatusUpdate alarmStatusUpdate3 = JacksonUtil.fromString(getWsClient().waitForReply(), AlarmStatusUpdate.class); + AlarmStatusUpdate alarmStatusUpdate3 = JacksonUtil.fromString(getWsClient().waitForUpdate(), AlarmStatusUpdate.class); Assert.assertEquals(1, alarmStatusUpdate3.getCmdId()); Assert.assertTrue(alarmStatusUpdate3.isActive()); //change severity + getWsClient().registerWaitForUpdate(); alarm2.setSeverity(AlarmSeverity.MAJOR); Alarm updatedAlarm = doPost("/api/alarm", alarm2, Alarm.class); Assert.assertNotNull(updatedAlarm); Assert.assertEquals(AlarmSeverity.MAJOR, updatedAlarm.getSeverity()); - AlarmStatusUpdate alarmStatusUpdate4 = JacksonUtil.fromString(getWsClient().waitForReply(), AlarmStatusUpdate.class); + AlarmStatusUpdate alarmStatusUpdate4 = JacksonUtil.fromString(getWsClient().waitForUpdate(), AlarmStatusUpdate.class); Assert.assertEquals(1, alarmStatusUpdate4.getCmdId()); Assert.assertFalse(alarmStatusUpdate4.isActive()); From bb7ea08e59f1c5300229eb92451532e37cb63751 Mon Sep 17 00:00:00 2001 From: ViacheslavKlimov Date: Fri, 14 Feb 2025 12:08:05 +0200 Subject: [PATCH 062/345] EDQS: refactoring, OOM handling, healthcheck --- .../TbRuleEngineQueueConsumerManager.java | 2 +- application/src/main/resources/logback.xml | 1 + .../EdqsEntityQueryControllerTest.java | 3 +- .../server/common/data/ObjectType.java | 8 +- common/edqs/pom.xml | 4 + .../server/edqs/processor/EdqsProcessor.java | 30 +++++-- .../server/edqs/processor/EdqsProducer.java | 2 +- .../server/edqs/repo/TenantRepo.java | 16 ++-- .../server/edqs/state/EdqsController.java | 40 +++++++++ .../server/edqs/state/EdqsStateService.java | 2 + .../edqs/state/KafkaEdqsStateService.java | 85 +++++++++++-------- .../edqs/state/LocalEdqsStateService.java | 12 ++- .../server/edqs/stats/EdqsStatsService.java | 6 +- .../consumer/MainQueueConsumerManager.java | 52 +++++++----- .../common/consumer/TbQueueConsumerTask.java | 2 +- .../kafka/TbKafkaConsumerStatsService.java | 13 +-- 16 files changed, 179 insertions(+), 99 deletions(-) create mode 100644 common/edqs/src/main/java/org/thingsboard/server/edqs/state/EdqsController.java diff --git a/application/src/main/java/org/thingsboard/server/service/queue/ruleengine/TbRuleEngineQueueConsumerManager.java b/application/src/main/java/org/thingsboard/server/service/queue/ruleengine/TbRuleEngineQueueConsumerManager.java index 3636eb05af..1f421974be 100644 --- a/application/src/main/java/org/thingsboard/server/service/queue/ruleengine/TbRuleEngineQueueConsumerManager.java +++ b/application/src/main/java/org/thingsboard/server/service/queue/ruleengine/TbRuleEngineQueueConsumerManager.java @@ -72,7 +72,7 @@ public class TbRuleEngineQueueConsumerManager extends MainQueueConsumerManager + diff --git a/application/src/test/java/org/thingsboard/server/controller/EdqsEntityQueryControllerTest.java b/application/src/test/java/org/thingsboard/server/controller/EdqsEntityQueryControllerTest.java index 9bb0bbe30a..1add0dae37 100644 --- a/application/src/test/java/org/thingsboard/server/controller/EdqsEntityQueryControllerTest.java +++ b/application/src/test/java/org/thingsboard/server/controller/EdqsEntityQueryControllerTest.java @@ -28,7 +28,6 @@ import org.thingsboard.server.dao.service.DaoSqlTest; import org.thingsboard.server.edqs.util.EdqsRocksDb; import java.util.concurrent.TimeUnit; -import java.util.function.BiPredicate; import static org.awaitility.Awaitility.await; @@ -45,7 +44,7 @@ public class EdqsEntityQueryControllerTest extends EntityQueryControllerTest { @Autowired private EdqsService edqsService; - @MockBean + @MockBean // so that we don't do backup for tests private EdqsRocksDb edqsRocksDb; @Before diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/ObjectType.java b/common/data/src/main/java/org/thingsboard/server/common/data/ObjectType.java index bc5ec58213..86252fb7f8 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/ObjectType.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/ObjectType.java @@ -15,9 +15,8 @@ */ package org.thingsboard.server.common.data; -import java.util.Arrays; import java.util.EnumSet; -import java.util.HashSet; +import java.util.List; import java.util.Set; public enum ObjectType { @@ -68,12 +67,13 @@ public enum ObjectType { TENANT, TENANT_PROFILE, CUSTOMER, DEVICE_PROFILE, DEVICE, ASSET_PROFILE, ASSET, EDGE, ENTITY_VIEW, USER, DASHBOARD, RULE_CHAIN, WIDGET_TYPE, WIDGETS_BUNDLE, API_USAGE_STATE, QUEUE_STATS ); - public static final Set edqsTypes = new HashSet<>(edqsTenantTypes); + public static final Set edqsTypes = EnumSet.copyOf(edqsTenantTypes); public static final Set edqsSystemTypes = EnumSet.of(TENANT, TENANT_PROFILE, USER, DASHBOARD, API_USAGE_STATE, ATTRIBUTE_KV, LATEST_TS_KV); + public static final Set unversionedTypes = EnumSet.of(QUEUE_STATS); static { - edqsTypes.addAll(Arrays.asList(RELATION, ATTRIBUTE_KV, LATEST_TS_KV)); + edqsTypes.addAll(List.of(RELATION, ATTRIBUTE_KV, LATEST_TS_KV)); } public EntityType toEntityType() { diff --git a/common/edqs/pom.xml b/common/edqs/pom.xml index e58c7c97a0..2abd1286b8 100644 --- a/common/edqs/pom.xml +++ b/common/edqs/pom.xml @@ -68,6 +68,10 @@ org.thingsboard.common queue + + org.springframework.boot + spring-boot-starter-web + org.apache.kafka kafka-clients diff --git a/common/edqs/src/main/java/org/thingsboard/server/edqs/processor/EdqsProcessor.java b/common/edqs/src/main/java/org/thingsboard/server/edqs/processor/EdqsProcessor.java index 6fe09f77d1..a90a029bc4 100644 --- a/common/edqs/src/main/java/org/thingsboard/server/edqs/processor/EdqsProcessor.java +++ b/common/edqs/src/main/java/org/thingsboard/server/edqs/processor/EdqsProcessor.java @@ -21,10 +21,12 @@ import com.google.common.util.concurrent.ListeningExecutorService; import com.google.common.util.concurrent.MoreExecutors; import jakarta.annotation.PostConstruct; import jakarta.annotation.PreDestroy; +import lombok.Getter; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.apache.commons.lang3.exception.ExceptionUtils; import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.context.ConfigurableApplicationContext; import org.springframework.context.annotation.Lazy; import org.springframework.context.event.EventListener; import org.springframework.stereotype.Service; @@ -72,6 +74,7 @@ import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; import java.util.concurrent.ScheduledExecutorService; import java.util.concurrent.atomic.AtomicInteger; +import java.util.function.Consumer; import java.util.stream.Collectors; @EdqsComponent @@ -85,8 +88,8 @@ public class EdqsProcessor implements TbQueueHandler, private final EdqRepository repository; private final EdqsConfig config; private final EdqsPartitionService partitionService; - @Autowired - @Lazy + private final ConfigurableApplicationContext applicationContext; + @Autowired @Lazy private EdqsStateService stateService; private MainQueueConsumerManager, QueueConfig> eventsConsumer; @@ -96,17 +99,30 @@ public class EdqsProcessor implements TbQueueHandler, private ExecutorService mgmtExecutor; private ScheduledExecutorService scheduler; private ListeningExecutorService requestExecutor; + private ExecutorService repartitionExecutor; private final VersionsStore versionsStore = new VersionsStore(); private final AtomicInteger counter = new AtomicInteger(); // FIXME: TMP + @Getter + private Consumer errorHandler; + @PostConstruct private void init() { consumersExecutor = Executors.newCachedThreadPool(ThingsBoardThreadFactory.forName("edqs-consumer")); mgmtExecutor = ThingsBoardExecutors.newWorkStealingPool(4, "edqs-consumer-mgmt"); scheduler = ThingsBoardExecutors.newSingleThreadScheduledExecutor("edqs-scheduler"); requestExecutor = MoreExecutors.listeningDecorator(ThingsBoardExecutors.newWorkStealingPool(12, "edqs-requests")); + repartitionExecutor = Executors.newSingleThreadExecutor(ThingsBoardThreadFactory.forName("edqs-repartition")); + errorHandler = error -> { + if (error instanceof OutOfMemoryError) { + log.error("OOM detected, shutting down"); + repository.clear(); + Executors.newSingleThreadExecutor(ThingsBoardThreadFactory.forName("edqs-shutdown")) + .execute(applicationContext::close); + } + }; eventsConsumer = MainQueueConsumerManager., QueueConfig>builder() .queueKey(new QueueKey(ServiceType.EDQS, EdqsQueue.EVENTS.getTopic())) @@ -117,7 +133,7 @@ public class EdqsProcessor implements TbQueueHandler, ToEdqsMsg msg = queueMsg.getValue(); log.trace("Processing message: {}", msg); process(msg, EdqsQueue.EVENTS); - } catch (Throwable t) { + } catch (Exception t) { log.error("Failed to process message: {}", queueMsg, t); } } @@ -127,6 +143,7 @@ public class EdqsProcessor implements TbQueueHandler, .consumerExecutor(consumersExecutor) .taskExecutor(mgmtExecutor) .scheduler(scheduler) + .uncaughtErrorHandler(errorHandler) .build(); responseTemplate = queueFactory.createEdqsResponseTemplate(); } @@ -141,7 +158,7 @@ public class EdqsProcessor implements TbQueueHandler, if (event.getServiceType() != ServiceType.EDQS) { return; } - consumersExecutor.submit(() -> { + repartitionExecutor.submit(() -> { // todo: maybe cancel the task if new event comes try { Set newPartitions = event.getNewPartitions().get(new QueueKey(ServiceType.EDQS)); Set partitions = newPartitions.stream() @@ -220,8 +237,8 @@ public class EdqsProcessor implements TbQueueHandler, if (!versionsStore.isNew(key, version)) { return; } - } else { - log.warn("[{}] {} doesn't have version: {}", tenantId, objectType, edqsMsg); + } else if (!ObjectType.unversionedTypes.contains(objectType)) { + log.warn("[{}] {} {} doesn't have version", tenantId, objectType, key); } if (queue != EdqsQueue.STATE) { stateService.save(tenantId, objectType, key, eventType, edqsMsg); @@ -272,6 +289,7 @@ public class EdqsProcessor implements TbQueueHandler, mgmtExecutor.shutdownNow(); scheduler.shutdownNow(); requestExecutor.shutdownNow(); + repartitionExecutor.shutdownNow(); } } diff --git a/common/edqs/src/main/java/org/thingsboard/server/edqs/processor/EdqsProducer.java b/common/edqs/src/main/java/org/thingsboard/server/edqs/processor/EdqsProducer.java index b21184110f..9b1e925367 100644 --- a/common/edqs/src/main/java/org/thingsboard/server/edqs/processor/EdqsProducer.java +++ b/common/edqs/src/main/java/org/thingsboard/server/edqs/processor/EdqsProducer.java @@ -53,7 +53,7 @@ public class EdqsProducer { TbQueueCallback callback = new TbQueueCallback() { @Override public void onSuccess(TbQueueMsgMetadata metadata) { - log.debug("[{}][{}][{}] Published msg to {}: {}", tenantId, type, key, topic, msg); // fixme log levels + log.trace("[{}][{}][{}] Published msg to {}: {}", tenantId, type, key, topic, msg); // fixme log levels } @Override diff --git a/common/edqs/src/main/java/org/thingsboard/server/edqs/repo/TenantRepo.java b/common/edqs/src/main/java/org/thingsboard/server/edqs/repo/TenantRepo.java index 68e1c42589..822061e918 100644 --- a/common/edqs/src/main/java/org/thingsboard/server/edqs/repo/TenantRepo.java +++ b/common/edqs/src/main/java/org/thingsboard/server/edqs/repo/TenantRepo.java @@ -153,7 +153,7 @@ public class TenantRepo { EntityData to = getOrCreate(entity.getTo()); boolean added = repo.add(from, to, TbStringPool.intern(entity.getType())); if (added) { - edqsStatsService.ifPresent(statService -> statService.reportTenantEdqsObject(tenantId, ObjectType.RELATION, EdqsEventType.UPDATED)); + edqsStatsService.ifPresent(statService -> statService.reportEvent(tenantId, ObjectType.RELATION, EdqsEventType.UPDATED)); } } else if (RelationTypeGroup.DASHBOARD.equals(entity.getTypeGroup())) { if (EntityRelation.CONTAINS_TYPE.equals(entity.getType()) && entity.getFrom().getEntityType() == EntityType.CUSTOMER) { @@ -172,7 +172,7 @@ public class TenantRepo { if (relationsRepo != null) { boolean removed = relationsRepo.remove(entityRelation.getFrom().getId(), entityRelation.getTo().getId(), entityRelation.getType()); if (removed) { - edqsStatsService.ifPresent(statService -> statService.reportTenantEdqsObject(tenantId, ObjectType.RELATION, EdqsEventType.DELETED)); + edqsStatsService.ifPresent(statService -> statService.reportEvent(tenantId, ObjectType.RELATION, EdqsEventType.DELETED)); } } } else if (RelationTypeGroup.DASHBOARD.equals(entityRelation.getTypeGroup())) { @@ -223,7 +223,7 @@ public class TenantRepo { EntityData removed = getEntityMap(entityType).remove(entityId); if (removed != null) { getEntitySet(entityType).remove(removed); - edqsStatsService.ifPresent(statService -> statService.reportTenantEdqsObject(tenantId, ObjectType.fromEntityType(entityType), EdqsEventType.DELETED)); + edqsStatsService.ifPresent(statService -> statService.reportEvent(tenantId, ObjectType.fromEntityType(entityType), EdqsEventType.DELETED)); UUID customerId = removed.getCustomerId(); if (customerId != null) { CustomerData customerData = (CustomerData) getEntityMap(EntityType.CUSTOMER).get(customerId); @@ -244,7 +244,7 @@ public class TenantRepo { Integer keyId = KeyDictionary.get(attributeKv.getKey()); boolean added = entityData.putAttr(keyId, attributeKv.getScope(), toDataPoint(attributeKv.getLastUpdateTs(), value)); if (added) { - edqsStatsService.ifPresent(statService -> statService.reportTenantEdqsObject(tenantId, ObjectType.ATTRIBUTE_KV, EdqsEventType.UPDATED)); + edqsStatsService.ifPresent(statService -> statService.reportEvent(tenantId, ObjectType.ATTRIBUTE_KV, EdqsEventType.UPDATED)); } } } @@ -254,7 +254,7 @@ public class TenantRepo { if (entityData != null) { boolean removed = entityData.removeAttr(KeyDictionary.get(attributeKv.getKey()), attributeKv.getScope()); if (removed) { - edqsStatsService.ifPresent(statService -> statService.reportTenantEdqsObject(tenantId, ObjectType.ATTRIBUTE_KV, EdqsEventType.DELETED)); + edqsStatsService.ifPresent(statService -> statService.reportEvent(tenantId, ObjectType.ATTRIBUTE_KV, EdqsEventType.DELETED)); } } } @@ -266,7 +266,7 @@ public class TenantRepo { Integer keyId = KeyDictionary.get(latestTsKv.getKey()); boolean added = entityData.putTs(keyId, toDataPoint(latestTsKv.getTs(), value)); if (added) { - edqsStatsService.ifPresent(statService -> statService.reportTenantEdqsObject(tenantId, ObjectType.LATEST_TS_KV, EdqsEventType.UPDATED)); + edqsStatsService.ifPresent(statService -> statService.reportEvent(tenantId, ObjectType.LATEST_TS_KV, EdqsEventType.UPDATED)); } } } @@ -276,7 +276,7 @@ public class TenantRepo { if (entityData != null) { boolean removed = entityData.removeTs(KeyDictionary.get(latestTsKv.getKey())); if (removed) { - edqsStatsService.ifPresent(statService -> statService.reportTenantEdqsObject(tenantId, ObjectType.LATEST_TS_KV, EdqsEventType.DELETED)); + edqsStatsService.ifPresent(statService -> statService.reportEvent(tenantId, ObjectType.LATEST_TS_KV, EdqsEventType.DELETED)); } } } @@ -331,7 +331,7 @@ public class TenantRepo { log.debug("[{}] Adding {} {}", tenantId, entityType, id); EntityData entityData = constructEntityData(entityType, entityId); getEntitySet(entityType).add(entityData); - edqsStatsService.ifPresent(statService -> statService.reportTenantEdqsObject(tenantId, ObjectType.fromEntityType(entityType), EdqsEventType.UPDATED)); + edqsStatsService.ifPresent(statService -> statService.reportEvent(tenantId, ObjectType.fromEntityType(entityType), EdqsEventType.UPDATED)); return entityData; }); } diff --git a/common/edqs/src/main/java/org/thingsboard/server/edqs/state/EdqsController.java b/common/edqs/src/main/java/org/thingsboard/server/edqs/state/EdqsController.java new file mode 100644 index 0000000000..721675245a --- /dev/null +++ b/common/edqs/src/main/java/org/thingsboard/server/edqs/state/EdqsController.java @@ -0,0 +1,40 @@ +/** + * Copyright © 2016-2024 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.edqs.state; + +import lombok.RequiredArgsConstructor; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +@RestController +@RequiredArgsConstructor +@RequestMapping("/api/edqs") +public class EdqsController { + + private final EdqsStateService edqsStateService; + + @GetMapping("/ready") + public ResponseEntity isReady() { + if (edqsStateService.isReady()) { + return ResponseEntity.ok().build(); + } else { + return ResponseEntity.badRequest().build(); + } + } + +} diff --git a/common/edqs/src/main/java/org/thingsboard/server/edqs/state/EdqsStateService.java b/common/edqs/src/main/java/org/thingsboard/server/edqs/state/EdqsStateService.java index 8866a1c771..06d080ac64 100644 --- a/common/edqs/src/main/java/org/thingsboard/server/edqs/state/EdqsStateService.java +++ b/common/edqs/src/main/java/org/thingsboard/server/edqs/state/EdqsStateService.java @@ -29,4 +29,6 @@ public interface EdqsStateService { void save(TenantId tenantId, ObjectType type, String key, EdqsEventType eventType, ToEdqsMsg msg); + boolean isReady(); + } diff --git a/common/edqs/src/main/java/org/thingsboard/server/edqs/state/KafkaEdqsStateService.java b/common/edqs/src/main/java/org/thingsboard/server/edqs/state/KafkaEdqsStateService.java index 84f3cfd22f..cfa831f720 100644 --- a/common/edqs/src/main/java/org/thingsboard/server/edqs/state/KafkaEdqsStateService.java +++ b/common/edqs/src/main/java/org/thingsboard/server/edqs/state/KafkaEdqsStateService.java @@ -42,10 +42,10 @@ import org.thingsboard.server.queue.edqs.EdqsConfig; import org.thingsboard.server.queue.edqs.EdqsQueue; import org.thingsboard.server.queue.edqs.EdqsQueueFactory; import org.thingsboard.server.queue.edqs.KafkaEdqsComponent; -import org.thingsboard.server.queue.util.AfterStartUp; import java.util.Set; import java.util.UUID; +import java.util.concurrent.CountDownLatch; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; import java.util.concurrent.ScheduledExecutorService; @@ -66,6 +66,8 @@ public class KafkaEdqsStateService implements EdqsStateService { private QueueConsumerManager> eventsConsumer; private EdqsProducer stateProducer; + private boolean initialRestoreDone; + private ExecutorService consumersExecutor; private ExecutorService mgmtExecutor; private ScheduledExecutorService scheduler; @@ -76,7 +78,7 @@ public class KafkaEdqsStateService implements EdqsStateService { @PostConstruct private void init() { - consumersExecutor = Executors.newCachedThreadPool(ThingsBoardThreadFactory.forName("edqs-backup-consumer")); + consumersExecutor = Executors.newCachedThreadPool(ThingsBoardThreadFactory.forName("edqs-consumer")); mgmtExecutor = ThingsBoardExecutors.newWorkStealingPool(4, "edqs-backup-consumer-mgmt"); scheduler = ThingsBoardExecutors.newSingleThreadScheduledExecutor("edqs-backup-scheduler"); @@ -92,8 +94,8 @@ public class KafkaEdqsStateService implements EdqsStateService { if (stateReadCount.incrementAndGet() % 100000 == 0) { log.info("[state] Processed {} msgs", stateReadCount.get()); } - } catch (Throwable t) { - log.error("Failed to process message: {}", queueMsg, t); + } catch (Exception e) { + log.error("Failed to process message: {}", queueMsg, e); // TODO: do something about the error - e.g. reprocess } } consumer.commit(); @@ -102,39 +104,48 @@ public class KafkaEdqsStateService implements EdqsStateService { .consumerExecutor(consumersExecutor) .taskExecutor(mgmtExecutor) .scheduler(scheduler) + .uncaughtErrorHandler(edqsProcessor.getErrorHandler()) .build(); - eventsConsumer = QueueConsumerManager.>builder() // FIXME Slavik writes to the state while we read it, slows down the start + ExecutorService backupExecutor = ThingsBoardExecutors.newLimitedTasksExecutor(12, 1000, "events-to-backup-executor"); + eventsConsumer = QueueConsumerManager.>builder() // FIXME Slavik writes to the state while we read it, slows down the start. maybe start backup consumer after restore is finished .name("edqs-events-to-backup-consumer") .pollInterval(config.getPollInterval()) .msgPackProcessor((msgs, consumer) -> { + CountDownLatch resultLatch = new CountDownLatch(msgs.size()); for (TbProtoQueueMsg queueMsg : msgs) { - try { - ToEdqsMsg msg = queueMsg.getValue(); - log.trace("Processing message: {}", msg); - - if (msg.hasEventMsg()) { - EdqsEventMsg eventMsg = msg.getEventMsg(); - String key = eventMsg.getKey(); - if (eventsReadCount.incrementAndGet() % 100000 == 0) { - log.info("[events-to-backup] Processed {} msgs", eventsReadCount.get()); - } - if (eventMsg.hasVersion()) { - if (!versionsStore.isNew(key, eventMsg.getVersion())) { - return; + backupExecutor.submit(() -> { + try { + ToEdqsMsg msg = queueMsg.getValue(); + log.trace("Processing message: {}", msg); + + if (msg.hasEventMsg()) { + EdqsEventMsg eventMsg = msg.getEventMsg(); + String key = eventMsg.getKey(); + int count = eventsReadCount.incrementAndGet(); + if (count % 100000 == 0) { + log.info("[events-to-backup] Processed {} msgs", count); + } + if (eventMsg.hasVersion()) { + if (!versionsStore.isNew(key, eventMsg.getVersion())) { + return; + } } - } - TenantId tenantId = getTenantId(msg); - ObjectType objectType = ObjectType.valueOf(eventMsg.getObjectType()); - EdqsEventType eventType = EdqsEventType.valueOf(eventMsg.getEventType()); - log.debug("[{}] Saving to backup [{}] [{}] [{}]", tenantId, objectType, eventType, key); - stateProducer.send(tenantId, objectType, key, msg); + TenantId tenantId = getTenantId(msg); + ObjectType objectType = ObjectType.valueOf(eventMsg.getObjectType()); + EdqsEventType eventType = EdqsEventType.valueOf(eventMsg.getEventType()); + log.debug("[{}] Saving to backup [{}] [{}] [{}]", tenantId, objectType, eventType, key); + stateProducer.send(tenantId, objectType, key, msg); + } + } catch (Throwable t) { + log.error("Failed to process message: {}", queueMsg, t); + } finally { + resultLatch.countDown(); } - } catch (Throwable t) { - log.error("Failed to process message: {}", queueMsg, t); - } + }); } + resultLatch.await(); consumer.commit(); }) .consumerCreator(() -> queueFactory.createEdqsMsgConsumer(EdqsQueue.EVENTS, "events-to-backup-consumer-group")) // shared by all instances consumer group @@ -149,22 +160,21 @@ public class KafkaEdqsStateService implements EdqsStateService { .build(); } - @AfterStartUp(order = AfterStartUp.REGULAR_SERVICE) - public void afterStartUp() { - eventsConsumer.subscribe(); - eventsConsumer.launch(); - } - @Override public void restore(Set partitions) { stateReadCount.set(0); //TODO Slavik: do not support remote mode in monolith setup long startTs = System.currentTimeMillis(); log.info("Restore started for partitions {}", partitions.stream().map(tpi -> tpi.getPartition().orElse(-1)).sorted().toList()); - stateConsumer.doUpdate(partitions); // calling blocking doUpdate instead of update stateConsumer.awaitStop(0); // consumers should stop on their own because EdqsQueue.STATE.stopWhenRead is true, we just need to wait - log.info("Restore finished in {} ms. Processed {} msgs", (System.currentTimeMillis() - startTs), stateReadCount.get()); + + if (!initialRestoreDone) { + initialRestoreDone = true; + + eventsConsumer.subscribe(); + eventsConsumer.launch(); + } } @Override @@ -172,6 +182,11 @@ public class KafkaEdqsStateService implements EdqsStateService { // do nothing here, backup is done by events consumer } + @Override + public boolean isReady() { + return initialRestoreDone; + } + private TenantId getTenantId(ToEdqsMsg edqsMsg) { return TenantId.fromUUID(new UUID(edqsMsg.getTenantIdMSB(), edqsMsg.getTenantIdLSB())); } diff --git a/common/edqs/src/main/java/org/thingsboard/server/edqs/state/LocalEdqsStateService.java b/common/edqs/src/main/java/org/thingsboard/server/edqs/state/LocalEdqsStateService.java index 8d2d563d90..6a61488182 100644 --- a/common/edqs/src/main/java/org/thingsboard/server/edqs/state/LocalEdqsStateService.java +++ b/common/edqs/src/main/java/org/thingsboard/server/edqs/state/LocalEdqsStateService.java @@ -43,13 +43,11 @@ public class LocalEdqsStateService implements EdqsStateService { @Autowired private EdqsRocksDb db; - private Set partitions; + private boolean restoreDone; @Override public void restore(Set partitions) { - if (this.partitions == null) { - this.partitions = partitions; - } else { + if (restoreDone) { return; } @@ -62,6 +60,7 @@ public class LocalEdqsStateService implements EdqsStateService { log.error("[{}] Failed to restore value", key, e); } }); + restoreDone = true; } @Override @@ -78,4 +77,9 @@ public class LocalEdqsStateService implements EdqsStateService { } } + @Override + public boolean isReady() { + return restoreDone; + } + } diff --git a/common/edqs/src/main/java/org/thingsboard/server/edqs/stats/EdqsStatsService.java b/common/edqs/src/main/java/org/thingsboard/server/edqs/stats/EdqsStatsService.java index 2ca036d23d..9619d086ad 100644 --- a/common/edqs/src/main/java/org/thingsboard/server/edqs/stats/EdqsStatsService.java +++ b/common/edqs/src/main/java/org/thingsboard/server/edqs/stats/EdqsStatsService.java @@ -52,9 +52,9 @@ public class EdqsStatsService { log.info("EDQS Stats: {}", values); } - public void reportTenantEdqsObject(TenantId tenantId, ObjectType objectType, EdqsEventType eventType) { + public void reportEvent(TenantId tenantId, ObjectType objectType, EdqsEventType eventType) { statsMap.computeIfAbsent(tenantId, id -> new EdqsStats(tenantId, statsFactory)) - .reportEdqsObject(objectType, eventType); + .reportEvent(objectType, eventType); } @Getter @@ -78,7 +78,7 @@ public class EdqsStatsService { .collect(Collectors.joining(", ")); } - public void reportEdqsObject(ObjectType objectType, EdqsEventType eventType) { + public void reportEvent(ObjectType objectType, EdqsEventType eventType) { AtomicInteger objectCounter = getOrCreateObjectCounter(objectType); if (eventType == EdqsEventType.UPDATED){ objectCounter.incrementAndGet(); diff --git a/common/queue/src/main/java/org/thingsboard/server/queue/common/consumer/MainQueueConsumerManager.java b/common/queue/src/main/java/org/thingsboard/server/queue/common/consumer/MainQueueConsumerManager.java index 2f44a9c4ae..9d45b168ee 100644 --- a/common/queue/src/main/java/org/thingsboard/server/queue/common/consumer/MainQueueConsumerManager.java +++ b/common/queue/src/main/java/org/thingsboard/server/queue/common/consumer/MainQueueConsumerManager.java @@ -39,6 +39,7 @@ import java.util.concurrent.ScheduledExecutorService; import java.util.concurrent.TimeUnit; import java.util.concurrent.locks.ReentrantLock; import java.util.function.BiFunction; +import java.util.function.Consumer; import java.util.stream.Collectors; @Slf4j @@ -53,6 +54,7 @@ public class MainQueueConsumerManager uncaughtErrorHandler; private final java.util.Queue tasks = new ConcurrentLinkedQueue<>(); private final ReentrantLock lock = new ReentrantLock(); @@ -68,7 +70,8 @@ public class MainQueueConsumerManager> consumerCreator, ExecutorService consumerExecutor, ScheduledExecutorService scheduler, - ExecutorService taskExecutor) { + ExecutorService taskExecutor, + Consumer uncaughtErrorHandler) { this.queueKey = queueKey; this.config = config; this.msgPackProcessor = msgPackProcessor; @@ -76,6 +79,7 @@ public class MainQueueConsumerManager consumerLoop = consumerExecutor.submit(() -> { ThingsBoardThreadFactory.updateCurrentThreadName(consumerTask.getKey().toString()); - try { - consumerLoop(consumerTask.getConsumer()); - } catch (Throwable e) { - log.error("Failure in consumer loop", e); - } + consumerLoop(consumerTask.getConsumer()); log.info("[{}] Consumer stopped", consumerTask.getKey()); }); consumerTask.setTask(consumerLoop); } private void consumerLoop(TbQueueConsumer consumer) { - while (!stopped && !consumer.isStopped()) { - try { - List msgs = consumer.poll(config.getPollInterval()); - if (msgs.isEmpty()) { - continue; - } - processMsgs(msgs, consumer, config); - } catch (Exception e) { - if (!consumer.isStopped()) { - log.warn("Failed to process messages from queue", e); - try { - Thread.sleep(config.getPollInterval()); - } catch (InterruptedException e2) { - log.trace("Failed to wait until the server has capacity to handle new requests", e2); + try { + while (!stopped && !consumer.isStopped()) { + try { + List msgs = consumer.poll(config.getPollInterval()); + if (msgs.isEmpty()) { + continue; + } + processMsgs(msgs, consumer, config); + } catch (Exception e) { + if (!consumer.isStopped()) { + log.warn("Failed to process messages from queue", e); + try { + Thread.sleep(config.getPollInterval()); + } catch (InterruptedException e2) { + log.trace("Failed to wait until the server has capacity to handle new requests", e2); + } } } } - } - if (consumer.isStopped()) { + if (consumer.isStopped()) { + consumer.unsubscribe(); + } + } catch (Throwable t) { + log.error("Failure in consumer loop", t); + if (uncaughtErrorHandler != null) { + uncaughtErrorHandler.accept(t); + } consumer.unsubscribe(); } } diff --git a/common/queue/src/main/java/org/thingsboard/server/queue/common/consumer/TbQueueConsumerTask.java b/common/queue/src/main/java/org/thingsboard/server/queue/common/consumer/TbQueueConsumerTask.java index 708e24fdac..4ed0ffa497 100644 --- a/common/queue/src/main/java/org/thingsboard/server/queue/common/consumer/TbQueueConsumerTask.java +++ b/common/queue/src/main/java/org/thingsboard/server/queue/common/consumer/TbQueueConsumerTask.java @@ -84,7 +84,7 @@ public class TbQueueConsumerTask { } log.trace("[{}] Awaited finish", key); } catch (Exception e) { - log.warn("[{}] Failed to await for consumer to stop", key, e); + log.warn("[{}] Failed to await for consumer to stop (timeout {} sec)", key, timeoutSec, e); } task = null; } diff --git a/common/queue/src/main/java/org/thingsboard/server/queue/kafka/TbKafkaConsumerStatsService.java b/common/queue/src/main/java/org/thingsboard/server/queue/kafka/TbKafkaConsumerStatsService.java index 0367a2be78..ade9204572 100644 --- a/common/queue/src/main/java/org/thingsboard/server/queue/kafka/TbKafkaConsumerStatsService.java +++ b/common/queue/src/main/java/org/thingsboard/server/queue/kafka/TbKafkaConsumerStatsService.java @@ -26,15 +26,10 @@ import org.apache.kafka.clients.consumer.ConsumerConfig; import org.apache.kafka.clients.consumer.KafkaConsumer; import org.apache.kafka.clients.consumer.OffsetAndMetadata; import org.apache.kafka.common.TopicPartition; -import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; -import org.springframework.context.annotation.Lazy; import org.springframework.stereotype.Component; import org.thingsboard.common.util.ThingsBoardExecutors; import org.thingsboard.server.common.data.StringUtils; -import org.thingsboard.server.common.data.id.TenantId; -import org.thingsboard.server.common.msg.queue.ServiceType; -import org.thingsboard.server.queue.discovery.PartitionService; import java.time.Duration; import java.util.ArrayList; @@ -56,10 +51,6 @@ public class TbKafkaConsumerStatsService { private final TbKafkaSettings kafkaSettings; private final TbKafkaConsumerStatisticConfig statsConfig; - @Lazy - @Autowired - private PartitionService partitionService; - private Consumer consumer; private ScheduledExecutorService statsPrintScheduler; @@ -111,9 +102,7 @@ public class TbKafkaConsumerStatsService { } private boolean isStatsPrintRequired() { - boolean isMyRuleEnginePartition = partitionService.isMyPartition(ServiceType.TB_RULE_ENGINE, TenantId.SYS_TENANT_ID, TenantId.SYS_TENANT_ID); - boolean isMyCorePartition = partitionService.isMyPartition(ServiceType.TB_CORE, TenantId.SYS_TENANT_ID, TenantId.SYS_TENANT_ID); - return log.isInfoEnabled() && (isMyRuleEnginePartition || isMyCorePartition); + return log.isInfoEnabled(); } private List getTopicsStatsWithLag(Map groupOffsets, Map endOffsets) { From 13fbcf4ba0fd5de64d6ec49fa9842973795384c5 Mon Sep 17 00:00:00 2001 From: ViacheslavKlimov Date: Fri, 14 Feb 2025 12:31:36 +0200 Subject: [PATCH 063/345] Fix init for readiness probe controller --- .../java/org/thingsboard/server/edqs/state/EdqsController.java | 2 ++ 1 file changed, 2 insertions(+) diff --git a/common/edqs/src/main/java/org/thingsboard/server/edqs/state/EdqsController.java b/common/edqs/src/main/java/org/thingsboard/server/edqs/state/EdqsController.java index 721675245a..06d5f89aa9 100644 --- a/common/edqs/src/main/java/org/thingsboard/server/edqs/state/EdqsController.java +++ b/common/edqs/src/main/java/org/thingsboard/server/edqs/state/EdqsController.java @@ -16,6 +16,7 @@ package org.thingsboard.server.edqs.state; import lombok.RequiredArgsConstructor; +import org.springframework.boot.autoconfigure.condition.ConditionalOnExpression; import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.RequestMapping; @@ -23,6 +24,7 @@ import org.springframework.web.bind.annotation.RestController; @RestController @RequiredArgsConstructor +@ConditionalOnExpression("'${service.type:null}'=='edqs'") @RequestMapping("/api/edqs") public class EdqsController { From 52a6540e5a7346e01239ebf7a63cf1c9f15d23df Mon Sep 17 00:00:00 2001 From: dashevchenko Date: Fri, 14 Feb 2025 15:12:00 +0200 Subject: [PATCH 064/345] added docker image for edqs --- edqs/src/main/conf/edqs.conf | 22 ++++ edqs/src/main/conf/logback.xml | 49 ++++++++ msa/edqs/docker/Dockerfile | 31 +++++ msa/edqs/docker/start-tb-edqs.sh | 31 +++++ msa/edqs/pom.xml | 190 +++++++++++++++++++++++++++++++ msa/pom.xml | 1 + 6 files changed, 324 insertions(+) create mode 100644 edqs/src/main/conf/edqs.conf create mode 100644 edqs/src/main/conf/logback.xml create mode 100644 msa/edqs/docker/Dockerfile create mode 100755 msa/edqs/docker/start-tb-edqs.sh create mode 100644 msa/edqs/pom.xml diff --git a/edqs/src/main/conf/edqs.conf b/edqs/src/main/conf/edqs.conf new file mode 100644 index 0000000000..ae880ef7e8 --- /dev/null +++ b/edqs/src/main/conf/edqs.conf @@ -0,0 +1,22 @@ +# +# Copyright © 2016-2024 The Thingsboard Authors +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +export JAVA_OPTS="$JAVA_OPTS -Xlog:gc*,heap*,age*,safepoint=debug:file=@pkg.logFolder@/gc.log:time,uptime,level,tags:filecount=10,filesize=10M" +export JAVA_OPTS="$JAVA_OPTS -XX:+IgnoreUnrecognizedVMOptions -XX:+HeapDumpOnOutOfMemoryError" +export JAVA_OPTS="$JAVA_OPTS -XX:-UseBiasedLocking -XX:+UseTLAB -XX:+ResizeTLAB -XX:+PerfDisableSharedMem -XX:+UseCondCardMark" +export JAVA_OPTS="$JAVA_OPTS -XX:+UseG1GC -XX:MaxGCPauseMillis=500 -XX:+UseStringDeduplication -XX:+ParallelRefProcEnabled -XX:MaxTenuringThreshold=10" +export LOG_FILENAME=${pkg.name}.out +export LOADER_PATH=${pkg.installFolder}/conf diff --git a/edqs/src/main/conf/logback.xml b/edqs/src/main/conf/logback.xml new file mode 100644 index 0000000000..4ff57d48ab --- /dev/null +++ b/edqs/src/main/conf/logback.xml @@ -0,0 +1,49 @@ + + + + + + + ${pkg.logFolder}/${pkg.name}.log + + ${pkg.logFolder}/${pkg.name}.%d{yyyy-MM-dd}.%i.log + 100MB + 30 + 3GB + + + %d{ISO8601} [%thread] %-5level %logger{36} - %msg%n + + + + + %d{ISO8601} [%thread] %-5level %logger{36} - %msg%n + + + + + + + + + + + diff --git a/msa/edqs/docker/Dockerfile b/msa/edqs/docker/Dockerfile new file mode 100644 index 0000000000..e634029bbf --- /dev/null +++ b/msa/edqs/docker/Dockerfile @@ -0,0 +1,31 @@ +# +# Copyright © 2016-2024 The Thingsboard Authors +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +FROM thingsboard/openjdk17:bookworm-slim + +COPY start-tb-edqs.sh ${pkg.name}.deb /tmp/ + +RUN chmod a+x /tmp/*.sh \ + && mv /tmp/start-tb-edqs.sh /usr/bin && \ + (yes | dpkg -i /tmp/${pkg.name}.deb) && \ + rm /tmp/${pkg.name}.deb && \ + (systemctl --no-reload disable --now ${pkg.name}.service > /dev/null 2>&1 || :) && \ + chown -R ${pkg.user}:${pkg.user} /tmp && \ + chmod 555 ${pkg.installFolder}/bin/${pkg.name}.jar + +USER ${pkg.user} + +CMD ["start-tb-edqs.sh"] \ No newline at end of file diff --git a/msa/edqs/docker/start-tb-edqs.sh b/msa/edqs/docker/start-tb-edqs.sh new file mode 100755 index 0000000000..bdd1ee48d3 --- /dev/null +++ b/msa/edqs/docker/start-tb-edqs.sh @@ -0,0 +1,31 @@ +#!/bin/bash +# +# Copyright © 2016-2024 The Thingsboard Authors +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +CONF_FOLDER=${pkg.installFolder}/conf +jarfile=${pkg.installFolder}/bin/${pkg.name}.jar +configfile=${pkg.name}.conf + +source "${CONF_FOLDER}/${configfile}" + +echo "Starting '${project.name}' ..." + +cd ${pkg.installFolder}/bin + +exec java -cp ${jarfile} $JAVA_OPTS -Dloader.main=org.thingsboard.server.edqs.ThingsboardEdqsApplication \ + -Dspring.jpa.hibernate.ddl-auto=none \ + -Dlogging.config=$CONF_FOLDER/logback.xml \ + org.springframework.boot.loader.launch.PropertiesLauncher diff --git a/msa/edqs/pom.xml b/msa/edqs/pom.xml new file mode 100644 index 0000000000..4874c06e4b --- /dev/null +++ b/msa/edqs/pom.xml @@ -0,0 +1,190 @@ + + + 4.0.0 + + org.thingsboard + 4.0.0-SNAPSHOT + msa + + org.thingsboard.msa + edqs + pom + + ThingsBoard Entity Data Query Microservice + https://thingsboard.io + ThingsBoard Entity Data Query Microservice + + + UTF-8 + ${basedir}/../.. + edqs + edqs + /var/log/${pkg.name} + /usr/share/${pkg.name} + pre-integration-test + + + + + org.thingsboard + edqs + ${project.version} + deb + deb + provided + + + + + + + org.apache.maven.plugins + maven-dependency-plugin + + + copy-edqs + package + + copy + + + + + org.thingsboard + edqs + deb + deb + ${pkg.name}.deb + ${project.build.directory} + + + + + + + + org.apache.maven.plugins + maven-resources-plugin + + + copy-docker-config + process-resources + + copy-resources + + + ${project.build.directory} + + + docker + true + + + + + + + + com.spotify + dockerfile-maven-plugin + + + build-docker-image + pre-integration-test + + build + + + ${dockerfile.skip} + ${docker.repo}/${docker.name} + true + false + ${project.build.directory} + + + + tag-docker-image + pre-integration-test + + tag + + + ${dockerfile.skip} + ${docker.repo}/${docker.name} + ${project.version} + + + + + + + + + push-docker-image + + + push-docker-image + + + + + + com.spotify + dockerfile-maven-plugin + + + push-latest-docker-image + pre-integration-test + + push + + + latest + ${docker.repo}/${docker.name} + + + + push-version-docker-image + pre-integration-test + + push + + + ${project.version} + ${docker.repo}/${docker.name} + + + + + + + + + + + jenkins + Jenkins Repository + https://repo.jenkins-ci.org/releases + + false + + + + diff --git a/msa/pom.xml b/msa/pom.xml index ff394295f4..259de5fb93 100644 --- a/msa/pom.xml +++ b/msa/pom.xml @@ -48,6 +48,7 @@ transport js-executor monitoring + edqs From 1f3c7acc51a8b9d979150e40a76f10b85b6365f7 Mon Sep 17 00:00:00 2001 From: dashevchenko Date: Fri, 14 Feb 2025 19:01:58 +0200 Subject: [PATCH 065/345] fixed flaky test --- .../server/controller/WebsocketApiTest.java | 15 ++++++--------- 1 file changed, 6 insertions(+), 9 deletions(-) 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 f8a9db43d0..51c9b08155 100644 --- a/application/src/test/java/org/thingsboard/server/controller/WebsocketApiTest.java +++ b/application/src/test/java/org/thingsboard/server/controller/WebsocketApiTest.java @@ -340,29 +340,26 @@ public class WebsocketApiTest extends AbstractControllerTest { Assert.assertEquals(0, update.getCount()); //create alarm, check count = 1 + getWsClient().registerWaitForUpdate(); + Alarm alarm = new Alarm(); alarm.setOriginator(tenantId); alarm.setType("TEST ALARM"); alarm.setSeverity(AlarmSeverity.WARNING); - alarm = doPost("/api/alarm", alarm, Alarm.class); - AlarmCountCmd cmd2 = new AlarmCountCmd(2, alarmCountQuery); - - getWsClient().send(cmd2); - - update = getWsClient().parseAlarmCountReply(getWsClient().waitForReply()); - Assert.assertEquals(2, update.getCmdId()); + update = getWsClient().parseAlarmCountReply(getWsClient().waitForUpdate()); + Assert.assertEquals(1, update.getCmdId()); Assert.assertEquals(1, update.getCount()); // set wrong entity id in filter, check count = 0 singleEntityFilter.setSingleEntity(tenantAdminUserId); - AlarmCountCmd cmd3 = new AlarmCountCmd(3, alarmCountQuery); + AlarmCountCmd cmd3 = new AlarmCountCmd(2, alarmCountQuery); getWsClient().send(cmd3); update = getWsClient().parseAlarmCountReply(getWsClient().waitForReply()); - Assert.assertEquals(3, update.getCmdId()); + Assert.assertEquals(2, update.getCmdId()); Assert.assertEquals(0, update.getCount()); } From dfb0549424a4f4d5a7c6d7734fa8282a6c2010b7 Mon Sep 17 00:00:00 2001 From: Andrii Landiak Date: Mon, 17 Feb 2025 16:32:59 +0200 Subject: [PATCH 066/345] VC support for calculated fields --- .../DefaultEntitiesExportImportService.java | 3 +- .../impl/CalculatedFieldExportService.java | 42 ++++++++++ .../impl/CalculatedFieldImportService.java | 78 +++++++++++++++++++ .../common/data/cf/CalculatedField.java | 17 ++++ .../server/common/data/sync/JsonTbEntity.java | 4 +- .../server/dao/cf/CalculatedFieldDao.java | 3 +- .../dao/sql/cf/CalculatedFieldRepository.java | 11 ++- .../dao/sql/cf/JpaCalculatedFieldDao.java | 20 +++++ 8 files changed, 174 insertions(+), 4 deletions(-) create mode 100644 application/src/main/java/org/thingsboard/server/service/sync/ie/exporting/impl/CalculatedFieldExportService.java create mode 100644 application/src/main/java/org/thingsboard/server/service/sync/ie/importing/impl/CalculatedFieldImportService.java 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 a9b278a19d..de437144ae 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 @@ -69,7 +69,8 @@ public class DefaultEntitiesExportImportService implements EntitiesExportImportS EntityType.DASHBOARD, EntityType.ASSET_PROFILE, EntityType.ASSET, EntityType.DEVICE_PROFILE, EntityType.DEVICE, EntityType.ENTITY_VIEW, EntityType.WIDGET_TYPE, EntityType.WIDGETS_BUNDLE, - EntityType.NOTIFICATION_TEMPLATE, EntityType.NOTIFICATION_TARGET, EntityType.NOTIFICATION_RULE + EntityType.NOTIFICATION_TEMPLATE, EntityType.NOTIFICATION_TARGET, EntityType.NOTIFICATION_RULE, + EntityType.CALCULATED_FIELD ); diff --git a/application/src/main/java/org/thingsboard/server/service/sync/ie/exporting/impl/CalculatedFieldExportService.java b/application/src/main/java/org/thingsboard/server/service/sync/ie/exporting/impl/CalculatedFieldExportService.java new file mode 100644 index 0000000000..fc1465b29c --- /dev/null +++ b/application/src/main/java/org/thingsboard/server/service/sync/ie/exporting/impl/CalculatedFieldExportService.java @@ -0,0 +1,42 @@ +/** + * Copyright © 2016-2024 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.service.sync.ie.exporting.impl; + +import org.springframework.stereotype.Service; +import org.thingsboard.server.common.data.EntityType; +import org.thingsboard.server.common.data.cf.CalculatedField; +import org.thingsboard.server.common.data.id.CalculatedFieldId; +import org.thingsboard.server.common.data.sync.ie.EntityExportData; +import org.thingsboard.server.queue.util.TbCoreComponent; +import org.thingsboard.server.service.sync.vc.data.EntitiesExportCtx; + +import java.util.Set; + +@Service +@TbCoreComponent +public class CalculatedFieldExportService extends BaseEntityExportService> { + + @Override + protected void setRelatedEntities(EntitiesExportCtx ctx, CalculatedField calculatedField, EntityExportData exportData) { + calculatedField.setEntityId(getExternalIdOrElseInternal(ctx, calculatedField.getEntityId())); + } + + @Override + public Set getSupportedEntityTypes() { + return Set.of(EntityType.CALCULATED_FIELD); + } + +} diff --git a/application/src/main/java/org/thingsboard/server/service/sync/ie/importing/impl/CalculatedFieldImportService.java b/application/src/main/java/org/thingsboard/server/service/sync/ie/importing/impl/CalculatedFieldImportService.java new file mode 100644 index 0000000000..07a306d511 --- /dev/null +++ b/application/src/main/java/org/thingsboard/server/service/sync/ie/importing/impl/CalculatedFieldImportService.java @@ -0,0 +1,78 @@ +/** + * Copyright © 2016-2024 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.service.sync.ie.importing.impl; + +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; +import org.thingsboard.server.common.data.EntityType; +import org.thingsboard.server.common.data.User; +import org.thingsboard.server.common.data.audit.ActionType; +import org.thingsboard.server.common.data.cf.CalculatedField; +import org.thingsboard.server.common.data.exception.ThingsboardException; +import org.thingsboard.server.common.data.id.CalculatedFieldId; +import org.thingsboard.server.common.data.id.TenantId; +import org.thingsboard.server.common.data.sync.ie.EntityExportData; +import org.thingsboard.server.dao.cf.CalculatedFieldService; +import org.thingsboard.server.dao.service.ConstraintValidator; +import org.thingsboard.server.queue.util.TbCoreComponent; +import org.thingsboard.server.service.sync.vc.data.EntitiesImportCtx; + +@Service +@TbCoreComponent +@RequiredArgsConstructor +public class CalculatedFieldImportService extends BaseEntityImportService> { + + private final CalculatedFieldService calculatedFieldService; + + @Override + protected void setOwner(TenantId tenantId, CalculatedField calculatedField, IdProvider idProvider) { + calculatedField.setTenantId(tenantId); + } + + @Override + protected CalculatedField prepare(EntitiesImportCtx ctx, CalculatedField calculatedField, CalculatedField oldEntity, EntityExportData exportData, IdProvider idProvider) { + calculatedField.setEntityId(idProvider.getInternalId(calculatedField.getEntityId())); + return calculatedField; + } + + @Override + protected CalculatedField saveOrUpdate(EntitiesImportCtx ctx, CalculatedField calculatedField, EntityExportData exportData, IdProvider idProvider) { + ConstraintValidator.validateFields(calculatedField); + return calculatedFieldService.save(calculatedField); + } + + @Override + protected CalculatedField deepCopy(CalculatedField calculatedField) { + return new CalculatedField(calculatedField); + } + + @Override + protected void onEntitySaved(User user, CalculatedField savedEntity, CalculatedField oldEntity) throws ThingsboardException { + entityActionService.logEntityAction(user, savedEntity.getId(), savedEntity, null, + oldEntity == null ? ActionType.ADDED : ActionType.UPDATED, null); + } + + @Override + protected void cleanupForComparison(CalculatedField e) { + super.cleanupForComparison(e); + } + + @Override + public EntityType getEntityType() { + return EntityType.CALCULATED_FIELD; + } + +} 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 f4b92b3802..0c6278be2b 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 @@ -37,11 +37,14 @@ import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.common.data.validation.Length; import org.thingsboard.server.common.data.validation.NoXss; +import java.io.Serial; + @Schema @Data @EqualsAndHashCode(callSuper = true) public class CalculatedField extends BaseData implements HasName, HasTenantId, HasVersion, ExportableEntity, HasDebugSettings { + @Serial private static final long serialVersionUID = 4491966747773381420L; private TenantId tenantId; @@ -78,6 +81,20 @@ public class CalculatedField extends BaseData implements HasN super(id); } + public CalculatedField(CalculatedField other) { + super(other); + this.tenantId = other.tenantId; + this.entityId = other.entityId; + this.type = other.type; + this.name = other.name; + this.configurationVersion = other.configurationVersion; + this.configuration = other.configuration; + this.version = other.version; + this.externalId = other.externalId; + this.debugMode = other.debugMode; + this.debugSettings = other.debugSettings; + } + public CalculatedField(TenantId tenantId, EntityId entityId, CalculatedFieldType type, String name, int configurationVersion, CalculatedFieldConfiguration configuration, Long version, CalculatedFieldId externalId) { this.tenantId = tenantId; this.entityId = entityId; diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/sync/JsonTbEntity.java b/common/data/src/main/java/org/thingsboard/server/common/data/sync/JsonTbEntity.java index 1b5459ec1a..6af6f6a034 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/sync/JsonTbEntity.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/sync/JsonTbEntity.java @@ -28,6 +28,7 @@ import org.thingsboard.server.common.data.EntityView; import org.thingsboard.server.common.data.TbResource; import org.thingsboard.server.common.data.asset.Asset; import org.thingsboard.server.common.data.asset.AssetProfile; +import org.thingsboard.server.common.data.cf.CalculatedField; import org.thingsboard.server.common.data.notification.rule.NotificationRule; import org.thingsboard.server.common.data.notification.targets.NotificationTarget; import org.thingsboard.server.common.data.notification.template.NotificationTemplate; @@ -58,7 +59,8 @@ import java.lang.annotation.Target; @Type(name = "NOTIFICATION_TEMPLATE", value = NotificationTemplate.class), @Type(name = "NOTIFICATION_TARGET", value = NotificationTarget.class), @Type(name = "NOTIFICATION_RULE", value = NotificationRule.class), - @Type(name = "TB_RESOURCE", value = TbResource.class) + @Type(name = "TB_RESOURCE", value = TbResource.class), + @Type(name = "CALCULATED_FIELD", value = CalculatedField.class) }) @JsonIgnoreProperties(value = {"tenantId", "createdTime", "version"}, ignoreUnknown = true) public @interface JsonTbEntity { diff --git a/dao/src/main/java/org/thingsboard/server/dao/cf/CalculatedFieldDao.java b/dao/src/main/java/org/thingsboard/server/dao/cf/CalculatedFieldDao.java index 3efb4011ed..99683aaa77 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/cf/CalculatedFieldDao.java +++ b/dao/src/main/java/org/thingsboard/server/dao/cf/CalculatedFieldDao.java @@ -22,10 +22,11 @@ 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.dao.Dao; +import org.thingsboard.server.dao.ExportableEntityDao; import java.util.List; -public interface CalculatedFieldDao extends Dao { +public interface CalculatedFieldDao extends Dao, ExportableEntityDao { List findAllByTenantId(TenantId tenantId); diff --git a/dao/src/main/java/org/thingsboard/server/dao/sql/cf/CalculatedFieldRepository.java b/dao/src/main/java/org/thingsboard/server/dao/sql/cf/CalculatedFieldRepository.java index 2aeca659bc..9a5c76df97 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/sql/cf/CalculatedFieldRepository.java +++ b/dao/src/main/java/org/thingsboard/server/dao/sql/cf/CalculatedFieldRepository.java @@ -18,13 +18,16 @@ package org.thingsboard.server.dao.sql.cf; import org.springframework.data.domain.Page; import org.springframework.data.domain.Pageable; import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.stereotype.Repository; import org.thingsboard.server.common.data.id.CalculatedFieldId; +import org.thingsboard.server.dao.ExportableEntityRepository; import org.thingsboard.server.dao.model.sql.CalculatedFieldEntity; import java.util.List; import java.util.UUID; -public interface CalculatedFieldRepository extends JpaRepository { +@Repository +public interface CalculatedFieldRepository extends JpaRepository, ExportableEntityRepository { boolean existsByTenantIdAndEntityId(UUID tenantId, UUID entityId); @@ -34,10 +37,16 @@ public interface CalculatedFieldRepository extends JpaRepository findAllByTenantIdAndEntityId(UUID tenantId, UUID entityId, Pageable pageable); + Page findByTenantId(UUID tenantId, Pageable pageable); + List findAllByTenantId(UUID tenantId); List removeAllByTenantIdAndEntityId(UUID tenantId, UUID entityId); long countByTenantIdAndEntityId(UUID tenantId, UUID entityId); + CalculatedFieldId findExternalIdById(UUID id); + + CalculatedFieldEntity findByTenantIdAndName(UUID tenantId, String name); + } diff --git a/dao/src/main/java/org/thingsboard/server/dao/sql/cf/JpaCalculatedFieldDao.java b/dao/src/main/java/org/thingsboard/server/dao/sql/cf/JpaCalculatedFieldDao.java index 703bdfbf6f..865847e15a 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/sql/cf/JpaCalculatedFieldDao.java +++ b/dao/src/main/java/org/thingsboard/server/dao/sql/cf/JpaCalculatedFieldDao.java @@ -93,6 +93,26 @@ public class JpaCalculatedFieldDao extends JpaAbstractDao findByTenantId(UUID tenantId, PageLink pageLink) { + return DaoUtil.toPageData(calculatedFieldRepository.findByTenantId(tenantId, DaoUtil.toPageable(pageLink))); + } + + @Override + public CalculatedField findByTenantIdAndName(UUID tenantId, String name) { + return DaoUtil.getData(calculatedFieldRepository.findByTenantIdAndName(tenantId, name)); + } + + @Override + public CalculatedField findByTenantIdAndExternalId(UUID tenantId, UUID externalId) { + return DaoUtil.getData(calculatedFieldRepository.findByTenantIdAndExternalId(tenantId, externalId)); + } + + @Override + public CalculatedFieldId getExternalIdByInternal(CalculatedFieldId internalId) { + return calculatedFieldRepository.findExternalIdById(internalId.getId()); + } + @Override protected Class getEntityClass() { return CalculatedFieldEntity.class; From 85f2fcd4d519bae15219c36e3ae345655f2ddcc2 Mon Sep 17 00:00:00 2001 From: Andrii Landiak Date: Mon, 17 Feb 2025 17:47:32 +0200 Subject: [PATCH 067/345] Add tests --- .../sync/ie/ExportImportServiceSqlTest.java | 71 ++++++++++++++++++- 1 file changed, 68 insertions(+), 3 deletions(-) diff --git a/application/src/test/java/org/thingsboard/server/service/sync/ie/ExportImportServiceSqlTest.java b/application/src/test/java/org/thingsboard/server/service/sync/ie/ExportImportServiceSqlTest.java index 0beaca9af9..3e47db1bc0 100644 --- a/application/src/test/java/org/thingsboard/server/service/sync/ie/ExportImportServiceSqlTest.java +++ b/application/src/test/java/org/thingsboard/server/service/sync/ie/ExportImportServiceSqlTest.java @@ -43,6 +43,15 @@ import org.thingsboard.server.common.data.User; import org.thingsboard.server.common.data.asset.Asset; import org.thingsboard.server.common.data.asset.AssetProfile; import org.thingsboard.server.common.data.audit.ActionType; +import org.thingsboard.server.common.data.cf.CalculatedField; +import org.thingsboard.server.common.data.cf.CalculatedFieldType; +import org.thingsboard.server.common.data.cf.configuration.Argument; +import org.thingsboard.server.common.data.cf.configuration.ArgumentType; +import org.thingsboard.server.common.data.cf.configuration.CalculatedFieldConfiguration; +import org.thingsboard.server.common.data.cf.configuration.Output; +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.device.data.DefaultDeviceTransportConfiguration; import org.thingsboard.server.common.data.device.data.DeviceData; import org.thingsboard.server.common.data.device.profile.DefaultDeviceProfileConfiguration; @@ -51,6 +60,7 @@ import org.thingsboard.server.common.data.device.profile.DeviceProfileData; import org.thingsboard.server.common.data.edge.EdgeEventActionType; import org.thingsboard.server.common.data.id.AssetId; import org.thingsboard.server.common.data.id.AssetProfileId; +import org.thingsboard.server.common.data.id.CalculatedFieldId; import org.thingsboard.server.common.data.id.CustomerId; import org.thingsboard.server.common.data.id.DashboardId; import org.thingsboard.server.common.data.id.DeviceId; @@ -79,6 +89,7 @@ import org.thingsboard.server.common.data.util.ThrowingRunnable; import org.thingsboard.server.controller.AbstractControllerTest; import org.thingsboard.server.dao.asset.AssetProfileService; import org.thingsboard.server.dao.asset.AssetService; +import org.thingsboard.server.dao.cf.CalculatedFieldService; import org.thingsboard.server.dao.customer.CustomerService; import org.thingsboard.server.dao.dashboard.DashboardService; import org.thingsboard.server.dao.device.DeviceProfileService; @@ -145,6 +156,8 @@ public class ExportImportServiceSqlTest extends AbstractControllerTest { protected TenantService tenantService; @Autowired protected EntityViewService entityViewService; + @Autowired + protected CalculatedFieldService calculatedFieldService; protected TenantId tenantId1; protected User tenantAdmin1; @@ -191,9 +204,10 @@ public class ExportImportServiceSqlTest extends AbstractControllerTest { Asset asset = createAsset(tenantId1, null, assetProfile.getId(), "Asset 1"); DeviceProfile deviceProfile = createDeviceProfile(tenantId1, ruleChain.getId(), dashboard.getId(), "Device profile 1"); Device device = createDevice(tenantId1, null, deviceProfile.getId(), "Device 1"); + CalculatedField calculatedField = createCalculatedField(tenantId1, device.getId(), device.getId()); Map entitiesExportData = Stream.of(customer.getId(), asset.getId(), device.getId(), - ruleChain.getId(), dashboard.getId(), assetProfile.getId(), deviceProfile.getId()) + ruleChain.getId(), dashboard.getId(), assetProfile.getId(), deviceProfile.getId(), calculatedField.getId()) .map(entityId -> { try { return exportEntity(tenantAdmin1, entityId, EntityExportSettings.builder() @@ -245,7 +259,6 @@ public class ExportImportServiceSqlTest extends AbstractControllerTest { verify(entityActionService, Mockito.never()).logEntityAction(any(), eq(importedAsset.getId()), eq(importedAsset), any(), eq(ActionType.UPDATED), isNull()); - EntityExportData updatedAssetEntity = getAndClone(entitiesExportData, EntityType.ASSET); updatedAssetEntity.getEntity().setLabel("t" + updatedAssetEntity.getEntity().getLabel()); Asset updatedAsset = importEntity(tenantAdmin2, updatedAssetEntity).getSavedEntity(); @@ -272,6 +285,19 @@ public class ExportImportServiceSqlTest extends AbstractControllerTest { updatedDeviceEntity.getEntity().setLabel("t" + updatedDeviceEntity.getEntity().getLabel()); Device updatedDevice = importEntity(tenantAdmin2, updatedDeviceEntity).getSavedEntity(); verify(tbClusterService).onDeviceUpdated(eq(updatedDevice), eq(importedDevice)); + + CalculatedField importedCalculatedField = (CalculatedField) importEntity(tenantAdmin2, getAndClone(entitiesExportData, EntityType.CALCULATED_FIELD)).getSavedEntity(); + verify(entityActionService).logEntityAction(any(), eq(importedCalculatedField.getId()), eq(importedCalculatedField), + any(), eq(ActionType.ADDED), isNull()); + importEntity(tenantAdmin2, getAndClone(entitiesExportData, EntityType.CALCULATED_FIELD)); + verify(entityActionService, Mockito.never()).logEntityAction(any(), eq(importedCalculatedField.getId()), eq(importedCalculatedField), + any(), eq(ActionType.UPDATED), isNull()); + + EntityExportData updatedCalculatedFieldEntity = getAndClone(entitiesExportData, EntityType.CALCULATED_FIELD); + updatedCalculatedFieldEntity.getEntity().setName("t" + updatedCalculatedFieldEntity.getEntity().getName()); + CalculatedField updatedCalculatedField = importEntity(tenantAdmin2, updatedCalculatedFieldEntity).getSavedEntity(); + verify(entityActionService).logEntityAction(any(), eq(updatedCalculatedField.getId()), eq(updatedCalculatedField), + any(), eq(ActionType.UPDATED), isNull()); } @Test @@ -290,9 +316,11 @@ public class ExportImportServiceSqlTest extends AbstractControllerTest { Device device = createDevice(tenantId1, customer.getId(), deviceProfile.getId(), "Device 1"); EntityView entityView = createEntityView(tenantId1, customer.getId(), device.getId(), "Entity view 1"); + CalculatedField calculatedField = createCalculatedField(tenantId1, device.getId(), device.getId()); + Map ids = new HashMap<>(); for (EntityId entityId : List.of(customer.getId(), ruleChain.getId(), dashboard.getId(), assetProfile.getId(), asset.getId(), - deviceProfile.getId(), device.getId(), entityView.getId(), ruleChain.getId(), dashboard.getId())) { + deviceProfile.getId(), device.getId(), entityView.getId(), ruleChain.getId(), dashboard.getId(), calculatedField.getId())) { EntityExportData exportData = exportEntity(getSecurityUser(tenantAdmin1), entityId); EntityImportResult importResult = importEntity(getSecurityUser(tenantAdmin2), exportData, EntityImportSettings.builder() .saveCredentials(false) @@ -333,6 +361,10 @@ public class ExportImportServiceSqlTest extends AbstractControllerTest { assertThat(exportedEntityView.getCustomerId()).isEqualTo(customer.getId()); assertThat(exportedEntityView.getEntityId()).isEqualTo(device.getId()); + CalculatedField exportedCalculatedField = (CalculatedField) exportEntity(tenantAdmin2, (CalculatedFieldId) ids.get(calculatedField.getId())).getEntity(); + assertThat(exportedCalculatedField.getName()).isEqualTo(calculatedField.getName()); + assertThat(exportedCalculatedField.getEntityId()).isEqualTo(device.getId()); + deviceProfile.setDefaultDashboardId(null); deviceProfileService.saveDeviceProfile(deviceProfile); DeviceProfile importedDeviceProfile = deviceProfileService.findDeviceProfileById(tenantId2, (DeviceProfileId) ids.get(deviceProfile.getId())); @@ -549,6 +581,39 @@ public class ExportImportServiceSqlTest extends AbstractControllerTest { return relation; } + private CalculatedField createCalculatedField(TenantId tenantId, EntityId entityId, EntityId referencedEntityId) { + CalculatedField calculatedField = new CalculatedField(); + calculatedField.setTenantId(tenantId); + calculatedField.setEntityId(entityId); + calculatedField.setType(CalculatedFieldType.SIMPLE); + calculatedField.setName("Test Calculated Field"); + calculatedField.setConfigurationVersion(1); + calculatedField.setConfiguration(getCalculatedFieldConfig(referencedEntityId)); + calculatedField.setVersion(1L); + return calculatedFieldService.save(calculatedField); + } + + private CalculatedFieldConfiguration getCalculatedFieldConfig(EntityId referencedEntityId) { + SimpleCalculatedFieldConfiguration config = new SimpleCalculatedFieldConfiguration(); + + Argument argument = new Argument(); + argument.setRefEntityId(referencedEntityId); + ReferencedEntityKey refEntityKey = new ReferencedEntityKey("temperature", ArgumentType.TS_LATEST, null); + argument.setRefEntityKey(refEntityKey); + + config.setArguments(Map.of("T", argument)); + + config.setExpression("T - (100 - H) / 5"); + + Output output = new Output(); + output.setName("output"); + output.setType(OutputType.TIME_SERIES); + + config.setOutput(output); + + return config; + } + protected , I extends EntityId> EntityExportData exportEntity(User user, I entityId) throws Exception { return exportEntity(user, entityId, EntityExportSettings.builder() .exportCredentials(true) From 44a7bf82c4156ef9bbfc1cd0364fe403f0afcfd9 Mon Sep 17 00:00:00 2001 From: Andrii Landiak Date: Mon, 17 Feb 2025 18:00:12 +0200 Subject: [PATCH 068/345] Fix ComponentLifecycleMsg: message and builder --- .../server/common/msg/plugin/ComponentLifecycleMsg.java | 4 ++++ .../org/thingsboard/server/common/util/ProtoUtils.java | 7 +++---- common/proto/src/main/proto/queue.proto | 4 ++-- 3 files changed, 9 insertions(+), 6 deletions(-) diff --git a/common/message/src/main/java/org/thingsboard/server/common/msg/plugin/ComponentLifecycleMsg.java b/common/message/src/main/java/org/thingsboard/server/common/msg/plugin/ComponentLifecycleMsg.java index 208b6ab2f5..7202a11a6c 100644 --- a/common/message/src/main/java/org/thingsboard/server/common/msg/plugin/ComponentLifecycleMsg.java +++ b/common/message/src/main/java/org/thingsboard/server/common/msg/plugin/ComponentLifecycleMsg.java @@ -26,6 +26,7 @@ import org.thingsboard.server.common.msg.MsgType; import org.thingsboard.server.common.msg.aware.TenantAwareMsg; import org.thingsboard.server.common.msg.cluster.ToAllNodesMsg; +import java.io.Serial; import java.util.Optional; /** @@ -33,6 +34,8 @@ import java.util.Optional; */ @Data public class ComponentLifecycleMsg implements TenantAwareMsg, ToAllNodesMsg { + + @Serial private static final long serialVersionUID = -5303421482781273062L; private final TenantId tenantId; @@ -66,4 +69,5 @@ public class ComponentLifecycleMsg implements TenantAwareMsg, ToAllNodesMsg { public MsgType getMsgType() { return MsgType.COMPONENT_LIFE_CYCLE_MSG; } + } diff --git a/common/proto/src/main/java/org/thingsboard/server/common/util/ProtoUtils.java b/common/proto/src/main/java/org/thingsboard/server/common/util/ProtoUtils.java index 07406977d2..057cf875f1 100644 --- a/common/proto/src/main/java/org/thingsboard/server/common/util/ProtoUtils.java +++ b/common/proto/src/main/java/org/thingsboard/server/common/util/ProtoUtils.java @@ -134,7 +134,7 @@ public class ProtoUtils { builder.setName(msg.getName()); } if (msg.getOldName() != null) { - builder.setName(msg.getOldName()); + builder.setOldName(msg.getOldName()); } return builder.build(); } @@ -149,8 +149,8 @@ public class ProtoUtils { .tenantId(TenantId.fromUUID(new UUID(proto.getTenantIdMSB(), proto.getTenantIdLSB()))) .entityId(entityId) .event(ComponentLifecycleEvent.values()[proto.getEventValue()]) - .name(proto.getName()) - .oldName(proto.getOldName()); + .name(proto.hasName() ? proto.getName() : null) + .oldName(proto.hasOldName() ? proto.getOldName() : null); if (proto.getProfileIdMSB() != 0 || proto.getProfileIdLSB() != 0) { var profileType = EntityType.DEVICE.equals(entityId.getEntityType()) ? EntityType.DEVICE_PROFILE : EntityType.ASSET_PROFILE; builder.profileId(EntityIdFactory.getByTypeAndUuid(profileType, new UUID(proto.getProfileIdMSB(), proto.getProfileIdLSB()))); @@ -166,7 +166,6 @@ public class ProtoUtils { return entityTypeByProtoNumber[entityType.getNumber()]; } - public static TransportProtos.ToEdgeSyncRequestMsgProto toProto(ToEdgeSyncRequest request) { return TransportProtos.ToEdgeSyncRequestMsgProto.newBuilder() .setTenantIdMSB(request.getTenantId().getId().getMostSignificantBits()) diff --git a/common/proto/src/main/proto/queue.proto b/common/proto/src/main/proto/queue.proto index d0050c310d..e782291f02 100644 --- a/common/proto/src/main/proto/queue.proto +++ b/common/proto/src/main/proto/queue.proto @@ -1188,8 +1188,8 @@ message ComponentLifecycleMsgProto { int64 entityIdLSB = 5; ComponentLifecycleEvent event = 6; //Since 4.0. To replace the - string oldName = 7; - string name = 8; + optional string oldName = 7; + optional string name = 8; int64 oldProfileIdMSB = 9; int64 oldProfileIdLSB = 10; int64 profileIdMSB = 11; From fc5af455192ee59ba6526d3855d5d6882a745984 Mon Sep 17 00:00:00 2001 From: ViacheslavKlimov Date: Tue, 18 Feb 2025 17:01:39 +0200 Subject: [PATCH 069/345] EDQS: optimize attributes and latest kv --- .../server/service/edqs/EdqsSyncService.java | 2 +- ...faultTelemetrySubscriptionServiceTest.java | 2 +- .../server/common/data/edqs/AttributeKv.java | 6 +- .../server/common/data/edqs}/DataPoint.java | 2 +- .../server/common/data/edqs/LatestTsKv.java | 6 +- .../server/edqs/data/BaseEntityData.java | 2 +- .../server/edqs/data/DeviceData.java | 2 +- .../server/edqs/data/EntityData.java | 2 +- .../edqs/data/dp/AbstractDataPoint.java | 1 + .../edqs/data/dp/CompressedJsonDataPoint.java | 4 +- .../data/dp/CompressedStringDataPoint.java | 23 +-- .../server/edqs/load/TenantRepoLoader.java | 144 ------------------ .../server/edqs/processor/EdqsConverter.java | 96 +++++++++--- .../server/edqs/processor/EdqsProducer.java | 7 + .../server/edqs/repo/TenantRepo.java | 46 +----- .../server/edqs/util/RepositoryUtils.java | 2 +- common/proto/src/main/proto/queue.proto | 19 ++- 17 files changed, 125 insertions(+), 241 deletions(-) rename common/{edqs/src/main/java/org/thingsboard/server/edqs/data/dp => data/src/main/java/org/thingsboard/server/common/data/edqs}/DataPoint.java (95%) delete mode 100644 common/edqs/src/main/java/org/thingsboard/server/edqs/load/TenantRepoLoader.java diff --git a/application/src/main/java/org/thingsboard/server/service/edqs/EdqsSyncService.java b/application/src/main/java/org/thingsboard/server/service/edqs/EdqsSyncService.java index 80aeee8635..66f088a344 100644 --- a/application/src/main/java/org/thingsboard/server/service/edqs/EdqsSyncService.java +++ b/application/src/main/java/org/thingsboard/server/service/edqs/EdqsSyncService.java @@ -166,7 +166,7 @@ public abstract class EdqsSyncService { if (entityIdInfo != null) { process(entityIdInfo.tenantId(), RELATION, relation.toData()); } else { - log.info("Relation from entity not found: " + relation.getFromId()); + log.info("Relation from entity not found: " + relation.getFromType() + " " + relation.getFromId()); } } } diff --git a/application/src/test/java/org/thingsboard/server/service/telemetry/DefaultTelemetrySubscriptionServiceTest.java b/application/src/test/java/org/thingsboard/server/service/telemetry/DefaultTelemetrySubscriptionServiceTest.java index 10fdd85504..677a414e2f 100644 --- a/application/src/test/java/org/thingsboard/server/service/telemetry/DefaultTelemetrySubscriptionServiceTest.java +++ b/application/src/test/java/org/thingsboard/server/service/telemetry/DefaultTelemetrySubscriptionServiceTest.java @@ -155,7 +155,7 @@ class DefaultTelemetrySubscriptionServiceTest { lenient().when(tbEntityViewService.findEntityViewsByTenantIdAndEntityIdAsync(tenantId, entityId)).thenReturn(immediateFuture(Collections.emptyList())); // send partition change event so currentPartitions set is populated - telemetryService.onTbApplicationEvent(new PartitionChangeEvent(this, ServiceType.TB_CORE, Map.of(new QueueKey(ServiceType.TB_CORE), Set.of(tpi)))); + telemetryService.onTbApplicationEvent(new PartitionChangeEvent(this, ServiceType.TB_CORE, Map.of(new QueueKey(ServiceType.TB_CORE), Set.of(tpi)), Collections.emptyMap())); } @AfterEach diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/edqs/AttributeKv.java b/common/data/src/main/java/org/thingsboard/server/common/data/edqs/AttributeKv.java index f2c35466dc..9f65f72be4 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/edqs/AttributeKv.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/edqs/AttributeKv.java @@ -35,8 +35,10 @@ public class AttributeKv implements EdqsObject { private String key; private Long version; - private Long lastUpdateTs; // optional (on deletion) - private KvEntry value; // optional (on deletion) + private DataPoint dataPoint; // optional (on deletion) + + private Long lastUpdateTs; // only for serialization + private KvEntry value; // only for serialization public AttributeKv(EntityId entityId, AttributeScope scope, AttributeKvEntry attributeKvEntry, long version) { this.entityId = entityId; diff --git a/common/edqs/src/main/java/org/thingsboard/server/edqs/data/dp/DataPoint.java b/common/data/src/main/java/org/thingsboard/server/common/data/edqs/DataPoint.java similarity index 95% rename from common/edqs/src/main/java/org/thingsboard/server/edqs/data/dp/DataPoint.java rename to common/data/src/main/java/org/thingsboard/server/common/data/edqs/DataPoint.java index 6cbdd35ad1..64b200fd45 100644 --- a/common/edqs/src/main/java/org/thingsboard/server/edqs/data/dp/DataPoint.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/edqs/DataPoint.java @@ -13,7 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package org.thingsboard.server.edqs.data.dp; +package org.thingsboard.server.common.data.edqs; import org.thingsboard.server.common.data.kv.DataType; diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/edqs/LatestTsKv.java b/common/data/src/main/java/org/thingsboard/server/common/data/edqs/LatestTsKv.java index 8aaabad1a7..19a767a320 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/edqs/LatestTsKv.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/edqs/LatestTsKv.java @@ -33,8 +33,10 @@ public class LatestTsKv implements EdqsObject { private String key; private Long version; - private Long ts; // optional (on deletion) - private KvEntry value; // optional (on deletion) + private DataPoint dataPoint; // optional (on deletion) + + private Long ts; // only for serialization + private KvEntry value; // only for serialization public LatestTsKv(EntityId entityId, TsKvEntry tsKvEntry, Long version) { this.entityId = entityId; diff --git a/common/edqs/src/main/java/org/thingsboard/server/edqs/data/BaseEntityData.java b/common/edqs/src/main/java/org/thingsboard/server/edqs/data/BaseEntityData.java index 84785b8f3c..1789cd8ace 100644 --- a/common/edqs/src/main/java/org/thingsboard/server/edqs/data/BaseEntityData.java +++ b/common/edqs/src/main/java/org/thingsboard/server/edqs/data/BaseEntityData.java @@ -25,7 +25,7 @@ import org.thingsboard.server.common.data.id.CustomerId; import org.thingsboard.server.common.data.permission.QueryContext; import org.thingsboard.server.common.data.query.EntityKeyType; import org.thingsboard.server.edqs.data.dp.BoolDataPoint; -import org.thingsboard.server.edqs.data.dp.DataPoint; +import org.thingsboard.server.common.data.edqs.DataPoint; import org.thingsboard.server.edqs.data.dp.LongDataPoint; import org.thingsboard.server.edqs.data.dp.StringDataPoint; import org.thingsboard.server.edqs.query.DataKey; diff --git a/common/edqs/src/main/java/org/thingsboard/server/edqs/data/DeviceData.java b/common/edqs/src/main/java/org/thingsboard/server/edqs/data/DeviceData.java index d416f86e92..f6794325d0 100644 --- a/common/edqs/src/main/java/org/thingsboard/server/edqs/data/DeviceData.java +++ b/common/edqs/src/main/java/org/thingsboard/server/edqs/data/DeviceData.java @@ -20,7 +20,7 @@ import org.thingsboard.server.common.data.AttributeScope; import org.thingsboard.server.common.data.EntityType; import org.thingsboard.server.common.data.edqs.fields.DeviceFields; import org.thingsboard.server.common.data.query.EntityKeyType; -import org.thingsboard.server.edqs.data.dp.DataPoint; +import org.thingsboard.server.common.data.edqs.DataPoint; import java.util.Map; import java.util.UUID; diff --git a/common/edqs/src/main/java/org/thingsboard/server/edqs/data/EntityData.java b/common/edqs/src/main/java/org/thingsboard/server/edqs/data/EntityData.java index f13816976a..59e2c6c0c4 100644 --- a/common/edqs/src/main/java/org/thingsboard/server/edqs/data/EntityData.java +++ b/common/edqs/src/main/java/org/thingsboard/server/edqs/data/EntityData.java @@ -20,7 +20,7 @@ import org.thingsboard.server.common.data.EntityType; import org.thingsboard.server.common.data.edqs.fields.EntityFields; import org.thingsboard.server.common.data.permission.QueryContext; import org.thingsboard.server.common.data.query.EntityKeyType; -import org.thingsboard.server.edqs.data.dp.DataPoint; +import org.thingsboard.server.common.data.edqs.DataPoint; import org.thingsboard.server.edqs.query.DataKey; import org.thingsboard.server.edqs.repo.TenantRepo; diff --git a/common/edqs/src/main/java/org/thingsboard/server/edqs/data/dp/AbstractDataPoint.java b/common/edqs/src/main/java/org/thingsboard/server/edqs/data/dp/AbstractDataPoint.java index 91bc54e958..50740af6e1 100644 --- a/common/edqs/src/main/java/org/thingsboard/server/edqs/data/dp/AbstractDataPoint.java +++ b/common/edqs/src/main/java/org/thingsboard/server/edqs/data/dp/AbstractDataPoint.java @@ -17,6 +17,7 @@ package org.thingsboard.server.edqs.data.dp; import lombok.Getter; import lombok.RequiredArgsConstructor; +import org.thingsboard.server.common.data.edqs.DataPoint; @RequiredArgsConstructor public abstract class AbstractDataPoint implements DataPoint { diff --git a/common/edqs/src/main/java/org/thingsboard/server/edqs/data/dp/CompressedJsonDataPoint.java b/common/edqs/src/main/java/org/thingsboard/server/edqs/data/dp/CompressedJsonDataPoint.java index 30f202181e..c1d1db94a8 100644 --- a/common/edqs/src/main/java/org/thingsboard/server/edqs/data/dp/CompressedJsonDataPoint.java +++ b/common/edqs/src/main/java/org/thingsboard/server/edqs/data/dp/CompressedJsonDataPoint.java @@ -19,8 +19,8 @@ import org.thingsboard.server.common.data.kv.DataType; public class CompressedJsonDataPoint extends CompressedStringDataPoint { - public CompressedJsonDataPoint(long ts, String value) { - super(ts, value); + public CompressedJsonDataPoint(long ts, byte[] compressedValue) { + super(ts, compressedValue); } @Override diff --git a/common/edqs/src/main/java/org/thingsboard/server/edqs/data/dp/CompressedStringDataPoint.java b/common/edqs/src/main/java/org/thingsboard/server/edqs/data/dp/CompressedStringDataPoint.java index 7056438338..0502bbccda 100644 --- a/common/edqs/src/main/java/org/thingsboard/server/edqs/data/dp/CompressedStringDataPoint.java +++ b/common/edqs/src/main/java/org/thingsboard/server/edqs/data/dp/CompressedStringDataPoint.java @@ -18,30 +18,18 @@ package org.thingsboard.server.edqs.data.dp; import lombok.Getter; import lombok.SneakyThrows; import org.thingsboard.server.common.data.kv.DataType; -import org.thingsboard.server.edqs.repo.TbBytePool; import org.xerial.snappy.Snappy; -import java.nio.charset.StandardCharsets; -import java.util.concurrent.atomic.AtomicInteger; -import java.util.concurrent.atomic.AtomicLong; - public class CompressedStringDataPoint extends AbstractDataPoint { public static final int MIN_STR_SIZE_TO_COMPRESS = 512; @Getter - private final byte[] value; - - public static final AtomicInteger cnt = new AtomicInteger(); - public static final AtomicLong uncompressedLength = new AtomicLong(); - public static final AtomicLong compressedLength = new AtomicLong(); + private final byte[] compressedValue; @SneakyThrows - public CompressedStringDataPoint(long ts, String value) { + public CompressedStringDataPoint(long ts, byte[] compressedValue) { super(ts); - cnt.incrementAndGet(); - uncompressedLength.addAndGet(value.getBytes(StandardCharsets.UTF_8).length); - this.value = TbBytePool.intern(Snappy.compress(value)); - compressedLength.addAndGet(this.value.length); + this.compressedValue = compressedValue; } @Override @@ -52,13 +40,12 @@ public class CompressedStringDataPoint extends AbstractDataPoint { @SneakyThrows @Override public String getStr() { - return Snappy.uncompressString(value); + return Snappy.uncompressString(compressedValue); } - @SneakyThrows @Override public String valueToString() { - return Snappy.uncompressString(value); + return getStr(); } } diff --git a/common/edqs/src/main/java/org/thingsboard/server/edqs/load/TenantRepoLoader.java b/common/edqs/src/main/java/org/thingsboard/server/edqs/load/TenantRepoLoader.java deleted file mode 100644 index 6302e60f4c..0000000000 --- a/common/edqs/src/main/java/org/thingsboard/server/edqs/load/TenantRepoLoader.java +++ /dev/null @@ -1,144 +0,0 @@ -/** - * Copyright © 2016-2024 The Thingsboard Authors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package org.thingsboard.server.edqs.load; - -import lombok.Getter; -import lombok.RequiredArgsConstructor; -import org.apache.commons.lang3.RandomStringUtils; -import org.thingsboard.server.common.data.AttributeScope; -import org.thingsboard.server.common.data.Device; -import org.thingsboard.server.common.data.EntityType; -import org.thingsboard.server.common.data.StringUtils; -import org.thingsboard.server.common.data.edqs.AttributeKv; -import org.thingsboard.server.common.data.edqs.LatestTsKv; -import org.thingsboard.server.common.data.id.DeviceId; -import org.thingsboard.server.common.data.kv.BooleanDataEntry; -import org.thingsboard.server.common.data.kv.DataType; -import org.thingsboard.server.common.data.kv.DoubleDataEntry; -import org.thingsboard.server.common.data.kv.JsonDataEntry; -import org.thingsboard.server.common.data.kv.KvEntry; -import org.thingsboard.server.common.data.kv.LongDataEntry; -import org.thingsboard.server.common.data.kv.StringDataEntry; -import org.thingsboard.server.edqs.processor.EdqsConverter; -import org.thingsboard.server.edqs.repo.TenantRepo; - -import java.util.HashMap; -import java.util.Map; -import java.util.Random; -import java.util.UUID; - -@RequiredArgsConstructor -public class TenantRepoLoader { - - private static final int DEVICE_COUNT = 100000; - private static final int ATTRS_PER_DEVICE = 30; - private static final int ATTRS_AVG_STR_LENGTH = 12; - private static final int ATTRS_AVG_JSON_LENGTH = 265; - private static final int TS_PER_DEVICE = 29; - private static final int TS_AVG_STR_LENGTH = 59; - private static final int TS_AVG_JSON_LENGTH = 4005; - - private static final Map ATTR_CHANCES = new HashMap<>(); - private static final Random random = new Random(); - - static { - ATTR_CHANCES.put(DataType.BOOLEAN, 5); - ATTR_CHANCES.put(DataType.STRING, 49); - ATTR_CHANCES.put(DataType.LONG, 34); - ATTR_CHANCES.put(DataType.DOUBLE, 2); - ATTR_CHANCES.put(DataType.JSON, 10); - } - - private static final Map TS_CHANCES = new HashMap<>(); - - static { - TS_CHANCES.put(DataType.BOOLEAN, 6); - TS_CHANCES.put(DataType.STRING, 19); - TS_CHANCES.put(DataType.LONG, 36); - TS_CHANCES.put(DataType.DOUBLE, 32); - TS_CHANCES.put(DataType.JSON, 7); - } - - - @Getter - private final TenantRepo tenantRepo; - - public void load() { - long ts = System.currentTimeMillis() - DEVICE_COUNT; - for (int i = 0; i < DEVICE_COUNT; i++) { - DeviceId deviceId = new DeviceId(UUID.randomUUID()); - Device device = new Device(); - device.setId(deviceId); - device.setCreatedTime(ts + i); - device.setName("Device " + i); - device.setLabel("Device Label" + i); - device.setType("Device Type " + (i % 100)); - tenantRepo.addOrUpdate(EdqsConverter.toEntity(EntityType.DEVICE, device)); - for (int j = 0; j < ATTRS_PER_DEVICE; j++) { - String key = getRandomKey(); - AttributeKv attributeKv = new AttributeKv(); - attributeKv.setEntityId(deviceId); - attributeKv.setScope(AttributeScope.SERVER_SCOPE); - attributeKv.setKey(key); - attributeKv.setLastUpdateTs(ts); - attributeKv.setValue(getRandomKvEntry(key, ATTR_CHANCES, ATTRS_AVG_STR_LENGTH, ATTRS_AVG_JSON_LENGTH)); - tenantRepo.addOrUpdateAttribute(attributeKv); - } - for (int j = 0; j < TS_PER_DEVICE; j++) { - String key = getRandomKey(); - LatestTsKv latestTsKv = new LatestTsKv(); - latestTsKv.setEntityId(deviceId); - latestTsKv.setKey(key); - latestTsKv.setTs(ts); - latestTsKv.setValue(getRandomKvEntry(key, TS_CHANCES, TS_AVG_STR_LENGTH, TS_AVG_JSON_LENGTH)); - tenantRepo.addOrUpdateLatestKv(latestTsKv); - } - } - } - - private KvEntry getRandomKvEntry(String key, Map chances, int strLength, int jsnLength) { - int i = random.nextInt(100); - int s = 0; - for (var pair : chances.entrySet()) { - s += pair.getValue(); - if (i < s) { - switch (pair.getKey()) { - case BOOLEAN -> { - return new BooleanDataEntry(key, random.nextBoolean()); - } - case LONG -> { - return new LongDataEntry(key, random.nextLong()); - } - case DOUBLE -> { - return new DoubleDataEntry(key, random.nextDouble()); - } - case STRING -> { - return new StringDataEntry(key, StringUtils.randomAlphanumeric(strLength)); - } - case JSON -> { - return new JsonDataEntry(key, StringUtils.randomAlphanumeric(jsnLength)); - } - } - } - } - throw new RuntimeException("Something went wrong"); - } - - private String getRandomKey() { - return RandomStringUtils.randomAlphabetic(10); - } - -} diff --git a/common/edqs/src/main/java/org/thingsboard/server/edqs/processor/EdqsConverter.java b/common/edqs/src/main/java/org/thingsboard/server/edqs/processor/EdqsConverter.java index e4b9c2a35e..f5e405e516 100644 --- a/common/edqs/src/main/java/org/thingsboard/server/edqs/processor/EdqsConverter.java +++ b/common/edqs/src/main/java/org/thingsboard/server/edqs/processor/EdqsConverter.java @@ -15,14 +15,17 @@ */ package org.thingsboard.server.edqs.processor; +import com.google.protobuf.ByteString; import lombok.RequiredArgsConstructor; import lombok.SneakyThrows; +import lombok.extern.slf4j.Slf4j; import org.springframework.stereotype.Service; import org.thingsboard.common.util.JacksonUtil; import org.thingsboard.server.common.data.AttributeScope; import org.thingsboard.server.common.data.EntityType; import org.thingsboard.server.common.data.ObjectType; import org.thingsboard.server.common.data.edqs.AttributeKv; +import org.thingsboard.server.common.data.edqs.DataPoint; import org.thingsboard.server.common.data.edqs.EdqsObject; import org.thingsboard.server.common.data.edqs.Entity; import org.thingsboard.server.common.data.edqs.LatestTsKv; @@ -31,15 +34,25 @@ import org.thingsboard.server.common.data.id.EntityId; import org.thingsboard.server.common.data.id.EntityIdFactory; import org.thingsboard.server.common.data.kv.KvEntry; import org.thingsboard.server.common.data.relation.EntityRelation; -import org.thingsboard.server.common.util.KvProtoUtil; import org.thingsboard.server.common.util.ProtoUtils; +import org.thingsboard.server.edqs.data.dp.BoolDataPoint; +import org.thingsboard.server.edqs.data.dp.CompressedJsonDataPoint; +import org.thingsboard.server.edqs.data.dp.CompressedStringDataPoint; +import org.thingsboard.server.edqs.data.dp.DoubleDataPoint; +import org.thingsboard.server.edqs.data.dp.JsonDataPoint; +import org.thingsboard.server.edqs.data.dp.LongDataPoint; +import org.thingsboard.server.edqs.data.dp.StringDataPoint; +import org.thingsboard.server.edqs.repo.TbBytePool; import org.thingsboard.server.gen.transport.TransportProtos; +import org.thingsboard.server.gen.transport.TransportProtos.DataPointProto; +import org.xerial.snappy.Snappy; import java.util.HashMap; import java.util.Map; import java.util.UUID; @Service +@Slf4j public class EdqsConverter { private final Map> converters = new HashMap<>(); @@ -50,7 +63,6 @@ public class EdqsConverter { converters.put(ObjectType.ATTRIBUTE_KV, new Converter() { @Override public byte[] serialize(ObjectType type, AttributeKv attributeKv) { - // TODO: some attributes may not fit into kafka var proto = TransportProtos.AttributeKvProto.newBuilder() .setEntityIdMSB(attributeKv.getEntityId().getId().getMostSignificantBits()) .setEntityIdLSB(attributeKv.getEntityId().getId().getLeastSignificantBits()) @@ -58,11 +70,8 @@ public class EdqsConverter { .setScope(TransportProtos.AttributeScopeProto.forNumber(attributeKv.getScope().ordinal())) .setKey(attributeKv.getKey()) .setVersion(attributeKv.getVersion()); - if (attributeKv.getLastUpdateTs() != null) { - proto.setLastUpdateTs(attributeKv.getLastUpdateTs()); - } - if (attributeKv.getValue() != null) { - proto.setValue(KvProtoUtil.toKeyValueTypeProto(attributeKv.getValue())); + if (attributeKv.getLastUpdateTs() != null && attributeKv.getValue() != null) { + proto.setDataPoint(toDataPointProto(attributeKv.getLastUpdateTs(), attributeKv.getValue())); } return proto.build().toByteArray(); } @@ -73,14 +82,13 @@ public class EdqsConverter { EntityId entityId = EntityIdFactory.getByTypeAndUuid(ProtoUtils.fromProto(proto.getEntityType()), new UUID(proto.getEntityIdMSB(), proto.getEntityIdLSB())); AttributeScope scope = AttributeScope.values()[proto.getScope().getNumber()]; - KvEntry value = proto.hasValue() ? KvProtoUtil.fromTsKvProto(proto.getValue()) : null; + DataPoint dataPoint = proto.hasDataPoint() ? fromDataPointProto(proto.getDataPoint()) : null; return AttributeKv.builder() .entityId(entityId) .scope(scope) .key(proto.getKey()) .version(proto.getVersion()) - .lastUpdateTs(proto.getLastUpdateTs()) - .value(value) + .dataPoint(dataPoint) .build(); } }); @@ -93,11 +101,8 @@ public class EdqsConverter { .setEntityType(ProtoUtils.toProto(latestTsKv.getEntityId().getEntityType())) .setKey(latestTsKv.getKey()) .setVersion(latestTsKv.getVersion()); - if (latestTsKv.getTs() != null) { - proto.setTs(latestTsKv.getTs()); - } - if (latestTsKv.getValue() != null) { - proto.setValue(KvProtoUtil.toKeyValueTypeProto(latestTsKv.getValue())); + if (latestTsKv.getTs() != null && latestTsKv.getValue() != null) { + proto.setDataPoint(toDataPointProto(latestTsKv.getTs(), latestTsKv.getValue())); } return proto.build().toByteArray(); } @@ -107,18 +112,73 @@ public class EdqsConverter { TransportProtos.LatestTsKvProto proto = TransportProtos.LatestTsKvProto.parseFrom(bytes); EntityId entityId = EntityIdFactory.getByTypeAndUuid(ProtoUtils.fromProto(proto.getEntityType()), new UUID(proto.getEntityIdMSB(), proto.getEntityIdLSB())); - KvEntry value = proto.hasValue() ? KvProtoUtil.fromTsKvProto(proto.getValue()) : null; + DataPoint dataPoint = proto.hasDataPoint() ? fromDataPointProto(proto.getDataPoint()) : null; return LatestTsKv.builder() .entityId(entityId) .key(proto.getKey()) - .ts(proto.getTs()) .version(proto.getVersion()) - .value(value) + .dataPoint(dataPoint) .build(); } }); } + public static DataPointProto toDataPointProto(long ts, KvEntry kvEntry) { + DataPointProto.Builder proto = DataPointProto.newBuilder(); + proto.setTs(ts); + switch (kvEntry.getDataType()) { + case BOOLEAN -> proto.setBoolV(kvEntry.getBooleanValue().get()); + case LONG -> proto.setLongV(kvEntry.getLongValue().get()); + case DOUBLE -> proto.setDoubleV(kvEntry.getDoubleValue().get()); + case STRING -> { + String strValue = kvEntry.getStrValue().get(); + if (strValue.length() < CompressedStringDataPoint.MIN_STR_SIZE_TO_COMPRESS) { + proto.setStringV(strValue); + } else { + proto.setCompressedStringV(ByteString.copyFrom(compress(strValue))); + } + } + case JSON -> { + String jsonValue = kvEntry.getJsonValue().get(); + if (jsonValue.length() < CompressedStringDataPoint.MIN_STR_SIZE_TO_COMPRESS) { + proto.setJsonV(jsonValue); + } else { + proto.setCompressedJsonV(ByteString.copyFrom(compress(jsonValue))); + } + } + } + return proto.build(); + } + + public static DataPoint fromDataPointProto(DataPointProto proto) { + long ts = proto.getTs(); + if (proto.hasBoolV()) { + return new BoolDataPoint(ts, proto.getBoolV()); + } else if (proto.hasLongV()) { + return new LongDataPoint(ts, proto.getLongV()); + } else if (proto.hasDoubleV()) { + return new DoubleDataPoint(ts, proto.getDoubleV()); + } else if (proto.hasStringV()) { + return new StringDataPoint(ts, proto.getStringV()); + } else if (proto.hasCompressedStringV()) { + return new CompressedStringDataPoint(ts, TbBytePool.intern(proto.getCompressedStringV().toByteArray())); + } else if (proto.hasJsonV()) { + return new JsonDataPoint(ts, proto.getJsonV()); + } else if (proto.hasCompressedJsonV()) { + return new CompressedJsonDataPoint(ts, TbBytePool.intern(proto.getCompressedJsonV().toByteArray())); + } else { + throw new IllegalArgumentException("Unsupported data point proto: " + proto); + } + } + + @SneakyThrows + private static byte[] compress(String value) { + byte[] compressed = Snappy.compress(value); + // TODO: limit the size + log.debug("Compressed {} bytes to {} bytes", value.length(), compressed.length); + return compressed; + } + public static Entity toEntity(EntityType entityType, Object entity) { Entity edqsEntity = new Entity(); edqsEntity.setType(entityType); diff --git a/common/edqs/src/main/java/org/thingsboard/server/edqs/processor/EdqsProducer.java b/common/edqs/src/main/java/org/thingsboard/server/edqs/processor/EdqsProducer.java index 9b1e925367..8973f3b5b8 100644 --- a/common/edqs/src/main/java/org/thingsboard/server/edqs/processor/EdqsProducer.java +++ b/common/edqs/src/main/java/org/thingsboard/server/edqs/processor/EdqsProducer.java @@ -17,6 +17,7 @@ package org.thingsboard.server.edqs.processor; import lombok.Builder; import lombok.extern.slf4j.Slf4j; +import org.apache.kafka.common.errors.RecordTooLargeException; import org.thingsboard.server.common.data.ObjectType; import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.common.msg.queue.TopicPartitionInfo; @@ -58,6 +59,12 @@ public class EdqsProducer { @Override public void onFailure(Throwable t) { + if (t instanceof RecordTooLargeException) { + if (!log.isDebugEnabled()) { + log.warn("[{}][{}][{}] Failed to publish msg to {}", tenantId, type, key, topic, t); // not logging the whole message + return; + } + } log.warn("[{}][{}][{}] Failed to publish msg to {}: {}", tenantId, type, key, topic, msg, t); } }; diff --git a/common/edqs/src/main/java/org/thingsboard/server/edqs/repo/TenantRepo.java b/common/edqs/src/main/java/org/thingsboard/server/edqs/repo/TenantRepo.java index 822061e918..9d6a4ab135 100644 --- a/common/edqs/src/main/java/org/thingsboard/server/edqs/repo/TenantRepo.java +++ b/common/edqs/src/main/java/org/thingsboard/server/edqs/repo/TenantRepo.java @@ -19,6 +19,7 @@ import lombok.extern.slf4j.Slf4j; import org.thingsboard.server.common.data.EntityType; import org.thingsboard.server.common.data.ObjectType; import org.thingsboard.server.common.data.edqs.AttributeKv; +import org.thingsboard.server.common.data.edqs.DataPoint; import org.thingsboard.server.common.data.edqs.EdqsEvent; import org.thingsboard.server.common.data.edqs.EdqsEventType; import org.thingsboard.server.common.data.edqs.EdqsObject; @@ -30,7 +31,6 @@ import org.thingsboard.server.common.data.edqs.query.QueryResult; import org.thingsboard.server.common.data.id.CustomerId; import org.thingsboard.server.common.data.id.EntityId; import org.thingsboard.server.common.data.id.TenantId; -import org.thingsboard.server.common.data.kv.KvEntry; import org.thingsboard.server.common.data.page.PageData; import org.thingsboard.server.common.data.permission.QueryContext; import org.thingsboard.server.common.data.query.EntityCountQuery; @@ -50,14 +50,6 @@ import org.thingsboard.server.edqs.data.EntityProfileData; import org.thingsboard.server.edqs.data.GenericData; import org.thingsboard.server.edqs.data.RelationsRepo; import org.thingsboard.server.edqs.data.TenantData; -import org.thingsboard.server.edqs.data.dp.BoolDataPoint; -import org.thingsboard.server.edqs.data.dp.CompressedJsonDataPoint; -import org.thingsboard.server.edqs.data.dp.CompressedStringDataPoint; -import org.thingsboard.server.edqs.data.dp.DataPoint; -import org.thingsboard.server.edqs.data.dp.DoubleDataPoint; -import org.thingsboard.server.edqs.data.dp.JsonDataPoint; -import org.thingsboard.server.edqs.data.dp.LongDataPoint; -import org.thingsboard.server.edqs.data.dp.StringDataPoint; import org.thingsboard.server.edqs.query.EdqsDataQuery; import org.thingsboard.server.edqs.query.EdqsQuery; import org.thingsboard.server.edqs.query.SortableEntityData; @@ -240,9 +232,8 @@ public class TenantRepo { public void addOrUpdateAttribute(AttributeKv attributeKv) { var entityData = getOrCreate(attributeKv.getEntityId()); if (entityData != null) { - KvEntry value = attributeKv.getValue(); Integer keyId = KeyDictionary.get(attributeKv.getKey()); - boolean added = entityData.putAttr(keyId, attributeKv.getScope(), toDataPoint(attributeKv.getLastUpdateTs(), value)); + boolean added = entityData.putAttr(keyId, attributeKv.getScope(), attributeKv.getDataPoint()); if (added) { edqsStatsService.ifPresent(statService -> statService.reportEvent(tenantId, ObjectType.ATTRIBUTE_KV, EdqsEventType.UPDATED)); } @@ -262,9 +253,8 @@ public class TenantRepo { public void addOrUpdateLatestKv(LatestTsKv latestTsKv) { var entityData = getOrCreate(latestTsKv.getEntityId()); if (entityData != null) { - KvEntry value = latestTsKv.getValue(); Integer keyId = KeyDictionary.get(latestTsKv.getKey()); - boolean added = entityData.putTs(keyId, toDataPoint(latestTsKv.getTs(), value)); + boolean added = entityData.putTs(keyId, latestTsKv.getDataPoint()); if (added) { edqsStatsService.ifPresent(statService -> statService.reportEvent(tenantId, ObjectType.LATEST_TS_KV, EdqsEventType.UPDATED)); } @@ -281,42 +271,12 @@ public class TenantRepo { } } - private DataPoint toDataPoint(long ts, KvEntry kvEntry) { - return switch (kvEntry.getDataType()) { - case BOOLEAN -> new BoolDataPoint(ts, kvEntry.getBooleanValue().get()); - case STRING -> getStrDataPoint(ts, kvEntry.getStrValue().get()); - case LONG -> new LongDataPoint(ts, kvEntry.getLongValue().get()); - case DOUBLE -> new DoubleDataPoint(ts, kvEntry.getDoubleValue().get()); - case JSON -> getJsonDataPoint(ts, kvEntry.getJsonValue().get()); - }; - } - public void processFields(EntityFields fields) { if (fields instanceof AssetFields assetFields) { assetFields.setType(TbStringPool.intern(assetFields.getType())); } } - private static DataPoint getStrDataPoint(long ts, String strV) { - DataPoint dp; - if (strV.length() < CompressedStringDataPoint.MIN_STR_SIZE_TO_COMPRESS) { - dp = new StringDataPoint(ts, strV); - } else { - dp = new CompressedStringDataPoint(ts, strV); - } - return dp; - } - - private static DataPoint getJsonDataPoint(long ts, String strV) { - DataPoint dp; - if (strV.length() < CompressedStringDataPoint.MIN_STR_SIZE_TO_COMPRESS) { - dp = new JsonDataPoint(ts, strV); - } else { - dp = new CompressedJsonDataPoint(ts, strV); - } - return dp; - } - public ConcurrentMap> getEntityMap(EntityType entityType) { return entityMapByType.computeIfAbsent(entityType, et -> new ConcurrentHashMap<>()); } 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 b06db5519b..de54571d11 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 @@ -18,6 +18,7 @@ package org.thingsboard.server.edqs.util; import lombok.extern.slf4j.Slf4j; import org.thingsboard.server.common.data.EntityType; import org.thingsboard.server.common.data.StringUtils; +import org.thingsboard.server.common.data.edqs.DataPoint; import org.thingsboard.server.common.data.permission.QueryContext; import org.thingsboard.server.common.data.query.BooleanFilterPredicate; import org.thingsboard.server.common.data.query.ComplexFilterPredicate; @@ -41,7 +42,6 @@ import org.thingsboard.server.common.data.query.StringFilterPredicate; import org.thingsboard.server.common.data.query.TsValue; import org.thingsboard.server.common.data.util.CollectionsUtil; import org.thingsboard.server.edqs.data.EntityData; -import org.thingsboard.server.edqs.data.dp.DataPoint; import org.thingsboard.server.edqs.query.DataKey; import org.thingsboard.server.edqs.query.EdqsCountQuery; import org.thingsboard.server.edqs.query.EdqsDataQuery; diff --git a/common/proto/src/main/proto/queue.proto b/common/proto/src/main/proto/queue.proto index d21862d3e1..36c289b0ab 100644 --- a/common/proto/src/main/proto/queue.proto +++ b/common/proto/src/main/proto/queue.proto @@ -178,8 +178,7 @@ message AttributeKvProto { AttributeScopeProto scope = 4; string key = 5; int64 version = 6; - int64 lastUpdateTs = 7; - KeyValueProto value = 8; + DataPointProto dataPoint = 7; } message TsKvProto { @@ -193,9 +192,8 @@ message LatestTsKvProto { int64 entityIdLSB = 2; EntityTypeProto entityType = 3; string key = 4; - int64 ts = 5; - int64 version = 6; - KeyValueProto value = 7; + int64 version = 5; + DataPointProto dataPoint = 6; } message TsKvListProto { @@ -203,6 +201,17 @@ message TsKvListProto { repeated KeyValueProto kv = 2; } +message DataPointProto { + int64 ts = 1; + optional bool boolV = 2; + optional int64 longV = 3; + optional double doubleV = 4; + optional string stringV = 5; + optional bytes compressedStringV = 6; + optional string jsonV = 7; + optional bytes compressedJsonV = 8; +} + message DeviceInfoProto { int64 tenantIdMSB = 1; int64 tenantIdLSB = 2; From 2a003709e0d93bfbb58ab0c20dac3c99841fe284 Mon Sep 17 00:00:00 2001 From: ViacheslavKlimov Date: Wed, 19 Feb 2025 11:21:01 +0200 Subject: [PATCH 070/345] Fix tests --- .../server/service/edqs/DefaultEdqsService.java | 4 ++-- .../server/common/data/edqs/AttributeKv.java | 6 ++++++ .../server/common/data/edqs/EdqsObject.java | 4 ++++ .../thingsboard/server/common/data/edqs/Entity.java | 6 ++++++ .../server/common/data/edqs/LatestTsKv.java | 6 ++++++ .../server/common/data/relation/EntityRelation.java | 6 ++++++ .../server/edqs/processor/EdqsProcessor.java | 3 ++- .../server/edqs/processor/EdqsProducer.java | 2 +- .../edqs/{util => state}/EdqsPartitionService.java | 2 +- .../server/edqs/state/KafkaEdqsStateService.java | 1 - .../server/edqs/{processor => util}/EdqsConverter.java | 2 +- .../thingsboard/server/edqs/repo/AbstractEDQTest.java | 10 +++++++--- 12 files changed, 42 insertions(+), 10 deletions(-) rename common/edqs/src/main/java/org/thingsboard/server/edqs/{util => state}/EdqsPartitionService.java (97%) rename common/edqs/src/main/java/org/thingsboard/server/edqs/{processor => util}/EdqsConverter.java (99%) diff --git a/application/src/main/java/org/thingsboard/server/service/edqs/DefaultEdqsService.java b/application/src/main/java/org/thingsboard/server/service/edqs/DefaultEdqsService.java index e7243ed3bc..5f30bc291a 100644 --- a/application/src/main/java/org/thingsboard/server/service/edqs/DefaultEdqsService.java +++ b/application/src/main/java/org/thingsboard/server/service/edqs/DefaultEdqsService.java @@ -55,9 +55,9 @@ import org.thingsboard.server.common.data.kv.KvEntry; import org.thingsboard.server.common.msg.edqs.EdqsService; import org.thingsboard.server.common.msg.queue.ServiceType; import org.thingsboard.server.dao.attributes.AttributesService; -import org.thingsboard.server.edqs.processor.EdqsConverter; +import org.thingsboard.server.edqs.util.EdqsConverter; import org.thingsboard.server.edqs.processor.EdqsProducer; -import org.thingsboard.server.edqs.util.EdqsPartitionService; +import org.thingsboard.server.edqs.state.EdqsPartitionService; import org.thingsboard.server.gen.transport.TransportProtos; import org.thingsboard.server.gen.transport.TransportProtos.EdqsEventMsg; import org.thingsboard.server.gen.transport.TransportProtos.EdqsRequestMsg; diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/edqs/AttributeKv.java b/common/data/src/main/java/org/thingsboard/server/common/data/edqs/AttributeKv.java index 9f65f72be4..c0e3abe9e3 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/edqs/AttributeKv.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/edqs/AttributeKv.java @@ -20,6 +20,7 @@ import lombok.Builder; import lombok.Data; import lombok.NoArgsConstructor; import org.thingsboard.server.common.data.AttributeScope; +import org.thingsboard.server.common.data.ObjectType; import org.thingsboard.server.common.data.id.EntityId; import org.thingsboard.server.common.data.kv.AttributeKvEntry; import org.thingsboard.server.common.data.kv.KvEntry; @@ -66,4 +67,9 @@ public class AttributeKv implements EdqsObject { return version; } + @Override + public ObjectType type() { + return ObjectType.ATTRIBUTE_KV; + } + } diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/edqs/EdqsObject.java b/common/data/src/main/java/org/thingsboard/server/common/data/edqs/EdqsObject.java index 9a2836149a..d1e463443c 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/edqs/EdqsObject.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/edqs/EdqsObject.java @@ -16,6 +16,7 @@ package org.thingsboard.server.common.data.edqs; import com.fasterxml.jackson.annotation.JsonIgnore; +import org.thingsboard.server.common.data.ObjectType; public interface EdqsObject { @@ -25,4 +26,7 @@ public interface EdqsObject { @JsonIgnore Long version(); + @JsonIgnore + ObjectType type(); + } diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/edqs/Entity.java b/common/data/src/main/java/org/thingsboard/server/common/data/edqs/Entity.java index decfbbd116..c08047c005 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/edqs/Entity.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/edqs/Entity.java @@ -20,6 +20,7 @@ import com.fasterxml.jackson.annotation.JsonTypeInfo; import lombok.Data; import lombok.NoArgsConstructor; import org.thingsboard.server.common.data.EntityType; +import org.thingsboard.server.common.data.ObjectType; import org.thingsboard.server.common.data.edqs.fields.EntityFields; import org.thingsboard.server.common.data.edqs.fields.EntityIdFields; @@ -59,4 +60,9 @@ public class Entity implements EdqsObject { return fields.getVersion(); } + @Override + public ObjectType type() { + return ObjectType.fromEntityType(type); + } + } diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/edqs/LatestTsKv.java b/common/data/src/main/java/org/thingsboard/server/common/data/edqs/LatestTsKv.java index 19a767a320..b6a466df12 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/edqs/LatestTsKv.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/edqs/LatestTsKv.java @@ -19,6 +19,7 @@ import lombok.AllArgsConstructor; import lombok.Builder; import lombok.Data; import lombok.NoArgsConstructor; +import org.thingsboard.server.common.data.ObjectType; import org.thingsboard.server.common.data.id.EntityId; import org.thingsboard.server.common.data.kv.KvEntry; import org.thingsboard.server.common.data.kv.TsKvEntry; @@ -61,4 +62,9 @@ public class LatestTsKv implements EdqsObject { return version; } + @Override + public ObjectType type() { + return ObjectType.LATEST_TS_KV; + } + } diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/relation/EntityRelation.java b/common/data/src/main/java/org/thingsboard/server/common/data/relation/EntityRelation.java index 927d592cd7..c04c490a26 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/relation/EntityRelation.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/relation/EntityRelation.java @@ -25,6 +25,7 @@ import lombok.ToString; import lombok.extern.slf4j.Slf4j; import org.thingsboard.server.common.data.BaseDataWithAdditionalInfo; import org.thingsboard.server.common.data.HasVersion; +import org.thingsboard.server.common.data.ObjectType; import org.thingsboard.server.common.data.edqs.EdqsObject; import org.thingsboard.server.common.data.id.EntityId; import org.thingsboard.server.common.data.validation.Length; @@ -127,4 +128,9 @@ public class EntityRelation implements HasVersion, Serializable, EdqsObject { return version; } + @Override + public ObjectType type() { + return ObjectType.RELATION; + } + } diff --git a/common/edqs/src/main/java/org/thingsboard/server/edqs/processor/EdqsProcessor.java b/common/edqs/src/main/java/org/thingsboard/server/edqs/processor/EdqsProcessor.java index a90a029bc4..7e9d60d09a 100644 --- a/common/edqs/src/main/java/org/thingsboard/server/edqs/processor/EdqsProcessor.java +++ b/common/edqs/src/main/java/org/thingsboard/server/edqs/processor/EdqsProcessor.java @@ -49,7 +49,8 @@ import org.thingsboard.server.common.msg.queue.ServiceType; import org.thingsboard.server.common.msg.queue.TopicPartitionInfo; import org.thingsboard.server.edqs.repo.EdqRepository; import org.thingsboard.server.edqs.state.EdqsStateService; -import org.thingsboard.server.edqs.util.EdqsPartitionService; +import org.thingsboard.server.edqs.util.EdqsConverter; +import org.thingsboard.server.edqs.state.EdqsPartitionService; import org.thingsboard.server.edqs.util.VersionsStore; import org.thingsboard.server.gen.transport.TransportProtos; import org.thingsboard.server.gen.transport.TransportProtos.EdqsEventMsg; diff --git a/common/edqs/src/main/java/org/thingsboard/server/edqs/processor/EdqsProducer.java b/common/edqs/src/main/java/org/thingsboard/server/edqs/processor/EdqsProducer.java index 8973f3b5b8..bfd0d9df59 100644 --- a/common/edqs/src/main/java/org/thingsboard/server/edqs/processor/EdqsProducer.java +++ b/common/edqs/src/main/java/org/thingsboard/server/edqs/processor/EdqsProducer.java @@ -21,7 +21,7 @@ import org.apache.kafka.common.errors.RecordTooLargeException; import org.thingsboard.server.common.data.ObjectType; import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.common.msg.queue.TopicPartitionInfo; -import org.thingsboard.server.edqs.util.EdqsPartitionService; +import org.thingsboard.server.edqs.state.EdqsPartitionService; import org.thingsboard.server.gen.transport.TransportProtos.ToEdqsMsg; import org.thingsboard.server.queue.TbQueueCallback; import org.thingsboard.server.queue.TbQueueMsgMetadata; diff --git a/common/edqs/src/main/java/org/thingsboard/server/edqs/util/EdqsPartitionService.java b/common/edqs/src/main/java/org/thingsboard/server/edqs/state/EdqsPartitionService.java similarity index 97% rename from common/edqs/src/main/java/org/thingsboard/server/edqs/util/EdqsPartitionService.java rename to common/edqs/src/main/java/org/thingsboard/server/edqs/state/EdqsPartitionService.java index 45de60687c..3d7757f17b 100644 --- a/common/edqs/src/main/java/org/thingsboard/server/edqs/util/EdqsPartitionService.java +++ b/common/edqs/src/main/java/org/thingsboard/server/edqs/state/EdqsPartitionService.java @@ -13,7 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package org.thingsboard.server.edqs.util; +package org.thingsboard.server.edqs.state; import lombok.RequiredArgsConstructor; import org.springframework.stereotype.Service; diff --git a/common/edqs/src/main/java/org/thingsboard/server/edqs/state/KafkaEdqsStateService.java b/common/edqs/src/main/java/org/thingsboard/server/edqs/state/KafkaEdqsStateService.java index cfa831f720..6ab1fb9e1f 100644 --- a/common/edqs/src/main/java/org/thingsboard/server/edqs/state/KafkaEdqsStateService.java +++ b/common/edqs/src/main/java/org/thingsboard/server/edqs/state/KafkaEdqsStateService.java @@ -30,7 +30,6 @@ import org.thingsboard.server.common.msg.queue.ServiceType; import org.thingsboard.server.common.msg.queue.TopicPartitionInfo; import org.thingsboard.server.edqs.processor.EdqsProcessor; import org.thingsboard.server.edqs.processor.EdqsProducer; -import org.thingsboard.server.edqs.util.EdqsPartitionService; import org.thingsboard.server.edqs.util.VersionsStore; import org.thingsboard.server.gen.transport.TransportProtos.EdqsEventMsg; import org.thingsboard.server.gen.transport.TransportProtos.ToEdqsMsg; diff --git a/common/edqs/src/main/java/org/thingsboard/server/edqs/processor/EdqsConverter.java b/common/edqs/src/main/java/org/thingsboard/server/edqs/util/EdqsConverter.java similarity index 99% rename from common/edqs/src/main/java/org/thingsboard/server/edqs/processor/EdqsConverter.java rename to common/edqs/src/main/java/org/thingsboard/server/edqs/util/EdqsConverter.java index f5e405e516..b7419451b9 100644 --- a/common/edqs/src/main/java/org/thingsboard/server/edqs/processor/EdqsConverter.java +++ b/common/edqs/src/main/java/org/thingsboard/server/edqs/util/EdqsConverter.java @@ -13,7 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package org.thingsboard.server.edqs.processor; +package org.thingsboard.server.edqs.util; import com.google.protobuf.ByteString; import lombok.RequiredArgsConstructor; diff --git a/edqs/src/test/java/org/thingsboard/server/edqs/repo/AbstractEDQTest.java b/edqs/src/test/java/org/thingsboard/server/edqs/repo/AbstractEDQTest.java index 8fb680df1c..1f22f9d5ae 100644 --- a/edqs/src/test/java/org/thingsboard/server/edqs/repo/AbstractEDQTest.java +++ b/edqs/src/test/java/org/thingsboard/server/edqs/repo/AbstractEDQTest.java @@ -59,7 +59,7 @@ import org.thingsboard.server.common.data.query.KeyFilter; import org.thingsboard.server.common.data.query.StringFilterPredicate; import org.thingsboard.server.common.data.relation.EntityRelation; import org.thingsboard.server.common.data.relation.RelationTypeGroup; -import org.thingsboard.server.edqs.processor.EdqsConverter; +import org.thingsboard.server.edqs.util.EdqsConverter; import java.util.Collections; import java.util.List; @@ -67,7 +67,7 @@ import java.util.UUID; @RunWith(SpringRunner.class) @Configuration -@ComponentScan({"org.thingsboard.server.edqs.repo"}) +@ComponentScan({"org.thingsboard.server.edqs.repo", "org.thingsboard.server.edqs.util"}) @EntityScan("org.thingsboard.server.edqs") @TestPropertySource(locations = {"classpath:edq-test.properties"}) @TestExecutionListeners({ @@ -77,6 +77,8 @@ public abstract class AbstractEDQTest { @Autowired protected InMemoryEdqRepository repository; + @Autowired + protected EdqsConverter edqsConverter; protected final TenantId tenantId = TenantId.fromUUID(UUID.randomUUID()); protected final CustomerId customerId = new CustomerId(UUID.randomUUID()); @@ -218,7 +220,7 @@ public abstract class AbstractEDQTest { } protected void createRelation(EntityType fromType, UUID fromId, EntityType toType, UUID toId, RelationTypeGroup group, String type) { - repository.get(tenantId).addOrUpdate(new EntityRelation(EntityIdFactory.getByTypeAndUuid(fromType, fromId), EntityIdFactory.getByTypeAndUuid(toType, toId), type, group)); + addOrUpdate(new EntityRelation(EntityIdFactory.getByTypeAndUuid(fromType, fromId), EntityIdFactory.getByTypeAndUuid(toType, toId), type, group)); } @@ -243,6 +245,8 @@ public abstract class AbstractEDQTest { } protected void addOrUpdate(EdqsObject edqsObject) { + byte[] serialized = edqsConverter.serialize(edqsObject.type(), edqsObject); + edqsObject = edqsConverter.deserialize(edqsObject.type(), serialized); repository.get(tenantId).addOrUpdate(edqsObject); } From 2580f4d3e5b758e4bc63ea3130fd1993de3b1b35 Mon Sep 17 00:00:00 2001 From: dashevchenko Date: Wed, 19 Feb 2025 12:05:05 +0200 Subject: [PATCH 071/345] fixed buildEntityDataQuery for AlarmDataQuery, key filtering for string null values --- .../server/service/subscription/TbAlarmDataSubCtx.java | 2 +- .../org/thingsboard/server/edqs/util/RepositoryUtils.java | 7 ++++--- 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/application/src/main/java/org/thingsboard/server/service/subscription/TbAlarmDataSubCtx.java b/application/src/main/java/org/thingsboard/server/service/subscription/TbAlarmDataSubCtx.java index 70a259af6c..f5964c5d73 100644 --- a/application/src/main/java/org/thingsboard/server/service/subscription/TbAlarmDataSubCtx.java +++ b/application/src/main/java/org/thingsboard/server/service/subscription/TbAlarmDataSubCtx.java @@ -359,7 +359,7 @@ public class TbAlarmDataSubCtx extends TbAbstractDataSubCtx { EntityDataSortOrder sortOrder = query.getPageLink().getSortOrder(); EntityDataSortOrder entitiesSortOrder; if (sortOrder == null || sortOrder.getKey().getType().equals(EntityKeyType.ALARM_FIELD)) { - entitiesSortOrder = new EntityDataSortOrder(new EntityKey(EntityKeyType.ENTITY_FIELD, ModelConstants.CREATED_TIME_PROPERTY)); + entitiesSortOrder = new EntityDataSortOrder(new EntityKey(EntityKeyType.ENTITY_FIELD, "createdTime")); } else { entitiesSortOrder = sortOrder; } 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 b06db5519b..e82c8d8b94 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 @@ -54,6 +54,7 @@ import java.util.ArrayList; import java.util.Collections; import java.util.Comparator; import java.util.List; +import java.util.Optional; import java.util.regex.Pattern; import java.util.stream.Stream; @@ -66,10 +67,10 @@ import static org.thingsboard.server.common.data.query.ComplexFilterPredicate.Co @Slf4j public class RepositoryUtils { - public static final Comparator SORT_ASC = Comparator.comparing(SortableEntityData::getSortValue) + public static final Comparator SORT_ASC = Comparator.comparing((SortableEntityData sed) -> Optional.ofNullable(sed.getSortValue()).orElse("")) .thenComparing(sp -> sp.getId().toString()); - public static final Comparator SORT_DESC = Comparator.comparing(SortableEntityData::getSortValue) + public static final Comparator SORT_DESC = Comparator.comparing((SortableEntityData sed) -> Optional.ofNullable(sed.getSortValue()).orElse("")) .thenComparing(sp -> sp.getId().toString()).reversed(); public static EntityType resolveEntityType(EntityFilter entityFilter) { @@ -210,7 +211,7 @@ public class RepositoryUtils { boolean checkResult = switch (valueType) { case STRING -> { String str = dp != null ? dp.valueToString() : null; - yield StringUtils.isEmpty(str) || checkKeyFilter(str, keyFilter.predicate()); + yield str != null && checkKeyFilter(str, keyFilter.predicate()); } case BOOLEAN -> { Boolean booleanValue = dp != null ? dp.getBool() : null; From 2431fcf6c5de78366ccb1b0cd4a45512fd046fa8 Mon Sep 17 00:00:00 2001 From: dashevchenko Date: Wed, 19 Feb 2025 12:18:44 +0200 Subject: [PATCH 072/345] fixed RepositoryUtilsTest --- .../org/thingsboard/server/edqs/repo/RepositoryUtilsTest.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/edqs/src/test/java/org/thingsboard/server/edqs/repo/RepositoryUtilsTest.java b/edqs/src/test/java/org/thingsboard/server/edqs/repo/RepositoryUtilsTest.java index 48a72a9cc6..1562978a15 100644 --- a/edqs/src/test/java/org/thingsboard/server/edqs/repo/RepositoryUtilsTest.java +++ b/edqs/src/test/java/org/thingsboard/server/edqs/repo/RepositoryUtilsTest.java @@ -54,7 +54,7 @@ import static org.assertj.core.api.Assertions.assertThat; public class RepositoryUtilsTest { private static Stream deviceNameFilters() { - return Stream.of(Arguments.of(null, getNameFilter(StringOperation.STARTS_WITH, "lora"), true), + return Stream.of(Arguments.of(null, getNameFilter(StringOperation.STARTS_WITH, "lora"), false), Arguments.of("loranet device 123", getNameFilter(StringOperation.STARTS_WITH, "lora"), true), Arguments.of("loranet 123", getNameFilter(StringOperation.STARTS_WITH, "ra"), false), Arguments.of("loranet 123", getNameFilter(StringOperation.ENDS_WITH, "123"), true), @@ -132,7 +132,7 @@ public class RepositoryUtilsTest { } private static Stream deviceNameComplexFilters() { - return Stream.of(Arguments.of(null, List.of(getComplexComplexDeviceNameFilter(StringOperation.STARTS_WITH, "lo", ComplexOperation.AND, StringOperation.ENDS_WITH, "123")), true), + return Stream.of(Arguments.of(null, List.of(getComplexComplexDeviceNameFilter(StringOperation.STARTS_WITH, "lo", ComplexOperation.AND, StringOperation.ENDS_WITH, "123")), false), Arguments.of("loranet 123", List.of(getComplexComplexDeviceNameFilter(StringOperation.STARTS_WITH, "lo", ComplexOperation.AND, StringOperation.ENDS_WITH, "123")), true), Arguments.of("loranet 123", List.of(getComplexComplexDeviceNameFilter(StringOperation.STARTS_WITH, "lo", ComplexOperation.AND, StringOperation.ENDS_WITH, "124")), false), Arguments.of("loranet 123", List.of(getComplexComplexDeviceNameFilter(StringOperation.STARTS_WITH, "lo", ComplexOperation.OR, StringOperation.STARTS_WITH, "net")), true), From 46005a957a2f91e6a104d5b2459146f89d40c873 Mon Sep 17 00:00:00 2001 From: dashevchenko Date: Wed, 19 Feb 2025 14:46:39 +0200 Subject: [PATCH 073/345] fixed test --- .../thingsboard/server/service/edqs/EdqsSyncService.java | 2 +- .../server/common/data/edqs/fields/TenantFields.java | 5 +++++ .../org/thingsboard/server/edqs/util/RepositoryUtils.java | 6 ++++-- .../thingsboard/server/edqs/repo/RepositoryUtilsTest.java | 4 ++-- 4 files changed, 12 insertions(+), 5 deletions(-) diff --git a/application/src/main/java/org/thingsboard/server/service/edqs/EdqsSyncService.java b/application/src/main/java/org/thingsboard/server/service/edqs/EdqsSyncService.java index 80aeee8635..8bfa16c95a 100644 --- a/application/src/main/java/org/thingsboard/server/service/edqs/EdqsSyncService.java +++ b/application/src/main/java/org/thingsboard/server/service/edqs/EdqsSyncService.java @@ -166,7 +166,7 @@ public abstract class EdqsSyncService { if (entityIdInfo != null) { process(entityIdInfo.tenantId(), RELATION, relation.toData()); } else { - log.info("Relation from entity not found: " + relation.getFromId()); + log.info("Relation from id not found: {} ", relation); } } } diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/edqs/fields/TenantFields.java b/common/data/src/main/java/org/thingsboard/server/common/data/edqs/fields/TenantFields.java index 6942d5ea7b..b86b36c1cd 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/edqs/fields/TenantFields.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/edqs/fields/TenantFields.java @@ -59,4 +59,9 @@ public class TenantFields extends AbstractEntityFields { public TenantFields(UUID id, Long version) { super(id, 0L, null, version); } + + @Override + public UUID getTenantId() { + return getId(); + } } 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 e82c8d8b94..331677e6b2 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 @@ -207,11 +207,13 @@ public class RepositoryUtils { default -> throw new IllegalStateException(); }; } - DataPoint dp = entity.getDataPoint(keyFilter.key(), null); + DataKey dataKey = keyFilter.key(); + DataPoint dp = entity.getDataPoint(dataKey, null); boolean checkResult = switch (valueType) { case STRING -> { String str = dp != null ? dp.valueToString() : null; - yield str != null && checkKeyFilter(str, keyFilter.predicate()); + yield (dataKey.type() == EntityKeyType.ENTITY_FIELD) ? (str == null || checkKeyFilter(str, keyFilter.predicate())) : + (str != null && checkKeyFilter(str, keyFilter.predicate())); } case BOOLEAN -> { Boolean booleanValue = dp != null ? dp.getBool() : null; diff --git a/edqs/src/test/java/org/thingsboard/server/edqs/repo/RepositoryUtilsTest.java b/edqs/src/test/java/org/thingsboard/server/edqs/repo/RepositoryUtilsTest.java index 1562978a15..48a72a9cc6 100644 --- a/edqs/src/test/java/org/thingsboard/server/edqs/repo/RepositoryUtilsTest.java +++ b/edqs/src/test/java/org/thingsboard/server/edqs/repo/RepositoryUtilsTest.java @@ -54,7 +54,7 @@ import static org.assertj.core.api.Assertions.assertThat; public class RepositoryUtilsTest { private static Stream deviceNameFilters() { - return Stream.of(Arguments.of(null, getNameFilter(StringOperation.STARTS_WITH, "lora"), false), + return Stream.of(Arguments.of(null, getNameFilter(StringOperation.STARTS_WITH, "lora"), true), Arguments.of("loranet device 123", getNameFilter(StringOperation.STARTS_WITH, "lora"), true), Arguments.of("loranet 123", getNameFilter(StringOperation.STARTS_WITH, "ra"), false), Arguments.of("loranet 123", getNameFilter(StringOperation.ENDS_WITH, "123"), true), @@ -132,7 +132,7 @@ public class RepositoryUtilsTest { } private static Stream deviceNameComplexFilters() { - return Stream.of(Arguments.of(null, List.of(getComplexComplexDeviceNameFilter(StringOperation.STARTS_WITH, "lo", ComplexOperation.AND, StringOperation.ENDS_WITH, "123")), false), + return Stream.of(Arguments.of(null, List.of(getComplexComplexDeviceNameFilter(StringOperation.STARTS_WITH, "lo", ComplexOperation.AND, StringOperation.ENDS_WITH, "123")), true), Arguments.of("loranet 123", List.of(getComplexComplexDeviceNameFilter(StringOperation.STARTS_WITH, "lo", ComplexOperation.AND, StringOperation.ENDS_WITH, "123")), true), Arguments.of("loranet 123", List.of(getComplexComplexDeviceNameFilter(StringOperation.STARTS_WITH, "lo", ComplexOperation.AND, StringOperation.ENDS_WITH, "124")), false), Arguments.of("loranet 123", List.of(getComplexComplexDeviceNameFilter(StringOperation.STARTS_WITH, "lo", ComplexOperation.OR, StringOperation.STARTS_WITH, "net")), true), From 44721a255ca6dbda140067da2358808a0353a152 Mon Sep 17 00:00:00 2001 From: dashevchenko Date: Wed, 19 Feb 2025 15:24:22 +0200 Subject: [PATCH 074/345] added grafana dashboard for edqs entities --- .../dashboards/edqs_entities.json | 161 ++++++++++++++++++ 1 file changed, 161 insertions(+) create mode 100644 docker/monitoring/grafana/provisioning/dashboards/edqs_entities.json diff --git a/docker/monitoring/grafana/provisioning/dashboards/edqs_entities.json b/docker/monitoring/grafana/provisioning/dashboards/edqs_entities.json new file mode 100644 index 0000000000..3e913d4856 --- /dev/null +++ b/docker/monitoring/grafana/provisioning/dashboards/edqs_entities.json @@ -0,0 +1,161 @@ +{ + "annotations": { + "list": [ + { + "builtIn": 1, + "datasource": "-- Grafana --", + "enable": true, + "hide": true, + "iconColor": "rgba(0, 211, 255, 1)", + "name": "Annotations & Alerts", + "target": { + "limit": 100, + "matchAny": false, + "tags": [], + "type": "dashboard" + }, + "type": "dashboard" + } + ] + }, + "editable": true, + "fiscalYearStartMonth": 0, + "graphTooltip": 0, + "id": 6, + "iteration": 1737564772936, + "links": [], + "liveNow": false, + "panels": [ + { + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "drawStyle": "line", + "fillOpacity": 0, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "lineInterpolation": "smooth", + "lineWidth": 1, + "pointSize": 1, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "auto", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "decimals": 0, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + }, + { + "color": "red", + "value": 80 + } + ] + } + }, + "overrides": [] + }, + "gridPos": { + "h": 10, + "w": 12, + "x": 0, + "y": 0 + }, + "id": 2, + "options": { + "legend": { + "calcs": [], + "displayMode": "list", + "placement": "bottom" + }, + "tooltip": { + "mode": "single", + "sort": "none" + } + }, + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "9BonzvTSz" + }, + "exemplar": true, + "expr": "sum by (objectType) (edqs_object_count{tenantId=~\"$tenantId\"})", + "interval": "", + "legendFormat": "{{objectType}}", + "refId": "A" + } + ], + "title": "EDQS object count", + "type": "timeseries" + } + ], + "refresh": "", + "schemaVersion": 35, + "style": "dark", + "tags": [], + "templating": { + "list": [ + { + "current": { + "selected": true, + "text": [ + "All" + ], + "value": [ + "$__all" + ] + }, + "definition": "label_values(edqs_object_count, tenantId)", + "hide": 0, + "includeAll": true, + "label": "Tenant", + "multi": true, + "name": "tenantId", + "options": [], + "query": { + "query": "label_values(edqs_object_count, tenantId)", + "refId": "StandardVariableQuery" + }, + "refresh": 1, + "regex": "", + "skipUrlSync": false, + "sort": 0, + "type": "query" + } + ] + }, + "time": { + "from": "now-15m", + "to": "now" + }, + "timepicker": {}, + "timezone": "", + "title": "EDQS", + "uid": "mK5A_DdHk", + "version": 9, + "weekStart": "" +} \ No newline at end of file From ded6daf2b3b01a463973218a2406fc8be86f722b Mon Sep 17 00:00:00 2001 From: ViacheslavKlimov Date: Wed, 19 Feb 2025 15:44:05 +0200 Subject: [PATCH 075/345] Kafka states restore improvements --- .../TbRuleEngineQueueConsumerManager.java | 10 +-- .../AbstractTbQueueConsumerTemplate.java | 3 + .../consumer/MainQueueConsumerManager.java | 59 ++++++++++---- .../PartitionedQueueConsumerManager.java | 80 +++++++++++++++++++ .../common/consumer/QueueStateService.java | 76 ++++++++++++++++++ .../{QueueEvent.java => QueueTaskType.java} | 5 +- .../consumer/TbQueueConsumerManagerTask.java | 49 ++++++++---- .../common/consumer/TbQueueConsumerTask.java | 5 +- 8 files changed, 245 insertions(+), 42 deletions(-) create mode 100644 common/queue/src/main/java/org/thingsboard/server/queue/common/consumer/PartitionedQueueConsumerManager.java create mode 100644 common/queue/src/main/java/org/thingsboard/server/queue/common/consumer/QueueStateService.java rename common/queue/src/main/java/org/thingsboard/server/queue/common/consumer/{QueueEvent.java => QueueTaskType.java} (83%) diff --git a/application/src/main/java/org/thingsboard/server/service/queue/ruleengine/TbRuleEngineQueueConsumerManager.java b/application/src/main/java/org/thingsboard/server/service/queue/ruleengine/TbRuleEngineQueueConsumerManager.java index 1f421974be..241aec25bd 100644 --- a/application/src/main/java/org/thingsboard/server/service/queue/ruleengine/TbRuleEngineQueueConsumerManager.java +++ b/application/src/main/java/org/thingsboard/server/service/queue/ruleengine/TbRuleEngineQueueConsumerManager.java @@ -33,14 +33,14 @@ import org.thingsboard.server.common.msg.queue.TopicPartitionInfo; import org.thingsboard.server.gen.transport.TransportProtos.ToRuleEngineMsg; import org.thingsboard.server.queue.TbQueueConsumer; import org.thingsboard.server.queue.common.TbProtoQueueMsg; -import org.thingsboard.server.queue.common.consumer.QueueEvent; +import org.thingsboard.server.queue.common.consumer.MainQueueConsumerManager; import org.thingsboard.server.queue.common.consumer.TbQueueConsumerManagerTask; +import org.thingsboard.server.queue.common.consumer.TbQueueConsumerManagerTask.DeleteQueueTask; import org.thingsboard.server.queue.common.consumer.TbQueueConsumerTask; import org.thingsboard.server.queue.discovery.QueueKey; import org.thingsboard.server.service.queue.TbMsgPackCallback; import org.thingsboard.server.service.queue.TbMsgPackProcessingContext; import org.thingsboard.server.service.queue.TbRuleEngineConsumerStats; -import org.thingsboard.server.queue.common.consumer.MainQueueConsumerManager; import org.thingsboard.server.service.queue.processing.TbRuleEngineProcessingDecision; import org.thingsboard.server.service.queue.processing.TbRuleEngineProcessingResult; import org.thingsboard.server.service.queue.processing.TbRuleEngineProcessingStrategy; @@ -78,13 +78,13 @@ public class TbRuleEngineQueueConsumerManager extends MainQueueConsumerManager i @Override public void commit() { if (consumerLock.isLocked()) { + if (stopped) { + return; + } log.error("commit. consumerLock is locked. will wait with no timeout. it looks like a race conditions or deadlock topic " + topic, new RuntimeException("stacktrace")); } consumerLock.lock(); diff --git a/common/queue/src/main/java/org/thingsboard/server/queue/common/consumer/MainQueueConsumerManager.java b/common/queue/src/main/java/org/thingsboard/server/queue/common/consumer/MainQueueConsumerManager.java index 9d45b168ee..7bd8076ce9 100644 --- a/common/queue/src/main/java/org/thingsboard/server/queue/common/consumer/MainQueueConsumerManager.java +++ b/common/queue/src/main/java/org/thingsboard/server/queue/common/consumer/MainQueueConsumerManager.java @@ -23,6 +23,8 @@ import org.thingsboard.server.common.data.queue.QueueConfig; import org.thingsboard.server.common.msg.queue.TopicPartitionInfo; import org.thingsboard.server.queue.TbQueueConsumer; import org.thingsboard.server.queue.TbQueueMsg; +import org.thingsboard.server.queue.common.consumer.TbQueueConsumerManagerTask.UpdateConfigTask; +import org.thingsboard.server.queue.common.consumer.TbQueueConsumerManagerTask.UpdatePartitionsTask; import org.thingsboard.server.queue.discovery.QueueKey; import java.util.Collection; @@ -31,6 +33,7 @@ import java.util.HashMap; import java.util.HashSet; import java.util.List; import java.util.Map; +import java.util.Optional; import java.util.Set; import java.util.concurrent.ConcurrentLinkedQueue; import java.util.concurrent.ExecutorService; @@ -87,20 +90,24 @@ public class MainQueueConsumerManager createConsumerWrapper(C config) { if (config.isConsumerPerPartition()) { - this.consumerWrapper = new ConsumerPerPartitionWrapper(); + return new ConsumerPerPartitionWrapper(); } else { - this.consumerWrapper = new SingleConsumerWrapper(); + return new SingleConsumerWrapper(); } - log.debug("[{}] Initialized consumer for queue: {}", queueKey, config); } public void update(C config) { - addTask(TbQueueConsumerManagerTask.configUpdate(config)); + addTask(new UpdateConfigTask(config)); } public void update(Set partitions) { - addTask(TbQueueConsumerManagerTask.partitionChange(partitions)); + addTask(new UpdatePartitionsTask(partitions)); } protected void addTask(TbQueueConsumerManagerTask todo) { @@ -125,10 +132,10 @@ public class MainQueueConsumerManager partitions) { + private void doUpdate(Set partitions) { this.partitions = partitions; consumerWrapper.updatePartitions(partitions); } @@ -195,6 +202,15 @@ public class MainQueueConsumerManager consumerTask.awaitCompletion(timeoutSec)); log.debug("[{}] Unsubscribed and stopped consumers", queueKey); } - private static String partitionsToString(Collection partitions) { + static String partitionsToString(Collection partitions) { return partitions.stream().map(tpi -> tpi.getFullTopicName() + (tpi.isUseInternalPartition() ? "[" + tpi.getPartition().orElse(-1) + "]" : "")) .collect(Collectors.joining(", ", "[", "]")); @@ -279,15 +295,24 @@ public class MainQueueConsumerManager removedPartitions = new HashSet<>(consumers.keySet()); removedPartitions.removeAll(partitions); + log.info("[{}] Added partitions: {}, removed partitions: {}", queueKey, partitionsToString(addedPartitions), partitionsToString(removedPartitions)); + removePartitions(removedPartitions); + addPartitions(addedPartitions, null); + } - removedPartitions.forEach((tpi) -> consumers.get(tpi).initiateStop()); - removedPartitions.forEach((tpi) -> consumers.remove(tpi).awaitCompletion()); + protected void removePartitions(Set removedPartitions) { + removedPartitions.forEach((tpi) -> Optional.ofNullable(consumers.get(tpi)).ifPresent(TbQueueConsumerTask::initiateStop)); + removedPartitions.forEach((tpi) -> Optional.ofNullable(consumers.remove(tpi)).ifPresent(TbQueueConsumerTask::awaitCompletion)); + } - addedPartitions.forEach((tpi) -> { + protected void addPartitions(Set partitions, Consumer onStop) { + partitions.forEach(tpi -> { Integer partitionId = tpi.getPartition().orElse(-1); String key = queueKey + "-" + partitionId; - TbQueueConsumerTask consumer = new TbQueueConsumerTask<>(key, () -> consumerCreator.apply(config, partitionId)); + Runnable callback = onStop != null ? () -> onStop.accept(tpi) : null; + + TbQueueConsumerTask consumer = new TbQueueConsumerTask<>(key, () -> consumerCreator.apply(config, partitionId), callback); consumers.put(tpi, consumer); consumer.subscribe(Set.of(tpi)); launchConsumer(consumer); @@ -316,7 +341,7 @@ public class MainQueueConsumerManager(queueKey, () -> consumerCreator.apply(config, null)); // no partitionId passed + consumer = new TbQueueConsumerTask<>(queueKey, () -> consumerCreator.apply(config, null), null); // no partitionId passed } consumer.subscribe(partitions); if (!consumer.isRunning()) { diff --git a/common/queue/src/main/java/org/thingsboard/server/queue/common/consumer/PartitionedQueueConsumerManager.java b/common/queue/src/main/java/org/thingsboard/server/queue/common/consumer/PartitionedQueueConsumerManager.java new file mode 100644 index 0000000000..57f0950cdb --- /dev/null +++ b/common/queue/src/main/java/org/thingsboard/server/queue/common/consumer/PartitionedQueueConsumerManager.java @@ -0,0 +1,80 @@ +/** + * Copyright © 2016-2024 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.queue.common.consumer; + +import lombok.Builder; +import lombok.Getter; +import lombok.extern.slf4j.Slf4j; +import org.thingsboard.server.common.data.queue.QueueConfig; +import org.thingsboard.server.common.msg.queue.TopicPartitionInfo; +import org.thingsboard.server.queue.TbQueueConsumer; +import org.thingsboard.server.queue.TbQueueMsg; +import org.thingsboard.server.queue.common.consumer.TbQueueConsumerManagerTask.AddPartitionsTask; +import org.thingsboard.server.queue.common.consumer.TbQueueConsumerManagerTask.RemovePartitionsTask; +import org.thingsboard.server.queue.discovery.QueueKey; + +import java.util.Set; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.ScheduledExecutorService; +import java.util.function.BiFunction; +import java.util.function.Consumer; + +@Slf4j +public class PartitionedQueueConsumerManager extends MainQueueConsumerManager { + + private final ConsumerPerPartitionWrapper consumerWrapper; + @Getter + private final String topic; + + @Builder(builderMethodName = "create") // not to conflict with super.builder() + public PartitionedQueueConsumerManager(QueueKey queueKey, String topic, long pollInterval, MsgPackProcessor msgPackProcessor, + BiFunction> consumerCreator, + ExecutorService consumerExecutor, ScheduledExecutorService scheduler, + ExecutorService taskExecutor, Consumer uncaughtErrorHandler) { + super(queueKey, QueueConfig.of(true, pollInterval), msgPackProcessor, consumerCreator, consumerExecutor, scheduler, taskExecutor, uncaughtErrorHandler); + this.topic = topic; + this.consumerWrapper = (ConsumerPerPartitionWrapper) super.consumerWrapper; + } + + @Override + public void update(Set partitions) { + throw new UnsupportedOperationException("Use manual addPartitions and removePartitions"); + } + + @Override + protected void processTask(TbQueueConsumerManagerTask task) { + if (task instanceof AddPartitionsTask addPartitionsTask) { + log.info("[{}] Added partitions: {}", queueKey, partitionsToString(addPartitionsTask.partitions())); + consumerWrapper.addPartitions(addPartitionsTask.partitions(), addPartitionsTask.onStop()); + } else if (task instanceof RemovePartitionsTask removePartitionsTask) { + log.info("[{}] Removed partitions: {}", queueKey, partitionsToString(removePartitionsTask.partitions())); + consumerWrapper.removePartitions(removePartitionsTask.partitions()); + } + } + + public void addPartitions(Set partitions) { + addPartitions(partitions, null); + } + + public void addPartitions(Set partitions, Consumer onStop) { + addTask(new AddPartitionsTask(partitions, onStop)); + } + + public void removePartitions(Set partitions) { + addTask(new RemovePartitionsTask(partitions)); + } + +} diff --git a/common/queue/src/main/java/org/thingsboard/server/queue/common/consumer/QueueStateService.java b/common/queue/src/main/java/org/thingsboard/server/queue/common/consumer/QueueStateService.java new file mode 100644 index 0000000000..bffe441f7c --- /dev/null +++ b/common/queue/src/main/java/org/thingsboard/server/queue/common/consumer/QueueStateService.java @@ -0,0 +1,76 @@ +/** + * Copyright © 2016-2024 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.queue.common.consumer; + +import lombok.Getter; +import org.thingsboard.server.common.msg.queue.TopicPartitionInfo; +import org.thingsboard.server.queue.TbQueueMsg; + +import java.util.Collections; +import java.util.HashSet; +import java.util.Set; +import java.util.concurrent.locks.Lock; +import java.util.concurrent.locks.ReentrantLock; +import java.util.stream.Collectors; + +public class QueueStateService { + + private PartitionedQueueConsumerManager stateConsumer; + private PartitionedQueueConsumerManager eventConsumer; + + @Getter + private Set partitions; + private final Lock lock = new ReentrantLock(); + + public void init(PartitionedQueueConsumerManager stateConsumer, PartitionedQueueConsumerManager eventConsumer) { + this.stateConsumer = stateConsumer; + this.eventConsumer = eventConsumer; + } + + public void update(Set newPartitions) { + lock.lock(); + Set oldPartitions = this.partitions != null ? this.partitions : Collections.emptySet(); + Set addedPartitions; + Set removedPartitions; + try { + addedPartitions = new HashSet<>(newPartitions); + addedPartitions.removeAll(oldPartitions); + removedPartitions = new HashSet<>(oldPartitions); + removedPartitions.removeAll(newPartitions); + this.partitions = newPartitions; + } finally { + lock.unlock(); + } + if (!removedPartitions.isEmpty()) { + stateConsumer.removePartitions(removedPartitions); + eventConsumer.removePartitions(removedPartitions.stream().map(tpi -> tpi.withTopic(eventConsumer.getTopic())).collect(Collectors.toSet())); + } + + if (!addedPartitions.isEmpty()) { + stateConsumer.addPartitions(addedPartitions, partition -> { + lock.lock(); + try { + if (this.partitions.contains(partition)) { + eventConsumer.addPartitions(Set.of(partition.withTopic(eventConsumer.getTopic()))); + } + } finally { + lock.unlock(); + } + }); + } + } + +} diff --git a/common/queue/src/main/java/org/thingsboard/server/queue/common/consumer/QueueEvent.java b/common/queue/src/main/java/org/thingsboard/server/queue/common/consumer/QueueTaskType.java similarity index 83% rename from common/queue/src/main/java/org/thingsboard/server/queue/common/consumer/QueueEvent.java rename to common/queue/src/main/java/org/thingsboard/server/queue/common/consumer/QueueTaskType.java index 1a78cfc2ba..4e218cd268 100644 --- a/common/queue/src/main/java/org/thingsboard/server/queue/common/consumer/QueueEvent.java +++ b/common/queue/src/main/java/org/thingsboard/server/queue/common/consumer/QueueTaskType.java @@ -17,8 +17,9 @@ package org.thingsboard.server.queue.common.consumer; import java.io.Serializable; -public enum QueueEvent implements Serializable { +public enum QueueTaskType implements Serializable { - PARTITION_CHANGE, CONFIG_UPDATE, DELETE + UPDATE_PARTITIONS, UPDATE_CONFIG, DELETE, + ADD_PARTITIONS, REMOVE_PARTITIONS } diff --git a/common/queue/src/main/java/org/thingsboard/server/queue/common/consumer/TbQueueConsumerManagerTask.java b/common/queue/src/main/java/org/thingsboard/server/queue/common/consumer/TbQueueConsumerManagerTask.java index 67bf370db0..7e2c848a66 100644 --- a/common/queue/src/main/java/org/thingsboard/server/queue/common/consumer/TbQueueConsumerManagerTask.java +++ b/common/queue/src/main/java/org/thingsboard/server/queue/common/consumer/TbQueueConsumerManagerTask.java @@ -15,34 +15,49 @@ */ package org.thingsboard.server.queue.common.consumer; -import lombok.AllArgsConstructor; -import lombok.Getter; -import lombok.ToString; import org.thingsboard.server.common.data.queue.QueueConfig; import org.thingsboard.server.common.msg.queue.TopicPartitionInfo; import java.util.Set; +import java.util.function.Consumer; -@Getter -@ToString -@AllArgsConstructor -public class TbQueueConsumerManagerTask { +public interface TbQueueConsumerManagerTask { - private final QueueEvent event; - private QueueConfig config; - private Set partitions; - private boolean drainQueue; + QueueTaskType getType(); - public static TbQueueConsumerManagerTask delete(boolean drainQueue) { - return new TbQueueConsumerManagerTask(QueueEvent.DELETE, null, null, drainQueue); + record DeleteQueueTask(boolean drainQueue) implements TbQueueConsumerManagerTask { + @Override + public QueueTaskType getType() { + return QueueTaskType.DELETE; + } } - public static TbQueueConsumerManagerTask configUpdate(QueueConfig config) { - return new TbQueueConsumerManagerTask(QueueEvent.CONFIG_UPDATE, config, null, false); + record UpdateConfigTask(QueueConfig config) implements TbQueueConsumerManagerTask { + @Override + public QueueTaskType getType() { + return QueueTaskType.UPDATE_CONFIG; + } } - public static TbQueueConsumerManagerTask partitionChange(Set partitions) { - return new TbQueueConsumerManagerTask(QueueEvent.PARTITION_CHANGE, null, partitions, false); + record UpdatePartitionsTask(Set partitions) implements TbQueueConsumerManagerTask { + @Override + public QueueTaskType getType() { + return QueueTaskType.UPDATE_PARTITIONS; + } + } + + record AddPartitionsTask(Set partitions, Consumer onStop) implements TbQueueConsumerManagerTask { + @Override + public QueueTaskType getType() { + return QueueTaskType.ADD_PARTITIONS; + } + } + + record RemovePartitionsTask(Set partitions) implements TbQueueConsumerManagerTask { + @Override + public QueueTaskType getType() { + return QueueTaskType.REMOVE_PARTITIONS; + } } } diff --git a/common/queue/src/main/java/org/thingsboard/server/queue/common/consumer/TbQueueConsumerTask.java b/common/queue/src/main/java/org/thingsboard/server/queue/common/consumer/TbQueueConsumerTask.java index 4ed0ffa497..96085bf49a 100644 --- a/common/queue/src/main/java/org/thingsboard/server/queue/common/consumer/TbQueueConsumerTask.java +++ b/common/queue/src/main/java/org/thingsboard/server/queue/common/consumer/TbQueueConsumerTask.java @@ -35,14 +35,17 @@ public class TbQueueConsumerTask { private final Object key; private volatile TbQueueConsumer consumer; private volatile Supplier> consumerSupplier; + @Getter + private final Runnable callback; @Setter private Future task; - public TbQueueConsumerTask(Object key, Supplier> consumerSupplier) { + public TbQueueConsumerTask(Object key, Supplier> consumerSupplier, Runnable callback) { this.key = key; this.consumer = null; this.consumerSupplier = consumerSupplier; + this.callback = callback; } public TbQueueConsumer getConsumer() { From 6bf56c8dc24d7e888e35d1a83069e07e862d728f Mon Sep 17 00:00:00 2001 From: ViacheslavKlimov Date: Wed, 19 Feb 2025 15:54:18 +0200 Subject: [PATCH 076/345] EDQS repartitioning improvements --- .../server/edqs/processor/EdqsProcessor.java | 64 ++++++----- .../server/edqs/state/EdqsStateService.java | 2 +- .../edqs/state/KafkaEdqsStateService.java | 101 ++++++++---------- .../edqs/state/LocalEdqsStateService.java | 39 +++---- 4 files changed, 94 insertions(+), 112 deletions(-) diff --git a/common/edqs/src/main/java/org/thingsboard/server/edqs/processor/EdqsProcessor.java b/common/edqs/src/main/java/org/thingsboard/server/edqs/processor/EdqsProcessor.java index 7e9d60d09a..fa1309f932 100644 --- a/common/edqs/src/main/java/org/thingsboard/server/edqs/processor/EdqsProcessor.java +++ b/common/edqs/src/main/java/org/thingsboard/server/edqs/processor/EdqsProcessor.java @@ -43,14 +43,13 @@ import org.thingsboard.server.common.data.edqs.query.QueryResult; import org.thingsboard.server.common.data.id.CustomerId; import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.common.data.page.PageData; -import org.thingsboard.server.common.data.queue.QueueConfig; import org.thingsboard.server.common.data.util.CollectionsUtil; import org.thingsboard.server.common.msg.queue.ServiceType; import org.thingsboard.server.common.msg.queue.TopicPartitionInfo; import org.thingsboard.server.edqs.repo.EdqRepository; +import org.thingsboard.server.edqs.state.EdqsPartitionService; import org.thingsboard.server.edqs.state.EdqsStateService; import org.thingsboard.server.edqs.util.EdqsConverter; -import org.thingsboard.server.edqs.state.EdqsPartitionService; import org.thingsboard.server.edqs.util.VersionsStore; import org.thingsboard.server.gen.transport.TransportProtos; import org.thingsboard.server.gen.transport.TransportProtos.EdqsEventMsg; @@ -59,7 +58,7 @@ import org.thingsboard.server.gen.transport.TransportProtos.ToEdqsMsg; import org.thingsboard.server.queue.TbQueueHandler; import org.thingsboard.server.queue.TbQueueResponseTemplate; import org.thingsboard.server.queue.common.TbProtoQueueMsg; -import org.thingsboard.server.queue.common.consumer.MainQueueConsumerManager; +import org.thingsboard.server.queue.common.consumer.PartitionedQueueConsumerManager; import org.thingsboard.server.queue.discovery.QueueKey; import org.thingsboard.server.queue.discovery.event.PartitionChangeEvent; import org.thingsboard.server.queue.edqs.EdqsComponent; @@ -93,7 +92,8 @@ public class EdqsProcessor implements TbQueueHandler, @Autowired @Lazy private EdqsStateService stateService; - private MainQueueConsumerManager, QueueConfig> eventsConsumer; + @Getter + private PartitionedQueueConsumerManager> eventsConsumer; private TbQueueResponseTemplate, TbProtoQueueMsg> responseTemplate; private ExecutorService consumersExecutor; @@ -125,11 +125,15 @@ public class EdqsProcessor implements TbQueueHandler, } }; - eventsConsumer = MainQueueConsumerManager., QueueConfig>builder() + eventsConsumer = PartitionedQueueConsumerManager.>create() .queueKey(new QueueKey(ServiceType.EDQS, EdqsQueue.EVENTS.getTopic())) - .config(QueueConfig.of(true, config.getPollInterval())) + .topic(EdqsQueue.EVENTS.getTopic()) + .pollInterval(config.getPollInterval()) .msgPackProcessor((msgs, consumer, config) -> { for (TbProtoQueueMsg queueMsg : msgs) { + if (consumer.isStopped()) { + return; + } try { ToEdqsMsg msg = queueMsg.getValue(); log.trace("Processing message: {}", msg); @@ -159,37 +163,31 @@ public class EdqsProcessor implements TbQueueHandler, if (event.getServiceType() != ServiceType.EDQS) { return; } - repartitionExecutor.submit(() -> { // todo: maybe cancel the task if new event comes - try { - Set newPartitions = event.getNewPartitions().get(new QueueKey(ServiceType.EDQS)); - Set partitions = newPartitions.stream() - .map(tpi -> tpi.withUseInternalPartition(true)) - .collect(Collectors.toSet()); + try { + Set newPartitions = event.getNewPartitions().get(new QueueKey(ServiceType.EDQS)); + Set partitions = newPartitions.stream() + .map(tpi -> tpi.withUseInternalPartition(true)) + .collect(Collectors.toSet()); - try { - stateService.restore(withTopic(partitions, EdqsQueue.STATE.getTopic())); // blocks until restored - } catch (Exception e) { - log.error("Failed to process restore for partitions {}", partitions, e); - } - eventsConsumer.update(withTopic(partitions, EdqsQueue.EVENTS.getTopic())); - responseTemplate.subscribe(withTopic(partitions, config.getRequestsTopic())); + stateService.process(withTopic(partitions, EdqsQueue.STATE.getTopic())); + // eventsConsumer's partitions are updated by stateService + responseTemplate.subscribe(withTopic(partitions, config.getRequestsTopic())); - Set oldPartitions = event.getOldPartitions().get(new QueueKey(ServiceType.EDQS)); - if (CollectionsUtil.isNotEmpty(oldPartitions)) { - Set removedPartitions = Sets.difference(oldPartitions, newPartitions).stream() - .map(tpi -> tpi.getPartition().orElse(-1)).collect(Collectors.toSet()); - if (config.getPartitioningStrategy() != EdqsPartitioningStrategy.TENANT && !removedPartitions.isEmpty()) { - log.warn("Partitions {} were removed but shouldn't be (due to NONE partitioning strategy)", removedPartitions); - } - repository.clearIf(tenantId -> { - Integer partition = partitionService.resolvePartition(tenantId); - return partition != null && removedPartitions.contains(partition); - }); + Set oldPartitions = event.getOldPartitions().get(new QueueKey(ServiceType.EDQS)); + if (CollectionsUtil.isNotEmpty(oldPartitions)) { + Set removedPartitions = Sets.difference(oldPartitions, newPartitions).stream() + .map(tpi -> tpi.getPartition().orElse(-1)).collect(Collectors.toSet()); + if (config.getPartitioningStrategy() != EdqsPartitioningStrategy.TENANT && !removedPartitions.isEmpty()) { + log.warn("Partitions {} were removed but shouldn't be (due to NONE partitioning strategy)", removedPartitions); } - } catch (Throwable t) { - log.error("Failed to handle partition change event {}", event, t); + repository.clearIf(tenantId -> { + Integer partition = partitionService.resolvePartition(tenantId); + return partition != null && removedPartitions.contains(partition); + }); } - }); + } catch (Throwable t) { + log.error("Failed to handle partition change event {}", event, t); + } } @Override diff --git a/common/edqs/src/main/java/org/thingsboard/server/edqs/state/EdqsStateService.java b/common/edqs/src/main/java/org/thingsboard/server/edqs/state/EdqsStateService.java index 06d080ac64..d45cc0de14 100644 --- a/common/edqs/src/main/java/org/thingsboard/server/edqs/state/EdqsStateService.java +++ b/common/edqs/src/main/java/org/thingsboard/server/edqs/state/EdqsStateService.java @@ -25,7 +25,7 @@ import java.util.Set; public interface EdqsStateService { - void restore(Set partitions); + void process(Set partitions); void save(TenantId tenantId, ObjectType type, String key, EdqsEventType eventType, ToEdqsMsg msg); diff --git a/common/edqs/src/main/java/org/thingsboard/server/edqs/state/KafkaEdqsStateService.java b/common/edqs/src/main/java/org/thingsboard/server/edqs/state/KafkaEdqsStateService.java index 6ab1fb9e1f..fc7f8d40d9 100644 --- a/common/edqs/src/main/java/org/thingsboard/server/edqs/state/KafkaEdqsStateService.java +++ b/common/edqs/src/main/java/org/thingsboard/server/edqs/state/KafkaEdqsStateService.java @@ -25,7 +25,6 @@ import org.thingsboard.common.util.ThingsBoardThreadFactory; import org.thingsboard.server.common.data.ObjectType; import org.thingsboard.server.common.data.edqs.EdqsEventType; import org.thingsboard.server.common.data.id.TenantId; -import org.thingsboard.server.common.data.queue.QueueConfig; import org.thingsboard.server.common.msg.queue.ServiceType; import org.thingsboard.server.common.msg.queue.TopicPartitionInfo; import org.thingsboard.server.edqs.processor.EdqsProcessor; @@ -34,8 +33,9 @@ import org.thingsboard.server.edqs.util.VersionsStore; import org.thingsboard.server.gen.transport.TransportProtos.EdqsEventMsg; import org.thingsboard.server.gen.transport.TransportProtos.ToEdqsMsg; import org.thingsboard.server.queue.common.TbProtoQueueMsg; -import org.thingsboard.server.queue.common.consumer.MainQueueConsumerManager; +import org.thingsboard.server.queue.common.consumer.PartitionedQueueConsumerManager; import org.thingsboard.server.queue.common.consumer.QueueConsumerManager; +import org.thingsboard.server.queue.common.consumer.QueueStateService; import org.thingsboard.server.queue.discovery.QueueKey; import org.thingsboard.server.queue.edqs.EdqsConfig; import org.thingsboard.server.queue.edqs.EdqsQueue; @@ -44,7 +44,6 @@ import org.thingsboard.server.queue.edqs.KafkaEdqsComponent; import java.util.Set; import java.util.UUID; -import java.util.concurrent.CountDownLatch; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; import java.util.concurrent.ScheduledExecutorService; @@ -54,19 +53,17 @@ import java.util.concurrent.atomic.AtomicInteger; @RequiredArgsConstructor @KafkaEdqsComponent @Slf4j -public class KafkaEdqsStateService implements EdqsStateService { +public class KafkaEdqsStateService extends QueueStateService, TbProtoQueueMsg> implements EdqsStateService { private final EdqsConfig config; private final EdqsPartitionService partitionService; private final EdqsQueueFactory queueFactory; private final EdqsProcessor edqsProcessor; - private MainQueueConsumerManager, QueueConfig> stateConsumer; - private QueueConsumerManager> eventsConsumer; + private PartitionedQueueConsumerManager> stateConsumer; + private QueueConsumerManager> eventsToBackupConsumer; private EdqsProducer stateProducer; - private boolean initialRestoreDone; - private ExecutorService consumersExecutor; private ExecutorService mgmtExecutor; private ScheduledExecutorService scheduler; @@ -81,11 +78,14 @@ public class KafkaEdqsStateService implements EdqsStateService { mgmtExecutor = ThingsBoardExecutors.newWorkStealingPool(4, "edqs-backup-consumer-mgmt"); scheduler = ThingsBoardExecutors.newSingleThreadScheduledExecutor("edqs-backup-scheduler"); - stateConsumer = MainQueueConsumerManager., QueueConfig>builder() // FIXME Slavik: if topic is empty + stateConsumer = PartitionedQueueConsumerManager.>create() // FIXME Slavik: if topic is empty .queueKey(new QueueKey(ServiceType.EDQS, EdqsQueue.STATE.getTopic())) - .config(QueueConfig.of(true, config.getPollInterval())) + .pollInterval(config.getPollInterval()) .msgPackProcessor((msgs, consumer, config) -> { for (TbProtoQueueMsg queueMsg : msgs) { + if (consumer.isStopped()) { + return; + } try { ToEdqsMsg msg = queueMsg.getValue(); log.trace("Processing message: {}", msg); @@ -94,7 +94,7 @@ public class KafkaEdqsStateService implements EdqsStateService { log.info("[state] Processed {} msgs", stateReadCount.get()); } } catch (Exception e) { - log.error("Failed to process message: {}", queueMsg, e); // TODO: do something about the error - e.g. reprocess + log.error("Failed to process message: {}", queueMsg, e); } } consumer.commit(); @@ -105,46 +105,43 @@ public class KafkaEdqsStateService implements EdqsStateService { .scheduler(scheduler) .uncaughtErrorHandler(edqsProcessor.getErrorHandler()) .build(); + super.init(stateConsumer, edqsProcessor.getEventsConsumer()); - ExecutorService backupExecutor = ThingsBoardExecutors.newLimitedTasksExecutor(12, 1000, "events-to-backup-executor"); - eventsConsumer = QueueConsumerManager.>builder() // FIXME Slavik writes to the state while we read it, slows down the start. maybe start backup consumer after restore is finished + eventsToBackupConsumer = QueueConsumerManager.>builder() .name("edqs-events-to-backup-consumer") .pollInterval(config.getPollInterval()) .msgPackProcessor((msgs, consumer) -> { - CountDownLatch resultLatch = new CountDownLatch(msgs.size()); for (TbProtoQueueMsg queueMsg : msgs) { - backupExecutor.submit(() -> { - try { - ToEdqsMsg msg = queueMsg.getValue(); - log.trace("Processing message: {}", msg); - - if (msg.hasEventMsg()) { - EdqsEventMsg eventMsg = msg.getEventMsg(); - String key = eventMsg.getKey(); - int count = eventsReadCount.incrementAndGet(); - if (count % 100000 == 0) { - log.info("[events-to-backup] Processed {} msgs", count); - } - if (eventMsg.hasVersion()) { - if (!versionsStore.isNew(key, eventMsg.getVersion())) { - return; - } - } + if (consumer.isStopped()) { + return; + } + try { + ToEdqsMsg msg = queueMsg.getValue(); + log.trace("Processing message: {}", msg); - TenantId tenantId = getTenantId(msg); - ObjectType objectType = ObjectType.valueOf(eventMsg.getObjectType()); - EdqsEventType eventType = EdqsEventType.valueOf(eventMsg.getEventType()); - log.debug("[{}] Saving to backup [{}] [{}] [{}]", tenantId, objectType, eventType, key); - stateProducer.send(tenantId, objectType, key, msg); + if (msg.hasEventMsg()) { + EdqsEventMsg eventMsg = msg.getEventMsg(); + String key = eventMsg.getKey(); + int count = eventsReadCount.incrementAndGet(); + if (count % 100000 == 0) { + log.info("[events-to-backup] Processed {} msgs", count); + } + if (eventMsg.hasVersion()) { + if (!versionsStore.isNew(key, eventMsg.getVersion())) { + continue; + } } - } catch (Throwable t) { - log.error("Failed to process message: {}", queueMsg, t); - } finally { - resultLatch.countDown(); + + TenantId tenantId = getTenantId(msg); + ObjectType objectType = ObjectType.valueOf(eventMsg.getObjectType()); + EdqsEventType eventType = EdqsEventType.valueOf(eventMsg.getEventType()); + log.debug("[{}] Saving to backup [{}] [{}] [{}]", tenantId, objectType, eventType, key); + stateProducer.send(tenantId, objectType, key, msg); } - }); + } catch (Throwable t) { + log.error("Failed to process message: {}", queueMsg, t); + } } - resultLatch.await(); consumer.commit(); }) .consumerCreator(() -> queueFactory.createEdqsMsgConsumer(EdqsQueue.EVENTS, "events-to-backup-consumer-group")) // shared by all instances consumer group @@ -160,20 +157,12 @@ public class KafkaEdqsStateService implements EdqsStateService { } @Override - public void restore(Set partitions) { - stateReadCount.set(0); //TODO Slavik: do not support remote mode in monolith setup - long startTs = System.currentTimeMillis(); - log.info("Restore started for partitions {}", partitions.stream().map(tpi -> tpi.getPartition().orElse(-1)).sorted().toList()); - stateConsumer.doUpdate(partitions); // calling blocking doUpdate instead of update - stateConsumer.awaitStop(0); // consumers should stop on their own because EdqsQueue.STATE.stopWhenRead is true, we just need to wait - log.info("Restore finished in {} ms. Processed {} msgs", (System.currentTimeMillis() - startTs), stateReadCount.get()); - - if (!initialRestoreDone) { - initialRestoreDone = true; - - eventsConsumer.subscribe(); - eventsConsumer.launch(); + public void process(Set partitions) { + if (getPartitions() == null) { + eventsToBackupConsumer.subscribe(); + eventsToBackupConsumer.launch(); } + super.update(partitions); } @Override @@ -194,7 +183,7 @@ public class KafkaEdqsStateService implements EdqsStateService { private void preDestroy() { stateConsumer.stop(); stateConsumer.awaitStop(); - eventsConsumer.stop(); + eventsToBackupConsumer.stop(); stateProducer.stop(); consumersExecutor.shutdownNow(); diff --git a/common/edqs/src/main/java/org/thingsboard/server/edqs/state/LocalEdqsStateService.java b/common/edqs/src/main/java/org/thingsboard/server/edqs/state/LocalEdqsStateService.java index 6a61488182..32f4159f9f 100644 --- a/common/edqs/src/main/java/org/thingsboard/server/edqs/state/LocalEdqsStateService.java +++ b/common/edqs/src/main/java/org/thingsboard/server/edqs/state/LocalEdqsStateService.java @@ -17,8 +17,6 @@ package org.thingsboard.server.edqs.state; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.context.annotation.Lazy; import org.springframework.stereotype.Service; import org.thingsboard.server.common.data.ObjectType; import org.thingsboard.server.common.data.edqs.EdqsEventType; @@ -38,29 +36,26 @@ import java.util.Set; @Slf4j public class LocalEdqsStateService implements EdqsStateService { - @Autowired @Lazy - private EdqsProcessor processor; - @Autowired - private EdqsRocksDb db; + private final EdqsProcessor processor; + private final EdqsRocksDb db; - private boolean restoreDone; + private Set partitions; @Override - public void restore(Set partitions) { - if (restoreDone) { - return; + public void process(Set partitions) { + if (this.partitions != null) { + db.forEach((key, value) -> { + try { + ToEdqsMsg edqsMsg = ToEdqsMsg.parseFrom(value); + log.trace("[{}] Restored msg from RocksDB: {}", key, edqsMsg); + processor.process(edqsMsg, EdqsQueue.STATE); + } catch (Exception e) { + log.error("[{}] Failed to restore value", key, e); + } + }); } - - db.forEach((key, value) -> { - try { - ToEdqsMsg edqsMsg = ToEdqsMsg.parseFrom(value); - log.trace("[{}] Restored msg from RocksDB: {}", key, edqsMsg); - processor.process(edqsMsg, EdqsQueue.STATE); - } catch (Exception e) { - log.error("[{}] Failed to restore value", key, e); - } - }); - restoreDone = true; + processor.getEventsConsumer().update(partitions); + this.partitions = partitions; } @Override @@ -79,7 +74,7 @@ public class LocalEdqsStateService implements EdqsStateService { @Override public boolean isReady() { - return restoreDone; + return partitions != null; } } From 4daf4ba9888d0ef98d0eb5f17843873f192fa5c7 Mon Sep 17 00:00:00 2001 From: Andrii Landiak Date: Wed, 19 Feb 2025 17:36:06 +0200 Subject: [PATCH 077/345] Fix refEntity import-export --- .../ie/exporting/impl/CalculatedFieldExportService.java | 7 +++++++ .../ie/importing/impl/CalculatedFieldImportService.java | 7 +++++++ .../server/service/sync/ie/ExportImportServiceSqlTest.java | 3 ++- 3 files changed, 16 insertions(+), 1 deletion(-) diff --git a/application/src/main/java/org/thingsboard/server/service/sync/ie/exporting/impl/CalculatedFieldExportService.java b/application/src/main/java/org/thingsboard/server/service/sync/ie/exporting/impl/CalculatedFieldExportService.java index fc1465b29c..3612f9f4e0 100644 --- a/application/src/main/java/org/thingsboard/server/service/sync/ie/exporting/impl/CalculatedFieldExportService.java +++ b/application/src/main/java/org/thingsboard/server/service/sync/ie/exporting/impl/CalculatedFieldExportService.java @@ -19,6 +19,7 @@ import org.springframework.stereotype.Service; import org.thingsboard.server.common.data.EntityType; import org.thingsboard.server.common.data.cf.CalculatedField; import org.thingsboard.server.common.data.id.CalculatedFieldId; +import org.thingsboard.server.common.data.id.EntityId; import org.thingsboard.server.common.data.sync.ie.EntityExportData; import org.thingsboard.server.queue.util.TbCoreComponent; import org.thingsboard.server.service.sync.vc.data.EntitiesExportCtx; @@ -32,6 +33,12 @@ public class CalculatedFieldExportService extends BaseEntityExportService ctx, CalculatedField calculatedField, EntityExportData exportData) { calculatedField.setEntityId(getExternalIdOrElseInternal(ctx, calculatedField.getEntityId())); + calculatedField.getConfiguration().getArguments().values().forEach(argument -> { + if (argument.getRefEntityId() != null) { + EntityId internalEntityId = getExternalIdOrElseInternal(ctx, argument.getRefEntityId()); + argument.setRefEntityId(internalEntityId); + } + }); } @Override diff --git a/application/src/main/java/org/thingsboard/server/service/sync/ie/importing/impl/CalculatedFieldImportService.java b/application/src/main/java/org/thingsboard/server/service/sync/ie/importing/impl/CalculatedFieldImportService.java index 07a306d511..978ac758df 100644 --- a/application/src/main/java/org/thingsboard/server/service/sync/ie/importing/impl/CalculatedFieldImportService.java +++ b/application/src/main/java/org/thingsboard/server/service/sync/ie/importing/impl/CalculatedFieldImportService.java @@ -23,6 +23,7 @@ import org.thingsboard.server.common.data.audit.ActionType; import org.thingsboard.server.common.data.cf.CalculatedField; import org.thingsboard.server.common.data.exception.ThingsboardException; import org.thingsboard.server.common.data.id.CalculatedFieldId; +import org.thingsboard.server.common.data.id.EntityId; import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.common.data.sync.ie.EntityExportData; import org.thingsboard.server.dao.cf.CalculatedFieldService; @@ -45,6 +46,12 @@ public class CalculatedFieldImportService extends BaseEntityImportService exportData, IdProvider idProvider) { calculatedField.setEntityId(idProvider.getInternalId(calculatedField.getEntityId())); + calculatedField.getConfiguration().getArguments().values().forEach(argument -> { + if (argument.getRefEntityId() != null) { + EntityId internalEntityId = idProvider.getInternalId(argument.getRefEntityId()); + argument.setRefEntityId(internalEntityId); + } + }); return calculatedField; } diff --git a/application/src/test/java/org/thingsboard/server/service/sync/ie/ExportImportServiceSqlTest.java b/application/src/test/java/org/thingsboard/server/service/sync/ie/ExportImportServiceSqlTest.java index 3e47db1bc0..7d1fee8ac3 100644 --- a/application/src/test/java/org/thingsboard/server/service/sync/ie/ExportImportServiceSqlTest.java +++ b/application/src/test/java/org/thingsboard/server/service/sync/ie/ExportImportServiceSqlTest.java @@ -204,7 +204,7 @@ public class ExportImportServiceSqlTest extends AbstractControllerTest { Asset asset = createAsset(tenantId1, null, assetProfile.getId(), "Asset 1"); DeviceProfile deviceProfile = createDeviceProfile(tenantId1, ruleChain.getId(), dashboard.getId(), "Device profile 1"); Device device = createDevice(tenantId1, null, deviceProfile.getId(), "Device 1"); - CalculatedField calculatedField = createCalculatedField(tenantId1, device.getId(), device.getId()); + CalculatedField calculatedField = createCalculatedField(tenantId1, device.getId(), asset.getId()); Map entitiesExportData = Stream.of(customer.getId(), asset.getId(), device.getId(), ruleChain.getId(), dashboard.getId(), assetProfile.getId(), deviceProfile.getId(), calculatedField.getId()) @@ -364,6 +364,7 @@ public class ExportImportServiceSqlTest extends AbstractControllerTest { CalculatedField exportedCalculatedField = (CalculatedField) exportEntity(tenantAdmin2, (CalculatedFieldId) ids.get(calculatedField.getId())).getEntity(); assertThat(exportedCalculatedField.getName()).isEqualTo(calculatedField.getName()); assertThat(exportedCalculatedField.getEntityId()).isEqualTo(device.getId()); + assertThat(exportedCalculatedField.getConfiguration().getReferencedEntities()).isEqualTo(calculatedField.getConfiguration().getReferencedEntities()); deviceProfile.setDefaultDashboardId(null); deviceProfileService.saveDeviceProfile(deviceProfile); From 7519889c1aef0044c49b7d186615bdb0aab4c8da Mon Sep 17 00:00:00 2001 From: ViacheslavKlimov Date: Thu, 20 Feb 2025 12:25:08 +0200 Subject: [PATCH 078/345] Refactoring for EDQS repartitioning --- .../EdqsEntityQueryControllerTest.java | 4 +- .../server/edqs/processor/EdqsProcessor.java | 22 ++++---- .../server/edqs/state/EdqsController.java | 42 --------------- .../server/edqs/state/EdqsStateService.java | 6 ++- .../edqs/state/KafkaEdqsStateService.java | 53 ++++++------------- .../edqs/state/LocalEdqsStateService.java | 15 ++++-- .../common/msg/queue/TopicPartitionInfo.java | 2 +- .../consumer/MainQueueConsumerManager.java | 3 ++ .../PartitionedQueueConsumerManager.java | 5 -- .../common/consumer/QueueStateService.java | 8 ++- 10 files changed, 57 insertions(+), 103 deletions(-) delete mode 100644 common/edqs/src/main/java/org/thingsboard/server/edqs/state/EdqsController.java diff --git a/application/src/test/java/org/thingsboard/server/controller/EdqsEntityQueryControllerTest.java b/application/src/test/java/org/thingsboard/server/controller/EdqsEntityQueryControllerTest.java index 1add0dae37..6833381f2d 100644 --- a/application/src/test/java/org/thingsboard/server/controller/EdqsEntityQueryControllerTest.java +++ b/application/src/test/java/org/thingsboard/server/controller/EdqsEntityQueryControllerTest.java @@ -33,8 +33,8 @@ import static org.awaitility.Awaitility.await; @DaoSqlTest @TestPropertySource(properties = { -// "queue.type=kafka", // uncomment to use Kafka -// "queue.kafka.bootstrap.servers=10.7.1.254:9092", + "queue.type=kafka", // uncomment to use Kafka + "queue.kafka.bootstrap.servers=192.168.0.105:9092", "queue.edqs.sync.enabled=true", "queue.edqs.api_enabled=true", "queue.edqs.mode=local" diff --git a/common/edqs/src/main/java/org/thingsboard/server/edqs/processor/EdqsProcessor.java b/common/edqs/src/main/java/org/thingsboard/server/edqs/processor/EdqsProcessor.java index fa1309f932..968e637a3d 100644 --- a/common/edqs/src/main/java/org/thingsboard/server/edqs/processor/EdqsProcessor.java +++ b/common/edqs/src/main/java/org/thingsboard/server/edqs/processor/EdqsProcessor.java @@ -92,12 +92,11 @@ public class EdqsProcessor implements TbQueueHandler, @Autowired @Lazy private EdqsStateService stateService; - @Getter - private PartitionedQueueConsumerManager> eventsConsumer; + private PartitionedQueueConsumerManager> eventConsumer; private TbQueueResponseTemplate, TbProtoQueueMsg> responseTemplate; private ExecutorService consumersExecutor; - private ExecutorService mgmtExecutor; + private ExecutorService taskExecutor; private ScheduledExecutorService scheduler; private ListeningExecutorService requestExecutor; private ExecutorService repartitionExecutor; @@ -112,7 +111,7 @@ public class EdqsProcessor implements TbQueueHandler, @PostConstruct private void init() { consumersExecutor = Executors.newCachedThreadPool(ThingsBoardThreadFactory.forName("edqs-consumer")); - mgmtExecutor = ThingsBoardExecutors.newWorkStealingPool(4, "edqs-consumer-mgmt"); + taskExecutor = ThingsBoardExecutors.newWorkStealingPool(4, "edqs-consumer-task-executor"); scheduler = ThingsBoardExecutors.newSingleThreadScheduledExecutor("edqs-scheduler"); requestExecutor = MoreExecutors.listeningDecorator(ThingsBoardExecutors.newWorkStealingPool(12, "edqs-requests")); repartitionExecutor = Executors.newSingleThreadExecutor(ThingsBoardThreadFactory.forName("edqs-repartition")); @@ -125,7 +124,7 @@ public class EdqsProcessor implements TbQueueHandler, } }; - eventsConsumer = PartitionedQueueConsumerManager.>create() + eventConsumer = PartitionedQueueConsumerManager.>create() .queueKey(new QueueKey(ServiceType.EDQS, EdqsQueue.EVENTS.getTopic())) .topic(EdqsQueue.EVENTS.getTopic()) .pollInterval(config.getPollInterval()) @@ -146,10 +145,12 @@ public class EdqsProcessor implements TbQueueHandler, }) .consumerCreator((config, partitionId) -> queueFactory.createEdqsMsgConsumer(EdqsQueue.EVENTS)) .consumerExecutor(consumersExecutor) - .taskExecutor(mgmtExecutor) + .taskExecutor(taskExecutor) .scheduler(scheduler) .uncaughtErrorHandler(errorHandler) .build(); + stateService.init(eventConsumer); + responseTemplate = queueFactory.createEdqsResponseTemplate(); } @@ -171,7 +172,7 @@ public class EdqsProcessor implements TbQueueHandler, stateService.process(withTopic(partitions, EdqsQueue.STATE.getTopic())); // eventsConsumer's partitions are updated by stateService - responseTemplate.subscribe(withTopic(partitions, config.getRequestsTopic())); + responseTemplate.subscribe(withTopic(partitions, config.getRequestsTopic())); // FIXME: we subscribe to partitions before we are ready. implement consumer-per-partition version for request template Set oldPartitions = event.getOldPartitions().get(new QueueKey(ServiceType.EDQS)); if (CollectionsUtil.isNotEmpty(oldPartitions)) { @@ -280,12 +281,13 @@ public class EdqsProcessor implements TbQueueHandler, @PreDestroy public void destroy() throws InterruptedException { - eventsConsumer.stop(); - eventsConsumer.awaitStop(); + eventConsumer.stop(); + eventConsumer.awaitStop(); responseTemplate.stop(); + stateService.stop(); consumersExecutor.shutdownNow(); - mgmtExecutor.shutdownNow(); + taskExecutor.shutdownNow(); scheduler.shutdownNow(); requestExecutor.shutdownNow(); repartitionExecutor.shutdownNow(); diff --git a/common/edqs/src/main/java/org/thingsboard/server/edqs/state/EdqsController.java b/common/edqs/src/main/java/org/thingsboard/server/edqs/state/EdqsController.java deleted file mode 100644 index 06d5f89aa9..0000000000 --- a/common/edqs/src/main/java/org/thingsboard/server/edqs/state/EdqsController.java +++ /dev/null @@ -1,42 +0,0 @@ -/** - * Copyright © 2016-2024 The Thingsboard Authors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package org.thingsboard.server.edqs.state; - -import lombok.RequiredArgsConstructor; -import org.springframework.boot.autoconfigure.condition.ConditionalOnExpression; -import org.springframework.http.ResponseEntity; -import org.springframework.web.bind.annotation.GetMapping; -import org.springframework.web.bind.annotation.RequestMapping; -import org.springframework.web.bind.annotation.RestController; - -@RestController -@RequiredArgsConstructor -@ConditionalOnExpression("'${service.type:null}'=='edqs'") -@RequestMapping("/api/edqs") -public class EdqsController { - - private final EdqsStateService edqsStateService; - - @GetMapping("/ready") - public ResponseEntity isReady() { - if (edqsStateService.isReady()) { - return ResponseEntity.ok().build(); - } else { - return ResponseEntity.badRequest().build(); - } - } - -} diff --git a/common/edqs/src/main/java/org/thingsboard/server/edqs/state/EdqsStateService.java b/common/edqs/src/main/java/org/thingsboard/server/edqs/state/EdqsStateService.java index d45cc0de14..1966618d4b 100644 --- a/common/edqs/src/main/java/org/thingsboard/server/edqs/state/EdqsStateService.java +++ b/common/edqs/src/main/java/org/thingsboard/server/edqs/state/EdqsStateService.java @@ -20,15 +20,19 @@ import org.thingsboard.server.common.data.edqs.EdqsEventType; import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.common.msg.queue.TopicPartitionInfo; import org.thingsboard.server.gen.transport.TransportProtos.ToEdqsMsg; +import org.thingsboard.server.queue.common.TbProtoQueueMsg; +import org.thingsboard.server.queue.common.consumer.PartitionedQueueConsumerManager; import java.util.Set; public interface EdqsStateService { + void init(PartitionedQueueConsumerManager> eventConsumer); + void process(Set partitions); void save(TenantId tenantId, ObjectType type, String key, EdqsEventType eventType, ToEdqsMsg msg); - boolean isReady(); + void stop(); } diff --git a/common/edqs/src/main/java/org/thingsboard/server/edqs/state/KafkaEdqsStateService.java b/common/edqs/src/main/java/org/thingsboard/server/edqs/state/KafkaEdqsStateService.java index fc7f8d40d9..f7382c9f15 100644 --- a/common/edqs/src/main/java/org/thingsboard/server/edqs/state/KafkaEdqsStateService.java +++ b/common/edqs/src/main/java/org/thingsboard/server/edqs/state/KafkaEdqsStateService.java @@ -15,13 +15,9 @@ */ package org.thingsboard.server.edqs.state; -import jakarta.annotation.PostConstruct; -import jakarta.annotation.PreDestroy; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.springframework.stereotype.Service; -import org.thingsboard.common.util.ThingsBoardExecutors; -import org.thingsboard.common.util.ThingsBoardThreadFactory; import org.thingsboard.server.common.data.ObjectType; import org.thingsboard.server.common.data.edqs.EdqsEventType; import org.thingsboard.server.common.data.id.TenantId; @@ -44,16 +40,13 @@ import org.thingsboard.server.queue.edqs.KafkaEdqsComponent; import java.util.Set; import java.util.UUID; -import java.util.concurrent.ExecutorService; -import java.util.concurrent.Executors; -import java.util.concurrent.ScheduledExecutorService; import java.util.concurrent.atomic.AtomicInteger; @Service @RequiredArgsConstructor @KafkaEdqsComponent @Slf4j -public class KafkaEdqsStateService extends QueueStateService, TbProtoQueueMsg> implements EdqsStateService { +public class KafkaEdqsStateService implements EdqsStateService { private final EdqsConfig config; private final EdqsPartitionService partitionService; @@ -61,25 +54,19 @@ public class KafkaEdqsStateService extends QueueStateService> stateConsumer; + private QueueStateService, TbProtoQueueMsg> queueStateService; private QueueConsumerManager> eventsToBackupConsumer; private EdqsProducer stateProducer; - private ExecutorService consumersExecutor; - private ExecutorService mgmtExecutor; - private ScheduledExecutorService scheduler; - private final VersionsStore versionsStore = new VersionsStore(); private final AtomicInteger stateReadCount = new AtomicInteger(); private final AtomicInteger eventsReadCount = new AtomicInteger(); - @PostConstruct - private void init() { - consumersExecutor = Executors.newCachedThreadPool(ThingsBoardThreadFactory.forName("edqs-consumer")); - mgmtExecutor = ThingsBoardExecutors.newWorkStealingPool(4, "edqs-backup-consumer-mgmt"); - scheduler = ThingsBoardExecutors.newSingleThreadScheduledExecutor("edqs-backup-scheduler"); - - stateConsumer = PartitionedQueueConsumerManager.>create() // FIXME Slavik: if topic is empty + @Override + public void init(PartitionedQueueConsumerManager> eventConsumer) { + stateConsumer = PartitionedQueueConsumerManager.>create() .queueKey(new QueueKey(ServiceType.EDQS, EdqsQueue.STATE.getTopic())) + .topic(EdqsQueue.STATE.getTopic()) .pollInterval(config.getPollInterval()) .msgPackProcessor((msgs, consumer, config) -> { for (TbProtoQueueMsg queueMsg : msgs) { @@ -100,12 +87,13 @@ public class KafkaEdqsStateService extends QueueStateService queueFactory.createEdqsMsgConsumer(EdqsQueue.STATE)) - .consumerExecutor(consumersExecutor) - .taskExecutor(mgmtExecutor) - .scheduler(scheduler) + .consumerExecutor(eventConsumer.getConsumerExecutor()) + .taskExecutor(eventConsumer.getTaskExecutor()) + .scheduler(eventConsumer.getScheduler()) .uncaughtErrorHandler(edqsProcessor.getErrorHandler()) .build(); - super.init(stateConsumer, edqsProcessor.getEventsConsumer()); + queueStateService = new QueueStateService<>(); + queueStateService.init(stateConsumer, eventConsumer); eventsToBackupConsumer = QueueConsumerManager.>builder() .name("edqs-events-to-backup-consumer") @@ -145,7 +133,7 @@ public class KafkaEdqsStateService extends QueueStateService queueFactory.createEdqsMsgConsumer(EdqsQueue.EVENTS, "events-to-backup-consumer-group")) // shared by all instances consumer group - .consumerExecutor(consumersExecutor) + .consumerExecutor(eventConsumer.getConsumerExecutor()) .threadPrefix("edqs-events-to-backup") .build(); @@ -158,11 +146,11 @@ public class KafkaEdqsStateService extends QueueStateService partitions) { - if (getPartitions() == null) { + if (queueStateService.getPartitions() == null) { eventsToBackupConsumer.subscribe(); eventsToBackupConsumer.launch(); } - super.update(partitions); + queueStateService.update(partitions); } @Override @@ -170,25 +158,16 @@ public class KafkaEdqsStateService extends QueueStateService> eventConsumer; private Set partitions; + @Override + public void init(PartitionedQueueConsumerManager> eventConsumer) { + this.eventConsumer = eventConsumer; + } + @Override public void process(Set partitions) { - if (this.partitions != null) { + if (this.partitions == null) { db.forEach((key, value) -> { try { ToEdqsMsg edqsMsg = ToEdqsMsg.parseFrom(value); @@ -54,7 +62,7 @@ public class LocalEdqsStateService implements EdqsStateService { } }); } - processor.getEventsConsumer().update(partitions); + eventConsumer.update(partitions); this.partitions = partitions; } @@ -73,8 +81,7 @@ public class LocalEdqsStateService implements EdqsStateService { } @Override - public boolean isReady() { - return partitions != null; + public void stop() { } } diff --git a/common/message/src/main/java/org/thingsboard/server/common/msg/queue/TopicPartitionInfo.java b/common/message/src/main/java/org/thingsboard/server/common/msg/queue/TopicPartitionInfo.java index 552bdf50d6..3bb489f209 100644 --- a/common/message/src/main/java/org/thingsboard/server/common/msg/queue/TopicPartitionInfo.java +++ b/common/message/src/main/java/org/thingsboard/server/common/msg/queue/TopicPartitionInfo.java @@ -58,7 +58,7 @@ public class TopicPartitionInfo { } public TopicPartitionInfo newByTopic(String topic) { - return new TopicPartitionInfo(topic, this.tenantId, this.partition, this.myPartition); + return new TopicPartitionInfo(topic, this.tenantId, this.partition, this.useInternalPartition, this.myPartition); } public String getTopic() { diff --git a/common/queue/src/main/java/org/thingsboard/server/queue/common/consumer/MainQueueConsumerManager.java b/common/queue/src/main/java/org/thingsboard/server/queue/common/consumer/MainQueueConsumerManager.java index 7bd8076ce9..df683b94cd 100644 --- a/common/queue/src/main/java/org/thingsboard/server/queue/common/consumer/MainQueueConsumerManager.java +++ b/common/queue/src/main/java/org/thingsboard/server/queue/common/consumer/MainQueueConsumerManager.java @@ -54,8 +54,11 @@ public class MainQueueConsumerManager msgPackProcessor; protected final BiFunction> consumerCreator; + @Getter protected final ExecutorService consumerExecutor; + @Getter protected final ScheduledExecutorService scheduler; + @Getter protected final ExecutorService taskExecutor; protected final Consumer uncaughtErrorHandler; diff --git a/common/queue/src/main/java/org/thingsboard/server/queue/common/consumer/PartitionedQueueConsumerManager.java b/common/queue/src/main/java/org/thingsboard/server/queue/common/consumer/PartitionedQueueConsumerManager.java index 57f0950cdb..1b47d4c073 100644 --- a/common/queue/src/main/java/org/thingsboard/server/queue/common/consumer/PartitionedQueueConsumerManager.java +++ b/common/queue/src/main/java/org/thingsboard/server/queue/common/consumer/PartitionedQueueConsumerManager.java @@ -49,11 +49,6 @@ public class PartitionedQueueConsumerManager extends MainQ this.consumerWrapper = (ConsumerPerPartitionWrapper) super.consumerWrapper; } - @Override - public void update(Set partitions) { - throw new UnsupportedOperationException("Use manual addPartitions and removePartitions"); - } - @Override protected void processTask(TbQueueConsumerManagerTask task) { if (task instanceof AddPartitionsTask addPartitionsTask) { diff --git a/common/queue/src/main/java/org/thingsboard/server/queue/common/consumer/QueueStateService.java b/common/queue/src/main/java/org/thingsboard/server/queue/common/consumer/QueueStateService.java index bffe441f7c..d022ad14de 100644 --- a/common/queue/src/main/java/org/thingsboard/server/queue/common/consumer/QueueStateService.java +++ b/common/queue/src/main/java/org/thingsboard/server/queue/common/consumer/QueueStateService.java @@ -41,6 +41,7 @@ public class QueueStateService { } public void update(Set newPartitions) { + newPartitions = withTopic(newPartitions, stateConsumer.getTopic()); lock.lock(); Set oldPartitions = this.partitions != null ? this.partitions : Collections.emptySet(); Set addedPartitions; @@ -54,9 +55,10 @@ public class QueueStateService { } finally { lock.unlock(); } + if (!removedPartitions.isEmpty()) { stateConsumer.removePartitions(removedPartitions); - eventConsumer.removePartitions(removedPartitions.stream().map(tpi -> tpi.withTopic(eventConsumer.getTopic())).collect(Collectors.toSet())); + eventConsumer.removePartitions(withTopic(removedPartitions, eventConsumer.getTopic())); } if (!addedPartitions.isEmpty()) { @@ -73,4 +75,8 @@ public class QueueStateService { } } + private Set withTopic(Set partitions, String topic) { + return partitions.stream().map(tpi -> tpi.withTopic(topic)).collect(Collectors.toSet()); + } + } From a48144d2bb6e49dbc69c5de0e852216423993a49 Mon Sep 17 00:00:00 2001 From: ViacheslavKlimov Date: Thu, 20 Feb 2025 17:34:28 +0200 Subject: [PATCH 079/345] EDQS code cleanup --- .../service/edqs/DefaultEdqsService.java | 1 - .../service/edqs/KafkaEdqsSyncService.java | 13 +- .../sync/tenant/TenantExportService.java | 224 ------------------ application/src/main/resources/logback.xml | 1 - .../src/main/resources/thingsboard.yml | 31 ++- .../EdqsEntityQueryControllerTest.java | 4 +- .../resources/application-test.properties | 2 +- .../src/test/resources/logback-test.xml | 2 +- .../server/edqs/util/EdqsRocksDb.java | 2 +- .../server/queue/edqs/EdqsConfig.java | 2 +- edqs/src/main/resources/edqs.yml | 22 +- 11 files changed, 48 insertions(+), 256 deletions(-) delete mode 100644 application/src/main/java/org/thingsboard/server/service/sync/tenant/TenantExportService.java diff --git a/application/src/main/java/org/thingsboard/server/service/edqs/DefaultEdqsService.java b/application/src/main/java/org/thingsboard/server/service/edqs/DefaultEdqsService.java index 5f30bc291a..d5a65d248c 100644 --- a/application/src/main/java/org/thingsboard/server/service/edqs/DefaultEdqsService.java +++ b/application/src/main/java/org/thingsboard/server/service/edqs/DefaultEdqsService.java @@ -125,7 +125,6 @@ public class DefaultEdqsService implements EdqsService { executor.submit(() -> { try { EdqsSyncState syncState = getSyncState(); - // FIXME: Slavik smart events check if (edqsSyncService.isSyncNeeded() || syncState == null || syncState.getStatus() != EdqsSyncStatus.FINISHED) { if (hashPartitionService.isSystemPartitionMine(ServiceType.TB_CORE)) { processSystemRequest(ToCoreEdqsRequest.builder() diff --git a/application/src/main/java/org/thingsboard/server/service/edqs/KafkaEdqsSyncService.java b/application/src/main/java/org/thingsboard/server/service/edqs/KafkaEdqsSyncService.java index 0c6f948770..8dd4dbe4cb 100644 --- a/application/src/main/java/org/thingsboard/server/service/edqs/KafkaEdqsSyncService.java +++ b/application/src/main/java/org/thingsboard/server/service/edqs/KafkaEdqsSyncService.java @@ -15,7 +15,6 @@ */ package org.thingsboard.server.service.edqs; -import jakarta.annotation.PostConstruct; import lombok.RequiredArgsConstructor; import org.springframework.boot.autoconfigure.condition.ConditionalOnExpression; import org.springframework.stereotype.Service; @@ -30,18 +29,16 @@ import java.util.Collections; @ConditionalOnExpression("'${queue.edqs.sync.enabled:true}' == 'true' && '${queue.type:null}' == 'kafka'") public class KafkaEdqsSyncService extends EdqsSyncService { - private final TbKafkaSettings kafkaSettings; - private TbKafkaAdmin kafkaAdmin; + private final boolean syncNeeded; - @PostConstruct - private void init() { - kafkaAdmin = new TbKafkaAdmin(kafkaSettings, Collections.emptyMap()); + public KafkaEdqsSyncService(TbKafkaSettings kafkaSettings) { + TbKafkaAdmin kafkaAdmin = new TbKafkaAdmin(kafkaSettings, Collections.emptyMap()); + this.syncNeeded = kafkaAdmin.isTopicEmpty(EdqsQueue.EVENTS.getTopic()); } @Override public boolean isSyncNeeded() { - return kafkaAdmin.isTopicEmpty(EdqsQueue.EVENTS.getTopic()); + return syncNeeded; } - } diff --git a/application/src/main/java/org/thingsboard/server/service/sync/tenant/TenantExportService.java b/application/src/main/java/org/thingsboard/server/service/sync/tenant/TenantExportService.java deleted file mode 100644 index 25ef73a081..0000000000 --- a/application/src/main/java/org/thingsboard/server/service/sync/tenant/TenantExportService.java +++ /dev/null @@ -1,224 +0,0 @@ -/** - * Copyright © 2016-2024 The Thingsboard Authors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package org.thingsboard.server.service.sync.tenant; - -import jakarta.annotation.PostConstruct; -import lombok.Data; -import lombok.RequiredArgsConstructor; -import lombok.SneakyThrows; -import lombok.extern.slf4j.Slf4j; -import org.springframework.stereotype.Service; -import org.thingsboard.server.common.data.AttributeScope; -import org.thingsboard.server.common.data.ObjectType; -import org.thingsboard.server.common.data.Tenant; -import org.thingsboard.server.common.data.audit.AuditLog; -import org.thingsboard.server.common.data.event.Event; -import org.thingsboard.server.common.data.event.EventType; -import org.thingsboard.server.common.data.id.EntityId; -import org.thingsboard.server.common.data.id.HasId; -import org.thingsboard.server.common.data.id.TenantId; -import org.thingsboard.server.common.data.edqs.AttributeKv; -import org.thingsboard.server.common.data.kv.AttributeKvEntry; -import org.thingsboard.server.common.data.edqs.LatestTsKv; -import org.thingsboard.server.common.data.kv.TsKvEntry; -import org.thingsboard.server.common.data.page.PageDataIterable; -import org.thingsboard.server.common.data.page.TimePageLink; -import org.thingsboard.server.common.data.relation.EntityRelation; -import org.thingsboard.server.dao.TenantEntityDao; -import org.thingsboard.server.dao.attributes.AttributesDao; -import org.thingsboard.server.dao.audit.AuditLogDao; -import org.thingsboard.server.dao.entity.EntityDaoRegistry; -import org.thingsboard.server.dao.event.EventDao; -import org.thingsboard.server.dao.model.ModelConstants; -import org.thingsboard.server.dao.relation.RelationDao; -import org.thingsboard.server.dao.sqlts.insert.sql.SqlPartitioningRepository; -import org.thingsboard.server.dao.tenant.TenantDao; -import org.thingsboard.server.dao.timeseries.TimeseriesLatestDao; - -import java.util.Collections; -import java.util.EnumSet; -import java.util.HashMap; -import java.util.List; -import java.util.Map; -import java.util.Set; -import java.util.concurrent.TimeUnit; -import java.util.function.BiConsumer; - -import static org.thingsboard.server.common.data.ObjectType.ATTRIBUTE_KV; -import static org.thingsboard.server.common.data.ObjectType.AUDIT_LOG; -import static org.thingsboard.server.common.data.ObjectType.EVENT; -import static org.thingsboard.server.common.data.ObjectType.LATEST_TS_KV; -import static org.thingsboard.server.common.data.ObjectType.RELATION; -import static org.thingsboard.server.common.data.ObjectType.TENANT; - -@Service -@RequiredArgsConstructor -@Slf4j -public class TenantExportService { - - private final EntityDaoRegistry entityDaoRegistry; - private final TenantDao tenantDao; - private final EventDao eventDao; - private final AuditLogDao auditLogDao; - private final AttributesDao attributesDao; - private final RelationDao relationDao; - private final TimeseriesLatestDao timeseriesLatestDao; - private final SqlPartitioningRepository partitioningRepository; - - private Map>> customExporters; - private Map relatedEntitiesExporters; - - private static final Set RELATED = EnumSet.of(EVENT, RELATION, ATTRIBUTE_KV, LATEST_TS_KV); - - @PostConstruct - private void init() { - relatedEntitiesExporters = Map.of( - RELATION, this::exportRelations, - EVENT, this::exportEvents, // todo: query by tenant - ATTRIBUTE_KV, this::exportAttributes, - LATEST_TS_KV, this::exportLatestTelemetry - ); - customExporters = Map.of( - AUDIT_LOG, this::exportAuditLogs - ); - } - - public void exportTenant(TenantId tenantId, ExportConfig config, BiConsumer processor) { - log.info("[{}] Exporting tenant", tenantId); - Tenant tenant = tenantDao.findById(TenantId.SYS_TENANT_ID, tenantId.getId()); - if (tenant == null) { - throw new IllegalArgumentException("Tenant with id " + tenantId + " not found"); - } - - Set objectTypes = config.getIncludedObjectTypes(); - if (objectTypes.contains(TENANT)) { - exportEntity(tenantId, TENANT, tenant, config, processor); - } - - for (ObjectType type : objectTypes) { - if (RELATED.contains(type) || type == TENANT) { - continue; - } - log.debug("[{}] Exporting {} entities", tenantId, type); - if (!customExporters.containsKey(type)) { - TenantEntityDao dao = entityDaoRegistry.getTenantEntityDao(type); - var entities = new PageDataIterable<>(pageLink -> dao.findAllByTenantId(tenantId, pageLink), 100); - for (Object entity : entities) { - exportEntity(tenantId, type, entity, config, processor); - } - } else { - customExporters.get(type).accept(tenantId, processor); - } - } - } - - private void exportEntity(TenantId tenantId, ObjectType type, Object entity, ExportConfig config, BiConsumer processor) { - processor.accept(type, entity); - if (entity instanceof HasId hasId && hasId.getId() instanceof EntityId entityId) { - relatedEntitiesExporters.forEach((relatedEntityType, exporter) -> { - if (config.getIncludedObjectTypes().contains(relatedEntityType)) { - exporter.export(tenantId, entityId, processor); - } - }); - } - } - - private Map getPartitions(String table) { - List partitionsStartTime = partitioningRepository.fetchPartitions(table).stream().sorted().toList(); - if (partitionsStartTime.isEmpty()) { - return Collections.emptyMap(); - } - - Map partitions = new HashMap<>(); - for (int i = 0; i < partitionsStartTime.size(); i++) { - Long startTime = partitionsStartTime.get(i); - Long endTime; - if (partitionsStartTime.size() - 1 == i) { - endTime = System.currentTimeMillis(); - } else { - endTime = partitionsStartTime.get(i + 1) - 1; - } - partitions.put(startTime, endTime); - } - return partitions; - } - - private void exportAuditLogs(TenantId tenantId, BiConsumer processor) { - Map partitions = getPartitions(ModelConstants.AUDIT_LOG_TABLE_NAME); - partitions.forEach((startTime, endTime) -> { - PageDataIterable auditLogs = new PageDataIterable<>(pageLink -> { - return auditLogDao.findAuditLogsByTenantId(tenantId.getId(), null, new TimePageLink(pageLink, startTime, endTime)); - }, 512); - for (AuditLog auditLog : auditLogs) { - processor.accept(AUDIT_LOG, auditLog); - } - }); - } - - private void exportAttributes(TenantId tenantId, EntityId entityId, BiConsumer processor) { - for (AttributeScope attributeScope : AttributeScope.values()) { - List attributes = attributesDao.findAll(tenantId, entityId, attributeScope); - for (AttributeKvEntry entry : attributes) { - AttributeKv attributeKv = new AttributeKv(entityId, attributeScope, entry, entry.getVersion()); - processor.accept(ATTRIBUTE_KV, attributeKv); - } - } - } - - private void exportRelations(TenantId tenantId, EntityId entityId, BiConsumer processor) { - List relations = relationDao.findAllByFrom(tenantId, entityId); - for (EntityRelation relation : relations) { - processor.accept(RELATION, relation); - } - } - - @SneakyThrows - private void exportLatestTelemetry(TenantId tenantId, EntityId entityId, BiConsumer processor) { - List latestTelemetry = timeseriesLatestDao.findAllLatest(tenantId, entityId).get(30, TimeUnit.SECONDS); - for (TsKvEntry tsKvEntry : latestTelemetry) { - LatestTsKv latestTsKv = new LatestTsKv(entityId, tsKvEntry, tsKvEntry.getVersion()); - processor.accept(LATEST_TS_KV, latestTsKv); - } - } - - private void exportEvents(TenantId tenantId, EntityId entityId, BiConsumer processor) { - for (EventType eventType : EventType.values()) { - Map partitions = getPartitions(eventType.getTable()); - partitions.forEach((startTime, endTime) -> { - PageDataIterable events = new PageDataIterable<>(pageLink -> { - return eventDao.findEvents(tenantId.getId(), entityId.getId(), eventType, new TimePageLink(pageLink, startTime, endTime)); - }, 512); - for (Event event : events) { - processor.accept(EVENT, event); - } - }); - } - } - - private interface Exporter { - - void export(TenantId tenantId, EntityId entityId, BiConsumer processor); - - } - - @Data - public static class ExportConfig { - - private Set includedObjectTypes; - - } - -} diff --git a/application/src/main/resources/logback.xml b/application/src/main/resources/logback.xml index 66d30777cd..7b71c1a272 100644 --- a/application/src/main/resources/logback.xml +++ b/application/src/main/resources/logback.xml @@ -28,7 +28,6 @@ - diff --git a/application/src/main/resources/thingsboard.yml b/application/src/main/resources/thingsboard.yml index b20d6f11c3..21459814b7 100644 --- a/application/src/main/resources/thingsboard.yml +++ b/application/src/main/resources/thingsboard.yml @@ -1696,22 +1696,35 @@ queue: print-interval-ms: "${TB_HOUSEKEEPER_STATS_PRINT_INTERVAL_MS:60000}" edqs: sync: - enabled: "${TB_EDQS_SYNC_ENABLED:true}" # Enable/disable EDQS synchronization with postgres db FIXME: disable by default before release - entity_batch_size: "${TB_EDQS_SYNC_ENTITY_BATCH_SIZE:10000}" # batch size of entities being synced with EDQS - ts_batch_size: "${TB_EDQS_SYNC_TS_BATCH_SIZE:10000}" # batch size of timeseries data being synced with EDQS - api_enabled: "${TB_EDQS_API_ENABLED:true}" # FIXME: disable by default before release - mode: "${TB_EDQS_MODE:local}" # local or remote + # Enable/disable EDQS synchronization FIXME: disable by default before release + enabled: "${TB_EDQS_SYNC_ENABLED:true}" + # Batch size of entities being synced with EDQS + entity_batch_size: "${TB_EDQS_SYNC_ENTITY_BATCH_SIZE:10000}" + # Batch size of timeseries data being synced with EDQS + ts_batch_size: "${TB_EDQS_SYNC_TS_BATCH_SIZE:10000}" + # Whether to forward entity data query requests to EDQS (otherwise use PostgreSQL implementation) FIXME: disable by default before release + api_enabled: "${TB_EDQS_API_ENABLED:true}" + # Mode of EDQS: local (for monolith) or remote (with separate EDQS microservices) + mode: "${TB_EDQS_MODE:local}" local: - rocksdb_path: "${TB_EDQS_ROCKSDB_PATH:/tmp/edqs-backup}" + # Path to RocksDB for EDQS backup when running in local mode + rocksdb_path: "${TB_EDQS_ROCKSDB_PATH:${user.home}/.rocksdb/edqs}" + # Number of partitions for EDQS topics partitions: "${TB_EDQS_PARTITIONS:12}" - partitioning_strategy: "${TB_EDQS_PARTITIONING_STRATEGY:tenant}" # tenant or none. For 'none', each instance handles all partitions and duplicates all the data + # EDQS partitioning strategy: tenant (partition is resolved by tenant id) or none (no specific strategy, resolving by message key) + partitioning_strategy: "${TB_EDQS_PARTITIONING_STRATEGY:tenant}" + # EDQS requests topic requests_topic: "${TB_EDQS_REQUESTS_TOPIC:edqs.requests}" + # EDQS responses topic responses_topic: "${TB_EDQS_RESPONSES_TOPIC:edqs.responses}" + # Poll interval for EDQS topics poll_interval: "${TB_EDQS_POLL_INTERVAL_MS:125}" + # Maximum amount of pending requests to EDQS max_pending_requests: "${TB_EDQS_MAX_PENDING_REQUESTS:10000}" - max_request_timeout: "${TB_EDQS_MAX_REQUEST_TIMEOUT:10000}" + # Maximum timeout for requests to EDQS + max_request_timeout: "${TB_EDQS_MAX_REQUEST_TIMEOUT:20000}" stats: - # Enable/disable statistics for EDQS service + # Enable/disable statistics for EDQS enabled: "${TB_EDQS_STATS_ENABLED:true}" # Statistics printing interval for EDQS print-interval-ms: "${TB_EDQS_STATS_PRINT_INTERVAL_MS:60000}" diff --git a/application/src/test/java/org/thingsboard/server/controller/EdqsEntityQueryControllerTest.java b/application/src/test/java/org/thingsboard/server/controller/EdqsEntityQueryControllerTest.java index 6833381f2d..efc290d3c5 100644 --- a/application/src/test/java/org/thingsboard/server/controller/EdqsEntityQueryControllerTest.java +++ b/application/src/test/java/org/thingsboard/server/controller/EdqsEntityQueryControllerTest.java @@ -33,8 +33,8 @@ import static org.awaitility.Awaitility.await; @DaoSqlTest @TestPropertySource(properties = { - "queue.type=kafka", // uncomment to use Kafka - "queue.kafka.bootstrap.servers=192.168.0.105:9092", +// "queue.type=kafka", // uncomment to use Kafka +// "queue.kafka.bootstrap.servers=192.168.0.105:9092", "queue.edqs.sync.enabled=true", "queue.edqs.api_enabled=true", "queue.edqs.mode=local" diff --git a/application/src/test/resources/application-test.properties b/application/src/test/resources/application-test.properties index 8933c36db5..bf90333c52 100644 --- a/application/src/test/resources/application-test.properties +++ b/application/src/test/resources/application-test.properties @@ -53,7 +53,7 @@ sql.ttl.audit_logs.ttl=2592000 sql.edge_events.partition_size=168 sql.ttl.edge_events.edge_event_ttl=2592000 -server.log_controller_error_stack_trace=true +server.log_controller_error_stack_trace=false transport.gateway.dashboard.sync.enabled=false diff --git a/application/src/test/resources/logback-test.xml b/application/src/test/resources/logback-test.xml index a0efcf52c1..13c93da411 100644 --- a/application/src/test/resources/logback-test.xml +++ b/application/src/test/resources/logback-test.xml @@ -9,7 +9,7 @@ - + diff --git a/common/edqs/src/main/java/org/thingsboard/server/edqs/util/EdqsRocksDb.java b/common/edqs/src/main/java/org/thingsboard/server/edqs/util/EdqsRocksDb.java index 9b91b21c0d..eb3ef42f66 100644 --- a/common/edqs/src/main/java/org/thingsboard/server/edqs/util/EdqsRocksDb.java +++ b/common/edqs/src/main/java/org/thingsboard/server/edqs/util/EdqsRocksDb.java @@ -33,7 +33,7 @@ public class EdqsRocksDb extends TbRocksDb { @Getter private boolean isNew; - public EdqsRocksDb(@Value("${queue.edqs.local.rocksdb_path:${java.io.tmpdir}/edqs-backup}") String path) { + public EdqsRocksDb(@Value("${queue.edqs.local.rocksdb_path:${user.home}/.rocksdb/edqs}") String path) { super(path, new Options().setCreateIfMissing(true)); } diff --git a/common/queue/src/main/java/org/thingsboard/server/queue/edqs/EdqsConfig.java b/common/queue/src/main/java/org/thingsboard/server/queue/edqs/EdqsConfig.java index f35827a8ce..8e3bd3b261 100644 --- a/common/queue/src/main/java/org/thingsboard/server/queue/edqs/EdqsConfig.java +++ b/common/queue/src/main/java/org/thingsboard/server/queue/edqs/EdqsConfig.java @@ -38,7 +38,7 @@ public class EdqsConfig { private long pollInterval; @Value("${queue.edqs.max_pending_requests:10000}") private int maxPendingRequests; - @Value("${queue.edqs.max_request_timeout:10000}") + @Value("${queue.edqs.max_request_timeout:20000}") private int maxRequestTimeout; public String getLabel() { diff --git a/edqs/src/main/resources/edqs.yml b/edqs/src/main/resources/edqs.yml index b67f2f5f9e..fa09da1cdd 100644 --- a/edqs/src/main/resources/edqs.yml +++ b/edqs/src/main/resources/edqs.yml @@ -47,19 +47,27 @@ spring: queue: type: "${TB_QUEUE_TYPE:kafka}" # kafka (Apache Kafka) prefix: "${TB_QUEUE_PREFIX:}" # Global queue prefix. If specified, prefix is added before default topic name: 'prefix.default_topic_name'. Prefix is applied to all topics (and consumer groups for kafka). - in_memory: - stats: - # For debug level - print-interval-ms: "${TB_QUEUE_IN_MEMORY_STATS_PRINT_INTERVAL_MS:60000}" edqs: - mode: "${TB_EDQS_MODE:local}" + # Number of partitions for EDQS topics partitions: "${TB_EDQS_PARTITIONS:12}" - partitioning_strategy: "${TB_EDQS_PARTITIONING_STRATEGY:tenant}" # tenant or none. For 'none', each instance handles all partitions and duplicates all the data + # EDQS partitioning strategy: tenant (partitions are resolved and distributed by tenant id) or none (partitions are resolved by message key; each instance has all the partitions) + partitioning_strategy: "${TB_EDQS_PARTITIONING_STRATEGY:tenant}" + # EDQS requests topic requests_topic: "${TB_EDQS_REQUESTS_TOPIC:edqs.requests}" + # EDQS responses topic responses_topic: "${TB_EDQS_RESPONSES_TOPIC:edqs.responses}" + # Poll interval for EDQS topics poll_interval: "${TB_EDQS_POLL_INTERVAL_MS:125}" + # Maximum amount of pending requests to EDQS max_pending_requests: "${TB_EDQS_MAX_PENDING_REQUESTS:10000}" - max_request_timeout: "${TB_EDQS_MAX_REQUEST_TIMEOUT:10000}" + # Maximum timeout for requests to EDQS + max_request_timeout: "${TB_EDQS_MAX_REQUEST_TIMEOUT:20000}" + stats: + # Enable/disable statistics for EDQS + enabled: "${TB_EDQS_STATS_ENABLED:true}" + # Statistics printing interval for EDQS + print-interval-ms: "${TB_EDQS_STATS_PRINT_INTERVAL_MS:60000}" + kafka: # Kafka Bootstrap nodes in "host:port" format bootstrap.servers: "${TB_KAFKA_SERVERS:localhost:9092}" From 347cb5bc368e344433a5474fde5f5c65205bc89c Mon Sep 17 00:00:00 2001 From: ViacheslavKlimov Date: Fri, 21 Feb 2025 11:48:21 +0200 Subject: [PATCH 080/345] EDQS code cleanup --- .../service/edqs/DefaultEdqsService.java | 3 + .../service/edqs/KafkaEdqsSyncService.java | 2 - .../server/common/data/ObjectType.java | 9 ++- .../server/common/data/alarm/AlarmType.java | 27 -------- .../data/edqs/fields/GenericFields.java | 16 +---- .../common/data/edqs/fields/TenantFields.java | 3 - .../data/page/BasePageDataIterable.java | 9 +-- .../common/data/page/PageDataIterable.java | 5 -- .../common/data/query/EntityFilter.java | 3 +- .../data/dp/CompressedStringDataPoint.java | 3 +- .../server/edqs/data/dp/JsonDataPoint.java | 2 +- .../server/edqs/data/dp/StringDataPoint.java | 2 +- .../server/edqs/processor/EdqsProcessor.java | 63 ++++++++++-------- .../server/edqs/processor/EdqsProducer.java | 10 +-- ...sitory.java => DefaultEdqsRepository.java} | 16 ++--- ...EdqRepository.java => EdqsRepository.java} | 5 +- .../server/edqs/repo/TenantRepo.java | 1 + .../edqs/state/KafkaEdqsStateService.java | 4 +- .../server/edqs/util/EdqsConverter.java | 5 +- .../edqs/{repo => util}/TbBytePool.java | 2 +- .../edqs/{repo => util}/TbStringPool.java | 2 +- .../queue/edqs/KafkaEdqsQueueFactory.java | 15 +++-- .../provider/KafkaMonolithQueueFactory.java | 2 +- .../provider/KafkaTbCoreQueueFactory.java | 2 +- .../common/util/ExceptionUtil.java | 10 +++ .../server/dao/TenantEntityDao.java | 5 -- .../server/dao/entity/EntityDaoRegistry.java | 18 +---- .../server/dao/model/sql/AlarmTypeEntity.java | 65 ------------------- .../dao/sql/alarm/AlarmTypeRepository.java | 30 --------- .../dao/sql/alarm/JpaAlarmCommentDao.java | 6 -- .../server/dao/sql/alarm/JpaAlarmDao.java | 6 -- .../server/dao/sql/alarm/JpaAlarmTypeDao.java | 46 ------------- .../dao/sql/alarm/JpaEntityAlarmDao.java | 6 -- .../server/dao/sql/asset/JpaAssetDao.java | 6 -- .../dao/sql/asset/JpaAssetProfileDao.java | 6 -- .../dao/sql/customer/JpaCustomerDao.java | 6 -- .../dao/sql/dashboard/JpaDashboardDao.java | 6 -- .../sql/device/DeviceProfileRepository.java | 1 - .../sql/device/JpaDeviceCredentialsDao.java | 6 -- .../server/dao/sql/device/JpaDeviceDao.java | 6 -- .../dao/sql/device/JpaDeviceProfileDao.java | 6 -- .../server/dao/sql/edge/JpaEdgeDao.java | 6 -- .../sql/entityview/EntityViewRepository.java | 1 - .../dao/sql/entityview/JpaEntityViewDao.java | 6 -- .../notification/JpaNotificationRuleDao.java | 6 -- .../JpaNotificationTargetDao.java | 6 -- .../JpaNotificationTemplateDao.java | 6 -- .../server/dao/sql/ota/JpaOtaPackageDao.java | 6 -- .../server/dao/sql/queue/JpaQueueDao.java | 6 -- .../dao/sql/queue/JpaQueueStatsDao.java | 6 -- .../dao/sql/resource/JpaTbResourceDao.java | 6 -- .../server/dao/sql/rpc/JpaRpcDao.java | 6 -- .../server/dao/sql/rule/JpaRuleChainDao.java | 6 -- .../server/dao/sql/rule/JpaRuleNodeDao.java | 6 -- .../dao/sql/settings/JpaAdminSettingsDao.java | 6 -- .../sql/usagerecord/JpaApiUsageStateDao.java | 6 -- .../dao/sql/user/JpaUserAuthSettingsDao.java | 6 -- .../dao/sql/user/JpaUserCredentialsDao.java | 6 -- .../server/dao/sql/user/JpaUserDao.java | 6 -- .../dao/sql/user/JpaUserSettingsDao.java | 6 -- .../dao/sql/widget/JpaWidgetTypeDao.java | 6 -- .../dao/sql/widget/JpaWidgetsBundleDao.java | 6 -- .../dao/service/EntityDaoRegistryTest.java | 31 --------- edqs/src/main/resources/logback.xml | 1 - .../server/edqs/repo/AbstractEDQTest.java | 4 +- ...q-test.properties => edqs-test.properties} | 0 66 files changed, 99 insertions(+), 504 deletions(-) delete mode 100644 common/data/src/main/java/org/thingsboard/server/common/data/alarm/AlarmType.java rename common/edqs/src/main/java/org/thingsboard/server/edqs/repo/{InMemoryEdqRepository.java => DefaultEdqsRepository.java} (89%) rename common/edqs/src/main/java/org/thingsboard/server/edqs/repo/{EdqRepository.java => EdqsRepository.java} (92%) rename common/edqs/src/main/java/org/thingsboard/server/edqs/{repo => util}/TbBytePool.java (96%) rename common/edqs/src/main/java/org/thingsboard/server/edqs/{repo => util}/TbStringPool.java (96%) delete mode 100644 dao/src/main/java/org/thingsboard/server/dao/model/sql/AlarmTypeEntity.java delete mode 100644 dao/src/main/java/org/thingsboard/server/dao/sql/alarm/AlarmTypeRepository.java delete mode 100644 dao/src/main/java/org/thingsboard/server/dao/sql/alarm/JpaAlarmTypeDao.java rename edqs/src/test/resources/{edq-test.properties => edqs-test.properties} (100%) diff --git a/application/src/main/java/org/thingsboard/server/service/edqs/DefaultEdqsService.java b/application/src/main/java/org/thingsboard/server/service/edqs/DefaultEdqsService.java index d5a65d248c..a6a9b9bb23 100644 --- a/application/src/main/java/org/thingsboard/server/service/edqs/DefaultEdqsService.java +++ b/application/src/main/java/org/thingsboard/server/service/edqs/DefaultEdqsService.java @@ -68,6 +68,7 @@ import org.thingsboard.server.gen.transport.TransportProtos.ToEdqsMsg; import org.thingsboard.server.queue.TbQueueRequestTemplate; import org.thingsboard.server.queue.common.TbProtoQueueMsg; import org.thingsboard.server.queue.discovery.HashPartitionService; +import org.thingsboard.server.queue.discovery.TopicService; import org.thingsboard.server.queue.edqs.EdqsQueue; import org.thingsboard.server.queue.environment.DistributedLock; import org.thingsboard.server.queue.environment.DistributedLockService; @@ -90,6 +91,7 @@ public class DefaultEdqsService implements EdqsService { private final DistributedLockService distributedLockService; private final AttributesService attributesService; private final EdqsPartitionService edqsPartitionService; + private final TopicService topicService; @Autowired @Lazy private TbClusterService clusterService; @Autowired @Lazy @@ -109,6 +111,7 @@ public class DefaultEdqsService implements EdqsService { eventsProducer = EdqsProducer.builder() .queue(EdqsQueue.EVENTS) .partitionService(edqsPartitionService) + .topicService(topicService) .producer(queueFactory.createEdqsMsgProducer(EdqsQueue.EVENTS)) .build(); if (apiEnabled) { diff --git a/application/src/main/java/org/thingsboard/server/service/edqs/KafkaEdqsSyncService.java b/application/src/main/java/org/thingsboard/server/service/edqs/KafkaEdqsSyncService.java index 8dd4dbe4cb..7dc25b92c8 100644 --- a/application/src/main/java/org/thingsboard/server/service/edqs/KafkaEdqsSyncService.java +++ b/application/src/main/java/org/thingsboard/server/service/edqs/KafkaEdqsSyncService.java @@ -15,7 +15,6 @@ */ package org.thingsboard.server.service.edqs; -import lombok.RequiredArgsConstructor; import org.springframework.boot.autoconfigure.condition.ConditionalOnExpression; import org.springframework.stereotype.Service; import org.thingsboard.server.queue.edqs.EdqsQueue; @@ -25,7 +24,6 @@ import org.thingsboard.server.queue.kafka.TbKafkaSettings; import java.util.Collections; @Service -@RequiredArgsConstructor @ConditionalOnExpression("'${queue.edqs.sync.enabled:true}' == 'true' && '${queue.type:null}' == 'kafka'") public class KafkaEdqsSyncService extends EdqsSyncService { diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/ObjectType.java b/common/data/src/main/java/org/thingsboard/server/common/data/ObjectType.java index 86252fb7f8..8fa08d38e9 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/ObjectType.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/ObjectType.java @@ -23,7 +23,6 @@ public enum ObjectType { TENANT, TENANT_PROFILE, CUSTOMER, - ADMIN_SETTINGS, QUEUE, RPC, RULE_CHAIN, @@ -32,8 +31,6 @@ public enum ObjectType { EVENT, RULE_NODE, USER, - USER_CREDENTIALS, - USER_AUTH_SETTINGS, EDGE, WIDGETS_BUNDLE, WIDGET_TYPE, @@ -54,7 +51,6 @@ public enum ObjectType { NOTIFICATION_TEMPLATE, NOTIFICATION_RULE, ALARM_COMMENT, - ALARM_TYPE, API_USAGE_STATE, QUEUE_STATS, @@ -70,7 +66,10 @@ public enum ObjectType { public static final Set edqsTypes = EnumSet.copyOf(edqsTenantTypes); public static final Set edqsSystemTypes = EnumSet.of(TENANT, TENANT_PROFILE, USER, DASHBOARD, API_USAGE_STATE, ATTRIBUTE_KV, LATEST_TS_KV); - public static final Set unversionedTypes = EnumSet.of(QUEUE_STATS); + public static final Set unversionedTypes = EnumSet.of( + QUEUE_STATS, // created once, never updated + TENANT_PROFILE // only for total count calculation + ); static { edqsTypes.addAll(List.of(RELATION, ATTRIBUTE_KV, LATEST_TS_KV)); diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/alarm/AlarmType.java b/common/data/src/main/java/org/thingsboard/server/common/data/alarm/AlarmType.java deleted file mode 100644 index 0813e72123..0000000000 --- a/common/data/src/main/java/org/thingsboard/server/common/data/alarm/AlarmType.java +++ /dev/null @@ -1,27 +0,0 @@ -/** - * Copyright © 2016-2024 The Thingsboard Authors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package org.thingsboard.server.common.data.alarm; - -import lombok.Data; -import org.thingsboard.server.common.data.id.TenantId; - -@Data -public class AlarmType { - - private TenantId tenantId; - private String type; - -} diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/edqs/fields/GenericFields.java b/common/data/src/main/java/org/thingsboard/server/common/data/edqs/fields/GenericFields.java index ff3af4ee65..94e8e35dcc 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/edqs/fields/GenericFields.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/edqs/fields/GenericFields.java @@ -15,27 +15,15 @@ */ package org.thingsboard.server.common.data.edqs.fields; -import com.fasterxml.jackson.databind.JsonNode; -import lombok.Data; import lombok.NoArgsConstructor; -import lombok.experimental.SuperBuilder; -import java.util.UUID; -import static org.thingsboard.server.common.data.edqs.fields.FieldsUtil.getText; +import java.util.UUID; -@Data @NoArgsConstructor -@SuperBuilder public class GenericFields extends AbstractEntityFields { - private String additionalInfo; - - public GenericFields(UUID id, long createdTime, UUID tenantId, String name, Long version, JsonNode additionalInfo) { - super(id, createdTime, tenantId, name, version); - this.additionalInfo = getText(additionalInfo); - } - public GenericFields(UUID id, long createdTime, UUID tenantId, String name, Long version) { super(id, createdTime, tenantId, name, version); } + } diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/edqs/fields/TenantFields.java b/common/data/src/main/java/org/thingsboard/server/common/data/edqs/fields/TenantFields.java index 6942d5ea7b..c11c221a9b 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/edqs/fields/TenantFields.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/edqs/fields/TenantFields.java @@ -56,7 +56,4 @@ public class TenantFields extends AbstractEntityFields { this.region = region; } - public TenantFields(UUID id, Long version) { - super(id, 0L, null, version); - } } diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/page/BasePageDataIterable.java b/common/data/src/main/java/org/thingsboard/server/common/data/page/BasePageDataIterable.java index 625a1d417c..8d46a46781 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/page/BasePageDataIterable.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/page/BasePageDataIterable.java @@ -23,7 +23,6 @@ import java.util.NoSuchElementException; public abstract class BasePageDataIterable implements Iterable, Iterator { private final int fetchSize; - private SortOrder sortOrder; private List currentItems; private int currentIdx; @@ -36,12 +35,6 @@ public abstract class BasePageDataIterable implements Iterable, Iterator iterator() { return this; @@ -50,7 +43,7 @@ public abstract class BasePageDataIterable implements Iterable, Iterator extends BasePageDataIterable { this.function = function; } - public PageDataIterable(FetchFunction function, int fetchSize, SortOrder sortOrder) { - super(fetchSize, sortOrder); - this.function = function; - } - @Override PageData fetchPageData(PageLink link) { return function.fetch(link); diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/query/EntityFilter.java b/common/data/src/main/java/org/thingsboard/server/common/data/query/EntityFilter.java index f881ed15c3..8e45e20efa 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/query/EntityFilter.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/query/EntityFilter.java @@ -39,7 +39,8 @@ import com.fasterxml.jackson.annotation.JsonTypeInfo; @JsonSubTypes.Type(value = AssetSearchQueryFilter.class, name = "assetSearchQuery"), @JsonSubTypes.Type(value = DeviceSearchQueryFilter.class, name = "deviceSearchQuery"), @JsonSubTypes.Type(value = EntityViewSearchQueryFilter.class, name = "entityViewSearchQuery"), - @JsonSubTypes.Type(value = EdgeSearchQueryFilter.class, name = "edgeSearchQuery")}) + @JsonSubTypes.Type(value = EdgeSearchQueryFilter.class, name = "edgeSearchQuery") +}) public interface EntityFilter { @JsonIgnore diff --git a/common/edqs/src/main/java/org/thingsboard/server/edqs/data/dp/CompressedStringDataPoint.java b/common/edqs/src/main/java/org/thingsboard/server/edqs/data/dp/CompressedStringDataPoint.java index 0502bbccda..0ef614de06 100644 --- a/common/edqs/src/main/java/org/thingsboard/server/edqs/data/dp/CompressedStringDataPoint.java +++ b/common/edqs/src/main/java/org/thingsboard/server/edqs/data/dp/CompressedStringDataPoint.java @@ -18,6 +18,7 @@ package org.thingsboard.server.edqs.data.dp; import lombok.Getter; import lombok.SneakyThrows; import org.thingsboard.server.common.data.kv.DataType; +import org.thingsboard.server.edqs.util.TbBytePool; import org.xerial.snappy.Snappy; public class CompressedStringDataPoint extends AbstractDataPoint { @@ -29,7 +30,7 @@ public class CompressedStringDataPoint extends AbstractDataPoint { @SneakyThrows public CompressedStringDataPoint(long ts, byte[] compressedValue) { super(ts); - this.compressedValue = compressedValue; + this.compressedValue = TbBytePool.intern(compressedValue); } @Override diff --git a/common/edqs/src/main/java/org/thingsboard/server/edqs/data/dp/JsonDataPoint.java b/common/edqs/src/main/java/org/thingsboard/server/edqs/data/dp/JsonDataPoint.java index 05cafa3edb..739cd1902e 100644 --- a/common/edqs/src/main/java/org/thingsboard/server/edqs/data/dp/JsonDataPoint.java +++ b/common/edqs/src/main/java/org/thingsboard/server/edqs/data/dp/JsonDataPoint.java @@ -17,7 +17,7 @@ package org.thingsboard.server.edqs.data.dp; import lombok.Getter; import org.thingsboard.server.common.data.kv.DataType; -import org.thingsboard.server.edqs.repo.TbStringPool; +import org.thingsboard.server.edqs.util.TbStringPool; public class JsonDataPoint extends AbstractDataPoint { diff --git a/common/edqs/src/main/java/org/thingsboard/server/edqs/data/dp/StringDataPoint.java b/common/edqs/src/main/java/org/thingsboard/server/edqs/data/dp/StringDataPoint.java index fabf15a359..281c467c31 100644 --- a/common/edqs/src/main/java/org/thingsboard/server/edqs/data/dp/StringDataPoint.java +++ b/common/edqs/src/main/java/org/thingsboard/server/edqs/data/dp/StringDataPoint.java @@ -17,7 +17,7 @@ package org.thingsboard.server.edqs.data.dp; import lombok.Getter; import org.thingsboard.server.common.data.kv.DataType; -import org.thingsboard.server.edqs.repo.TbStringPool; +import org.thingsboard.server.edqs.util.TbStringPool; public class StringDataPoint extends AbstractDataPoint { diff --git a/common/edqs/src/main/java/org/thingsboard/server/edqs/processor/EdqsProcessor.java b/common/edqs/src/main/java/org/thingsboard/server/edqs/processor/EdqsProcessor.java index 968e637a3d..42e00be7fc 100644 --- a/common/edqs/src/main/java/org/thingsboard/server/edqs/processor/EdqsProcessor.java +++ b/common/edqs/src/main/java/org/thingsboard/server/edqs/processor/EdqsProcessor.java @@ -24,12 +24,12 @@ import jakarta.annotation.PreDestroy; import lombok.Getter; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; -import org.apache.commons.lang3.exception.ExceptionUtils; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.context.ConfigurableApplicationContext; import org.springframework.context.annotation.Lazy; import org.springframework.context.event.EventListener; import org.springframework.stereotype.Service; +import org.thingsboard.common.util.ExceptionUtil; import org.thingsboard.common.util.JacksonUtil; import org.thingsboard.common.util.ThingsBoardExecutors; import org.thingsboard.common.util.ThingsBoardThreadFactory; @@ -46,7 +46,7 @@ import org.thingsboard.server.common.data.page.PageData; import org.thingsboard.server.common.data.util.CollectionsUtil; import org.thingsboard.server.common.msg.queue.ServiceType; import org.thingsboard.server.common.msg.queue.TopicPartitionInfo; -import org.thingsboard.server.edqs.repo.EdqRepository; +import org.thingsboard.server.edqs.repo.EdqsRepository; import org.thingsboard.server.edqs.state.EdqsPartitionService; import org.thingsboard.server.edqs.state.EdqsStateService; import org.thingsboard.server.edqs.util.EdqsConverter; @@ -68,6 +68,7 @@ import org.thingsboard.server.queue.edqs.EdqsQueue; import org.thingsboard.server.queue.edqs.EdqsQueueFactory; import org.thingsboard.server.queue.util.AfterStartUp; +import java.util.Objects; import java.util.Set; import java.util.UUID; import java.util.concurrent.ExecutorService; @@ -85,7 +86,7 @@ public class EdqsProcessor implements TbQueueHandler, private final EdqsQueueFactory queueFactory; private final EdqsConverter converter; - private final EdqRepository repository; + private final EdqsRepository repository; private final EdqsConfig config; private final EdqsPartitionService partitionService; private final ConfigurableApplicationContext applicationContext; @@ -99,11 +100,10 @@ public class EdqsProcessor implements TbQueueHandler, private ExecutorService taskExecutor; private ScheduledExecutorService scheduler; private ListeningExecutorService requestExecutor; - private ExecutorService repartitionExecutor; private final VersionsStore versionsStore = new VersionsStore(); - private final AtomicInteger counter = new AtomicInteger(); // FIXME: TMP + private final AtomicInteger counter = new AtomicInteger(); @Getter private Consumer errorHandler; @@ -114,7 +114,6 @@ public class EdqsProcessor implements TbQueueHandler, taskExecutor = ThingsBoardExecutors.newWorkStealingPool(4, "edqs-consumer-task-executor"); scheduler = ThingsBoardExecutors.newSingleThreadScheduledExecutor("edqs-scheduler"); requestExecutor = MoreExecutors.listeningDecorator(ThingsBoardExecutors.newWorkStealingPool(12, "edqs-requests")); - repartitionExecutor = Executors.newSingleThreadExecutor(ThingsBoardThreadFactory.forName("edqs-repartition")); errorHandler = error -> { if (error instanceof OutOfMemoryError) { log.error("OOM detected, shutting down"); @@ -135,7 +134,6 @@ public class EdqsProcessor implements TbQueueHandler, } try { ToEdqsMsg msg = queueMsg.getValue(); - log.trace("Processing message: {}", msg); process(msg, EdqsQueue.EVENTS); } catch (Exception t) { log.error("Failed to process message: {}", queueMsg, t); @@ -195,27 +193,19 @@ public class EdqsProcessor implements TbQueueHandler, public ListenableFuture> handle(TbProtoQueueMsg queueMsg) { ToEdqsMsg toEdqsMsg = queueMsg.getValue(); return requestExecutor.submit(() -> { - EdqsResponse response = new EdqsResponse(); + EdqsRequest request; + TenantId tenantId; + CustomerId customerId; try { - EdqsRequest request = JacksonUtil.fromString(toEdqsMsg.getRequestMsg().getValue(), EdqsRequest.class); - TenantId tenantId = getTenantId(toEdqsMsg); - CustomerId customerId = getCustomerId(toEdqsMsg); - log.info("[{}] Handling request: {}", tenantId, request); - - if (request.getEntityDataQuery() != null) { - PageData result = repository.findEntityDataByQuery(tenantId, customerId, - request.getEntityDataQuery(), false); - response.setEntityDataQueryResult(result.mapData(QueryResult::toOldEntityData)); - } else if (request.getEntityCountQuery() != null) { - long result = repository.countEntitiesByQuery(tenantId, customerId, request.getEntityCountQuery(), tenantId.isSysTenantId()); - response.setEntityCountQueryResult(result); - } - - log.info("Answering with response: {}", response); - } catch (Throwable e) { - response.setError(ExceptionUtils.getStackTrace(e)); // TODO: return only the message - log.info("Answering with error", e); + request = Objects.requireNonNull(JacksonUtil.fromString(toEdqsMsg.getRequestMsg().getValue(), EdqsRequest.class)); + tenantId = getTenantId(toEdqsMsg); + customerId = getCustomerId(toEdqsMsg); + } catch (Exception e) { + log.error("Failed to parse request msg: {}", toEdqsMsg, e); + throw e; } + + EdqsResponse response = processRequest(tenantId, customerId, request); return new TbProtoQueueMsg<>(queueMsg.getKey(), FromEdqsMsg.newBuilder() .setResponseMsg(TransportProtos.EdqsResponseMsg.newBuilder() .setValue(JacksonUtil.toString(response)) @@ -224,7 +214,27 @@ public class EdqsProcessor implements TbQueueHandler, }); } + private EdqsResponse processRequest(TenantId tenantId, CustomerId customerId, EdqsRequest request) { + EdqsResponse response = new EdqsResponse(); + try { + if (request.getEntityDataQuery() != null) { + PageData result = repository.findEntityDataByQuery(tenantId, customerId, + request.getEntityDataQuery(), false); + response.setEntityDataQueryResult(result.mapData(QueryResult::toOldEntityData)); + } else if (request.getEntityCountQuery() != null) { + long result = repository.countEntitiesByQuery(tenantId, customerId, request.getEntityCountQuery(), tenantId.isSysTenantId()); + response.setEntityCountQueryResult(result); + } + log.trace("[{}] Request: {}, response: {}", tenantId, request, response); + } catch (Throwable e) { + log.error("[{}] Failed to process request: {}", tenantId, request, e); + response.setError(ExceptionUtil.getMessage(e)); + } + return response; + } + public void process(ToEdqsMsg edqsMsg, EdqsQueue queue) { + log.trace("Processing message: {}", edqsMsg); if (edqsMsg.hasEventMsg()) { EdqsEventMsg eventMsg = edqsMsg.getEventMsg(); TenantId tenantId = getTenantId(edqsMsg); @@ -290,7 +300,6 @@ public class EdqsProcessor implements TbQueueHandler, taskExecutor.shutdownNow(); scheduler.shutdownNow(); requestExecutor.shutdownNow(); - repartitionExecutor.shutdownNow(); } } diff --git a/common/edqs/src/main/java/org/thingsboard/server/edqs/processor/EdqsProducer.java b/common/edqs/src/main/java/org/thingsboard/server/edqs/processor/EdqsProducer.java index bfd0d9df59..9ea027ec10 100644 --- a/common/edqs/src/main/java/org/thingsboard/server/edqs/processor/EdqsProducer.java +++ b/common/edqs/src/main/java/org/thingsboard/server/edqs/processor/EdqsProducer.java @@ -27,6 +27,7 @@ import org.thingsboard.server.queue.TbQueueCallback; import org.thingsboard.server.queue.TbQueueMsgMetadata; import org.thingsboard.server.queue.TbQueueProducer; import org.thingsboard.server.queue.common.TbProtoQueueMsg; +import org.thingsboard.server.queue.discovery.TopicService; import org.thingsboard.server.queue.edqs.EdqsQueue; import org.thingsboard.server.queue.kafka.TbKafkaProducerTemplate; @@ -35,26 +36,27 @@ public class EdqsProducer { private final EdqsQueue queue; private final EdqsPartitionService partitionService; + private final TopicService topicService; private final TbQueueProducer> producer; @Builder public EdqsProducer(EdqsQueue queue, EdqsPartitionService partitionService, + TopicService topicService, TbQueueProducer> producer) { this.queue = queue; this.partitionService = partitionService; + this.topicService = topicService; this.producer = producer; } - // TODO: queue prefix! - public void send(TenantId tenantId, ObjectType type, String key, ToEdqsMsg msg) { - String topic = queue.getTopic(); + String topic = topicService.buildTopicName(queue.getTopic()); TbQueueCallback callback = new TbQueueCallback() { @Override public void onSuccess(TbQueueMsgMetadata metadata) { - log.trace("[{}][{}][{}] Published msg to {}: {}", tenantId, type, key, topic, msg); // fixme log levels + log.trace("[{}][{}][{}] Published msg to {}: {}", tenantId, type, key, topic, msg); } @Override diff --git a/common/edqs/src/main/java/org/thingsboard/server/edqs/repo/InMemoryEdqRepository.java b/common/edqs/src/main/java/org/thingsboard/server/edqs/repo/DefaultEdqsRepository.java similarity index 89% rename from common/edqs/src/main/java/org/thingsboard/server/edqs/repo/InMemoryEdqRepository.java rename to common/edqs/src/main/java/org/thingsboard/server/edqs/repo/DefaultEdqsRepository.java index 7736f303ac..bdfe76552a 100644 --- a/common/edqs/src/main/java/org/thingsboard/server/edqs/repo/InMemoryEdqRepository.java +++ b/common/edqs/src/main/java/org/thingsboard/server/edqs/repo/DefaultEdqsRepository.java @@ -39,7 +39,7 @@ import java.util.function.Predicate; @AllArgsConstructor @Service @Slf4j -public class InMemoryEdqRepository implements EdqRepository { +public class DefaultEdqsRepository implements EdqsRepository { private final static ConcurrentMap repos = new ConcurrentHashMap<>(); private final Optional statsService; @@ -51,7 +51,7 @@ public class InMemoryEdqRepository implements EdqRepository { @Override public void processEvent(EdqsEvent event) { if (event.getEventType() == EdqsEventType.DELETED && event.getObjectType() == ObjectType.TENANT) { - log.info("Deleting tenant repo: {}", event); + log.info("Tenant {} deleted", event.getTenantId()); repos.remove(event.getTenantId()); } else { get(event.getTenantId()).processEvent(event); @@ -62,25 +62,25 @@ public class InMemoryEdqRepository implements EdqRepository { public long countEntitiesByQuery(TenantId tenantId, CustomerId customerId, EntityCountQuery query, boolean ignorePermissionCheck) { long startNs = System.nanoTime(); long result = 0; - if (TenantId.SYS_TENANT_ID.equals(tenantId)) { + if (!tenantId.isSysTenantId()) { + result = get(tenantId).countEntitiesByQuery(customerId, query, ignorePermissionCheck); + } else { for (TenantRepo repo : repos.values()) { result += repo.countEntitiesByQuery(customerId, query, ignorePermissionCheck); } - } else { - result = get(tenantId).countEntitiesByQuery(customerId, query, ignorePermissionCheck); } double timingMs = (double) (System.nanoTime() - startNs) / 1000_000; - log.info("countEntitiesByQuery: {} ms", timingMs); + log.info("countEntitiesByQuery done in {} ms", timingMs); return result; } @Override public PageData findEntityDataByQuery(TenantId tenantId, CustomerId customerId, - EntityDataQuery query, boolean ignorePermissionCheck) { + EntityDataQuery query, boolean ignorePermissionCheck) { long startNs = System.nanoTime(); var result = get(tenantId).findEntityDataByQuery(customerId, query, ignorePermissionCheck); double timingMs = (double) (System.nanoTime() - startNs) / 1000_000; - log.info("findEntityDataByQuery: {} ms", timingMs); + log.info("findEntityDataByQuery done in {} ms", timingMs); return result; } diff --git a/common/edqs/src/main/java/org/thingsboard/server/edqs/repo/EdqRepository.java b/common/edqs/src/main/java/org/thingsboard/server/edqs/repo/EdqsRepository.java similarity index 92% rename from common/edqs/src/main/java/org/thingsboard/server/edqs/repo/EdqRepository.java rename to common/edqs/src/main/java/org/thingsboard/server/edqs/repo/EdqsRepository.java index 231efd9a81..b1c72b1483 100644 --- a/common/edqs/src/main/java/org/thingsboard/server/edqs/repo/EdqRepository.java +++ b/common/edqs/src/main/java/org/thingsboard/server/edqs/repo/EdqsRepository.java @@ -25,13 +25,10 @@ import org.thingsboard.server.common.data.query.EntityDataQuery; import java.util.function.Predicate; -public interface EdqRepository { +public interface EdqsRepository { void processEvent(EdqsEvent event); - @Deprecated - default void addOrUpdate(TenantId tenantId, Object object) {} - long countEntitiesByQuery(TenantId tenantId, CustomerId customerId, EntityCountQuery query, boolean ignorePermissionCheck); PageData findEntityDataByQuery(TenantId tenantId, CustomerId customerId, EntityDataQuery query, boolean ignorePermissionCheck); diff --git a/common/edqs/src/main/java/org/thingsboard/server/edqs/repo/TenantRepo.java b/common/edqs/src/main/java/org/thingsboard/server/edqs/repo/TenantRepo.java index 9d6a4ab135..043c0bd6c3 100644 --- a/common/edqs/src/main/java/org/thingsboard/server/edqs/repo/TenantRepo.java +++ b/common/edqs/src/main/java/org/thingsboard/server/edqs/repo/TenantRepo.java @@ -57,6 +57,7 @@ import org.thingsboard.server.edqs.query.processor.EntityQueryProcessor; import org.thingsboard.server.edqs.query.processor.EntityQueryProcessorFactory; import org.thingsboard.server.edqs.stats.EdqsStatsService; import org.thingsboard.server.edqs.util.RepositoryUtils; +import org.thingsboard.server.edqs.util.TbStringPool; import java.util.ArrayList; import java.util.Collections; diff --git a/common/edqs/src/main/java/org/thingsboard/server/edqs/state/KafkaEdqsStateService.java b/common/edqs/src/main/java/org/thingsboard/server/edqs/state/KafkaEdqsStateService.java index f7382c9f15..f5ba2ba841 100644 --- a/common/edqs/src/main/java/org/thingsboard/server/edqs/state/KafkaEdqsStateService.java +++ b/common/edqs/src/main/java/org/thingsboard/server/edqs/state/KafkaEdqsStateService.java @@ -33,6 +33,7 @@ import org.thingsboard.server.queue.common.consumer.PartitionedQueueConsumerMana import org.thingsboard.server.queue.common.consumer.QueueConsumerManager; import org.thingsboard.server.queue.common.consumer.QueueStateService; import org.thingsboard.server.queue.discovery.QueueKey; +import org.thingsboard.server.queue.discovery.TopicService; import org.thingsboard.server.queue.edqs.EdqsConfig; import org.thingsboard.server.queue.edqs.EdqsQueue; import org.thingsboard.server.queue.edqs.EdqsQueueFactory; @@ -52,6 +53,7 @@ public class KafkaEdqsStateService implements EdqsStateService { private final EdqsPartitionService partitionService; private final EdqsQueueFactory queueFactory; private final EdqsProcessor edqsProcessor; + private final TopicService topicService; private PartitionedQueueConsumerManager> stateConsumer; private QueueStateService, TbProtoQueueMsg> queueStateService; @@ -75,7 +77,6 @@ public class KafkaEdqsStateService implements EdqsStateService { } try { ToEdqsMsg msg = queueMsg.getValue(); - log.trace("Processing message: {}", msg); edqsProcessor.process(msg, EdqsQueue.STATE); if (stateReadCount.incrementAndGet() % 100000 == 0) { log.info("[state] Processed {} msgs", stateReadCount.get()); @@ -140,6 +141,7 @@ public class KafkaEdqsStateService implements EdqsStateService { stateProducer = EdqsProducer.builder() .queue(EdqsQueue.STATE) .partitionService(partitionService) + .topicService(topicService) .producer(queueFactory.createEdqsMsgProducer(EdqsQueue.STATE)) .build(); } diff --git a/common/edqs/src/main/java/org/thingsboard/server/edqs/util/EdqsConverter.java b/common/edqs/src/main/java/org/thingsboard/server/edqs/util/EdqsConverter.java index b7419451b9..817b0df4f1 100644 --- a/common/edqs/src/main/java/org/thingsboard/server/edqs/util/EdqsConverter.java +++ b/common/edqs/src/main/java/org/thingsboard/server/edqs/util/EdqsConverter.java @@ -42,7 +42,6 @@ import org.thingsboard.server.edqs.data.dp.DoubleDataPoint; import org.thingsboard.server.edqs.data.dp.JsonDataPoint; import org.thingsboard.server.edqs.data.dp.LongDataPoint; import org.thingsboard.server.edqs.data.dp.StringDataPoint; -import org.thingsboard.server.edqs.repo.TbBytePool; import org.thingsboard.server.gen.transport.TransportProtos; import org.thingsboard.server.gen.transport.TransportProtos.DataPointProto; import org.xerial.snappy.Snappy; @@ -161,11 +160,11 @@ public class EdqsConverter { } else if (proto.hasStringV()) { return new StringDataPoint(ts, proto.getStringV()); } else if (proto.hasCompressedStringV()) { - return new CompressedStringDataPoint(ts, TbBytePool.intern(proto.getCompressedStringV().toByteArray())); + return new CompressedStringDataPoint(ts, proto.getCompressedStringV().toByteArray()); } else if (proto.hasJsonV()) { return new JsonDataPoint(ts, proto.getJsonV()); } else if (proto.hasCompressedJsonV()) { - return new CompressedJsonDataPoint(ts, TbBytePool.intern(proto.getCompressedJsonV().toByteArray())); + return new CompressedJsonDataPoint(ts, proto.getCompressedJsonV().toByteArray()); } else { throw new IllegalArgumentException("Unsupported data point proto: " + proto); } diff --git a/common/edqs/src/main/java/org/thingsboard/server/edqs/repo/TbBytePool.java b/common/edqs/src/main/java/org/thingsboard/server/edqs/util/TbBytePool.java similarity index 96% rename from common/edqs/src/main/java/org/thingsboard/server/edqs/repo/TbBytePool.java rename to common/edqs/src/main/java/org/thingsboard/server/edqs/util/TbBytePool.java index a284590161..a5a5394910 100644 --- a/common/edqs/src/main/java/org/thingsboard/server/edqs/repo/TbBytePool.java +++ b/common/edqs/src/main/java/org/thingsboard/server/edqs/util/TbBytePool.java @@ -13,7 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package org.thingsboard.server.edqs.repo; +package org.thingsboard.server.edqs.util; import com.google.common.hash.Hashing; import org.springframework.util.ConcurrentReferenceHashMap; diff --git a/common/edqs/src/main/java/org/thingsboard/server/edqs/repo/TbStringPool.java b/common/edqs/src/main/java/org/thingsboard/server/edqs/util/TbStringPool.java similarity index 96% rename from common/edqs/src/main/java/org/thingsboard/server/edqs/repo/TbStringPool.java rename to common/edqs/src/main/java/org/thingsboard/server/edqs/util/TbStringPool.java index ceb15affdf..151d6bb1b4 100644 --- a/common/edqs/src/main/java/org/thingsboard/server/edqs/repo/TbStringPool.java +++ b/common/edqs/src/main/java/org/thingsboard/server/edqs/util/TbStringPool.java @@ -13,7 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package org.thingsboard.server.edqs.repo; +package org.thingsboard.server.edqs.util; import org.springframework.util.ConcurrentReferenceHashMap; diff --git a/common/queue/src/main/java/org/thingsboard/server/queue/edqs/KafkaEdqsQueueFactory.java b/common/queue/src/main/java/org/thingsboard/server/queue/edqs/KafkaEdqsQueueFactory.java index 4b599670a1..9f973ce3f1 100644 --- a/common/queue/src/main/java/org/thingsboard/server/queue/edqs/KafkaEdqsQueueFactory.java +++ b/common/queue/src/main/java/org/thingsboard/server/queue/edqs/KafkaEdqsQueueFactory.java @@ -27,6 +27,7 @@ import org.thingsboard.server.queue.TbQueueResponseTemplate; import org.thingsboard.server.queue.common.DefaultTbQueueResponseTemplate; import org.thingsboard.server.queue.common.TbProtoQueueMsg; import org.thingsboard.server.queue.discovery.TbServiceInfoProvider; +import org.thingsboard.server.queue.discovery.TopicService; import org.thingsboard.server.queue.kafka.TbKafkaAdmin; import org.thingsboard.server.queue.kafka.TbKafkaConsumerStatsService; import org.thingsboard.server.queue.kafka.TbKafkaConsumerTemplate; @@ -47,12 +48,13 @@ public class KafkaEdqsQueueFactory implements EdqsQueueFactory { private final EdqsConfig edqsConfig; private final TbServiceInfoProvider serviceInfoProvider; private final TbKafkaConsumerStatsService consumerStatsService; + private final TopicService topicService; private final AtomicInteger consumerCounter = new AtomicInteger(); public KafkaEdqsQueueFactory(TbKafkaSettings kafkaSettings, TbKafkaTopicConfigs topicConfigs, EdqsConfig edqsConfig, TbServiceInfoProvider serviceInfoProvider, - TbKafkaConsumerStatsService consumerStatsService) { + TbKafkaConsumerStatsService consumerStatsService, TopicService topicService) { this.edqsEventsAdmin = new TbKafkaAdmin(kafkaSettings, topicConfigs.getEdqsEventsConfigs()); this.edqsRequestsAdmin = new TbKafkaAdmin(kafkaSettings, topicConfigs.getEdqsRequestsConfigs()); this.edqsStateAdmin = new TbKafkaAdmin(kafkaSettings, topicConfigs.getEdqsStateConfigs()); @@ -60,6 +62,7 @@ public class KafkaEdqsQueueFactory implements EdqsQueueFactory { this.edqsConfig = edqsConfig; this.serviceInfoProvider = serviceInfoProvider; this.consumerStatsService = consumerStatsService; + this.topicService = topicService; } @Override @@ -72,11 +75,11 @@ public class KafkaEdqsQueueFactory implements EdqsQueueFactory { public TbQueueConsumer> createEdqsMsgConsumer(EdqsQueue queue, String group) { return TbKafkaConsumerTemplate.>builder() .settings(kafkaSettings) - .topic(queue.getTopic()) + .topic(topicService.buildTopicName(queue.getTopic())) .readFromBeginning(queue.isReadFromBeginning()) .stopWhenRead(queue.isStopWhenRead()) .clientId("edqs-" + queue.name().toLowerCase() + "-" + consumerCounter.getAndIncrement() + "-consumer-" + serviceInfoProvider.getServiceId()) - .groupId(group) + .groupId(topicService.buildTopicName(group)) .decoder(msg -> new TbProtoQueueMsg<>(msg.getKey(), ToEdqsMsg.parseFrom(msg.getData()), msg.getHeaders())) .admin(queue == EdqsQueue.STATE ? edqsStateAdmin : edqsEventsAdmin) .statsService(consumerStatsService) @@ -97,16 +100,16 @@ public class KafkaEdqsQueueFactory implements EdqsQueueFactory { String requestsConsumerGroup = "edqs-requests-consumer-group-" + edqsConfig.getLabel(); var requestConsumer = TbKafkaConsumerTemplate.>builder() .settings(kafkaSettings) - .topic(edqsConfig.getRequestsTopic()) + .topic(topicService.buildTopicName(edqsConfig.getRequestsTopic())) .clientId("edqs-requests-consumer-" + serviceInfoProvider.getServiceId()) - .groupId(requestsConsumerGroup) + .groupId(topicService.buildTopicName(requestsConsumerGroup)) .decoder(msg -> new TbProtoQueueMsg<>(msg.getKey(), TransportProtos.ToEdqsMsg.parseFrom(msg.getData()), msg.getHeaders())) .admin(edqsRequestsAdmin) .statsService(consumerStatsService); var responseProducer = TbKafkaProducerTemplate.>builder() .settings(kafkaSettings) .clientId("edqs-response-producer-" + serviceInfoProvider.getServiceId()) - .defaultTopic(edqsConfig.getResponsesTopic()) + .defaultTopic(topicService.buildTopicName(edqsConfig.getResponsesTopic())) .admin(edqsRequestsAdmin); return DefaultTbQueueResponseTemplate., TbProtoQueueMsg>builder() .requestTemplate(requestConsumer.build()) diff --git a/common/queue/src/main/java/org/thingsboard/server/queue/provider/KafkaMonolithQueueFactory.java b/common/queue/src/main/java/org/thingsboard/server/queue/provider/KafkaMonolithQueueFactory.java index e5b981bf5d..4acb5d94ae 100644 --- a/common/queue/src/main/java/org/thingsboard/server/queue/provider/KafkaMonolithQueueFactory.java +++ b/common/queue/src/main/java/org/thingsboard/server/queue/provider/KafkaMonolithQueueFactory.java @@ -522,7 +522,7 @@ public class KafkaMonolithQueueFactory implements TbCoreQueueFactory, TbRuleEngi var responseConsumer = TbKafkaConsumerTemplate.>builder() .settings(kafkaSettings) .topic(topicService.buildTopicName(edqsConfig.getResponsesTopic() + "." + serviceInfoProvider.getServiceId())) - .clientId(topicService.buildTopicName("monolith-edqs-response-consumer-" + serviceInfoProvider.getServiceId())) + .clientId("monolith-edqs-response-consumer-" + serviceInfoProvider.getServiceId()) .groupId(topicService.buildTopicName("monolith-edqs-response-consumer-" + serviceInfoProvider.getServiceId())) .decoder(msg -> new TbProtoQueueMsg<>(msg.getKey(), FromEdqsMsg.parseFrom(msg.getData()), msg.getHeaders())) .admin(edqsRequestsAdmin) diff --git a/common/queue/src/main/java/org/thingsboard/server/queue/provider/KafkaTbCoreQueueFactory.java b/common/queue/src/main/java/org/thingsboard/server/queue/provider/KafkaTbCoreQueueFactory.java index 4e12696d3e..646cdc676a 100644 --- a/common/queue/src/main/java/org/thingsboard/server/queue/provider/KafkaTbCoreQueueFactory.java +++ b/common/queue/src/main/java/org/thingsboard/server/queue/provider/KafkaTbCoreQueueFactory.java @@ -471,7 +471,7 @@ public class KafkaTbCoreQueueFactory implements TbCoreQueueFactory { var responseConsumer = TbKafkaConsumerTemplate.>builder() .settings(kafkaSettings) .topic(topicService.buildTopicName(edqsConfig.getResponsesTopic() + "." + serviceInfoProvider.getServiceId())) - .clientId(topicService.buildTopicName("tb-core-edqs-response-consumer-" + serviceInfoProvider.getServiceId())) + .clientId("tb-core-edqs-response-consumer-" + serviceInfoProvider.getServiceId()) .groupId(topicService.buildTopicName("tb-core-edqs-response-consumer-" + serviceInfoProvider.getServiceId())) .decoder(msg -> new TbProtoQueueMsg<>(msg.getKey(), FromEdqsMsg.parseFrom(msg.getData()), msg.getHeaders())) .admin(edqsRequestsAdmin) diff --git a/common/util/src/main/java/org/thingsboard/common/util/ExceptionUtil.java b/common/util/src/main/java/org/thingsboard/common/util/ExceptionUtil.java index ace7d1540b..21b20fec7b 100644 --- a/common/util/src/main/java/org/thingsboard/common/util/ExceptionUtil.java +++ b/common/util/src/main/java/org/thingsboard/common/util/ExceptionUtil.java @@ -64,4 +64,14 @@ public class ExceptionUtil { } } } + + public static String getMessage(Throwable t) { + String message = t.getMessage(); + if (StringUtils.isNotEmpty(message)) { + return message; + } else { + return t.getClass().getSimpleName(); + } + } + } diff --git a/dao/src/main/java/org/thingsboard/server/dao/TenantEntityDao.java b/dao/src/main/java/org/thingsboard/server/dao/TenantEntityDao.java index 19fe45ed88..f6391fd26f 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/TenantEntityDao.java +++ b/dao/src/main/java/org/thingsboard/server/dao/TenantEntityDao.java @@ -15,7 +15,6 @@ */ package org.thingsboard.server.dao; -import org.thingsboard.server.common.data.ObjectType; import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.common.data.page.PageData; import org.thingsboard.server.common.data.page.PageLink; @@ -30,8 +29,4 @@ public interface TenantEntityDao { throw new UnsupportedOperationException(); } - default ObjectType getType() { - throw new UnsupportedOperationException(); - } - } diff --git a/dao/src/main/java/org/thingsboard/server/dao/entity/EntityDaoRegistry.java b/dao/src/main/java/org/thingsboard/server/dao/entity/EntityDaoRegistry.java index 24f8aa4d77..86887e94ec 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/entity/EntityDaoRegistry.java +++ b/dao/src/main/java/org/thingsboard/server/dao/entity/EntityDaoRegistry.java @@ -18,9 +18,7 @@ package org.thingsboard.server.dao.entity; import lombok.extern.slf4j.Slf4j; import org.springframework.stereotype.Service; import org.thingsboard.server.common.data.EntityType; -import org.thingsboard.server.common.data.ObjectType; import org.thingsboard.server.dao.Dao; -import org.thingsboard.server.dao.TenantEntityDao; import java.util.EnumMap; import java.util.List; @@ -31,20 +29,14 @@ import java.util.Map; @SuppressWarnings({"unchecked"}) public class EntityDaoRegistry { - private final Map> tenantEntityDaos = new EnumMap<>(ObjectType.class); private final Map> entityDaos = new EnumMap<>(EntityType.class); - private EntityDaoRegistry(List> entityDaos, List> tenantEntityDaos) { + private EntityDaoRegistry(List> entityDaos) { entityDaos.forEach(dao -> { if (dao.getEntityType() != null) { this.entityDaos.put(dao.getEntityType(), dao); } }); - tenantEntityDaos.forEach(dao -> { - if (dao.getType() != null) { - this.tenantEntityDaos.put(dao.getType(), dao); - } - }); } public Dao getDao(EntityType entityType) { @@ -55,12 +47,4 @@ public class EntityDaoRegistry { return dao; } - public TenantEntityDao getTenantEntityDao(ObjectType objectType) { - TenantEntityDao dao = tenantEntityDaos.get(objectType); - if (dao == null) { - throw new IllegalArgumentException("Missing tenant entity dao for entity type " + objectType); - } - return (TenantEntityDao) dao; - } - } diff --git a/dao/src/main/java/org/thingsboard/server/dao/model/sql/AlarmTypeEntity.java b/dao/src/main/java/org/thingsboard/server/dao/model/sql/AlarmTypeEntity.java deleted file mode 100644 index 729d7a44f0..0000000000 --- a/dao/src/main/java/org/thingsboard/server/dao/model/sql/AlarmTypeEntity.java +++ /dev/null @@ -1,65 +0,0 @@ -/** - * Copyright © 2016-2024 The Thingsboard Authors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package org.thingsboard.server.dao.model.sql; - -import jakarta.persistence.Column; -import jakarta.persistence.Entity; -import jakarta.persistence.Id; -import jakarta.persistence.IdClass; -import jakarta.persistence.Table; -import lombok.Data; -import org.thingsboard.server.common.data.alarm.AlarmType; -import org.thingsboard.server.common.data.id.TenantId; -import org.thingsboard.server.dao.model.ModelConstants; -import org.thingsboard.server.dao.model.ToData; - -import java.util.UUID; - -@Data -@Entity -@Table(name = ModelConstants.ALARM_TYPES_TABLE_NAME) -@IdClass(AlarmTypeCompositeKey.class) -public class AlarmTypeEntity implements ToData { - - @Id - @Column(name = ModelConstants.TENANT_ID_PROPERTY, nullable = false) - private UUID tenantId; - - @Id - @Column(name = ModelConstants.ALARM_TYPE_PROPERTY, nullable = false) - private String type; - - public AlarmTypeEntity() {} - - public AlarmTypeEntity(AlarmType alarmType) { - setTenantId(alarmType.getTenantId().getId()); - setType(alarmType.getType()); - } - - public AlarmTypeEntity(UUID tenantId, String type) { - this.tenantId = tenantId; - this.type = type; - } - - @Override - public AlarmType toData() { - AlarmType alarmType = new AlarmType(); - alarmType.setTenantId(TenantId.fromUUID(tenantId)); - alarmType.setType(type); - return alarmType; - } - -} diff --git a/dao/src/main/java/org/thingsboard/server/dao/sql/alarm/AlarmTypeRepository.java b/dao/src/main/java/org/thingsboard/server/dao/sql/alarm/AlarmTypeRepository.java deleted file mode 100644 index b84542e3c3..0000000000 --- a/dao/src/main/java/org/thingsboard/server/dao/sql/alarm/AlarmTypeRepository.java +++ /dev/null @@ -1,30 +0,0 @@ -/** - * Copyright © 2016-2024 The Thingsboard Authors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package org.thingsboard.server.dao.sql.alarm; - -import org.springframework.data.domain.Page; -import org.springframework.data.domain.Pageable; -import org.springframework.data.jpa.repository.JpaRepository; -import org.thingsboard.server.dao.model.sql.AlarmTypeCompositeKey; -import org.thingsboard.server.dao.model.sql.AlarmTypeEntity; - -import java.util.UUID; - -public interface AlarmTypeRepository extends JpaRepository { - - Page findByTenantId(UUID tenantId, Pageable pageable); - -} diff --git a/dao/src/main/java/org/thingsboard/server/dao/sql/alarm/JpaAlarmCommentDao.java b/dao/src/main/java/org/thingsboard/server/dao/sql/alarm/JpaAlarmCommentDao.java index 9d74b9bf8e..f6d85fc762 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/sql/alarm/JpaAlarmCommentDao.java +++ b/dao/src/main/java/org/thingsboard/server/dao/sql/alarm/JpaAlarmCommentDao.java @@ -22,7 +22,6 @@ import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Value; import org.springframework.data.jpa.repository.JpaRepository; import org.springframework.stereotype.Component; -import org.thingsboard.server.common.data.ObjectType; import org.thingsboard.server.common.data.alarm.AlarmComment; import org.thingsboard.server.common.data.alarm.AlarmCommentInfo; import org.thingsboard.server.common.data.id.AlarmId; @@ -93,9 +92,4 @@ public class JpaAlarmCommentDao extends JpaPartitionedAbstractDao implements A return EntityType.ALARM; } - @Override - public ObjectType getType() { - return ObjectType.ALARM; - } - } diff --git a/dao/src/main/java/org/thingsboard/server/dao/sql/alarm/JpaAlarmTypeDao.java b/dao/src/main/java/org/thingsboard/server/dao/sql/alarm/JpaAlarmTypeDao.java deleted file mode 100644 index 532f06eb03..0000000000 --- a/dao/src/main/java/org/thingsboard/server/dao/sql/alarm/JpaAlarmTypeDao.java +++ /dev/null @@ -1,46 +0,0 @@ -/** - * Copyright © 2016-2024 The Thingsboard Authors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package org.thingsboard.server.dao.sql.alarm; - -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.stereotype.Component; -import org.thingsboard.server.common.data.ObjectType; -import org.thingsboard.server.common.data.alarm.AlarmType; -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.dao.DaoUtil; -import org.thingsboard.server.dao.TenantEntityDao; -import org.thingsboard.server.dao.util.SqlDao; - -@Component -@SqlDao -public class JpaAlarmTypeDao implements TenantEntityDao { - - @Autowired - private AlarmTypeRepository alarmTypeRepository; - - @Override - public PageData findAllByTenantId(TenantId tenantId, PageLink pageLink) { - return DaoUtil.toPageData(alarmTypeRepository.findByTenantId(tenantId.getId(), DaoUtil.toPageable(pageLink, "tenantId", "type"))); - } - - @Override - public ObjectType getType() { - return ObjectType.ALARM_TYPE; - } - -} diff --git a/dao/src/main/java/org/thingsboard/server/dao/sql/alarm/JpaEntityAlarmDao.java b/dao/src/main/java/org/thingsboard/server/dao/sql/alarm/JpaEntityAlarmDao.java index 6bbc59559a..ebffd50656 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/sql/alarm/JpaEntityAlarmDao.java +++ b/dao/src/main/java/org/thingsboard/server/dao/sql/alarm/JpaEntityAlarmDao.java @@ -17,7 +17,6 @@ package org.thingsboard.server.dao.sql.alarm; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Component; -import org.thingsboard.server.common.data.ObjectType; import org.thingsboard.server.common.data.alarm.EntityAlarm; import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.common.data.page.PageData; @@ -38,9 +37,4 @@ public class JpaEntityAlarmDao implements TenantEntityDao { return DaoUtil.toPageData(entityAlarmRepository.findByTenantId(tenantId.getId(), DaoUtil.toPageable(pageLink, "entityId", "alarmId"))); } - @Override - public ObjectType getType() { - return ObjectType.ENTITY_ALARM; - } - } diff --git a/dao/src/main/java/org/thingsboard/server/dao/sql/asset/JpaAssetDao.java b/dao/src/main/java/org/thingsboard/server/dao/sql/asset/JpaAssetDao.java index 176eb0856a..721f46901a 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/sql/asset/JpaAssetDao.java +++ b/dao/src/main/java/org/thingsboard/server/dao/sql/asset/JpaAssetDao.java @@ -23,7 +23,6 @@ import org.springframework.data.jpa.repository.JpaRepository; import org.springframework.stereotype.Component; import org.thingsboard.server.common.data.EntitySubtype; import org.thingsboard.server.common.data.EntityType; -import org.thingsboard.server.common.data.ObjectType; import org.thingsboard.server.common.data.asset.Asset; import org.thingsboard.server.common.data.asset.AssetInfo; import org.thingsboard.server.common.data.edqs.fields.AssetFields; @@ -285,9 +284,4 @@ public class JpaAssetDao extends JpaAbstractDao implements A return EntityType.ASSET; } - @Override - public ObjectType getType() { - return ObjectType.ASSET; - } - } diff --git a/dao/src/main/java/org/thingsboard/server/dao/sql/asset/JpaAssetProfileDao.java b/dao/src/main/java/org/thingsboard/server/dao/sql/asset/JpaAssetProfileDao.java index d43871823e..55f72d9682 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/sql/asset/JpaAssetProfileDao.java +++ b/dao/src/main/java/org/thingsboard/server/dao/sql/asset/JpaAssetProfileDao.java @@ -22,7 +22,6 @@ import org.springframework.data.jpa.repository.JpaRepository; import org.springframework.stereotype.Component; import org.thingsboard.server.common.data.EntityInfo; import org.thingsboard.server.common.data.EntityType; -import org.thingsboard.server.common.data.ObjectType; import org.thingsboard.server.common.data.asset.AssetProfile; import org.thingsboard.server.common.data.asset.AssetProfileInfo; import org.thingsboard.server.common.data.edqs.fields.AssetProfileFields; @@ -157,9 +156,4 @@ public class JpaAssetProfileDao extends JpaAbstractDao imp return EntityType.CUSTOMER; } - @Override - public ObjectType getType() { - return ObjectType.CUSTOMER; - } - } diff --git a/dao/src/main/java/org/thingsboard/server/dao/sql/dashboard/JpaDashboardDao.java b/dao/src/main/java/org/thingsboard/server/dao/sql/dashboard/JpaDashboardDao.java index 9422f38aae..1431837864 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/sql/dashboard/JpaDashboardDao.java +++ b/dao/src/main/java/org/thingsboard/server/dao/sql/dashboard/JpaDashboardDao.java @@ -21,7 +21,6 @@ import org.springframework.data.jpa.repository.JpaRepository; import org.springframework.stereotype.Component; import org.thingsboard.server.common.data.Dashboard; import org.thingsboard.server.common.data.EntityType; -import org.thingsboard.server.common.data.ObjectType; import org.thingsboard.server.common.data.edqs.fields.DashboardFields; import org.thingsboard.server.common.data.id.DashboardId; import org.thingsboard.server.common.data.id.TenantId; @@ -108,9 +107,4 @@ public class JpaDashboardDao extends JpaAbstractDao return EntityType.DASHBOARD; } - @Override - public ObjectType getType() { - return ObjectType.DASHBOARD; - } - } diff --git a/dao/src/main/java/org/thingsboard/server/dao/sql/device/DeviceProfileRepository.java b/dao/src/main/java/org/thingsboard/server/dao/sql/device/DeviceProfileRepository.java index 18f7f35710..4b87f0274d 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/sql/device/DeviceProfileRepository.java +++ b/dao/src/main/java/org/thingsboard/server/dao/sql/device/DeviceProfileRepository.java @@ -25,7 +25,6 @@ import org.thingsboard.server.common.data.DeviceProfileInfo; import org.thingsboard.server.common.data.DeviceTransportType; import org.thingsboard.server.common.data.EntityInfo; import org.thingsboard.server.common.data.edqs.fields.DeviceProfileFields; -import org.thingsboard.server.common.data.edqs.fields.GenericFields; import org.thingsboard.server.dao.ExportableEntityRepository; import org.thingsboard.server.dao.model.sql.DeviceProfileEntity; diff --git a/dao/src/main/java/org/thingsboard/server/dao/sql/device/JpaDeviceCredentialsDao.java b/dao/src/main/java/org/thingsboard/server/dao/sql/device/JpaDeviceCredentialsDao.java index 31d2aa6cb9..25b996eb57 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/sql/device/JpaDeviceCredentialsDao.java +++ b/dao/src/main/java/org/thingsboard/server/dao/sql/device/JpaDeviceCredentialsDao.java @@ -19,7 +19,6 @@ import lombok.extern.slf4j.Slf4j; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.data.jpa.repository.JpaRepository; import org.springframework.stereotype.Component; -import org.thingsboard.server.common.data.ObjectType; import org.thingsboard.server.common.data.id.DeviceId; import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.common.data.page.PageData; @@ -76,9 +75,4 @@ public class JpaDeviceCredentialsDao extends JpaAbstractDao implement return EntityType.DEVICE; } - @Override - public ObjectType getType() { - return ObjectType.DEVICE; - } - } diff --git a/dao/src/main/java/org/thingsboard/server/dao/sql/device/JpaDeviceProfileDao.java b/dao/src/main/java/org/thingsboard/server/dao/sql/device/JpaDeviceProfileDao.java index c4d48dff35..8f7dcfb916 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/sql/device/JpaDeviceProfileDao.java +++ b/dao/src/main/java/org/thingsboard/server/dao/sql/device/JpaDeviceProfileDao.java @@ -25,7 +25,6 @@ import org.thingsboard.server.common.data.DeviceProfileInfo; import org.thingsboard.server.common.data.DeviceTransportType; import org.thingsboard.server.common.data.EntityInfo; import org.thingsboard.server.common.data.EntityType; -import org.thingsboard.server.common.data.ObjectType; import org.thingsboard.server.common.data.StringUtils; import org.thingsboard.server.common.data.edqs.fields.DeviceProfileFields; import org.thingsboard.server.common.data.id.DeviceProfileId; @@ -175,9 +174,4 @@ public class JpaDeviceProfileDao extends JpaAbstractDao implements Edge return EntityType.EDGE; } - @Override - public ObjectType getType() { - return ObjectType.EDGE; - } - } diff --git a/dao/src/main/java/org/thingsboard/server/dao/sql/entityview/EntityViewRepository.java b/dao/src/main/java/org/thingsboard/server/dao/sql/entityview/EntityViewRepository.java index b18fd58248..3cc212497b 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/sql/entityview/EntityViewRepository.java +++ b/dao/src/main/java/org/thingsboard/server/dao/sql/entityview/EntityViewRepository.java @@ -22,7 +22,6 @@ import org.springframework.data.jpa.repository.JpaRepository; import org.springframework.data.jpa.repository.Query; import org.springframework.data.repository.query.Param; import org.thingsboard.server.common.data.edqs.fields.EntityViewFields; -import org.thingsboard.server.common.data.edqs.fields.GenericFields; import org.thingsboard.server.dao.ExportableEntityRepository; import org.thingsboard.server.dao.model.sql.EntityViewEntity; import org.thingsboard.server.dao.model.sql.EntityViewInfoEntity; diff --git a/dao/src/main/java/org/thingsboard/server/dao/sql/entityview/JpaEntityViewDao.java b/dao/src/main/java/org/thingsboard/server/dao/sql/entityview/JpaEntityViewDao.java index 4c30e26f05..4172af056f 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/sql/entityview/JpaEntityViewDao.java +++ b/dao/src/main/java/org/thingsboard/server/dao/sql/entityview/JpaEntityViewDao.java @@ -25,7 +25,6 @@ import org.thingsboard.server.common.data.EntitySubtype; import org.thingsboard.server.common.data.EntityType; import org.thingsboard.server.common.data.EntityView; import org.thingsboard.server.common.data.EntityViewInfo; -import org.thingsboard.server.common.data.ObjectType; import org.thingsboard.server.common.data.edqs.fields.EntityViewFields; import org.thingsboard.server.common.data.id.EntityViewId; import org.thingsboard.server.common.data.id.TenantId; @@ -236,9 +235,4 @@ public class JpaEntityViewDao extends JpaAbstractDao implements Q return EntityType.QUEUE; } - @Override - public ObjectType getType() { - return ObjectType.QUEUE; - } - } diff --git a/dao/src/main/java/org/thingsboard/server/dao/sql/queue/JpaQueueStatsDao.java b/dao/src/main/java/org/thingsboard/server/dao/sql/queue/JpaQueueStatsDao.java index 41e2de02a6..3e8f2c11ce 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/sql/queue/JpaQueueStatsDao.java +++ b/dao/src/main/java/org/thingsboard/server/dao/sql/queue/JpaQueueStatsDao.java @@ -21,7 +21,6 @@ import org.springframework.data.domain.Limit; import org.springframework.data.jpa.repository.JpaRepository; import org.springframework.stereotype.Component; import org.thingsboard.server.common.data.EntityType; -import org.thingsboard.server.common.data.ObjectType; import org.thingsboard.server.common.data.edqs.fields.QueueStatsFields; import org.thingsboard.server.common.data.id.QueueStatsId; import org.thingsboard.server.common.data.id.TenantId; @@ -67,11 +66,6 @@ public class JpaQueueStatsDao extends JpaAbstractDao implements RpcDao, return EntityType.RPC; } - @Override - public ObjectType getType() { - return ObjectType.RPC; - } - } diff --git a/dao/src/main/java/org/thingsboard/server/dao/sql/rule/JpaRuleChainDao.java b/dao/src/main/java/org/thingsboard/server/dao/sql/rule/JpaRuleChainDao.java index ad0d596474..d5cd046e98 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/sql/rule/JpaRuleChainDao.java +++ b/dao/src/main/java/org/thingsboard/server/dao/sql/rule/JpaRuleChainDao.java @@ -21,7 +21,6 @@ import org.springframework.data.domain.Limit; import org.springframework.data.jpa.repository.JpaRepository; import org.springframework.stereotype.Component; import org.thingsboard.server.common.data.EntityType; -import org.thingsboard.server.common.data.ObjectType; import org.thingsboard.server.common.data.edqs.fields.RuleChainFields; import org.thingsboard.server.common.data.id.RuleChainId; import org.thingsboard.server.common.data.id.TenantId; @@ -152,9 +151,4 @@ public class JpaRuleChainDao extends JpaAbstractDao return EntityType.RULE_CHAIN; } - @Override - public ObjectType getType() { - return ObjectType.RULE_CHAIN; - } - } diff --git a/dao/src/main/java/org/thingsboard/server/dao/sql/rule/JpaRuleNodeDao.java b/dao/src/main/java/org/thingsboard/server/dao/sql/rule/JpaRuleNodeDao.java index 307f15c7f4..43087b8652 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/sql/rule/JpaRuleNodeDao.java +++ b/dao/src/main/java/org/thingsboard/server/dao/sql/rule/JpaRuleNodeDao.java @@ -20,7 +20,6 @@ import org.springframework.beans.factory.annotation.Autowired; import org.springframework.data.jpa.repository.JpaRepository; import org.springframework.stereotype.Component; import org.thingsboard.server.common.data.EntityType; -import org.thingsboard.server.common.data.ObjectType; import org.thingsboard.server.common.data.id.RuleChainId; import org.thingsboard.server.common.data.id.RuleNodeId; import org.thingsboard.server.common.data.id.TenantId; @@ -118,9 +117,4 @@ public class JpaRuleNodeDao extends JpaAbstractDao imp return EntityType.RULE_NODE; } - @Override - public ObjectType getType() { - return ObjectType.RULE_NODE; - } - } diff --git a/dao/src/main/java/org/thingsboard/server/dao/sql/settings/JpaAdminSettingsDao.java b/dao/src/main/java/org/thingsboard/server/dao/sql/settings/JpaAdminSettingsDao.java index 9e0f6a70cb..6a4ae62b48 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/sql/settings/JpaAdminSettingsDao.java +++ b/dao/src/main/java/org/thingsboard/server/dao/sql/settings/JpaAdminSettingsDao.java @@ -21,7 +21,6 @@ import org.springframework.data.jpa.repository.JpaRepository; import org.springframework.stereotype.Component; import org.springframework.transaction.annotation.Transactional; import org.thingsboard.server.common.data.AdminSettings; -import org.thingsboard.server.common.data.ObjectType; import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.common.data.page.PageData; import org.thingsboard.server.common.data.page.PageLink; @@ -78,9 +77,4 @@ public class JpaAdminSettingsDao extends JpaAbstractDao implements User return EntityType.USER; } - @Override - public ObjectType getType() { - return ObjectType.USER; - } - } diff --git a/dao/src/main/java/org/thingsboard/server/dao/sql/user/JpaUserSettingsDao.java b/dao/src/main/java/org/thingsboard/server/dao/sql/user/JpaUserSettingsDao.java index b41bfa7728..34f496e0a7 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/sql/user/JpaUserSettingsDao.java +++ b/dao/src/main/java/org/thingsboard/server/dao/sql/user/JpaUserSettingsDao.java @@ -18,7 +18,6 @@ package org.thingsboard.server.dao.sql.user; import lombok.extern.slf4j.Slf4j; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Component; -import org.thingsboard.server.common.data.ObjectType; import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.common.data.id.UserId; import org.thingsboard.server.common.data.page.PageData; @@ -74,9 +73,4 @@ public class JpaUserSettingsDao implements UserSettingsDao, TenantEntityDao ignored = EnumSet.of(TENANT, TENANT_PROFILE, RELATION, EVENT, ATTRIBUTE_KV, LATEST_TS_KV, AUDIT_LOG, - OAUTH2_CLIENT, OAUTH2_DOMAIN, OAUTH2_MOBILE); - for (ObjectType type : ObjectType.values()) { - if (ignored.contains(type)) { - continue; - } - - TenantEntityDao dao = assertDoesNotThrow(() -> entityDaoRegistry.getTenantEntityDao(type)); - assertDoesNotThrow(() -> { - dao.findAllByTenantId(tenantId, new PageLink(100)); - }); - } - } - } diff --git a/edqs/src/main/resources/logback.xml b/edqs/src/main/resources/logback.xml index 8680457ab1..d96cb1a7d3 100644 --- a/edqs/src/main/resources/logback.xml +++ b/edqs/src/main/resources/logback.xml @@ -28,7 +28,6 @@ - diff --git a/edqs/src/test/java/org/thingsboard/server/edqs/repo/AbstractEDQTest.java b/edqs/src/test/java/org/thingsboard/server/edqs/repo/AbstractEDQTest.java index 1f22f9d5ae..6f38e7b6d6 100644 --- a/edqs/src/test/java/org/thingsboard/server/edqs/repo/AbstractEDQTest.java +++ b/edqs/src/test/java/org/thingsboard/server/edqs/repo/AbstractEDQTest.java @@ -69,14 +69,14 @@ import java.util.UUID; @Configuration @ComponentScan({"org.thingsboard.server.edqs.repo", "org.thingsboard.server.edqs.util"}) @EntityScan("org.thingsboard.server.edqs") -@TestPropertySource(locations = {"classpath:edq-test.properties"}) +@TestPropertySource(locations = {"classpath:edqs-test.properties"}) @TestExecutionListeners({ DependencyInjectionTestExecutionListener.class, DirtiesContextTestExecutionListener.class}) public abstract class AbstractEDQTest { @Autowired - protected InMemoryEdqRepository repository; + protected DefaultEdqsRepository repository; @Autowired protected EdqsConverter edqsConverter; diff --git a/edqs/src/test/resources/edq-test.properties b/edqs/src/test/resources/edqs-test.properties similarity index 100% rename from edqs/src/test/resources/edq-test.properties rename to edqs/src/test/resources/edqs-test.properties From 965210f17b58b0ca42f698cf58178c247f7cca44 Mon Sep 17 00:00:00 2001 From: ViacheslavKlimov Date: Fri, 21 Feb 2025 14:39:26 +0200 Subject: [PATCH 081/345] Multiple improvements for EDQS --- .../src/main/resources/thingsboard.yml | 10 +- .../EdqsEntityQueryControllerTest.java | 8 +- .../server/edqs/processor/EdqsProcessor.java | 5 +- .../server/edqs/repo/TenantRepo.java | 2 +- .../server/edqs/state/EdqsStateService.java | 2 + .../edqs/state/KafkaEdqsStateService.java | 23 ++- .../edqs/state/LocalEdqsStateService.java | 5 + .../server/edqs/stats/EdqsStatsService.java | 7 +- .../common/msg/queue/TopicPartitionInfo.java | 11 +- .../consumer/MainQueueConsumerManager.java | 11 +- .../PartitionedQueueConsumerManager.java | 4 +- .../common/consumer/QueueStateService.java | 17 ++ .../queue/discovery/HashPartitionService.java | 2 +- .../server/queue/edqs/EdqsComponent.java | 1 - .../queue/kafka/TbKafkaConsumerTemplate.java | 4 +- .../common/stats/DefaultStatsFactory.java | 2 +- .../server/dao/entity/BaseEntityService.java | 4 +- .../dao/sql/query/DummyEdqsService.java | 4 +- .../server/edqs/EdqsController.java | 41 +++++ .../edqs/ThingsboardEdqsApplication.java | 67 +------- edqs/src/main/resources/edqs.yml | 147 ++---------------- edqs/src/main/resources/logback.xml | 1 + 22 files changed, 139 insertions(+), 239 deletions(-) create mode 100644 edqs/src/main/java/org/thingsboard/server/edqs/EdqsController.java diff --git a/application/src/main/resources/thingsboard.yml b/application/src/main/resources/thingsboard.yml index 21459814b7..46ab8f25de 100644 --- a/application/src/main/resources/thingsboard.yml +++ b/application/src/main/resources/thingsboard.yml @@ -1696,14 +1696,14 @@ queue: print-interval-ms: "${TB_HOUSEKEEPER_STATS_PRINT_INTERVAL_MS:60000}" edqs: sync: - # Enable/disable EDQS synchronization FIXME: disable by default before release - enabled: "${TB_EDQS_SYNC_ENABLED:true}" + # Enable/disable EDQS synchronization + enabled: "${TB_EDQS_SYNC_ENABLED:false}" # Batch size of entities being synced with EDQS entity_batch_size: "${TB_EDQS_SYNC_ENTITY_BATCH_SIZE:10000}" # Batch size of timeseries data being synced with EDQS ts_batch_size: "${TB_EDQS_SYNC_TS_BATCH_SIZE:10000}" - # Whether to forward entity data query requests to EDQS (otherwise use PostgreSQL implementation) FIXME: disable by default before release - api_enabled: "${TB_EDQS_API_ENABLED:true}" + # Whether to forward entity data query requests to EDQS (otherwise use PostgreSQL implementation) + api_enabled: "${TB_EDQS_API_ENABLED:false}" # Mode of EDQS: local (for monolith) or remote (with separate EDQS microservices) mode: "${TB_EDQS_MODE:local}" local: @@ -1727,7 +1727,7 @@ queue: # Enable/disable statistics for EDQS enabled: "${TB_EDQS_STATS_ENABLED:true}" # Statistics printing interval for EDQS - print-interval-ms: "${TB_EDQS_STATS_PRINT_INTERVAL_MS:60000}" + print-interval-ms: "${TB_EDQS_STATS_PRINT_INTERVAL_MS:300000}" vc: # Default topic name diff --git a/application/src/test/java/org/thingsboard/server/controller/EdqsEntityQueryControllerTest.java b/application/src/test/java/org/thingsboard/server/controller/EdqsEntityQueryControllerTest.java index efc290d3c5..e4948330a5 100644 --- a/application/src/test/java/org/thingsboard/server/controller/EdqsEntityQueryControllerTest.java +++ b/application/src/test/java/org/thingsboard/server/controller/EdqsEntityQueryControllerTest.java @@ -25,6 +25,7 @@ import org.thingsboard.server.common.data.query.EntityData; import org.thingsboard.server.common.data.query.EntityDataQuery; import org.thingsboard.server.common.msg.edqs.EdqsService; import org.thingsboard.server.dao.service.DaoSqlTest; +import org.thingsboard.server.edqs.state.EdqsStateService; import org.thingsboard.server.edqs.util.EdqsRocksDb; import java.util.concurrent.TimeUnit; @@ -34,7 +35,7 @@ import static org.awaitility.Awaitility.await; @DaoSqlTest @TestPropertySource(properties = { // "queue.type=kafka", // uncomment to use Kafka -// "queue.kafka.bootstrap.servers=192.168.0.105:9092", +// "queue.kafka.bootstrap.servers=10.7.1.254:9092", "queue.edqs.sync.enabled=true", "queue.edqs.api_enabled=true", "queue.edqs.mode=local" @@ -44,12 +45,15 @@ public class EdqsEntityQueryControllerTest extends EntityQueryControllerTest { @Autowired private EdqsService edqsService; + @Autowired(required = false) + private EdqsStateService edqsStateService; + @MockBean // so that we don't do backup for tests private EdqsRocksDb edqsRocksDb; @Before public void before() { - await().atMost(TIMEOUT, TimeUnit.SECONDS).until(() -> edqsService.isApiEnabled()); + await().atMost(TIMEOUT, TimeUnit.SECONDS).until(() -> edqsService.isApiEnabled() && edqsStateService.isReady()); } @Override diff --git a/common/edqs/src/main/java/org/thingsboard/server/edqs/processor/EdqsProcessor.java b/common/edqs/src/main/java/org/thingsboard/server/edqs/processor/EdqsProcessor.java index 42e00be7fc..9349946a80 100644 --- a/common/edqs/src/main/java/org/thingsboard/server/edqs/processor/EdqsProcessor.java +++ b/common/edqs/src/main/java/org/thingsboard/server/edqs/processor/EdqsProcessor.java @@ -24,9 +24,7 @@ import jakarta.annotation.PreDestroy; import lombok.Getter; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; -import org.springframework.beans.factory.annotation.Autowired; import org.springframework.context.ConfigurableApplicationContext; -import org.springframework.context.annotation.Lazy; import org.springframework.context.event.EventListener; import org.springframework.stereotype.Service; import org.thingsboard.common.util.ExceptionUtil; @@ -90,8 +88,7 @@ public class EdqsProcessor implements TbQueueHandler, private final EdqsConfig config; private final EdqsPartitionService partitionService; private final ConfigurableApplicationContext applicationContext; - @Autowired @Lazy - private EdqsStateService stateService; + private final EdqsStateService stateService; private PartitionedQueueConsumerManager> eventConsumer; private TbQueueResponseTemplate, TbProtoQueueMsg> responseTemplate; diff --git a/common/edqs/src/main/java/org/thingsboard/server/edqs/repo/TenantRepo.java b/common/edqs/src/main/java/org/thingsboard/server/edqs/repo/TenantRepo.java index 043c0bd6c3..9003eb4da2 100644 --- a/common/edqs/src/main/java/org/thingsboard/server/edqs/repo/TenantRepo.java +++ b/common/edqs/src/main/java/org/thingsboard/server/edqs/repo/TenantRepo.java @@ -105,7 +105,7 @@ public class TenantRepo { public void processEvent(EdqsEvent event) { EdqsObject edqsObject = event.getObject(); - log.debug("[{}] Processing event: {}", tenantId, event); + log.trace("[{}] Processing event: {}", tenantId, event); if (event.getEventType() == EdqsEventType.UPDATED) { addOrUpdate(edqsObject); } else if (event.getEventType() == EdqsEventType.DELETED) { diff --git a/common/edqs/src/main/java/org/thingsboard/server/edqs/state/EdqsStateService.java b/common/edqs/src/main/java/org/thingsboard/server/edqs/state/EdqsStateService.java index 1966618d4b..bb0aa67239 100644 --- a/common/edqs/src/main/java/org/thingsboard/server/edqs/state/EdqsStateService.java +++ b/common/edqs/src/main/java/org/thingsboard/server/edqs/state/EdqsStateService.java @@ -33,6 +33,8 @@ public interface EdqsStateService { void save(TenantId tenantId, ObjectType type, String key, EdqsEventType eventType, ToEdqsMsg msg); + boolean isReady(); + void stop(); } diff --git a/common/edqs/src/main/java/org/thingsboard/server/edqs/state/KafkaEdqsStateService.java b/common/edqs/src/main/java/org/thingsboard/server/edqs/state/KafkaEdqsStateService.java index f5ba2ba841..05eebb188c 100644 --- a/common/edqs/src/main/java/org/thingsboard/server/edqs/state/KafkaEdqsStateService.java +++ b/common/edqs/src/main/java/org/thingsboard/server/edqs/state/KafkaEdqsStateService.java @@ -17,6 +17,8 @@ package org.thingsboard.server.edqs.state; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.context.annotation.Lazy; import org.springframework.stereotype.Service; import org.thingsboard.server.common.data.ObjectType; import org.thingsboard.server.common.data.edqs.EdqsEventType; @@ -52,8 +54,9 @@ public class KafkaEdqsStateService implements EdqsStateService { private final EdqsConfig config; private final EdqsPartitionService partitionService; private final EdqsQueueFactory queueFactory; - private final EdqsProcessor edqsProcessor; private final TopicService topicService; + @Autowired @Lazy + private EdqsProcessor edqsProcessor; private PartitionedQueueConsumerManager> stateConsumer; private QueueStateService, TbProtoQueueMsg> queueStateService; @@ -63,6 +66,7 @@ public class KafkaEdqsStateService implements EdqsStateService { private final VersionsStore versionsStore = new VersionsStore(); private final AtomicInteger stateReadCount = new AtomicInteger(); private final AtomicInteger eventsReadCount = new AtomicInteger(); + private Boolean ready; @Override public void init(PartitionedQueueConsumerManager> eventConsumer) { @@ -72,9 +76,6 @@ public class KafkaEdqsStateService implements EdqsStateService { .pollInterval(config.getPollInterval()) .msgPackProcessor((msgs, consumer, config) -> { for (TbProtoQueueMsg queueMsg : msgs) { - if (consumer.isStopped()) { - return; - } try { ToEdqsMsg msg = queueMsg.getValue(); edqsProcessor.process(msg, EdqsQueue.STATE); @@ -124,7 +125,7 @@ public class KafkaEdqsStateService implements EdqsStateService { TenantId tenantId = getTenantId(msg); ObjectType objectType = ObjectType.valueOf(eventMsg.getObjectType()); EdqsEventType eventType = EdqsEventType.valueOf(eventMsg.getEventType()); - log.debug("[{}] Saving to backup [{}] [{}] [{}]", tenantId, objectType, eventType, key); + log.trace("[{}] Saving to backup [{}] [{}] [{}]", tenantId, objectType, eventType, key); stateProducer.send(tenantId, objectType, key, msg); } } catch (Throwable t) { @@ -160,6 +161,18 @@ public class KafkaEdqsStateService implements EdqsStateService { // do nothing here, backup is done by events consumer } + @Override + public boolean isReady() { + if (ready == null) { + Set partitionsInProgress = queueStateService.getPartitionsInProgress(); + if (partitionsInProgress != null && partitionsInProgress.isEmpty()) { + ready = true; // once true - always true, not to change readiness status on each repartitioning + } + } + log.error("ready: {}", ready); + return ready != null && ready; + } + private TenantId getTenantId(ToEdqsMsg edqsMsg) { return TenantId.fromUUID(new UUID(edqsMsg.getTenantIdMSB(), edqsMsg.getTenantIdLSB())); } diff --git a/common/edqs/src/main/java/org/thingsboard/server/edqs/state/LocalEdqsStateService.java b/common/edqs/src/main/java/org/thingsboard/server/edqs/state/LocalEdqsStateService.java index 6869f14b74..c1620e5ec7 100644 --- a/common/edqs/src/main/java/org/thingsboard/server/edqs/state/LocalEdqsStateService.java +++ b/common/edqs/src/main/java/org/thingsboard/server/edqs/state/LocalEdqsStateService.java @@ -80,6 +80,11 @@ public class LocalEdqsStateService implements EdqsStateService { } } + @Override + public boolean isReady() { + return partitions != null; + } + @Override public void stop() { } diff --git a/common/edqs/src/main/java/org/thingsboard/server/edqs/stats/EdqsStatsService.java b/common/edqs/src/main/java/org/thingsboard/server/edqs/stats/EdqsStatsService.java index 9619d086ad..d28334c5d9 100644 --- a/common/edqs/src/main/java/org/thingsboard/server/edqs/stats/EdqsStatsService.java +++ b/common/edqs/src/main/java/org/thingsboard/server/edqs/stats/EdqsStatsService.java @@ -43,9 +43,12 @@ public class EdqsStatsService { private final ConcurrentHashMap statsMap = new ConcurrentHashMap<>(); private final StatsFactory statsFactory; - @Scheduled(initialDelayString = "${queue.edqs.stats.print-interval-ms:60000}", - fixedDelayString = "${queue.edqs.stats.print-interval-ms:60000}") + @Scheduled(initialDelayString = "${queue.edqs.stats.print-interval-ms:300000}", + fixedDelayString = "${queue.edqs.stats.print-interval-ms:300000}") private void reportStats() { + if (statsMap.isEmpty()) { + return; + } String values = statsMap.entrySet().stream() .map(kv -> "TenantId [" + kv.getKey() + "] stats [" + kv.getValue() + "]") .collect(Collectors.joining(System.lineSeparator())); diff --git a/common/message/src/main/java/org/thingsboard/server/common/msg/queue/TopicPartitionInfo.java b/common/message/src/main/java/org/thingsboard/server/common/msg/queue/TopicPartitionInfo.java index 3bb489f209..da7f16ec0b 100644 --- a/common/message/src/main/java/org/thingsboard/server/common/msg/queue/TopicPartitionInfo.java +++ b/common/message/src/main/java/org/thingsboard/server/common/msg/queue/TopicPartitionInfo.java @@ -17,13 +17,11 @@ package org.thingsboard.server.common.msg.queue; import lombok.Builder; import lombok.Getter; -import lombok.ToString; import org.thingsboard.server.common.data.id.TenantId; import java.util.Objects; import java.util.Optional; -@ToString public class TopicPartitionInfo { private final String topic; @@ -97,4 +95,13 @@ public class TopicPartitionInfo { return Objects.hash(fullTopicName, partition); } + @Override + public String toString() { + String str = fullTopicName; + if (useInternalPartition) { + str += "[" + partition + "]"; + } + return str; + } + } diff --git a/common/queue/src/main/java/org/thingsboard/server/queue/common/consumer/MainQueueConsumerManager.java b/common/queue/src/main/java/org/thingsboard/server/queue/common/consumer/MainQueueConsumerManager.java index df683b94cd..a73ee17bd2 100644 --- a/common/queue/src/main/java/org/thingsboard/server/queue/common/consumer/MainQueueConsumerManager.java +++ b/common/queue/src/main/java/org/thingsboard/server/queue/common/consumer/MainQueueConsumerManager.java @@ -43,7 +43,6 @@ import java.util.concurrent.TimeUnit; import java.util.concurrent.locks.ReentrantLock; import java.util.function.BiFunction; import java.util.function.Consumer; -import java.util.stream.Collectors; @Slf4j public class MainQueueConsumerManager { @@ -270,12 +269,6 @@ public class MainQueueConsumerManager partitions) { - return partitions.stream().map(tpi -> tpi.getFullTopicName() + (tpi.isUseInternalPartition() ? - "[" + tpi.getPartition().orElse(-1) + "]" : "")) - .collect(Collectors.joining(", ", "[", "]")); - } - public interface MsgPackProcessor { void process(List msgs, TbQueueConsumer consumer, C config) throws Exception; } @@ -299,7 +292,7 @@ public class MainQueueConsumerManager removedPartitions = new HashSet<>(consumers.keySet()); removedPartitions.removeAll(partitions); - log.info("[{}] Added partitions: {}, removed partitions: {}", queueKey, partitionsToString(addedPartitions), partitionsToString(removedPartitions)); + log.info("[{}] Added partitions: {}, removed partitions: {}", queueKey, addedPartitions, removedPartitions); removePartitions(removedPartitions); addPartitions(addedPartitions, null); } @@ -333,7 +326,7 @@ public class MainQueueConsumerManager partitions) { - log.info("[{}] New partitions: {}", queueKey, partitionsToString(partitions)); + log.info("[{}] New partitions: {}", queueKey, partitions); if (partitions.isEmpty()) { if (consumer != null && consumer.isRunning()) { consumer.initiateStop(); diff --git a/common/queue/src/main/java/org/thingsboard/server/queue/common/consumer/PartitionedQueueConsumerManager.java b/common/queue/src/main/java/org/thingsboard/server/queue/common/consumer/PartitionedQueueConsumerManager.java index 1b47d4c073..6ab0668f30 100644 --- a/common/queue/src/main/java/org/thingsboard/server/queue/common/consumer/PartitionedQueueConsumerManager.java +++ b/common/queue/src/main/java/org/thingsboard/server/queue/common/consumer/PartitionedQueueConsumerManager.java @@ -52,10 +52,10 @@ public class PartitionedQueueConsumerManager extends MainQ @Override protected void processTask(TbQueueConsumerManagerTask task) { if (task instanceof AddPartitionsTask addPartitionsTask) { - log.info("[{}] Added partitions: {}", queueKey, partitionsToString(addPartitionsTask.partitions())); + log.info("[{}] Added partitions: {}", queueKey, addPartitionsTask.partitions()); consumerWrapper.addPartitions(addPartitionsTask.partitions(), addPartitionsTask.onStop()); } else if (task instanceof RemovePartitionsTask removePartitionsTask) { - log.info("[{}] Removed partitions: {}", queueKey, partitionsToString(removePartitionsTask.partitions())); + log.info("[{}] Removed partitions: {}", queueKey, removePartitionsTask.partitions()); consumerWrapper.removePartitions(removePartitionsTask.partitions()); } } diff --git a/common/queue/src/main/java/org/thingsboard/server/queue/common/consumer/QueueStateService.java b/common/queue/src/main/java/org/thingsboard/server/queue/common/consumer/QueueStateService.java index d022ad14de..00015457c1 100644 --- a/common/queue/src/main/java/org/thingsboard/server/queue/common/consumer/QueueStateService.java +++ b/common/queue/src/main/java/org/thingsboard/server/queue/common/consumer/QueueStateService.java @@ -16,16 +16,19 @@ package org.thingsboard.server.queue.common.consumer; import lombok.Getter; +import lombok.extern.slf4j.Slf4j; import org.thingsboard.server.common.msg.queue.TopicPartitionInfo; import org.thingsboard.server.queue.TbQueueMsg; import java.util.Collections; import java.util.HashSet; import java.util.Set; +import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.locks.Lock; import java.util.concurrent.locks.ReentrantLock; import java.util.stream.Collectors; +@Slf4j public class QueueStateService { private PartitionedQueueConsumerManager stateConsumer; @@ -33,6 +36,9 @@ public class QueueStateService { @Getter private Set partitions; + private final Set partitionsInProgress = ConcurrentHashMap.newKeySet(); + private boolean initialized; + private final Lock lock = new ReentrantLock(); public void init(PartitionedQueueConsumerManager stateConsumer, PartitionedQueueConsumerManager eventConsumer) { @@ -62,9 +68,15 @@ public class QueueStateService { } if (!addedPartitions.isEmpty()) { + partitionsInProgress.addAll(addedPartitions); stateConsumer.addPartitions(addedPartitions, partition -> { lock.lock(); try { + partitionsInProgress.remove(partition); + log.info("Finished partition {} (still in progress: {})", partition, partitionsInProgress); + if (partitionsInProgress.isEmpty()) { + log.info("All partitions processed"); + } if (this.partitions.contains(partition)) { eventConsumer.addPartitions(Set.of(partition.withTopic(eventConsumer.getTopic()))); } @@ -73,10 +85,15 @@ public class QueueStateService { } }); } + initialized = true; } private Set withTopic(Set partitions, String topic) { return partitions.stream().map(tpi -> tpi.withTopic(topic)).collect(Collectors.toSet()); } + public Set getPartitionsInProgress() { + return initialized ? partitionsInProgress : null; + } + } diff --git a/common/queue/src/main/java/org/thingsboard/server/queue/discovery/HashPartitionService.java b/common/queue/src/main/java/org/thingsboard/server/queue/discovery/HashPartitionService.java index ea9ba3f245..5787516b25 100644 --- a/common/queue/src/main/java/org/thingsboard/server/queue/discovery/HashPartitionService.java +++ b/common/queue/src/main/java/org/thingsboard/server/queue/discovery/HashPartitionService.java @@ -59,7 +59,7 @@ import static org.thingsboard.server.common.data.DataConstants.MAIN_QUEUE_NAME; @Slf4j public class HashPartitionService implements PartitionService { - @Value("${queue.core.topic}") + @Value("${queue.core.topic:tb_core}") private String coreTopic; @Value("${queue.core.partitions:10}") private Integer corePartitions; diff --git a/common/queue/src/main/java/org/thingsboard/server/queue/edqs/EdqsComponent.java b/common/queue/src/main/java/org/thingsboard/server/queue/edqs/EdqsComponent.java index 27a1e09f79..ba113495d1 100644 --- a/common/queue/src/main/java/org/thingsboard/server/queue/edqs/EdqsComponent.java +++ b/common/queue/src/main/java/org/thingsboard/server/queue/edqs/EdqsComponent.java @@ -21,7 +21,6 @@ import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; @Retention(RetentionPolicy.RUNTIME) -// TODO: tb-core ? @ConditionalOnExpression("'${queue.edqs.sync.enabled:true}'=='true' && ('${service.type:null}'=='edqs' || " + "(('${service.type:null}'=='monolith' || '${service.type:null}'=='tb-core') && " + "'${queue.edqs.mode:null}'=='local'))") diff --git a/common/queue/src/main/java/org/thingsboard/server/queue/kafka/TbKafkaConsumerTemplate.java b/common/queue/src/main/java/org/thingsboard/server/queue/kafka/TbKafkaConsumerTemplate.java index 47c776c742..9b580c762e 100644 --- a/common/queue/src/main/java/org/thingsboard/server/queue/kafka/TbKafkaConsumerTemplate.java +++ b/common/queue/src/main/java/org/thingsboard/server/queue/kafka/TbKafkaConsumerTemplate.java @@ -54,6 +54,7 @@ public class TbKafkaConsumerTemplate extends AbstractTbQue private final boolean readFromBeginning; // reset offset to beginning private final boolean stopWhenRead; // stop consuming when reached an empty msg pack + private int readCount; private Map endOffsets; // needed if stopWhenRead is true private boolean partitionsAssigned = false; @@ -152,6 +153,7 @@ public class TbKafkaConsumerTemplate extends AbstractTbQue records.forEach(record -> { recordList.add(record); if (stopWhenRead) { + readCount++; int partition = record.partition(); Long endOffset = endOffsets.get(partition); if (endOffset == null) { @@ -166,7 +168,7 @@ public class TbKafkaConsumerTemplate extends AbstractTbQue }); } if (stopWhenRead && endOffsets.isEmpty()) { - log.info("Reached end offset for {}, stopping consumer", consumer.assignment()); + log.info("Finished reading {}, processed {} messages", partitions, readCount); stop(); } return recordList; diff --git a/common/stats/src/main/java/org/thingsboard/server/common/stats/DefaultStatsFactory.java b/common/stats/src/main/java/org/thingsboard/server/common/stats/DefaultStatsFactory.java index 2fc7970fc9..e1940e55fe 100644 --- a/common/stats/src/main/java/org/thingsboard/server/common/stats/DefaultStatsFactory.java +++ b/common/stats/src/main/java/org/thingsboard/server/common/stats/DefaultStatsFactory.java @@ -38,7 +38,7 @@ public class DefaultStatsFactory implements StatsFactory { private static final Counter STUB_COUNTER = new StubCounter(); - @Autowired(required = false) // FIXME Slavik !!! + @Autowired // FIXME Slavik !!! private MeterRegistry meterRegistry; @Value("${metrics.enabled:false}") diff --git a/dao/src/main/java/org/thingsboard/server/dao/entity/BaseEntityService.java b/dao/src/main/java/org/thingsboard/server/dao/entity/BaseEntityService.java index 3f941b776c..1a0f3be1e7 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/entity/BaseEntityService.java +++ b/dao/src/main/java/org/thingsboard/server/dao/entity/BaseEntityService.java @@ -142,12 +142,12 @@ public class BaseEntityService extends AbstractEntityService implements EntitySe private EdqsResponse processEdqsRequest(TenantId tenantId, CustomerId customerId, EdqsRequest request) { EdqsResponse response; try { - log.info("Sending request to EDQS: {}", request); + log.debug("[{}] Sending request to EDQS: {}", tenantId, request); response = edqsService.processRequest(tenantId, customerId, request).get(); } catch (InterruptedException | ExecutionException e) { throw new RuntimeException(e); } - log.info("Received response from EDQS: {}", response); + log.debug("[{}] Received response from EDQS: {}", tenantId, response); if (response.getError() != null) { throw new RuntimeException(response.getError()); } diff --git a/dao/src/main/java/org/thingsboard/server/dao/sql/query/DummyEdqsService.java b/dao/src/main/java/org/thingsboard/server/dao/sql/query/DummyEdqsService.java index b69aa52ec5..9586cb62f8 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/sql/query/DummyEdqsService.java +++ b/dao/src/main/java/org/thingsboard/server/dao/sql/query/DummyEdqsService.java @@ -16,7 +16,7 @@ package org.thingsboard.server.dao.sql.query; import com.google.common.util.concurrent.ListenableFuture; -import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; +import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; import org.springframework.stereotype.Service; import org.thingsboard.server.common.data.ObjectType; import org.thingsboard.server.common.data.edqs.EdqsObject; @@ -30,7 +30,7 @@ import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.common.msg.edqs.EdqsService; @Service -@ConditionalOnProperty(value = "queue.edqs.sync.enabled", havingValue = "false", matchIfMissing = true) +@ConditionalOnMissingBean(value = EdqsService.class, ignored = DummyEdqsService.class) public class DummyEdqsService implements EdqsService { @Override diff --git a/edqs/src/main/java/org/thingsboard/server/edqs/EdqsController.java b/edqs/src/main/java/org/thingsboard/server/edqs/EdqsController.java new file mode 100644 index 0000000000..f2e686a84c --- /dev/null +++ b/edqs/src/main/java/org/thingsboard/server/edqs/EdqsController.java @@ -0,0 +1,41 @@ +/** + * Copyright © 2016-2024 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.edqs; + +import lombok.RequiredArgsConstructor; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; +import org.thingsboard.server.edqs.state.EdqsStateService; + +@RestController +@RequiredArgsConstructor +@RequestMapping("/api/edqs") +public class EdqsController { + + private final EdqsStateService edqsStateService; + + @GetMapping("/ready") + public ResponseEntity isReady() { + if (edqsStateService.isReady()) { + return ResponseEntity.ok().build(); + } else { + return ResponseEntity.badRequest().build(); + } + } + +} diff --git a/edqs/src/main/java/org/thingsboard/server/edqs/ThingsboardEdqsApplication.java b/edqs/src/main/java/org/thingsboard/server/edqs/ThingsboardEdqsApplication.java index 044020023d..2f5185728a 100644 --- a/edqs/src/main/java/org/thingsboard/server/edqs/ThingsboardEdqsApplication.java +++ b/edqs/src/main/java/org/thingsboard/server/edqs/ThingsboardEdqsApplication.java @@ -18,6 +18,7 @@ package org.thingsboard.server.edqs; import lombok.extern.slf4j.Slf4j; import org.springframework.boot.SpringApplication; import org.springframework.boot.SpringBootConfiguration; +import org.springframework.boot.autoconfigure.EnableAutoConfiguration; import org.springframework.context.annotation.ComponentScan; import org.springframework.scheduling.annotation.EnableAsync; import org.springframework.scheduling.annotation.EnableScheduling; @@ -27,6 +28,7 @@ import java.util.Arrays; @SpringBootConfiguration @EnableAsync @EnableScheduling +@EnableAutoConfiguration @ComponentScan({"org.thingsboard.server.edqs", "org.thingsboard.server.queue.edqs", "org.thingsboard.server.queue.discovery", "org.thingsboard.server.queue.kafka", "org.thingsboard.server.queue.settings", "org.thingsboard.server.queue.environment", "org.thingsboard.server.common.stats"}) @Slf4j @@ -39,71 +41,6 @@ public class ThingsboardEdqsApplication { SpringApplication.run(ThingsboardEdqsApplication.class, updateArguments(args)); } - // @Bean -// public ApplicationRunner runner(CSVLoader loader, EdqRepository edqRepository) { -// return args -> { -// long startTs = System.currentTimeMillis(); -// var loader = new TenantRepoLoader(new TenantRepo(TenantId.fromUUID(UUID.fromString("2a209df0-c7ff-11ea-a3e0-f321b0429d60")))); -// loader.load(); -// log.info("Loaded all in {} ms", System.currentTimeMillis() - startTs); - - - -// log.info("Compressed {} strings/json, Before: {}, After: {}", -// CompressedStringDataPoint.cnt.get(), -// CompressedStringDataPoint.uncompressedLength.get(), -// CompressedStringDataPoint.compressedLength.get()); -// -// log.info("Deduplicated {} short and {} long strings", -// TbStringPool.size(), TbBytePool.size()); -// -// var tenantId = TenantId.fromUUID(UUID.fromString("2a209df0-c7ff-11ea-a3e0-f321b0429d60")); -// var customerId = new CustomerId(UUID.fromString("fcbf2f50-d0d9-11ea-bea3-177755191a6e")); -// System.gc(); -// -// while (true) { -// EntityTypeFilter filter = new EntityTypeFilter(); -// filter.setEntityType(EntityType.DEVICE); -// var pageLink = new EntityDataPageLink(20, 0, null, new EntityDataSortOrder(new EntityKey(EntityKeyType.TIME_SERIES, "state"), EntityDataSortOrder.Direction.DESC), false); -// -// var entityFields = Arrays.asList(new EntityKey(EntityKeyType.ENTITY_FIELD, "name"), new EntityKey(EntityKeyType.ENTITY_FIELD, "createdTime")); -// var latestValues = Arrays.asList(new EntityKey(EntityKeyType.TIME_SERIES, "state")); -// KeyFilter nameFilter = new KeyFilter(); -// nameFilter.setKey(new EntityKey(EntityKeyType.ENTITY_FIELD, "name")); -// var predicate = new StringFilterPredicate(); -// predicate.setIgnoreCase(false); -// predicate.setOperation(StringFilterPredicate.StringOperation.CONTAINS); -// predicate.setValue(new FilterPredicateValue<>("LoRa-")); -// nameFilter.setPredicate(predicate); -// nameFilter.setValueType(EntityKeyValueType.STRING); -// -// EntityDataQuery edq = new EntityDataQuery(filter, pageLink, entityFields, latestValues, Arrays.asList(nameFilter)); -// var result = edqRepository.findEntityDataByQuery(tenantId, customerId, RepositoryUtils.ALL_READ_PERMISSIONS, edq, false); -// log.info("Device count: {}", result.getTotalElements()); -// log.info("First: {}", result.getData().get(0).getEntityId()); -// log.info("Last: {}", result.getData().get(19).getEntityId()); -// -// pageLink.setSortOrder(new EntityDataSortOrder(new EntityKey(EntityKeyType.TIME_SERIES, "state"), EntityDataSortOrder.Direction.ASC)); -// result = edqRepository.findEntityDataByQuery(tenantId, customerId, RepositoryUtils.ALL_READ_PERMISSIONS, edq, false); -// log.info("Device count: {}", result.getTotalElements()); -// log.info("First: {}", result.getData().get(0).getEntityId()); -// log.info("Last: {}", result.getData().get(19).getEntityId()); -// -// result.getData().forEach(data -> { -// System.err.println(data.getEntityId() + ":"); -// data.getLatest().forEach((type, values) -> { -// System.err.println(type); -// values.forEach((key, tsValue) -> { -// System.err.println(key + " = " + tsValue.getValue()); -// }); -// }); -// System.err.println(); -// }); -// Thread.sleep(5000); -// } -// }; -// } - private static String[] updateArguments(String[] args) { if (Arrays.stream(args).noneMatch(arg -> arg.startsWith(SPRING_CONFIG_NAME_KEY))) { String[] modifiedArgs = new String[args.length + 1]; diff --git a/edqs/src/main/resources/edqs.yml b/edqs/src/main/resources/edqs.yml index fa09da1cdd..3d30c5c6fa 100644 --- a/edqs/src/main/resources/edqs.yml +++ b/edqs/src/main/resources/edqs.yml @@ -14,6 +14,12 @@ # limitations under the License. # +server: + # Server bind-address + address: "${HTTP_BIND_ADDRESS:0.0.0.0}" + # Server bind port + port: "${HTTP_BIND_PORT:8080}" + # Application info parameters app: # Application version @@ -38,10 +44,9 @@ zk: # The delay is recommended because the initialization of rule chain actors is time-consuming. Avoiding unnecessary recalculations during a restart can enhance system performance and stability. recalculate_delay: "${ZOOKEEPER_RECALCULATE_DELAY_MS:0}" -# SQL DAO Configuration parameters spring: main: - web-application-type: "none" + allow-circular-references: "true" # Spring Boot configuration property that controls whether circular dependencies between beans are allowed. # Queue configuration parameters queue: @@ -66,7 +71,7 @@ queue: # Enable/disable statistics for EDQS enabled: "${TB_EDQS_STATS_ENABLED:true}" # Statistics printing interval for EDQS - print-interval-ms: "${TB_EDQS_STATS_PRINT_INTERVAL_MS:60000}" + print-interval-ms: "${TB_EDQS_STATS_PRINT_INTERVAL_MS:300000}" kafka: # Kafka Bootstrap nodes in "host:port" format @@ -137,9 +142,9 @@ queue: - key: max.poll.interval.ms # Example of specific consumer properties value per topic for VC value: "${TB_QUEUE_KAFKA_VC_MAX_POLL_INTERVAL_MS:600000}" - # tb_rule_engine.sq: - # - key: max.poll.records - # value: "${TB_QUEUE_KAFKA_SQ_MAX_POLL_RECORDS:1024}" + # tb_rule_engine.sq: + # - key: max.poll.records + # value: "${TB_QUEUE_KAFKA_SQ_MAX_POLL_RECORDS:1024}" tb_housekeeper: # Consumer properties for Housekeeper tasks topic - key: max.poll.records @@ -209,133 +214,6 @@ queue: request_poll_interval: "${TB_QUEUE_TRANSPORT_REQUEST_POLL_INTERVAL_MS:25}" # Interval in milliseconds to poll api response from transport microservices response_poll_interval: "${TB_QUEUE_TRANSPORT_RESPONSE_POLL_INTERVAL_MS:25}" - core: - # Default topic name of Kafka, RabbitMQ, etc. queue - topic: "${TB_QUEUE_CORE_TOPIC:tb_core}" - # Interval in milliseconds to poll messages by Core microservices - poll-interval: "${TB_QUEUE_CORE_POLL_INTERVAL_MS:25}" - # Amount of partitions used by Core microservices - partitions: "${TB_QUEUE_CORE_PARTITIONS:10}" - # Timeout for processing a message pack by Core microservices - pack-processing-timeout: "${TB_QUEUE_CORE_PACK_PROCESSING_TIMEOUT_MS:2000}" - # Enable/disable a separate consumer per partition for Core queue - consumer-per-partition: "${TB_QUEUE_CORE_CONSUMER_PER_PARTITION:true}" - ota: - # Default topic name for OTA updates - topic: "${TB_QUEUE_CORE_OTA_TOPIC:tb_ota_package}" - # The interval of processing the OTA updates for devices. Used to avoid any harm to the network due to many parallel OTA updates - pack-interval-ms: "${TB_QUEUE_CORE_OTA_PACK_INTERVAL_MS:60000}" - # The size of OTA updates notifications fetched from the queue. The queue stores pairs of firmware and device ids - pack-size: "${TB_QUEUE_CORE_OTA_PACK_SIZE:100}" - # Stats topic name for queue Kafka, RabbitMQ, etc. - usage-stats-topic: "${TB_QUEUE_US_TOPIC:tb_usage_stats}" - stats: - # Enable/disable statistics for Core microservices - enabled: "${TB_QUEUE_CORE_STATS_ENABLED:true}" - # Statistics printing interval for Core microservices - print-interval-ms: "${TB_QUEUE_CORE_STATS_PRINT_INTERVAL_MS:60000}" - housekeeper: - # Topic name for Housekeeper tasks - topic: "${TB_HOUSEKEEPER_TOPIC:tb_housekeeper}" - # Topic name for Housekeeper tasks to be reprocessed - reprocessing-topic: "${TB_HOUSEKEEPER_REPROCESSING_TOPIC:tb_housekeeper.reprocessing}" - # Poll interval for topics related to Housekeeper - poll-interval-ms: "${TB_HOUSEKEEPER_POLL_INTERVAL_MS:500}" - # Timeout in milliseconds for task processing. Tasks that fail to finish on time will be submitted for reprocessing - task-processing-timeout-ms: "${TB_HOUSEKEEPER_TASK_PROCESSING_TIMEOUT_MS:120000}" - # Comma-separated list of task types that shouldn't be processed. Available task types: - # DELETE_ATTRIBUTES, DELETE_TELEMETRY (both DELETE_LATEST_TS and DELETE_TS_HISTORY will be disabled), - # DELETE_LATEST_TS, DELETE_TS_HISTORY, DELETE_EVENTS, DELETE_ALARMS, UNASSIGN_ALARMS - disabled-task-types: "${TB_HOUSEKEEPER_DISABLED_TASK_TYPES:}" - # Delay in milliseconds between tasks reprocessing - task-reprocessing-delay-ms: "${TB_HOUSEKEEPER_TASK_REPROCESSING_DELAY_MS:3000}" - # Maximum amount of task reprocessing attempts. After exceeding, the task will be dropped - max-reprocessing-attempts: "${TB_HOUSEKEEPER_MAX_REPROCESSING_ATTEMPTS:10}" - stats: - # Enable/disable statistics for Housekeeper - enabled: "${TB_HOUSEKEEPER_STATS_ENABLED:true}" - # Statistics printing interval for Housekeeper - print-interval-ms: "${TB_HOUSEKEEPER_STATS_PRINT_INTERVAL_MS:60000}" - - vc: - # Default topic name for Kafka, RabbitMQ, etc. - topic: "${TB_QUEUE_VC_TOPIC:tb_version_control}" - # Number of partitions to associate with this queue. Used for scaling the number of messages that can be processed in parallel - partitions: "${TB_QUEUE_VC_PARTITIONS:10}" - # Interval in milliseconds between polling of the messages if no new messages arrive - poll-interval: "${TB_QUEUE_VC_INTERVAL_MS:25}" - # Timeout before retrying all failed and timed-out messages from the processing pack - pack-processing-timeout: "${TB_QUEUE_VC_PACK_PROCESSING_TIMEOUT_MS:180000}" - # Timeout for a request to VC-executor (for a request for the version of the entity, for a commit charge, etc.) - request-timeout: "${TB_QUEUE_VC_REQUEST_TIMEOUT:180000}" - # Queue settings for Kafka, RabbitMQ, etc. Limit for single message size - msg-chunk-size: "${TB_QUEUE_VC_MSG_CHUNK_SIZE:250000}" - js: - # JS Eval request topic - request_topic: "${REMOTE_JS_EVAL_REQUEST_TOPIC:js_eval.requests}" - # JS Eval responses topic prefix that is combined with node id - response_topic_prefix: "${REMOTE_JS_EVAL_RESPONSE_TOPIC:js_eval.responses}" - # JS Eval max pending requests - max_pending_requests: "${REMOTE_JS_MAX_PENDING_REQUESTS:10000}" - # JS Eval max request timeout - max_eval_requests_timeout: "${REMOTE_JS_MAX_EVAL_REQUEST_TIMEOUT:60000}" - # JS max request timeout - max_requests_timeout: "${REMOTE_JS_MAX_REQUEST_TIMEOUT:10000}" - # JS execution max request timeout - max_exec_requests_timeout: "${REMOTE_JS_MAX_EXEC_REQUEST_TIMEOUT:2000}" - # JS response poll interval - response_poll_interval: "${REMOTE_JS_RESPONSE_POLL_INTERVAL_MS:25}" - rule-engine: - # Deprecated. It will be removed in the nearest releases - topic: "${TB_QUEUE_RULE_ENGINE_TOPIC:tb_rule_engine}" - # Interval in milliseconds to poll messages by Rule Engine - poll-interval: "${TB_QUEUE_RULE_ENGINE_POLL_INTERVAL_MS:25}" - # Timeout for processing a message pack of Rule Engine - pack-processing-timeout: "${TB_QUEUE_RULE_ENGINE_PACK_PROCESSING_TIMEOUT_MS:2000}" - stats: - # Enable/disable statistics for Rule Engine - enabled: "${TB_QUEUE_RULE_ENGINE_STATS_ENABLED:true}" - # Statistics printing interval for Rule Engine - print-interval-ms: "${TB_QUEUE_RULE_ENGINE_STATS_PRINT_INTERVAL_MS:60000}" - # Max length of the error message that is printed by statistics - max-error-message-length: "${TB_QUEUE_RULE_ENGINE_MAX_ERROR_MESSAGE_LENGTH:4096}" - # After a queue is deleted (or the profile's isolation option was disabled), Rule Engine will continue reading related topics during this period before deleting the actual topics - topic-deletion-delay: "${TB_QUEUE_RULE_ENGINE_TOPIC_DELETION_DELAY_SEC:15}" - # Size of the thread pool that handles such operations as partition changes, config updates, queue deletion - management-thread-pool-size: "${TB_QUEUE_RULE_ENGINE_MGMT_THREAD_POOL_SIZE:12}" - transport: - # For high-priority notifications that require minimum latency and processing time - notifications_topic: "${TB_QUEUE_TRANSPORT_NOTIFICATIONS_TOPIC:tb_transport.notifications}" - # Interval in milliseconds to poll messages - poll_interval: "${TB_QUEUE_TRANSPORT_NOTIFICATIONS_POLL_INTERVAL_MS:25}" - integration: - # Name of hash function used for consistent hash ring in Cluster Mode. See architecture docs for more details. Valid values - murmur3_32, murmur3_128 or sha256 - partitions: "${TB_QUEUE_INTEGRATION_PARTITIONS:3}" - # Default notification topic name used by queue - notifications_topic: "${TB_QUEUE_INTEGRATION_NOTIFICATIONS_TOPIC:tb_ie.notifications}" - # Default downlink topic name used by queue - downlink_topic: "${TB_QUEUE_INTEGRATION_DOWNLINK_TOPIC:tb_ie.downlink}" - # Default uplink topic name used by queue - uplink_topic: "${TB_QUEUE_INTEGRATION_UPLINK_TOPIC:tb_ie.uplink}" - # Interval in milliseconds to poll messages by integrations - poll_interval: "${TB_QUEUE_INTEGRATION_POLL_INTERVAL_MS:25}" - # Timeout for processing a message pack by integrations - pack-processing-timeout: "${TB_QUEUE_INTEGRATION_PACK_PROCESSING_TIMEOUT_MS:10000}" - integration_api: - # Default Integration Api request topic name used by queue - requests_topic: "${TB_QUEUE_INTEGRATION_EXECUTOR_API_REQUEST_TOPIC:tb_ie.api.requests}" - # Default Integration Api response topic name used by queue - responses_topic: "${TB_QUEUE_INTEGRATION_EXECUTOR_API_RESPONSE_TOPIC:tb_ie.api.responses}" - # Maximum pending api requests from integration executor to be handled by server< - max_pending_requests: "${TB_QUEUE_INTEGRATION_EXECUTOR_MAX_PENDING_REQUESTS:10000}" - # Maximum timeout in milliseconds to handle api request from integration executor microservice by server - max_requests_timeout: "${TB_QUEUE_INTEGRATION_EXECUTOR_MAX_REQUEST_TIMEOUT:10000}" - # Amount of threads used to invoke callbacks - max_callback_threads: "${TB_QUEUE_INTEGRATION_EXECUTOR_MAX_CALLBACK_THREADS:10}" - # Interval in milliseconds to poll api requests from integration executor microservices - request_poll_interval: "${TB_QUEUE_INTEGRATION_EXECUTOR_REQUEST_POLL_INTERVAL_MS:25}" - # Interval in milliseconds to poll api response from integration executor microservices - response_poll_interval: "${TB_QUEUE_INTEGRATION_EXECUTOR_RESPONSE_POLL_INTERVAL_MS:25}" # General service parameters service: @@ -343,7 +221,8 @@ service: # Unique id for this service (autogenerated if empty) id: "${TB_SERVICE_ID:}" edqs: - label: "${TB_EDQS_LABEL:}" # services with the same label will share the list of partitions + # EDQS instances with the same label will share the same list of partitions + label: "${TB_EDQS_LABEL:}" # Metrics parameters metrics: diff --git a/edqs/src/main/resources/logback.xml b/edqs/src/main/resources/logback.xml index d96cb1a7d3..0aedb9d092 100644 --- a/edqs/src/main/resources/logback.xml +++ b/edqs/src/main/resources/logback.xml @@ -26,6 +26,7 @@ + From 278ec36f376cd707562ec2b6b650f6eaf273832e Mon Sep 17 00:00:00 2001 From: ViacheslavKlimov Date: Fri, 21 Feb 2025 15:48:33 +0200 Subject: [PATCH 082/345] Fixes for EDQS --- .../server/edqs/processor/EdqsProcessor.java | 8 ++------ .../server/edqs/state/LocalEdqsStateService.java | 10 ++++++++-- .../server/common/msg/queue/TopicPartitionInfo.java | 6 ++++++ .../queue/common/AbstractTbQueueConsumerTemplate.java | 10 ++++------ .../queue/common/consumer/QueueStateService.java | 7 ++----- .../server/queue/edqs/InMemoryEdqsQueueFactory.java | 6 ++++-- .../server/queue/edqs/KafkaEdqsQueueFactory.java | 10 +++++++--- .../server/common/stats/DefaultStatsFactory.java | 2 +- 8 files changed, 34 insertions(+), 25 deletions(-) diff --git a/common/edqs/src/main/java/org/thingsboard/server/edqs/processor/EdqsProcessor.java b/common/edqs/src/main/java/org/thingsboard/server/edqs/processor/EdqsProcessor.java index 9349946a80..ac7af4691d 100644 --- a/common/edqs/src/main/java/org/thingsboard/server/edqs/processor/EdqsProcessor.java +++ b/common/edqs/src/main/java/org/thingsboard/server/edqs/processor/EdqsProcessor.java @@ -76,6 +76,8 @@ import java.util.concurrent.atomic.AtomicInteger; import java.util.function.Consumer; import java.util.stream.Collectors; +import static org.thingsboard.server.common.msg.queue.TopicPartitionInfo.withTopic; + @EdqsComponent @Service @RequiredArgsConstructor @@ -280,12 +282,6 @@ public class EdqsProcessor implements TbQueueHandler, } } - private Set withTopic(Set partitions, String topic) { - return partitions.stream() - .map(tpi -> tpi.withTopic(topic)) - .collect(Collectors.toSet()); - } - @PreDestroy public void destroy() throws InterruptedException { eventConsumer.stop(); diff --git a/common/edqs/src/main/java/org/thingsboard/server/edqs/state/LocalEdqsStateService.java b/common/edqs/src/main/java/org/thingsboard/server/edqs/state/LocalEdqsStateService.java index c1620e5ec7..67724dd8b1 100644 --- a/common/edqs/src/main/java/org/thingsboard/server/edqs/state/LocalEdqsStateService.java +++ b/common/edqs/src/main/java/org/thingsboard/server/edqs/state/LocalEdqsStateService.java @@ -17,6 +17,8 @@ package org.thingsboard.server.edqs.state; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.context.annotation.Lazy; import org.springframework.stereotype.Service; import org.thingsboard.server.common.data.ObjectType; import org.thingsboard.server.common.data.edqs.EdqsEventType; @@ -32,14 +34,17 @@ import org.thingsboard.server.queue.edqs.InMemoryEdqsComponent; import java.util.Set; +import static org.thingsboard.server.common.msg.queue.TopicPartitionInfo.withTopic; + @Service @RequiredArgsConstructor @InMemoryEdqsComponent @Slf4j public class LocalEdqsStateService implements EdqsStateService { - private final EdqsProcessor processor; private final EdqsRocksDb db; + @Autowired @Lazy + private EdqsProcessor processor; private PartitionedQueueConsumerManager> eventConsumer; private Set partitions; @@ -61,8 +66,9 @@ public class LocalEdqsStateService implements EdqsStateService { log.error("[{}] Failed to restore value", key, e); } }); + log.info("Restore completed"); } - eventConsumer.update(partitions); + eventConsumer.update(withTopic(partitions, EdqsQueue.EVENTS.getTopic())); this.partitions = partitions; } diff --git a/common/message/src/main/java/org/thingsboard/server/common/msg/queue/TopicPartitionInfo.java b/common/message/src/main/java/org/thingsboard/server/common/msg/queue/TopicPartitionInfo.java index da7f16ec0b..c7a23451d1 100644 --- a/common/message/src/main/java/org/thingsboard/server/common/msg/queue/TopicPartitionInfo.java +++ b/common/message/src/main/java/org/thingsboard/server/common/msg/queue/TopicPartitionInfo.java @@ -21,6 +21,8 @@ import org.thingsboard.server.common.data.id.TenantId; import java.util.Objects; import java.util.Optional; +import java.util.Set; +import java.util.stream.Collectors; public class TopicPartitionInfo { @@ -75,6 +77,10 @@ public class TopicPartitionInfo { return new TopicPartitionInfo(topic, this.tenantId, this.partition, this.useInternalPartition, this.myPartition); } + public static Set withTopic(Set partitions, String topic) { + return partitions.stream().map(tpi -> tpi.withTopic(topic)).collect(Collectors.toSet()); + } + public TopicPartitionInfo withUseInternalPartition(boolean useInternalPartition) { return new TopicPartitionInfo(this.topic, this.tenantId, this.partition, useInternalPartition, this.myPartition); } diff --git a/common/queue/src/main/java/org/thingsboard/server/queue/common/AbstractTbQueueConsumerTemplate.java b/common/queue/src/main/java/org/thingsboard/server/queue/common/AbstractTbQueueConsumerTemplate.java index 0e399929f4..88e8f60dee 100644 --- a/common/queue/src/main/java/org/thingsboard/server/queue/common/AbstractTbQueueConsumerTemplate.java +++ b/common/queue/src/main/java/org/thingsboard/server/queue/common/AbstractTbQueueConsumerTemplate.java @@ -31,7 +31,6 @@ import java.util.Set; import java.util.concurrent.ConcurrentLinkedQueue; import java.util.concurrent.TimeUnit; import java.util.concurrent.locks.ReentrantLock; -import java.util.stream.Collectors; import static java.util.Collections.emptyList; @@ -95,7 +94,7 @@ public abstract class AbstractTbQueueConsumerTemplate i partitions = subscribeQueue.poll(); } if (!subscribed) { - log.info("Subscribing to topics {}", getFullTopicNames()); + log.info("Subscribing to {}", partitions); doSubscribe(partitions); subscribed = true; } @@ -168,7 +167,7 @@ public abstract class AbstractTbQueueConsumerTemplate i @Override public void unsubscribe() { - log.info("Unsubscribing and stopping consumer for topics {}", getFullTopicNames()); + log.info("Unsubscribing and stopping consumer for {}", partitions); stopped = true; consumerLock.lock(); try { @@ -201,9 +200,8 @@ public abstract class AbstractTbQueueConsumerTemplate i return Collections.emptyList(); } return partitions.stream() - .map(tpi -> tpi.getFullTopicName() + (tpi.isUseInternalPartition() ? - "[" + tpi.getPartition().orElse(-1) + "]" : "")) - .collect(Collectors.toList()); + .map(TopicPartitionInfo::getFullTopicName) + .toList(); } protected boolean isLongPollingSupported() { diff --git a/common/queue/src/main/java/org/thingsboard/server/queue/common/consumer/QueueStateService.java b/common/queue/src/main/java/org/thingsboard/server/queue/common/consumer/QueueStateService.java index 00015457c1..2671f03b95 100644 --- a/common/queue/src/main/java/org/thingsboard/server/queue/common/consumer/QueueStateService.java +++ b/common/queue/src/main/java/org/thingsboard/server/queue/common/consumer/QueueStateService.java @@ -26,7 +26,8 @@ import java.util.Set; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.locks.Lock; import java.util.concurrent.locks.ReentrantLock; -import java.util.stream.Collectors; + +import static org.thingsboard.server.common.msg.queue.TopicPartitionInfo.withTopic; @Slf4j public class QueueStateService { @@ -88,10 +89,6 @@ public class QueueStateService { initialized = true; } - private Set withTopic(Set partitions, String topic) { - return partitions.stream().map(tpi -> tpi.withTopic(topic)).collect(Collectors.toSet()); - } - public Set getPartitionsInProgress() { return initialized ? partitionsInProgress : null; } diff --git a/common/queue/src/main/java/org/thingsboard/server/queue/edqs/InMemoryEdqsQueueFactory.java b/common/queue/src/main/java/org/thingsboard/server/queue/edqs/InMemoryEdqsQueueFactory.java index 20b4beadcf..dc6e9f4791 100644 --- a/common/queue/src/main/java/org/thingsboard/server/queue/edqs/InMemoryEdqsQueueFactory.java +++ b/common/queue/src/main/java/org/thingsboard/server/queue/edqs/InMemoryEdqsQueueFactory.java @@ -18,7 +18,8 @@ package org.thingsboard.server.queue.edqs; import lombok.RequiredArgsConstructor; import org.springframework.stereotype.Component; import org.thingsboard.common.util.ThingsBoardExecutors; -import org.thingsboard.server.common.stats.DummyMessagesStats; +import org.thingsboard.server.common.stats.StatsFactory; +import org.thingsboard.server.common.stats.StatsType; import org.thingsboard.server.gen.transport.TransportProtos.FromEdqsMsg; import org.thingsboard.server.gen.transport.TransportProtos.ToEdqsMsg; import org.thingsboard.server.queue.TbQueueConsumer; @@ -37,6 +38,7 @@ public class InMemoryEdqsQueueFactory implements EdqsQueueFactory { private final InMemoryStorage storage; private final EdqsConfig edqsConfig; + private final StatsFactory statsFactory; @Override public TbQueueConsumer> createEdqsMsgConsumer(EdqsQueue queue) { @@ -69,7 +71,7 @@ public class InMemoryEdqsQueueFactory implements EdqsQueueFactory { .maxPendingRequests(edqsConfig.getMaxPendingRequests()) .requestTimeout(edqsConfig.getMaxRequestTimeout()) .pollInterval(edqsConfig.getPollInterval()) - .stats(new DummyMessagesStats()) // FIXME + .stats(statsFactory.createMessagesStats(StatsType.EDQS.getName())) .executor(ThingsBoardExecutors.newWorkStealingPool(5, "edqs")) .build(); } diff --git a/common/queue/src/main/java/org/thingsboard/server/queue/edqs/KafkaEdqsQueueFactory.java b/common/queue/src/main/java/org/thingsboard/server/queue/edqs/KafkaEdqsQueueFactory.java index 9f973ce3f1..9561849dd1 100644 --- a/common/queue/src/main/java/org/thingsboard/server/queue/edqs/KafkaEdqsQueueFactory.java +++ b/common/queue/src/main/java/org/thingsboard/server/queue/edqs/KafkaEdqsQueueFactory.java @@ -17,7 +17,8 @@ package org.thingsboard.server.queue.edqs; import org.springframework.stereotype.Component; import org.thingsboard.common.util.ThingsBoardExecutors; -import org.thingsboard.server.common.stats.DummyMessagesStats; +import org.thingsboard.server.common.stats.StatsFactory; +import org.thingsboard.server.common.stats.StatsType; import org.thingsboard.server.gen.transport.TransportProtos; import org.thingsboard.server.gen.transport.TransportProtos.FromEdqsMsg; import org.thingsboard.server.gen.transport.TransportProtos.ToEdqsMsg; @@ -49,12 +50,14 @@ public class KafkaEdqsQueueFactory implements EdqsQueueFactory { private final TbServiceInfoProvider serviceInfoProvider; private final TbKafkaConsumerStatsService consumerStatsService; private final TopicService topicService; + private final StatsFactory statsFactory; private final AtomicInteger consumerCounter = new AtomicInteger(); public KafkaEdqsQueueFactory(TbKafkaSettings kafkaSettings, TbKafkaTopicConfigs topicConfigs, EdqsConfig edqsConfig, TbServiceInfoProvider serviceInfoProvider, - TbKafkaConsumerStatsService consumerStatsService, TopicService topicService) { + TbKafkaConsumerStatsService consumerStatsService, TopicService topicService, + StatsFactory statsFactory) { this.edqsEventsAdmin = new TbKafkaAdmin(kafkaSettings, topicConfigs.getEdqsEventsConfigs()); this.edqsRequestsAdmin = new TbKafkaAdmin(kafkaSettings, topicConfigs.getEdqsRequestsConfigs()); this.edqsStateAdmin = new TbKafkaAdmin(kafkaSettings, topicConfigs.getEdqsStateConfigs()); @@ -63,6 +66,7 @@ public class KafkaEdqsQueueFactory implements EdqsQueueFactory { this.serviceInfoProvider = serviceInfoProvider; this.consumerStatsService = consumerStatsService; this.topicService = topicService; + this.statsFactory = statsFactory; } @Override @@ -117,7 +121,7 @@ public class KafkaEdqsQueueFactory implements EdqsQueueFactory { .maxPendingRequests(edqsConfig.getMaxPendingRequests()) .requestTimeout(edqsConfig.getMaxRequestTimeout()) .pollInterval(edqsConfig.getPollInterval()) - .stats(new DummyMessagesStats()) // FIXME + .stats(statsFactory.createMessagesStats(StatsType.EDQS.getName())) .executor(ThingsBoardExecutors.newWorkStealingPool(5, "edqs")) .build(); } diff --git a/common/stats/src/main/java/org/thingsboard/server/common/stats/DefaultStatsFactory.java b/common/stats/src/main/java/org/thingsboard/server/common/stats/DefaultStatsFactory.java index e1940e55fe..ca97792186 100644 --- a/common/stats/src/main/java/org/thingsboard/server/common/stats/DefaultStatsFactory.java +++ b/common/stats/src/main/java/org/thingsboard/server/common/stats/DefaultStatsFactory.java @@ -38,7 +38,7 @@ public class DefaultStatsFactory implements StatsFactory { private static final Counter STUB_COUNTER = new StubCounter(); - @Autowired // FIXME Slavik !!! + @Autowired private MeterRegistry meterRegistry; @Value("${metrics.enabled:false}") From fb161730fb28fd4d53907426dd2038e5401f4b48 Mon Sep 17 00:00:00 2001 From: dashevchenko Date: Fri, 21 Feb 2025 18:38:26 +0200 Subject: [PATCH 083/345] added msa tests for edqs --- .../controller/EntityQueryController.java | 7 + .../entitiy/EdqsEntityServiceTest.java | 49 +++++ .../service/entitiy/EntityServiceTest.java | 3 +- .../common/data/edqs/fields/FieldsUtil.java | 4 +- .../edqs/repo/InMemoryEdqRepository.java | 9 +- .../server/dao/entity/BaseEntityService.java | 4 +- .../dao/sql/query/DummyEdqsService.java | 1 + docker/.env | 1 + docker/compose-utils.sh | 12 ++ docker/docker-compose.edqs.yml | 33 +++ docker/docker-remove-services.sh | 4 +- docker/docker-start-services.sh | 4 +- docker/docker-stop-services.sh | 4 +- docker/edqs.env | 7 + docker/tb-node.env | 2 + edqs/src/main/resources/edqs.yml | 5 + .../server/msa/ContainerTestSuite.java | 11 +- .../server/msa/TestRestClient.java | 100 ++++++++- .../server/msa/ThingsBoardDbInstaller.java | 3 +- .../msa/edqs/EdqsEntityDataQueryTest.java | 202 ++++++++++++++++++ .../server/msa/ui/utils/EntityPrototypes.java | 32 +++ 21 files changed, 477 insertions(+), 20 deletions(-) create mode 100644 docker/docker-compose.edqs.yml create mode 100644 docker/edqs.env create mode 100644 msa/black-box-tests/src/test/java/org/thingsboard/server/msa/edqs/EdqsEntityDataQueryTest.java diff --git a/application/src/main/java/org/thingsboard/server/controller/EntityQueryController.java b/application/src/main/java/org/thingsboard/server/controller/EntityQueryController.java index f28d485b30..9c2c70d019 100644 --- a/application/src/main/java/org/thingsboard/server/controller/EntityQueryController.java +++ b/application/src/main/java/org/thingsboard/server/controller/EntityQueryController.java @@ -20,6 +20,7 @@ import io.swagger.v3.oas.annotations.media.Schema; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.http.ResponseEntity; import org.springframework.security.access.prepost.PreAuthorize; +import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.RequestBody; import org.springframework.web.bind.annotation.RequestMapping; @@ -144,4 +145,10 @@ public class EntityQueryController extends BaseController { edqsService.processSystemRequest(request); } + @PreAuthorize("hasAnyAuthority('SYS_ADMIN')") + @GetMapping("/edqs/enabled") + public boolean isEdqsApiEnabled() { + return edqsService.isApiEnabled(); + } + } diff --git a/application/src/test/java/org/thingsboard/server/service/entitiy/EdqsEntityServiceTest.java b/application/src/test/java/org/thingsboard/server/service/entitiy/EdqsEntityServiceTest.java index cb377e0431..61f246ca3e 100644 --- a/application/src/test/java/org/thingsboard/server/service/entitiy/EdqsEntityServiceTest.java +++ b/application/src/test/java/org/thingsboard/server/service/entitiy/EdqsEntityServiceTest.java @@ -15,20 +15,35 @@ */ package org.thingsboard.server.service.entitiy; +import com.google.common.collect.Lists; import org.junit.Before; +import org.junit.Test; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.mock.mockito.MockBean; import org.springframework.test.context.TestPropertySource; +import org.thingsboard.server.common.data.EntityType; +import org.thingsboard.server.common.data.asset.Asset; import org.thingsboard.server.common.data.id.CustomerId; +import org.thingsboard.server.common.data.id.IdBased; import org.thingsboard.server.common.data.page.PageData; import org.thingsboard.server.common.data.query.EntityCountQuery; import org.thingsboard.server.common.data.query.EntityData; import org.thingsboard.server.common.data.query.EntityDataQuery; +import org.thingsboard.server.common.data.query.RelationsQueryFilter; +import org.thingsboard.server.common.data.relation.EntitySearchDirection; +import org.thingsboard.server.common.data.relation.RelationEntityTypeFilter; import org.thingsboard.server.common.msg.edqs.EdqsService; import org.thingsboard.server.dao.service.DaoSqlTest; import org.thingsboard.server.edqs.util.EdqsRocksDb; +import java.util.ArrayList; +import java.util.Collections; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.UUID; import java.util.concurrent.TimeUnit; +import java.util.stream.Collectors; import static org.awaitility.Awaitility.await; @@ -51,6 +66,40 @@ public class EdqsEntityServiceTest extends EntityServiceTest { await().atMost(TIMEOUT, TimeUnit.SECONDS).until(() -> edqsService.isApiEnabled()); } + // sql implementation has a bug with data duplication, edqs implementation returns correct value + @Override + @Test + public void testCountHierarchicalEntitiesByMultiRootQuery() throws InterruptedException { + List buildings = new ArrayList<>(); + List apartments = new ArrayList<>(); + Map> entityNameByTypeMap = new HashMap<>(); + Map childParentRelationMap = new HashMap<>(); + createMultiRootHierarchy(buildings, apartments, entityNameByTypeMap, childParentRelationMap); + + RelationsQueryFilter filter = new RelationsQueryFilter(); + filter.setMultiRoot(true); + filter.setMultiRootEntitiesType(EntityType.ASSET); + filter.setMultiRootEntityIds(buildings.stream().map(IdBased::getId).map(d -> d.getId().toString()).collect(Collectors.toSet())); + filter.setDirection(EntitySearchDirection.FROM); + + EntityCountQuery countQuery = new EntityCountQuery(filter); + countByQueryAndCheck(countQuery, 63); + + filter.setFilters(Collections.singletonList(new RelationEntityTypeFilter("AptToHeat", Collections.singletonList(EntityType.DEVICE)))); + countByQueryAndCheck(countQuery, 27); + + filter.setMultiRootEntitiesType(EntityType.ASSET); + filter.setMultiRootEntityIds(apartments.stream().map(IdBased::getId).map(d -> d.getId().toString()).collect(Collectors.toSet())); + filter.setDirection(EntitySearchDirection.TO); + filter.setFilters(Lists.newArrayList( + new RelationEntityTypeFilter("buildingToApt", Collections.singletonList(EntityType.ASSET)), + new RelationEntityTypeFilter("AptToEnergy", Collections.singletonList(EntityType.DEVICE)))); + countByQueryAndCheck(countQuery, 3); + + deviceService.deleteDevicesByTenantId(tenantId); + assetService.deleteAssetsByTenantId(tenantId); + } + @Override protected PageData findByQueryAndCheck(CustomerId customerId, EntityDataQuery query, long expectedResultSize) { return await().atMost(15, TimeUnit.SECONDS).until(() -> findByQuery(customerId, query), 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 a559eca6ce..44e636b04f 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 @@ -462,7 +462,6 @@ public class EntityServiceTest extends AbstractControllerTest { deviceService.deleteDevicesByTenantId(tenantId); } - // fails for sql implementation until we fix the issue with the relation query @Test public void testCountHierarchicalEntitiesByMultiRootQuery() throws InterruptedException { List buildings = new ArrayList<>(); @@ -489,7 +488,7 @@ public class EntityServiceTest extends AbstractControllerTest { filter.setFilters(Lists.newArrayList( new RelationEntityTypeFilter("buildingToApt", Collections.singletonList(EntityType.ASSET)), new RelationEntityTypeFilter("AptToEnergy", Collections.singletonList(EntityType.DEVICE)))); - countByQueryAndCheck(countQuery, 3); + countByQueryAndCheck(countQuery, 9); deviceService.deleteDevicesByTenantId(tenantId); assetService.deleteAssetsByTenantId(tenantId); diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/edqs/fields/FieldsUtil.java b/common/data/src/main/java/org/thingsboard/server/common/data/edqs/fields/FieldsUtil.java index 6543a6d779..8d939e07e5 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/edqs/fields/FieldsUtil.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/edqs/fields/FieldsUtil.java @@ -240,7 +240,7 @@ public class FieldsUtil { .build(); } - private static AssetProfileFields toFields(DeviceProfile entity) { + private static AssetProfileFields toFields(AssetProfile entity) { return AssetProfileFields.builder() .id(entity.getUuidId()) .createdTime(entity.getCreatedTime()) @@ -250,7 +250,7 @@ public class FieldsUtil { .build(); } - private static DeviceProfileFields toFields(AssetProfile entity) { + private static DeviceProfileFields toFields(DeviceProfile entity) { return DeviceProfileFields.builder() .id(entity.getUuidId()) .createdTime(entity.getCreatedTime()) diff --git a/common/edqs/src/main/java/org/thingsboard/server/edqs/repo/InMemoryEdqRepository.java b/common/edqs/src/main/java/org/thingsboard/server/edqs/repo/InMemoryEdqRepository.java index 7736f303ac..1b6903fc3c 100644 --- a/common/edqs/src/main/java/org/thingsboard/server/edqs/repo/InMemoryEdqRepository.java +++ b/common/edqs/src/main/java/org/thingsboard/server/edqs/repo/InMemoryEdqRepository.java @@ -61,14 +61,7 @@ public class InMemoryEdqRepository implements EdqRepository { @Override public long countEntitiesByQuery(TenantId tenantId, CustomerId customerId, EntityCountQuery query, boolean ignorePermissionCheck) { long startNs = System.nanoTime(); - long result = 0; - if (TenantId.SYS_TENANT_ID.equals(tenantId)) { - for (TenantRepo repo : repos.values()) { - result += repo.countEntitiesByQuery(customerId, query, ignorePermissionCheck); - } - } else { - result = get(tenantId).countEntitiesByQuery(customerId, query, ignorePermissionCheck); - } + long result = get(tenantId).countEntitiesByQuery(customerId, query, ignorePermissionCheck); double timingMs = (double) (System.nanoTime() - startNs) / 1000_000; log.info("countEntitiesByQuery: {} ms", timingMs); return result; diff --git a/dao/src/main/java/org/thingsboard/server/dao/entity/BaseEntityService.java b/dao/src/main/java/org/thingsboard/server/dao/entity/BaseEntityService.java index 3f941b776c..7705614a05 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/entity/BaseEntityService.java +++ b/dao/src/main/java/org/thingsboard/server/dao/entity/BaseEntityService.java @@ -95,7 +95,7 @@ public class BaseEntityService extends AbstractEntityService implements EntitySe validateId(customerId, id -> INCORRECT_CUSTOMER_ID + id); validateEntityCountQuery(query); - if (edqsService.isApiEnabled() && validForEdqs(query)) { // TODO: separate boolean param whether to use in dashboards; but sync to edqs - always + if (edqsService.isApiEnabled() && validForEdqs(query) && !tenantId.isSysTenantId()) { // TODO: separate boolean param whether to use in dashboards; but sync to edqs - always EdqsRequest request = EdqsRequest.builder() .entityCountQuery(query) .build(); @@ -112,7 +112,7 @@ public class BaseEntityService extends AbstractEntityService implements EntitySe validateId(customerId, id -> INCORRECT_CUSTOMER_ID + id); validateEntityDataQuery(query); - if (edqsService.isApiEnabled() && validForEdqs(query)) { // TODO: separate boolean param whether to use in dashboards; but sync to edqs - always + if (edqsService.isApiEnabled() && validForEdqs(query) && !tenantId.isSysTenantId()) { // TODO: separate boolean param whether to use in dashboards; but sync to edqs - always EdqsRequest request = EdqsRequest.builder() .entityDataQuery(query) .build(); diff --git a/dao/src/main/java/org/thingsboard/server/dao/sql/query/DummyEdqsService.java b/dao/src/main/java/org/thingsboard/server/dao/sql/query/DummyEdqsService.java index b69aa52ec5..14ab995a45 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/sql/query/DummyEdqsService.java +++ b/dao/src/main/java/org/thingsboard/server/dao/sql/query/DummyEdqsService.java @@ -16,6 +16,7 @@ package org.thingsboard.server.dao.sql.query; import com.google.common.util.concurrent.ListenableFuture; +import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; import org.springframework.stereotype.Service; import org.thingsboard.server.common.data.ObjectType; diff --git a/docker/.env b/docker/.env index 37c9768296..38724263fc 100644 --- a/docker/.env +++ b/docker/.env @@ -14,6 +14,7 @@ COAP_TRANSPORT_DOCKER_NAME=tb-coap-transport LWM2M_TRANSPORT_DOCKER_NAME=tb-lwm2m-transport SNMP_TRANSPORT_DOCKER_NAME=tb-snmp-transport TB_VC_EXECUTOR_DOCKER_NAME=tb-vc-executor +EDQS_DOCKER_NAME=edqs TB_VERSION=latest diff --git a/docker/compose-utils.sh b/docker/compose-utils.sh index aa5a9db08d..9411c22b7e 100755 --- a/docker/compose-utils.sh +++ b/docker/compose-utils.sh @@ -128,6 +128,18 @@ function additionalStartupServices() { echo $ADDITIONAL_STARTUP_SERVICES } +function additionalComposeEdqsArgs() { + source .env + + if [ "$EDQS_ENABLED" = true ] + then + ADDITIONAL_COMPOSE_EDQS_ARGS="-f docker-compose.edqs.yml" + echo ADDITIONAL_COMPOSE_EDQS_ARGS + else + echo "" + fi +} + function permissionList() { PERMISSION_LIST=" 799 799 tb-node/log diff --git a/docker/docker-compose.edqs.yml b/docker/docker-compose.edqs.yml new file mode 100644 index 0000000000..f989c12a2e --- /dev/null +++ b/docker/docker-compose.edqs.yml @@ -0,0 +1,33 @@ +# +# Copyright © 2016-2024 The Thingsboard Authors +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +version: '3.0' + +services: + edqs-1: + restart: always + image: "${DOCKER_REPO}/${EDQS_DOCKER_NAME}:${TB_VERSION}" + env_file: + - edqs.env + depends_on: + - zookeeper + edqs-2: + restart: always + image: "${DOCKER_REPO}/${EDQS_DOCKER_NAME}:${TB_VERSION}" + env_file: + - edqs.env + depends_on: + - zookeeper diff --git a/docker/docker-remove-services.sh b/docker/docker-remove-services.sh index c77499f833..d7fd891925 100755 --- a/docker/docker-remove-services.sh +++ b/docker/docker-remove-services.sh @@ -29,8 +29,10 @@ ADDITIONAL_CACHE_ARGS=$(additionalComposeCacheArgs) || exit $? ADDITIONAL_COMPOSE_MONITORING_ARGS=$(additionalComposeMonitoringArgs) || exit $? +ADDITIONAL_COMPOSE_EDQS_ARGS=$(additionalComposeEdqsArgs) || exit $? + COMPOSE_ARGS="\ - -f docker-compose.yml ${ADDITIONAL_CACHE_ARGS} ${ADDITIONAL_COMPOSE_ARGS} ${ADDITIONAL_COMPOSE_QUEUE_ARGS} ${ADDITIONAL_COMPOSE_MONITORING_ARGS} \ + -f docker-compose.yml ${ADDITIONAL_CACHE_ARGS} ${ADDITIONAL_COMPOSE_ARGS} ${ADDITIONAL_COMPOSE_QUEUE_ARGS} ${ADDITIONAL_COMPOSE_MONITORING_ARGS} ${ADDITIONAL_COMPOSE_EDQS_ARGS} \ down -v" case $COMPOSE_VERSION in diff --git a/docker/docker-start-services.sh b/docker/docker-start-services.sh index 42af042108..3d4479c001 100755 --- a/docker/docker-start-services.sh +++ b/docker/docker-start-services.sh @@ -29,10 +29,12 @@ ADDITIONAL_CACHE_ARGS=$(additionalComposeCacheArgs) || exit $? ADDITIONAL_COMPOSE_MONITORING_ARGS=$(additionalComposeMonitoringArgs) || exit $? +ADDITIONAL_COMPOSE_EDQS_ARGS=$(additionalComposeEdqsArgs) || exit $? + checkFolders --create || exit $? COMPOSE_ARGS="\ - -f docker-compose.yml ${ADDITIONAL_CACHE_ARGS} ${ADDITIONAL_COMPOSE_ARGS} ${ADDITIONAL_COMPOSE_QUEUE_ARGS} ${ADDITIONAL_COMPOSE_MONITORING_ARGS} \ + -f docker-compose.yml ${ADDITIONAL_CACHE_ARGS} ${ADDITIONAL_COMPOSE_ARGS} ${ADDITIONAL_COMPOSE_QUEUE_ARGS} ${ADDITIONAL_COMPOSE_MONITORING_ARGS} ${ADDITIONAL_COMPOSE_EDQS_ARGS} \ up -d" case $COMPOSE_VERSION in diff --git a/docker/docker-stop-services.sh b/docker/docker-stop-services.sh index 2a753cc348..b7037fce69 100755 --- a/docker/docker-stop-services.sh +++ b/docker/docker-stop-services.sh @@ -29,8 +29,10 @@ ADDITIONAL_CACHE_ARGS=$(additionalComposeCacheArgs) || exit $? ADDITIONAL_COMPOSE_MONITORING_ARGS=$(additionalComposeMonitoringArgs) || exit $? +ADDITIONAL_COMPOSE_EDQS_ARGS=$(additionalComposeEdqsArgs) || exit $? + COMPOSE_ARGS="\ - -f docker-compose.yml ${ADDITIONAL_CACHE_ARGS} ${ADDITIONAL_COMPOSE_ARGS} ${ADDITIONAL_COMPOSE_QUEUE_ARGS} ${ADDITIONAL_COMPOSE_MONITORING_ARGS} \ + -f docker-compose.yml ${ADDITIONAL_CACHE_ARGS} ${ADDITIONAL_COMPOSE_ARGS} ${ADDITIONAL_COMPOSE_QUEUE_ARGS} ${ADDITIONAL_COMPOSE_MONITORING_ARGS} ${ADDITIONAL_COMPOSE_EDQS_ARGS}\ stop" case $COMPOSE_VERSION in diff --git a/docker/edqs.env b/docker/edqs.env new file mode 100644 index 0000000000..3c91c92e54 --- /dev/null +++ b/docker/edqs.env @@ -0,0 +1,7 @@ +ZOOKEEPER_ENABLED=true +ZOOKEEPER_URL=zookeeper:2181 +TB_KAFKA_SERVERS=kafka:9092 + +TB_EDQS_STATS_ENABLED=false +METRICS_ENABLED=true +METRICS_ENDPOINTS_EXPOSE=prometheus diff --git a/docker/tb-node.env b/docker/tb-node.env index 85a60eb51a..d40ca66621 100644 --- a/docker/tb-node.env +++ b/docker/tb-node.env @@ -9,3 +9,5 @@ HTTP_LOG_CONTROLLER_ERROR_STACK_TRACE=false METRICS_ENABLED=true METRICS_ENDPOINTS_EXPOSE=prometheus + +TB_EDQS_MODE=remote \ No newline at end of file diff --git a/edqs/src/main/resources/edqs.yml b/edqs/src/main/resources/edqs.yml index b67f2f5f9e..a55391451e 100644 --- a/edqs/src/main/resources/edqs.yml +++ b/edqs/src/main/resources/edqs.yml @@ -60,6 +60,11 @@ queue: poll_interval: "${TB_EDQS_POLL_INTERVAL_MS:125}" max_pending_requests: "${TB_EDQS_MAX_PENDING_REQUESTS:10000}" max_request_timeout: "${TB_EDQS_MAX_REQUEST_TIMEOUT:10000}" + stats: + # Enable/disable statistics for EDQS service + enabled: "${TB_EDQS_STATS_ENABLED:true}" + # Statistics printing interval for EDQS + print-interval-ms: "${TB_EDQS_STATS_PRINT_INTERVAL_MS:60000}" kafka: # Kafka Bootstrap nodes in "host:port" format bootstrap.servers: "${TB_KAFKA_SERVERS:localhost:9092}" diff --git a/msa/black-box-tests/src/test/java/org/thingsboard/server/msa/ContainerTestSuite.java b/msa/black-box-tests/src/test/java/org/thingsboard/server/msa/ContainerTestSuite.java index bbaeabe907..d22861e286 100644 --- a/msa/black-box-tests/src/test/java/org/thingsboard/server/msa/ContainerTestSuite.java +++ b/msa/black-box-tests/src/test/java/org/thingsboard/server/msa/ContainerTestSuite.java @@ -114,6 +114,7 @@ public class ContainerTestSuite { List composeFiles = new ArrayList<>(Arrays.asList( new File(targetDir + "docker-compose.yml"), + new File(targetDir + "docker-compose.edqs.yml"), new File(targetDir + "docker-compose.volumes.yml"), new File(targetDir + "docker-compose.mosquitto.yml"), new File(targetDir + (IS_HYBRID_MODE ? "docker-compose.hybrid.yml" : "docker-compose.postgres.yml")), @@ -162,6 +163,12 @@ public class ContainerTestSuite { composeFiles.add(new File(targetDir + "docker-compose.cassandra.volumes.yml")); } + // to trigger edqs synchronization + if (true) { + addToFile(targetDir, "tb-node.env", + Map.of("TB_EDQS_SYNC_ENABLED", "true")); + } + testContainer = new DockerComposeContainerImpl<>(composeFiles) .withPull(false) .withLocalCompose(true) @@ -180,7 +187,9 @@ public class ContainerTestSuite { .waitingFor("tb-mqtt-transport2", Wait.forLogMessage(TRANSPORTS_LOG_REGEXP, 1).withStartupTimeout(CONTAINER_STARTUP_TIMEOUT)) .waitingFor("tb-vc-executor1", Wait.forLogMessage(TB_VC_LOG_REGEXP, 1).withStartupTimeout(CONTAINER_STARTUP_TIMEOUT)) .waitingFor("tb-vc-executor2", Wait.forLogMessage(TB_VC_LOG_REGEXP, 1).withStartupTimeout(CONTAINER_STARTUP_TIMEOUT)) - .waitingFor("tb-js-executor", Wait.forLogMessage(TB_JS_EXECUTOR_LOG_REGEXP, 1).withStartupTimeout(CONTAINER_STARTUP_TIMEOUT)); + .waitingFor("tb-js-executor", Wait.forLogMessage(TB_JS_EXECUTOR_LOG_REGEXP, 1).withStartupTimeout(CONTAINER_STARTUP_TIMEOUT)) + .waitingFor("edqs-1", Wait.forHttp("/api/edqs/ready").withStartupTimeout(CONTAINER_STARTUP_TIMEOUT)) + .waitingFor("edqs-2", Wait.forHttp("/api/edqs/ready").withStartupTimeout(CONTAINER_STARTUP_TIMEOUT)); testContainer.start(); setActive(true); } catch (Exception e) { diff --git a/msa/black-box-tests/src/test/java/org/thingsboard/server/msa/TestRestClient.java b/msa/black-box-tests/src/test/java/org/thingsboard/server/msa/TestRestClient.java index d137f8ed59..3c61c65ef1 100644 --- a/msa/black-box-tests/src/test/java/org/thingsboard/server/msa/TestRestClient.java +++ b/msa/black-box-tests/src/test/java/org/thingsboard/server/msa/TestRestClient.java @@ -28,6 +28,7 @@ import io.restassured.internal.ValidatableResponseImpl; import io.restassured.path.json.JsonPath; import io.restassured.response.ValidatableResponse; import io.restassured.specification.RequestSpecification; +import org.thingsboard.common.util.JacksonUtil; import org.thingsboard.server.common.data.Customer; import org.thingsboard.server.common.data.Dashboard; import org.thingsboard.server.common.data.Device; @@ -35,6 +36,7 @@ import org.thingsboard.server.common.data.DeviceProfile; import org.thingsboard.server.common.data.EntityView; import org.thingsboard.server.common.data.EventInfo; import org.thingsboard.server.common.data.TbResource; +import org.thingsboard.server.common.data.Tenant; import org.thingsboard.server.common.data.User; import org.thingsboard.server.common.data.alarm.Alarm; import org.thingsboard.server.common.data.asset.Asset; @@ -56,6 +58,9 @@ import org.thingsboard.server.common.data.id.UserId; import org.thingsboard.server.common.data.page.PageData; import org.thingsboard.server.common.data.page.PageLink; import org.thingsboard.server.common.data.page.TimePageLink; +import org.thingsboard.server.common.data.query.EntityCountQuery; +import org.thingsboard.server.common.data.query.EntityData; +import org.thingsboard.server.common.data.query.EntityDataQuery; import org.thingsboard.server.common.data.relation.EntityRelation; import org.thingsboard.server.common.data.relation.RelationTypeGroup; import org.thingsboard.server.common.data.rpc.Rpc; @@ -66,7 +71,6 @@ import org.thingsboard.server.common.data.security.DeviceCredentials; import java.util.HashMap; import java.util.List; import java.util.Map; -import java.util.UUID; import static io.restassured.RestAssured.given; import static java.net.HttpURLConnection.HTTP_BAD_REQUEST; @@ -110,6 +114,37 @@ public class TestRestClient { requestSpec.header(JWT_TOKEN_HEADER_PARAM, "Bearer " + token); } + public JsonNode getActivateRequest(String password) { + ObjectNode response = given().spec(requestSpec).get("/api/noauth/activate?activateToken={activateToken}", token) + .then() + .statusCode(HTTP_OK) + .extract() + .as(ObjectNode.class); + return JacksonUtil.newObjectNode() + .put("activateToken", this.token) + .put("password", password); + } + + public void activateAndLoginAsUser(JsonNode activateRequest) { + ObjectNode tokenInfo = given().spec(requestSpec).body(activateRequest) + .post("/api/noauth/activate") + .then() + .extract() + .as(ObjectNode.class); + token = tokenInfo.get("token").asText(); + refreshToken = tokenInfo.get("refreshToken").asText(); + requestSpec.header(JWT_TOKEN_HEADER_PARAM, "Bearer " + token); + } + + public Tenant postTenant(Tenant tenant) { + return given().spec(requestSpec).body(tenant) + .post("/api/tenant") + .then() + .statusCode(HTTP_OK) + .extract() + .as(Tenant.class); + } + public Device postDevice(String accessToken, Device device) { return given().spec(requestSpec).body(device) .pathParams("accessToken", accessToken) @@ -479,6 +514,28 @@ public class TestRestClient { .as(User.class); } + public UserId createUserAndLogin(User user, String password) { + UserId userId = postUser(user).getId(); + getUserToken(userId.getId().toString()); + return userId; + } + + public void getUserToken(String id) { + ObjectNode tokenInfo = given().spec(requestSpec) + .get("/api/user/" + id + "/token") + .then() + .extract() + .as(ObjectNode.class); + token = tokenInfo.get("token").asText(); + refreshToken = tokenInfo.get("refreshToken").asText(); + requestSpec.header(JWT_TOKEN_HEADER_PARAM, "Bearer " + token); + } + + protected void resetTokens() { + this.token = null; + this.refreshToken = null; + } + public void deleteUser(UserId userId) { given().spec(requestSpec) .delete("/api/user/{userId}", userId.getId()) @@ -643,4 +700,45 @@ public class TestRestClient { } return urlParams; } + + public PageData postEntityDataQuery(EntityDataQuery entityDataQuery) { + return given().spec(requestSpec).body(entityDataQuery) + .post("/api/entitiesQuery/find") + .then() + .statusCode(HTTP_OK) + .extract() + .as(new TypeRef<>() {}); + } + + public Long postCountDataQuery(EntityCountQuery entityCountQuery) { + return given().spec(requestSpec).body(entityCountQuery) + .post("/api/entitiesQuery/count") + .then() + .statusCode(HTTP_OK) + .extract() + .as(Long.class); + } + + public Boolean isEdqsApiEnabled() { + return given().spec(requestSpec) + .get("/api/edqs/enabled") + .then() + .statusCode(HTTP_OK) + .extract() + .as(Boolean.class); + } + + public void assignDeviceToCustomer(CustomerId customerId, DeviceId id) { + given().spec(requestSpec) + .post("/api/customer/" + customerId.getId().toString() + "/device/" + id.getId().toString()) + .then() + .statusCode(HTTP_OK); + } + + public void deleteTenant(TenantId tenantId) { + given().spec(requestSpec) + .delete("/api/tenant/" + tenantId.getId().toString()) + .then() + .statusCode(HTTP_OK); + } } diff --git a/msa/black-box-tests/src/test/java/org/thingsboard/server/msa/ThingsBoardDbInstaller.java b/msa/black-box-tests/src/test/java/org/thingsboard/server/msa/ThingsBoardDbInstaller.java index aaa4f001c6..75f9779916 100644 --- a/msa/black-box-tests/src/test/java/org/thingsboard/server/msa/ThingsBoardDbInstaller.java +++ b/msa/black-box-tests/src/test/java/org/thingsboard/server/msa/ThingsBoardDbInstaller.java @@ -220,7 +220,8 @@ public class ThingsBoardDbInstaller { dockerCompose.withCommand("up -d postgres" + additionalServices); dockerCompose.invokeCompose(); - dockerCompose.withCommand("run --no-deps --rm -e INSTALL_TB=true -e LOAD_DEMO=true tb-core1"); + dockerCompose.withCommand("run --no-deps --rm -e INSTALL_TB=true -e LOAD_DEMO=true -e TB_EDQS_SYNC_ENABLED=false " + + "tb-core1"); dockerCompose.invokeCompose(); } finally { diff --git a/msa/black-box-tests/src/test/java/org/thingsboard/server/msa/edqs/EdqsEntityDataQueryTest.java b/msa/black-box-tests/src/test/java/org/thingsboard/server/msa/edqs/EdqsEntityDataQueryTest.java new file mode 100644 index 0000000000..1317011cf0 --- /dev/null +++ b/msa/black-box-tests/src/test/java/org/thingsboard/server/msa/edqs/EdqsEntityDataQueryTest.java @@ -0,0 +1,202 @@ +/** + * Copyright © 2016-2024 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.msa.edqs; + +import com.fasterxml.jackson.databind.node.ObjectNode; +import org.testcontainers.shaded.org.apache.commons.lang3.RandomStringUtils; +import org.testng.annotations.AfterClass; +import org.testng.annotations.BeforeClass; +import org.testng.annotations.BeforeMethod; +import org.testng.annotations.Test; +import org.thingsboard.server.common.data.Device; +import org.thingsboard.server.common.data.EntityType; +import org.thingsboard.server.common.data.StringUtils; +import org.thingsboard.server.common.data.id.CustomerId; +import org.thingsboard.server.common.data.id.TenantId; +import org.thingsboard.server.common.data.id.UserId; +import org.thingsboard.server.common.data.page.PageData; +import org.thingsboard.server.common.data.query.DeviceTypeFilter; +import org.thingsboard.server.common.data.query.EntityCountQuery; +import org.thingsboard.server.common.data.query.EntityData; +import org.thingsboard.server.common.data.query.EntityDataPageLink; +import org.thingsboard.server.common.data.query.EntityDataQuery; +import org.thingsboard.server.common.data.query.EntityDataSortOrder; +import org.thingsboard.server.common.data.query.EntityKey; +import org.thingsboard.server.common.data.query.EntityKeyType; +import org.thingsboard.server.common.data.query.EntityTypeFilter; +import org.thingsboard.server.msa.AbstractContainerTest; +import org.thingsboard.server.msa.DisableUIListeners; +import org.thingsboard.server.msa.ui.utils.EntityPrototypes; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.concurrent.TimeUnit; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.awaitility.Awaitility.await; +import static org.thingsboard.server.msa.ui.utils.EntityPrototypes.defaultCustomer; +import static org.thingsboard.server.msa.ui.utils.EntityPrototypes.defaultCustomerAdmin; +import static org.thingsboard.server.msa.ui.utils.EntityPrototypes.defaultDeviceProfile; +import static org.thingsboard.server.msa.ui.utils.EntityPrototypes.defaultTenantAdmin; + +@DisableUIListeners +public class EdqsEntityDataQueryTest extends AbstractContainerTest { + + private TenantId tenantId; + private CustomerId customerId; + private TenantId tenantId2; + private CustomerId customerId2; + private UserId tenantAdminId; + private UserId customerUserId; + private UserId tenant2AdminId; + private UserId customer2UserId; + private final List tenantDevices = new ArrayList<>(); + private final List tenant2Devices = new ArrayList<>(); + private final String deviceProfile = "LoRa-" + RandomStringUtils.randomAlphabetic(10); + + @BeforeClass + public void beforeClass() throws Exception { + testRestClient.login("sysadmin@thingsboard.org", "sysadmin"); + await().atMost(TIMEOUT, TimeUnit.SECONDS).until(() -> testRestClient.isEdqsApiEnabled()); + + tenantId = testRestClient.postTenant(EntityPrototypes.defaultTenantPrototype("Tenant")).getId(); + tenantAdminId = testRestClient.createUserAndLogin(defaultTenantAdmin(tenantId, "tenantAdmin@thingsboard.org"), "tenant"); + testRestClient.postDeviceProfile(defaultDeviceProfile(deviceProfile)); + createDevices(deviceProfile, tenantDevices, 97); + customerId = testRestClient.postCustomer(defaultCustomer(tenantId, "Customer")).getId(); + customerUserId = testRestClient.postUser(defaultCustomerAdmin(tenantId, customerId, "customerUser@thingsboard.org")).getId(); + assignDevicesToCustomer(customerId, tenantDevices, 12); + + testRestClient.login("sysadmin@thingsboard.org", "sysadmin"); + tenantId2 = testRestClient.postTenant(EntityPrototypes.defaultTenantPrototype("Tenant")).getId(); + tenant2AdminId = testRestClient.createUserAndLogin(defaultTenantAdmin(tenantId2, "tenant2Admin@thingsboard.org"), "tenant"); + testRestClient.postDeviceProfile(defaultDeviceProfile(deviceProfile)); + createDevices(deviceProfile, tenant2Devices, 97); + customerId2 = testRestClient.postCustomer(defaultCustomer(tenantId2, "Customer")).getId(); + customer2UserId = testRestClient.postUser(defaultCustomerAdmin(tenantId2, customerId2, "customer2User@thingsboard.org")).getId(); + assignDevicesToCustomer(customerId2, tenant2Devices, 12); + } + + @BeforeMethod + public void beforeMethod() { + testRestClient.login("sysadmin@thingsboard.org", "sysadmin"); + } + + @AfterClass + public void afterClass() { + testRestClient.login("sysadmin@thingsboard.org", "sysadmin"); + testRestClient.deleteTenant(tenantId); + testRestClient.deleteTenant(tenantId2); + } + + @Test + public void testSysAdminCountEntitiesByQuery() { + EntityTypeFilter allDeviceFilter = new EntityTypeFilter(); + allDeviceFilter.setEntityType(EntityType.DEVICE); + EntityCountQuery query = new EntityCountQuery(allDeviceFilter); + await("Waiting for total device count") + .atMost(30, TimeUnit.SECONDS) + .until(() -> testRestClient.postCountDataQuery(query).compareTo(97L * 2) >= 0); + + testRestClient.getUserToken(tenantAdminId.getId().toString()); + await("Waiting for total device count") + .atMost(30, TimeUnit.SECONDS) + .until(() -> testRestClient.postCountDataQuery(query).equals(97L)); + + testRestClient.login("sysadmin@thingsboard.org", "sysadmin"); + testRestClient.getUserToken(tenant2AdminId.getId().toString()); + await("Waiting for total device count") + .atMost(30, TimeUnit.SECONDS) + .until(() -> testRestClient.postCountDataQuery(query).equals(97L)); + } + + @Test + public void testRetrieveTenantDevicesByDeviceTypeFilter() { + // login tenant admin + testRestClient.getUserToken(tenantAdminId.getId().toString()); + checkUserDevices(tenantDevices); + + // login customer user + testRestClient.getUserToken(customerUserId.getId().toString()); + checkUserDevices(tenantDevices.subList(0, 12)); + + // login other tenant admin + testRestClient.login("sysadmin@thingsboard.org", "sysadmin"); + testRestClient.getUserToken(tenant2AdminId.getId().toString()); + checkUserDevices(tenant2Devices); + } + + private void checkUserDevices(List devices) { + DeviceTypeFilter filter = new DeviceTypeFilter(); + filter.setDeviceTypes(List.of(deviceProfile)); + filter.setDeviceNameFilter(""); + + EntityDataSortOrder sortOrder = new EntityDataSortOrder(new EntityKey(EntityKeyType.ENTITY_FIELD, "createdTime"), EntityDataSortOrder.Direction.ASC); + EntityDataPageLink pageLink = new EntityDataPageLink(10, 0, null, sortOrder); + List entityFields = Collections.singletonList(new EntityKey(EntityKeyType.ENTITY_FIELD, "name")); + List latestFields = Collections.singletonList(new EntityKey(EntityKeyType.TIME_SERIES, "temperature")); + EntityDataQuery query = new EntityDataQuery(filter, pageLink, entityFields, latestFields, null); + + EntityTypeFilter allDeviceFilter = new EntityTypeFilter(); + allDeviceFilter.setEntityType(EntityType.DEVICE); + EntityCountQuery countQuery = new EntityCountQuery(allDeviceFilter); + await("Waiting for total device count") + .atMost(30, TimeUnit.SECONDS) + .until(() -> testRestClient.postCountDataQuery(countQuery).intValue() == devices.size()); + + PageData result = testRestClient.postEntityDataQuery(query); + assertThat(result.getTotalElements()).isEqualTo(devices.size()); + List retrievedDevices = result.getData(); + + assertThat(retrievedDevices).hasSize(10); + List retrievedDeviceNames = retrievedDevices.stream().map(entityData -> entityData.getLatest().get(EntityKeyType.ENTITY_FIELD).get("name").getValue()).toList(); + assertThat(retrievedDeviceNames).containsExactlyInAnyOrderElementsOf(devices.stream().map(Device::getName).toList().subList(0, 10)); + } + + private String createDevices(String deviceType, List tenantDevices, int deviceCount) throws InterruptedException { + String prefix = StringUtils.randomAlphabetic(5); + for (int i = 0; i < deviceCount; i++) { + Device device = new Device(); + device.setName(prefix + "Device" + i); + device.setType(deviceType); + device.setLabel("testLabel" + (int) (Math.random() * 1000)); + //TO make sure devices have different created time + Thread.sleep(1); + String token = RandomStringUtils.randomAlphabetic(10); + Device saved = testRestClient.postDevice(token, device); + tenantDevices.add(saved); + + // save timeseries data + testRestClient.postTelemetry(token, createDeviceTelemetry(i)); + } + return deviceType; + } + + private void assignDevicesToCustomer(CustomerId customerId, List devices, int deviceCount) { + for (int i = 0; i < deviceCount; i++) { + Device device = devices.get(i); + testRestClient.assignDeviceToCustomer(customerId, device.getId()); + } + } + + protected ObjectNode createDeviceTelemetry(int temperature) { + ObjectNode objectNode = mapper.createObjectNode(); + objectNode.put("temperature", temperature); + return objectNode; + } + +} diff --git a/msa/black-box-tests/src/test/java/org/thingsboard/server/msa/ui/utils/EntityPrototypes.java b/msa/black-box-tests/src/test/java/org/thingsboard/server/msa/ui/utils/EntityPrototypes.java index d16cbe35e7..e1c684ddf9 100644 --- a/msa/black-box-tests/src/test/java/org/thingsboard/server/msa/ui/utils/EntityPrototypes.java +++ b/msa/black-box-tests/src/test/java/org/thingsboard/server/msa/ui/utils/EntityPrototypes.java @@ -25,6 +25,7 @@ import org.thingsboard.server.common.data.DeviceProfileProvisionType; import org.thingsboard.server.common.data.DeviceProfileType; import org.thingsboard.server.common.data.DeviceTransportType; import org.thingsboard.server.common.data.EntityView; +import org.thingsboard.server.common.data.Tenant; import org.thingsboard.server.common.data.User; import org.thingsboard.server.common.data.alarm.Alarm; import org.thingsboard.server.common.data.alarm.AlarmSeverity; @@ -37,12 +38,26 @@ import org.thingsboard.server.common.data.device.profile.DisabledDeviceProfilePr import org.thingsboard.server.common.data.id.CustomerId; import org.thingsboard.server.common.data.id.DeviceProfileId; import org.thingsboard.server.common.data.id.EntityId; +import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.common.data.id.UserId; import org.thingsboard.server.common.data.rule.RuleChain; import org.thingsboard.server.common.data.security.Authority; public class EntityPrototypes { + public static Tenant defaultTenantPrototype(String tenantName) { + Tenant tenant = new Tenant(); + tenant.setTitle(tenantName); + return tenant; + } + + public static Customer defaultCustomer(TenantId tenantId, String title) { + Customer customer = new Customer(); + customer.setTenantId(tenantId); + customer.setTitle(title); + return customer; + } + public static Customer defaultCustomerPrototype(String entityName) { Customer customer = new Customer(); customer.setTitle(entityName); @@ -169,6 +184,23 @@ public class EntityPrototypes { return user; } + public static User defaultTenantAdmin(TenantId tenantId, String email) { + User user = new User(); + user.setTenantId(tenantId); + user.setEmail(email); + user.setAuthority(Authority.TENANT_ADMIN); + return user; + } + + public static User defaultCustomerAdmin(TenantId tenantId, CustomerId customerId, String email) { + User user = new User(); + user.setTenantId(tenantId); + user.setCustomerId(customerId); + user.setEmail(email); + user.setAuthority(Authority.CUSTOMER_USER); + return user; + } + public static User defaultUser(String email, CustomerId customerId, String name) { User user = new User(); user.setEmail(email); From a6a6112ecc81092add19940df5d709eb0d35f893 Mon Sep 17 00:00:00 2001 From: ViacheslavKlimov Date: Mon, 24 Feb 2025 12:11:05 +0200 Subject: [PATCH 084/345] Cleanup for EDQS --- .../service/edqs/DefaultEdqsService.java | 8 +-- .../src/main/resources/thingsboard.yml | 10 +++ common/edqs/pom.xml | 6 +- .../server/edqs/repo/TenantRepo.java | 17 ++--- .../edqs/state/KafkaEdqsStateService.java | 1 - .../server/queue/edqs/EdqsQueue.java | 4 +- .../queue/environment/DistributedLock.java | 2 +- .../environment/DistributedLockService.java | 2 +- .../DummyDistributedLockService.java | 6 +- .../environment/ZkDistributedLockService.java | 6 +- .../queue/kafka/TbKafkaConsumerTemplate.java | 2 +- .../common/stats/DummyMessagesStats.java | 54 --------------- .../thingsboard/common/util/JacksonUtil.java | 4 -- .../attributes/CachedAttributesService.java | 2 +- .../server/dao/entity/BaseEntityService.java | 6 +- .../server/dao/entity/EntityDaoRegistry.java | 13 ++-- .../server/dao/model/ModelConstants.java | 2 - .../dao/model/sql/AlarmTypeCompositeKey.java | 33 --------- .../server/dao/sql/rule/JpaRuleNodeDao.java | 5 -- .../dao/sql/rule/RuleNodeRepository.java | 3 - .../dao/sql/user/JpaUserAuthSettingsDao.java | 16 ----- .../widget/WidgetsBundleWidgetRepository.java | 7 -- .../sqlts/latest/TsKvLatestRepository.java | 7 -- edqs/pom.xml | 48 ------------- edqs/src/main/resources/edqs.yml | 67 +++---------------- 25 files changed, 53 insertions(+), 278 deletions(-) delete mode 100644 common/stats/src/main/java/org/thingsboard/server/common/stats/DummyMessagesStats.java delete mode 100644 dao/src/main/java/org/thingsboard/server/dao/model/sql/AlarmTypeCompositeKey.java diff --git a/application/src/main/java/org/thingsboard/server/service/edqs/DefaultEdqsService.java b/application/src/main/java/org/thingsboard/server/service/edqs/DefaultEdqsService.java index a6a9b9bb23..92905dc336 100644 --- a/application/src/main/java/org/thingsboard/server/service/edqs/DefaultEdqsService.java +++ b/application/src/main/java/org/thingsboard/server/service/edqs/DefaultEdqsService.java @@ -55,9 +55,9 @@ import org.thingsboard.server.common.data.kv.KvEntry; import org.thingsboard.server.common.msg.edqs.EdqsService; import org.thingsboard.server.common.msg.queue.ServiceType; import org.thingsboard.server.dao.attributes.AttributesService; -import org.thingsboard.server.edqs.util.EdqsConverter; import org.thingsboard.server.edqs.processor.EdqsProducer; import org.thingsboard.server.edqs.state.EdqsPartitionService; +import org.thingsboard.server.edqs.util.EdqsConverter; import org.thingsboard.server.gen.transport.TransportProtos; import org.thingsboard.server.gen.transport.TransportProtos.EdqsEventMsg; import org.thingsboard.server.gen.transport.TransportProtos.EdqsRequestMsg; @@ -103,7 +103,7 @@ public class DefaultEdqsService implements EdqsService { private EdqsProducer eventsProducer; private TbQueueRequestTemplate, TbProtoQueueMsg> requestTemplate; private ExecutorService executor; - private DistributedLock syncLock; + private DistributedLock syncLock; @PostConstruct private void init() { @@ -313,14 +313,14 @@ public class DefaultEdqsService implements EdqsService { .flatMap(KvEntry::getJsonValue) .map(value -> JacksonUtil.fromString(value, EdqsSyncState.class)) .orElse(null); - log.info("getSyncState = {}", state); + log.info("EDQS sync state: {}", state); return state; } @SneakyThrows private void saveSyncState(EdqsSyncStatus status) { EdqsSyncState state = new EdqsSyncState(status); - log.info("saveSyncState {}", state); + log.info("New EDQS sync state: {}", state); attributesService.save(TenantId.SYS_TENANT_ID, TenantId.SYS_TENANT_ID, AttributeScope.SERVER_SCOPE, new BaseAttributeKvEntry( new JsonDataEntry("edqsSyncState", JacksonUtil.toString(state)), System.currentTimeMillis())).get(30, TimeUnit.SECONDS); diff --git a/application/src/main/resources/thingsboard.yml b/application/src/main/resources/thingsboard.yml index 46ab8f25de..e12c6aee2d 100644 --- a/application/src/main/resources/thingsboard.yml +++ b/application/src/main/resources/thingsboard.yml @@ -1586,6 +1586,16 @@ queue: - key: max.poll.records # Amount of records to be returned in a single poll. For Housekeeper reprocessing topic, we should consume messages (tasks) one by one value: "${TB_QUEUE_KAFKA_HOUSEKEEPER_REPROCESSING_MAX_POLL_RECORDS:1}" + edqs.events: + # Key-value properties for Kafka consumer for edqs.events topic + - key: max.poll.records + # Max poll records for edqs.events topic + value: "${TB_QUEUE_KAFKA_EDQS_EVENTS_MAX_POLL_RECORDS:512}" + edqs.state: + # Key-value properties for Kafka consumer for edqs.state topic + - key: max.poll.records + # Max poll records for edqs.state topic + value: "${TB_QUEUE_KAFKA_EDQS_STATE_MAX_POLL_RECORDS:512}" other-inline: "${TB_QUEUE_KAFKA_OTHER_PROPERTIES:}" # In this section you can specify custom parameters (semicolon separated) for Kafka consumer/producer/admin # Example "metrics.recording.level:INFO;metrics.sample.window.ms:30000" other: # DEPRECATED. In this section, you can specify custom parameters for Kafka consumer/producer and expose the env variables to configure outside # - key: "request.timeout.ms" # refer to https://docs.confluent.io/platform/current/installation/configuration/producer-configs.html#producerconfigs_request.timeout.ms diff --git a/common/edqs/pom.xml b/common/edqs/pom.xml index 2abd1286b8..e5f5b759ca 100644 --- a/common/edqs/pom.xml +++ b/common/edqs/pom.xml @@ -27,7 +27,7 @@ edqs jar - Thingsboard Server EDQS API + ThingsBoard EDQS API https://thingsboard.io @@ -72,10 +72,6 @@ org.springframework.boot spring-boot-starter-web - - org.apache.kafka - kafka-clients - com.github.ben-manes.caffeine caffeine diff --git a/common/edqs/src/main/java/org/thingsboard/server/edqs/repo/TenantRepo.java b/common/edqs/src/main/java/org/thingsboard/server/edqs/repo/TenantRepo.java index 9003eb4da2..aaf91bc37b 100644 --- a/common/edqs/src/main/java/org/thingsboard/server/edqs/repo/TenantRepo.java +++ b/common/edqs/src/main/java/org/thingsboard/server/edqs/repo/TenantRepo.java @@ -84,7 +84,7 @@ import static org.thingsboard.server.edqs.util.RepositoryUtils.resolveEntityType @Slf4j public class TenantRepo { - public static final Comparator> CREATED_TIME_COMPARATOR = Comparator.comparingLong(o -> o.getFields() != null ? o.getFields().getCreatedTime() : 0); // FIXME: fields may be null at first + public static final Comparator> CREATED_TIME_COMPARATOR = Comparator.comparingLong(ed -> ed.getFields().getCreatedTime()); public static final Comparator> CREATED_TIME_AND_ID_COMPARATOR = CREATED_TIME_COMPARATOR .thenComparing(EntityData::getId); public static final Comparator> CREATED_TIME_AND_ID_DESC_COMPARATOR = CREATED_TIME_AND_ID_COMPARATOR.reversed(); @@ -186,7 +186,11 @@ public class TenantRepo { EntityData entityData = getOrCreate(entityType, entityId); processFields(fields); - entityData.setFields(entity.getFields()); + EntityFields oldFields = entityData.getFields(); + entityData.setFields(fields); + if (oldFields == null) { + getEntitySet(entityType).add(entityData); + } UUID newCustomerId = fields.getCustomerId(); UUID oldCustomerId = entityData.getCustomerId(); @@ -291,7 +295,6 @@ public class TenantRepo { return getEntityMap(entityType).computeIfAbsent(entityId, id -> { log.debug("[{}] Adding {} {}", tenantId, entityType, id); EntityData entityData = constructEntityData(entityType, entityId); - getEntitySet(entityType).add(entityData); edqsStatsService.ifPresent(statService -> statService.reportEvent(tenantId, ObjectType.fromEntityType(entityType), EdqsEventType.UPDATED)); return entityData; }); @@ -331,9 +334,6 @@ public class TenantRepo { EdqsDataQuery query = RepositoryUtils.toNewQuery(oldQuery); log.info("[{}][{}] findEntityDataByQuery: {}", tenantId, customerId, query); QueryContext ctx = buildContext(customerId, query.getEntityFilter(), ignorePermissionCheck); - if (ctx == null) { - return PageData.emptyPageData(); - } EntityQueryProcessor queryProcessor = EntityQueryProcessorFactory.create(this, ctx, query); return sortAndConvert(query, queryProcessor.processQuery(), ctx); } @@ -342,9 +342,6 @@ public class TenantRepo { EdqsQuery query = RepositoryUtils.toNewQuery(oldQuery); log.info("[{}][{}] countEntitiesByQuery: {}", tenantId, customerId, query); QueryContext ctx = buildContext(customerId, query.getEntityFilter(), ignorePermissionCheck); - if (ctx == null) { - return 0; - } EntityQueryProcessor queryProcessor = EntityQueryProcessorFactory.create(this, ctx, query); return queryProcessor.count(); } @@ -383,7 +380,7 @@ public class TenantRepo { // IMPLEMENTATION THAT IS BASED ON TIM SORT (For offset + query.getPageSize() > totalSize / 2) // data.sort(comparator); // var result = data.subList(offset, endIndex); - log.debug("EDQ Sorted in {}", TimeUnit.NANOSECONDS.toMillis(System.nanoTime() - startTs)); + log.trace("EDQ Sorted in {}", TimeUnit.NANOSECONDS.toMillis(System.nanoTime() - startTs)); return new PageData<>(toQueryResult(result, query, ctx), totalPages, totalSize, totalSize > requiredSize); } } diff --git a/common/edqs/src/main/java/org/thingsboard/server/edqs/state/KafkaEdqsStateService.java b/common/edqs/src/main/java/org/thingsboard/server/edqs/state/KafkaEdqsStateService.java index 05eebb188c..c9bae14265 100644 --- a/common/edqs/src/main/java/org/thingsboard/server/edqs/state/KafkaEdqsStateService.java +++ b/common/edqs/src/main/java/org/thingsboard/server/edqs/state/KafkaEdqsStateService.java @@ -169,7 +169,6 @@ public class KafkaEdqsStateService implements EdqsStateService { ready = true; // once true - always true, not to change readiness status on each repartitioning } } - log.error("ready: {}", ready); return ready != null && ready; } diff --git a/common/queue/src/main/java/org/thingsboard/server/queue/edqs/EdqsQueue.java b/common/queue/src/main/java/org/thingsboard/server/queue/edqs/EdqsQueue.java index c773ea4e93..45126186a3 100644 --- a/common/queue/src/main/java/org/thingsboard/server/queue/edqs/EdqsQueue.java +++ b/common/queue/src/main/java/org/thingsboard/server/queue/edqs/EdqsQueue.java @@ -24,8 +24,8 @@ public enum EdqsQueue { STATE("edqs.state", true, true); private final String topic; - private final boolean readFromBeginning; // read from the beginning of the topic, instead of the latest committed offset - private final boolean stopWhenRead; // stop consuming when reached an empty msg pack + private final boolean readFromBeginning; + private final boolean stopWhenRead; EdqsQueue(String topic, boolean readFromBeginning, boolean stopWhenRead) { this.topic = topic; diff --git a/common/queue/src/main/java/org/thingsboard/server/queue/environment/DistributedLock.java b/common/queue/src/main/java/org/thingsboard/server/queue/environment/DistributedLock.java index b69b98dd4f..9e4b681dab 100644 --- a/common/queue/src/main/java/org/thingsboard/server/queue/environment/DistributedLock.java +++ b/common/queue/src/main/java/org/thingsboard/server/queue/environment/DistributedLock.java @@ -15,7 +15,7 @@ */ package org.thingsboard.server.queue.environment; -public interface DistributedLock { +public interface DistributedLock { void lock(); diff --git a/common/queue/src/main/java/org/thingsboard/server/queue/environment/DistributedLockService.java b/common/queue/src/main/java/org/thingsboard/server/queue/environment/DistributedLockService.java index 61a21cca7c..17b0f84978 100644 --- a/common/queue/src/main/java/org/thingsboard/server/queue/environment/DistributedLockService.java +++ b/common/queue/src/main/java/org/thingsboard/server/queue/environment/DistributedLockService.java @@ -17,6 +17,6 @@ package org.thingsboard.server.queue.environment; public interface DistributedLockService { - DistributedLock getLock(String key); + DistributedLock getLock(String key); } diff --git a/common/queue/src/main/java/org/thingsboard/server/queue/environment/DummyDistributedLockService.java b/common/queue/src/main/java/org/thingsboard/server/queue/environment/DummyDistributedLockService.java index 03b6d0f2d9..d289c5cb34 100644 --- a/common/queue/src/main/java/org/thingsboard/server/queue/environment/DummyDistributedLockService.java +++ b/common/queue/src/main/java/org/thingsboard/server/queue/environment/DummyDistributedLockService.java @@ -27,12 +27,12 @@ import java.util.concurrent.locks.ReentrantLock; public class DummyDistributedLockService implements DistributedLockService { @Override - public DistributedLock getLock(String key) { - return new DummyDistributedLock<>(); + public DistributedLock getLock(String key) { + return new DummyDistributedLock(); } @RequiredArgsConstructor - private static class DummyDistributedLock implements DistributedLock { + private static class DummyDistributedLock implements DistributedLock { private final ReentrantLock lock = new ReentrantLock(); diff --git a/common/queue/src/main/java/org/thingsboard/server/queue/environment/ZkDistributedLockService.java b/common/queue/src/main/java/org/thingsboard/server/queue/environment/ZkDistributedLockService.java index 07b8b503f9..c0b9b4d8d3 100644 --- a/common/queue/src/main/java/org/thingsboard/server/queue/environment/ZkDistributedLockService.java +++ b/common/queue/src/main/java/org/thingsboard/server/queue/environment/ZkDistributedLockService.java @@ -33,12 +33,12 @@ public class ZkDistributedLockService implements DistributedLockService { private final ZkDiscoveryService zkDiscoveryService; @Override - public DistributedLock getLock(String key) { - return new ZkDistributedLock<>(key); + public DistributedLock getLock(String key) { + return new ZkDistributedLock(key); } @RequiredArgsConstructor - private class ZkDistributedLock implements DistributedLock { + private class ZkDistributedLock implements DistributedLock { private final InterProcessLock interProcessLock; diff --git a/common/queue/src/main/java/org/thingsboard/server/queue/kafka/TbKafkaConsumerTemplate.java b/common/queue/src/main/java/org/thingsboard/server/queue/kafka/TbKafkaConsumerTemplate.java index 9b580c762e..6d4ed1073c 100644 --- a/common/queue/src/main/java/org/thingsboard/server/queue/kafka/TbKafkaConsumerTemplate.java +++ b/common/queue/src/main/java/org/thingsboard/server/queue/kafka/TbKafkaConsumerTemplate.java @@ -53,7 +53,7 @@ public class TbKafkaConsumerTemplate extends AbstractTbQue private final String groupId; private final boolean readFromBeginning; // reset offset to beginning - private final boolean stopWhenRead; // stop consuming when reached an empty msg pack + private final boolean stopWhenRead; // stop consuming when reached end offset remembered on start private int readCount; private Map endOffsets; // needed if stopWhenRead is true diff --git a/common/stats/src/main/java/org/thingsboard/server/common/stats/DummyMessagesStats.java b/common/stats/src/main/java/org/thingsboard/server/common/stats/DummyMessagesStats.java deleted file mode 100644 index 7847860dcd..0000000000 --- a/common/stats/src/main/java/org/thingsboard/server/common/stats/DummyMessagesStats.java +++ /dev/null @@ -1,54 +0,0 @@ -/** - * Copyright © 2016-2024 The Thingsboard Authors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package org.thingsboard.server.common.stats; - -public class DummyMessagesStats implements MessagesStats { - @Override - public void incrementTotal(int amount) { - - } - - @Override - public void incrementSuccessful(int amount) { - - } - - @Override - public void incrementFailed(int amount) { - - } - - @Override - public int getTotal() { - return 0; - } - - @Override - public int getSuccessful() { - return 0; - } - - @Override - public int getFailed() { - return 0; - } - - @Override - public void reset() { - - } - -} diff --git a/common/util/src/main/java/org/thingsboard/common/util/JacksonUtil.java b/common/util/src/main/java/org/thingsboard/common/util/JacksonUtil.java index 8dc98377d3..fe0e019537 100644 --- a/common/util/src/main/java/org/thingsboard/common/util/JacksonUtil.java +++ b/common/util/src/main/java/org/thingsboard/common/util/JacksonUtil.java @@ -559,10 +559,6 @@ public class JacksonUtil { } } - public static JsonNode getValueByPath(ObjectNode node, String path) { - return node.at("/" + path.replace(".", "/")); - } - @Data public static class JsonNodeProcessingTask { private final String path; diff --git a/dao/src/main/java/org/thingsboard/server/dao/attributes/CachedAttributesService.java b/dao/src/main/java/org/thingsboard/server/dao/attributes/CachedAttributesService.java index 3c12ae3209..63371d97e4 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/attributes/CachedAttributesService.java +++ b/dao/src/main/java/org/thingsboard/server/dao/attributes/CachedAttributesService.java @@ -281,7 +281,7 @@ public class CachedAttributesService implements AttributesService { String key = deleted.getValue(); if (scope != null && key != null) { cache.evict(new AttributeCacheKey(scope, entityId, key)); - // FIXME: version is Long.MAX_VALUE because we expect that the entity is deleted and there won't be any attributes after this + // using version as Long.MAX_VALUE because we expect that the entity is deleted and there won't be any attributes after this edqsService.onDelete(tenantId, ObjectType.ATTRIBUTE_KV, new AttributeKv(entityId, scope, key, Long.MAX_VALUE)); } }); diff --git a/dao/src/main/java/org/thingsboard/server/dao/entity/BaseEntityService.java b/dao/src/main/java/org/thingsboard/server/dao/entity/BaseEntityService.java index 1a0f3be1e7..fb8078dd99 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/entity/BaseEntityService.java +++ b/dao/src/main/java/org/thingsboard/server/dao/entity/BaseEntityService.java @@ -95,7 +95,7 @@ public class BaseEntityService extends AbstractEntityService implements EntitySe validateId(customerId, id -> INCORRECT_CUSTOMER_ID + id); validateEntityCountQuery(query); - if (edqsService.isApiEnabled() && validForEdqs(query)) { // TODO: separate boolean param whether to use in dashboards; but sync to edqs - always + if (edqsService.isApiEnabled() && validForEdqs(query)) { EdqsRequest request = EdqsRequest.builder() .entityCountQuery(query) .build(); @@ -112,7 +112,7 @@ public class BaseEntityService extends AbstractEntityService implements EntitySe validateId(customerId, id -> INCORRECT_CUSTOMER_ID + id); validateEntityDataQuery(query); - if (edqsService.isApiEnabled() && validForEdqs(query)) { // TODO: separate boolean param whether to use in dashboards; but sync to edqs - always + if (edqsService.isApiEnabled() && validForEdqs(query)) { EdqsRequest request = EdqsRequest.builder() .entityDataQuery(query) .build(); @@ -135,7 +135,7 @@ public class BaseEntityService extends AbstractEntityService implements EntitySe return new PageData<>(result, entityDataByQuery.getTotalPages(), entityDataByQuery.getTotalElements(), entityDataByQuery.hasNext()); } - private boolean validForEdqs(EntityCountQuery query) { + private boolean validForEdqs(EntityCountQuery query) { // for compatibility with PE return true; } diff --git a/dao/src/main/java/org/thingsboard/server/dao/entity/EntityDaoRegistry.java b/dao/src/main/java/org/thingsboard/server/dao/entity/EntityDaoRegistry.java index 86887e94ec..46e6a51830 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/entity/EntityDaoRegistry.java +++ b/dao/src/main/java/org/thingsboard/server/dao/entity/EntityDaoRegistry.java @@ -29,18 +29,19 @@ import java.util.Map; @SuppressWarnings({"unchecked"}) public class EntityDaoRegistry { - private final Map> entityDaos = new EnumMap<>(EntityType.class); + private final Map> daos = new EnumMap<>(EntityType.class); - private EntityDaoRegistry(List> entityDaos) { - entityDaos.forEach(dao -> { - if (dao.getEntityType() != null) { - this.entityDaos.put(dao.getEntityType(), dao); + private EntityDaoRegistry(List> daos) { + daos.forEach(dao -> { + EntityType entityType = dao.getEntityType(); + if (entityType != null) { + this.daos.put(entityType, dao); } }); } public Dao getDao(EntityType entityType) { - Dao dao = (Dao) entityDaos.get(entityType); + Dao dao = (Dao) daos.get(entityType); if (dao == null) { throw new IllegalArgumentException("Missing dao for entity type " + entityType); } diff --git a/dao/src/main/java/org/thingsboard/server/dao/model/ModelConstants.java b/dao/src/main/java/org/thingsboard/server/dao/model/ModelConstants.java index f762be0429..d404c3128c 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/model/ModelConstants.java +++ b/dao/src/main/java/org/thingsboard/server/dao/model/ModelConstants.java @@ -281,8 +281,6 @@ public class ModelConstants { public static final String ALARM_COMMENT_TYPE = "type"; public static final String ALARM_COMMENT_COMMENT = "comment"; - public static final String ALARM_TYPES_TABLE_NAME = "alarm_types"; - /** * Entity relation constants. */ diff --git a/dao/src/main/java/org/thingsboard/server/dao/model/sql/AlarmTypeCompositeKey.java b/dao/src/main/java/org/thingsboard/server/dao/model/sql/AlarmTypeCompositeKey.java deleted file mode 100644 index 60f5232142..0000000000 --- a/dao/src/main/java/org/thingsboard/server/dao/model/sql/AlarmTypeCompositeKey.java +++ /dev/null @@ -1,33 +0,0 @@ -/** - * Copyright © 2016-2024 The Thingsboard Authors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package org.thingsboard.server.dao.model.sql; - -import lombok.AllArgsConstructor; -import lombok.Data; -import lombok.NoArgsConstructor; - -import java.io.Serializable; -import java.util.UUID; - -@Data -@NoArgsConstructor -@AllArgsConstructor -public class AlarmTypeCompositeKey implements Serializable { - - private UUID tenantId; - private String type; - -} diff --git a/dao/src/main/java/org/thingsboard/server/dao/sql/rule/JpaRuleNodeDao.java b/dao/src/main/java/org/thingsboard/server/dao/sql/rule/JpaRuleNodeDao.java index 43087b8652..3bf9ab2963 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/sql/rule/JpaRuleNodeDao.java +++ b/dao/src/main/java/org/thingsboard/server/dao/sql/rule/JpaRuleNodeDao.java @@ -107,11 +107,6 @@ public class JpaRuleNodeDao extends JpaAbstractDao imp ruleNodeRepository.deleteAllById(ruleNodeIds.stream().map(RuleNodeId::getId).collect(Collectors.toList())); } - @Override - public PageData findAllByTenantId(TenantId tenantId, PageLink pageLink) { - return DaoUtil.toPageData(ruleNodeRepository.findByTenantId(tenantId.getId(), DaoUtil.toPageable(pageLink))); - } - @Override public EntityType getEntityType() { return EntityType.RULE_NODE; diff --git a/dao/src/main/java/org/thingsboard/server/dao/sql/rule/RuleNodeRepository.java b/dao/src/main/java/org/thingsboard/server/dao/sql/rule/RuleNodeRepository.java index bf19d61a9d..264ea88326 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/sql/rule/RuleNodeRepository.java +++ b/dao/src/main/java/org/thingsboard/server/dao/sql/rule/RuleNodeRepository.java @@ -63,7 +63,4 @@ public interface RuleNodeRepository extends JpaRepository @Query("DELETE FROM RuleNodeEntity e where e.id in :ids") void deleteByIdIn(@Param("ids") List ids); - @Query("SELECT n FROM RuleNodeEntity n WHERE n.ruleChainId IN (SELECT rc.id FROM RuleChainEntity rc WHERE rc.tenantId = :tenantId)") - Page findByTenantId(@Param("tenantId") UUID tenantId, Pageable pageable); - } diff --git a/dao/src/main/java/org/thingsboard/server/dao/sql/user/JpaUserAuthSettingsDao.java b/dao/src/main/java/org/thingsboard/server/dao/sql/user/JpaUserAuthSettingsDao.java index 32b6acf08b..307cadce91 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/sql/user/JpaUserAuthSettingsDao.java +++ b/dao/src/main/java/org/thingsboard/server/dao/sql/user/JpaUserAuthSettingsDao.java @@ -18,12 +18,8 @@ package org.thingsboard.server.dao.sql.user; import lombok.RequiredArgsConstructor; import org.springframework.data.jpa.repository.JpaRepository; import org.springframework.stereotype.Component; -import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.common.data.id.UserId; -import org.thingsboard.server.common.data.page.PageData; -import org.thingsboard.server.common.data.page.PageLink; import org.thingsboard.server.common.data.security.UserAuthSettings; -import org.thingsboard.server.common.data.security.model.mfa.account.AccountTwoFaSettings; import org.thingsboard.server.dao.DaoUtil; import org.thingsboard.server.dao.TenantEntityDao; import org.thingsboard.server.dao.model.sql.UserAuthSettingsEntity; @@ -50,18 +46,6 @@ public class JpaUserAuthSettingsDao extends JpaAbstractDao findAllByTenantId(TenantId tenantId, PageLink pageLink) { - PageData data = DaoUtil.toPageData(repository.findByTenantId(tenantId.getId(), DaoUtil.toPageable(pageLink))); - data.getData().forEach(settings -> { - AccountTwoFaSettings twoFaSettings = settings.getTwoFaSettings(); - if (twoFaSettings != null && twoFaSettings.getConfigs() != null) { - twoFaSettings.getConfigs().values().forEach(config -> config.setSerializeHiddenFields(true)); - } - }); - return data; - } - @Override protected Class getEntityClass() { return UserAuthSettingsEntity.class; diff --git a/dao/src/main/java/org/thingsboard/server/dao/sql/widget/WidgetsBundleWidgetRepository.java b/dao/src/main/java/org/thingsboard/server/dao/sql/widget/WidgetsBundleWidgetRepository.java index 3a024e1687..89533106f3 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/sql/widget/WidgetsBundleWidgetRepository.java +++ b/dao/src/main/java/org/thingsboard/server/dao/sql/widget/WidgetsBundleWidgetRepository.java @@ -15,11 +15,7 @@ */ package org.thingsboard.server.dao.sql.widget; -import org.springframework.data.domain.Page; -import org.springframework.data.domain.Pageable; import org.springframework.data.jpa.repository.JpaRepository; -import org.springframework.data.jpa.repository.Query; -import org.springframework.data.repository.query.Param; import org.thingsboard.server.dao.model.sql.WidgetsBundleWidgetCompositeKey; import org.thingsboard.server.dao.model.sql.WidgetsBundleWidgetEntity; @@ -30,7 +26,4 @@ public interface WidgetsBundleWidgetRepository extends JpaRepository findAllByWidgetsBundleId(UUID widgetsBundleId); - @Query("SELECT w FROM WidgetsBundleWidgetEntity w WHERE w.widgetsBundleId IN (SELECT b.id FROM WidgetsBundleEntity b WHERE b.tenantId = :tenantId)") - Page findByTenantId(@Param("tenantId") UUID tenantId, Pageable pageable); - } diff --git a/dao/src/main/java/org/thingsboard/server/dao/sqlts/latest/TsKvLatestRepository.java b/dao/src/main/java/org/thingsboard/server/dao/sqlts/latest/TsKvLatestRepository.java index 2c97ba30ba..8b44e2c9ad 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/sqlts/latest/TsKvLatestRepository.java +++ b/dao/src/main/java/org/thingsboard/server/dao/sqlts/latest/TsKvLatestRepository.java @@ -15,12 +15,9 @@ */ package org.thingsboard.server.dao.sqlts.latest; -import org.springframework.data.domain.Page; -import org.springframework.data.domain.Pageable; import org.springframework.data.jpa.repository.JpaRepository; import org.springframework.data.jpa.repository.Query; import org.springframework.data.repository.query.Param; -import org.thingsboard.server.dao.model.sql.AttributeKvEntity; import org.thingsboard.server.dao.model.sqlts.latest.TsKvLatestCompositeKey; import org.thingsboard.server.dao.model.sqlts.latest.TsKvLatestEntity; @@ -44,10 +41,6 @@ public interface TsKvLatestRepository extends JpaRepository findAllKeysByEntityIds(@Param("entityIds") List entityIds); - @Query("SELECT e FROM TsKvLatestEntity e ORDER BY e.entityId ASC, e.key ASC") - Page findAll(Pageable pageable); - - @Query(value = "SELECT entity_id, key, ts, bool_v, str_v, long_v, dbl_v, json_v, version FROM ts_kv_latest WHERE (entity_id, key) > " + "(:entityId, :key) ORDER BY entity_id, key LIMIT :batchSize", nativeQuery = true) List findNextBatch(@Param("entityId") UUID entityId, diff --git a/edqs/pom.xml b/edqs/pom.xml index eb824cb77f..8eb6edfb23 100644 --- a/edqs/pom.xml +++ b/edqs/pom.xml @@ -28,8 +28,6 @@ ThingsBoard Entity Data Query Service Application https://thingsboard.io - ThingsBoard Professional Edition: IoT Platform - Device management, data collection, processing and visualization - UTF-8 @@ -90,16 +88,6 @@ exe provided - - org.thingsboard - tools - test - - - org.springframework.security - spring-security-test - test - org.springframework.boot spring-boot-starter-test @@ -115,42 +103,6 @@ awaitility test - - org.dbunit - dbunit - test - - - com.github.springtestdbunit - spring-test-dbunit - test - - - org.testcontainers - cassandra - test - - - org.testcontainers - postgresql - test - - - org.testcontainers - jdbc - test - - - org.java-websocket - Java-WebSocket - test - - - org.eclipse.milo - sdk-server - ${milo.version} - test - org.assertj assertj-core diff --git a/edqs/src/main/resources/edqs.yml b/edqs/src/main/resources/edqs.yml index 3d30c5c6fa..80771c2265 100644 --- a/edqs/src/main/resources/edqs.yml +++ b/edqs/src/main/resources/edqs.yml @@ -132,29 +132,17 @@ queue: # Key-value properties for Kafka consumer per specific topic, e.g. tb_ota_package is a topic name for ota, tb_rule_engine.sq is a topic name for default SequentialByOriginator queue. # Check TB_QUEUE_CORE_OTA_TOPIC and TB_QUEUE_RE_SQ_TOPIC params consumer-properties-per-topic: - tb_ota_package: - # Key-value properties for Kafka consumer per specific topic, e.g. tb_ota_package is a topic name for ota, tb_rule_engine.sq is a topic name for default SequentialByOriginator queue. Check TB_QUEUE_CORE_OTA_TOPIC and TB_QUEUE_RE_SQ_TOPIC params + edqs.events: + # Key-value properties for Kafka consumer for edqs.events topic - key: max.poll.records - # Example of specific consumer properties value per topic - value: "${TB_QUEUE_KAFKA_OTA_MAX_POLL_RECORDS:10}" - tb_version_control: - # Example of specific consumer properties value per topic for VC - - key: max.poll.interval.ms - # Example of specific consumer properties value per topic for VC - value: "${TB_QUEUE_KAFKA_VC_MAX_POLL_INTERVAL_MS:600000}" - # tb_rule_engine.sq: - # - key: max.poll.records - # value: "${TB_QUEUE_KAFKA_SQ_MAX_POLL_RECORDS:1024}" - tb_housekeeper: - # Consumer properties for Housekeeper tasks topic + # Max poll records for edqs.events topic + value: "${TB_QUEUE_KAFKA_EDQS_EVENTS_MAX_POLL_RECORDS:512}" + edqs.state: + # Key-value properties for Kafka consumer for edqs.state topic - key: max.poll.records - # Amount of records to be returned in a single poll. For Housekeeper tasks topic, we should consume messages (tasks) one by one - value: "${TB_QUEUE_KAFKA_HOUSEKEEPER_MAX_POLL_RECORDS:1}" - tb_housekeeper.reprocessing: - # Consumer properties for Housekeeper reprocessing topic - - key: max.poll.records - # Amount of records to be returned in a single poll. For Housekeeper reprocessing topic, we should consume messages (tasks) one by one - value: "${TB_QUEUE_KAFKA_HOUSEKEEPER_REPROCESSING_MAX_POLL_RECORDS:1}" + # Max poll records for edqs.state topic + value: "${TB_QUEUE_KAFKA_EDQS_STATE_MAX_POLL_RECORDS:512}" + other-inline: "${TB_QUEUE_KAFKA_OTHER_PROPERTIES:}" # In this section you can specify custom parameters (semicolon separated) for Kafka consumer/producer/admin # Example "metrics.recording.level:INFO;metrics.sample.window.ms:30000" other: # DEPRECATED. In this section, you can specify custom parameters for Kafka consumer/producer and expose the env variables to configure outside # - key: "request.timeout.ms" # refer to https://docs.confluent.io/platform/current/installation/configuration/producer-configs.html#producerconfigs_request.timeout.ms @@ -162,26 +150,6 @@ queue: # - key: "session.timeout.ms" # refer to https://docs.confluent.io/platform/current/installation/configuration/consumer-configs.html#consumerconfigs_session.timeout.ms # value: "${TB_QUEUE_KAFKA_SESSION_TIMEOUT_MS:10000}" # (10 seconds) topic-properties: - # Kafka properties for Rule Engine - rule-engine: "${TB_QUEUE_KAFKA_RE_TOPIC_PROPERTIES:retention.ms:604800000;segment.bytes:52428800;retention.bytes:1048576000;partitions:1;min.insync.replicas:1}" - # Kafka properties for Core topics - core: "${TB_QUEUE_KAFKA_CORE_TOPIC_PROPERTIES:retention.ms:604800000;segment.bytes:52428800;retention.bytes:1048576000;partitions:1;min.insync.replicas:1}" - # Kafka properties for Transport Api topics - transport-api: "${TB_QUEUE_KAFKA_TA_TOPIC_PROPERTIES:retention.ms:604800000;segment.bytes:52428800;retention.bytes:1048576000;partitions:10;min.insync.replicas:1}" - # Kafka properties for Notifications topics - notifications: "${TB_QUEUE_KAFKA_NOTIFICATIONS_TOPIC_PROPERTIES:retention.ms:604800000;segment.bytes:52428800;retention.bytes:1048576000;partitions:1;min.insync.replicas:1}" - # Kafka properties for JS Executor topics - js-executor: "${TB_QUEUE_KAFKA_JE_TOPIC_PROPERTIES:retention.ms:604800000;segment.bytes:52428800;retention.bytes:104857600;partitions:100;min.insync.replicas:1}" - # Kafka properties for OTA updates topic - ota-updates: "${TB_QUEUE_KAFKA_OTA_TOPIC_PROPERTIES:retention.ms:604800000;segment.bytes:52428800;retention.bytes:1048576000;partitions:10;min.insync.replicas:1}" - # Kafka properties for Version Control topic - version-control: "${TB_QUEUE_KAFKA_VC_TOPIC_PROPERTIES:retention.ms:604800000;segment.bytes:52428800;retention.bytes:1048576000;partitions:1;min.insync.replicas:1}" - # Kafka properties for Integration Api topics - integration-api: "${TB_QUEUE_KAFKA_INTEGRATION_TOPIC_PROPERTIES:retention.ms:604800000;segment.bytes:52428800;retention.bytes:1048576000;partitions:10;min.insync.replicas:1}" - # Kafka properties for Housekeeper tasks topic - housekeeper: "${TB_QUEUE_KAFKA_HOUSEKEEPER_TOPIC_PROPERTIES:retention.ms:604800000;segment.bytes:52428800;retention.bytes:1048576000;partitions:10;min.insync.replicas:1}" - # Kafka properties for Housekeeper reprocessing topic; retention.ms is set to 90 days; partitions is set to 1 since only one reprocessing service is running at a time - housekeeper-reprocessing: "${TB_QUEUE_KAFKA_HOUSEKEEPER_REPROCESSING_TOPIC_PROPERTIES:retention.ms:7776000000;segment.bytes:52428800;retention.bytes:1048576000;partitions:1;min.insync.replicas:1}" # Kafka properties for EDQS events topics. Partitions number must be the same as queue.edqs.partitions edqs-events: "${TB_QUEUE_KAFKA_EDQS_EVENTS_TOPIC_PROPERTIES:retention.ms:604800000;segment.bytes:52428800;retention.bytes:-1;partitions:12;min.insync.replicas:1}" # Kafka properties for EDQS requests topic (default: 3 minutes retention). Partitions number must be the same as queue.edqs.partitions @@ -197,23 +165,6 @@ queue: kafka-response-timeout-ms: "${TB_QUEUE_KAFKA_CONSUMER_STATS_RESPONSE_TIMEOUT_MS:1000}" partitions: hash_function_name: "${TB_QUEUE_PARTITIONS_HASH_FUNCTION_NAME:murmur3_128}" # murmur3_32, murmur3_128 or sha256 - transport_api: - # Topic used to consume api requests from transport microservices - requests_topic: "${TB_QUEUE_TRANSPORT_API_REQUEST_TOPIC:tb_transport.api.requests}" - # Topic used to produce api responses to transport microservices - responses_topic: "${TB_QUEUE_TRANSPORT_API_RESPONSE_TOPIC:tb_transport.api.responses}" - # Maximum pending api requests from transport microservices to be handled by server - max_pending_requests: "${TB_QUEUE_TRANSPORT_MAX_PENDING_REQUESTS:10000}" - # Maximum timeout in milliseconds to handle api request from transport microservice by server - max_requests_timeout: "${TB_QUEUE_TRANSPORT_MAX_REQUEST_TIMEOUT:10000}" - # Amount of threads used to invoke callbacks - max_callback_threads: "${TB_QUEUE_TRANSPORT_MAX_CALLBACK_THREADS:100}" - # Amount of threads used for transport API requests - max_core_handler_threads: "${TB_QUEUE_TRANSPORT_MAX_CORE_HANDLER_THREADS:16}" - # Interval in milliseconds to poll api requests from transport microservices - request_poll_interval: "${TB_QUEUE_TRANSPORT_REQUEST_POLL_INTERVAL_MS:25}" - # Interval in milliseconds to poll api response from transport microservices - response_poll_interval: "${TB_QUEUE_TRANSPORT_RESPONSE_POLL_INTERVAL_MS:25}" # General service parameters service: From bf6301a1de1ba17233fc65715f7e01cbcf8ee59e Mon Sep 17 00:00:00 2001 From: Sergey Matvienko Date: Mon, 24 Feb 2025 11:58:21 +0100 Subject: [PATCH 085/345] StatsPersistTick scheduled future cancel on actor destroy --- .../org/thingsboard/server/actors/ActorSystemContext.java | 5 +++-- .../thingsboard/server/actors/service/ComponentActor.java | 8 +++++++- .../actors/shared/AbstractContextAwareMsgProcessor.java | 5 +++-- .../server/actors/shared/ComponentMsgProcessor.java | 6 ++++-- 4 files changed, 17 insertions(+), 7 deletions(-) diff --git a/application/src/main/java/org/thingsboard/server/actors/ActorSystemContext.java b/application/src/main/java/org/thingsboard/server/actors/ActorSystemContext.java index 1c18a31f68..c15c87be1c 100644 --- a/application/src/main/java/org/thingsboard/server/actors/ActorSystemContext.java +++ b/application/src/main/java/org/thingsboard/server/actors/ActorSystemContext.java @@ -127,6 +127,7 @@ import java.io.StringWriter; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.ConcurrentMap; import java.util.concurrent.ScheduledExecutorService; +import java.util.concurrent.ScheduledFuture; import java.util.concurrent.TimeUnit; @Slf4j @@ -763,9 +764,9 @@ public class ActorSystemContext { appActor.tellWithHighPriority(tbActorMsg); } - public void schedulePeriodicMsgWithDelay(TbActorRef ctx, TbActorMsg msg, long delayInMs, long periodInMs) { + public ScheduledFuture schedulePeriodicMsgWithDelay(TbActorRef ctx, TbActorMsg msg, long delayInMs, long periodInMs) { log.debug("Scheduling periodic msg {} every {} ms with delay {} ms", msg, periodInMs, delayInMs); - getScheduler().scheduleWithFixedDelay(() -> ctx.tell(msg), delayInMs, periodInMs, TimeUnit.MILLISECONDS); + return getScheduler().scheduleWithFixedDelay(() -> ctx.tell(msg), delayInMs, periodInMs, TimeUnit.MILLISECONDS); } public void scheduleMsgWithDelay(TbActorRef ctx, TbActorMsg msg, long delayInMs) { diff --git a/application/src/main/java/org/thingsboard/server/actors/service/ComponentActor.java b/application/src/main/java/org/thingsboard/server/actors/service/ComponentActor.java index 5aea88d622..bf50273814 100644 --- a/application/src/main/java/org/thingsboard/server/actors/service/ComponentActor.java +++ b/application/src/main/java/org/thingsboard/server/actors/service/ComponentActor.java @@ -29,6 +29,9 @@ import org.thingsboard.server.common.msg.TbActorStopReason; import org.thingsboard.server.common.msg.plugin.ComponentLifecycleMsg; import org.thingsboard.server.common.msg.queue.PartitionChangeMsg; +import java.util.Optional; +import java.util.concurrent.ScheduledFuture; + /** * @author Andrew Shvayka */ @@ -41,6 +44,7 @@ public abstract class ComponentActor statsScheduledFuture = null; public ComponentActor(ActorSystemContext systemContext, TenantId tenantId, T id) { super(systemContext); @@ -75,7 +79,7 @@ public abstract class ComponentActor x.cancel(false)); + statsScheduledFuture = null; } catch (Exception e) { log.warn("[{}][{}] Failed to stop {} processor: {}", tenantId, id, id.getEntityType(), e.getMessage()); logAndPersist("OnStop", e, true); diff --git a/application/src/main/java/org/thingsboard/server/actors/shared/AbstractContextAwareMsgProcessor.java b/application/src/main/java/org/thingsboard/server/actors/shared/AbstractContextAwareMsgProcessor.java index 571d79d18c..a34a8ca537 100644 --- a/application/src/main/java/org/thingsboard/server/actors/shared/AbstractContextAwareMsgProcessor.java +++ b/application/src/main/java/org/thingsboard/server/actors/shared/AbstractContextAwareMsgProcessor.java @@ -21,6 +21,7 @@ import org.thingsboard.server.actors.TbActorCtx; import org.thingsboard.server.common.msg.TbActorMsg; import java.util.concurrent.ScheduledExecutorService; +import java.util.concurrent.ScheduledFuture; @Slf4j public abstract class AbstractContextAwareMsgProcessor { @@ -36,8 +37,8 @@ public abstract class AbstractContextAwareMsgProcessor { return systemContext.getScheduler(); } - protected void schedulePeriodicMsgWithDelay(TbActorCtx ctx, TbActorMsg msg, long delayInMs, long periodInMs) { - systemContext.schedulePeriodicMsgWithDelay(ctx, msg, delayInMs, periodInMs); + protected ScheduledFuture schedulePeriodicMsgWithDelay(TbActorCtx ctx, TbActorMsg msg, long delayInMs, long periodInMs) { + return systemContext.schedulePeriodicMsgWithDelay(ctx, msg, delayInMs, periodInMs); } protected void scheduleMsgWithDelay(TbActorCtx ctx, TbActorMsg msg, long delayInMs) { diff --git a/application/src/main/java/org/thingsboard/server/actors/shared/ComponentMsgProcessor.java b/application/src/main/java/org/thingsboard/server/actors/shared/ComponentMsgProcessor.java index 298874cc51..c37b35400c 100644 --- a/application/src/main/java/org/thingsboard/server/actors/shared/ComponentMsgProcessor.java +++ b/application/src/main/java/org/thingsboard/server/actors/shared/ComponentMsgProcessor.java @@ -27,6 +27,8 @@ import org.thingsboard.server.common.msg.TbMsg; import org.thingsboard.server.common.msg.queue.PartitionChangeMsg; import org.thingsboard.server.common.msg.queue.RuleNodeException; +import java.util.concurrent.ScheduledFuture; + @Slf4j public abstract class ComponentMsgProcessor extends AbstractContextAwareMsgProcessor { @@ -77,8 +79,8 @@ public abstract class ComponentMsgProcessor extends Abstract start(context); } - public void scheduleStatsPersistTick(TbActorCtx context, long statsPersistFrequency) { - schedulePeriodicMsgWithDelay(context, StatsPersistTick.INSTANCE, statsPersistFrequency, statsPersistFrequency); + public ScheduledFuture scheduleStatsPersistTick(TbActorCtx context, long statsPersistFrequency) { + return schedulePeriodicMsgWithDelay(context, StatsPersistTick.INSTANCE, statsPersistFrequency, statsPersistFrequency); } protected boolean checkMsgValid(TbMsg tbMsg) { From 09ecfa8d602c6fad407934ca9a79002b10da8e42 Mon Sep 17 00:00:00 2001 From: deaflynx Date: Mon, 24 Feb 2025 15:41:43 +0200 Subject: [PATCH 086/345] UI: Mobile center page name add warnings 256 limit/only white spaces. --- .../default-mobile-page-panel.component.html | 16 ++++++++++++++++ .../default-mobile-page-panel.component.ts | 4 ++-- .../layout/mobile-page-item-row.component.html | 8 ++++++++ .../layout/mobile-page-item-row.component.ts | 2 +- 4 files changed, 27 insertions(+), 3 deletions(-) diff --git a/ui-ngx/src/app/modules/home/pages/mobile/bundes/layout/default-mobile-page-panel.component.html b/ui-ngx/src/app/modules/home/pages/mobile/bundes/layout/default-mobile-page-panel.component.html index 3ed1601a03..b6bd373eff 100644 --- a/ui-ngx/src/app/modules/home/pages/mobile/bundes/layout/default-mobile-page-panel.component.html +++ b/ui-ngx/src/app/modules/home/pages/mobile/bundes/layout/default-mobile-page-panel.component.html @@ -43,6 +43,22 @@ mobile.page-name + + warning + + + warning +
diff --git a/ui-ngx/src/app/modules/home/pages/mobile/bundes/layout/default-mobile-page-panel.component.ts b/ui-ngx/src/app/modules/home/pages/mobile/bundes/layout/default-mobile-page-panel.component.ts index f42cedd942..58c515afc4 100644 --- a/ui-ngx/src/app/modules/home/pages/mobile/bundes/layout/default-mobile-page-panel.component.ts +++ b/ui-ngx/src/app/modules/home/pages/mobile/bundes/layout/default-mobile-page-panel.component.ts @@ -17,7 +17,7 @@ import { Component, DestroyRef, EventEmitter, inject, Input, OnInit, Output, ViewEncapsulation } from '@angular/core'; import { DefaultMobilePage, defaultMobilePageMap, hideDefaultMenuItems } from '@shared/models/mobile-app.models'; import { TbPopoverComponent } from '@shared/components/popover.component'; -import { FormBuilder } from '@angular/forms'; +import { FormBuilder, Validators } from '@angular/forms'; import { takeUntilDestroyed } from '@angular/core/rxjs-interop'; @Component({ @@ -43,7 +43,7 @@ export class DefaultMobilePagePanelComponent implements OnInit { mobilePageFormGroup = this.fb.group({ visible: [true], icon: [''], - label: [''], + label: ['', [Validators.pattern(/\S/), Validators.maxLength(255)]], }); isCleanupEnabled = false; diff --git a/ui-ngx/src/app/modules/home/pages/mobile/bundes/layout/mobile-page-item-row.component.html b/ui-ngx/src/app/modules/home/pages/mobile/bundes/layout/mobile-page-item-row.component.html index 6ece48086b..22cede48b6 100644 --- a/ui-ngx/src/app/modules/home/pages/mobile/bundes/layout/mobile-page-item-row.component.html +++ b/ui-ngx/src/app/modules/home/pages/mobile/bundes/layout/mobile-page-item-row.component.html @@ -50,6 +50,14 @@ class="tb-error"> warning + + warning + diff --git a/ui-ngx/src/app/modules/home/pages/mobile/bundes/layout/mobile-page-item-row.component.ts b/ui-ngx/src/app/modules/home/pages/mobile/bundes/layout/mobile-page-item-row.component.ts index 7083d98860..eb83ae78d4 100644 --- a/ui-ngx/src/app/modules/home/pages/mobile/bundes/layout/mobile-page-item-row.component.ts +++ b/ui-ngx/src/app/modules/home/pages/mobile/bundes/layout/mobile-page-item-row.component.ts @@ -99,7 +99,7 @@ export class MobilePageItemRowComponent implements ControlValueAccessor, OnInit, mobilePageRowForm = this.fb.group({ visible: [true, []], icon: ['', []], - label: ['', [Validators.pattern(/\S/)]], + label: ['', [Validators.pattern(/\S/), Validators.maxLength(255)]], type: [MobilePageType.DEFAULT] }); From f28f14f2fc1f0ebc35c6b686c1ab91cf460e546f Mon Sep 17 00:00:00 2001 From: dashevchenko Date: Mon, 24 Feb 2025 17:36:42 +0200 Subject: [PATCH 087/345] minor refactoring --- .../controller/EntityQueryControllerTest.java | 3 ++- docker/edqs.env | 1 - .../server/msa/ContainerTestSuite.java | 6 ++--- .../server/msa/TestRestClient.java | 22 ------------------- 4 files changed, 4 insertions(+), 28 deletions(-) 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 d9652946ab..757f58b457 100644 --- a/application/src/test/java/org/thingsboard/server/controller/EntityQueryControllerTest.java +++ b/application/src/test/java/org/thingsboard/server/controller/EntityQueryControllerTest.java @@ -713,7 +713,8 @@ public class EntityQueryControllerTest extends AbstractControllerTest { // all devices with ownerName = TEST TENANT EntityCountQuery query = new EntityCountQuery(filter, List.of(activeAlarmTimeFilter, tenantOwnerNameFilter)); - countByQueryAndCheck(query, numOfDevices); + await().atMost(TIMEOUT, TimeUnit.SECONDS).until(() -> countByQuery(query), + result -> result == numOfDevices); // all devices with ownerName = TEST TENANT EntityCountQuery activeAlarmTimeToLongQuery = new EntityCountQuery(filter, List.of(activeAlarmTimeToLongFilter, tenantOwnerNameFilter)); diff --git a/docker/edqs.env b/docker/edqs.env index 3c91c92e54..33e64d0c96 100644 --- a/docker/edqs.env +++ b/docker/edqs.env @@ -2,6 +2,5 @@ ZOOKEEPER_ENABLED=true ZOOKEEPER_URL=zookeeper:2181 TB_KAFKA_SERVERS=kafka:9092 -TB_EDQS_STATS_ENABLED=false METRICS_ENABLED=true METRICS_ENDPOINTS_EXPOSE=prometheus diff --git a/msa/black-box-tests/src/test/java/org/thingsboard/server/msa/ContainerTestSuite.java b/msa/black-box-tests/src/test/java/org/thingsboard/server/msa/ContainerTestSuite.java index d22861e286..5491331d65 100644 --- a/msa/black-box-tests/src/test/java/org/thingsboard/server/msa/ContainerTestSuite.java +++ b/msa/black-box-tests/src/test/java/org/thingsboard/server/msa/ContainerTestSuite.java @@ -164,10 +164,8 @@ public class ContainerTestSuite { } // to trigger edqs synchronization - if (true) { - addToFile(targetDir, "tb-node.env", - Map.of("TB_EDQS_SYNC_ENABLED", "true")); - } + addToFile(targetDir, "tb-node.env", + Map.of("TB_EDQS_SYNC_ENABLED", "true")); testContainer = new DockerComposeContainerImpl<>(composeFiles) .withPull(false) diff --git a/msa/black-box-tests/src/test/java/org/thingsboard/server/msa/TestRestClient.java b/msa/black-box-tests/src/test/java/org/thingsboard/server/msa/TestRestClient.java index 3c61c65ef1..4e6825f2a9 100644 --- a/msa/black-box-tests/src/test/java/org/thingsboard/server/msa/TestRestClient.java +++ b/msa/black-box-tests/src/test/java/org/thingsboard/server/msa/TestRestClient.java @@ -114,28 +114,6 @@ public class TestRestClient { requestSpec.header(JWT_TOKEN_HEADER_PARAM, "Bearer " + token); } - public JsonNode getActivateRequest(String password) { - ObjectNode response = given().spec(requestSpec).get("/api/noauth/activate?activateToken={activateToken}", token) - .then() - .statusCode(HTTP_OK) - .extract() - .as(ObjectNode.class); - return JacksonUtil.newObjectNode() - .put("activateToken", this.token) - .put("password", password); - } - - public void activateAndLoginAsUser(JsonNode activateRequest) { - ObjectNode tokenInfo = given().spec(requestSpec).body(activateRequest) - .post("/api/noauth/activate") - .then() - .extract() - .as(ObjectNode.class); - token = tokenInfo.get("token").asText(); - refreshToken = tokenInfo.get("refreshToken").asText(); - requestSpec.header(JWT_TOKEN_HEADER_PARAM, "Bearer " + token); - } - public Tenant postTenant(Tenant tenant) { return given().spec(requestSpec).body(tenant) .post("/api/tenant") From 1b0b10a15aa15fa0204f26fc4d10f1138b91ac47 Mon Sep 17 00:00:00 2001 From: Oleksandra Matviienko Date: Wed, 5 Feb 2025 14:45:00 +0100 Subject: [PATCH 088/345] Usage of long refactored to int in LinkedHashMapRemoveEldest class Signed-off-by: Oleksandra Matviienko --- .../server/actors/ActorSystemContext.java | 2 +- .../common/util/LinkedHashMapRemoveEldest.java | 4 ++-- .../common/util/LinkedHashMapRemoveEldestTest.java | 14 +++++++------- 3 files changed, 10 insertions(+), 10 deletions(-) diff --git a/application/src/main/java/org/thingsboard/server/actors/ActorSystemContext.java b/application/src/main/java/org/thingsboard/server/actors/ActorSystemContext.java index 1c18a31f68..b13cf8e490 100644 --- a/application/src/main/java/org/thingsboard/server/actors/ActorSystemContext.java +++ b/application/src/main/java/org/thingsboard/server/actors/ActorSystemContext.java @@ -489,7 +489,7 @@ public class ActorSystemContext { @Value("${actors.session.max_concurrent_sessions_per_device:1}") @Getter - private long maxConcurrentSessionsPerDevice; + private int maxConcurrentSessionsPerDevice; @Value("${actors.session.sync.timeout:10000}") @Getter diff --git a/common/util/src/main/java/org/thingsboard/common/util/LinkedHashMapRemoveEldest.java b/common/util/src/main/java/org/thingsboard/common/util/LinkedHashMapRemoveEldest.java index 99f766d4f6..9798706d32 100644 --- a/common/util/src/main/java/org/thingsboard/common/util/LinkedHashMapRemoveEldest.java +++ b/common/util/src/main/java/org/thingsboard/common/util/LinkedHashMapRemoveEldest.java @@ -34,10 +34,10 @@ import java.util.function.BiConsumer; @ToString(callSuper = true) @EqualsAndHashCode(callSuper = true) public class LinkedHashMapRemoveEldest extends LinkedHashMap { - final long maxEntries; + final int maxEntries; final BiConsumer removalConsumer; - public LinkedHashMapRemoveEldest(long maxEntries, BiConsumer removalConsumer) { + public LinkedHashMapRemoveEldest(int maxEntries, BiConsumer removalConsumer) { this.maxEntries = maxEntries; this.removalConsumer = removalConsumer; } diff --git a/common/util/src/test/java/org/thingsboard/common/util/LinkedHashMapRemoveEldestTest.java b/common/util/src/test/java/org/thingsboard/common/util/LinkedHashMapRemoveEldestTest.java index e52dd895a0..277c9a95c9 100644 --- a/common/util/src/test/java/org/thingsboard/common/util/LinkedHashMapRemoveEldestTest.java +++ b/common/util/src/test/java/org/thingsboard/common/util/LinkedHashMapRemoveEldestTest.java @@ -27,10 +27,10 @@ import static org.hamcrest.MatcherAssert.assertThat; public class LinkedHashMapRemoveEldestTest { - public static final long MAX_ENTRIES = 10L; - long removeCount = 0; + public static final int MAX_ENTRIES = 10; + int removeCount = 0; - void removalConsumer(Long id, String name) { + void removalConsumer(Integer id, String name) { removeCount++; assertThat(id, is(Matchers.lessThan(MAX_ENTRIES))); assertThat(name, is(id.toString())); @@ -39,7 +39,7 @@ public class LinkedHashMapRemoveEldestTest { @Test public void givenMap_whenOverSized_thenVerifyRemovedEldest() { //given - LinkedHashMapRemoveEldest map = + LinkedHashMapRemoveEldest map = new LinkedHashMapRemoveEldest<>(MAX_ENTRIES, this::removalConsumer); assertThat(map.getMaxEntries(), is(MAX_ENTRIES)); @@ -49,14 +49,14 @@ public class LinkedHashMapRemoveEldestTest { assertThat(map.size(), is(0)); //when - for (long i = 0; i < MAX_ENTRIES * 2; i++) { + for (int i = 0; i < MAX_ENTRIES * 2; i++) { map.put(i, String.valueOf(i)); } //then - assertThat((long) map.size(), is(MAX_ENTRIES)); + assertThat( map.size(), is(MAX_ENTRIES)); assertThat(removeCount, is(MAX_ENTRIES)); - for (long i = MAX_ENTRIES; i < MAX_ENTRIES * 2; i++) { + for (int i = MAX_ENTRIES; i < MAX_ENTRIES * 2; i++) { assertThat(map.get(i), is(String.valueOf(i))); } } From 8eabe34ac6eb7297884bfa6b368640debc99ab8f Mon Sep 17 00:00:00 2001 From: dashevchenko Date: Tue, 25 Feb 2025 11:08:05 +0200 Subject: [PATCH 089/345] edqs docker image renamed --- docker/.env | 2 +- docker/docker-compose.edqs.yml | 4 ++-- .../java/org/thingsboard/server/msa/ContainerTestSuite.java | 4 ++-- .../thingsboard/server/msa/edqs/EdqsEntityDataQueryTest.java | 1 - msa/edqs/pom.xml | 2 +- 5 files changed, 6 insertions(+), 7 deletions(-) diff --git a/docker/.env b/docker/.env index 38724263fc..f6d9ebc0f8 100644 --- a/docker/.env +++ b/docker/.env @@ -14,7 +14,7 @@ COAP_TRANSPORT_DOCKER_NAME=tb-coap-transport LWM2M_TRANSPORT_DOCKER_NAME=tb-lwm2m-transport SNMP_TRANSPORT_DOCKER_NAME=tb-snmp-transport TB_VC_EXECUTOR_DOCKER_NAME=tb-vc-executor -EDQS_DOCKER_NAME=edqs +EDQS_DOCKER_NAME=tb-edqs TB_VERSION=latest diff --git a/docker/docker-compose.edqs.yml b/docker/docker-compose.edqs.yml index f989c12a2e..3a01a3c970 100644 --- a/docker/docker-compose.edqs.yml +++ b/docker/docker-compose.edqs.yml @@ -17,14 +17,14 @@ version: '3.0' services: - edqs-1: + tb-edqs-1: restart: always image: "${DOCKER_REPO}/${EDQS_DOCKER_NAME}:${TB_VERSION}" env_file: - edqs.env depends_on: - zookeeper - edqs-2: + tb-edqs-2: restart: always image: "${DOCKER_REPO}/${EDQS_DOCKER_NAME}:${TB_VERSION}" env_file: diff --git a/msa/black-box-tests/src/test/java/org/thingsboard/server/msa/ContainerTestSuite.java b/msa/black-box-tests/src/test/java/org/thingsboard/server/msa/ContainerTestSuite.java index 5491331d65..45e227ed03 100644 --- a/msa/black-box-tests/src/test/java/org/thingsboard/server/msa/ContainerTestSuite.java +++ b/msa/black-box-tests/src/test/java/org/thingsboard/server/msa/ContainerTestSuite.java @@ -186,8 +186,8 @@ public class ContainerTestSuite { .waitingFor("tb-vc-executor1", Wait.forLogMessage(TB_VC_LOG_REGEXP, 1).withStartupTimeout(CONTAINER_STARTUP_TIMEOUT)) .waitingFor("tb-vc-executor2", Wait.forLogMessage(TB_VC_LOG_REGEXP, 1).withStartupTimeout(CONTAINER_STARTUP_TIMEOUT)) .waitingFor("tb-js-executor", Wait.forLogMessage(TB_JS_EXECUTOR_LOG_REGEXP, 1).withStartupTimeout(CONTAINER_STARTUP_TIMEOUT)) - .waitingFor("edqs-1", Wait.forHttp("/api/edqs/ready").withStartupTimeout(CONTAINER_STARTUP_TIMEOUT)) - .waitingFor("edqs-2", Wait.forHttp("/api/edqs/ready").withStartupTimeout(CONTAINER_STARTUP_TIMEOUT)); + .waitingFor("tb-edqs-1", Wait.forHttp("/api/edqs/ready").withStartupTimeout(CONTAINER_STARTUP_TIMEOUT)) + .waitingFor("tb-edqs-2", Wait.forHttp("/api/edqs/ready").withStartupTimeout(CONTAINER_STARTUP_TIMEOUT)); testContainer.start(); setActive(true); } catch (Exception e) { diff --git a/msa/black-box-tests/src/test/java/org/thingsboard/server/msa/edqs/EdqsEntityDataQueryTest.java b/msa/black-box-tests/src/test/java/org/thingsboard/server/msa/edqs/EdqsEntityDataQueryTest.java index 1317011cf0..55a5f11821 100644 --- a/msa/black-box-tests/src/test/java/org/thingsboard/server/msa/edqs/EdqsEntityDataQueryTest.java +++ b/msa/black-box-tests/src/test/java/org/thingsboard/server/msa/edqs/EdqsEntityDataQueryTest.java @@ -53,7 +53,6 @@ import static org.thingsboard.server.msa.ui.utils.EntityPrototypes.defaultCustom import static org.thingsboard.server.msa.ui.utils.EntityPrototypes.defaultDeviceProfile; import static org.thingsboard.server.msa.ui.utils.EntityPrototypes.defaultTenantAdmin; -@DisableUIListeners public class EdqsEntityDataQueryTest extends AbstractContainerTest { private TenantId tenantId; diff --git a/msa/edqs/pom.xml b/msa/edqs/pom.xml index 4874c06e4b..7e7a244cc2 100644 --- a/msa/edqs/pom.xml +++ b/msa/edqs/pom.xml @@ -35,7 +35,7 @@ UTF-8 ${basedir}/../.. edqs - edqs + tb-edqs /var/log/${pkg.name} /usr/share/${pkg.name} pre-integration-test From 6c8887995a9b5de7beb3ae1243c6dc9e45bda120 Mon Sep 17 00:00:00 2001 From: dashevchenko Date: Tue, 25 Feb 2025 13:23:26 +0200 Subject: [PATCH 090/345] fixed license, included edqs tests to test suite --- docker/docker-compose.edqs.yml | 2 +- edqs/src/main/conf/edqs.conf | 2 +- edqs/src/main/conf/logback.xml | 2 +- .../server/msa/ContainerTestSuite.java | 3 ++- .../thingsboard/server/msa/TestRestClient.java | 10 +++++++--- .../msa/edqs/EdqsEntityDataQueryTest.java | 18 +++++++++++------- msa/black-box-tests/src/test/resources/all.xml | 10 ++++++++++ msa/edqs/docker/Dockerfile | 2 +- msa/edqs/docker/start-tb-edqs.sh | 2 +- msa/edqs/pom.xml | 2 +- 10 files changed, 36 insertions(+), 17 deletions(-) diff --git a/docker/docker-compose.edqs.yml b/docker/docker-compose.edqs.yml index 3a01a3c970..c50740faeb 100644 --- a/docker/docker-compose.edqs.yml +++ b/docker/docker-compose.edqs.yml @@ -1,5 +1,5 @@ # -# Copyright © 2016-2024 The Thingsboard Authors +# 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. diff --git a/edqs/src/main/conf/edqs.conf b/edqs/src/main/conf/edqs.conf index ae880ef7e8..3f96fd590d 100644 --- a/edqs/src/main/conf/edqs.conf +++ b/edqs/src/main/conf/edqs.conf @@ -1,5 +1,5 @@ # -# Copyright © 2016-2024 The Thingsboard Authors +# 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. diff --git a/edqs/src/main/conf/logback.xml b/edqs/src/main/conf/logback.xml index 4ff57d48ab..850a28b212 100644 --- a/edqs/src/main/conf/logback.xml +++ b/edqs/src/main/conf/logback.xml @@ -1,7 +1,7 @@ +
+ + + + warning + + + + + + +
+ +
+
diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/map/map-action-button-row.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/map/map-action-button-row.component.ts new file mode 100644 index 0000000000..a28e6fccb6 --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/map/map-action-button-row.component.ts @@ -0,0 +1,100 @@ +/// +/// Copyright © 2016-2024 The Thingsboard Authors +/// +/// Licensed under the Apache License, Version 2.0 (the "License"); +/// you may not use this file except in compliance with the License. +/// You may obtain a copy of the License at +/// +/// http://www.apache.org/licenses/LICENSE-2.0 +/// +/// Unless required by applicable law or agreed to in writing, software +/// distributed under the License is distributed on an "AS IS" BASIS, +/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +/// See the License for the specific language governing permissions and +/// limitations under the License. +/// + +import { Component, EventEmitter, forwardRef, Output } from '@angular/core'; +import { + ControlValueAccessor, + FormBuilder, + FormGroup, + NG_VALIDATORS, + NG_VALUE_ACCESSOR, + ValidationErrors, + Validator +} from '@angular/forms'; +import { MapActionButtonSettings } from '@home/components/widget/lib/maps/models/map.models'; +import { WidgetAction, WidgetActionType, widgetType } from '@shared/models/widget.models'; +import { takeUntilDestroyed } from '@angular/core/rxjs-interop'; +import { isEmptyStr } from '@core/utils'; + +@Component({ + selector: 'tb-map-action-button-row', + templateUrl: 'map-action-button-row.component.html', + providers: [{ + provide: NG_VALUE_ACCESSOR, + useExisting: forwardRef(() => MapActionButtonRowComponent), + multi: true + }, + { + provide: NG_VALIDATORS, + useExisting: forwardRef(() => MapActionButtonRowComponent), + multi: true + }] +}) +export class MapActionButtonRowComponent implements ControlValueAccessor, Validator { + + @Output() + buttonRemoved = new EventEmitter(); + + mapActionButton = this.fb.group({ + label: [''], + icon: [''], + color: [''], + action: this.fb.control(null) + }, {validators: this.validateButtonConfig()}); + + additionalWidgetActionTypes = [WidgetActionType.placeMapItem]; + readonly widgetType = widgetType; + + private propagateChange = (_val: any) => {}; + + constructor(private fb: FormBuilder) { + this.mapActionButton.valueChanges.pipe( + takeUntilDestroyed() + ).subscribe(value => this.propagateChange(value)) + } + + registerOnChange(fn: any) { + this.propagateChange = fn; + } + + registerOnTouched(_fn: any): void { } + + setDisabledState(isDisabled: boolean) { + if (isDisabled) { + this.mapActionButton.disable({emitEvent: false}); + } else { + this.mapActionButton.enable({emitEvent: false}); + } + } + + validate(): ValidationErrors | null { + return this.mapActionButton.valid ? null : { + mapButtonAction: false + }; + } + + writeValue(value: MapActionButtonSettings) { + this.mapActionButton.patchValue(value, {emitEvent: false}); + } + + private validateButtonConfig() { + return (c: FormGroup) => { + return !c.value.icon && isEmptyStr(c.value.label) ? { + invalidButtonConfig: true + } : null; + }; + } +} diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/map/map-action-buttons-settings.component.html b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/map/map-action-buttons-settings.component.html new file mode 100644 index 0000000000..ddc4777134 --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/map/map-action-buttons-settings.component.html @@ -0,0 +1,63 @@ + +
+ + + {{ 'widgets.maps.map-action.map-action-buttons' | translate }} + + +
+
+
widgets.maps.map-action.label
+
widgets.maps.map-action.icon
+
widgets.maps.map-action.color
+
widgets.maps.map-action.action
+
+
+
+ @for (mapAction of mapActionButtonsForm.controls; track mapAction) { +
+ + +
+ +
+
+ } @empty { + {{ 'widgets.maps.map-action.no-action-buttons-configured' | translate }} + } +
+
+ +
+
+
diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/map/map-action-buttons-settings.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/map/map-action-buttons-settings.component.ts new file mode 100644 index 0000000000..8c304a7ca5 --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/map/map-action-buttons-settings.component.ts @@ -0,0 +1,107 @@ +/// +/// Copyright © 2016-2024 The Thingsboard Authors +/// +/// Licensed under the Apache License, Version 2.0 (the "License"); +/// you may not use this file except in compliance with the License. +/// You may obtain a copy of the License at +/// +/// http://www.apache.org/licenses/LICENSE-2.0 +/// +/// Unless required by applicable law or agreed to in writing, software +/// distributed under the License is distributed on an "AS IS" BASIS, +/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +/// See the License for the specific language governing permissions and +/// limitations under the License. +/// + +import { Component, forwardRef } from '@angular/core'; +import { + ControlValueAccessor, + FormBuilder, + NG_VALIDATORS, + NG_VALUE_ACCESSOR, + ValidationErrors, + Validator +} from '@angular/forms'; +import { + defaultMapActionButtonSettings, + MapActionButtonSettings +} from '@home/components/widget/lib/maps/models/map.models'; +import { CdkDragDrop } from '@angular/cdk/drag-drop'; +import { takeUntilDestroyed } from '@angular/core/rxjs-interop'; + +@Component({ + selector: 'tb-map-action-button-settings', + templateUrl: './map-action-buttons-settings.component.html', + providers: [{ + provide: NG_VALUE_ACCESSOR, + useExisting: forwardRef(() => MapActionButtonsSettingsComponent), + multi: true + }, + { + provide: NG_VALIDATORS, + useExisting: forwardRef(() => MapActionButtonsSettingsComponent), + multi: true + }] +}) +export class MapActionButtonsSettingsComponent implements ControlValueAccessor, Validator { + + mapActionButtonsForm = this.fb.array([]); + + private propagateChange = (_val: any) => {}; + + constructor(private fb: FormBuilder) { + this.mapActionButtonsForm.valueChanges.pipe( + takeUntilDestroyed() + ).subscribe(value => this.propagateChange(value)); + } + + registerOnChange(fn: any): void { + this.propagateChange = fn; + } + + registerOnTouched(_fn: any): void { } + + setDisabledState(isDisabled: boolean): void { + if (isDisabled) { + this.mapActionButtonsForm.disable({emitEvent: false}); + } else { + this.mapActionButtonsForm.enable({emitEvent: false}); + } + } + + validate(): ValidationErrors | null { + return this.mapActionButtonsForm.valid ? null : { + mapActionButtons: false + }; + } + + writeValue(buttons: MapActionButtonSettings[] = []) { + if (buttons?.length === this.mapActionButtonsForm.length) { + this.mapActionButtonsForm.patchValue(buttons, {emitEvent: false}); + } else { + this.mapActionButtonsForm.clear({emitEvent: false}); + buttons.forEach( + button => this.mapActionButtonsForm.push(this.fb.control(button), {emitEvent: false}) + ); + } + } + + get dragEnabled(): boolean { + return this.mapActionButtonsForm.length > 1; + } + + buttonDrop(event: CdkDragDrop) { + const actionButton = this.mapActionButtonsForm.at(event.previousIndex); + this.mapActionButtonsForm.removeAt(event.previousIndex, {emitEvent: false}); + this.mapActionButtonsForm.insert(event.currentIndex, actionButton); + } + + addButton() { + this.mapActionButtonsForm.push(this.fb.control(defaultMapActionButtonSettings)); + } + + removeButton(index: number) { + this.mapActionButtonsForm.removeAt(index); + } +} 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 d2bc0e33e4..0cf6b9d81f 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 @@ -93,6 +93,8 @@
-
+
diff --git a/ui-ngx/src/app/modules/home/components/widget/widget-config.component.ts b/ui-ngx/src/app/modules/home/components/widget/widget-config.component.ts index 1721a7f068..78dd827403 100644 --- a/ui-ngx/src/app/modules/home/components/widget/widget-config.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/widget-config.component.ts @@ -322,7 +322,7 @@ export class WidgetConfigComponent extends PageComponent implements OnInit, OnDe private buildHeader() { this.headerOptions.length = 0; - if (this.widgetType !== widgetType.static) { + if (this.displayData) { this.headerOptions.push( { name: this.translate.instant('widget-config.data'), @@ -710,6 +710,10 @@ export class WidgetConfigComponent extends PageComponent implements OnInit, OnDe return this.modelValue?.basicModeDirective?.length && !this.basicModeDirectiveError; } + public get displayData(): boolean { + return !this.modelValue?.typeParameters?.hideDataTab && this.widgetType !== widgetType.static; + } + public get displayAppearance(): boolean { return this.displayAppearanceDataSettings || this.displayAdvancedAppearance; } diff --git a/ui-ngx/src/app/shared/models/widget.models.ts b/ui-ngx/src/app/shared/models/widget.models.ts index 5950dcf911..e5e125e171 100644 --- a/ui-ngx/src/app/shared/models/widget.models.ts +++ b/ui-ngx/src/app/shared/models/widget.models.ts @@ -185,6 +185,7 @@ export interface WidgetTypeParameters { previewHeight?: string; embedTitlePanel?: boolean; overflowVisible?: boolean; + hideDataTab?: boolean; hideDataSettings?: boolean; defaultDataKeysFunction?: (configComponent: any, configData: any) => DataKey[]; defaultLatestDataKeysFunction?: (configComponent: any, configData: any) => DataKey[]; 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 67657a20e9..b282e7ce24 100644 --- a/ui-ngx/src/assets/locale/locale.constant-en_US.json +++ b/ui-ngx/src/assets/locale/locale.constant-en_US.json @@ -6803,8 +6803,19 @@ "max-value": "Maximum value" }, "maps": { + "map-type": "Map type", + "map-type-map": "Map", + "map-type-image": "Image", + "overlays": "Overlays", + "markers": "Markers", + "polygons": "Polygons", + "circles": "Circles", "layers": "Layers", "map-layers": "Map layers", + "tencent-provider-normal": "Tencent.Normal", + "tencent-provider-satellite": "Tencent.Satellite", + "tencent-provider-terrain": "Tencent.Terrain", + "custom": "Custom", "select-entity": "Select entity", "select-entity-hint": "Hint: after selection click at the map to set position", "tooltips": { diff --git a/ui-ngx/src/typings/leaflet-extend-tb.d.ts b/ui-ngx/src/typings/leaflet-extend-tb.d.ts index fd11f317a2..9b8f357c18 100644 --- a/ui-ngx/src/typings/leaflet-extend-tb.d.ts +++ b/ui-ngx/src/typings/leaflet-extend-tb.d.ts @@ -15,12 +15,21 @@ /// import { FormattedData } from '@shared/models/widget.models'; +import L from 'leaflet'; +import { TileLayerOptions } from 'leaflet'; // redeclare module, maintains compatibility with @types/leaflet declare module 'leaflet' { interface MarkerOptions { tbMarkerData?: FormattedData; } + + interface TileLayer { + _url: string; + _getSubdomain(tilePoint: L.Coords): string; + _globalTileRange: L.Bounds; + } + namespace TB { interface SidebarControlOptions extends ControlOptions { @@ -48,6 +57,7 @@ declare module 'leaflet' { interface LayerData { title: string; + attributionPrefix?: string; layer: Layer; mini: Layer; } @@ -65,5 +75,23 @@ declare module 'leaflet' { function sidebarPane(options: O): SidebarPaneControl; function layers(options: LayersControlOptions): LayersControl; + + namespace TileLayer { + + interface ChinaProvidersData { + [provider: string]: { + [type: string]: string; + Subdomains: string; + }; + } + + class ChinaProvider extends L.TileLayer { + constructor(type: string, options?: TileLayerOptions); + } + } + + namespace tileLayer { + function chinaProvider(type: string, options?: TileLayerOptions): TileLayer.ChinaProvider; + } } } From 9cd62f39ea6298e05946c81b542f64dc2145857c Mon Sep 17 00:00:00 2001 From: Igor Kulikov Date: Fri, 3 Jan 2025 19:51:40 +0200 Subject: [PATCH 012/345] UI: Fix map widget default config. Fix import dialog validation. --- application/src/main/data/json/system/widget_types/map.json | 2 +- .../home/components/widget/lib/maps/map-widget.models.ts | 2 +- ui-ngx/src/app/shared/import-export/import-dialog.component.ts | 1 + 3 files changed, 3 insertions(+), 2 deletions(-) diff --git a/application/src/main/data/json/system/widget_types/map.json b/application/src/main/data/json/system/widget_types/map.json index 1d3443c55d..8019b104fb 100644 --- a/application/src/main/data/json/system/widget_types/map.json +++ b/application/src/main/data/json/system/widget_types/map.json @@ -17,7 +17,7 @@ "settingsDirective": "tb-map-widget-settings", "hasBasicMode": true, "basicModeDirective": "tb-map-basic-config", - "defaultConfig": "{\"datasources\":[],\"timewindow\":{\"realtime\":{\"timewindowMs\":60000}},\"showTitle\":true,\"backgroundColor\":\"rgba(0, 0, 0, 0)\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"0px\",\"settings\":{\"mapType\":\"geoMap\",\"layers\":[{\"provider\":\"openstreet-map\",\"layerType\":\"openStreetMapnik\"},{\"provider\":\"openstreet-map\",\"layerType\":\"openStreetHot\"},{\"provider\":\"openstreet-map\",\"layerType\":\"esriWorldStreetMap\"},{\"provider\":\"openstreet-map\",\"layerType\":\"esriWorldTopoMap\"},{\"provider\":\"openstreet-map\",\"layerType\":\"esriWorldImagery\"},{\"provider\":\"openstreet-map\",\"layerType\":\"cartoDbPositron\"},{\"provider\":\"openstreet-map\",\"layerType\":\"cartoDbDarkMatter\"},{\"provider\":\"google-map\",\"layerType\":\"roadmap\",\"apiKey\":\"AIzaSyDoEx2kaGz3PxwbI9T7ccTSg5xjdw8Nw8Q\"},{\"provider\":\"google-map\",\"layerType\":\"satellite\",\"apiKey\":\"AIzaSyDoEx2kaGz3PxwbI9T7ccTSg5xjdw8Nw8Q\"},{\"provider\":\"google-map\",\"layerType\":\"hybrid\",\"apiKey\":\"AIzaSyDoEx2kaGz3PxwbI9T7ccTSg5xjdw8Nw8Q\"},{\"provider\":\"google-map\",\"layerType\":\"terrain\",\"apiKey\":\"AIzaSyDoEx2kaGz3PxwbI9T7ccTSg5xjdw8Nw8Q\"},{\"provider\":\"tencent-map\",\"layerType\":\"tencentNormal\"},{\"provider\":\"tencent-map\",\"layerType\":\"tencentSatellite\"},{\"provider\":\"tencent-map\",\"layerType\":\"tencentTerrain\"},{\"provider\":\"here\",\"layerType\":\"hereNormalDay\",\"apiKey\":\"kVXykxAfZ6LS4EbCTO02soFVfjA7HoBzNVVH9u7nzoE\"},{\"provider\":\"here\",\"layerType\":\"hereNormalNight\",\"apiKey\":\"kVXykxAfZ6LS4EbCTO02soFVfjA7HoBzNVVH9u7nzoE\"},{\"provider\":\"here\",\"layerType\":\"hereHybridDay\",\"apiKey\":\"kVXykxAfZ6LS4EbCTO02soFVfjA7HoBzNVVH9u7nzoE\"},{\"provider\":\"here\",\"layerType\":\"hereTerrainDay\",\"apiKey\":\"kVXykxAfZ6LS4EbCTO02soFVfjA7HoBzNVVH9u7nzoE\"},{\"provider\":\"custom\",\"tileUrl\":\"https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png\",\"label\":\"Custom 1\"},{\"provider\":\"custom\",\"tileUrl\":\"http://a.tile2.opencyclemap.org/transport/{z}/{x}/{y}.png\",\"label\":\"Custom 2\"},{\"provider\":\"custom\",\"tileUrl\":\"http://b.tile2.opencyclemap.org/transport/{z}/{x}/{y}.png\",\"label\":\"Custom 3\"}],\"markers\":[],\"polygons\":[],\"circles\":[],\"additionalDataSources\":[],\"controlsPosition\":\"topleft\",\"zoomActions\":[\"scroll\",\"doubleClick\",\"controlButtons\"],\"fitMapBounds\":true,\"useDefaultCenterPosition\":false,\"defaultCenterPosition\":\"0,0\",\"defaultZoomLevel\":null,\"mapPageSize\":16384,\"background\":{\"type\":\"color\",\"color\":\"#fff\",\"overlay\":{\"enabled\":false,\"color\":\"rgba(255,255,255,0.72)\",\"blur\":3}},\"padding\":\"12px\"},\"title\":\"Map\",\"useDashboardTimewindow\":true,\"displayTimewindow\":true,\"showTitleIcon\":false,\"titleTooltip\":\"\",\"dropShadow\":true,\"enableFullscreen\":true,\"widgetStyle\":{},\"widgetCss\":\"\",\"titleStyle\":{\"fontSize\":\"16px\",\"fontWeight\":400},\"pageSize\":1024,\"noDataDisplayMessage\":\"\",\"configMode\":\"basic\",\"titleFont\":null,\"titleColor\":null,\"margin\":\"0px\",\"borderRadius\":\"0px\",\"iconSize\":\"24px\",\"titleIcon\":\"map\",\"iconColor\":\"#1F6BDD\",\"actions\":{}}" + "defaultConfig": "{\"datasources\":[],\"timewindow\":{\"realtime\":{\"timewindowMs\":60000}},\"showTitle\":true,\"backgroundColor\":\"rgba(0, 0, 0, 0)\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"0px\",\"settings\":{},\"title\":\"Map\",\"useDashboardTimewindow\":true,\"displayTimewindow\":true,\"showTitleIcon\":false,\"titleTooltip\":\"\",\"dropShadow\":true,\"enableFullscreen\":true,\"widgetStyle\":{},\"widgetCss\":\"\",\"titleStyle\":{\"fontSize\":\"16px\",\"fontWeight\":400},\"pageSize\":1024,\"noDataDisplayMessage\":\"\",\"configMode\":\"basic\",\"titleFont\":null,\"titleColor\":null,\"margin\":\"0px\",\"borderRadius\":\"0px\",\"iconSize\":\"24px\",\"titleIcon\":\"map\",\"iconColor\":\"#1F6BDD\",\"actions\":{}}" }, "resources": [], "scada": false, diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/maps/map-widget.models.ts b/ui-ngx/src/app/modules/home/components/widget/lib/maps/map-widget.models.ts index e023b9557c..bc0d790036 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/maps/map-widget.models.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/maps/map-widget.models.ts @@ -34,5 +34,5 @@ export const mapWidgetDefaultSettings: MapWidgetSettings = blur: 3 } }, - padding: '6px' + padding: '8px' } as MapWidgetSettings); diff --git a/ui-ngx/src/app/shared/import-export/import-dialog.component.ts b/ui-ngx/src/app/shared/import-export/import-dialog.component.ts index e4256229c4..16800ceadc 100644 --- a/ui-ngx/src/app/shared/import-export/import-dialog.component.ts +++ b/ui-ngx/src/app/shared/import-export/import-dialog.component.ts @@ -78,6 +78,7 @@ export class ImportDialogComponent extends DialogComponent { this.importTypeChanged(); }); + this.importTypeChanged(); } isErrorState(control: UntypedFormControl | null, form: FormGroupDirective | NgForm | null): boolean { From 632df0de8b83a22463704d21a2d68cd6a42f43c7 Mon Sep 17 00:00:00 2001 From: mpetrov Date: Mon, 6 Jan 2025 12:05:32 +0200 Subject: [PATCH 013/345] Changed gateway dist branch --- application/src/main/resources/thingsboard.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/application/src/main/resources/thingsboard.yml b/application/src/main/resources/thingsboard.yml index 1fa09cd9bb..f8d0cc297e 100644 --- a/application/src/main/resources/thingsboard.yml +++ b/application/src/main/resources/thingsboard.yml @@ -1271,7 +1271,7 @@ transport: # URL of gateways dashboard repository repository_url: "${TB_GATEWAY_DASHBOARD_SYNC_REPOSITORY_URL:https://github.com/thingsboard/gateway-management-extensions-dist.git}" # Branch of gateways dashboard repository to work with - branch: "${TB_GATEWAY_DASHBOARD_SYNC_BRANCH:main}" + branch: "${TB_GATEWAY_DASHBOARD_SYNC_BRANCH:develop}" # Fetch frequency in hours for gateways dashboard repository fetch_frequency: "${TB_GATEWAY_DASHBOARD_SYNC_FETCH_FREQUENCY:24}" From 7b02d9079bcfce29d2f714d379b54d606065ce14 Mon Sep 17 00:00:00 2001 From: Igor Kulikov Date: Mon, 6 Jan 2025 19:28:33 +0200 Subject: [PATCH 014/345] UI: Map layer settings. --- .../basic/map/map-basic-config.component.ts | 4 +- .../widget/lib/maps/leaflet/leaflet-tb.ts | 9 +- .../components/widget/lib/maps/map-layer.ts | 120 ++++----- .../components/widget/lib/maps/map.models.ts | 182 +++++++++----- .../home/components/widget/lib/maps/map.ts | 8 +- .../common/map/map-layer-row.component.html | 84 +++++++ .../common/map/map-layer-row.component.scss | 36 +++ .../common/map/map-layer-row.component.ts | 230 ++++++++++++++++++ .../map-layer-settings-panel.component.html | 101 ++++++++ .../map-layer-settings-panel.component.scss | 49 ++++ .../map/map-layer-settings-panel.component.ts | 148 +++++++++++ .../common/map/map-layers.component.html | 57 +++++ .../common/map/map-layers.component.scss | 39 +++ .../common/map/map-layers.component.ts | 156 ++++++++++++ .../common/map/map-settings.component.html | 16 +- .../common/map/map-settings.component.ts | 24 +- .../common/widget-settings-common.module.ts | 8 + .../map/map-widget-settings.component.ts | 4 +- .../assets/locale/locale.constant-en_US.json | 76 +++++- 19 files changed, 1193 insertions(+), 158 deletions(-) create mode 100644 ui-ngx/src/app/modules/home/components/widget/lib/settings/common/map/map-layer-row.component.html create mode 100644 ui-ngx/src/app/modules/home/components/widget/lib/settings/common/map/map-layer-row.component.scss create mode 100644 ui-ngx/src/app/modules/home/components/widget/lib/settings/common/map/map-layer-row.component.ts create mode 100644 ui-ngx/src/app/modules/home/components/widget/lib/settings/common/map/map-layer-settings-panel.component.html create mode 100644 ui-ngx/src/app/modules/home/components/widget/lib/settings/common/map/map-layer-settings-panel.component.scss create mode 100644 ui-ngx/src/app/modules/home/components/widget/lib/settings/common/map/map-layer-settings-panel.component.ts create mode 100644 ui-ngx/src/app/modules/home/components/widget/lib/settings/common/map/map-layers.component.html create mode 100644 ui-ngx/src/app/modules/home/components/widget/lib/settings/common/map/map-layers.component.scss create mode 100644 ui-ngx/src/app/modules/home/components/widget/lib/settings/common/map/map-layers.component.ts diff --git a/ui-ngx/src/app/modules/home/components/widget/config/basic/map/map-basic-config.component.ts b/ui-ngx/src/app/modules/home/components/widget/config/basic/map/map-basic-config.component.ts index fb36874e5e..74c57519b7 100644 --- a/ui-ngx/src/app/modules/home/components/widget/config/basic/map/map-basic-config.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/config/basic/map/map-basic-config.component.ts @@ -21,7 +21,7 @@ import { AppState } from '@core/core.state'; import { WidgetConfigComponent } from '@home/components/widget/widget-config.component'; import { UntypedFormBuilder, UntypedFormGroup, Validators } from '@angular/forms'; import { WidgetConfigComponentData } from '@home/models/widget-component.models'; -import { isUndefined, mergeDeep } from '@core/utils'; +import { isUndefined, mergeDeep, mergeDeepIgnoreArray } from '@core/utils'; import { mapWidgetDefaultSettings, MapWidgetSettings } from '@home/components/widget/lib/maps/map-widget.models'; import { cssSizeToStrSize, resolveCssSize } from '@shared/models/widget-settings.models'; import { WidgetConfig } from '@shared/models/widget.models'; @@ -47,7 +47,7 @@ export class MapBasicConfigComponent extends BasicWidgetConfigComponent { } protected onConfigSet(configData: WidgetConfigComponentData) { - const settings: MapWidgetSettings = mergeDeep({} as MapWidgetSettings, + const settings: MapWidgetSettings = mergeDeepIgnoreArray({} as MapWidgetSettings, mapWidgetDefaultSettings, configData.config.settings as MapWidgetSettings); const iconSize = resolveCssSize(configData.config.iconSize); this.mapWidgetConfigForm = this.fb.group({ diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/maps/leaflet/leaflet-tb.ts b/ui-ngx/src/app/modules/home/components/widget/lib/maps/leaflet/leaflet-tb.ts index 1dd32220ba..660cf1dafb 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/maps/leaflet/leaflet-tb.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/maps/leaflet/leaflet-tb.ts @@ -15,6 +15,7 @@ /// import L, { Coords, TB, TileLayerOptions } from 'leaflet'; +import { guid } from '@core/utils'; class SidebarControl extends L.Control { @@ -133,7 +134,6 @@ class SidebarPaneControl extends L.Contr if (!this.button.hasClass("disabled")) { this.options.sidebar.togglePane(this.$ui, this.button); } - //$(".leaflet-control .control-button").tooltip("hide"); } } @@ -143,13 +143,14 @@ class LayersControl extends SidebarPaneControl { } public onAddPane(map: L.Map, button: JQuery, $ui: JQuery, toggle: (e: JQuery.MouseEventBase) => void) { + const paneId = guid(); const layers = this.options.layers; const baseSection = $("
") .attr('class', 'tb-layers-container') .appendTo($ui); layers.forEach((layerData, i) => { - const id = 'map-ui-layer-' + i; + const id = `map-ui-layer-${paneId}-${i}`; const buttonContainer = $('
') .appendTo(baseSection); const mapContainer = $('
') @@ -191,7 +192,9 @@ class LayersControl extends SidebarPaneControl { }); - input.on('click', () => { + input.on('click', (e: JQuery.MouseEventBase) => { + e.stopPropagation(); + e.preventDefault(); layers.forEach((other) => { if (other.layer === layerData.layer) { map.addLayer(other.layer); diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/maps/map-layer.ts b/ui-ngx/src/app/modules/home/components/widget/lib/maps/map-layer.ts index c10208b9a7..ddea67c613 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/maps/map-layer.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/maps/map-layer.ts @@ -15,20 +15,18 @@ /// import { - CustomMapLayerSettings, defaultCustomMapLayerSettings, - defaultGoogleMapLayerSettings, defaultHereMapLayerSettings, + CustomMapLayerSettings, + defaultCustomMapLayerSettings, + defaultGoogleMapLayerSettings, + defaultHereMapLayerSettings, + defaultLayerTitle, defaultOpenStreetMapLayerSettings, defaultTencentMapLayerSettings, - GoogleLayerType, GoogleMapLayerSettings, - googleMapLayerTranslationMap, hereLayerTranslationMap, HereLayerType, HereMapLayerSettings, + HereMapLayerSettings, MapLayerSettings, MapProvider, - OpenStreetLayerType, OpenStreetMapLayerSettings, - openStreetMapLayerTranslationMap, - tencentLayerTranslationMap, - TencentLayerType, TencentMapLayerSettings } from '@home/components/widget/lib/maps/map.models'; import { WidgetContext } from '@home/models/widget-component.models'; @@ -46,10 +44,10 @@ export abstract class TbMapLayer { inputSettings: DeepPartial) { switch (inputSettings.provider) { - case MapProvider.google: - return new TbGoogleMapLayer(ctx, inputSettings); case MapProvider.openstreet: return new TbOpenStreetMapLayer(ctx, inputSettings); + case MapProvider.google: + return new TbGoogleMapLayer(ctx, inputSettings); case MapProvider.tencent: return new TbTencentMapLayer(ctx, inputSettings); case MapProvider.here: @@ -66,21 +64,6 @@ export abstract class TbMapLayer { this.settings = mergeDeep({} as S, this.defaultSettings(), this.inputSettings as S); } - protected abstract defaultSettings(): S; - - protected title(): string { - const customTranslate = this.ctx.$injector.get(CustomTranslatePipe); - if (this.settings.label) { - return customTranslate.transform(this.settings.label); - } else { - return this.generateTitle(); - } - } - - protected abstract generateTitle(): string; - - protected abstract createLayer(): Observable; - public loadLayer(theMap: L.Map): Observable { return this.createLayer().pipe( switchMap((layer) => { @@ -107,6 +90,47 @@ export abstract class TbMapLayer { }) ); } + + private title(): string { + const customTranslate = this.ctx.$injector.get(CustomTranslatePipe); + if (this.settings.label) { + return customTranslate.transform(this.settings.label); + } else { + return this.generateTitle(); + } + } + + private generateTitle(): string { + const translationKey = defaultLayerTitle(this.settings); + if (translationKey) { + return this.ctx.translate.instant(translationKey); + } else { + return 'Unknown'; + } + }; + + protected abstract defaultSettings(): S; + + protected abstract createLayer(): Observable; + +} + +class TbOpenStreetMapLayer extends TbMapLayer { + + constructor(protected ctx: WidgetContext, + protected inputSettings: DeepPartial) { + super(ctx, inputSettings); + } + + protected defaultSettings(): OpenStreetMapLayerSettings { + return defaultOpenStreetMapLayerSettings; + } + + protected createLayer(): Observable { + const layer = L.tileLayer.provider(this.settings.layerType); + return of(layer); + } + } class TbGoogleMapLayer extends TbMapLayer { @@ -122,11 +146,6 @@ class TbGoogleMapLayer extends TbMapLayer { return defaultGoogleMapLayerSettings; } - protected generateTitle(): string { - const layerType = GoogleLayerType[this.settings.layerType]; - return this.ctx.translate.instant(googleMapLayerTranslationMap.get(layerType)); - } - protected createLayer(): Observable { return this.loadGoogle().pipe( map((loaded) => { @@ -162,29 +181,6 @@ class TbGoogleMapLayer extends TbMapLayer { } } -class TbOpenStreetMapLayer extends TbMapLayer { - - constructor(protected ctx: WidgetContext, - protected inputSettings: DeepPartial) { - super(ctx, inputSettings); - } - - protected defaultSettings(): OpenStreetMapLayerSettings { - return defaultOpenStreetMapLayerSettings; - } - - protected generateTitle(): string { - const layerType = OpenStreetLayerType[this.settings.layerType]; - return this.ctx.translate.instant(openStreetMapLayerTranslationMap.get(layerType)); - } - - protected createLayer(): Observable { - const layer = L.tileLayer.provider(OpenStreetLayerType[this.settings.layerType]); - return of(layer); - } - -} - class TbTencentMapLayer extends TbMapLayer { constructor(protected ctx: WidgetContext, @@ -196,13 +192,8 @@ class TbTencentMapLayer extends TbMapLayer { return defaultTencentMapLayerSettings; } - protected generateTitle(): string { - const layerType = TencentLayerType[this.settings.layerType]; - return this.ctx.translate.instant(tencentLayerTranslationMap.get(layerType)); - } - protected createLayer(): Observable { - const layer = L.TB.tileLayer.chinaProvider(TencentLayerType[this.settings.layerType], { + const layer = L.TB.tileLayer.chinaProvider(this.settings.layerType, { attribution: '©2024 Tencent - GS(2023)1171号' }); return of(layer); @@ -221,13 +212,8 @@ class TbHereMapLayer extends TbMapLayer { return defaultHereMapLayerSettings; } - protected generateTitle(): string { - const layerType = HereLayerType[this.settings.layerType]; - return this.ctx.translate.instant(hereLayerTranslationMap.get(layerType)); - } - protected createLayer(): Observable { - const layer = L.tileLayer.provider(HereLayerType[this.settings.layerType], {useV3: true, apiKey: this.settings.apiKey} as any); + const layer = L.tileLayer.provider(this.settings.layerType, {useV3: true, apiKey: this.settings.apiKey} as any); return of(layer); } @@ -244,10 +230,6 @@ class TbCustomMapLayer extends TbMapLayer { return defaultCustomMapLayerSettings; } - protected generateTitle(): string { - return this.ctx.translate.instant('widgets.maps.custom'); - } - protected createLayer(): Observable { const layer = L.tileLayer(this.settings.tileUrl); return of(layer); diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/maps/map.models.ts b/ui-ngx/src/app/modules/home/components/widget/lib/maps/map.models.ts index 66810e0867..48f2362f0d 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/maps/map.models.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/maps/map.models.ts @@ -18,6 +18,7 @@ import { DataKey, DatasourceType } from '@shared/models/widget.models'; import { EntityType } from '@shared/models/entity-type.models'; import { DataKeyType } from '@shared/models/telemetry/telemetry.models'; import { mergeDeep } from '@core/utils'; +import { AbstractControl, ValidationErrors } from '@angular/forms'; export enum MapType { geoMap = 'geoMap', @@ -133,46 +134,85 @@ export const defaultBaseMapSettings: BaseMapSettings = { }; export enum MapProvider { - google = 'google-map', - openstreet = 'openstreet-map', + openstreet = 'openstreet', + google = 'google', here = 'here', - tencent = 'tencent-map', + tencent = 'tencent', custom = 'custom' } -export interface MapLayerSettings { - label?: string; - provider: MapProvider; -} +export const mapProviders = Object.keys(MapProvider) as MapProvider[]; -export enum GoogleLayerType { - roadmap = 'roadmap', - satellite = 'satellite', - hybrid = 'hybrid', - terrain = 'terrain' -} - -export const googleMapLayerTranslationMap = new Map( +export const mapProviderTranslationMap = new Map( [ - [GoogleLayerType.roadmap, 'widgets.maps.google-map-type-roadmap'], - [GoogleLayerType.satellite, 'widgets.maps.google-map-type-satelite'], - [GoogleLayerType.hybrid, 'widgets.maps.google-map-type-hybrid'], - [GoogleLayerType.terrain, 'widgets.maps.google-map-type-terrain'] + [MapProvider.openstreet, 'widgets.maps.layer.provider.openstreet.title'], + [MapProvider.google, 'widgets.maps.layer.provider.google.title'], + [MapProvider.here, 'widgets.maps.layer.provider.here.title'], + [MapProvider.tencent, 'widgets.maps.layer.provider.tencent.title'], + [MapProvider.custom, 'widgets.maps.layer.provider.custom.title'] ] ); -export interface GoogleMapLayerSettings extends MapLayerSettings { - provider: MapProvider.google; - layerType: GoogleLayerType; - apiKey: string; +export interface MapLayerSettings { + label?: string; + provider: MapProvider; } -export const defaultGoogleMapLayerSettings: GoogleMapLayerSettings = { - provider: MapProvider.google, - layerType: GoogleLayerType.roadmap, - apiKey: 'AIzaSyDoEx2kaGz3PxwbI9T7ccTSg5xjdw8Nw8Q' +export const mapLayerValid = (layer: MapLayerSettings): boolean => { + if (!layer.provider) { + return false; + } + switch (layer.provider) { + case MapProvider.openstreet: + const openStreetLayer = layer as OpenStreetMapLayerSettings; + return !!openStreetLayer.layerType; + case MapProvider.google: + const googleLayer = layer as GoogleMapLayerSettings; + return !!googleLayer.layerType && !!googleLayer.apiKey; + case MapProvider.here: + const hereLayer = layer as HereMapLayerSettings; + return !!hereLayer.layerType && !!hereLayer.apiKey; + case MapProvider.tencent: + const tencentLayer = layer as TencentMapLayerSettings; + return !!tencentLayer.layerType; + case MapProvider.custom: + const customLayer = layer as CustomMapLayerSettings; + return !!customLayer.tileUrl; + } }; +export const mapLayerValidator = (control: AbstractControl): ValidationErrors | null => { + const layer: MapLayerSettings = control.value; + if (!mapLayerValid(layer)) { + return { + layer: true + }; + } + return null; +}; + +export const defaultLayerTitle = (layer: MapLayerSettings): string => { + if (!layer.provider) { + return null; + } + switch (layer.provider) { + case MapProvider.openstreet: + const openStreetLayer = layer as OpenStreetMapLayerSettings; + return openStreetMapLayerTranslationMap.get(openStreetLayer.layerType); + case MapProvider.google: + const googleLayer = layer as GoogleMapLayerSettings; + return googleMapLayerTranslationMap.get(googleLayer.layerType); + case MapProvider.here: + const hereLayer = layer as HereMapLayerSettings; + return hereLayerTranslationMap.get(hereLayer.layerType); + case MapProvider.tencent: + const tencentLayer = layer as TencentMapLayerSettings; + return tencentLayerTranslationMap.get(tencentLayer.layerType); + case MapProvider.custom: + return 'widgets.maps.layer.provider.custom.title'; + } +} + export enum OpenStreetLayerType { openStreetMapnik = 'OpenStreetMap.Mapnik', openStreetHot = 'OpenStreetMap.HOT', @@ -183,15 +223,17 @@ export enum OpenStreetLayerType { cartoDbDarkMatter = 'CartoDB.DarkMatter' } +export const openStreetLayerTypes = Object.values(OpenStreetLayerType) as OpenStreetLayerType[]; + export const openStreetMapLayerTranslationMap = new Map( [ - [OpenStreetLayerType.openStreetMapnik, 'widgets.maps.openstreet-provider-mapnik'], - [OpenStreetLayerType.openStreetHot, 'widgets.maps.openstreet-provider-hot'], - [OpenStreetLayerType.esriWorldStreetMap, 'widgets.maps.openstreet-provider-esri-street'], - [OpenStreetLayerType.esriWorldTopoMap, 'widgets.maps.openstreet-provider-esri-topo'], - [OpenStreetLayerType.esriWorldImagery, 'widgets.maps.openstreet-provider-esri-imagery'], - [OpenStreetLayerType.cartoDbPositron, 'widgets.maps.openstreet-provider-cartodb-positron'], - [OpenStreetLayerType.cartoDbDarkMatter, 'widgets.maps.openstreet-provider-cartodb-dark-matter'] + [OpenStreetLayerType.openStreetMapnik, 'widgets.maps.layer.provider.openstreet.mapnik'], + [OpenStreetLayerType.openStreetHot, 'widgets.maps.layer.provider.openstreet.hot'], + [OpenStreetLayerType.esriWorldStreetMap, 'widgets.maps.layer.provider.openstreet.esri-street'], + [OpenStreetLayerType.esriWorldTopoMap, 'widgets.maps.layer.provider.openstreet.esri-topo'], + [OpenStreetLayerType.esriWorldImagery, 'widgets.maps.layer.provider.openstreet.esri-imagery'], + [OpenStreetLayerType.cartoDbPositron, 'widgets.maps.layer.provider.openstreet.cartodb-positron'], + [OpenStreetLayerType.cartoDbDarkMatter, 'widgets.maps.layer.provider.openstreet.cartodb-dark-matter'] ] ); @@ -205,6 +247,36 @@ export const defaultOpenStreetMapLayerSettings: OpenStreetMapLayerSettings = { layerType: OpenStreetLayerType.openStreetMapnik } +export enum GoogleLayerType { + roadmap = 'roadmap', + satellite = 'satellite', + hybrid = 'hybrid', + terrain = 'terrain' +} + +export const googleMapLayerTypes = Object.values(GoogleLayerType) as GoogleLayerType[]; + +export const googleMapLayerTranslationMap = new Map( + [ + [GoogleLayerType.roadmap, 'widgets.maps.layer.provider.google.roadmap'], + [GoogleLayerType.satellite, 'widgets.maps.layer.provider.google.satellite'], + [GoogleLayerType.hybrid, 'widgets.maps.layer.provider.google.hybrid'], + [GoogleLayerType.terrain, 'widgets.maps.layer.provider.google.terrain'] + ] +); + +export interface GoogleMapLayerSettings extends MapLayerSettings { + provider: MapProvider.google; + layerType: GoogleLayerType; + apiKey: string; +} + +export const defaultGoogleMapLayerSettings: GoogleMapLayerSettings = { + provider: MapProvider.google, + layerType: GoogleLayerType.roadmap, + apiKey: 'AIzaSyDoEx2kaGz3PxwbI9T7ccTSg5xjdw8Nw8Q' +}; + export enum HereLayerType { hereNormalDay = 'HEREv3.normalDay', hereNormalNight = 'HEREv3.normalNight', @@ -212,12 +284,14 @@ export enum HereLayerType { hereTerrainDay = 'HEREv3.terrainDay' } +export const hereLayerTypes = Object.values(HereLayerType) as HereLayerType[]; + export const hereLayerTranslationMap = new Map( [ - [HereLayerType.hereNormalDay, 'widgets.maps.here-map-normal-day'], - [HereLayerType.hereNormalNight, 'widgets.maps.here-map-normal-night'], - [HereLayerType.hereHybridDay, 'widgets.maps.here-map-hybrid-day'], - [HereLayerType.hereTerrainDay, 'widgets.maps.here-map-terrain-day'] + [HereLayerType.hereNormalDay, 'widgets.maps.layer.provider.here.normal-day'], + [HereLayerType.hereNormalNight, 'widgets.maps.layer.provider.here.normal-night'], + [HereLayerType.hereHybridDay, 'widgets.maps.layer.provider.here.hybrid-day'], + [HereLayerType.hereTerrainDay, 'widgets.maps.layer.provider.here.terrain-day'] ] ); @@ -239,11 +313,13 @@ export enum TencentLayerType { tencentTerrain = 'Tencent.Terrain' } +export const tencentLayerTypes = Object.values(TencentLayerType) as TencentLayerType[]; + export const tencentLayerTranslationMap = new Map( [ - [TencentLayerType.tencentNormal, 'widgets.maps.tencent-provider-normal'], - [TencentLayerType.tencentSatellite, 'widgets.maps.tencent-provider-satellite'], - [TencentLayerType.tencentTerrain, 'widgets.maps.tencent-provider-terrain'] + [TencentLayerType.tencentNormal, 'widgets.maps.layer.provider.tencent.normal'], + [TencentLayerType.tencentSatellite, 'widgets.maps.layer.provider.tencent.satellite'], + [TencentLayerType.tencentTerrain, 'widgets.maps.layer.provider.tencent.terrain'] ] ); @@ -269,10 +345,10 @@ export const defaultCustomMapLayerSettings: CustomMapLayerSettings = { export const defaultMapLayerSettings = (provider: MapProvider): MapLayerSettings => { switch (provider) { - case MapProvider.google: - return defaultGoogleMapLayerSettings; case MapProvider.openstreet: return defaultOpenStreetMapLayerSettings; + case MapProvider.google: + return defaultGoogleMapLayerSettings; case MapProvider.here: return defaultHereMapLayerSettings; case MapProvider.tencent: @@ -284,16 +360,17 @@ export const defaultMapLayerSettings = (provider: MapProvider): MapLayerSettings export const defaultMapLayers: MapLayerSettings[] = (Object.keys(OpenStreetLayerType) as OpenStreetLayerType[]).map(type => ({ provider: MapProvider.openstreet, - layerType: type -} as MapLayerSettings)).concat((Object.keys(GoogleLayerType) as GoogleLayerType[]).map(type => - mergeDeep({} as MapLayerSettings, defaultGoogleMapLayerSettings, {layerType: type} as GoogleMapLayerSettings)).concat( + layerType: OpenStreetLayerType[type] +} as MapLayerSettings)); +/*.concat((Object.keys(GoogleLayerType) as GoogleLayerType[]).map(type => + mergeDeep({} as MapLayerSettings, defaultGoogleMapLayerSettings, {layerType: GoogleLayerType[type]} as GoogleMapLayerSettings)).concat( (Object.keys(TencentLayerType) as TencentLayerType[]).map(type => ({ provider: MapProvider.tencent, - layerType: type + layerType: TencentLayerType[type] } as MapLayerSettings)) )).concat( (Object.keys(HereLayerType) as HereLayerType[]).map(type => - mergeDeep({} as MapLayerSettings, defaultHereMapLayerSettings, {layerType: type} as HereMapLayerSettings)) + mergeDeep({} as MapLayerSettings, defaultHereMapLayerSettings, {layerType: HereLayerType[type]} as HereMapLayerSettings)) ).concat([ mergeDeep({} as MapLayerSettings, defaultCustomMapLayerSettings, {label: 'Custom 1'} as CustomMapLayerSettings), mergeDeep({} as MapLayerSettings, defaultCustomMapLayerSettings, { @@ -304,14 +381,7 @@ export const defaultMapLayers: MapLayerSettings[] = (Object.keys(OpenStreetLayer tileUrl: 'http://b.tile2.opencyclemap.org/transport/{z}/{x}/{y}.png', label: 'Custom 3' } as CustomMapLayerSettings) -]); - /* - (Object.keys(OpenStreetLayerType) as OpenStreetLayerType[]).map(type => ({ - provider: MapProvider.openstreet, - layerType: type -} as MapLayerSettings)).concat( - (Object.keys(GoogleLayerType) as GoogleLayerType[]).map(type => - mergeDeep({} as GoogleMapLayerSettings, defaultGoogleMapLayerSettings, {layerType: type} as GoogleMapLayerSettings)));*/ +]);*/ export interface GeoMapSettings extends BaseMapSettings { layers?: MapLayerSettings[]; 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 1bb64c0ee9..cf61fc34ef 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 @@ -27,7 +27,7 @@ import { parseCenterPosition } from '@home/components/widget/lib/maps/map.models'; import { WidgetContext } from '@home/models/widget-component.models'; -import { mergeDeep } from '@core/utils'; +import { mergeDeep, mergeDeepIgnoreArray } from '@core/utils'; import { DeepPartial } from '@shared/models/common'; import L from 'leaflet'; import { forkJoin, Observable, of } from 'rxjs'; @@ -61,7 +61,7 @@ export abstract class TbMap { protected constructor(protected ctx: WidgetContext, protected inputSettings: DeepPartial, protected containerElement: HTMLElement) { - this.settings = mergeDeep({} as S, this.defaultSettings(), this.inputSettings as S); + this.settings = mergeDeepIgnoreArray({} as S, this.defaultSettings(), this.inputSettings as S); $(containerElement).empty(); $(containerElement).addClass('tb-map-layout'); const mapElement = $('
'); @@ -181,8 +181,8 @@ class TbGeoMap extends TbMap { sidebar, position: this.settings.controlsPosition, uiClass: 'tb-layers', - paneTitle: this.ctx.translate.instant('widgets.maps.map-layers'), - buttonTitle: this.ctx.translate.instant('widgets.maps.layers'), + paneTitle: this.ctx.translate.instant('widgets.maps.layer.map-layers'), + buttonTitle: this.ctx.translate.instant('widgets.maps.layer.layers'), }).addTo(this.map); } } diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/map/map-layer-row.component.html b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/map/map-layer-row.component.html new file mode 100644 index 0000000000..6a00c6aac2 --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/map/map-layer-row.component.html @@ -0,0 +1,84 @@ + +
+ + + + + + + {{ mapProviderTranslationMap.get(provider) | translate }} + + + + + + + {{ openStreetMapLayerTranslationMap.get(layerType) | translate }} + + + + + + + {{ googleMapLayerTranslationMap.get(layerType) | translate }} + + + + + + + {{ hereLayerTranslationMap.get(layerType) | translate }} + + + + + + + {{ tencentLayerTranslationMap.get(layerType) | translate }} + + + + + + +
+ +
+ +
+
+
diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/map/map-layer-row.component.scss b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/map/map-layer-row.component.scss new file mode 100644 index 0000000000..6480756f07 --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/map/map-layer-row.component.scss @@ -0,0 +1,36 @@ +/** + * Copyright © 2016-2024 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +@import '../../../../../../../../../scss/constants'; + +.tb-form-table-row.tb-map-layer-row { + + .tb-label-field { + flex: 1 1 33.33%; + } + + .tb-provider-field { + flex: 1 1 33.33%; + } + + .tb-layer-field { + flex: 1 1 33.33%; + } + + .tb-remove-button { + width: 40px; + min-width: 40px; + } +} diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/map/map-layer-row.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/map/map-layer-row.component.ts new file mode 100644 index 0000000000..eca994af6b --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/map/map-layer-row.component.ts @@ -0,0 +1,230 @@ +/// +/// Copyright © 2016-2024 The Thingsboard Authors +/// +/// Licensed under the Apache License, Version 2.0 (the "License"); +/// you may not use this file except in compliance with the License. +/// You may obtain a copy of the License at +/// +/// http://www.apache.org/licenses/LICENSE-2.0 +/// +/// Unless required by applicable law or agreed to in writing, software +/// distributed under the License is distributed on an "AS IS" BASIS, +/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +/// See the License for the specific language governing permissions and +/// limitations under the License. +/// + +import { + ChangeDetectorRef, + Component, + DestroyRef, + EventEmitter, + forwardRef, + Input, + OnInit, + Output, + Renderer2, + ViewContainerRef, + ViewEncapsulation +} from '@angular/core'; +import { + ControlValueAccessor, + NG_VALUE_ACCESSOR, + UntypedFormBuilder, + UntypedFormGroup, + Validators +} from '@angular/forms'; +import { MatButton } from '@angular/material/button'; +import { TbPopoverService } from '@shared/components/popover.service'; +import { TranslateService } from '@ngx-translate/core'; +import { takeUntilDestroyed } from '@angular/core/rxjs-interop'; +import { + defaultLayerTitle, + defaultMapLayerSettings, + googleMapLayerTranslationMap, + googleMapLayerTypes, + hereLayerTranslationMap, + hereLayerTypes, + MapLayerSettings, + MapProvider, + mapProviders, + mapProviderTranslationMap, + openStreetLayerTypes, + openStreetMapLayerTranslationMap, + tencentLayerTranslationMap, + tencentLayerTypes +} from '@home/components/widget/lib/maps/map.models'; +import { deepClone } from '@core/utils'; +import { + MapLayerSettingsPanelComponent +} from '@home/components/widget/lib/settings/common/map/map-layer-settings-panel.component'; + +@Component({ + selector: 'tb-map-layer-row', + templateUrl: './map-layer-row.component.html', + styleUrls: ['./map-layer-row.component.scss'], + providers: [ + { + provide: NG_VALUE_ACCESSOR, + useExisting: forwardRef(() => MapLayerRowComponent), + multi: true + } + ], + encapsulation: ViewEncapsulation.None +}) +export class MapLayerRowComponent implements ControlValueAccessor, OnInit { + + MapProvider = MapProvider; + + mapProviders = mapProviders; + + mapProviderTranslationMap = mapProviderTranslationMap; + + openStreetLayerTypes = openStreetLayerTypes; + + openStreetMapLayerTranslationMap = openStreetMapLayerTranslationMap; + + googleMapLayerTypes = googleMapLayerTypes; + + googleMapLayerTranslationMap = googleMapLayerTranslationMap; + + hereLayerTypes = hereLayerTypes; + + hereLayerTranslationMap = hereLayerTranslationMap; + + tencentLayerTypes = tencentLayerTypes; + + tencentLayerTranslationMap = tencentLayerTranslationMap; + + @Input() + disabled: boolean; + + @Output() + layerRemoved = new EventEmitter(); + + layerFormGroup: UntypedFormGroup; + + modelValue: MapLayerSettings; + + private propagateChange = (_val: any) => {}; + + constructor(private fb: UntypedFormBuilder, + private translate: TranslateService, + private popoverService: TbPopoverService, + private renderer: Renderer2, + private viewContainerRef: ViewContainerRef, + private cd: ChangeDetectorRef, + private destroyRef: DestroyRef) { + } + + ngOnInit() { + this.layerFormGroup = this.fb.group({ + label: [null, []], + provider: [null, [Validators.required]], + layerType: [null, [Validators.required]], + tileUrl: [null, [Validators.required]], + apiKey: [null, [Validators.required]] + }); + this.layerFormGroup.valueChanges.pipe( + takeUntilDestroyed(this.destroyRef) + ).subscribe( + () => this.updateModel() + ); + this.layerFormGroup.get('provider').valueChanges.pipe( + takeUntilDestroyed(this.destroyRef) + ).subscribe((newProvider: MapProvider) => { + this.onProviderChanged(newProvider); + }); + } + + registerOnChange(fn: any): void { + this.propagateChange = fn; + } + + registerOnTouched(_fn: any): void { + } + + setDisabledState(isDisabled: boolean): void { + this.disabled = isDisabled; + if (isDisabled) { + this.layerFormGroup.disable({emitEvent: false}); + } else { + this.layerFormGroup.enable({emitEvent: false}); + this.updateValidators(); + } + } + + writeValue(value: MapLayerSettings): void { + this.modelValue = value; + this.layerFormGroup.patchValue( + value, {emitEvent: false} + ); + this.updateValidators(); + this.cd.markForCheck(); + } + + labelPlaceholder(): string { + let translationKey = defaultLayerTitle(this.modelValue); + if (!translationKey) { + translationKey = 'widget-config.set'; + } + return this.translate.instant(translationKey); + } + + editLayer($event: Event, matButton: MatButton) { + if ($event) { + $event.stopPropagation(); + } + const trigger = matButton._elementRef.nativeElement; + if (this.popoverService.hasPopover(trigger)) { + this.popoverService.hidePopover(trigger); + } else { + const ctx: any = { + mapLayerSettings: deepClone(this.modelValue) + }; + const mapLayerSettingsPanelPopover = this.popoverService.displayPopover(trigger, this.renderer, + this.viewContainerRef, MapLayerSettingsPanelComponent, ['leftOnly', 'leftTopOnly', 'leftBottomOnly'], true, null, + ctx, + {}, + {}, {}, true); + mapLayerSettingsPanelPopover.tbComponentRef.instance.popover = mapLayerSettingsPanelPopover; + mapLayerSettingsPanelPopover.tbComponentRef.instance.mapLayerSettingsApplied.subscribe((layer) => { + mapLayerSettingsPanelPopover.hide(); + this.layerFormGroup.patchValue( + layer, + {emitEvent: false}); + this.updateValidators(); + this.updateModel(); + }); + } + } + + private onProviderChanged(newProvider: MapProvider) { + this.modelValue = {...defaultMapLayerSettings(newProvider), label: this.modelValue.label}; + this.layerFormGroup.patchValue( + this.modelValue, {emitEvent: false} + ); + this.updateValidators(); + } + + private updateValidators() { + const provider: MapProvider = this.layerFormGroup.get('provider').value; + if (provider === MapProvider.custom) { + this.layerFormGroup.get('tileUrl').enable({emitEvent: false}); + this.layerFormGroup.get('layerType').disable({emitEvent: false}); + } else { + this.layerFormGroup.get('tileUrl').disable({emitEvent: false}); + this.layerFormGroup.get('layerType').enable({emitEvent: false}); + } + if ([MapProvider.google, MapProvider.here].includes(provider)) { + this.layerFormGroup.get('apiKey').enable({emitEvent: false}); + } else { + this.layerFormGroup.get('apiKey').disable({emitEvent: false}); + } + } + + private updateModel() { + this.modelValue = this.layerFormGroup.value; + this.propagateChange(this.modelValue); + } +} diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/map/map-layer-settings-panel.component.html b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/map/map-layer-settings-panel.component.html new file mode 100644 index 0000000000..ab509e8d15 --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/map/map-layer-settings-panel.component.html @@ -0,0 +1,101 @@ + +
+
widgets.maps.layer.layer-settings
+
+
+
widgets.maps.layer.label
+ + + +
+
+
widgets.maps.layer.provider.provider
+ + + + {{ mapProviderTranslationMap.get(provider) | translate }} + + + +
+
+
{{ (MapProvider.custom === layerFormGroup.get('provider').value ? 'widgets.maps.layer.provider.custom.tile-url' : 'widgets.maps.layer.layer') | translate }}
+ + + + {{ openStreetMapLayerTranslationMap.get(layerType) | translate }} + + + + + + + {{ googleMapLayerTranslationMap.get(layerType) | translate }} + + + + + + + {{ hereLayerTranslationMap.get(layerType) | translate }} + + + + + + + {{ tencentLayerTranslationMap.get(layerType) | translate }} + + + + + + +
+
+
widgets.maps.layer.credentials.credentials
+
+
widgets.maps.layer.credentials.api-key
+ + + +
+
+
+
+ + +
+
diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/map/map-layer-settings-panel.component.scss b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/map/map-layer-settings-panel.component.scss new file mode 100644 index 0000000000..6411af9025 --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/map/map-layer-settings-panel.component.scss @@ -0,0 +1,49 @@ +/** + * Copyright © 2016-2024 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +@import '../../../../../../../../../scss/constants'; + +.tb-map-layer-settings-panel { + width: 540px; + display: flex; + flex-direction: column; + gap: 16px; + @media #{$mat-lt-md} { + width: 90vw; + } + .tb-map-layer-settings-panel-content { + display: flex; + flex-direction: column; + gap: 16px; + overflow: auto; + margin: -10px; + padding: 10px; + } + .tb-map-layer-settings-title { + font-size: 16px; + font-weight: 500; + line-height: 24px; + letter-spacing: 0.25px; + color: rgba(0, 0, 0, 0.87); + } + .tb-map-layer-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/map-layer-settings-panel.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/map/map-layer-settings-panel.component.ts new file mode 100644 index 0000000000..341cd80388 --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/map/map-layer-settings-panel.component.ts @@ -0,0 +1,148 @@ +/// +/// Copyright © 2016-2024 The Thingsboard Authors +/// +/// Licensed under the Apache License, Version 2.0 (the "License"); +/// you may not use this file except in compliance with the License. +/// You may obtain a copy of the License at +/// +/// http://www.apache.org/licenses/LICENSE-2.0 +/// +/// Unless required by applicable law or agreed to in writing, software +/// distributed under the License is distributed on an "AS IS" BASIS, +/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +/// See the License for the specific language governing permissions and +/// limitations under the License. +/// + +import { Component, 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 { + defaultLayerTitle, + defaultMapLayerSettings, + googleMapLayerTranslationMap, + googleMapLayerTypes, + hereLayerTranslationMap, + hereLayerTypes, + MapLayerSettings, + MapProvider, + mapProviders, + mapProviderTranslationMap, + openStreetLayerTypes, + openStreetMapLayerTranslationMap, + tencentLayerTranslationMap, + tencentLayerTypes +} from '@home/components/widget/lib/maps/map.models'; +import { TranslateService } from '@ngx-translate/core'; + +@Component({ + selector: 'tb-map-layer-settings-panel', + templateUrl: './map-layer-settings-panel.component.html', + providers: [], + styleUrls: ['./map-layer-settings-panel.component.scss'], + encapsulation: ViewEncapsulation.None +}) +export class MapLayerSettingsPanelComponent implements OnInit { + + MapProvider = MapProvider; + + mapProviders = mapProviders; + + mapProviderTranslationMap = mapProviderTranslationMap; + + openStreetLayerTypes = openStreetLayerTypes; + + openStreetMapLayerTranslationMap = openStreetMapLayerTranslationMap; + + googleMapLayerTypes = googleMapLayerTypes; + + googleMapLayerTranslationMap = googleMapLayerTranslationMap; + + hereLayerTypes = hereLayerTypes; + + hereLayerTranslationMap = hereLayerTranslationMap; + + tencentLayerTypes = tencentLayerTypes; + + tencentLayerTranslationMap = tencentLayerTranslationMap; + + @Input() + mapLayerSettings: MapLayerSettings; + + @Input() + popover: TbPopoverComponent; + + @Output() + mapLayerSettingsApplied = new EventEmitter(); + + layerFormGroup: UntypedFormGroup; + + constructor(private fb: UntypedFormBuilder, + private translate: TranslateService, + private destroyRef: DestroyRef) { + } + + ngOnInit(): void { + this.layerFormGroup = this.fb.group( + { + label: [null, []], + provider: [null, [Validators.required]], + layerType: [null, [Validators.required]], + tileUrl: [null, [Validators.required]], + apiKey: [null, [Validators.required]] + } + ); + this.layerFormGroup.patchValue( + this.mapLayerSettings, {emitEvent: false} + ); + this.layerFormGroup.get('provider').valueChanges.pipe( + takeUntilDestroyed(this.destroyRef) + ).subscribe((newProvider: MapProvider) => { + this.onProviderChanged(newProvider); + }); + this.updateValidators(); + } + + cancel() { + this.popover?.hide(); + } + + labelPlaceholder(): string { + let translationKey = defaultLayerTitle(this.layerFormGroup.value); + if (!translationKey) { + translationKey = 'widget-config.set'; + } + return this.translate.instant(translationKey); + } + + applyLayerSettings() { + const layerSettings: MapLayerSettings = this.layerFormGroup.value; + this.mapLayerSettingsApplied.emit(layerSettings); + } + + private onProviderChanged(newProvider: MapProvider) { + let modelValue: MapLayerSettings = this.layerFormGroup.value; + modelValue = {...defaultMapLayerSettings(newProvider), label: modelValue.label}; + this.layerFormGroup.patchValue( + modelValue, {emitEvent: false} + ); + this.updateValidators(); + } + + private updateValidators() { + const provider: MapProvider = this.layerFormGroup.get('provider').value; + if (provider === MapProvider.custom) { + this.layerFormGroup.get('tileUrl').enable({emitEvent: false}); + this.layerFormGroup.get('layerType').disable({emitEvent: false}); + } else { + this.layerFormGroup.get('tileUrl').disable({emitEvent: false}); + this.layerFormGroup.get('layerType').enable({emitEvent: false}); + } + if ([MapProvider.google, MapProvider.here].includes(provider)) { + this.layerFormGroup.get('apiKey').enable({emitEvent: false}); + } else { + this.layerFormGroup.get('apiKey').disable({emitEvent: false}); + } + } +} diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/map/map-layers.component.html b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/map/map-layers.component.html new file mode 100644 index 0000000000..84765f3ad4 --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/map/map-layers.component.html @@ -0,0 +1,57 @@ + +
+
+
+
widgets.maps.layer.label
+
widgets.maps.layer.provider.provider
+
widgets.maps.layer.layer
+
+
+
+
+ +
+ +
+
+
+
+
+ +
+
+ + {{ 'widgets.maps.layer.no-layers' | translate }} + diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/map/map-layers.component.scss b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/map/map-layers.component.scss new file mode 100644 index 0000000000..6737ad2c87 --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/map/map-layers.component.scss @@ -0,0 +1,39 @@ +/** + * Copyright © 2016-2024 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +.tb-map-layers { + .tb-form-table-header-cell { + &.tb-label-header { + flex: 1 1 33.33%; + } + &.tb-provider-header { + flex: 1 1 33.33%; + } + &.tb-layer-header { + flex: 1 1 33.33%; + } + &.tb-actions-header { + width: 120px; + min-width: 120px; + } + } + + .tb-form-table-body { + tb-map-layer-row { + overflow: hidden; + } + } +} diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/map/map-layers.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/map/map-layers.component.ts new file mode 100644 index 0000000000..9569c324c4 --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/map/map-layers.component.ts @@ -0,0 +1,156 @@ +/// +/// Copyright © 2016-2024 The Thingsboard Authors +/// +/// Licensed under the Apache License, Version 2.0 (the "License"); +/// you may not use this file except in compliance with the License. +/// You may obtain a copy of the License at +/// +/// http://www.apache.org/licenses/LICENSE-2.0 +/// +/// Unless required by applicable law or agreed to in writing, software +/// distributed under the License is distributed on an "AS IS" BASIS, +/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +/// See the License for the specific language governing permissions and +/// limitations under the License. +/// + +import { Component, DestroyRef, forwardRef, Input, OnInit, ViewEncapsulation } from '@angular/core'; +import { + AbstractControl, + ControlValueAccessor, + NG_VALIDATORS, + NG_VALUE_ACCESSOR, + UntypedFormArray, + UntypedFormBuilder, + UntypedFormControl, + UntypedFormGroup, + Validator +} from '@angular/forms'; +import { mergeDeep } from '@core/utils'; +import { CdkDragDrop } from '@angular/cdk/drag-drop'; +import { takeUntilDestroyed } from '@angular/core/rxjs-interop'; +import { + defaultMapLayerSettings, + MapLayerSettings, + mapLayerValid, + mapLayerValidator, + MapProvider +} from '@home/components/widget/lib/maps/map.models'; + +@Component({ + selector: 'tb-map-layers', + templateUrl: './map-layers.component.html', + styleUrls: ['./map-layers.component.scss'], + providers: [ + { + provide: NG_VALUE_ACCESSOR, + useExisting: forwardRef(() => MapLayersComponent), + multi: true + }, + { + provide: NG_VALIDATORS, + useExisting: forwardRef(() => MapLayersComponent), + multi: true + } + ], + encapsulation: ViewEncapsulation.None +}) +export class MapLayersComponent implements ControlValueAccessor, OnInit, Validator { + + @Input() + disabled: boolean; + + layersFormGroup: UntypedFormGroup; + + get dragEnabled(): boolean { + return this.layersFormArray().controls.length > 1; + } + + private propagateChange = (_val: any) => {}; + + constructor(private fb: UntypedFormBuilder, + private destroyRef: DestroyRef) { + } + + ngOnInit() { + this.layersFormGroup = this.fb.group({ + layers: [this.fb.array([]), []] + }); + this.layersFormGroup.valueChanges.pipe( + takeUntilDestroyed(this.destroyRef) + ).subscribe( + () => { + let layers: MapLayerSettings[] = this.layersFormGroup.get('layers').value; + if (layers) { + layers = layers.filter(layer => mapLayerValid(layer)); + } + this.propagateChange(layers); + } + ); + } + + registerOnChange(fn: any): void { + this.propagateChange = fn; + } + + registerOnTouched(fn: any): void { + } + + setDisabledState(isDisabled: boolean): void { + this.disabled = isDisabled; + if (isDisabled) { + this.layersFormGroup.disable({emitEvent: false}); + } else { + this.layersFormGroup.enable({emitEvent: false}); + } + } + + writeValue(value: MapLayerSettings[] | undefined): void { + const layers: MapLayerSettings[] = value || []; + this.layersFormGroup.setControl('layers', this.prepareLayersFormArray(layers), {emitEvent: false}); + } + + public validate(c: UntypedFormControl) { + const valid = this.layersFormGroup.valid; + return valid ? null : { + layers: { + valid: false, + }, + }; + } + + layerDrop(event: CdkDragDrop) { + const layersArray = this.layersFormGroup.get('layers') as UntypedFormArray; + const layer = layersArray.at(event.previousIndex); + layersArray.removeAt(event.previousIndex); + layersArray.insert(event.currentIndex, layer); + } + + layersFormArray(): UntypedFormArray { + return this.layersFormGroup.get('layers') as UntypedFormArray; + } + + trackByLayer(index: number, layerControl: AbstractControl): any { + return layerControl; + } + + removeLayer(index: number) { + (this.layersFormGroup.get('layers') as UntypedFormArray).removeAt(index); + } + + addLayer() { + const layer = mergeDeep({} as MapLayerSettings, + defaultMapLayerSettings(MapProvider.openstreet)); + const layersArray = this.layersFormGroup.get('layers') as UntypedFormArray; + const layerControl = this.fb.control(layer, [mapLayerValidator]); + layersArray.push(layerControl); + } + + private prepareLayersFormArray(layers: MapLayerSettings[]): UntypedFormArray { + const layersControls: Array = []; + layers.forEach((layer) => { + layersControls.push(this.fb.control(layer, [mapLayerValidator])); + }); + return this.fb.array(layersControls); + } +} 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 28fe014dfa..ec606c3736 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 @@ -19,24 +19,26 @@
- {{ 'widgets.maps.map-type' | translate }} + {{ 'widgets.maps.map-type.type' | translate }}
- {{ 'widgets.maps.map-type-map' | translate }} - {{ 'widgets.maps.map-type-image' | translate }} + {{ 'widgets.maps.map-type.map' | translate }} + {{ 'widgets.maps.map-type.image' | translate }}
+
- {{ 'widgets.maps.overlays' | translate }} + {{ 'widgets.maps.overlays.overlays' | translate }}
- {{ 'widgets.maps.markers' | translate }} - {{ 'widgets.maps.polygons' | translate }} - {{ 'widgets.maps.circles' | translate }} + {{ 'widgets.maps.overlays.markers' | translate }} + {{ 'widgets.maps.overlays.polygons' | translate }} + {{ 'widgets.maps.overlays.circles' | translate }}
diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/map/map-settings.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/map/map-settings.component.ts index 8e5244b5ed..bdf24c4549 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/map/map-settings.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/map/map-settings.component.ts @@ -15,7 +15,13 @@ /// import { Component, DestroyRef, forwardRef, Input, OnInit } from '@angular/core'; -import { ControlValueAccessor, NG_VALUE_ACCESSOR, UntypedFormBuilder, UntypedFormGroup } from '@angular/forms'; +import { + ControlValueAccessor, + NG_VALIDATORS, + NG_VALUE_ACCESSOR, + UntypedFormBuilder, UntypedFormControl, + UntypedFormGroup, Validator +} from '@angular/forms'; import { ImageSourceType, MapSetting, MapType } from '@home/components/widget/lib/maps/map.models'; import { takeUntilDestroyed } from '@angular/core/rxjs-interop'; import { merge } from 'rxjs'; @@ -29,10 +35,15 @@ import { merge } from 'rxjs'; provide: NG_VALUE_ACCESSOR, useExisting: forwardRef(() => MapSettingsComponent), multi: true + }, + { + provide: NG_VALIDATORS, + useExisting: forwardRef(() => MapSettingsComponent), + multi: true } ] }) -export class MapSettingsComponent implements OnInit, ControlValueAccessor { +export class MapSettingsComponent implements OnInit, ControlValueAccessor, Validator { MapType = MapType; @@ -110,6 +121,15 @@ export class MapSettingsComponent implements OnInit, ControlValueAccessor { this.updateValidators(); } + public validate(c: UntypedFormControl) { + const valid = this.mapSettingsFormGroup.valid; + return valid ? null : { + mapSettings: { + valid: false, + }, + }; + } + private updateValidators() { const mapType: MapType = this.mapSettingsFormGroup.get('mapType').value; const imageSourceType: ImageSourceType = this.mapSettingsFormGroup.get('imageSourceType').value; 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 c05b778f94..e9279d853c 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 @@ -184,6 +184,11 @@ import { DynamicFormArrayComponent } from '@home/components/widget/lib/settings/common/dynamic-form/dynamic-form-array.component'; import { MapSettingsComponent } from '@home/components/widget/lib/settings/common/map/map-settings.component'; +import { MapLayersComponent } from '@home/components/widget/lib/settings/common/map/map-layers.component'; +import { MapLayerRowComponent } from '@home/components/widget/lib/settings/common/map/map-layer-row.component'; +import { + MapLayerSettingsPanelComponent +} from '@home/components/widget/lib/settings/common/map/map-layer-settings-panel.component'; @NgModule({ declarations: [ @@ -254,6 +259,9 @@ import { MapSettingsComponent } from '@home/components/widget/lib/settings/commo DynamicFormSelectItemRowComponent, DynamicFormComponent, DynamicFormArrayComponent, + MapLayerSettingsPanelComponent, + MapLayerRowComponent, + MapLayersComponent, MapSettingsComponent ], imports: [ diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/map/map-widget-settings.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/settings/map/map-widget-settings.component.ts index 474f151161..9cef21f147 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/settings/map/map-widget-settings.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/map/map-widget-settings.component.ts @@ -19,7 +19,7 @@ import { WidgetSettings, WidgetSettingsComponent } from '@shared/models/widget.m import { UntypedFormBuilder, UntypedFormGroup } from '@angular/forms'; import { Store } from '@ngrx/store'; import { AppState } from '@core/core.state'; -import { mergeDeep } from '@core/utils'; +import { mergeDeep, mergeDeepIgnoreArray } from '@core/utils'; import { mapWidgetDefaultSettings, MapWidgetSettings } from '@home/components/widget/lib/maps/map-widget.models'; @Component({ @@ -41,7 +41,7 @@ export class MapWidgetSettingsComponent extends WidgetSettingsComponent { } protected defaultSettings(): WidgetSettings { - return mergeDeep({} as MapWidgetSettings, mapWidgetDefaultSettings); + return mergeDeepIgnoreArray({} as MapWidgetSettings, mapWidgetDefaultSettings); } protected onSettingsSet(settings: WidgetSettings) { 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 b282e7ce24..da5f80d49f 100644 --- a/ui-ngx/src/assets/locale/locale.constant-en_US.json +++ b/ui-ngx/src/assets/locale/locale.constant-en_US.json @@ -6803,19 +6803,69 @@ "max-value": "Maximum value" }, "maps": { - "map-type": "Map type", - "map-type-map": "Map", - "map-type-image": "Image", - "overlays": "Overlays", - "markers": "Markers", - "polygons": "Polygons", - "circles": "Circles", - "layers": "Layers", - "map-layers": "Map layers", - "tencent-provider-normal": "Tencent.Normal", - "tencent-provider-satellite": "Tencent.Satellite", - "tencent-provider-terrain": "Tencent.Terrain", - "custom": "Custom", + "map-type": { + "type": "Map type", + "map": "Map", + "image": "Image" + }, + "layer": { + "label": "Label", + "layer": "Layer", + "layers": "Layers", + "map-layers": "Map layers", + "add-layer": "Add layer", + "layer-settings": "Layer settings", + "remove-layer": "Remove layer", + "no-layers": "No layers configured", + "provider": { + "provider": "Provider", + "openstreet": { + "title": "OpenStreet", + "mapnik": "Mapnik", + "hot": "HOT", + "esri-street": "WorldStreetMap", + "esri-topo": "WorldTopoMap", + "esri-imagery": "WorldImagery", + "cartodb-positron": "Positron", + "cartodb-dark-matter": "DarkMatter" + + }, + "google": { + "title": "Google", + "roadmap": "Roadmap", + "satellite": "Satellite", + "hybrid": "Hybrid", + "terrain": "Terrain" + }, + "here": { + "title": "HERE", + "normal-day": "Normal day", + "normal-night": "Normal night", + "hybrid-day": "Hybrid day", + "terrain-day": "Terrain day" + }, + "tencent": { + "title": "Tencent", + "normal": "Normal", + "satellite": "Satellite", + "terrain": "Terrain" + }, + "custom": { + "title": "Custom", + "tile-url": "Tile URL" + } + }, + "credentials": { + "credentials": "Credentials", + "api-key": "API Key" + } + }, + "overlays": { + "overlays": "Overlays", + "markers": "Markers", + "polygons": "Polygons", + "circles": "Circles" + }, "select-entity": "Select entity", "select-entity-hint": "Hint: after selection click at the map to set position", "tooltips": { From 4044783439358c94ba2b58656e3b62ecb9c5f292 Mon Sep 17 00:00:00 2001 From: Igor Kulikov Date: Mon, 6 Jan 2025 19:36:33 +0200 Subject: [PATCH 015/345] UI: Fix layer toggle radio buttons. --- .../home/components/widget/lib/maps/leaflet/leaflet-tb.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/maps/leaflet/leaflet-tb.ts b/ui-ngx/src/app/modules/home/components/widget/lib/maps/leaflet/leaflet-tb.ts index 660cf1dafb..966b7326eb 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/maps/leaflet/leaflet-tb.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/maps/leaflet/leaflet-tb.ts @@ -194,7 +194,6 @@ class LayersControl extends SidebarPaneControl { input.on('click', (e: JQuery.MouseEventBase) => { e.stopPropagation(); - e.preventDefault(); layers.forEach((other) => { if (other.layer === layerData.layer) { map.addLayer(other.layer); From e08f05ced3b2ffd40735aeadd1e3ddc3b06d86cd Mon Sep 17 00:00:00 2001 From: Igor Kulikov Date: Tue, 7 Jan 2025 20:17:04 +0200 Subject: [PATCH 016/345] UI: Map data layers implementation. --- ui-ngx/src/app/core/utils.ts | 11 +- ui-ngx/src/app/modules/common/modules-map.ts | 4 +- .../basic/map/map-basic-config.component.html | 8 +- .../config/datasource.component.models.ts | 2 +- .../widget/config/datasource.component.ts | 2 +- .../widget/config/target-device.component.ts | 2 +- .../config/widget-config-components.module.ts | 3 - .../config/widget-config.component.models.ts | 10 +- .../config/widget-settings.component.ts | 9 + .../widget/lib/maps/map-data-layer.ts | 170 ++++++++ .../components/widget/lib/maps/map.models.ts | 213 +++++++++- .../home/components/widget/lib/maps/map.ts | 86 +++- .../alias/entity-alias-select.component.html | 53 ++- .../entity-alias-select.component.models.ts | 0 .../alias/entity-alias-select.component.scss | 0 .../alias/entity-alias-select.component.ts | 11 + .../map/map-data-layer-row.component.html | 129 ++++++ .../map/map-data-layer-row.component.scss | 45 +++ .../map/map-data-layer-row.component.ts | 380 ++++++++++++++++++ .../common/map/map-data-layers.component.html | 51 +++ .../common/map/map-data-layers.component.scss | 42 ++ .../common/map/map-data-layers.component.ts | 177 ++++++++ .../common/map/map-settings.component.html | 14 +- .../common/map/map-settings.component.ts | 22 +- .../common/widget-settings-common.module.ts | 14 +- .../map/map-widget-settings.component.html | 8 +- .../widget/widget-config.component.html | 1 + .../entity/entity-autocomplete.component.html | 22 +- .../entity/entity-autocomplete.component.ts | 7 + ui-ngx/src/app/shared/models/widget.models.ts | 7 +- .../assets/locale/locale.constant-en_US.json | 34 ++ 31 files changed, 1468 insertions(+), 69 deletions(-) create mode 100644 ui-ngx/src/app/modules/home/components/widget/lib/maps/map-data-layer.ts rename ui-ngx/src/app/modules/home/components/{ => widget/lib/settings/common}/alias/entity-alias-select.component.html (56%) rename ui-ngx/src/app/modules/home/components/{ => widget/lib/settings/common}/alias/entity-alias-select.component.models.ts (100%) rename ui-ngx/src/app/modules/home/components/{ => widget/lib/settings/common}/alias/entity-alias-select.component.scss (100%) rename ui-ngx/src/app/modules/home/components/{ => widget/lib/settings/common}/alias/entity-alias-select.component.ts (97%) create mode 100644 ui-ngx/src/app/modules/home/components/widget/lib/settings/common/map/map-data-layer-row.component.html create mode 100644 ui-ngx/src/app/modules/home/components/widget/lib/settings/common/map/map-data-layer-row.component.scss create mode 100644 ui-ngx/src/app/modules/home/components/widget/lib/settings/common/map/map-data-layer-row.component.ts create mode 100644 ui-ngx/src/app/modules/home/components/widget/lib/settings/common/map/map-data-layers.component.html create mode 100644 ui-ngx/src/app/modules/home/components/widget/lib/settings/common/map/map-data-layers.component.scss create mode 100644 ui-ngx/src/app/modules/home/components/widget/lib/settings/common/map/map-data-layers.component.ts diff --git a/ui-ngx/src/app/core/utils.ts b/ui-ngx/src/app/core/utils.ts index fed2de125f..7e2e5d1e52 100644 --- a/ui-ngx/src/app/core/utils.ts +++ b/ui-ngx/src/app/core/utils.ts @@ -491,11 +491,12 @@ export const createLabelFromSubscriptionEntityInfo = (entityInfo: SubscriptionEn export const hasDatasourceLabelsVariables = (pattern: string): boolean => varsRegex.test(pattern) !== null; -export function formattedDataFormDatasourceData(input: DatasourceData[], dataIndex?: number, ts?: number): FormattedData[] { - return _(input).groupBy(el => el.datasource.entityName + el.datasource.entityType) +export function formattedDataFormDatasourceData(input: DatasourceData[], dataIndex?: number, ts?: number, + groupFunction: (el: DatasourceData) => any = (el) => el.datasource.entityName + el.datasource.entityType): FormattedData[] { + return _(input).groupBy(groupFunction) .values().value().map((entityArray, i) => { - const datasource = entityArray[0].datasource; - const obj = formattedDataFromDatasource(datasource, i); + const datasource = entityArray[0].datasource as D; + const obj = formattedDataFromDatasource(datasource, i); entityArray.filter(el => el.data.length).forEach(el => { const index = isDefined(dataIndex) ? dataIndex : el.data.length - 1; const dataSet = isDefined(ts) ? el.data.find(data => data[0] === ts) : el.data[index]; @@ -537,7 +538,7 @@ export function formattedDataArrayFromDatasourceData(input: DatasourceData[]): F }); } -export function formattedDataFromDatasource(datasource: Datasource, dsIndex: number): FormattedData { +export function formattedDataFromDatasource(datasource: D, dsIndex: number): FormattedData { return { entityName: datasource.entityName, deviceName: datasource.entityName, diff --git a/ui-ngx/src/app/modules/common/modules-map.ts b/ui-ngx/src/app/modules/common/modules-map.ts index 257e1bf348..9e65bdb4db 100644 --- a/ui-ngx/src/app/modules/common/modules-map.ts +++ b/ui-ngx/src/app/modules/common/modules-map.ts @@ -231,7 +231,7 @@ import * as EntityFilterViewComponent from '@home/components/entity/entity-filte import * as EntityAliasDialogComponent from '@home/components/alias/entity-alias-dialog.component'; import * as EntityFilterComponent from '@home/components/entity/entity-filter.component'; import * as RelationFiltersComponent from '@home/components/relation/relation-filters.component'; -import * as EntityAliasSelectComponent from '@home/components/alias/entity-alias-select.component'; +import * as EntityAliasSelectComponent from '@home/components/widget/lib/settings/common/alias/entity-alias-select.component'; import * as DataKeysComponent from '@home/components/widget/config/data-keys.component'; import * as DataKeyConfigDialogComponent from '@home/components/widget/config/data-key-config-dialog.component'; import * as DataKeyConfigComponent from '@home/components/widget/config/data-key-config.component'; @@ -577,7 +577,7 @@ class ModulesMap implements IModulesMap { '@home/components/alias/entity-alias-dialog.component': EntityAliasDialogComponent, '@home/components/entity/entity-filter.component': EntityFilterComponent, '@home/components/relation/relation-filters.component': RelationFiltersComponent, - '@home/components/alias/entity-alias-select.component': EntityAliasSelectComponent, + '@home/components/widget/lib/settings/common/alias/entity-alias-select.component': EntityAliasSelectComponent, '@home/components/widget/config/data-keys.component': DataKeysComponent, '@home/components/widget/config/data-key-config-dialog.component': DataKeyConfigDialogComponent, '@home/components/widget/config/data-key-config.component': DataKeyConfigComponent, diff --git a/ui-ngx/src/app/modules/home/components/widget/config/basic/map/map-basic-config.component.html b/ui-ngx/src/app/modules/home/components/widget/config/basic/map/map-basic-config.component.html index 25acce8553..d2aaf8a3ab 100644 --- a/ui-ngx/src/app/modules/home/components/widget/config/basic/map/map-basic-config.component.html +++ b/ui-ngx/src/app/modules/home/components/widget/config/basic/map/map-basic-config.component.html @@ -16,7 +16,13 @@ --> - + +
widget-config.appearance
diff --git a/ui-ngx/src/app/modules/home/components/widget/config/datasource.component.models.ts b/ui-ngx/src/app/modules/home/components/widget/config/datasource.component.models.ts index 2028582f52..8745136894 100644 --- a/ui-ngx/src/app/modules/home/components/widget/config/datasource.component.models.ts +++ b/ui-ngx/src/app/modules/home/components/widget/config/datasource.component.models.ts @@ -14,7 +14,7 @@ /// limitations under the License. /// -import { EntityAliasSelectCallbacks } from '@home/components/alias/entity-alias-select.component.models'; +import { EntityAliasSelectCallbacks } from '@home/components/widget/lib/settings/common/alias/entity-alias-select.component.models'; import { FilterSelectCallbacks } from '@home/components/filter/filter-select.component.models'; import { DataKeysCallbacks } from '@home/components/widget/config/data-keys.component.models'; diff --git a/ui-ngx/src/app/modules/home/components/widget/config/datasource.component.ts b/ui-ngx/src/app/modules/home/components/widget/config/datasource.component.ts index b62f462576..11e3af65dd 100644 --- a/ui-ngx/src/app/modules/home/components/widget/config/datasource.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/config/datasource.component.ts @@ -37,7 +37,7 @@ import { AlarmSearchStatus } from '@shared/models/alarm.models'; import { Dashboard } from '@shared/models/dashboard.models'; import { WidgetConfigComponent } from '@home/components/widget/widget-config.component'; import { IAliasController } from '@core/api/widget-api.models'; -import { EntityAliasSelectCallbacks } from '@home/components/alias/entity-alias-select.component.models'; +import { EntityAliasSelectCallbacks } from '@home/components/widget/lib/settings/common/alias/entity-alias-select.component.models'; import { FilterSelectCallbacks } from '@home/components/filter/filter-select.component.models'; import { DataKeysCallbacks, DataKeySettingsFunction } from '@home/components/widget/config/data-keys.component.models'; import { EntityType } from '@shared/models/entity-type.models'; diff --git a/ui-ngx/src/app/modules/home/components/widget/config/target-device.component.ts b/ui-ngx/src/app/modules/home/components/widget/config/target-device.component.ts index 14e3187f61..daccadbdfd 100644 --- a/ui-ngx/src/app/modules/home/components/widget/config/target-device.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/config/target-device.component.ts @@ -31,7 +31,7 @@ import { WidgetConfigComponent } from '@home/components/widget/widget-config.com import { TargetDevice, TargetDeviceType } from '@shared/models/widget.models'; import { EntityType } from '@shared/models/entity-type.models'; import { IAliasController } from '@core/api/widget-api.models'; -import { EntityAliasSelectCallbacks } from '@home/components/alias/entity-alias-select.component.models'; +import { EntityAliasSelectCallbacks } from '@home/components/widget/lib/settings/common/alias/entity-alias-select.component.models'; import { takeUntilDestroyed } from '@angular/core/rxjs-interop'; @Component({ diff --git a/ui-ngx/src/app/modules/home/components/widget/config/widget-config-components.module.ts b/ui-ngx/src/app/modules/home/components/widget/config/widget-config-components.module.ts index 4470fc1327..cd0061d22b 100644 --- a/ui-ngx/src/app/modules/home/components/widget/config/widget-config-components.module.ts +++ b/ui-ngx/src/app/modules/home/components/widget/config/widget-config-components.module.ts @@ -24,7 +24,6 @@ import { DataKeyConfigDialogComponent } from '@home/components/widget/config/dat import { DataKeyConfigComponent } from '@home/components/widget/config/data-key-config.component'; import { DatasourceComponent } from '@home/components/widget/config/datasource.component'; import { DatasourcesComponent } from '@home/components/widget/config/datasources.component'; -import { EntityAliasSelectComponent } from '@home/components/alias/entity-alias-select.component'; import { FilterSelectComponent } from '@home/components/filter/filter-select.component'; import { WidgetSettingsModule } from '@home/components/widget/lib/settings/widget-settings.module'; import { WidgetSettingsComponent } from '@home/components/widget/config/widget-settings.component'; @@ -45,7 +44,6 @@ import { TargetDeviceComponent } from '@home/components/widget/config/target-dev DatasourceComponent, DatasourcesComponent, TargetDeviceComponent, - EntityAliasSelectComponent, FilterSelectComponent, TimewindowStyleComponent, TimewindowStylePanelComponent, @@ -67,7 +65,6 @@ import { TargetDeviceComponent } from '@home/components/widget/config/target-dev DatasourceComponent, DatasourcesComponent, TargetDeviceComponent, - EntityAliasSelectComponent, FilterSelectComponent, TimewindowStyleComponent, TimewindowStylePanelComponent, diff --git a/ui-ngx/src/app/modules/home/components/widget/config/widget-config.component.models.ts b/ui-ngx/src/app/modules/home/components/widget/config/widget-config.component.models.ts index ec588d69ad..a1c6d2c2b4 100644 --- a/ui-ngx/src/app/modules/home/components/widget/config/widget-config.component.models.ts +++ b/ui-ngx/src/app/modules/home/components/widget/config/widget-config.component.models.ts @@ -23,7 +23,7 @@ import { PageComponent } from '@shared/components/page.component'; import { Store } from '@ngrx/store'; import { AppState } from '@core/core.state'; import { AbstractControl, UntypedFormGroup } from '@angular/forms'; -import { DataKey, DatasourceType, WidgetConfigMode, widgetType } from '@shared/models/widget.models'; +import { DataKey, DatasourceType, Widget, WidgetConfigMode, widgetType } from '@shared/models/widget.models'; import { WidgetConfigComponent } from '@home/components/widget/widget-config.component'; import { isDefinedAndNotNull } from '@core/utils'; import { IAliasController } from '@core/api/widget-api.models'; @@ -67,6 +67,10 @@ export abstract class BasicWidgetConfigComponent extends PageComponent implement return this.widgetConfigComponent.widgetConfigCallbacks; } + get functionsOnly(): boolean { + return this.widgetConfigComponent.functionsOnly; + } + get widgetType(): widgetType { return this.widgetConfigComponent.widgetType; } @@ -75,6 +79,10 @@ export abstract class BasicWidgetConfigComponent extends PageComponent implement return this.widgetConfigComponent.widgetEditMode; } + get widget(): Widget { + return this.widgetConfigComponent.widget; + } + widgetConfigChangedEmitter = new EventEmitter(); widgetConfigChanged = this.widgetConfigChangedEmitter.asObservable(); diff --git a/ui-ngx/src/app/modules/home/components/widget/config/widget-settings.component.ts b/ui-ngx/src/app/modules/home/components/widget/config/widget-settings.component.ts index ab63e0e55d..6594e610ab 100644 --- a/ui-ngx/src/app/modules/home/components/widget/config/widget-settings.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/config/widget-settings.component.ts @@ -75,6 +75,9 @@ export class WidgetSettingsComponent implements ControlValueAccessor, OnDestroy, @Input() callbacks: WidgetConfigCallbacks; + @Input() + functionsOnly: boolean; + @Input() dashboard: Dashboard; @@ -140,6 +143,11 @@ export class WidgetSettingsComponent implements ControlValueAccessor, OnDestroy, this.definedSettingsComponent.dataKeyCallbacks = this.callbacks; } } + if (propName === 'functionsOnly') { + if (this.definedSettingsComponent) { + this.definedSettingsComponent.functionsOnly = this.functionsOnly; + } + } if (propName === 'widgetConfig') { if (this.definedSettingsComponent) { this.definedSettingsComponent.widgetConfig = this.widgetConfig; @@ -229,6 +237,7 @@ export class WidgetSettingsComponent implements ControlValueAccessor, OnDestroy, this.definedSettingsComponent = this.definedSettingsComponentRef.instance; this.definedSettingsComponent.aliasController = this.aliasController; this.definedSettingsComponent.callbacks = this.callbacks; + this.definedSettingsComponent.functionsOnly = this.functionsOnly; this.definedSettingsComponent.dataKeyCallbacks = this.callbacks; this.definedSettingsComponent.dashboard = this.dashboard; this.definedSettingsComponent.widget = this.widget; diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/maps/map-data-layer.ts b/ui-ngx/src/app/modules/home/components/widget/lib/maps/map-data-layer.ts new file mode 100644 index 0000000000..9d47c3a272 --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/widget/lib/maps/map-data-layer.ts @@ -0,0 +1,170 @@ +/// +/// Copyright © 2016-2024 The Thingsboard Authors +/// +/// Licensed under the Apache License, Version 2.0 (the "License"); +/// you may not use this file except in compliance with the License. +/// You may obtain a copy of the License at +/// +/// http://www.apache.org/licenses/LICENSE-2.0 +/// +/// Unless required by applicable law or agreed to in writing, software +/// distributed under the License is distributed on an "AS IS" BASIS, +/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +/// See the License for the specific language governing permissions and +/// limitations under the License. +/// + +import { + CirclesDataLayerSettings, + MapDataLayerSettings, mapDataSourceSettingsToDatasource, + MarkersDataLayerSettings, PolygonsDataLayerSettings, TbMapDatasource +} from '@home/components/widget/lib/maps/map.models'; +import { TbMap } from '@home/components/widget/lib/maps/map'; +import { FormattedData } from '@shared/models/widget.models'; +import { Observable, of } from 'rxjs'; +import { guid } from '@core/utils'; +import L from 'leaflet'; + +abstract class TbDataLayerItem> { + + protected layer: L.Layer; + +} + +export enum MapDataLayerType { + marker = 'marker', + polygon = 'polygon', + circle = 'circle' +} + +export abstract class TbMapDataLayer { + + protected datasource: TbMapDatasource; + + protected mapDataId = guid(); + + protected constructor(protected map: TbMap, + protected settings: S) { + } + + public setup(): Observable { + this.datasource = mapDataSourceSettingsToDatasource(this.settings); + this.datasource.dataKeys = this.settings.additionalDataKeys ? [...this.settings.additionalDataKeys] : []; + this.mapDataId = this.datasource.mapDataIds[0]; + this.datasource = this.setupDatasource(this.datasource); + return this.doSetup(); + } + + public getDatasource(): TbMapDatasource { + return this.datasource; + } + + public updateData(dsData: FormattedData[]) { + const layerData = dsData.filter(d => d.$datasource.mapDataIds.includes(this.mapDataId)); + this.onData(layerData, dsData); + } + + protected setupDatasource(datasource: TbMapDatasource): TbMapDatasource { + return datasource; + } + + public abstract dataLayerType(): MapDataLayerType; + + protected abstract doSetup(): Observable; + + protected abstract onData(layerData: FormattedData[], dsData: FormattedData[]); + +} + +export class TbMarkersDataLayer extends TbMapDataLayer { + + constructor(protected map: TbMap, + protected settings: MarkersDataLayerSettings) { + super(map, settings); + } + + public dataLayerType(): MapDataLayerType { + return MapDataLayerType.marker; + } + + protected setupDatasource(datasource: TbMapDatasource): TbMapDatasource { + datasource.dataKeys.push(this.settings.xKey, this.settings.yKey); + return datasource; + } + + protected doSetup(): Observable { + return of(null); + } + + protected onData(layerData: FormattedData[], dsData: FormattedData[]) { + layerData.forEach((data, index) => { + console.log(`[${this.mapDataId}][${index}]: Markers layer data updated!`); + console.log(data); + this.markerData(data, dsData); + }); + } + + private markerData(data: FormattedData, dsData: FormattedData[]) { + const xKeyVal = data[this.settings.xKey.label]; + const yKeyVal = data[this.settings.yKey.label]; + } + +} + +export class TbPolygonsDataLayer extends TbMapDataLayer { + + constructor(protected map: TbMap, + protected settings: PolygonsDataLayerSettings) { + super(map, settings); + } + + public dataLayerType(): MapDataLayerType { + return MapDataLayerType.polygon; + } + + protected setupDatasource(datasource: TbMapDatasource): TbMapDatasource { + datasource.dataKeys.push(this.settings.polygonKey); + return datasource; + } + + protected doSetup(): Observable { + return of(null); + } + + protected onData(layerData: FormattedData[], dsData: FormattedData[]) { + layerData.forEach((data, index) => { + console.log(`[${this.mapDataId}][${index}]: Polygons layer data updated!`); + console.log(data); + }); + } + +} + +export class TbCirclesDataLayer extends TbMapDataLayer { + + constructor(protected map: TbMap, + protected settings: CirclesDataLayerSettings) { + super(map, settings); + } + + public dataLayerType(): MapDataLayerType { + return MapDataLayerType.circle; + } + + protected setupDatasource(datasource: TbMapDatasource): TbMapDatasource { + datasource.dataKeys.push(this.settings.circleKey); + return datasource; + } + + protected doSetup(): Observable { + return of(null); + } + + protected onData(layerData: FormattedData[], dsData: FormattedData[]) { + layerData.forEach((data, index) => { + console.log(`[${this.mapDataId}][${index}]: Circles layer data updated!`); + console.log(data); + }); + } + +} diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/maps/map.models.ts b/ui-ngx/src/app/modules/home/components/widget/lib/maps/map.models.ts index 48f2362f0d..fe7dde4114 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/maps/map.models.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/maps/map.models.ts @@ -14,11 +14,11 @@ /// limitations under the License. /// -import { DataKey, DatasourceType } from '@shared/models/widget.models'; -import { EntityType } from '@shared/models/entity-type.models'; +import { DataKey, Datasource, DatasourceType } from '@shared/models/widget.models'; import { DataKeyType } from '@shared/models/telemetry/telemetry.models'; -import { mergeDeep } from '@core/utils'; -import { AbstractControl, ValidationErrors } from '@angular/forms'; +import { guid, mergeDeep } from '@core/utils'; +import { AbstractControl, ValidationErrors, ValidatorFn } from '@angular/forms'; +import { materialColors } from '@shared/models/material.models'; export enum MapType { geoMap = 'geoMap', @@ -27,33 +27,133 @@ export enum MapType { export interface MapDataSourceSettings { dsType: DatasourceType; - dsEntityType?: EntityType; - dsEntityId?: string; + dsDeviceId?: string; dsEntityAliasId?: string; dsFilterId?: string; } +export interface TbMapDatasource extends Datasource { + mapDataIds: string[]; +} + +export const mapDataSourceSettingsToDatasource = (settings: MapDataSourceSettings): TbMapDatasource => { + return { + type: settings.dsType, + deviceId: settings.dsDeviceId, + entityAliasId: settings.dsEntityAliasId, + filterId: settings.dsFilterId, + dataKeys: [], + mapDataIds: [guid()] + }; +}; + export interface MapDataLayerSettings extends MapDataSourceSettings { additionalDataKeys?: DataKey[]; group?: string; } +export type MapDataLayerType = 'markers' | 'polygons' | 'circles'; + +export const mapDataLayerValid = (dataLayer: MapDataLayerSettings, type: MapDataLayerType): boolean => { + if (!dataLayer.dsType || ![DatasourceType.function, DatasourceType.device, DatasourceType.entity].includes(dataLayer.dsType)) { + return false; + } + switch (dataLayer.dsType) { + case DatasourceType.function: + break; + case DatasourceType.device: + if (!dataLayer.dsDeviceId) { + return false; + } + break; + case DatasourceType.entity: + if (!dataLayer.dsEntityAliasId) { + return false; + } + break; + } + switch (type) { + case 'markers': + const markersDataLayer = dataLayer as MarkersDataLayerSettings; + if (!markersDataLayer.xKey?.type || !markersDataLayer.xKey?.name || + !markersDataLayer.yKey?.type || !markersDataLayer.xKey?.name) { + return false; + } + break; + case 'polygons': + const polygonsDataLayer = dataLayer as PolygonsDataLayerSettings; + if (!polygonsDataLayer.polygonKey?.type || !polygonsDataLayer.polygonKey?.name) { + return false; + } + break; + case 'circles': + const circlesDataLayer = dataLayer as CirclesDataLayerSettings; + if (!circlesDataLayer.circleKey?.type || !circlesDataLayer.circleKey?.name) { + return false; + } + break; + } + return true; +}; + +export const mapDataLayerValidator = (type: MapDataLayerType): ValidatorFn => { + return (control: AbstractControl): ValidationErrors | null => { + const layer: MapDataLayerSettings = control.value; + if (!mapDataLayerValid(layer, type)) { + return { + layer: true + }; + } + return null; + }; +}; + export interface MarkersDataLayerSettings extends MapDataLayerSettings { xKey: DataKey; yKey: DataKey; } -export const defaultMarkersDataLayerSettings = (mapType: MapType): MarkersDataLayerSettings => ({ - dsType: DatasourceType.entity, +const defaultMarkerLatitudeFunction = 'var value = prevValue || 15.833293;\n' + + 'if (time % 5000 < 500) {\n' + + ' value += Math.random() * 0.05 - 0.025;\n' + + '}\n' + + 'return value;'; + +const defaultMarkerLongitudeFunction = 'var value = prevValue || -90.454350;\n' + + 'if (time % 5000 < 500) {\n' + + ' value += Math.random() * 0.05 - 0.025;\n' + + '}\n' + + 'return value;'; + +const defaultMarkerXPosFunction = 'var value = prevValue || 0.2;\n' + + 'if (time % 5000 < 500) {\n' + + ' value += Math.random() * 0.05 - 0.025;\n' + + '}\n' + + 'return value;'; + +const defaultMarkerYPosFunction = 'var value = prevValue || 0.3;\n' + + 'if (time % 5000 < 500) {\n' + + ' value += Math.random() * 0.05 - 0.025;\n' + + '}\n' + + 'return value;'; + +export const defaultMarkersDataLayerSettings = (mapType: MapType, functionsOnly = false): MarkersDataLayerSettings => ({ + dsType: functionsOnly ? DatasourceType.function : DatasourceType.entity, xKey: { - name: MapType.geoMap === mapType ? 'latitude' : 'xPos', + name: functionsOnly ? 'f(x)' : (MapType.geoMap === mapType ? 'latitude' : 'xPos'), label: MapType.geoMap === mapType ? 'latitude' : 'xPos', - type: DataKeyType.attribute + type: functionsOnly ? DataKeyType.function : DataKeyType.attribute, + funcBody: functionsOnly ? (MapType.geoMap === mapType ? defaultMarkerLatitudeFunction : defaultMarkerXPosFunction) : undefined, + settings: {}, + color: materialColors[0].value }, yKey: { - name: MapType.geoMap === mapType ? 'longitude' : 'yPos', + name: functionsOnly ? 'f(x)' : (MapType.geoMap === mapType ? 'longitude' : 'yPos'), label: MapType.geoMap === mapType ? 'longitude' : 'yPos', - type: DataKeyType.attribute + type: functionsOnly ? DataKeyType.function : DataKeyType.attribute, + funcBody: functionsOnly ? (MapType.geoMap === mapType ? defaultMarkerLongitudeFunction : defaultMarkerYPosFunction) : undefined, + settings: {}, + color: materialColors[0].value } }); @@ -61,25 +161,40 @@ export interface PolygonsDataLayerSettings extends MapDataLayerSettings { polygonKey: DataKey; } -export const defaultPolygonsDataLayerSettings: PolygonsDataLayerSettings = { - dsType: DatasourceType.entity, +export const defaultPolygonsDataLayerSettings = (functionsOnly = false): PolygonsDataLayerSettings => ({ + dsType: functionsOnly ? DatasourceType.function : DatasourceType.entity, polygonKey: { - name: 'perimeter', + name: functionsOnly ? 'f(x)' : 'perimeter', label: 'perimeter', - type: DataKeyType.attribute + type: functionsOnly ? DataKeyType.function : DataKeyType.attribute, + settings: {}, + color: materialColors[0].value } -}; +}); export interface CirclesDataLayerSettings extends MapDataLayerSettings { circleKey: DataKey; } -export const defaultCirclesDataLayerSettings: CirclesDataLayerSettings = { - dsType: DatasourceType.entity, +export const defaultCirclesDataLayerSettings = (functionsOnly = false): CirclesDataLayerSettings => ({ + dsType: functionsOnly ? DatasourceType.function : DatasourceType.entity, circleKey: { - name: 'perimeter', + name: functionsOnly ? 'f(x)' : 'perimeter', label: 'perimeter', - type: DataKeyType.attribute + type: functionsOnly ? DataKeyType.function : DataKeyType.attribute, + settings: {}, + color: materialColors[0].value + } +}); + +export const defaultMapDataLayerSettings = (mapType: MapType, dataLayerType: MapDataLayerType, functionsOnly = false): MapDataLayerSettings => { + switch (dataLayerType) { + case 'markers': + return defaultMarkersDataLayerSettings(mapType, functionsOnly); + case 'polygons': + return defaultPolygonsDataLayerSettings(functionsOnly); + case 'circles': + return defaultCirclesDataLayerSettings(functionsOnly); } }; @@ -87,6 +202,14 @@ export interface AdditionalMapDataSourceSettings extends MapDataSourceSettings { dataKeys: DataKey[]; } +export const additionalMapDataSourcesToDatasources = (additionalMapDataSources: AdditionalMapDataSourceSettings[]): TbMapDatasource[] => { + return additionalMapDataSources.map(addDs => { + const res = mapDataSourceSettingsToDatasource(addDs); + res.dataKeys = addDs.dataKeys; + return res; + }); +}; + export enum MapControlsPosition { topleft = 'topleft', topright = 'topright', @@ -428,3 +551,51 @@ export function parseCenterPosition(position: string | [number, number]): [numbe } return [0, 0]; } + +export const mergeMapDatasources = (target: TbMapDatasource[], source: TbMapDatasource[]): TbMapDatasource[] => { + const appendDatasources: TbMapDatasource[] = []; + for (const sourceDs of source) { + let merged = false; + for (let i = 0; i < target.length; i++) { + const targetDs = target[i]; + if (mapDatasourceIsSame(targetDs, sourceDs)) { + target[i] = mergeMapDatasource(targetDs, sourceDs); + merged = true; + break; + } + } + if (!merged) { + appendDatasources.push(sourceDs); + } + } + target.push(...appendDatasources); + return target; +}; + +const mapDatasourceIsSame = (ds1: TbMapDatasource, ds2: TbMapDatasource): boolean => { + if (ds1.type === ds2.type) { + switch (ds1.type) { + case DatasourceType.function: + return true; + case DatasourceType.device: + return ds1.deviceId === ds2.deviceId; + case DatasourceType.entity: + return ds1.entityAliasId === ds2.entityAliasId; + } + } + return false; +} + +const mergeMapDatasource = (target: TbMapDatasource, source: TbMapDatasource): TbMapDatasource => { + target.mapDataIds.push(...source.mapDataIds); + const appendKeys: DataKey[] = []; + for (const sourceKey of source.dataKeys) { + const found = + target.dataKeys.find(key => key.type === sourceKey.type && key.name === sourceKey.name && key.label === sourceKey.label); + if (!found) { + appendKeys.push(sourceKey); + } + } + target.dataKeys.push(...appendKeys); + return target; +} 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 cf61fc34ef..c664418ce8 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 @@ -15,6 +15,7 @@ /// import { + additionalMapDataSourcesToDatasources, BaseMapSettings, DEFAULT_ZOOM_LEVEL, defaultGeoMapSettings, @@ -23,17 +24,27 @@ import { ImageMapSettings, MapSetting, MapType, - MapZoomAction, - parseCenterPosition + MapZoomAction, mergeMapDatasources, + parseCenterPosition, TbMapDatasource } from '@home/components/widget/lib/maps/map.models'; import { WidgetContext } from '@home/models/widget-component.models'; -import { mergeDeep, mergeDeepIgnoreArray } from '@core/utils'; +import { formattedDataFormDatasourceData, isDefinedAndNotNull, mergeDeepIgnoreArray } from '@core/utils'; import { DeepPartial } from '@shared/models/common'; import L from 'leaflet'; import { forkJoin, Observable, of } from 'rxjs'; import { TbMapLayer } from '@home/components/widget/lib/maps/map-layer'; import { map, switchMap, tap } from 'rxjs/operators'; import '@home/components/widget/lib/maps/leaflet/leaflet-tb'; +import { + MapDataLayerType, + TbCirclesDataLayer, + TbMapDataLayer, + TbMarkersDataLayer, + TbPolygonsDataLayer +} from '@home/components/widget/lib/maps/map-data-layer'; +import { IWidgetSubscription, WidgetSubscriptionOptions } from '@core/api/widget-api.models'; +import { FormattedData, widgetType } from '@shared/models/widget.models'; +import { EntityDataPageLink } from '@shared/models/query/query.models'; export abstract class TbMap { @@ -54,6 +65,8 @@ export abstract class TbMap { protected defaultCenterPosition: [number, number]; protected bounds: L.LatLngBounds; + protected dataLayers: TbMapDataLayer[]; + protected mapElement: HTMLElement; private readonly mapResize$: ResizeObserver; @@ -112,6 +125,73 @@ export abstract class TbMap { } else { this.bounds = new L.LatLngBounds(null, null); } + this.setupDataLayers(); + } + + private setupDataLayers() { + this.dataLayers = []; + if (this.settings.markers) { + this.dataLayers.push(...this.settings.markers.map(settings => new TbMarkersDataLayer(this, settings))); + } + if (this.settings.polygons) { + this.dataLayers.push(...this.settings.polygons.map(settings => new TbPolygonsDataLayer(this, settings))); + } + if (this.settings.circles) { + this.dataLayers.push(...this.settings.circles.map(settings => new TbCirclesDataLayer(this, settings))); + } + if (this.dataLayers.length) { + const setup = this.dataLayers.map(dl => dl.setup()); + forkJoin(setup).subscribe( + () => { + let datasources: TbMapDatasource[]; + for (const layerType of (Object.keys(MapDataLayerType) as MapDataLayerType[])) { + const typeDatasources = this.dataLayers.filter(dl => dl.dataLayerType() === layerType).map(dl => dl.getDatasource()); + if (!datasources) { + datasources = typeDatasources; + } else { + datasources = mergeMapDatasources(datasources, typeDatasources); + } + } + const additionalDatasources = additionalMapDataSourcesToDatasources(this.settings.additionalDataSources); + datasources = mergeMapDatasources(datasources, additionalDatasources); + const dataLayersSubscriptionOptions: WidgetSubscriptionOptions = { + datasources, + hasDataPageLink: true, + useDashboardTimewindow: false, + type: widgetType.latest, + callbacks: { + onDataUpdated: (subscription) => { + this.update(subscription); + } + } + }; + this.ctx.subscriptionApi.createSubscription(dataLayersSubscriptionOptions, false).subscribe( + (dataLayersSubscription) => { + let pageSize = this.settings.mapPageSize; + if (isDefinedAndNotNull(this.ctx.widgetConfig.pageSize)) { + pageSize = Math.max(pageSize, this.ctx.widgetConfig.pageSize); + } + const pageLink: EntityDataPageLink = { + page: 0, + pageSize, + textSearch: null, + dynamic: true + }; + dataLayersSubscription.paginatedDataSubscriptionUpdated.subscribe(() => { + // this.map.resetState(); + }); + dataLayersSubscription.subscribeAllForPaginatedData(pageLink, null); + } + ); + } + ); + } + } + + private update(subscription: IWidgetSubscription) { + const dsData = formattedDataFormDatasourceData(subscription.data, + undefined, undefined, el => el.datasource.entityId + el.datasource.mapDataIds[0]); + this.dataLayers.forEach(dl => dl.updateData(dsData)); } private resize() { diff --git a/ui-ngx/src/app/modules/home/components/alias/entity-alias-select.component.html b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/alias/entity-alias-select.component.html similarity index 56% rename from ui-ngx/src/app/modules/home/components/alias/entity-alias-select.component.html rename to ui-ngx/src/app/modules/home/components/widget/lib/settings/common/alias/entity-alias-select.component.html index 008d1aa9fa..db71e367e8 100644 --- a/ui-ngx/src/app/modules/home/components/alias/entity-alias-select.component.html +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/alias/entity-alias-select.component.html @@ -15,9 +15,14 @@ limitations under the License. --> - - {{ 'entity.entity-alias' | translate }} - + {{ 'entity.entity-alias' | translate }} + - - - + + + warning + +
+ +
+ +
+
+
diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/map/map-data-layer-row.component.scss b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/map/map-data-layer-row.component.scss new file mode 100644 index 0000000000..d4ebdbaeed --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/map/map-data-layer-row.component.scss @@ -0,0 +1,45 @@ +/** + * Copyright © 2016-2024 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +@import '../../../../../../../../../scss/constants'; + +.tb-form-table-row.tb-map-data-layer-row { + + .tb-source-field { + flex: 1 1 50%; + display: flex; + gap: 12px; + .tb-ds-type-field, .tb-device-field, .tb-entity-alias-field { + flex: 1; + } + } + + .tb-x-pos-field { + flex: 1 1 25%; + } + + .tb-y-pos-field { + flex: 1 1 25%; + } + + .tb-key-field { + flex: 1 1 50%; + } + + .tb-remove-button { + width: 40px; + min-width: 40px; + } +} diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/map/map-data-layer-row.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/map/map-data-layer-row.component.ts new file mode 100644 index 0000000000..1d9d2014e2 --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/map/map-data-layer-row.component.ts @@ -0,0 +1,380 @@ +/// +/// Copyright © 2016-2024 The Thingsboard Authors +/// +/// Licensed under the Apache License, Version 2.0 (the "License"); +/// you may not use this file except in compliance with the License. +/// You may obtain a copy of the License at +/// +/// http://www.apache.org/licenses/LICENSE-2.0 +/// +/// Unless required by applicable law or agreed to in writing, software +/// distributed under the License is distributed on an "AS IS" BASIS, +/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +/// See the License for the specific language governing permissions and +/// limitations under the License. +/// + +import { + ChangeDetectorRef, + Component, + DestroyRef, + EventEmitter, + forwardRef, + Input, + OnInit, + Output, + Renderer2, + ViewContainerRef, + ViewEncapsulation +} from '@angular/core'; +import { + ControlValueAccessor, + NG_VALUE_ACCESSOR, + UntypedFormBuilder, + UntypedFormGroup, + Validators +} from '@angular/forms'; +import { MatButton } from '@angular/material/button'; +import { TbPopoverService } from '@shared/components/popover.service'; +import { TranslateService } from '@ngx-translate/core'; +import { takeUntilDestroyed } from '@angular/core/rxjs-interop'; +import { + CirclesDataLayerSettings, + MapDataLayerSettings, + MapDataLayerType, + MapType, + MarkersDataLayerSettings, + PolygonsDataLayerSettings +} from '@home/components/widget/lib/maps/map.models'; +import { + DataKey, + DataKeyConfigMode, + DatasourceType, + datasourceTypeTranslationMap, + widgetType +} from '@shared/models/widget.models'; +import { EntityType } from '@shared/models/entity-type.models'; +import { DataKeyType } from '@shared/models/telemetry/telemetry.models'; +import { MapSettingsComponent } from '@home/components/widget/lib/settings/common/map/map-settings.component'; +import { IAliasController } from '@core/api/widget-api.models'; +import { DataKeysCallbacks } from '@home/components/widget/config/data-keys.component.models'; +import { + DataKeyConfigDialogComponent, + DataKeyConfigDialogData +} from '@home/components/widget/config/data-key-config-dialog.component'; +import { deepClone } from '@core/utils'; +import { MatDialog } from '@angular/material/dialog'; +import { + EntityAliasSelectCallbacks +} from '@home/components/widget/lib/settings/common/alias/entity-alias-select.component.models'; + +@Component({ + selector: 'tb-map-data-layer-row', + templateUrl: './map-data-layer-row.component.html', + styleUrls: ['./map-data-layer-row.component.scss'], + providers: [ + { + provide: NG_VALUE_ACCESSOR, + useExisting: forwardRef(() => MapDataLayerRowComponent), + multi: true + } + ], + encapsulation: ViewEncapsulation.None +}) +export class MapDataLayerRowComponent implements ControlValueAccessor, OnInit { + + DatasourceType = DatasourceType; + DataKeyType = DataKeyType; + + EntityType = EntityType; + + MapType = MapType; + + datasourceTypes: Array = []; + datasourceTypesTranslations = datasourceTypeTranslationMap; + + @Input() + disabled: boolean; + + @Input() + mapType: MapType = MapType.geoMap; + + @Input() + dataLayerType: MapDataLayerType = 'markers'; + + get functionsOnly(): boolean { + return this.mapSettingsComponent.functionsOnly; + } + + get aliasController(): IAliasController { + return this.mapSettingsComponent.aliasController; + } + + get dataKeyCallbacks(): DataKeysCallbacks { + return this.mapSettingsComponent.callbacks; + } + + public get entityAliasSelectCallbacks(): EntityAliasSelectCallbacks { + return this.mapSettingsComponent.callbacks; + } + + @Output() + dataLayerRemoved = new EventEmitter(); + + generateDataKey = this._generateDataKey.bind(this); + + dataLayerFormGroup: UntypedFormGroup; + + modelValue: MapDataLayerSettings; + + editDataLayerText: string; + + removeDataLayerText: string; + + private propagateChange = (_val: any) => {}; + + constructor(private mapSettingsComponent: MapSettingsComponent, + private fb: UntypedFormBuilder, + private dialog: MatDialog, + private translate: TranslateService, + private popoverService: TbPopoverService, + private renderer: Renderer2, + private viewContainerRef: ViewContainerRef, + private cd: ChangeDetectorRef, + private destroyRef: DestroyRef) { + } + + ngOnInit() { + if (this.functionsOnly) { + this.datasourceTypes = [DatasourceType.function]; + } else { + this.datasourceTypes = [DatasourceType.function, DatasourceType.device, DatasourceType.entity]; + } + this.dataLayerFormGroup = this.fb.group({ + dsType: [null, [Validators.required]], + dsDeviceId: [null, [Validators.required]], + dsEntityAliasId: [null, [Validators.required]] + }); + switch (this.dataLayerType) { + case 'markers': + this.editDataLayerText = 'widgets.maps.data-layer.marker.marker-configuration'; + this.removeDataLayerText = 'widgets.maps.data-layer.marker.remove-marker'; + this.dataLayerFormGroup.addControl('xKey', this.fb.control(null, Validators.required)); + this.dataLayerFormGroup.addControl('yKey', this.fb.control(null, Validators.required)); + break; + case 'polygons': + this.editDataLayerText = 'widgets.maps.data-layer.polygon.polygon-configuration'; + this.removeDataLayerText = 'widgets.maps.data-layer.polygon.remove-polygon'; + this.dataLayerFormGroup.addControl('polygonKey', this.fb.control(null, Validators.required)); + break; + case 'circles': + this.editDataLayerText = 'widgets.maps.data-layer.circle.circle-configuration'; + this.removeDataLayerText = 'widgets.maps.data-layer.circle.remove-circle'; + this.dataLayerFormGroup.addControl('circleKey', this.fb.control(null, Validators.required)); + break; + } + this.dataLayerFormGroup.valueChanges.pipe( + takeUntilDestroyed(this.destroyRef) + ).subscribe( + () => this.updateModel() + ); + this.dataLayerFormGroup.get('dsType').valueChanges.pipe( + takeUntilDestroyed(this.destroyRef) + ).subscribe( + (newDsType: DatasourceType) => this.onDsTypeChanged(newDsType) + ); + } + + registerOnChange(fn: any): void { + this.propagateChange = fn; + } + + registerOnTouched(_fn: any): void { + } + + setDisabledState(isDisabled: boolean): void { + this.disabled = isDisabled; + if (isDisabled) { + this.dataLayerFormGroup.disable({emitEvent: false}); + } else { + this.dataLayerFormGroup.enable({emitEvent: false}); + this.updateValidators(); + } + } + + writeValue(value: MapDataLayerSettings): void { + this.modelValue = value; + this.dataLayerFormGroup.patchValue( + { + dsType: value?.dsType, + dsDeviceId: value?.dsDeviceId, + dsEntityAliasId: value?.dsEntityAliasId + }, {emitEvent: false} + ); + switch (this.dataLayerType) { + case 'markers': + const markersDataLayer = value as MarkersDataLayerSettings; + this.dataLayerFormGroup.patchValue( + { + xKey: markersDataLayer?.xKey, + yKey: markersDataLayer?.yKey + }, {emitEvent: false} + ); + break; + case 'polygons': + const polygonsDataLayer = value as PolygonsDataLayerSettings; + this.dataLayerFormGroup.patchValue( + { + polygonKey: polygonsDataLayer?.polygonKey + }, {emitEvent: false} + ); + break; + case 'circles': + const circlesDataLayer = value as CirclesDataLayerSettings; + this.dataLayerFormGroup.patchValue( + { + circleKey: circlesDataLayer?.circleKey + }, {emitEvent: false} + ); + break; + } + this.updateValidators(); + this.cd.markForCheck(); + } + + editKey(keyType: 'xKey' | 'yKey' | 'polygonKey' | 'circleKey') { + const targetDataKey: DataKey = this.dataLayerFormGroup.get(keyType).value; + this.dialog.open(DataKeyConfigDialogComponent, + { + disableClose: true, + panelClass: ['tb-dialog', 'tb-fullscreen-dialog'], + data: { + dataKey: deepClone(targetDataKey), + dataKeyConfigMode: DataKeyConfigMode.general, + aliasController: this.aliasController, + widgetType: widgetType.latest, + deviceId: this.dataLayerFormGroup.get('dsDeviceId').value, + entityAliasId: this.dataLayerFormGroup.get('dsEntityAliasId').value, + showPostProcessing: true, + callbacks: this.mapSettingsComponent.callbacks, + hideDataKeyColor: true, + hideDataKeyDecimals: true, + hideDataKeyUnits: true, + widget: this.mapSettingsComponent.widget, + dashboard: null, + dataKeySettingsForm: null, + dataKeySettingsDirective: null + } + }).afterClosed().subscribe((updatedDataKey) => { + if (updatedDataKey) { + this.dataLayerFormGroup.get(keyType).patchValue(updatedDataKey); + } + }); + } + + editDataLayer($event: Event, matButton: MatButton) { + if ($event) { + $event.stopPropagation(); + } + const trigger = matButton._elementRef.nativeElement; + if (this.popoverService.hasPopover(trigger)) { + this.popoverService.hidePopover(trigger); + } else { + /*const ctx: any = { + mapLayerSettings: deepClone(this.modelValue) + }; + const mapLayerSettingsPanelPopover = this.popoverService.displayPopover(trigger, this.renderer, + this.viewContainerRef, MapLayerSettingsPanelComponent, ['leftOnly', 'leftTopOnly', 'leftBottomOnly'], true, null, + ctx, + {}, + {}, {}, true); + mapLayerSettingsPanelPopover.tbComponentRef.instance.popover = mapLayerSettingsPanelPopover; + mapLayerSettingsPanelPopover.tbComponentRef.instance.mapLayerSettingsApplied.subscribe((layer) => { + mapLayerSettingsPanelPopover.hide(); + this.layerFormGroup.patchValue( + layer, + {emitEvent: false}); + this.updateValidators(); + this.updateModel(); + });*/ + } + } + + private _generateDataKey(key: DataKey): DataKey { + key = this.dataKeyCallbacks.generateDataKey(key.name, key.type, null, false, + null); + return key; + } + + private onDsTypeChanged(newDsType: DatasourceType) { + let updateModel = false; + switch (this.dataLayerType) { + case 'markers': + const xKey: DataKey = this.dataLayerFormGroup.get('xKey').value; + if (this.updateDataKeyToNewDsType(xKey, newDsType)) { + this.dataLayerFormGroup.get('xKey').patchValue(xKey, {emitEvent: false}); + updateModel = true; + } + const yKey: DataKey = this.dataLayerFormGroup.get('yKey').value; + if (this.updateDataKeyToNewDsType(yKey, newDsType)) { + this.dataLayerFormGroup.get('yKey').patchValue(yKey, {emitEvent: false}); + updateModel = true; + } + break; + case 'polygons': + const polygonKey: DataKey = this.dataLayerFormGroup.get('polygonKey').value; + if (this.updateDataKeyToNewDsType(polygonKey, newDsType)) { + this.dataLayerFormGroup.get('polygonKey').patchValue(polygonKey, {emitEvent: false}); + updateModel = true; + } + break; + case 'circles': + const circleKey: DataKey = this.dataLayerFormGroup.get('circleKey').value; + if (this.updateDataKeyToNewDsType(circleKey, newDsType)) { + this.dataLayerFormGroup.get('circleKey').patchValue(circleKey, {emitEvent: false}); + updateModel = true; + } + break; + } + this.updateValidators(); + if (updateModel) { + this.updateModel(); + } + } + + private updateDataKeyToNewDsType(dataKey: DataKey, newDsType: DatasourceType): boolean { + if (newDsType === DatasourceType.function) { + if (dataKey.type !== DataKeyType.function) { + dataKey.type = DataKeyType.function; + return true; + } + } else { + if (dataKey.type === DataKeyType.function) { + dataKey.type = DataKeyType.attribute; + return true; + } + } + return false; + } + + private updateValidators() { + const dsType: DatasourceType = this.dataLayerFormGroup.get('dsType').value; + if (dsType === DatasourceType.function) { + this.dataLayerFormGroup.get('dsDeviceId').disable({emitEvent: false}); + this.dataLayerFormGroup.get('dsEntityAliasId').disable({emitEvent: false}); + } else if (dsType === DatasourceType.device) { + this.dataLayerFormGroup.get('dsDeviceId').enable({emitEvent: false}); + this.dataLayerFormGroup.get('dsEntityAliasId').disable({emitEvent: false}); + } else { + this.dataLayerFormGroup.get('dsDeviceId').disable({emitEvent: false}); + this.dataLayerFormGroup.get('dsEntityAliasId').enable({emitEvent: false}); + } + } + + private updateModel() { + this.modelValue = {...this.modelValue, ...this.dataLayerFormGroup.value}; + this.propagateChange(this.modelValue); + } + + protected readonly datasourceType = DatasourceType; +} diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/map/map-data-layers.component.html b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/map/map-data-layers.component.html new file mode 100644 index 0000000000..0c19ff6a9b --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/map/map-data-layers.component.html @@ -0,0 +1,51 @@ + +
+
+
+
widgets.maps.data-layer.source
+
+ {{ (mapType === MapType.geoMap ? 'widgets.maps.data-layer.marker.latitude-key' : 'widgets.maps.data-layer.marker.x-pos-key') | translate }} +
+
+ {{ (mapType === MapType.geoMap ? 'widgets.maps.data-layer.marker.longitude-key' : 'widgets.maps.data-layer.marker.y-pos-key') | translate }} +
+
widgets.maps.data-layer.polygon.polygon-key
+
widgets.maps.data-layer.circle.circle-key
+
+
+
+
+ + +
+
+
+
+ +
+
+ + {{ noDataLayersText | translate }} + diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/map/map-data-layers.component.scss b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/map/map-data-layers.component.scss new file mode 100644 index 0000000000..7795f9de6e --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/map/map-data-layers.component.scss @@ -0,0 +1,42 @@ +/** + * Copyright © 2016-2024 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +.tb-map-data-layers { + .tb-form-table-header-cell { + &.tb-source-header { + flex: 1 1 50%; + } + &.tb-x-pos-header { + flex: 1 1 25%; + } + &.tb-y-pos-header { + flex: 1 1 25%; + } + &.tb-key-header { + flex: 1 1 50%; + } + &.tb-actions-header { + width: 80px; + min-width: 80px; + } + } + + .tb-form-table-body { + tb-map-data-layer-row { + overflow: hidden; + } + } +} diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/map/map-data-layers.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/map/map-data-layers.component.ts new file mode 100644 index 0000000000..5c8aa5534d --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/map/map-data-layers.component.ts @@ -0,0 +1,177 @@ +/// +/// Copyright © 2016-2024 The Thingsboard Authors +/// +/// Licensed under the Apache License, Version 2.0 (the "License"); +/// you may not use this file except in compliance with the License. +/// You may obtain a copy of the License at +/// +/// http://www.apache.org/licenses/LICENSE-2.0 +/// +/// Unless required by applicable law or agreed to in writing, software +/// distributed under the License is distributed on an "AS IS" BASIS, +/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +/// See the License for the specific language governing permissions and +/// limitations under the License. +/// + +import { Component, DestroyRef, forwardRef, Input, OnInit, ViewEncapsulation } from '@angular/core'; +import { + AbstractControl, + ControlValueAccessor, + NG_VALIDATORS, + NG_VALUE_ACCESSOR, + UntypedFormArray, + UntypedFormBuilder, + UntypedFormControl, + UntypedFormGroup, + Validator +} from '@angular/forms'; +import { mergeDeep } from '@core/utils'; +import { takeUntilDestroyed } from '@angular/core/rxjs-interop'; +import { + defaultMapDataLayerSettings, + MapDataLayerSettings, + MapDataLayerType, + mapDataLayerValid, + mapDataLayerValidator, + MapType +} from '@home/components/widget/lib/maps/map.models'; +import { MapSettingsComponent } from '@home/components/widget/lib/settings/common/map/map-settings.component'; + +@Component({ + selector: 'tb-map-data-layers', + templateUrl: './map-data-layers.component.html', + styleUrls: ['./map-data-layers.component.scss'], + providers: [ + { + provide: NG_VALUE_ACCESSOR, + useExisting: forwardRef(() => MapDataLayersComponent), + multi: true + }, + { + provide: NG_VALIDATORS, + useExisting: forwardRef(() => MapDataLayersComponent), + multi: true + } + ], + encapsulation: ViewEncapsulation.None +}) +export class MapDataLayersComponent implements ControlValueAccessor, OnInit, Validator { + + MapType = MapType; + + @Input() + disabled: boolean; + + @Input() + mapType: MapType = MapType.geoMap; + + @Input() + dataLayerType: MapDataLayerType = 'markers'; + + get functionsOnly(): boolean { + return this.mapSettingsComponent.functionsOnly; + } + + dataLayersFormGroup: UntypedFormGroup; + + addDataLayerText: string; + + noDataLayersText: string; + + private propagateChange = (_val: any) => {}; + + constructor(private mapSettingsComponent: MapSettingsComponent, + private fb: UntypedFormBuilder, + private destroyRef: DestroyRef) { + } + + ngOnInit() { + switch (this.dataLayerType) { + case 'markers': + this.addDataLayerText = 'widgets.maps.data-layer.marker.add-marker'; + this.noDataLayersText = 'widgets.maps.data-layer.marker.no-markers'; + break; + case 'polygons': + this.addDataLayerText = 'widgets.maps.data-layer.polygon.add-polygon'; + this.noDataLayersText = 'widgets.maps.data-layer.polygon.no-polygons'; + break; + case 'circles': + this.addDataLayerText = 'widgets.maps.data-layer.circle.add-circle'; + this.noDataLayersText = 'widgets.maps.data-layer.circle.no-circles'; + break; + } + this.dataLayersFormGroup = this.fb.group({ + dataLayers: [this.fb.array([]), []] + }); + this.dataLayersFormGroup.valueChanges.pipe( + takeUntilDestroyed(this.destroyRef) + ).subscribe( + () => { + let layers: MapDataLayerSettings[] = this.dataLayersFormGroup.get('dataLayers').value; + if (layers) { + layers = layers.filter(layer => mapDataLayerValid(layer, this.dataLayerType)); + } + this.propagateChange(layers); + } + ); + } + + registerOnChange(fn: any): void { + this.propagateChange = fn; + } + + registerOnTouched(fn: any): void { + } + + setDisabledState(isDisabled: boolean): void { + this.disabled = isDisabled; + if (isDisabled) { + this.dataLayersFormGroup.disable({emitEvent: false}); + } else { + this.dataLayersFormGroup.enable({emitEvent: false}); + } + } + + writeValue(value: MapDataLayerSettings[] | undefined): void { + const dataLayers: MapDataLayerSettings[] = value || []; + this.dataLayersFormGroup.setControl('dataLayers', this.prepareDataLayersFormArray(dataLayers), {emitEvent: false}); + } + + public validate(c: UntypedFormControl) { + const valid = this.dataLayersFormGroup.valid; + return valid ? null : { + dataLayers: { + valid: false, + }, + }; + } + + dataLayersFormArray(): UntypedFormArray { + return this.dataLayersFormGroup.get('dataLayers') as UntypedFormArray; + } + + trackByDataLayer(index: number, dataLayerControl: AbstractControl): any { + return dataLayerControl; + } + + removeDataLayer(index: number) { + (this.dataLayersFormGroup.get('dataLayers') as UntypedFormArray).removeAt(index); + } + + addDataLayer() { + const dataLayer = mergeDeep({} as MapDataLayerSettings, + defaultMapDataLayerSettings(this.mapType, this.dataLayerType, this.functionsOnly)); + const dataLayersArray = this.dataLayersFormGroup.get('dataLayers') as UntypedFormArray; + const dataLayerControl = this.fb.control(dataLayer, [mapDataLayerValidator(this.dataLayerType)]); + dataLayersArray.push(dataLayerControl); + } + + private prepareDataLayersFormArray(dataLayers: MapDataLayerSettings[]): UntypedFormArray { + const dataLayersControls: Array = []; + dataLayers.forEach((dataLayer) => { + dataLayersControls.push(this.fb.control(dataLayer, [mapDataLayerValidator(this.dataLayerType)])); + }); + return this.fb.array(dataLayersControls); + } +} 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 ec606c3736..9652daaf7b 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 @@ -34,12 +34,24 @@
{{ 'widgets.maps.overlays.overlays' | translate }}
- {{ 'widgets.maps.overlays.markers' | translate }} {{ 'widgets.maps.overlays.polygons' | translate }} {{ 'widgets.maps.overlays.circles' | translate }}
+ + +
diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/map/map-settings.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/map/map-settings.component.ts index bdf24c4549..864a0f3ea9 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/map/map-settings.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/map/map-settings.component.ts @@ -22,9 +22,14 @@ import { UntypedFormBuilder, UntypedFormControl, UntypedFormGroup, Validator } from '@angular/forms'; -import { ImageSourceType, MapSetting, MapType } from '@home/components/widget/lib/maps/map.models'; +import { ImageSourceType, MapDataLayerType, MapSetting, MapType } from '@home/components/widget/lib/maps/map.models'; import { takeUntilDestroyed } from '@angular/core/rxjs-interop'; import { merge } from 'rxjs'; +import { coerceBoolean } from '@shared/decorators/coercion'; +import { IAliasController } from '@core/api/widget-api.models'; +import { DataKeysCallbacks } from '@home/components/widget/config/data-keys.component.models'; +import { WidgetConfigCallbacks } from '@home/components/widget/config/widget-config.component.models'; +import { Widget } from '@shared/models/widget.models'; @Component({ selector: 'tb-map-settings', @@ -50,13 +55,26 @@ export class MapSettingsComponent implements OnInit, ControlValueAccessor, Valid @Input() disabled: boolean; + @Input() + @coerceBoolean() + functionsOnly = false; + + @Input() + aliasController: IAliasController; + + @Input() + callbacks: WidgetConfigCallbacks; + + @Input() + widget: Widget; + private modelValue: MapSetting; private propagateChange = null; public mapSettingsFormGroup: UntypedFormGroup; - overlaysMode: 'markers' | 'polygons' | 'circles' = 'markers'; + dataLayerMode: MapDataLayerType = 'markers'; constructor(private fb: UntypedFormBuilder, private destroyRef: DestroyRef) { 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 e9279d853c..7018862d08 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 @@ -189,6 +189,12 @@ import { MapLayerRowComponent } from '@home/components/widget/lib/settings/commo import { MapLayerSettingsPanelComponent } from '@home/components/widget/lib/settings/common/map/map-layer-settings-panel.component'; +import { MapDataLayersComponent } from '@home/components/widget/lib/settings/common/map/map-data-layers.component'; +import { MapDataLayerRowComponent } from '@home/components/widget/lib/settings/common/map/map-data-layer-row.component'; +import { WidgetConfigComponentsModule } from '@home/components/widget/config/widget-config-components.module'; +import { + EntityAliasSelectComponent +} from '@home/components/widget/lib/settings/common/alias/entity-alias-select.component'; @NgModule({ declarations: [ @@ -262,7 +268,10 @@ import { MapLayerSettingsPanelComponent, MapLayerRowComponent, MapLayersComponent, - MapSettingsComponent + MapDataLayerRowComponent, + MapDataLayersComponent, + MapSettingsComponent, + EntityAliasSelectComponent ], imports: [ CommonModule, @@ -337,7 +346,8 @@ import { DynamicFormSelectItemRowComponent, DynamicFormComponent, DynamicFormArrayComponent, - MapSettingsComponent + MapSettingsComponent, + EntityAliasSelectComponent ], providers: [ ColorSettingsComponentService, diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/map/map-widget-settings.component.html b/ui-ngx/src/app/modules/home/components/widget/lib/settings/map/map-widget-settings.component.html index a2c998ac8c..d34506c9cc 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/settings/map/map-widget-settings.component.html +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/map/map-widget-settings.component.html @@ -16,7 +16,13 @@ --> - + +
widget-config.card-appearance
diff --git a/ui-ngx/src/app/modules/home/components/widget/widget-config.component.html b/ui-ngx/src/app/modules/home/components/widget/widget-config.component.html index 80b41c012e..9e2b1bf1fb 100644 --- a/ui-ngx/src/app/modules/home/components/widget/widget-config.component.html +++ b/ui-ngx/src/app/modules/home/components/widget/widget-config.component.html @@ -303,6 +303,7 @@ - - {{ label | translate }} + {{ label | translate }} {{ 'entity.create-new' | translate }} + + warning + @@ -59,7 +75,7 @@
- + {{ requiredErrorText | translate }} diff --git a/ui-ngx/src/app/shared/components/entity/entity-autocomplete.component.ts b/ui-ngx/src/app/shared/components/entity/entity-autocomplete.component.ts index 417fc35bf4..a2920c9822 100644 --- a/ui-ngx/src/app/shared/components/entity/entity-autocomplete.component.ts +++ b/ui-ngx/src/app/shared/components/entity/entity-autocomplete.component.ts @@ -116,6 +116,9 @@ export class EntityAutocompleteComponent implements ControlValueAccessor, OnInit @Input() requiredText: string; + @Input() + placeholder: string; + @Input() @coerceBoolean() useFullEntityId: boolean; @@ -123,6 +126,10 @@ export class EntityAutocompleteComponent implements ControlValueAccessor, OnInit @Input() appearance: MatFormFieldAppearance = 'fill'; + @Input() + @coerceBoolean() + inlineField: boolean; + @Input() @coerceBoolean() required: boolean; diff --git a/ui-ngx/src/app/shared/models/widget.models.ts b/ui-ngx/src/app/shared/models/widget.models.ts index e5e125e171..bbb462d9af 100644 --- a/ui-ngx/src/app/shared/models/widget.models.ts +++ b/ui-ngx/src/app/shared/models/widget.models.ts @@ -510,8 +510,8 @@ export const datasourcesHasOnlyComparisonAggregation = (datasources?: Array { + $datasource: D; entityName: string; deviceName: string; entityId: string; @@ -861,6 +861,7 @@ export interface IWidgetSettingsComponent { aliasController: IAliasController; callbacks: WidgetConfigCallbacks; dataKeyCallbacks: DataKeysCallbacks; + functionsOnly: boolean; dashboard: Dashboard; widget: Widget; widgetConfig: WidgetConfigComponentData; @@ -882,6 +883,8 @@ export abstract class WidgetSettingsComponent extends PageComponent implements dataKeyCallbacks: DataKeysCallbacks; + functionsOnly: boolean; + dashboard: Dashboard; widget: Widget; 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 da5f80d49f..946f338ddd 100644 --- a/ui-ngx/src/assets/locale/locale.constant-en_US.json +++ b/ui-ngx/src/assets/locale/locale.constant-en_US.json @@ -2332,6 +2332,7 @@ "alias-required": "Entity alias is required.", "remove-alias": "Remove entity alias", "add-alias": "Add entity alias", + "edit-alias": "Edit entity alias", "entity-list": "Entity list", "entity-type": "Entity type", "entity-types": "Entity types", @@ -6866,6 +6867,39 @@ "polygons": "Polygons", "circles": "Circles" }, + "data-layer": { + "source": "Source", + "marker": { + "latitude-key": "Latitude key", + "longitude-key": "Longitude key", + "x-pos-key": "X position key", + "y-pos-key": "Y position key", + "latitude-key-required": "Latitude key required", + "longitude-key-required": "Longitude key required", + "x-pos-key-required": "X position key required", + "y-pos-key-required": "Y position key required", + "no-markers": "No markers configured", + "add-marker": "Add marker", + "marker-configuration": "Marker configuration", + "remove-marker": "Remove marker" + }, + "polygon": { + "polygon-key": "Polygon key", + "polygon-key-required": "Polygon key required", + "no-polygons": "No polygons configured", + "add-polygon": "Add polygon", + "polygon-configuration": "Polygon configuration", + "remove-polygon": "Remove polygon" + }, + "circle": { + "circle-key": "Circle key", + "circle-key-required": "Circle key required", + "no-circles": "No circles configured", + "add-circle": "Add circle", + "circle-configuration": "Circle configuration", + "remove-circle": "Remove circle" + } + }, "select-entity": "Select entity", "select-entity-hint": "Hint: after selection click at the map to set position", "tooltips": { From c4468eb469527e645bc66389497041a090828d99 Mon Sep 17 00:00:00 2001 From: Igor Kulikov Date: Wed, 8 Jan 2025 16:57:34 +0200 Subject: [PATCH 017/345] UI: Map data layers implementation. --- .../alias/aliases-entity-select.component.ts | 3 +- .../widget/lib/maps/map-data-layer.ts | 278 ++++++++++++++++-- .../widget/lib/maps/map-widget.component.ts | 7 +- .../components/widget/lib/maps/map.models.ts | 56 +++- .../home/components/widget/lib/maps/map.ts | 135 ++++++++- 5 files changed, 433 insertions(+), 46 deletions(-) diff --git a/ui-ngx/src/app/modules/home/components/alias/aliases-entity-select.component.ts b/ui-ngx/src/app/modules/home/components/alias/aliases-entity-select.component.ts index 69a012d9a0..0a6e4c0bbd 100644 --- a/ui-ngx/src/app/modules/home/components/alias/aliases-entity-select.component.ts +++ b/ui-ngx/src/app/modules/home/components/alias/aliases-entity-select.component.ts @@ -93,6 +93,7 @@ export class AliasesEntitySelectComponent implements OnInit, OnDestroy { setTimeout(() => { this.updateDisplayValue(); this.updateEntityAliasesInfo(); + this.cd.detectChanges(); }, 0); } )); @@ -101,6 +102,7 @@ export class AliasesEntitySelectComponent implements OnInit, OnDestroy { setTimeout(() => { this.updateDisplayValue(); this.updateEntityAliasesInfo(); + this.cd.detectChanges(); }, 0); } )); @@ -184,7 +186,6 @@ export class AliasesEntitySelectComponent implements OnInit, OnDestroy { displayValue = this.translate.instant('entity.entities'); } this.displayValue = displayValue; - this.cd.detectChanges(); } private updateEntityAliasesInfo() { diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/maps/map-data-layer.ts b/ui-ngx/src/app/modules/home/components/widget/lib/maps/map-data-layer.ts index 9d47c3a272..008a55cd4b 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/maps/map-data-layer.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/maps/map-data-layer.ts @@ -16,19 +16,49 @@ import { CirclesDataLayerSettings, - MapDataLayerSettings, mapDataSourceSettingsToDatasource, - MarkersDataLayerSettings, PolygonsDataLayerSettings, TbMapDatasource + isCutPolygon, + isValidLatLng, + MapDataLayerSettings, + mapDataSourceSettingsToDatasource, + MapType, + MarkersDataLayerSettings, + PolygonsDataLayerSettings, + TbCircleData, + TbMapDatasource } from '@home/components/widget/lib/maps/map.models'; import { TbMap } from '@home/components/widget/lib/maps/map'; import { FormattedData } from '@shared/models/widget.models'; import { Observable, of } from 'rxjs'; -import { guid } from '@core/utils'; -import L from 'leaflet'; +import { guid, isDefinedAndNotNull, isEmptyStr, isNotEmptyStr, isString } from '@core/utils'; +import L, { LatLngBounds } from 'leaflet'; +import { isJSON } from '@home/components/widget/lib/maps-legacy/maps-utils'; -abstract class TbDataLayerItem> { +abstract class TbDataLayerItem> { protected layer: L.Layer; + constructor(data: FormattedData, + dsData: FormattedData[], + protected settings: S, + protected dataLayer: L) { + this.layer = this.create(data, dsData); + this.dataLayer.getFeatureGroup().addLayer(this.layer); + } + + protected abstract create(data: FormattedData, dsData: FormattedData[]): L.Layer; + + public abstract update(data: FormattedData, dsData: FormattedData[]): void; + + public remove() { + this.dataLayer.getFeatureGroup().removeLayer(this.layer); + } + + protected updateLayer(newLayer: L.Layer) { + this.dataLayer.getFeatureGroup().removeLayer(this.layer); + this.layer = newLayer; + this.dataLayer.getFeatureGroup().addLayer(this.layer); + } + } export enum MapDataLayerType { @@ -37,12 +67,16 @@ export enum MapDataLayerType { circle = 'circle' } -export abstract class TbMapDataLayer { +export abstract class TbMapDataLayer> { protected datasource: TbMapDatasource; protected mapDataId = guid(); + protected featureGroup = L.featureGroup(); + + protected layerItems = new Map>(); + protected constructor(protected map: TbMap, protected settings: S) { } @@ -59,24 +93,83 @@ export abstract class TbMapDataLayer { return this.datasource; } + public getFeatureGroup(): L.FeatureGroup { + return this.featureGroup; + } + + public getBounds(): LatLngBounds { + return this.featureGroup.getBounds(); + } + public updateData(dsData: FormattedData[]) { const layerData = dsData.filter(d => d.$datasource.mapDataIds.includes(this.mapDataId)); - this.onData(layerData, dsData); + const rawItems = layerData.filter(d => this.isValidLayerData(d)); + const toDelete = new Set(Array.from(this.layerItems.keys())); + rawItems.forEach((data, index) => { + let layerItem = this.layerItems.get(data.entityId); + if (layerItem) { + layerItem.update(data, dsData); + } else { + layerItem = this.createLayerItem(data, dsData); + this.layerItems.set(data.entityId, layerItem); + } + toDelete.delete(data.entityId); + }); + toDelete.forEach((key) => { + const item = this.layerItems.get(key); + item.remove(); + this.layerItems.delete(key); + }); } protected setupDatasource(datasource: TbMapDatasource): TbMapDatasource { return datasource; } + protected mapType(): MapType { + return this.map.type(); + } + public abstract dataLayerType(): MapDataLayerType; protected abstract doSetup(): Observable; - protected abstract onData(layerData: FormattedData[], dsData: FormattedData[]); + protected abstract isValidLayerData(layerData: FormattedData): boolean; + + protected abstract createLayerItem(data: FormattedData, dsData: FormattedData[]): TbDataLayerItem; } -export class TbMarkersDataLayer extends TbMapDataLayer { +class TbMarkerDataLayerItem extends TbDataLayerItem { + + private location: L.LatLng; + private marker: L.Marker; + + constructor(data: FormattedData, + dsData: FormattedData[], + protected settings: MarkersDataLayerSettings, + protected dataLayer: TbMarkersDataLayer) { + super(data, dsData, settings, dataLayer); + } + + protected create(data: FormattedData, dsData: FormattedData[]): L.Layer { + this.location = this.dataLayer.extractLocation(data); + this.marker = L.marker(this.location, { + tbMarkerData: data + }); + return this.marker; + } + public update(data: FormattedData, dsData: FormattedData[]): void { + const position = this.dataLayer.extractLocation(data); + if (!this.marker.getLatLng().equals(position)) { + this.location = position; + this.marker.setLatLng(position); + } + } + +} + +export class TbMarkersDataLayer extends TbMapDataLayer { constructor(protected map: TbMap, protected settings: MarkersDataLayerSettings) { @@ -96,22 +189,96 @@ export class TbMarkersDataLayer extends TbMapDataLayer return of(null); } - protected onData(layerData: FormattedData[], dsData: FormattedData[]) { - layerData.forEach((data, index) => { - console.log(`[${this.mapDataId}][${index}]: Markers layer data updated!`); - console.log(data); - this.markerData(data, dsData); - }); + protected isValidLayerData(layerData: FormattedData): boolean { + return !!this.extractPosition(layerData); } - private markerData(data: FormattedData, dsData: FormattedData[]) { - const xKeyVal = data[this.settings.xKey.label]; - const yKeyVal = data[this.settings.yKey.label]; + protected createLayerItem(data: FormattedData, dsData: FormattedData[]): TbMarkerDataLayerItem { + return new TbMarkerDataLayerItem(data, dsData, this.settings, this); + } + + private extractPosition(data: FormattedData): {x: number; y: number} { + if (data) { + const xKeyVal = data[this.settings.xKey.label]; + const yKeyVal = data[this.settings.yKey.label]; + switch (this.mapType()) { + case MapType.geoMap: + if (!isValidLatLng(xKeyVal, yKeyVal)) { + return null; + } + break; + case MapType.image: + if (!isDefinedAndNotNull(xKeyVal) || isEmptyStr(xKeyVal) || isNaN(xKeyVal) || !isDefinedAndNotNull(yKeyVal) || isEmptyStr(yKeyVal) || isNaN(yKeyVal)) { + return null; + } + break; + } + return {x: xKeyVal, y: yKeyVal}; + } else { + return null; + } + } + + public extractLocation(data: FormattedData): L.LatLng { + const position = this.extractPosition(data); + if (position) { + return this.map.positionToLatLng(position); + } else { + return null; + } } } -export class TbPolygonsDataLayer extends TbMapDataLayer { +class TbPolygonDataLayerItem extends TbDataLayerItem { + + private polygon: L.Polygon; + + constructor(data: FormattedData, + dsData: FormattedData[], + protected settings: PolygonsDataLayerSettings, + protected dataLayer: TbPolygonsDataLayer) { + super(data, dsData, settings, dataLayer); + } + + protected create(data: FormattedData, dsData: FormattedData[]): L.Layer { + const polyData = this.dataLayer.extractPolygonCoordinates(data); + const polyConstructor = isCutPolygon(polyData) || polyData.length !== 2 ? L.polygon : L.rectangle; + this.polygon = polyConstructor(polyData, { + fill: true, + fillColor: '#3a77e7', + color: '#0742ad', + weight: 1, + fillOpacity: 0.4, + opacity: 1 + }); + return this.polygon; + } + public update(data: FormattedData, dsData: FormattedData[]): void { + const polyData = this.dataLayer.extractPolygonCoordinates(data); + if (isCutPolygon(polyData) || polyData.length !== 2) { + if (this.polygon instanceof L.Rectangle) { + this.polygon = L.polygon(polyData, { + fill: true, + fillColor: '#3a77e7', + color: '#0742ad', + weight: 1, + fillOpacity: 0.4, + opacity: 1 + }); + this.updateLayer(this.polygon); + } else { + this.polygon.setLatLngs(polyData); + } + } else if (polyData.length === 2) { + const bounds = new L.LatLngBounds(polyData); + // @ts-ignore + this.leafletPoly.setBounds(bounds); + } + } +} + +export class TbPolygonsDataLayer extends TbMapDataLayer { constructor(protected map: TbMap, protected settings: PolygonsDataLayerSettings) { @@ -131,16 +298,62 @@ export class TbPolygonsDataLayer extends TbMapDataLayer[], dsData: FormattedData[]) { - layerData.forEach((data, index) => { - console.log(`[${this.mapDataId}][${index}]: Polygons layer data updated!`); - console.log(data); + protected isValidLayerData(layerData: FormattedData): boolean { + return layerData && ((isNotEmptyStr(layerData[this.settings.polygonKey.label]) && !isJSON(layerData[this.settings.polygonKey.label]) + || Array.isArray(layerData[this.settings.polygonKey.label]))); + } + + protected createLayerItem(data: FormattedData, dsData: FormattedData[]): TbPolygonDataLayerItem { + return new TbPolygonDataLayerItem(data, dsData, this.settings, this); + } + + public extractPolygonCoordinates(data: FormattedData) { + let rawPolyData = data[this.settings.polygonKey.label]; + if (isString(rawPolyData)) { + rawPolyData = JSON.parse(rawPolyData); + } + return this.map.toPolygonCoordinates(rawPolyData); + } +} + +class TbCircleDataLayerItem extends TbDataLayerItem { + + private circle: L.Circle; + + constructor(data: FormattedData, + dsData: FormattedData[], + protected settings: CirclesDataLayerSettings, + protected dataLayer: TbCirclesDataLayer) { + super(data, dsData, settings, dataLayer); + } + + protected create(data: FormattedData, dsData: FormattedData[]): L.Layer { + const circleData = this.dataLayer.extractCircleCoordinates(data); + const center = new L.LatLng(circleData.latitude, circleData.longitude); + this.circle = L.circle(center, { + radius: circleData.radius, + fillColor: '#3a77e7', + color: '#0742ad', + weight: 1, + fillOpacity: 0.4, + opacity: 1 }); + return this.circle; } + public update(data: FormattedData, dsData: FormattedData[]): void { + const circleData = this.dataLayer.extractCircleCoordinates(data); + const center = new L.LatLng(circleData.latitude, circleData.longitude); + if (!this.circle.getLatLng().equals(center)) { + this.circle.setLatLng(center); + } + if (this.circle.getRadius() !== circleData.radius) { + this.circle.setRadius(circleData.radius); + } + } } -export class TbCirclesDataLayer extends TbMapDataLayer { +export class TbCirclesDataLayer extends TbMapDataLayer { constructor(protected map: TbMap, protected settings: CirclesDataLayerSettings) { @@ -160,11 +373,18 @@ export class TbCirclesDataLayer extends TbMapDataLayer return of(null); } - protected onData(layerData: FormattedData[], dsData: FormattedData[]) { - layerData.forEach((data, index) => { - console.log(`[${this.mapDataId}][${index}]: Circles layer data updated!`); - console.log(data); - }); + protected isValidLayerData(layerData: FormattedData): boolean { + return layerData && isNotEmptyStr(layerData[this.settings.circleKey.label]) && isJSON(layerData[this.settings.circleKey.label]); + } + + protected createLayerItem(data: FormattedData, dsData: FormattedData[]): TbDataLayerItem { + throw new TbCircleDataLayerItem(data, dsData, this.settings, this); } + public extractCircleCoordinates(data: FormattedData) { + const circleData: TbCircleData = JSON.parse(data[this.settings.circleKey.label]); + return this.map.convertCircleData(circleData); + } + + } diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/maps/map-widget.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/maps/map-widget.component.ts index 58d4d4fcc0..da394f6292 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/maps/map-widget.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/maps/map-widget.component.ts @@ -43,7 +43,7 @@ import { DomSanitizer } from '@angular/platform-browser'; styleUrls: ['./map-widget.component.scss'], encapsulation: ViewEncapsulation.None }) -export class MapWidgetComponent implements OnInit, OnDestroy, AfterViewInit { +export class MapWidgetComponent implements OnInit, OnDestroy { @ViewChild('mapElement', {static: false}) mapElement: ElementRef; @@ -78,10 +78,6 @@ export class MapWidgetComponent implements OnInit, OnDestroy, AfterViewInit { this.padding = this.settings.background.overlay.enabled ? undefined : this.settings.padding; } - ngAfterViewInit() { - this.map = TbMap.fromSettings(this.ctx, this.settings, this.mapElement.nativeElement); - } - ngOnDestroy() { if (this.map) { this.map.destroy(); @@ -92,6 +88,7 @@ export class MapWidgetComponent implements OnInit, OnDestroy, AfterViewInit { const borderRadius = this.ctx.$widgetElement.css('borderRadius'); this.overlayStyle = {...this.overlayStyle, ...{borderRadius}}; this.cd.detectChanges(); + this.map = TbMap.fromSettings(this.ctx, this.settings, this.mapElement.nativeElement); } } diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/maps/map.models.ts b/ui-ngx/src/app/modules/home/components/widget/lib/maps/map.models.ts index fe7dde4114..ce540ecec0 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/maps/map.models.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/maps/map.models.ts @@ -16,9 +16,10 @@ import { DataKey, Datasource, DatasourceType } from '@shared/models/widget.models'; import { DataKeyType } from '@shared/models/telemetry/telemetry.models'; -import { guid, mergeDeep } from '@core/utils'; +import { guid, isDefinedAndNotNull, isString, mergeDeep } from '@core/utils'; import { AbstractControl, ValidationErrors, ValidatorFn } from '@angular/forms'; import { materialColors } from '@shared/models/material.models'; +import L from 'leaflet'; export enum MapType { geoMap = 'geoMap', @@ -114,25 +115,25 @@ export interface MarkersDataLayerSettings extends MapDataLayerSettings { } const defaultMarkerLatitudeFunction = 'var value = prevValue || 15.833293;\n' + - 'if (time % 5000 < 500) {\n' + + 'if (time % 500 < 500) {\n' + ' value += Math.random() * 0.05 - 0.025;\n' + '}\n' + 'return value;'; const defaultMarkerLongitudeFunction = 'var value = prevValue || -90.454350;\n' + - 'if (time % 5000 < 500) {\n' + + 'if (time % 500 < 500) {\n' + ' value += Math.random() * 0.05 - 0.025;\n' + '}\n' + 'return value;'; const defaultMarkerXPosFunction = 'var value = prevValue || 0.2;\n' + - 'if (time % 5000 < 500) {\n' + + 'if (time % 500 < 500) {\n' + ' value += Math.random() * 0.05 - 0.025;\n' + '}\n' + 'return value;'; const defaultMarkerYPosFunction = 'var value = prevValue || 0.3;\n' + - 'if (time % 5000 < 500) {\n' + + 'if (time % 500 < 500) {\n' + ' value += Math.random() * 0.05 - 0.025;\n' + '}\n' + 'return value;'; @@ -235,6 +236,7 @@ export interface BaseMapSettings { useDefaultCenterPosition: boolean; defaultCenterPosition?: string; defaultZoomLevel: number; + minZoomLevel: number; mapPageSize: number; } @@ -253,6 +255,7 @@ export const defaultBaseMapSettings: BaseMapSettings = { useDefaultCenterPosition: false, defaultCenterPosition: '0,0', defaultZoomLevel: null, + minZoomLevel: 16, mapPageSize: DEFAULT_MAP_PAGE_SIZE }; @@ -539,7 +542,48 @@ export type MapSetting = GeoMapSettings & ImageMapSettings; export const defaultMapSettings: MapSetting = defaultGeoMapSettings; -export function parseCenterPosition(position: string | [number, number]): [number, number] { +export interface TbCircleData { + latitude: number; + longitude: number; + radius: number; +} + +export const isValidLatitude = (latitude: any): boolean => + isDefinedAndNotNull(latitude) && + !isString(latitude) && + !isNaN(latitude) && isFinite(latitude) && Math.abs(latitude) <= 90; + +export const isValidLongitude = (longitude: any): boolean => + isDefinedAndNotNull(longitude) && + !isString(longitude) && + !isNaN(longitude) && isFinite(longitude) && Math.abs(longitude) <= 180; + +export const isValidLatLng = (latitude: any, longitude: any): boolean => + isValidLatitude(latitude) && isValidLongitude(longitude); + +export const isCutPolygon = (data): boolean => { + return data.length > 1 && Array.isArray(data[0]) && (Array.isArray(data[0][0]) || data[0][0] instanceof L.LatLng); +} + +export const latLngPointToBounds = (point: L.LatLng, southWest: L.LatLng, northEast: L.LatLng, offset = 0): L.LatLng => { + const maxLngMap = northEast.lng - offset; + const minLngMap = southWest.lng + offset; + const maxLatMap = northEast.lat - offset; + const minLatMap = southWest.lat + offset; + if (point.lng > maxLngMap) { + point.lng = maxLngMap; + } else if (point.lng < minLngMap) { + point.lng = minLngMap; + } + if (point.lat > maxLatMap) { + point.lat = maxLatMap; + } else if (point.lat < minLatMap) { + point.lat = minLatMap; + } + return point; +} + +export const parseCenterPosition = (position: string | [number, number]): [number, number] => { if (typeof (position) === 'string') { const parts = position.split(','); if (parts.length === 2) { 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 c664418ce8..bcab794377 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 @@ -22,15 +22,19 @@ import { defaultImageMapSettings, GeoMapSettings, ImageMapSettings, + latLngPointToBounds, MapSetting, MapType, - MapZoomAction, mergeMapDatasources, - parseCenterPosition, TbMapDatasource + MapZoomAction, + mergeMapDatasources, + parseCenterPosition, + TbCircleData, + TbMapDatasource } from '@home/components/widget/lib/maps/map.models'; import { WidgetContext } from '@home/models/widget-component.models'; import { formattedDataFormDatasourceData, isDefinedAndNotNull, mergeDeepIgnoreArray } from '@core/utils'; import { DeepPartial } from '@shared/models/common'; -import L from 'leaflet'; +import L, { LatLngBounds, LatLngTuple, PointExpression, Projection } from 'leaflet'; import { forkJoin, Observable, of } from 'rxjs'; import { TbMapLayer } from '@home/components/widget/lib/maps/map-layer'; import { map, switchMap, tap } from 'rxjs/operators'; @@ -43,7 +47,7 @@ import { TbPolygonsDataLayer } from '@home/components/widget/lib/maps/map-data-layer'; import { IWidgetSubscription, WidgetSubscriptionOptions } from '@core/api/widget-api.models'; -import { FormattedData, widgetType } from '@shared/models/widget.models'; +import { widgetType } from '@shared/models/widget.models'; import { EntityDataPageLink } from '@shared/models/query/query.models'; export abstract class TbMap { @@ -64,8 +68,12 @@ export abstract class TbMap { protected defaultCenterPosition: [number, number]; protected bounds: L.LatLngBounds; + protected ignoreUpdateBounds = false; - protected dataLayers: TbMapDataLayer[]; + protected southWest = new L.LatLng(-Projection.SphericalMercator['MAX_LATITUDE'], -180); + protected northEast = new L.LatLng(Projection.SphericalMercator['MAX_LATITUDE'], 180); + + protected dataLayers: TbMapDataLayer[]; protected mapElement: HTMLElement; @@ -140,6 +148,10 @@ export abstract class TbMap { this.dataLayers.push(...this.settings.circles.map(settings => new TbCirclesDataLayer(this, settings))); } if (this.dataLayers.length) { + + // TODO: Groups + this.dataLayers.forEach(dl => this.map.addLayer(dl.getFeatureGroup())); + const setup = this.dataLayers.map(dl => dl.setup()); forkJoin(setup).subscribe( () => { @@ -192,6 +204,7 @@ export abstract class TbMap { const dsData = formattedDataFormDatasourceData(subscription.data, undefined, undefined, el => el.datasource.entityId + el.datasource.mapDataIds[0]); this.dataLayers.forEach(dl => dl.updateData(dsData)); + this.updateBounds(); } private resize() { @@ -199,6 +212,49 @@ export abstract class TbMap { this.map?.invalidateSize(); } + private updateBounds() { + const bounds = new L.LatLngBounds(null, null); + this.dataLayers.forEach(dl => bounds.extend(dl.getBounds())); + const mapBounds = this.map.getBounds(); + if (bounds.isValid() && (!this.bounds || !this.bounds.isValid() || !this.bounds.equals(bounds) + && this.settings.fitMapBounds ? !mapBounds.contains(bounds) : false)) { + this.bounds = bounds; + if (!this.ignoreUpdateBounds) { + this.fitBounds(bounds); + } + } + } + + private fitBounds(bounds: LatLngBounds, padding?: PointExpression) { + if (bounds.isValid()) { + this.bounds = !!this.bounds ? this.bounds.extend(bounds) : bounds; + if (!this.settings.fitMapBounds && this.settings.defaultZoomLevel) { + this.map.setZoom(this.settings.defaultZoomLevel, { animate: false }); + if (this.settings.useDefaultCenterPosition) { + this.map.panTo(this.defaultCenterPosition, { animate: false }); + } + else { + this.map.panTo(this.bounds.getCenter()); + } + } else { + this.map.once('zoomend', () => { + let minZoom = this.settings.minZoomLevel; + if (this.settings.defaultZoomLevel) { + minZoom = Math.max(minZoom, this.settings.defaultZoomLevel); + } + if (this.map.getZoom() > minZoom) { + this.map.setZoom(minZoom, { animate: false }); + } + }); + if (this.settings.useDefaultCenterPosition) { + this.bounds = this.bounds.extend(this.defaultCenterPosition); + } + this.map.fitBounds(this.bounds, { padding: padding || [50, 50], animate: false }); + this.map.invalidateSize(); + } + } + } + protected abstract defaultSettings(): S; protected abstract createMap(): Observable; @@ -209,6 +265,10 @@ export abstract class TbMap { return of(null); } + public type(): MapType { + return this.settings.mapType; + } + public destroy() { if (this.mapResize$) { this.mapResize$.disconnect(); @@ -218,6 +278,12 @@ export abstract class TbMap { } } + public abstract positionToLatLng(position: {x: number; y: number}): L.LatLng; + + public abstract toPolygonCoordinates(expression: (LatLngTuple | LatLngTuple[] | LatLngTuple[][])[]): any; + + public abstract convertCircleData(circle: TbCircleData): TbCircleData; + } class TbGeoMap extends TbMap { @@ -280,6 +346,28 @@ class TbGeoMap extends TbMap { ); } + public positionToLatLng(position: {x: number; y: number}): L.LatLng { + return L.latLng(position.x, position.y) as L.LatLng; + } + + public toPolygonCoordinates(expression: (LatLngTuple | LatLngTuple[] | LatLngTuple[][])[]): any { + return (expression).map((el) => { + if (!Array.isArray(el[0]) && el.length === 2) { + return el; + } else if (Array.isArray(el) && el.length) { + return this.toPolygonCoordinates(el as LatLngTuple[] | LatLngTuple[][]); + } else { + return null; + } + }).filter(el => !!el); + } + + public convertCircleData(circle: TbCircleData): TbCircleData { + const centerPoint = latLngPointToBounds(new L.LatLng(circle.latitude, circle.longitude), this.southWest, this.northEast); + circle.latitude = centerPoint.lat; + circle.longitude = centerPoint.lng; + return circle; + } } @@ -287,6 +375,9 @@ class TbImageMap extends TbMap { private maxZoom = 4; + private width = 0; + private height = 0; + constructor(protected ctx: WidgetContext, protected inputSettings: DeepPartial, protected mapElement: HTMLElement) { @@ -312,4 +403,38 @@ class TbImageMap extends TbMap { } protected onResize(): void {} + + public positionToLatLng(position: {x: number; y: number}): L.LatLng { + return this.pointToLatLng( + position.x * this.width, + position.y * this.height); + } + + public pointToLatLng(x: number, y: number): L.LatLng { + return L.CRS.Simple.pointToLatLng({ x, y } as L.PointExpression, this.maxZoom - 1); + } + + public toPolygonCoordinates(expression: (LatLngTuple | LatLngTuple[] | LatLngTuple[][])[]): any { + return (expression).map((el) => { + if (!Array.isArray(el[0]) && !Array.isArray(el[1]) && el.length === 2) { + return this.pointToLatLng( + el[0] * this.width, + el[1] * this.height + ); + } else if (Array.isArray(el) && el.length) { + return this.toPolygonCoordinates(el as LatLngTuple[] | LatLngTuple[][]); + } else { + return null; + } + }).filter(el => !!el); + } + + public convertCircleData(circle: TbCircleData): TbCircleData { + const centerPoint = this.pointToLatLng(circle.latitude * this.width, circle.longitude * this.height); + circle.latitude = centerPoint.lat; + circle.longitude = centerPoint.lng; + circle.radius = circle.radius * this.width; + return circle; + } + } From d8ea4f680b35502bb22aea96c4fdc1d3feef2490 Mon Sep 17 00:00:00 2001 From: Igor Kulikov Date: Fri, 10 Jan 2025 18:03:40 +0200 Subject: [PATCH 018/345] UI: Merkers data layer configuration. --- ui-ngx/src/app/core/utils.ts | 23 +- ui-ngx/src/app/modules/common/modules-map.ts | 16 +- .../add-widget-dialog.component.ts | 2 +- .../dashboard-page/edit-widget.component.ts | 2 +- .../aggregated-data-key-row.component.ts | 4 +- .../aggregated-data-keys-panel.component.ts | 2 +- .../chart/comparison-key-row.component.ts | 2 +- .../basic/common/data-key-row.component.ts | 6 +- .../basic/common/data-keys-panel.component.ts | 2 +- .../widget/config/datasource.component.html | 8 + .../config/datasource.component.models.ts | 4 +- .../widget/config/datasource.component.ts | 4 +- .../widget/config/datasources.component.ts | 2 +- .../config/widget-config-components.module.ts | 17 +- .../widget/lib/chart/time-series-chart.ts | 2 +- .../widget/lib/maps/leaflet/leaflet-tb.ts | 86 ++++- .../widget/lib/maps/map-data-layer.ts | 332 +++++++++++++++++- .../components/widget/lib/maps/map.models.ts | 166 ++++++++- .../home/components/widget/lib/maps/map.scss | 32 +- .../home/components/widget/lib/maps/map.ts | 96 +++-- ...me-series-chart-line-settings.component.ts | 2 +- .../common/advanced-range.component.ts | 2 +- .../chart/chart-bar-settings.component.ts | 2 +- ...me-series-chart-threshold-row.component.ts | 2 +- ...series-chart-thresholds-panel.component.ts | 2 +- .../common/color-range-list.component.ts | 2 +- .../common/color-settings-panel.component.ts | 2 +- .../common/color-settings.component.ts | 2 +- .../filter/filter-select.component.html | 2 +- .../filter/filter-select.component.models.ts | 0 .../common}/filter/filter-select.component.ts | 11 +- .../lib/settings/common/gradient.component.ts | 2 +- .../data-key-config-dialog.component.html | 0 .../data-key-config-dialog.component.scss | 0 .../key}/data-key-config-dialog.component.ts | 2 +- .../key}/data-key-config.component.html | 0 .../common/key}/data-key-config.component.ts | 2 +- .../{ => key}/data-key-input.component.html | 13 +- .../{ => key}/data-key-input.component.scss | 27 +- .../{ => key}/data-key-input.component.ts | 18 +- .../common/key}/data-keys.component.html | 8 +- .../common/key}/data-keys.component.models.ts | 0 .../common/key}/data-keys.component.scss | 31 ++ .../common/key}/data-keys.component.ts | 62 +++- .../map/map-data-layer-dialog.component.html | 198 +++++++++++ .../map/map-data-layer-dialog.component.scss | 41 +++ .../map/map-data-layer-dialog.component.ts | 239 +++++++++++++ .../map/map-data-layer-row.component.html | 40 ++- .../map/map-data-layer-row.component.ts | 126 ++----- .../common/map/map-data-layers.component.html | 1 + .../common/map/map-data-layers.component.ts | 8 +- .../common/map/map-settings.component.html | 3 + .../map/map-settings.component.models.ts | 29 ++ .../common/map/map-settings.component.ts | 60 +++- .../common/value-source-data-key.component.ts | 2 +- .../common/widget-settings-common.module.ts | 28 +- .../widget}/widget-settings.component.html | 0 .../widget}/widget-settings.component.scss | 0 .../widget}/widget-settings.component.ts | 0 .../device-key-autocomplete.component.html | 3 +- .../settings/gauge/tick-value.component.ts | 2 +- .../widget/widget-config.component.ts | 2 +- .../home/models/widget-component.models.ts | 2 +- .../string-items-list.component.html | 2 +- .../string-items-list.component.scss | 22 ++ .../components/string-items-list.component.ts | 7 +- ui-ngx/src/app/shared/models/widget.models.ts | 2 +- .../assets/locale/locale.constant-en_US.json | 3 + ui-ngx/src/typings/leaflet-extend-tb.d.ts | 23 +- 69 files changed, 1556 insertions(+), 289 deletions(-) rename ui-ngx/src/app/modules/home/components/{ => widget/lib/settings/common}/filter/filter-select.component.html (97%) rename ui-ngx/src/app/modules/home/components/{ => widget/lib/settings/common}/filter/filter-select.component.models.ts (100%) rename ui-ngx/src/app/modules/home/components/{ => widget/lib/settings/common}/filter/filter-select.component.ts (95%) rename ui-ngx/src/app/modules/home/components/widget/{config => lib/settings/common/key}/data-key-config-dialog.component.html (100%) rename ui-ngx/src/app/modules/home/components/widget/{config => lib/settings/common/key}/data-key-config-dialog.component.scss (100%) rename ui-ngx/src/app/modules/home/components/widget/{config => lib/settings/common/key}/data-key-config-dialog.component.ts (97%) rename ui-ngx/src/app/modules/home/components/widget/{config => lib/settings/common/key}/data-key-config.component.html (100%) rename ui-ngx/src/app/modules/home/components/widget/{config => lib/settings/common/key}/data-key-config.component.ts (99%) rename ui-ngx/src/app/modules/home/components/widget/lib/settings/common/{ => key}/data-key-input.component.html (92%) rename ui-ngx/src/app/modules/home/components/widget/lib/settings/common/{ => key}/data-key-input.component.scss (70%) rename ui-ngx/src/app/modules/home/components/widget/lib/settings/common/{ => key}/data-key-input.component.ts (96%) rename ui-ngx/src/app/modules/home/components/widget/{config => lib/settings/common/key}/data-keys.component.html (97%) rename ui-ngx/src/app/modules/home/components/widget/{config => lib/settings/common/key}/data-keys.component.models.ts (100%) rename ui-ngx/src/app/modules/home/components/widget/{config => lib/settings/common/key}/data-keys.component.scss (81%) rename ui-ngx/src/app/modules/home/components/widget/{config => lib/settings/common/key}/data-keys.component.ts (95%) create mode 100644 ui-ngx/src/app/modules/home/components/widget/lib/settings/common/map/map-data-layer-dialog.component.html create mode 100644 ui-ngx/src/app/modules/home/components/widget/lib/settings/common/map/map-data-layer-dialog.component.scss create mode 100644 ui-ngx/src/app/modules/home/components/widget/lib/settings/common/map/map-data-layer-dialog.component.ts create mode 100644 ui-ngx/src/app/modules/home/components/widget/lib/settings/common/map/map-settings.component.models.ts rename ui-ngx/src/app/modules/home/components/widget/{config => lib/settings/common/widget}/widget-settings.component.html (100%) rename ui-ngx/src/app/modules/home/components/widget/{config => lib/settings/common/widget}/widget-settings.component.scss (100%) rename ui-ngx/src/app/modules/home/components/widget/{config => lib/settings/common/widget}/widget-settings.component.ts (100%) create mode 100644 ui-ngx/src/app/shared/components/string-items-list.component.scss diff --git a/ui-ngx/src/app/core/utils.ts b/ui-ngx/src/app/core/utils.ts index 7e2e5d1e52..95615cf516 100644 --- a/ui-ngx/src/app/core/utils.ts +++ b/ui-ngx/src/app/core/utils.ts @@ -17,7 +17,7 @@ import _ from 'lodash'; import { from, Observable, of, ReplaySubject, Subject } from 'rxjs'; import { catchError, finalize, share } from 'rxjs/operators'; -import { Datasource, DatasourceData, FormattedData, ReplaceInfo } from '@app/shared/models/widget.models'; +import { DataKey, Datasource, DatasourceData, FormattedData, ReplaceInfo } from '@app/shared/models/widget.models'; import { EntityId } from '@shared/models/id/entity-id'; import { NULL_UUID } from '@shared/models/id/has-uuid'; import { baseDetailsPageByEntityType, EntityType } from '@shared/models/entity-type.models'; @@ -846,7 +846,7 @@ function prepareMessageFromData(data): string { } } -export function genNextLabel(name: string, datasources: Datasource[]): string { +export const genNextLabel = (name: string, datasources: Datasource[]): string => { let label = name; let i = 1; let matches = false; @@ -880,6 +880,25 @@ export function genNextLabel(name: string, datasources: Datasource[]): string { return label; } +export const genNextLabelForDataKeys = (name: string, dataKeys: DataKey[]): string => { + let label = name; + let i = 1; + let matches = false; + if (dataKeys) { + do { + matches = false; + dataKeys.forEach((dataKey) => { + if (dataKey?.label === label) { + i++; + label = name + ' ' + i; + matches = true; + } + }); + } while (matches) + } + return label; +} + export const getOS = (): string => { const userAgent = window.navigator.userAgent.toLowerCase(); const macosPlatforms = /(macintosh|macintel|macppc|mac68k|macos|mac_powerpc)/i; diff --git a/ui-ngx/src/app/modules/common/modules-map.ts b/ui-ngx/src/app/modules/common/modules-map.ts index 9e65bdb4db..040edd19c3 100644 --- a/ui-ngx/src/app/modules/common/modules-map.ts +++ b/ui-ngx/src/app/modules/common/modules-map.ts @@ -232,9 +232,9 @@ import * as EntityAliasDialogComponent from '@home/components/alias/entity-alias import * as EntityFilterComponent from '@home/components/entity/entity-filter.component'; import * as RelationFiltersComponent from '@home/components/relation/relation-filters.component'; import * as EntityAliasSelectComponent from '@home/components/widget/lib/settings/common/alias/entity-alias-select.component'; -import * as DataKeysComponent from '@home/components/widget/config/data-keys.component'; -import * as DataKeyConfigDialogComponent from '@home/components/widget/config/data-key-config-dialog.component'; -import * as DataKeyConfigComponent from '@home/components/widget/config/data-key-config.component'; +import * as DataKeysComponent from '@home/components/widget/lib/settings/common/key/data-keys.component'; +import * as DataKeyConfigDialogComponent from '@home/components/widget/lib/settings/common/key/data-key-config-dialog.component'; +import * as DataKeyConfigComponent from '@home/components/widget/lib/settings/common/key/data-key-config.component'; import * as LegendConfigComponent from '@home/components/widget/lib/settings/common/legend-config.component'; import * as ManageWidgetActionsComponent from '@home/components/widget/action/manage-widget-actions.component'; import * as WidgetActionDialogComponent from '@home/components/widget/action/widget-action-dialog.component'; @@ -268,7 +268,7 @@ import * as ComplexFilterPredicateDialogComponent from '@home/components/filter/ import * as KeyFilterDialogComponent from '@home/components/filter/key-filter-dialog.component'; import * as FiltersDialogComponent from '@home/components/filter/filters-dialog.component'; import * as FilterDialogComponent from '@home/components/filter/filter-dialog.component'; -import * as FilterSelectComponent from '@home/components/filter/filter-select.component'; +import * as FilterSelectComponent from '@home/components/widget/lib/settings/common/filter/filter-select.component'; import * as FiltersEditComponent from '@home/components/filter/filters-edit.component'; import * as FiltersEditPanelComponent from '@home/components/filter/filters-edit-panel.component'; import * as UserFilterDialogComponent from '@home/components/filter/user-filter-dialog.component'; @@ -578,9 +578,9 @@ class ModulesMap implements IModulesMap { '@home/components/entity/entity-filter.component': EntityFilterComponent, '@home/components/relation/relation-filters.component': RelationFiltersComponent, '@home/components/widget/lib/settings/common/alias/entity-alias-select.component': EntityAliasSelectComponent, - '@home/components/widget/config/data-keys.component': DataKeysComponent, - '@home/components/widget/config/data-key-config-dialog.component': DataKeyConfigDialogComponent, - '@home/components/widget/config/data-key-config.component': DataKeyConfigComponent, + '@home/components/widget/lib/settings/common/key/data-keys.component': DataKeysComponent, + '@home/components/widget/lib/settings/common/key/data-key-config-dialog.component': DataKeyConfigDialogComponent, + '@home/components/widget/lib/settings/common/key/data-key-config.component': DataKeyConfigComponent, '@home/components/widget/lib/settings/common/legend-config.component': LegendConfigComponent, '@home/components/widget/action/manage-widget-actions.component': ManageWidgetActionsComponent, '@home/components/widget/action/widget-action-dialog.component': WidgetActionDialogComponent, @@ -610,7 +610,7 @@ class ModulesMap implements IModulesMap { '@home/components/filter/key-filter-dialog.component': KeyFilterDialogComponent, '@home/components/filter/filters-dialog.component': FiltersDialogComponent, '@home/components/filter/filter-dialog.component': FilterDialogComponent, - '@home/components/filter/filter-select.component': FilterSelectComponent, + '@home/components/widget/lib/settings/common/filter/filter-select.component': FilterSelectComponent, '@home/components/filter/filters-edit.component': FiltersEditComponent, '@home/components/filter/filters-edit-panel.component': FiltersEditPanelComponent, '@home/components/filter/user-filter-dialog.component': UserFilterDialogComponent, diff --git a/ui-ngx/src/app/modules/home/components/dashboard-page/add-widget-dialog.component.ts b/ui-ngx/src/app/modules/home/components/dashboard-page/add-widget-dialog.component.ts index fe96529a1d..a89df99db1 100644 --- a/ui-ngx/src/app/modules/home/components/dashboard-page/add-widget-dialog.component.ts +++ b/ui-ngx/src/app/modules/home/components/dashboard-page/add-widget-dialog.component.ts @@ -29,7 +29,7 @@ import { WidgetConfigComponentData, WidgetInfo } from '@home/models/widget-compo import { isDefined, isDefinedAndNotNull } from '@core/utils'; import { TranslateService } from '@ngx-translate/core'; import { WidgetConfigComponent } from '@home/components/widget/widget-config.component'; -import { DataKeySettingsFunction } from '@home/components/widget/config/data-keys.component.models'; +import { DataKeySettingsFunction } from '@home/components/widget/lib/settings/common/key/data-keys.component.models'; export interface AddWidgetDialogData { dashboard: Dashboard; diff --git a/ui-ngx/src/app/modules/home/components/dashboard-page/edit-widget.component.ts b/ui-ngx/src/app/modules/home/components/dashboard-page/edit-widget.component.ts index 191bf17700..d89de90fef 100644 --- a/ui-ngx/src/app/modules/home/components/dashboard-page/edit-widget.component.ts +++ b/ui-ngx/src/app/modules/home/components/dashboard-page/edit-widget.component.ts @@ -27,7 +27,7 @@ import { WidgetConfigComponentData } from '../../models/widget-component.models' import { isDefined, isDefinedAndNotNull } from '@core/utils'; import { UntypedFormBuilder, UntypedFormGroup } from '@angular/forms'; import { WidgetConfigComponent } from '@home/components/widget/widget-config.component'; -import { DataKeySettingsFunction } from '@home/components/widget/config/data-keys.component.models'; +import { DataKeySettingsFunction } from '@home/components/widget/lib/settings/common/key/data-keys.component.models'; import { coerceBoolean } from '@shared/decorators/coercion'; @Component({ diff --git a/ui-ngx/src/app/modules/home/components/widget/config/basic/cards/aggregated-data-key-row.component.ts b/ui-ngx/src/app/modules/home/components/widget/config/basic/cards/aggregated-data-key-row.component.ts index 5f100dbe3e..76bdddadf3 100644 --- a/ui-ngx/src/app/modules/home/components/widget/config/basic/cards/aggregated-data-key-row.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/config/basic/cards/aggregated-data-key-row.component.ts @@ -45,7 +45,7 @@ import { TruncatePipe } from '@shared/pipe/truncate.pipe'; import { DataKeyConfigDialogComponent, DataKeyConfigDialogData -} from '@home/components/widget/config/data-key-config-dialog.component'; +} from '@home/components/widget/lib/settings/common/key/data-key-config-dialog.component'; import { deepClone, formatValue } from '@core/utils'; import { AggregatedValueCardKeyPosition, @@ -59,7 +59,7 @@ import { takeUntilDestroyed } from '@angular/core/rxjs-interop'; @Component({ selector: 'tb-aggregated-data-key-row', templateUrl: './aggregated-data-key-row.component.html', - styleUrls: ['./aggregated-data-key-row.component.scss', '../../data-keys.component.scss'], + styleUrls: ['./aggregated-data-key-row.component.scss', '../../../lib/settings/common/key/data-keys.component.scss'], providers: [ { provide: NG_VALUE_ACCESSOR, diff --git a/ui-ngx/src/app/modules/home/components/widget/config/basic/cards/aggregated-data-keys-panel.component.ts b/ui-ngx/src/app/modules/home/components/widget/config/basic/cards/aggregated-data-keys-panel.component.ts index 39481874e3..c6706cc32b 100644 --- a/ui-ngx/src/app/modules/home/components/widget/config/basic/cards/aggregated-data-keys-panel.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/config/basic/cards/aggregated-data-keys-panel.component.ts @@ -38,7 +38,7 @@ import { WidgetConfigComponent } from '@home/components/widget/widget-config.com import { DataKey, DatasourceType, widgetType } from '@shared/models/widget.models'; import { DataKeyType } from '@shared/models/telemetry/telemetry.models'; import { UtilsService } from '@core/services/utils.service'; -import { DataKeysCallbacks } from '@home/components/widget/config/data-keys.component.models'; +import { DataKeysCallbacks } from '@home/components/widget/lib/settings/common/key/data-keys.component.models'; import { aggregatedValueCardDefaultKeySettings } from '@home/components/widget/lib/cards/aggregated-value-card.models'; import { takeUntilDestroyed } from '@angular/core/rxjs-interop'; diff --git a/ui-ngx/src/app/modules/home/components/widget/config/basic/chart/comparison-key-row.component.ts b/ui-ngx/src/app/modules/home/components/widget/config/basic/chart/comparison-key-row.component.ts index 48ffda9c7c..02e397cf85 100644 --- a/ui-ngx/src/app/modules/home/components/widget/config/basic/chart/comparison-key-row.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/config/basic/chart/comparison-key-row.component.ts @@ -34,7 +34,7 @@ import { takeUntilDestroyed } from '@angular/core/rxjs-interop'; @Component({ selector: 'tb-comparison-key-row', templateUrl: './comparison-key-row.component.html', - styleUrls: ['./comparison-key-row.component.scss', '../../data-keys.component.scss'], + styleUrls: ['./comparison-key-row.component.scss', '../../../lib/settings/common/key/data-keys.component.scss'], providers: [ { provide: NG_VALUE_ACCESSOR, diff --git a/ui-ngx/src/app/modules/home/components/widget/config/basic/common/data-key-row.component.ts b/ui-ngx/src/app/modules/home/components/widget/config/basic/common/data-key-row.component.ts index 95332c3791..f749d1201c 100644 --- a/ui-ngx/src/app/modules/home/components/widget/config/basic/common/data-key-row.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/config/basic/common/data-key-row.component.ts @@ -41,12 +41,12 @@ import { MatDialog } from '@angular/material/dialog'; import { WidgetConfigComponent } from '@home/components/widget/widget-config.component'; import { DataKey, DataKeyConfigMode, DatasourceType, Widget, widgetType } from '@shared/models/widget.models'; import { DataKeyType } from '@shared/models/telemetry/telemetry.models'; -import { DataKeySettingsFunction } from '@home/components/widget/config/data-keys.component.models'; +import { DataKeySettingsFunction } from '@home/components/widget/lib/settings/common/key/data-keys.component.models'; import { merge } from 'rxjs'; import { DataKeyConfigDialogComponent, DataKeyConfigDialogData -} from '@home/components/widget/config/data-key-config-dialog.component'; +} from '@home/components/widget/lib/settings/common/key/data-key-config-dialog.component'; import { deepClone } from '@core/utils'; import { Dashboard } from '@shared/models/dashboard.models'; import { IAliasController } from '@core/api/widget-api.models'; @@ -78,7 +78,7 @@ export const dataKeyRowValidator = (control: AbstractControl): ValidationErrors @Component({ selector: 'tb-data-key-row', templateUrl: './data-key-row.component.html', - styleUrls: ['./data-key-row.component.scss', '../../data-keys.component.scss'], + styleUrls: ['./data-key-row.component.scss', '../../../lib/settings/common/key/data-keys.component.scss'], providers: [ { provide: NG_VALUE_ACCESSOR, diff --git a/ui-ngx/src/app/modules/home/components/widget/config/basic/common/data-keys-panel.component.ts b/ui-ngx/src/app/modules/home/components/widget/config/basic/common/data-keys-panel.component.ts index a9bea410ab..e5ede137bc 100644 --- a/ui-ngx/src/app/modules/home/components/widget/config/basic/common/data-keys-panel.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/config/basic/common/data-keys-panel.component.ts @@ -43,7 +43,7 @@ import { dataKeyRowValidator, dataKeyValid } from '@home/components/widget/confi import { CdkDragDrop } from '@angular/cdk/drag-drop'; import { DataKeyType } from '@shared/models/telemetry/telemetry.models'; import { UtilsService } from '@core/services/utils.service'; -import { DataKeysCallbacks, DataKeySettingsFunction } from '@home/components/widget/config/data-keys.component.models'; +import { DataKeysCallbacks, DataKeySettingsFunction } from '@home/components/widget/lib/settings/common/key/data-keys.component.models'; import { coerceBoolean } from '@shared/decorators/coercion'; import { TimeSeriesChartYAxisId } from '@home/components/widget/lib/chart/time-series-chart.models'; import { FormProperty } from '@shared/models/dynamic-form.models'; diff --git a/ui-ngx/src/app/modules/home/components/widget/config/datasource.component.html b/ui-ngx/src/app/modules/home/components/widget/config/datasource.component.html index 8af7753525..b19398faab 100644 --- a/ui-ngx/src/app/modules/home/components/widget/config/datasource.component.html +++ b/ui-ngx/src/app/modules/home/components/widget/config/datasource.component.html @@ -66,6 +66,10 @@ { @@ -26,6 +26,8 @@ class SidebarControl extends L.Control { private map: L.Map; + private buttonContainer: JQuery; + constructor(options: TB.SidebarControlOptions) { super(options); this.sidebar = $('
'); @@ -38,8 +40,9 @@ class SidebarControl extends L.Control { } } - addPane(pane: JQuery): this { + addPane(pane: JQuery, button: JQuery): this { pane.hide().appendTo(this.sidebar); + button.appendTo(this.buttonContainer); return this; } @@ -50,6 +53,7 @@ class SidebarControl extends L.Control { this.current.hide().trigger('hide'); this.currentButton.removeClass('active'); + if (this.current === pane) { if (['topleft', 'bottomleft'].includes(position)) { this.map.panBy([-paneWidth, 0], { animate: false }); @@ -58,20 +62,26 @@ class SidebarControl extends L.Control { this.current = this.currentButton = $(); } else { this.sidebar.show(); - this.current = pane; - this.currentButton = button || $(); - if (['topleft', 'bottomleft'].includes(position)) { + if (['topleft', 'bottomleft'].includes(position) && !this.current.length) { this.map.panBy([paneWidth, 0], { animate: false }); } + this.current = pane; + this.currentButton = button || $(); } - this.map.invalidateSize({ pan: false, animate: false }); this.current.show().trigger('show'); this.currentButton.addClass('active'); + this.map.invalidateSize({ pan: false, animate: false }); + } + + onAdd(map: L.Map): HTMLElement { + this.buttonContainer = $("
") + .attr('class', 'leaflet-bar'); + return this.buttonContainer[0]; } addTo(map: L.Map): this { this.map = map; - return this; + return super.addTo(map); } } @@ -84,9 +94,7 @@ class SidebarPaneControl extends L.Contr super(options); } - onAdd(map: L.Map): HTMLElement { - const $container = $("
") - .attr('class', 'leaflet-bar'); + addTo(map: L.Map): this { this.button = $("") .attr('class', 'tb-control-button') @@ -98,7 +106,6 @@ class SidebarPaneControl extends L.Contr if (this.options.buttonTitle) { this.button.attr('title', this.options.buttonTitle); } - this.button.appendTo($container); this.$ui = $('
') .attr('class', this.options.uiClass); @@ -117,13 +124,13 @@ class SidebarPaneControl extends L.Contr this.toggle(e); }))); - this.options.sidebar.addPane(this.$ui); + this.options.sidebar.addPane(this.$ui, this.button); this.onAddPane(map, this.button, this.$ui, (e) => { this.toggle(e); }); - return $container[0]; + return this; } public onAddPane(map: L.Map, button: JQuery, $ui: JQuery, toggle: (e: JQuery.MouseEventBase) => void) {} @@ -216,6 +223,53 @@ class LayersControl extends SidebarPaneControl { } } +class GroupsControl extends SidebarPaneControl { + constructor(options: TB.GroupsControlOptions) { + super(options); + } + + public onAddPane(map: L.Map, button: JQuery, $ui: JQuery, toggle: (e: JQuery.MouseEventBase) => void) { + const paneId = guid(); + const groups = this.options.groups; + const baseSection = $("
") + .attr('class', 'tb-layers-container') + .appendTo($ui); + + groups.forEach((groupData, i) => { + const id = `map-group-layer-${paneId}-${i}`; + const checkBoxContainer = $('
') + .appendTo(baseSection); + const input = $('') + .prop('id', id) + .prop('checked', groupData.enabled) + .appendTo(checkBoxContainer); + + $('
+ + {{ requiredText | translate }} + diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/data-key-input.component.scss b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/key/data-key-input.component.scss similarity index 70% rename from ui-ngx/src/app/modules/home/components/widget/lib/settings/common/data-key-input.component.scss rename to ui-ngx/src/app/modules/home/components/widget/lib/settings/common/key/data-key-input.component.scss index f8564a46ac..cb01c98c37 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/data-key-input.component.scss +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/key/data-key-input.component.scss @@ -14,7 +14,7 @@ * limitations under the License. */ .tb-data-key-input { - .mat-mdc-form-field.tb-inline-field.tb-key-field { + .mat-mdc-form-field.tb-key-field { width: 100%; &.mat-form-field-appearance-fill { .mdc-text-field--filled.mdc-text-field--disabled { @@ -23,14 +23,25 @@ } } } - .mat-mdc-text-field-wrapper:not(.mdc-text-field--outlined) { - padding-left: 8px; - padding-right: 0; - + .mat-mdc-text-field-wrapper { + &:not(.mdc-text-field--outlined) { + padding-left: 8px; + padding-right: 0; + .mat-mdc-form-field-infix { + padding-top: 0; + padding-bottom: 6px; + } + } + &.mdc-text-field--outlined { + .mat-mdc-form-field-infix { + padding-top: 12px; + padding-bottom: 12px; + } + .mdc-evolution-chip-set__chips { + margin-left: 0; + } + } .mat-mdc-form-field-infix { - padding-top: 0; - padding-bottom: 6px; - .mdc-evolution-chip-set .mdc-evolution-chip { margin: 0; } diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/data-key-input.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/key/data-key-input.component.ts similarity index 96% rename from ui-ngx/src/app/modules/home/components/widget/lib/settings/common/data-key-input.component.ts rename to ui-ngx/src/app/modules/home/components/widget/lib/settings/common/key/data-key-input.component.ts index e08ec36d8d..6ea515e096 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/data-key-input.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/key/data-key-input.component.ts @@ -50,13 +50,14 @@ import { UtilsService } from '@core/services/utils.service'; import { alarmFields } from '@shared/models/alarm.models'; import { filter, map, mergeMap, publishReplay, refCount, share, tap } from 'rxjs/operators'; import { AggregationType } from '@shared/models/time/time.models'; -import { DataKeysCallbacks } from '@home/components/widget/config/data-keys.component.models'; +import { DataKeysCallbacks } from './data-keys.component.models'; import { IAliasController } from '@core/api/widget-api.models'; +import { MatFormFieldAppearance, SubscriptSizing } from '@angular/material/form-field'; @Component({ selector: 'tb-data-key-input', templateUrl: './data-key-input.component.html', - styleUrls: ['./data-key-input.component.scss', '../../../config/data-keys.component.scss'], + styleUrls: ['./data-key-input.component.scss', './data-keys.component.scss'], providers: [ { provide: NG_VALUE_ACCESSOR, @@ -83,6 +84,19 @@ export class DataKeyInputComponent implements ControlValueAccessor, OnInit, OnCh @Input() disabled: boolean; + @Input() + label: string; + + @Input() + appearance: MatFormFieldAppearance = 'fill'; + + @Input() + subscriptSizing: SubscriptSizing = 'fixed'; + + @Input() + @coerceBoolean() + inlineField = true; + @Input() @coerceBoolean() required = false; diff --git a/ui-ngx/src/app/modules/home/components/widget/config/data-keys.component.html b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/key/data-keys.component.html similarity index 97% rename from ui-ngx/src/app/modules/home/components/widget/config/data-keys.component.html rename to ui-ngx/src/app/modules/home/components/widget/lib/settings/common/key/data-keys.component.html index 9cb03bde9e..c60d29a9d1 100644 --- a/ui-ngx/src/app/modules/home/components/widget/config/data-keys.component.html +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/key/data-keys.component.html @@ -15,8 +15,10 @@ limitations under the License. --> - - {{placeholder}} + + {{ label ? label : placeholder}}
drag_indicator
-
{{ 'widget-config.appearance' | translate }}
+ +
+
+
{{ 'widgets.maps.data-layer.marker.marker-type' | translate }}
+ + {{ 'widgets.maps.data-layer.marker.marker-type-default' | translate }} + {{ 'widgets.maps.data-layer.marker.marker-type-image' | translate }} + +
+
+
widgets.maps.data-layer.color
+ +
+
+
widgets.maps.data-layer.marker.image
+ +
+
+
+
widgets.maps.data-layer.marker.marker-offset
+
+
widgets.maps.data-layer.marker.offset-horizontal
+ + + +
widgets.maps.data-layer.marker.offset-vertical
+ + + +
+
+
{{ 'widgets.maps.data-layer.groups' | translate }}
diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/map/map-data-layer-dialog.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/map/map-data-layer-dialog.component.ts index 9b7ea1b757..afcb164be8 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/map/map-data-layer-dialog.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/map/map-data-layer-dialog.component.ts @@ -22,6 +22,7 @@ import { MapDataLayerType, MapType, MarkersDataLayerSettings, + MarkerType, PolygonsDataLayerSettings } from '@home/components/widget/lib/maps/map.models'; import { Store } from '@ngrx/store'; @@ -61,6 +62,8 @@ export class MapDataLayerDialogComponent extends DialogComponent = []; datasourceTypesTranslations = datasourceTypeTranslationMap; @@ -103,7 +106,17 @@ export class MapDataLayerDialogComponent extends DialogComponent + this.updateValidators() + ); break; case 'polygons': const polygonsDataLayer = this.settings as PolygonsDataLayerSettings; @@ -179,6 +192,16 @@ export class MapDataLayerDialogComponent extends DialogComponent +
+
widgets.maps.data-layer.marker.marker-image
+
+ + + {{ 'widgets.maps.data-layer.marker.marker-image-type-image' | translate }} + + + {{ 'widgets.maps.data-layer.marker.marker-image-type-function' | translate }} + + +
+
+
+ +
+
{{ 'widgets.maps.data-layer.marker.custom-marker-image-size' | translate }}
+ + +
px
+
+
+
+
+
+
+ + + + +
+
+
+ + + +
+
diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/map/marker-image-settings-panel.component.scss b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/map/marker-image-settings-panel.component.scss new file mode 100644 index 0000000000..e37052f11a --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/map/marker-image-settings-panel.component.scss @@ -0,0 +1,53 @@ +/** + * Copyright © 2016-2024 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +@import '../../../../../../../../../scss/constants'; + +.tb-marker-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-marker-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-marker-image-settings-panel-body { + flex: 1; + display: flex; + flex-direction: column; + min-height: 0; + } + .tb-marker-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/marker-image-settings-panel.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/map/marker-image-settings-panel.component.ts new file mode 100644 index 0000000000..9184f26c5b --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/map/marker-image-settings-panel.component.ts @@ -0,0 +1,101 @@ +/// +/// Copyright © 2016-2024 The Thingsboard Authors +/// +/// Licensed under the Apache License, Version 2.0 (the "License"); +/// you may not use this file except in compliance with the License. +/// You may obtain a copy of the License at +/// +/// http://www.apache.org/licenses/LICENSE-2.0 +/// +/// Unless required by applicable law or agreed to in writing, software +/// distributed under the License is distributed on an "AS IS" BASIS, +/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +/// See the License for the specific language governing permissions and +/// limitations under the License. +/// + +import { Component, DestroyRef, EventEmitter, Input, OnInit, Output, ViewEncapsulation } from '@angular/core'; +import { PageComponent } from '@shared/components/page.component'; +import { TbPopoverComponent } from '@shared/components/popover.component'; +import { UntypedFormBuilder, UntypedFormGroup, Validators } from '@angular/forms'; +import { Store } from '@ngrx/store'; +import { AppState } from '@core/core.state'; +import { WidgetService } from '@core/http/widget.service'; +import { takeUntilDestroyed } from '@angular/core/rxjs-interop'; +import { MarkerImageSettings, MarkerImageType } from '@home/components/widget/lib/maps/map.models'; + +@Component({ + selector: 'tb-marker-image-settings-panel', + templateUrl: './marker-image-settings-panel.component.html', + providers: [], + styleUrls: ['./marker-image-settings-panel.component.scss'], + encapsulation: ViewEncapsulation.None +}) +export class MarkerImageSettingsPanelComponent extends PageComponent implements OnInit { + + @Input() + markerImageSettings: MarkerImageSettings; + + @Input() + popover: TbPopoverComponent; + + @Output() + markerImageSettingsApplied = new EventEmitter(); + + MarkerImageType = MarkerImageType; + + markerImageSettingsFormGroup: UntypedFormGroup; + + functionScopeVariables = this.widgetService.getWidgetScopeVariables(); + + constructor(private fb: UntypedFormBuilder, + private widgetService: WidgetService, + protected store: Store, + private destroyRef: DestroyRef) { + super(store); + } + + ngOnInit(): void { + this.markerImageSettingsFormGroup = this.fb.group( + { + type: [this.markerImageSettings?.type || MarkerImageType.image, []], + image: [this.markerImageSettings?.image, [Validators.required]], + imageSize: [this.markerImageSettings?.imageSize, [Validators.min(1)]], + imageFunction: [this.markerImageSettings?.imageFunction, [Validators.required]], + images: [this.markerImageSettings?.images, []] + } + ); + this.markerImageSettingsFormGroup.get('type').valueChanges.pipe( + takeUntilDestroyed(this.destroyRef) + ).subscribe(() => { + this.updateValidators(); + setTimeout(() => {this.popover?.updatePosition();}, 0); + }); + this.updateValidators(); + } + + cancel() { + this.popover?.hide(); + } + + applyMarkerImageSettings() { + const markerImageSettings: MarkerImageSettings = this.markerImageSettingsFormGroup.value; + this.markerImageSettingsApplied.emit(markerImageSettings); + } + + private updateValidators() { + const type: MarkerImageType = this.markerImageSettingsFormGroup.get('type').value; + if (type === MarkerImageType.image) { + this.markerImageSettingsFormGroup.get('image').enable({emitEvent: false}); + this.markerImageSettingsFormGroup.get('imageSize').enable({emitEvent: false}); + this.markerImageSettingsFormGroup.get('imageFunction').disable({emitEvent: false}); + this.markerImageSettingsFormGroup.get('images').disable({emitEvent: false}); + } else { + this.markerImageSettingsFormGroup.get('image').disable({emitEvent: false}); + this.markerImageSettingsFormGroup.get('imageSize').disable({emitEvent: false}); + this.markerImageSettingsFormGroup.get('imageFunction').enable({emitEvent: false}); + this.markerImageSettingsFormGroup.get('images').enable({emitEvent: false}); + } + } + +} diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/map/marker-image-settings.component.html b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/map/marker-image-settings.component.html new file mode 100644 index 0000000000..a3e3a41a6c --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/map/marker-image-settings.component.html @@ -0,0 +1,28 @@ + + + + + diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/map/marker-image-settings.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/map/marker-image-settings.component.ts new file mode 100644 index 0000000000..c8f2bd2725 --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/map/marker-image-settings.component.ts @@ -0,0 +1,96 @@ +/// +/// Copyright © 2016-2024 The Thingsboard Authors +/// +/// Licensed under the Apache License, Version 2.0 (the "License"); +/// you may not use this file except in compliance with the License. +/// You may obtain a copy of the License at +/// +/// http://www.apache.org/licenses/LICENSE-2.0 +/// +/// Unless required by applicable law or agreed to in writing, software +/// distributed under the License is distributed on an "AS IS" BASIS, +/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +/// See the License for the specific language governing permissions and +/// limitations under the License. +/// + +import { ChangeDetectorRef, Component, 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 { MarkerImageSettings, MarkerImageType } from '@home/components/widget/lib/maps/map.models'; +import { + MarkerImageSettingsPanelComponent +} from '@home/components/widget/lib/settings/common/map/marker-image-settings-panel.component'; + +@Component({ + selector: 'tb-marker-image-settings', + templateUrl: './marker-image-settings.component.html', + styleUrls: [], + providers: [ + { + provide: NG_VALUE_ACCESSOR, + useExisting: forwardRef(() => MarkerImageSettingsComponent), + multi: true + } + ] +}) +export class MarkerImageSettingsComponent implements ControlValueAccessor { + + @Input() + disabled: boolean; + + MarkerImageType = MarkerImageType; + + modelValue: MarkerImageSettings; + + 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: MarkerImageSettings): 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 { + const ctx: any = { + markerImageSettings: this.modelValue, + }; + const markerImageSettingsPanelPopover = this.popoverService.displayPopover(trigger, this.renderer, + this.viewContainerRef, MarkerImageSettingsPanelComponent, 'left', true, null, + ctx, + {}, + {}, {}, true); + markerImageSettingsPanelPopover.tbComponentRef.instance.popover = markerImageSettingsPanelPopover; + markerImageSettingsPanelPopover.tbComponentRef.instance.markerImageSettingsApplied.subscribe((markerImageSettings) => { + markerImageSettingsPanelPopover.hide(); + this.modelValue = markerImageSettings; + this.propagateChange(this.modelValue); + this.cd.detectChanges(); + }); + } + } +} 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 2416b32ade..12805131d5 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 @@ -204,6 +204,18 @@ import { } from '@home/components/widget/lib/settings/common/key/data-key-config-dialog.component'; import { DataKeyConfigComponent } from '@home/components/widget/lib/settings/common/key/data-key-config.component'; import { WidgetSettingsComponent } from '@home/components/widget/lib/settings/common/widget/widget-settings.component'; +import { + DataLayerColorSettingsComponent +} from '@home/components/widget/lib/settings/common/map/data-layer-color-settings.component'; +import { + DataLayerColorSettingsPanelComponent +} from '@home/components/widget/lib/settings/common/map/data-layer-color-settings-panel.component'; +import { + MarkerImageSettingsComponent +} from '@home/components/widget/lib/settings/common/map/marker-image-settings.component'; +import { + MarkerImageSettingsPanelComponent +} from '@home/components/widget/lib/settings/common/map/marker-image-settings-panel.component'; @NgModule({ declarations: [ @@ -277,6 +289,10 @@ import { WidgetSettingsComponent } from '@home/components/widget/lib/settings/co MapLayerSettingsPanelComponent, MapLayerRowComponent, MapLayersComponent, + DataLayerColorSettingsComponent, + DataLayerColorSettingsPanelComponent, + MarkerImageSettingsComponent, + MarkerImageSettingsPanelComponent, MapDataLayerDialogComponent, MapDataLayerRowComponent, MapDataLayersComponent, 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 a100ae7b18..15609560db 100644 --- a/ui-ngx/src/assets/locale/locale.constant-en_US.json +++ b/ui-ngx/src/assets/locale/locale.constant-en_US.json @@ -6875,6 +6875,11 @@ "source": "Source", "additional-data-keys": "Additional data keys", "groups": "Groups", + "color": "Color", + "color-settings": "Color settings", + "color-type-constant": "Constant", + "color-type-function": "Function", + "color-function": "Color function", "marker": { "latitude-key": "Latitude key", "longitude-key": "Longitude key", @@ -6887,7 +6892,20 @@ "no-markers": "No markers configured", "add-marker": "Add marker", "marker-configuration": "Marker configuration", - "remove-marker": "Remove marker" + "remove-marker": "Remove marker", + "marker-type": "Marker type", + "marker-type-default": "Default", + "marker-type-image": "Image", + "image": "Image", + "marker-image": "Marker image", + "marker-image-type-image": "Constant", + "marker-image-type-function": "Function", + "custom-marker-image-size": "Custom marker image size", + "marker-image-function": "Function", + "marker-images": "Marker images", + "marker-offset": "Marker offset", + "offset-horizontal": "Horizontal", + "offset-vertical": "Vertical" }, "polygon": { "polygon-key": "Polygon key", From b848df943bbaa26e018b9a8d8d6e73cd4252ef64 Mon Sep 17 00:00:00 2001 From: YevhenBondarenko Date: Mon, 13 Jan 2025 19:24:37 +0100 Subject: [PATCH 020/345] set default usage.stats.report.interval to 60 sec --- application/src/main/resources/thingsboard.yml | 2 +- msa/vc-executor/src/main/resources/tb-vc-executor.yml | 2 +- transport/coap/src/main/resources/tb-coap-transport.yml | 2 +- transport/http/src/main/resources/tb-http-transport.yml | 2 +- transport/lwm2m/src/main/resources/tb-lwm2m-transport.yml | 2 +- transport/mqtt/src/main/resources/tb-mqtt-transport.yml | 2 +- transport/snmp/src/main/resources/tb-snmp-transport.yml | 2 +- 7 files changed, 7 insertions(+), 7 deletions(-) diff --git a/application/src/main/resources/thingsboard.yml b/application/src/main/resources/thingsboard.yml index 163a87046d..bcb0517df0 100644 --- a/application/src/main/resources/thingsboard.yml +++ b/application/src/main/resources/thingsboard.yml @@ -187,7 +187,7 @@ usage: # Enable/Disable the collection of API usage statistics on a customer level enabled_per_customer: "${USAGE_STATS_REPORT_PER_CUSTOMER_ENABLED:false}" # Statistics reporting interval, set to send summarized data every 10 seconds by default - interval: "${USAGE_STATS_REPORT_INTERVAL:10}" + interval: "${USAGE_STATS_REPORT_INTERVAL:60}" # Amount of statistic messages in pack pack_size: "${USAGE_STATS_REPORT_PACK_SIZE:1024}" check: diff --git a/msa/vc-executor/src/main/resources/tb-vc-executor.yml b/msa/vc-executor/src/main/resources/tb-vc-executor.yml index 32cdb6085f..f2ff04fddd 100644 --- a/msa/vc-executor/src/main/resources/tb-vc-executor.yml +++ b/msa/vc-executor/src/main/resources/tb-vc-executor.yml @@ -288,7 +288,7 @@ usage: # Enable/Disable collection of statistics about API usage on a customer level enabled_per_customer: "${USAGE_STATS_REPORT_PER_CUSTOMER_ENABLED:false}" # Interval of reporting the statistics. By default, the summarized statistics are sent every 10 seconds - interval: "${USAGE_STATS_REPORT_INTERVAL:10}" + interval: "${USAGE_STATS_REPORT_INTERVAL:60}" # Amount of statistic messages in pack pack_size: "${USAGE_STATS_REPORT_PACK_SIZE:1024}" diff --git a/transport/coap/src/main/resources/tb-coap-transport.yml b/transport/coap/src/main/resources/tb-coap-transport.yml index 23ddc2efc5..7e998eb0c4 100644 --- a/transport/coap/src/main/resources/tb-coap-transport.yml +++ b/transport/coap/src/main/resources/tb-coap-transport.yml @@ -477,7 +477,7 @@ usage: # Enable/Disable collection of statistics about API usage on a customer level enabled_per_customer: "${USAGE_STATS_REPORT_PER_CUSTOMER_ENABLED:false}" # Interval of reporting the statistics. By default, the summarized statistics are sent every 10 seconds - interval: "${USAGE_STATS_REPORT_INTERVAL:10}" + interval: "${USAGE_STATS_REPORT_INTERVAL:60}" # Amount of statistic messages in pack pack_size: "${USAGE_STATS_REPORT_PACK_SIZE:1024}" diff --git a/transport/http/src/main/resources/tb-http-transport.yml b/transport/http/src/main/resources/tb-http-transport.yml index bc0cea209e..d8c157e091 100644 --- a/transport/http/src/main/resources/tb-http-transport.yml +++ b/transport/http/src/main/resources/tb-http-transport.yml @@ -447,7 +447,7 @@ usage: # Enable/Disable collection of statistics about API usage on a customer level enabled_per_customer: "${USAGE_STATS_REPORT_PER_CUSTOMER_ENABLED:false}" # Interval of reporting the statistics. By default, the summarized statistics are sent every 10 seconds - interval: "${USAGE_STATS_REPORT_INTERVAL:10}" + interval: "${USAGE_STATS_REPORT_INTERVAL:60}" # Amount of statistic messages in pack pack_size: "${USAGE_STATS_REPORT_PACK_SIZE:1024}" diff --git a/transport/lwm2m/src/main/resources/tb-lwm2m-transport.yml b/transport/lwm2m/src/main/resources/tb-lwm2m-transport.yml index c2c0bf8e93..11dd4f1251 100644 --- a/transport/lwm2m/src/main/resources/tb-lwm2m-transport.yml +++ b/transport/lwm2m/src/main/resources/tb-lwm2m-transport.yml @@ -547,7 +547,7 @@ usage: # Enable/Disable collection of statistics about API usage on a customer level enabled_per_customer: "${USAGE_STATS_REPORT_PER_CUSTOMER_ENABLED:false}" # Interval of reporting the statistics. By default, the summarized statistics are sent every 10 seconds - interval: "${USAGE_STATS_REPORT_INTERVAL:10}" + interval: "${USAGE_STATS_REPORT_INTERVAL:60}" # Amount of statistic messages in pack pack_size: "${USAGE_STATS_REPORT_PACK_SIZE:1024}" diff --git a/transport/mqtt/src/main/resources/tb-mqtt-transport.yml b/transport/mqtt/src/main/resources/tb-mqtt-transport.yml index 88bd34280d..f2ce184b66 100644 --- a/transport/mqtt/src/main/resources/tb-mqtt-transport.yml +++ b/transport/mqtt/src/main/resources/tb-mqtt-transport.yml @@ -480,7 +480,7 @@ usage: # Enable/Disable collection of statistics about API usage on a customer level enabled_per_customer: "${USAGE_STATS_REPORT_PER_CUSTOMER_ENABLED:false}" # Interval of reporting the statistics. By default, the summarized statistics are sent every 10 seconds - interval: "${USAGE_STATS_REPORT_INTERVAL:10}" + interval: "${USAGE_STATS_REPORT_INTERVAL:60}" # Amount of statistic messages in pack pack_size: "${USAGE_STATS_REPORT_PACK_SIZE:1024}" diff --git a/transport/snmp/src/main/resources/tb-snmp-transport.yml b/transport/snmp/src/main/resources/tb-snmp-transport.yml index 0e587b6028..08bf7dbb54 100644 --- a/transport/snmp/src/main/resources/tb-snmp-transport.yml +++ b/transport/snmp/src/main/resources/tb-snmp-transport.yml @@ -433,7 +433,7 @@ usage: # Enable/Disable collection of statistics about API usage on a customer level enabled_per_customer: "${USAGE_STATS_REPORT_PER_CUSTOMER_ENABLED:false}" # Interval of reporting the statistics. By default, the summarized statistics are sent every 10 seconds - interval: "${USAGE_STATS_REPORT_INTERVAL:10}" + interval: "${USAGE_STATS_REPORT_INTERVAL:60}" # Amount of statistic messages in pack pack_size: "${USAGE_STATS_REPORT_PACK_SIZE:1024}" From 933d1447d2cef1f0c82c90a3e19c942d43c59543 Mon Sep 17 00:00:00 2001 From: YevhenBondarenko Date: Tue, 14 Jan 2025 12:08:53 +0100 Subject: [PATCH 021/345] refactoring due to comments (removed serviceId duplications in ToUsageStatsServiceMsgPack) --- .../service/apiusage/DefaultTbApiUsageStateService.java | 7 ++++--- common/proto/src/main/proto/queue.proto | 2 +- .../queue/usagestats/DefaultTbApiUsageReportClient.java | 3 +-- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/application/src/main/java/org/thingsboard/server/service/apiusage/DefaultTbApiUsageStateService.java b/application/src/main/java/org/thingsboard/server/service/apiusage/DefaultTbApiUsageStateService.java index 803d85b57a..0cc02e5cf4 100644 --- a/application/src/main/java/org/thingsboard/server/service/apiusage/DefaultTbApiUsageStateService.java +++ b/application/src/main/java/org/thingsboard/server/service/apiusage/DefaultTbApiUsageStateService.java @@ -157,11 +157,12 @@ public class DefaultTbApiUsageStateService extends AbstractPartitionBasedService @Override public void process(TbProtoQueueMsg msgPack, TbCallback callback) { - msgPack.getValue().getMsgsList().forEach(this::process); + String serviceId = msgPack.getValue().getServiceId(); + msgPack.getValue().getMsgsList().forEach(msg -> process(msg, serviceId)); callback.onSuccess(); } - private void process(ToUsageStatsServiceMsg statsMsg) { + private void process(ToUsageStatsServiceMsg statsMsg, String serviceId) { TenantId tenantId = TenantId.fromUUID(new UUID(statsMsg.getTenantIdMSB(), statsMsg.getTenantIdLSB())); EntityId ownerId; if (statsMsg.getCustomerIdMSB() != 0 && statsMsg.getCustomerIdLSB() != 0) { @@ -170,7 +171,7 @@ public class DefaultTbApiUsageStateService extends AbstractPartitionBasedService ownerId = tenantId; } - processEntityUsageStats(tenantId, ownerId, statsMsg.getValuesList(), statsMsg.getServiceId()); + processEntityUsageStats(tenantId, ownerId, statsMsg.getValuesList(), serviceId); } private void processEntityUsageStats(TenantId tenantId, EntityId ownerId, List values, String serviceId) { diff --git a/common/proto/src/main/proto/queue.proto b/common/proto/src/main/proto/queue.proto index 3541654b73..0e318c3b30 100644 --- a/common/proto/src/main/proto/queue.proto +++ b/common/proto/src/main/proto/queue.proto @@ -1615,11 +1615,11 @@ message ToUsageStatsServiceMsg { repeated UsageStatsKVProto values = 5; int64 customerIdMSB = 6; int64 customerIdLSB = 7; - string serviceId = 8; } message ToUsageStatsServiceMsgPack { repeated ToUsageStatsServiceMsg msgs = 1; + string serviceId = 2; } message ToOtaPackageStateServiceMsg { diff --git a/common/queue/src/main/java/org/thingsboard/server/queue/usagestats/DefaultTbApiUsageReportClient.java b/common/queue/src/main/java/org/thingsboard/server/queue/usagestats/DefaultTbApiUsageReportClient.java index e667256e06..8c0439362b 100644 --- a/common/queue/src/main/java/org/thingsboard/server/queue/usagestats/DefaultTbApiUsageReportClient.java +++ b/common/queue/src/main/java/org/thingsboard/server/queue/usagestats/DefaultTbApiUsageReportClient.java @@ -115,7 +115,6 @@ public class DefaultTbApiUsageReportClient implements TbApiUsageReportClient { newStatsMsg.setCustomerIdLSB(customerId.getId().getLeastSignificantBits()); } - newStatsMsg.setServiceId(serviceInfoProvider.getServiceId()); return newStatsMsg; }); @@ -124,7 +123,6 @@ public class DefaultTbApiUsageReportClient implements TbApiUsageReportClient { .setValue(value); statsMsg.addValues(statsItem.build()); }); - statsForKey.clear(); } Map> reportStatsPerTpi = new HashMap<>(); @@ -160,6 +158,7 @@ public class DefaultTbApiUsageReportClient implements TbApiUsageReportClient { .map(partition -> ToUsageStatsServiceMsgPack.newBuilder() .addAllMsgs(partition) + .setServiceId(serviceInfoProvider.getServiceId()) .build()) .toList(); } From 89900a6deeb39eabbf6cabcf54497eae729bf2a0 Mon Sep 17 00:00:00 2001 From: YevhenBondarenko Date: Tue, 14 Jan 2025 13:53:48 +0100 Subject: [PATCH 022/345] minor refactoring due to better merge with pe --- .../DefaultTbApiUsageStateService.java | 25 ++++++++----------- 1 file changed, 11 insertions(+), 14 deletions(-) diff --git a/application/src/main/java/org/thingsboard/server/service/apiusage/DefaultTbApiUsageStateService.java b/application/src/main/java/org/thingsboard/server/service/apiusage/DefaultTbApiUsageStateService.java index 0cc02e5cf4..af4da51a45 100644 --- a/application/src/main/java/org/thingsboard/server/service/apiusage/DefaultTbApiUsageStateService.java +++ b/application/src/main/java/org/thingsboard/server/service/apiusage/DefaultTbApiUsageStateService.java @@ -61,7 +61,6 @@ import org.thingsboard.server.dao.tenant.TbTenantProfileCache; import org.thingsboard.server.dao.tenant.TenantService; import org.thingsboard.server.dao.timeseries.TimeseriesService; import org.thingsboard.server.dao.usagerecord.ApiUsageStateService; -import org.thingsboard.server.gen.transport.TransportProtos.ToUsageStatsServiceMsg; import org.thingsboard.server.gen.transport.TransportProtos.ToUsageStatsServiceMsgPack; import org.thingsboard.server.gen.transport.TransportProtos.UsageStatsKVProto; import org.thingsboard.server.queue.common.TbProtoQueueMsg; @@ -158,20 +157,18 @@ public class DefaultTbApiUsageStateService extends AbstractPartitionBasedService @Override public void process(TbProtoQueueMsg msgPack, TbCallback callback) { String serviceId = msgPack.getValue().getServiceId(); - msgPack.getValue().getMsgsList().forEach(msg -> process(msg, serviceId)); - callback.onSuccess(); - } - - private void process(ToUsageStatsServiceMsg statsMsg, String serviceId) { - TenantId tenantId = TenantId.fromUUID(new UUID(statsMsg.getTenantIdMSB(), statsMsg.getTenantIdLSB())); - EntityId ownerId; - if (statsMsg.getCustomerIdMSB() != 0 && statsMsg.getCustomerIdLSB() != 0) { - ownerId = new CustomerId(new UUID(statsMsg.getCustomerIdMSB(), statsMsg.getCustomerIdLSB())); - } else { - ownerId = tenantId; - } + msgPack.getValue().getMsgsList().forEach(msg -> { + TenantId tenantId = TenantId.fromUUID(new UUID(msg.getTenantIdMSB(), msg.getTenantIdLSB())); + EntityId ownerId; + if (msg.getCustomerIdMSB() != 0 && msg.getCustomerIdLSB() != 0) { + ownerId = new CustomerId(new UUID(msg.getCustomerIdMSB(), msg.getCustomerIdLSB())); + } else { + ownerId = tenantId; + } - processEntityUsageStats(tenantId, ownerId, statsMsg.getValuesList(), serviceId); + processEntityUsageStats(tenantId, ownerId, msg.getValuesList(), serviceId); + }); + callback.onSuccess(); } private void processEntityUsageStats(TenantId tenantId, EntityId ownerId, List values, String serviceId) { From 304fd26be497af0eeb5d2c46ae37771d2a055d49 Mon Sep 17 00:00:00 2001 From: Igor Kulikov Date: Tue, 14 Jan 2025 17:40:22 +0200 Subject: [PATCH 023/345] UI: Map data layer appearance. --- .../widget/lib/maps/map-data-layer.ts | 320 +++++++++++++++--- .../components/widget/lib/maps/map.models.ts | 159 ++++++++- .../home/components/widget/lib/maps/map.ts | 80 ++++- ...data-layer-pattern-settings.component.html | 86 +++++ .../data-layer-pattern-settings.component.ts | 180 ++++++++++ .../map/map-data-layer-dialog.component.html | 31 ++ .../map/map-data-layer-dialog.component.ts | 35 +- .../map/map-data-layer-row.component.html | 5 + .../map/map-data-layer-row.component.scss | 2 +- .../map/map-data-layer-row.component.ts | 5 + .../common/widget-settings-common.module.ts | 4 + .../assets/locale/locale.constant-en_US.json | 21 +- 12 files changed, 846 insertions(+), 82 deletions(-) create mode 100644 ui-ngx/src/app/modules/home/components/widget/lib/settings/common/map/data-layer-pattern-settings.component.html create mode 100644 ui-ngx/src/app/modules/home/components/widget/lib/settings/common/map/data-layer-pattern-settings.component.ts diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/maps/map-data-layer.ts b/ui-ngx/src/app/modules/home/components/widget/lib/maps/map-data-layer.ts index 2b825e09be..0cabfe9fef 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/maps/map-data-layer.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/maps/map-data-layer.ts @@ -19,6 +19,9 @@ import { createColorMarkerURI, DataLayerColorSettings, DataLayerColorType, + DataLayerPatternSettings, + DataLayerPatternType, + DataLayerTooltipTrigger, defaultBaseCirclesDataLayerSettings, defaultBaseMarkersDataLayerSettings, defaultBasePolygonsDataLayerSettings, @@ -37,14 +40,15 @@ import { MarkerImageType, MarkersDataLayerSettings, MarkerType, - PolygonsDataLayerSettings, + PolygonsDataLayerSettings, processTooltipTemplate, ShapeDataLayerSettings, TbCircleData, TbMapDatasource } from '@home/components/widget/lib/maps/map.models'; import { TbMap } from '@home/components/widget/lib/maps/map'; -import { FormattedData } from '@shared/models/widget.models'; -import { Observable, of } from 'rxjs'; +import { Datasource, FormattedData } from '@shared/models/widget.models'; +import { forkJoin, Observable, of } from 'rxjs'; import { + createLabelFromPattern, guid, isDefined, isDefinedAndNotNull, @@ -55,37 +59,98 @@ import { parseTbFunction, safeExecuteTbFunction } from '@core/utils'; -import L, { LatLngBounds } from 'leaflet'; +import L, { LatLngBounds, PathOptions } from 'leaflet'; import { CompiledTbFunction } from '@shared/models/js-function.models'; import { catchError, map } from 'rxjs/operators'; import tinycolor from 'tinycolor2'; import { WidgetContext } from '@home/models/widget-component.models'; import { ImagePipe } from '@shared/pipe/image.pipe'; +import { CustomTranslatePipe } from '@shared/pipe/custom-translate.pipe'; abstract class TbDataLayerItem> { protected layer: L.Layer; + protected tooltip: L.Popup; - constructor(data: FormattedData, - dsData: FormattedData[], - protected settings: S, - protected dataLayer: L) { + protected constructor(data: FormattedData, + dsData: FormattedData[], + protected settings: S, + protected dataLayer: L) { this.layer = this.create(data, dsData); + if (this.settings.tooltip?.show) { + this.createTooltip(data.$datasource); + this.updateTooltip(data, dsData); + } + this.createEventListeners(data, dsData); this.dataLayer.getFeatureGroup().addLayer(this.layer); } protected abstract create(data: FormattedData, dsData: FormattedData[]): L.Layer; + protected abstract unbindLabel(): void; + + protected abstract bindLabel(content: L.Content): void; + + protected abstract createEventListeners(data: FormattedData, dsData: FormattedData[]): void; + public abstract update(data: FormattedData, dsData: FormattedData[]): void; public remove() { + this.layer.off(); this.dataLayer.getFeatureGroup().removeLayer(this.layer); } - protected updateLayer(newLayer: L.Layer) { - this.dataLayer.getFeatureGroup().removeLayer(this.layer); - this.layer = newLayer; - this.dataLayer.getFeatureGroup().addLayer(this.layer); + protected updateTooltip(data: FormattedData, dsData: FormattedData[]) { + if (this.settings.tooltip.show) { + let tooltipTemplate = this.dataLayer.dataLayerTooltipProcessor.processPattern(data, dsData); + tooltipTemplate = processTooltipTemplate(tooltipTemplate); + this.tooltip.setContent(tooltipTemplate); + if (this.tooltip.isOpen() && this.tooltip.getElement()) { + this.bindTooltipActions(data.$datasource); + } + } + } + + protected updateLabel(data: FormattedData, dsData: FormattedData[]) { + if (this.settings.label.show) { + this.unbindLabel(); + const label = this.dataLayer.dataLayerLabelProcessor.processPattern(data, dsData); + const labelColor = this.dataLayer.getCtx().widgetConfig.color; + const content: L.Content = `
${label}
`; + this.bindLabel(content); + } + } + + private createTooltip(datasource: TbMapDatasource) { + this.tooltip = L.popup(); + this.layer.bindPopup(this.tooltip, {autoClose: this.settings.tooltip.autoclose, closeOnClick: false}); + if (this.settings.tooltip.trigger === DataLayerTooltipTrigger.hover) { + this.layer.off('click'); + this.layer.on('mouseover', () => { + this.layer.openPopup(); + }); + this.layer.on('mousemove', (e) => { + this.tooltip.setLatLng(e.latlng); + }); + this.layer.on('mouseout', () => { + this.layer.closePopup(); + }); + } + this.layer.on('popupopen', () => { + this.bindTooltipActions(datasource); + (this.layer as any)._popup._closeButton.addEventListener('click', (event: Event) => { + event.preventDefault(); + }); + }); + } + + private bindTooltipActions(datasource: TbMapDatasource) { + const actions = this.tooltip.getElement().getElementsByClassName('tb-custom-action'); + Array.from(actions).forEach( + (element: HTMLElement) => { + const actionName = element.getAttribute('data-action-name'); + this.dataLayer.getMap().tooltipElementClick(element, actionName, datasource); + }); } } @@ -96,6 +161,76 @@ export enum MapDataLayerType { circle = 'circle' } +class DataLayerPatternProcessor { + + private patternFunction: CompiledTbFunction; + private pattern: string; + + constructor(private dataLayer: TbMapDataLayer, + private settings: DataLayerPatternSettings) {} + + public setup(): Observable { + if (this.settings.type === DataLayerPatternType.function) { + return parseTbFunction(this.dataLayer.getCtx().http, this.settings.patternFunction, ['data', 'dsData']).pipe( + map((parsed) => { + this.patternFunction = parsed; + return null; + }) + ); + } else { + this.pattern = this.settings.pattern; + return of(null) + } + } + + public processPattern(data: FormattedData, dsData: FormattedData[]): string { + let pattern: string; + if (this.settings.type === DataLayerPatternType.function) { + pattern = safeExecuteTbFunction(this.patternFunction, [data, dsData]); + } else { + pattern = this.pattern; + } + const text = createLabelFromPattern(pattern, data); + const customTranslate = this.dataLayer.getCtx().$injector.get(CustomTranslatePipe); + return customTranslate.transform(text); + } + +} + +class DataLayerColorProcessor { + + private colorFunction: CompiledTbFunction; + private color: string; + + constructor(private dataLayer: TbMapDataLayer, + private settings: DataLayerColorSettings) {} + + public setup(): Observable { + if (this.settings.type === DataLayerColorType.function) { + return parseTbFunction(this.dataLayer.getCtx().http, this.settings.colorFunction, ['data', 'dsData']).pipe( + map((parsed) => { + this.colorFunction = parsed; + return null; + }) + ); + } else { + this.color = this.settings.color; + return of(null) + } + } + + public processColor(data: FormattedData, dsData: FormattedData[]): string { + let color: string; + if (this.settings.type === DataLayerColorType.function) { + color = safeExecuteTbFunction(this.colorFunction, [data, dsData]); + } else { + color = this.color; + } + return color; + } + +} + export abstract class TbMapDataLayer> implements L.TB.DataLayer { protected settings: S; @@ -112,6 +247,9 @@ export abstract class TbMapDataLayer, inputSettings: S) { this.settings = mergeDeepIgnoreArray({} as S, this.defaultBaseSettings() as S, inputSettings); @@ -120,15 +258,22 @@ export abstract class TbMapDataLayer { + public setup(): Observable { this.datasource = mapDataSourceSettingsToDatasource(this.settings); this.datasource.dataKeys = this.settings.additionalDataKeys ? [...this.settings.additionalDataKeys] : []; this.mapDataId = this.datasource.mapDataIds[0]; this.datasource = this.setupDatasource(this.datasource); - return this.doSetup(); + return forkJoin( + [ + this.dataLayerLabelProcessor ? this.dataLayerLabelProcessor.setup() : of(null), + this.dataLayerTooltipProcessor ? this.dataLayerTooltipProcessor.setup() : of(null), + this.doSetup() + ]); } public getDatasource(): TbMapDatasource { @@ -192,6 +337,9 @@ export abstract class TbMapDataLayer { + return this.map; + } protected setupDatasource(datasource: TbMapDatasource): TbMapDatasource { return datasource; @@ -205,7 +353,7 @@ export abstract class TbMapDataLayer; - protected abstract doSetup(): Observable; + protected abstract doSetup(): Observable; protected abstract isValidLayerData(layerData: FormattedData): boolean; @@ -236,12 +384,26 @@ class TbMarkerDataLayerItem extends TbDataLayerItem, dsData: FormattedData[]): void { + this.dataLayer.getMap().markerClick(this.marker, data.$datasource); + } + + protected unbindLabel() { + this.marker.unbindTooltip(); + } + + protected bindLabel(content: L.Content): void { + this.marker.bindTooltip(content, { className: 'tb-marker-label', permanent: true, direction: 'top', offset: this.labelOffset }); + } + public update(data: FormattedData, dsData: FormattedData[]): void { const position = this.dataLayer.extractLocation(data); if (!this.marker.getLatLng().equals(position)) { this.location = position; this.marker.setLatLng(position); } + this.updateTooltip(data, dsData); this.updateMarkerIcon(data, dsData); } @@ -255,15 +417,10 @@ class TbMarkerDataLayerItem extends TbDataLayerItem, dsData: FormattedData[]) { - - } - } abstract class MarkerIconProcessor { @@ -448,11 +605,10 @@ export class TbMarkersDataLayer extends TbMapDataLayer { + private polygonContainer: L.FeatureGroup; private polygon: L.Polygon; constructor(data: FormattedData, @@ -532,29 +689,41 @@ class TbPolygonDataLayerItem extends TbDataLayerItem, dsData: FormattedData[]): L.Layer { const polyData = this.dataLayer.extractPolygonCoordinates(data); const polyConstructor = isCutPolygon(polyData) || polyData.length !== 2 ? L.polygon : L.rectangle; + const style = this.dataLayer.getShapeStyle(data, dsData); this.polygon = polyConstructor(polyData, { - fill: true, - fillColor: '#3a77e7', - color: '#0742ad', - weight: 1, - fillOpacity: 0.4, - opacity: 1 + ...style }); - return this.polygon; + + this.polygonContainer = L.featureGroup(); + this.polygon.addTo(this.polygonContainer); + + this.updateLabel(data, dsData); + return this.polygonContainer; + } + + protected createEventListeners(data: FormattedData, dsData: FormattedData[]): void { + this.dataLayer.getMap().polygonClick(this.polygonContainer, data.$datasource); + } + + protected unbindLabel() { + this.polygonContainer.unbindTooltip(); } + + protected bindLabel(content: L.Content): void { + this.polygonContainer.bindTooltip(content, {className: 'tb-polygon-label', permanent: true, direction: 'center'}) + .openTooltip(this.polygonContainer.getBounds().getCenter()); + } + public update(data: FormattedData, dsData: FormattedData[]): void { const polyData = this.dataLayer.extractPolygonCoordinates(data); + const style = this.dataLayer.getShapeStyle(data, dsData); if (isCutPolygon(polyData) || polyData.length !== 2) { if (this.polygon instanceof L.Rectangle) { + this.polygonContainer.removeLayer(this.polygon); this.polygon = L.polygon(polyData, { - fill: true, - fillColor: '#3a77e7', - color: '#0742ad', - weight: 1, - fillOpacity: 0.4, - opacity: 1 + ...style }); - this.updateLayer(this.polygon); + this.polygon.addTo(this.polygonContainer); } else { this.polygon.setLatLngs(polyData); } @@ -563,10 +732,44 @@ class TbPolygonDataLayerItem extends TbDataLayerItem> extends TbMapDataLayer { + + public fillColorProcessor: DataLayerColorProcessor; + public strokeColorProcessor: DataLayerColorProcessor; + + protected constructor(protected map: TbMap, + inputSettings: S) { + super(map, inputSettings); + } + + protected doSetup(): Observable { + this.fillColorProcessor = new DataLayerColorProcessor(this, this.settings.fillColor); + this.strokeColorProcessor = new DataLayerColorProcessor(this, this.settings.strokeColor); + return forkJoin([this.fillColorProcessor.setup(), this.strokeColorProcessor.setup()]); + } + + public getShapeStyle(data: FormattedData, dsData: FormattedData[]): L.PathOptions { + const fill = this.fillColorProcessor.processColor(data, dsData); + const stroke = this.strokeColorProcessor.processColor(data, dsData); + const style: L.PathOptions = { + fill: true, + fillColor: fill, + color: stroke, + weight: this.settings.strokeWeight, + fillOpacity: 1, + opacity: 1 + }; + return style; } } -export class TbPolygonsDataLayer extends TbMapDataLayer { +export class TbPolygonsDataLayer extends TbShapesDataLayer { constructor(protected map: TbMap, inputSettings: PolygonsDataLayerSettings) { @@ -586,8 +789,8 @@ export class TbPolygonsDataLayer extends TbMapDataLayer { - return of(null); + protected doSetup(): Observable { + return super.doSetup(); } protected isValidLayerData(layerData: FormattedData): boolean { @@ -622,17 +825,28 @@ class TbCircleDataLayerItem extends TbDataLayerItem, dsData: FormattedData[]): L.Layer { const circleData = this.dataLayer.extractCircleCoordinates(data); const center = new L.LatLng(circleData.latitude, circleData.longitude); + const style = this.dataLayer.getShapeStyle(data, dsData); this.circle = L.circle(center, { radius: circleData.radius, - fillColor: '#3a77e7', - color: '#0742ad', - weight: 1, - fillOpacity: 0.4, - opacity: 1 + ...style }); + this.updateLabel(data, dsData); return this.circle; } + protected createEventListeners(data: FormattedData, dsData: FormattedData[]): void { + this.dataLayer.getMap().circleClick(this.circle, data.$datasource); + } + + protected unbindLabel() { + this.circle.unbindTooltip(); + } + + protected bindLabel(content: L.Content): void { + this.circle.bindTooltip(content, { className: 'tb-polygon-label', permanent: true, direction: 'center'}) + .openTooltip(this.circle.getLatLng()); + } + public update(data: FormattedData, dsData: FormattedData[]): void { const circleData = this.dataLayer.extractCircleCoordinates(data); const center = new L.LatLng(circleData.latitude, circleData.longitude); @@ -642,10 +856,14 @@ class TbCircleDataLayerItem extends TbDataLayerItem { +export class TbCirclesDataLayer extends TbShapesDataLayer { constructor(protected map: TbMap, inputSettings: CirclesDataLayerSettings) { @@ -666,7 +884,7 @@ export class TbCirclesDataLayer extends TbMapDataLayer { - return of(null); + return super.doSetup(); } protected isValidLayerData(layerData: FormattedData): boolean { diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/maps/map.models.ts b/ui-ngx/src/app/modules/home/components/widget/lib/maps/map.models.ts index f575576fc6..76264946e7 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/maps/map.models.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/maps/map.models.ts @@ -46,6 +46,7 @@ export enum MapType { export interface MapDataSourceSettings { dsType: DatasourceType; + dsLabel?: string; dsDeviceId?: string; dsEntityAliasId?: string; dsFilterId?: string; @@ -58,6 +59,7 @@ export interface TbMapDatasource extends Datasource { export const mapDataSourceSettingsToDatasource = (settings: MapDataSourceSettings): TbMapDatasource => { return { type: settings.dsType, + name: settings.dsLabel, deviceId: settings.dsDeviceId, entityAliasId: settings.dsEntityAliasId, filterId: settings.dsFilterId, @@ -66,11 +68,64 @@ export const mapDataSourceSettingsToDatasource = (settings: MapDataSourceSetting }; }; + +export enum DataLayerPatternType { + pattern = 'pattern', + function = 'function' +} + +export interface DataLayerPatternSettings { + show: boolean; + type: DataLayerPatternType; + pattern?: string; + patternFunction?: TbFunction; +} + +export enum DataLayerTooltipTrigger { + click = 'click', + hover = 'hover' +} + +export const dataLayerTooltipTriggers = Object.keys(DataLayerTooltipTrigger) as DataLayerTooltipTrigger[]; + +export const dataLayerTooltipTriggerTranslationMap = new Map( + [ + [DataLayerTooltipTrigger.click, 'widgets.maps.data-layer.tooltip-trigger-click'], + [DataLayerTooltipTrigger.hover, 'widgets.maps.data-layer.tooltip-trigger-hover'] + ] +); + +export interface DataLayerTooltipSettings extends DataLayerPatternSettings { + trigger: DataLayerTooltipTrigger; + autoclose: boolean; + offsetX: number; + offsetY: number; +} + export interface MapDataLayerSettings extends MapDataSourceSettings { additionalDataKeys?: DataKey[]; + label: DataLayerPatternSettings; + tooltip: DataLayerTooltipSettings; groups?: string[]; } +export const defaultBaseDataLayerSettings: Partial = { + label: { + show: true, + type: DataLayerPatternType.pattern, + pattern: '${entityName}' + }, + tooltip: { + show: true, + trigger: DataLayerTooltipTrigger.click, + autoclose: true, + type: DataLayerPatternType.pattern, + pattern: '${entityName}

Latitude: ${latitude:7}
Longitude: ${longitude:7}
Temperature: ${temperature} °C
See tooltip settings for details', + offsetX: 0, + offsetY: -1 + } +} + export type MapDataLayerType = 'markers' | 'polygons' | 'circles'; export const mapDataLayerValid = (dataLayer: MapDataLayerSettings, type: MapDataLayerType): boolean => { @@ -192,6 +247,7 @@ const defaultMarkerYPosFunction = 'var value = prevValue || 0.3;\n' + export const defaultMarkersDataLayerSettings = (mapType: MapType, functionsOnly = false): MarkersDataLayerSettings => mergeDeep({ dsType: functionsOnly ? DatasourceType.function : DatasourceType.entity, + dsLabel: functionsOnly ? 'First point' : '', xKey: { name: functionsOnly ? 'f(x)' : (MapType.geoMap === mapType ? 'latitude' : 'xPos'), label: MapType.geoMap === mapType ? 'latitude' : 'xPos', @@ -210,27 +266,34 @@ export const defaultMarkersDataLayerSettings = (mapType: MapType, functionsOnly } } as MarkersDataLayerSettings, defaultBaseMarkersDataLayerSettings as MarkersDataLayerSettings); -export const defaultBaseMarkersDataLayerSettings: Partial = { +export const defaultBaseMarkersDataLayerSettings: Partial = mergeDeep({ markerType: MarkerType.default, markerColor: { type: DataLayerColorType.constant, - color: '#FE7569', + color: '#307FE5', }, markerImage: { type: MarkerImageType.image, - image: createColorMarkerURI(tinycolor('#FE7569')), + image: createColorMarkerURI(tinycolor('#307FE5')), imageSize: 34 }, markerOffsetX: 0.5, markerOffsetY: 1 -}; +} as MarkersDataLayerSettings, defaultBaseDataLayerSettings); + +export interface ShapeDataLayerSettings extends MapDataLayerSettings { + fillColor: DataLayerColorSettings; + strokeColor: DataLayerColorSettings; + strokeWeight: number; +} -export interface PolygonsDataLayerSettings extends MapDataLayerSettings { +export interface PolygonsDataLayerSettings extends ShapeDataLayerSettings { polygonKey: DataKey; } export const defaultPolygonsDataLayerSettings = (functionsOnly = false): PolygonsDataLayerSettings => mergeDeep({ dsType: functionsOnly ? DatasourceType.function : DatasourceType.entity, + dsLabel: functionsOnly ? 'First polygon' : '', polygonKey: { name: functionsOnly ? 'f(x)' : 'perimeter', label: 'perimeter', @@ -240,16 +303,26 @@ export const defaultPolygonsDataLayerSettings = (functionsOnly = false): Polygon } } as PolygonsDataLayerSettings, defaultBasePolygonsDataLayerSettings as PolygonsDataLayerSettings); -export const defaultBasePolygonsDataLayerSettings: Partial = { - -} - -export interface CirclesDataLayerSettings extends MapDataLayerSettings { +export const defaultBasePolygonsDataLayerSettings: Partial = mergeDeep({ + fillColor: { + type: DataLayerColorType.constant, + color: 'rgba(51,136,255,0.2)', + }, + strokeColor: { + type: DataLayerColorType.constant, + color: '#3388ff', + }, + strokeWeight: 3 +} as Partial, defaultBaseDataLayerSettings, + {label: {show: false}, tooltip: {show: false, pattern: '${entityName}

TimeStamp: ${ts:7}'}} as Partial) + +export interface CirclesDataLayerSettings extends ShapeDataLayerSettings { circleKey: DataKey; } export const defaultCirclesDataLayerSettings = (functionsOnly = false): CirclesDataLayerSettings => mergeDeep({ dsType: functionsOnly ? DatasourceType.function : DatasourceType.entity, + dsLabel: functionsOnly ? 'First circle' : '', circleKey: { name: functionsOnly ? 'f(x)' : 'perimeter', label: 'perimeter', @@ -259,9 +332,18 @@ export const defaultCirclesDataLayerSettings = (functionsOnly = false): CirclesD } } as CirclesDataLayerSettings, defaultBaseCirclesDataLayerSettings as CirclesDataLayerSettings); -export const defaultBaseCirclesDataLayerSettings: Partial = { - -} +export const defaultBaseCirclesDataLayerSettings: Partial = mergeDeep({ + fillColor: { + type: DataLayerColorType.constant, + color: 'rgba(51,136,255,0.2)', + }, + strokeColor: { + type: DataLayerColorType.constant, + color: '#3388ff', + }, + strokeWeight: 3 +} as Partial, defaultBaseDataLayerSettings, + {label: {show: false}, tooltip: {show: false, pattern: '${entityName}

TimeStamp: ${ts:7}'}} as Partial) export const defaultMapDataLayerSettings = (mapType: MapType, dataLayerType: MapDataLayerType, functionsOnly = false): MapDataLayerSettings => { switch (dataLayerType) { @@ -274,6 +356,17 @@ export const defaultMapDataLayerSettings = (mapType: MapType, dataLayerType: Map } }; +export const defaultBaseMapDataLayerSettings = (dataLayerType: MapDataLayerType): T => { + switch (dataLayerType) { + case 'markers': + return defaultBaseMarkersDataLayerSettings as T; + case 'polygons': + return defaultBasePolygonsDataLayerSettings as T; + case 'circles': + return defaultBaseCirclesDataLayerSettings as T; + } +} + export interface AdditionalMapDataSourceSettings extends MapDataSourceSettings { dataKeys: DataKey[]; } @@ -617,6 +710,8 @@ export type MapSetting = GeoMapSettings & ImageMapSettings; export const defaultMapSettings: MapSetting = defaultGeoMapSettings; +export type MapActionHandler = ($event: Event, datasource: TbMapDatasource) => void; + export interface MarkerImageInfo { url: string; size: number; @@ -722,7 +817,7 @@ const mapDatasourceIsSame = (ds1: TbMapDatasource, ds2: TbMapDatasource): boolea if (ds1.type === ds2.type) { switch (ds1.type) { case DatasourceType.function: - return true; + return ds1.name === ds2.name; case DatasourceType.device: case DatasourceType.entity: if (ds1.filterId === ds2.filterId) { @@ -804,3 +899,39 @@ export const loadImageWithAspect = (imagePipe: ImagePipe, imageUrl: string): Obs return of(null); } }; + +const linkActionRegex = /([^<]*)<\/link-act>/g; +const buttonActionRegex = /([^<]*)<\/button-act>/g; + +const createTooltipLinkElement = (actionName: string, actionText: string): string => { + return `
${actionText}`; +} + +const creatTooltipButtonElement = (actionName: string, actionText: string): string => { + return ``; +} + +export const processTooltipTemplate = (template: string): string => { + let actionTags: string; + let actionText: string; + let actionName: string; + let action: string; + + let match = linkActionRegex.exec(template); + while (match !== null) { + [actionTags, actionName, actionText] = match; + action = createTooltipLinkElement(actionName, actionText); + template = template.replace(actionTags, action); + match = linkActionRegex.exec(template); + } + + match = buttonActionRegex.exec(template); + while (match !== null) { + [actionTags, actionName, actionText] = match; + action = creatTooltipButtonElement(actionName, actionText); + template = template.replace(actionTags, action); + match = buttonActionRegex.exec(template); + } + + return template; +} 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 973d035683..a85406daa9 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 @@ -22,7 +22,7 @@ import { defaultImageMapSettings, GeoMapSettings, ImageMapSettings, - latLngPointToBounds, + latLngPointToBounds, MapActionHandler, MapSetting, MapType, MapZoomAction, @@ -34,7 +34,7 @@ import { import { WidgetContext } from '@home/models/widget-component.models'; import { formattedDataFormDatasourceData, isDefinedAndNotNull, mergeDeepIgnoreArray } from '@core/utils'; import { DeepPartial } from '@shared/models/common'; -import L, { LatLngBounds, LatLngTuple, PointExpression, Projection } from 'leaflet'; +import L, { LatLngBounds, LatLngTuple, LeafletMouseEvent, PointExpression, Projection } from 'leaflet'; import { forkJoin, Observable, of } from 'rxjs'; import { TbMapLayer } from '@home/components/widget/lib/maps/map-layer'; import { map, switchMap, tap } from 'rxjs/operators'; @@ -47,7 +47,7 @@ import { TbPolygonsDataLayer } from '@home/components/widget/lib/maps/map-data-layer'; import { IWidgetSubscription, WidgetSubscriptionOptions } from '@core/api/widget-api.models'; -import { widgetType } from '@shared/models/widget.models'; +import { Datasource, WidgetActionDescriptor, widgetType } from '@shared/models/widget.models'; import { EntityDataPageLink } from '@shared/models/query/query.models'; import { CustomTranslatePipe } from '@shared/pipe/custom-translate.pipe'; import ITooltipsterInstance = JQueryTooltipster.ITooltipsterInstance; @@ -82,12 +82,23 @@ export abstract class TbMap { private readonly mapResize$: ResizeObserver; + private readonly tooltipActions: { [name: string]: MapActionHandler }; + private readonly markerClickActions: { [name: string]: MapActionHandler }; + private readonly polygonClickActions: { [name: string]: MapActionHandler }; + private readonly circleClickActions: { [name: string]: MapActionHandler }; + private tooltipInstances: ITooltipsterInstance[] = []; protected constructor(protected ctx: WidgetContext, protected inputSettings: DeepPartial, protected containerElement: HTMLElement) { this.settings = mergeDeepIgnoreArray({} as S, this.defaultSettings(), this.inputSettings as S); + + this.tooltipActions = this.loadActions('tooltipAction'); + this.markerClickActions = this.loadActions('markerClick'); + this.polygonClickActions = this.loadActions('polygonClick'); + this.circleClickActions = this.loadActions('circleClick'); + $(containerElement).empty(); $(containerElement).addClass('tb-map-layout'); const mapElement = $('
'); @@ -331,12 +342,33 @@ export abstract class TbMap { if (this.settings.useDefaultCenterPosition) { bounds = bounds.extend(this.defaultCenterPosition); } - this.map.fitBounds(bounds, { padding: [10, 10], animate: false }); + this.map.fitBounds(bounds, { padding: [50, 50], animate: false }); this.map.invalidateSize(); } } } + private loadActions(name: string): { [name: string]: MapActionHandler } { + const descriptors = this.ctx.actionsApi.getActionDescriptors(name); + const actions: { [name: string]: MapActionHandler } = {}; + descriptors.forEach(descriptor => { + actions[descriptor.name] = ($event: Event, datasource: TbMapDatasource) => this.onCustomAction(descriptor, $event, datasource); + }); + return actions; + } + + private onCustomAction(descriptor: WidgetActionDescriptor, $event: Event, entityInfo: TbMapDatasource) { + if ($event) { + $event.preventDefault(); + $event.stopPropagation(); + } + const { entityId, entityName, entityLabel, entityType } = entityInfo; + this.ctx.actionsApi.handleWidgetAction($event, descriptor, { + entityType, + id: entityId + }, entityName, null, entityLabel); + } + protected abstract defaultSettings(): S; protected abstract createMap(): Observable; @@ -370,6 +402,46 @@ export abstract class TbMap { return this.settings.mapType; } + public tooltipElementClick(element: HTMLElement, action: string, datasource: TbMapDatasource): void { + if (element && this.tooltipActions[action]) { + element.onclick = ($event) => + { + this.tooltipActions[action]($event, datasource); + return false; + }; + } + } + + public markerClick(marker: L.Layer, datasource: TbMapDatasource): void { + if (Object.keys(this.markerClickActions).length) { + marker.on('click', (event: LeafletMouseEvent) => { + for (const action in this.markerClickActions) { + this.markerClickActions[action](event.originalEvent, datasource); + } + }); + } + } + + public polygonClick(polygon: L.Layer, datasource: TbMapDatasource): void { + if (Object.keys(this.polygonClickActions).length) { + polygon.on('click', (event: LeafletMouseEvent) => { + for (const action in this.polygonClickActions) { + this.polygonClickActions[action](event.originalEvent, datasource); + } + }); + } + } + + public circleClick(circle: L.Layer, datasource: TbMapDatasource): void { + if (Object.keys(this.circleClickActions).length) { + circle.on('click', (event: LeafletMouseEvent) => { + for (const action in this.circleClickActions) { + this.circleClickActions[action](event.originalEvent, datasource); + } + }); + } + } + public destroy() { if (this.mapResize$) { this.mapResize$.disconnect(); diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/map/data-layer-pattern-settings.component.html b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/map/data-layer-pattern-settings.component.html new file mode 100644 index 0000000000..892558b047 --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/map/data-layer-pattern-settings.component.html @@ -0,0 +1,86 @@ + + +
+ + + +
+ + {{ (patternType === 'label' ? 'widgets.maps.data-layer.label' : 'widgets.maps.data-layer.tooltip') | translate }} + + + {{ 'widgets.maps.data-layer.pattern-type-pattern' | translate }} + {{ 'widgets.maps.data-layer.pattern-type-function' | translate }} + +
+
+
+ + + + + + +
+
widgets.maps.data-layer.tooltip-trigger
+ + + + {{ dataLayerTooltipTriggerTranslationMap.get(trigger) | translate }} + + + +
+
+ + {{ 'widgets.maps.data-layer.auto-close-tooltips' | translate }} + +
+
+
widgets.maps.data-layer.tooltip-offset
+
+
widgets.maps.data-layer.tooltip-offset-horizontal
+ + + +
widgets.maps.data-layer.tooltip-offset-vertical
+ + + +
+
+
+
+
+
+
diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/map/data-layer-pattern-settings.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/map/data-layer-pattern-settings.component.ts new file mode 100644 index 0000000000..204e1db59a --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/map/data-layer-pattern-settings.component.ts @@ -0,0 +1,180 @@ +/// +/// Copyright © 2016-2024 The Thingsboard Authors +/// +/// Licensed under the Apache License, Version 2.0 (the "License"); +/// you may not use this file except in compliance with the License. +/// You may obtain a copy of the License at +/// +/// http://www.apache.org/licenses/LICENSE-2.0 +/// +/// Unless required by applicable law or agreed to in writing, software +/// distributed under the License is distributed on an "AS IS" BASIS, +/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +/// See the License for the specific language governing permissions and +/// limitations under the License. +/// + +import { Component, DestroyRef, forwardRef, Input, OnInit } from '@angular/core'; +import { + ControlValueAccessor, + NG_VALIDATORS, + NG_VALUE_ACCESSOR, + UntypedFormBuilder, + UntypedFormControl, + UntypedFormGroup, + Validator, + Validators +} from '@angular/forms'; +import { merge } from 'rxjs'; +import { WidgetService } from '@core/http/widget.service'; +import { takeUntilDestroyed } from '@angular/core/rxjs-interop'; +import { + DataLayerPatternSettings, + DataLayerPatternType, + DataLayerTooltipSettings, dataLayerTooltipTriggers, dataLayerTooltipTriggerTranslationMap +} from '@home/components/widget/lib/maps/map.models'; +import { coerceBoolean } from '@shared/decorators/coercion'; + +@Component({ + selector: 'tb-data-layer-pattern-settings', + templateUrl: './data-layer-pattern-settings.component.html', + styleUrls: ['./../../widget-settings.scss'], + providers: [ + { + provide: NG_VALUE_ACCESSOR, + useExisting: forwardRef(() => DataLayerPatternSettingsComponent), + multi: true + }, + { + provide: NG_VALIDATORS, + useExisting: forwardRef(() => DataLayerPatternSettingsComponent), + multi: true + } + ] +}) +export class DataLayerPatternSettingsComponent implements OnInit, ControlValueAccessor, Validator { + + DataLayerPatternType = DataLayerPatternType; + + dataLayerTooltipTriggers = dataLayerTooltipTriggers; + + dataLayerTooltipTriggerTranslationMap = dataLayerTooltipTriggerTranslationMap; + + settingsExpanded = false; + + functionScopeVariables = this.widgetService.getWidgetScopeVariables(); + + @Input() + disabled: boolean; + + @Input() + patternType: 'label' | 'tooltip' = 'label'; + + @Input() + @coerceBoolean() + hasTooltipOffset = false; + + private modelValue: DataLayerPatternSettings | DataLayerTooltipSettings; + + private propagateChange = null; + + public patternSettingsFormGroup: UntypedFormGroup; + + constructor(private fb: UntypedFormBuilder, + private widgetService: WidgetService, + private destroyRef: DestroyRef) { + } + + ngOnInit(): void { + + this.patternSettingsFormGroup = this.fb.group({ + show: [null, []], + type: [null, []], + pattern: [null, [Validators.required]], + patternFunction: [null, [Validators.required]] + }); + if (this.patternType === 'tooltip') { + this.patternSettingsFormGroup.addControl('trigger', this.fb.control(null, [])); + this.patternSettingsFormGroup.addControl('autoclose', this.fb.control(null, [])); + if (this.hasTooltipOffset) { + this.patternSettingsFormGroup.addControl('offsetX', this.fb.control(null, [])); + this.patternSettingsFormGroup.addControl('offsetY', this.fb.control(null, [])); + } + } + this.patternSettingsFormGroup.valueChanges.pipe( + takeUntilDestroyed(this.destroyRef) + ).subscribe(() => { + this.updateModel(); + }); + merge(this.patternSettingsFormGroup.get('show').valueChanges, + this.patternSettingsFormGroup.get('type').valueChanges + ).pipe( + takeUntilDestroyed(this.destroyRef) + ).subscribe(() => { + this.updateValidators(); + }); + } + + registerOnChange(fn: any): void { + this.propagateChange = fn; + } + + registerOnTouched(_fn: any): void { + } + + setDisabledState(isDisabled: boolean): void { + this.disabled = isDisabled; + if (isDisabled) { + this.patternSettingsFormGroup.disable({emitEvent: false}); + } else { + this.patternSettingsFormGroup.enable({emitEvent: false}); + this.updateValidators(); + } + } + + writeValue(value: DataLayerPatternSettings | DataLayerTooltipSettings): void { + this.modelValue = value; + this.patternSettingsFormGroup.patchValue( + value, {emitEvent: false} + ); + this.updateValidators(); + this.settingsExpanded = this.patternSettingsFormGroup.get('show').value; + this.patternSettingsFormGroup.get('show').valueChanges.pipe( + takeUntilDestroyed(this.destroyRef) + ).subscribe((show) => { + this.settingsExpanded = show; + }); + } + + public validate(c: UntypedFormControl) { + const valid = this.patternSettingsFormGroup.valid; + return valid ? null : { + [this.patternType]: { + valid: false, + }, + }; + } + + private updateValidators() { + const show: boolean = this.patternSettingsFormGroup.get('show').value; + const type: DataLayerPatternType = this.patternSettingsFormGroup.get('type').value; + if (show) { + this.patternSettingsFormGroup.enable({emitEvent: false}); + if (type === DataLayerPatternType.pattern) { + this.patternSettingsFormGroup.get('pattern').enable({emitEvent: false}); + this.patternSettingsFormGroup.get('patternFunction').disable({emitEvent: false}); + } else { + this.patternSettingsFormGroup.get('pattern').disable({emitEvent: false}); + this.patternSettingsFormGroup.get('patternFunction').enable({emitEvent: false}); + } + } else { + this.patternSettingsFormGroup.disable({emitEvent: false}); + this.patternSettingsFormGroup.get('show').enable({emitEvent: false}); + } + } + + private updateModel() { + this.modelValue = this.patternSettingsFormGroup.getRawValue(); + this.propagateChange(this.modelValue); + } +} diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/map/map-data-layer-dialog.component.html b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/map/map-data-layer-dialog.component.html index 711a004e4e..9ecaac60a7 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/map/map-data-layer-dialog.component.html +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/map/map-data-layer-dialog.component.html @@ -34,6 +34,12 @@
+ + datasource.label + +
+ +
+
widgets.maps.data-layer.fill-color
+ +
+
+
widgets.maps.data-layer.stroke
+
+ + + px + + +
+
+
+ + + +
+
DataKey = (key) => key; + private requiredValue: boolean; get required(): boolean { return this.requiredValue || !this.optDataKeys || this.isCountDatasource; @@ -222,7 +242,6 @@ export class DataKeysComponent implements ControlValueAccessor, OnInit, OnChange private keysValidator = this._keysValidator.bind(this); constructor(@SkipSelf() private errorStateMatcher: ErrorStateMatcher, - private datasourceComponent: DatasourceComponent, private translate: TranslateService, private utils: UtilsService, private dialog: MatDialog, @@ -463,8 +482,13 @@ export class DataKeysComponent implements ControlValueAccessor, OnInit, OnChange } private addFromChipValue(chip: DataKey) { - const key = this.callbacks.generateDataKey(chip.name, chip.type, this.dataKeySettingsForm, this.latestDataKeys, - this.datakeySettingsFunction); + let key: DataKey; + if (this.generateKey) { + key = this.generateKey(chip); + } else { + key = this.callbacks.generateDataKey(chip.name, chip.type, this.dataKeySettingsForm, this.latestDataKeys, + this.datakeySettingsFunction); + } this.addKey(key); } @@ -687,7 +711,7 @@ export class DataKeysComponent implements ControlValueAccessor, OnInit, OnChange } get dragDisabled(): boolean { - return this.keys.length < 2; + return this.keys.length < 2 || this.disableDrag; } get maxDataKeysSet(): boolean { diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/map/map-data-layer-dialog.component.html b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/map/map-data-layer-dialog.component.html new file mode 100644 index 0000000000..872b94a20a --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/map/map-data-layer-dialog.component.html @@ -0,0 +1,198 @@ + +
+ +

{{dialogTitle}}

+ + +
+
+
+
+
{{ 'widget-config.datasource' | translate }}
+ + {{ datasourceTypesTranslations.get(type) | translate }} + +
+
+ + + + + + +
+
+
+
{{ 'datakey.keys' | translate }}
+
+
+ + + + +
+ + + + + + +
+
+
+
{{ 'widget-config.appearance' | translate }}
+
+
+
{{ 'widgets.maps.data-layer.groups' | translate }}
+ + +
+
+
+ + +
+
diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/map/map-data-layer-dialog.component.scss b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/map/map-data-layer-dialog.component.scss new file mode 100644 index 0000000000..8fc035a151 --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/map/map-data-layer-dialog.component.scss @@ -0,0 +1,41 @@ +/** + * Copyright © 2016-2024 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +.tb-map-data-layer-dialog { + --mdc-outlined-text-field-outline-color: rgba(0,0,0,0.12); + --mat-form-field-trailing-icon-color: rgba(0,0,0,0.38); + .tb-inline-chips { + --mat-form-field-container-vertical-padding: 12px; + .mat-mdc-text-field-wrapper { + &.mdc-text-field--outlined { + padding-left: 16px; + padding-right: 16px; + .mat-mdc-form-field-infix { + .mdc-evolution-chip-set .mdc-evolution-chip { + margin: 0; + } + .mdc-evolution-chip-set__chips { + gap: 8px; + margin-left: 0; + } + input.mat-mdc-chip-input { + height: 32px; + margin-left: 0; + } + } + } + } + } +} diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/map/map-data-layer-dialog.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/map/map-data-layer-dialog.component.ts new file mode 100644 index 0000000000..9b7ea1b757 --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/map/map-data-layer-dialog.component.ts @@ -0,0 +1,239 @@ +/// +/// Copyright © 2016-2024 The Thingsboard Authors +/// +/// Licensed under the Apache License, Version 2.0 (the "License"); +/// you may not use this file except in compliance with the License. +/// You may obtain a copy of the License at +/// +/// http://www.apache.org/licenses/LICENSE-2.0 +/// +/// Unless required by applicable law or agreed to in writing, software +/// distributed under the License is distributed on an "AS IS" BASIS, +/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +/// See the License for the specific language governing permissions and +/// limitations under the License. +/// + +import { Component, DestroyRef, Inject, ViewEncapsulation } from '@angular/core'; +import { DialogComponent } from '@shared/components/dialog.component'; +import { + CirclesDataLayerSettings, + MapDataLayerSettings, + MapDataLayerType, + MapType, + MarkersDataLayerSettings, + PolygonsDataLayerSettings +} from '@home/components/widget/lib/maps/map.models'; +import { Store } from '@ngrx/store'; +import { AppState } from '@core/core.state'; +import { Router } from '@angular/router'; +import { MAT_DIALOG_DATA, MatDialogRef } from '@angular/material/dialog'; +import { FormBuilder, UntypedFormGroup, Validators } from '@angular/forms'; +import { DataKey, DatasourceType, datasourceTypeTranslationMap, widgetType } from '@shared/models/widget.models'; +import { DataKeyType } from '@shared/models/telemetry/telemetry.models'; +import { takeUntilDestroyed } from '@angular/core/rxjs-interop'; +import { EntityType } from '@shared/models/entity-type.models'; +import { MapSettingsContext } from '@home/components/widget/lib/settings/common/map/map-settings.component.models'; +import { genNextLabelForDataKeys } from '@core/utils'; + +export interface MapDataLayerDialogData { + settings: MapDataLayerSettings; + mapType: MapType; + dataLayerType: MapDataLayerType; + context: MapSettingsContext; +} + +@Component({ + selector: 'tb-map-data-layer-dialog', + templateUrl: './map-data-layer-dialog.component.html', + styleUrls: ['./map-data-layer-dialog.component.scss'], + encapsulation: ViewEncapsulation.None +}) +export class MapDataLayerDialogComponent extends DialogComponent { + + DatasourceType = DatasourceType; + + EntityType = EntityType; + + MapType = MapType; + + DataKeyType = DataKeyType; + + widgetType = widgetType; + + datasourceTypes: Array = []; + datasourceTypesTranslations = datasourceTypeTranslationMap; + + dataLayerFormGroup: UntypedFormGroup; + + settings = this.data.settings; + mapType = this.data.mapType; + dataLayerType = this.data.dataLayerType; + context = this.data.context; + + generateAdditionalDataKey = this.generateDataKey.bind(this); + + dialogTitle: string; + + constructor(protected store: Store, + protected router: Router, + @Inject(MAT_DIALOG_DATA) public data: MapDataLayerDialogData, + public dialogRef: MatDialogRef, + private fb: FormBuilder, + private destroyRef: DestroyRef) { + super(store, router, dialogRef); + + if (this.context.functionsOnly) { + this.datasourceTypes = [DatasourceType.function]; + } else { + this.datasourceTypes = [DatasourceType.function, DatasourceType.device, DatasourceType.entity]; + } + + this.dataLayerFormGroup = this.fb.group({ + dsType: [this.settings.dsType, [Validators.required]], + dsDeviceId: [this.settings.dsDeviceId, [Validators.required]], + dsEntityAliasId: [this.settings.dsEntityAliasId, [Validators.required]], + dsFilterId: [this.settings.dsFilterId, []], + additionalDataKeys: [this.settings.additionalDataKeys, []], + groups: [this.settings.groups, []] + }); + + switch (this.dataLayerType) { + case 'markers': + const markersDataLayer = this.settings as MarkersDataLayerSettings; + this.dialogTitle = 'widgets.maps.data-layer.marker.marker-configuration'; + this.dataLayerFormGroup.addControl('xKey', this.fb.control(markersDataLayer.xKey, Validators.required)); + this.dataLayerFormGroup.addControl('yKey', this.fb.control(markersDataLayer.yKey, Validators.required)); + break; + case 'polygons': + const polygonsDataLayer = this.settings as PolygonsDataLayerSettings; + this.dialogTitle = 'widgets.maps.data-layer.polygon.polygon-configuration'; + this.dataLayerFormGroup.addControl('polygonKey', this.fb.control(polygonsDataLayer.polygonKey, Validators.required)); + break; + case 'circles': + const circlesDataLayer = this.settings as CirclesDataLayerSettings; + this.dialogTitle = 'widgets.maps.data-layer.circle.circle-configuration'; + this.dataLayerFormGroup.addControl('circleKey', this.fb.control(circlesDataLayer.circleKey, Validators.required)); + break; + } + this.dataLayerFormGroup.get('dsType').valueChanges.pipe( + takeUntilDestroyed(this.destroyRef) + ).subscribe( + (newDsType: DatasourceType) => this.onDsTypeChanged(newDsType) + ); + this.updateValidators(); + } + + private onDsTypeChanged(newDsType: DatasourceType) { + switch (this.dataLayerType) { + case 'markers': + const xKey: DataKey = this.dataLayerFormGroup.get('xKey').value; + if (this.updateDataKeyToNewDsType(xKey, newDsType)) { + this.dataLayerFormGroup.get('xKey').patchValue(xKey, {emitEvent: false}); + } + const yKey: DataKey = this.dataLayerFormGroup.get('yKey').value; + if (this.updateDataKeyToNewDsType(yKey, newDsType)) { + this.dataLayerFormGroup.get('yKey').patchValue(yKey, {emitEvent: false}); + } + break; + case 'polygons': + const polygonKey: DataKey = this.dataLayerFormGroup.get('polygonKey').value; + if (this.updateDataKeyToNewDsType(polygonKey, newDsType)) { + this.dataLayerFormGroup.get('polygonKey').patchValue(polygonKey, {emitEvent: false}); + } + break; + case 'circles': + const circleKey: DataKey = this.dataLayerFormGroup.get('circleKey').value; + if (this.updateDataKeyToNewDsType(circleKey, newDsType)) { + this.dataLayerFormGroup.get('circleKey').patchValue(circleKey, {emitEvent: false}); + } + break; + } + this.updateValidators(); + } + + private updateDataKeyToNewDsType(dataKey: DataKey, newDsType: DatasourceType): boolean { + if (newDsType === DatasourceType.function) { + if (dataKey.type !== DataKeyType.function) { + dataKey.type = DataKeyType.function; + return true; + } + } else { + if (dataKey.type === DataKeyType.function) { + dataKey.type = DataKeyType.attribute; + return true; + } + } + return false; + } + + private updateValidators() { + const dsType: DatasourceType = this.dataLayerFormGroup.get('dsType').value; + if (dsType === DatasourceType.function) { + this.dataLayerFormGroup.get('dsDeviceId').disable({emitEvent: false}); + this.dataLayerFormGroup.get('dsEntityAliasId').disable({emitEvent: false}); + } else if (dsType === DatasourceType.device) { + this.dataLayerFormGroup.get('dsDeviceId').enable({emitEvent: false}); + this.dataLayerFormGroup.get('dsEntityAliasId').disable({emitEvent: false}); + } else { + this.dataLayerFormGroup.get('dsDeviceId').disable({emitEvent: false}); + this.dataLayerFormGroup.get('dsEntityAliasId').enable({emitEvent: false}); + } + } + + editKey(keyType: 'xKey' | 'yKey' | 'polygonKey' | 'circleKey') { + const targetDataKey: DataKey = this.dataLayerFormGroup.get(keyType).value; + this.context.editKey(targetDataKey, + this.dataLayerFormGroup.get('dsDeviceId').value, this.dataLayerFormGroup.get('dsEntityAliasId').value).subscribe( + (updatedDataKey) => { + if (updatedDataKey) { + this.dataLayerFormGroup.get(keyType).patchValue(updatedDataKey); + } + } + ); + } + + private generateDataKey(key: DataKey): DataKey { + const dataKey = this.context.callbacks.generateDataKey(key.name, key.type, null, false, null); + const dataKeys: DataKey[] = []; + switch (this.dataLayerType) { + case 'markers': + const xKey: DataKey = this.dataLayerFormGroup.get('xKey').value; + if (xKey) { + dataKeys.push(xKey); + } + const yKey: DataKey = this.dataLayerFormGroup.get('yKey').value; + if (yKey) { + dataKeys.push(yKey); + } + break; + case 'polygons': + const polygonKey: DataKey = this.dataLayerFormGroup.get('polygonKey').value; + if (polygonKey) { + dataKeys.push(polygonKey); + } + break; + case 'circles': + const circleKey: DataKey = this.dataLayerFormGroup.get('circleKey').value; + if (circleKey) { + dataKeys.push(circleKey); + } + break; + } + const additionalKeys: DataKey[] = this.dataLayerFormGroup.get('additionalDataKeys').value; + if (additionalKeys) { + dataKeys.push(...additionalKeys); + } + dataKey.label = genNextLabelForDataKeys(dataKey.label, dataKeys); + return dataKey; + } + + cancel(): void { + this.dialogRef.close(null); + } + + save(): void { + const settings: MapDataLayerSettings = this.dataLayerFormGroup.getRawValue(); + this.dialogRef.close(settings); + } +} diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/map/map-data-layer-row.component.html b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/map/map-data-layer-row.component.html index fcbda3bead..e55df4c8f9 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/map/map-data-layer-row.component.html +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/map/map-data-layer-row.component.html @@ -38,9 +38,9 @@ *ngIf="dataLayerFormGroup.get('dsType').value === DatasourceType.entity" inlineField tbRequired - [aliasController]="aliasController" + [aliasController]="context.aliasController" formControlName="dsEntityAliasId" - [callbacks]="entityAliasSelectCallbacks"> + [callbacks]="context.callbacks">
@@ -67,11 +68,12 @@ [datasourceType]="dataLayerFormGroup.get('dsType').value" [entityAliasId]="dataLayerFormGroup.get('dsEntityAliasId').value" [deviceId]="dataLayerFormGroup.get('dsDeviceId').value" - [aliasController]="aliasController" - [dataKeyType]="functionsOnly ? DataKeyType.function : null" + [aliasController]="context.aliasController" + [widgetType]="widgetType.latest" + [dataKeyType]="context.functionsOnly ? DataKeyType.function : null" [dataKeyTypes]="[DataKeyType.attribute, DataKeyType.timeseries]" - [callbacks]="dataKeyCallbacks" - [generateKey]="generateDataKey" + [callbacks]="context.callbacks" + [generateKey]="context.generateDataKey" (keyEdit)="editKey('yKey')" formControlName="yKey"> @@ -83,11 +85,12 @@ [datasourceType]="dataLayerFormGroup.get('dsType').value" [entityAliasId]="dataLayerFormGroup.get('dsEntityAliasId').value" [deviceId]="dataLayerFormGroup.get('dsDeviceId').value" - [aliasController]="aliasController" - [dataKeyType]="functionsOnly ? DataKeyType.function : null" + [aliasController]="context.aliasController" + [widgetType]="widgetType.latest" + [dataKeyType]="context.functionsOnly ? DataKeyType.function : null" [dataKeyTypes]="[DataKeyType.attribute, DataKeyType.timeseries]" - [callbacks]="dataKeyCallbacks" - [generateKey]="generateDataKey" + [callbacks]="context.callbacks" + [generateKey]="context.generateDataKey" (keyEdit)="editKey('polygonKey')" formControlName="polygonKey"> @@ -99,11 +102,12 @@ [datasourceType]="dataLayerFormGroup.get('dsType').value" [entityAliasId]="dataLayerFormGroup.get('dsEntityAliasId').value" [deviceId]="dataLayerFormGroup.get('dsDeviceId').value" - [aliasController]="aliasController" - [dataKeyType]="functionsOnly ? DataKeyType.function : null" + [aliasController]="context.aliasController" + [widgetType]="widgetType.latest" + [dataKeyType]="context.functionsOnly ? DataKeyType.function : null" [dataKeyTypes]="[DataKeyType.attribute, DataKeyType.timeseries]" - [callbacks]="dataKeyCallbacks" - [generateKey]="generateDataKey" + [callbacks]="context.callbacks" + [generateKey]="context.generateDataKey" (keyEdit)="editKey('circleKey')" formControlName="circleKey"> diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/map/map-data-layer-row.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/map/map-data-layer-row.component.ts index 1d9d2014e2..814796cf09 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/map/map-data-layer-row.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/map/map-data-layer-row.component.ts @@ -23,8 +23,6 @@ import { Input, OnInit, Output, - Renderer2, - ViewContainerRef, ViewEncapsulation } from '@angular/core'; import { @@ -35,8 +33,6 @@ import { Validators } from '@angular/forms'; import { MatButton } from '@angular/material/button'; -import { TbPopoverService } from '@shared/components/popover.service'; -import { TranslateService } from '@ngx-translate/core'; import { takeUntilDestroyed } from '@angular/core/rxjs-interop'; import { CirclesDataLayerSettings, @@ -46,27 +42,17 @@ import { MarkersDataLayerSettings, PolygonsDataLayerSettings } from '@home/components/widget/lib/maps/map.models'; -import { - DataKey, - DataKeyConfigMode, - DatasourceType, - datasourceTypeTranslationMap, - widgetType -} from '@shared/models/widget.models'; +import { DataKey, DatasourceType, datasourceTypeTranslationMap, widgetType } from '@shared/models/widget.models'; import { EntityType } from '@shared/models/entity-type.models'; import { DataKeyType } from '@shared/models/telemetry/telemetry.models'; import { MapSettingsComponent } from '@home/components/widget/lib/settings/common/map/map-settings.component'; -import { IAliasController } from '@core/api/widget-api.models'; -import { DataKeysCallbacks } from '@home/components/widget/config/data-keys.component.models'; -import { - DataKeyConfigDialogComponent, - DataKeyConfigDialogData -} from '@home/components/widget/config/data-key-config-dialog.component'; import { deepClone } from '@core/utils'; import { MatDialog } from '@angular/material/dialog'; import { - EntityAliasSelectCallbacks -} from '@home/components/widget/lib/settings/common/alias/entity-alias-select.component.models'; + MapDataLayerDialogComponent, + MapDataLayerDialogData +} from '@home/components/widget/lib/settings/common/map/map-data-layer-dialog.component'; +import { MapSettingsContext } from '@home/components/widget/lib/settings/common/map/map-settings.component.models'; @Component({ selector: 'tb-map-data-layer-row', @@ -90,6 +76,8 @@ export class MapDataLayerRowComponent implements ControlValueAccessor, OnInit { MapType = MapType; + widgetType = widgetType; + datasourceTypes: Array = []; datasourceTypesTranslations = datasourceTypeTranslationMap; @@ -102,27 +90,12 @@ export class MapDataLayerRowComponent implements ControlValueAccessor, OnInit { @Input() dataLayerType: MapDataLayerType = 'markers'; - get functionsOnly(): boolean { - return this.mapSettingsComponent.functionsOnly; - } - - get aliasController(): IAliasController { - return this.mapSettingsComponent.aliasController; - } - - get dataKeyCallbacks(): DataKeysCallbacks { - return this.mapSettingsComponent.callbacks; - } - - public get entityAliasSelectCallbacks(): EntityAliasSelectCallbacks { - return this.mapSettingsComponent.callbacks; - } + @Input() + context: MapSettingsContext; @Output() dataLayerRemoved = new EventEmitter(); - generateDataKey = this._generateDataKey.bind(this); - dataLayerFormGroup: UntypedFormGroup; modelValue: MapDataLayerSettings; @@ -136,16 +109,12 @@ export class MapDataLayerRowComponent implements ControlValueAccessor, OnInit { constructor(private mapSettingsComponent: MapSettingsComponent, private fb: UntypedFormBuilder, private dialog: MatDialog, - private translate: TranslateService, - private popoverService: TbPopoverService, - private renderer: Renderer2, - private viewContainerRef: ViewContainerRef, private cd: ChangeDetectorRef, private destroyRef: DestroyRef) { } ngOnInit() { - if (this.functionsOnly) { + if (this.context.functionsOnly) { this.datasourceTypes = [DatasourceType.function]; } else { this.datasourceTypes = [DatasourceType.function, DatasourceType.device, DatasourceType.entity]; @@ -244,66 +213,37 @@ export class MapDataLayerRowComponent implements ControlValueAccessor, OnInit { editKey(keyType: 'xKey' | 'yKey' | 'polygonKey' | 'circleKey') { const targetDataKey: DataKey = this.dataLayerFormGroup.get(keyType).value; - this.dialog.open(DataKeyConfigDialogComponent, - { - disableClose: true, - panelClass: ['tb-dialog', 'tb-fullscreen-dialog'], - data: { - dataKey: deepClone(targetDataKey), - dataKeyConfigMode: DataKeyConfigMode.general, - aliasController: this.aliasController, - widgetType: widgetType.latest, - deviceId: this.dataLayerFormGroup.get('dsDeviceId').value, - entityAliasId: this.dataLayerFormGroup.get('dsEntityAliasId').value, - showPostProcessing: true, - callbacks: this.mapSettingsComponent.callbacks, - hideDataKeyColor: true, - hideDataKeyDecimals: true, - hideDataKeyUnits: true, - widget: this.mapSettingsComponent.widget, - dashboard: null, - dataKeySettingsForm: null, - dataKeySettingsDirective: null + this.context.editKey(targetDataKey, + this.dataLayerFormGroup.get('dsDeviceId').value, this.dataLayerFormGroup.get('dsEntityAliasId').value).subscribe( + (updatedDataKey) => { + if (updatedDataKey) { + this.dataLayerFormGroup.get(keyType).patchValue(updatedDataKey); } - }).afterClosed().subscribe((updatedDataKey) => { - if (updatedDataKey) { - this.dataLayerFormGroup.get(keyType).patchValue(updatedDataKey); } - }); + ); } editDataLayer($event: Event, matButton: MatButton) { if ($event) { $event.stopPropagation(); } - const trigger = matButton._elementRef.nativeElement; - if (this.popoverService.hasPopover(trigger)) { - this.popoverService.hidePopover(trigger); - } else { - /*const ctx: any = { - mapLayerSettings: deepClone(this.modelValue) - }; - const mapLayerSettingsPanelPopover = this.popoverService.displayPopover(trigger, this.renderer, - this.viewContainerRef, MapLayerSettingsPanelComponent, ['leftOnly', 'leftTopOnly', 'leftBottomOnly'], true, null, - ctx, - {}, - {}, {}, true); - mapLayerSettingsPanelPopover.tbComponentRef.instance.popover = mapLayerSettingsPanelPopover; - mapLayerSettingsPanelPopover.tbComponentRef.instance.mapLayerSettingsApplied.subscribe((layer) => { - mapLayerSettingsPanelPopover.hide(); - this.layerFormGroup.patchValue( - layer, - {emitEvent: false}); + this.dialog.open(MapDataLayerDialogComponent, { + disableClose: true, + panelClass: ['tb-dialog', 'tb-fullscreen-dialog'], + data: { + settings: deepClone(this.modelValue), + mapType: this.mapType, + dataLayerType: this.dataLayerType, + context: this.context + } + }).afterClosed().subscribe((settings) => { + if (settings) { + this.modelValue = settings; + this.dataLayerFormGroup.patchValue(settings); this.updateValidators(); - this.updateModel(); - });*/ - } - } - - private _generateDataKey(key: DataKey): DataKey { - key = this.dataKeyCallbacks.generateDataKey(key.name, key.type, null, false, - null); - return key; + } + }); } private onDsTypeChanged(newDsType: DatasourceType) { @@ -375,6 +315,4 @@ export class MapDataLayerRowComponent implements ControlValueAccessor, OnInit { this.modelValue = {...this.modelValue, ...this.dataLayerFormGroup.value}; this.propagateChange(this.modelValue); } - - protected readonly datasourceType = DatasourceType; } diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/map/map-data-layers.component.html b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/map/map-data-layers.component.html index 0c19ff6a9b..02bd08e7bd 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/map/map-data-layers.component.html +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/map/map-data-layers.component.html @@ -34,6 +34,7 @@ diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/map/map-data-layers.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/map/map-data-layers.component.ts index 5c8aa5534d..09d3c0a2dc 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/map/map-data-layers.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/map/map-data-layers.component.ts @@ -37,6 +37,7 @@ import { MapType } from '@home/components/widget/lib/maps/map.models'; import { MapSettingsComponent } from '@home/components/widget/lib/settings/common/map/map-settings.component'; +import { MapSettingsContext } from '@home/components/widget/lib/settings/common/map/map-settings.component.models'; @Component({ selector: 'tb-map-data-layers', @@ -69,9 +70,8 @@ export class MapDataLayersComponent implements ControlValueAccessor, OnInit, Val @Input() dataLayerType: MapDataLayerType = 'markers'; - get functionsOnly(): boolean { - return this.mapSettingsComponent.functionsOnly; - } + @Input() + context: MapSettingsContext; dataLayersFormGroup: UntypedFormGroup; @@ -161,7 +161,7 @@ export class MapDataLayersComponent implements ControlValueAccessor, OnInit, Val addDataLayer() { const dataLayer = mergeDeep({} as MapDataLayerSettings, - defaultMapDataLayerSettings(this.mapType, this.dataLayerType, this.functionsOnly)); + defaultMapDataLayerSettings(this.mapType, this.dataLayerType, this.context.functionsOnly)); const dataLayersArray = this.dataLayersFormGroup.get('dataLayers') as UntypedFormArray; const dataLayerControl = this.fb.control(dataLayer, [mapDataLayerValidator(this.dataLayerType)]); dataLayersArray.push(dataLayerControl); 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 9652daaf7b..dd0b91b11e 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 @@ -44,14 +44,17 @@
diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/map/map-settings.component.models.ts b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/map/map-settings.component.models.ts new file mode 100644 index 0000000000..931a424ca3 --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/map/map-settings.component.models.ts @@ -0,0 +1,29 @@ +/// +/// Copyright © 2016-2024 The Thingsboard Authors +/// +/// Licensed under the Apache License, Version 2.0 (the "License"); +/// you may not use this file except in compliance with the License. +/// You may obtain a copy of the License at +/// +/// http://www.apache.org/licenses/LICENSE-2.0 +/// +/// Unless required by applicable law or agreed to in writing, software +/// distributed under the License is distributed on an "AS IS" BASIS, +/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +/// See the License for the specific language governing permissions and +/// limitations under the License. +/// + +import { IAliasController } from '@core/api/widget-api.models'; +import { WidgetConfigCallbacks } from '@home/components/widget/config/widget-config.component.models'; +import { DataKey, Widget } from '@shared/models/widget.models'; +import { Observable } from 'rxjs'; + +export interface MapSettingsContext { + functionsOnly: boolean; + aliasController: IAliasController; + callbacks: WidgetConfigCallbacks; + widget: Widget; + editKey: (key: DataKey, deviceId: string, entityAliasId: string) => Observable; + generateDataKey: (key: DataKey) => DataKey; +} diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/map/map-settings.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/map/map-settings.component.ts index 864a0f3ea9..ece61c0992 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/map/map-settings.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/map/map-settings.component.ts @@ -19,17 +19,25 @@ import { ControlValueAccessor, NG_VALIDATORS, NG_VALUE_ACCESSOR, - UntypedFormBuilder, UntypedFormControl, - UntypedFormGroup, Validator + UntypedFormBuilder, + UntypedFormControl, + UntypedFormGroup, + Validator } from '@angular/forms'; import { ImageSourceType, MapDataLayerType, MapSetting, MapType } from '@home/components/widget/lib/maps/map.models'; import { takeUntilDestroyed } from '@angular/core/rxjs-interop'; -import { merge } from 'rxjs'; +import { merge, Observable } from 'rxjs'; import { coerceBoolean } from '@shared/decorators/coercion'; import { IAliasController } from '@core/api/widget-api.models'; -import { DataKeysCallbacks } from '@home/components/widget/config/data-keys.component.models'; import { WidgetConfigCallbacks } from '@home/components/widget/config/widget-config.component.models'; -import { Widget } from '@shared/models/widget.models'; +import { DataKey, DataKeyConfigMode, Widget, widgetType } from '@shared/models/widget.models'; +import { MapSettingsContext } from '@home/components/widget/lib/settings/common/map/map-settings.component.models'; +import { + DataKeyConfigDialogComponent, + DataKeyConfigDialogData +} from '@home/components/widget/lib/settings/common/key/data-key-config-dialog.component'; +import { deepClone } from '@core/utils'; +import { MatDialog } from '@angular/material/dialog'; @Component({ selector: 'tb-map-settings', @@ -68,6 +76,8 @@ export class MapSettingsComponent implements OnInit, ControlValueAccessor, Valid @Input() widget: Widget; + context: MapSettingsContext; + private modelValue: MapSetting; private propagateChange = null; @@ -77,10 +87,21 @@ export class MapSettingsComponent implements OnInit, ControlValueAccessor, Valid dataLayerMode: MapDataLayerType = 'markers'; constructor(private fb: UntypedFormBuilder, + private dialog: MatDialog, private destroyRef: DestroyRef) { } ngOnInit(): void { + + this.context = { + functionsOnly: this.functionsOnly, + aliasController: this.aliasController, + callbacks: this.callbacks, + widget: this.widget, + editKey: this.editKey.bind(this), + generateDataKey: this.generateDataKey.bind(this) + }; + this.mapSettingsFormGroup = this.fb.group({ mapType: [null, []], layers: [null, []], @@ -176,4 +197,33 @@ export class MapSettingsComponent implements OnInit, ControlValueAccessor, Valid this.modelValue = this.mapSettingsFormGroup.getRawValue(); this.propagateChange(this.modelValue); } + + private editKey(key: DataKey, deviceId: string, entityAliasId: string): Observable { + return this.dialog.open(DataKeyConfigDialogComponent, + { + disableClose: true, + panelClass: ['tb-dialog', 'tb-fullscreen-dialog'], + data: { + dataKey: deepClone(key), + dataKeyConfigMode: DataKeyConfigMode.general, + aliasController: this.aliasController, + widgetType: widgetType.latest, + deviceId, + entityAliasId, + showPostProcessing: true, + callbacks: this.callbacks, + hideDataKeyColor: true, + hideDataKeyDecimals: true, + hideDataKeyUnits: true, + widget: this.widget, + dashboard: null, + dataKeySettingsForm: null, + dataKeySettingsDirective: null + } + }).afterClosed(); + } + + private generateDataKey(key: DataKey): DataKey { + return this.callbacks.generateDataKey(key.name, key.type, null, false, null); + } } diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/value-source-data-key.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/value-source-data-key.component.ts index a541291b08..807e38019b 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/value-source-data-key.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/value-source-data-key.component.ts @@ -29,7 +29,7 @@ import { AppState } from '@core/core.state'; import { IAliasController } from '@core/api/widget-api.models'; import { DataKeyType } from '@shared/models/telemetry/telemetry.models'; import { DataKey, Datasource, DatasourceType } from '@app/shared/models/widget.models'; -import { DataKeysCallbacks } from '@home/components/widget/config/data-keys.component.models'; +import { DataKeysCallbacks } from '@home/components/widget/lib/settings/common/key/data-keys.component.models'; import { ValueSourceConfig, ValueSourceType, 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 7018862d08..2416b32ade 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 @@ -102,7 +102,7 @@ import { import { TimeSeriesChartThresholdRowComponent } from '@home/components/widget/lib/settings/common/chart/time-series-chart-threshold-row.component'; -import { DataKeyInputComponent } from '@home/components/widget/lib/settings/common/data-key-input.component'; +import { DataKeyInputComponent } from '@home/components/widget/lib/settings/common/key/data-key-input.component'; import { EntityAliasInputComponent } from '@home/components/widget/lib/settings/common/entity-alias-input.component'; import { TimeSeriesChartThresholdSettingsPanelComponent @@ -191,10 +191,19 @@ import { } from '@home/components/widget/lib/settings/common/map/map-layer-settings-panel.component'; import { MapDataLayersComponent } from '@home/components/widget/lib/settings/common/map/map-data-layers.component'; import { MapDataLayerRowComponent } from '@home/components/widget/lib/settings/common/map/map-data-layer-row.component'; -import { WidgetConfigComponentsModule } from '@home/components/widget/config/widget-config-components.module'; import { EntityAliasSelectComponent } from '@home/components/widget/lib/settings/common/alias/entity-alias-select.component'; +import { + MapDataLayerDialogComponent +} from '@home/components/widget/lib/settings/common/map/map-data-layer-dialog.component'; +import { FilterSelectComponent } from '@home/components/widget/lib/settings/common/filter/filter-select.component'; +import { DataKeysComponent } from '@home/components/widget/lib/settings/common/key/data-keys.component'; +import { + DataKeyConfigDialogComponent +} from '@home/components/widget/lib/settings/common/key/data-key-config-dialog.component'; +import { DataKeyConfigComponent } from '@home/components/widget/lib/settings/common/key/data-key-config.component'; +import { WidgetSettingsComponent } from '@home/components/widget/lib/settings/common/widget/widget-settings.component'; @NgModule({ declarations: [ @@ -268,10 +277,16 @@ import { MapLayerSettingsPanelComponent, MapLayerRowComponent, MapLayersComponent, + MapDataLayerDialogComponent, MapDataLayerRowComponent, MapDataLayersComponent, MapSettingsComponent, - EntityAliasSelectComponent + EntityAliasSelectComponent, + FilterSelectComponent, + DataKeysComponent, + DataKeyConfigDialogComponent, + DataKeyConfigComponent, + WidgetSettingsComponent ], imports: [ CommonModule, @@ -347,7 +362,12 @@ import { DynamicFormComponent, DynamicFormArrayComponent, MapSettingsComponent, - EntityAliasSelectComponent + EntityAliasSelectComponent, + FilterSelectComponent, + DataKeysComponent, + DataKeyConfigDialogComponent, + DataKeyConfigComponent, + WidgetSettingsComponent ], providers: [ ColorSettingsComponentService, diff --git a/ui-ngx/src/app/modules/home/components/widget/config/widget-settings.component.html b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/widget/widget-settings.component.html similarity index 100% rename from ui-ngx/src/app/modules/home/components/widget/config/widget-settings.component.html rename to ui-ngx/src/app/modules/home/components/widget/lib/settings/common/widget/widget-settings.component.html diff --git a/ui-ngx/src/app/modules/home/components/widget/config/widget-settings.component.scss b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/widget/widget-settings.component.scss similarity index 100% rename from ui-ngx/src/app/modules/home/components/widget/config/widget-settings.component.scss rename to ui-ngx/src/app/modules/home/components/widget/lib/settings/common/widget/widget-settings.component.scss diff --git a/ui-ngx/src/app/modules/home/components/widget/config/widget-settings.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/widget/widget-settings.component.ts similarity index 100% rename from ui-ngx/src/app/modules/home/components/widget/config/widget-settings.component.ts rename to ui-ngx/src/app/modules/home/components/widget/lib/settings/common/widget/widget-settings.component.ts diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/control/device-key-autocomplete.component.html b/ui-ngx/src/app/modules/home/components/widget/lib/settings/control/device-key-autocomplete.component.html index 668613012e..ae42e2df2f 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/settings/control/device-key-autocomplete.component.html +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/control/device-key-autocomplete.component.html @@ -15,11 +15,10 @@ limitations under the License. --> - {{ (keyType === dataKeyType.attribute ? attributeLabel diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/gauge/tick-value.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/settings/gauge/tick-value.component.ts index 200c4d749a..5c7c2f6052 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/settings/gauge/tick-value.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/gauge/tick-value.component.ts @@ -22,7 +22,7 @@ import { Store } from '@ngrx/store'; import { AppState } from '@core/core.state'; import { TranslateService } from '@ngx-translate/core'; import { IAliasController } from '@core/api/widget-api.models'; -import { DataKeysCallbacks } from '@home/components/widget/config/data-keys.component.models'; +import { DataKeysCallbacks } from '@home/components/widget/lib/settings/common/key/data-keys.component.models'; import { Datasource } from '@shared/models/widget.models'; import { takeUntilDestroyed } from '@angular/core/rxjs-interop'; diff --git a/ui-ngx/src/app/modules/home/components/widget/widget-config.component.ts b/ui-ngx/src/app/modules/home/components/widget/widget-config.component.ts index 78dd827403..215a33cd4a 100644 --- a/ui-ngx/src/app/modules/home/components/widget/widget-config.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/widget-config.component.ts @@ -81,7 +81,7 @@ import { ToggleHeaderOption } from '@shared/components/toggle-header.component'; import { coerceBoolean } from '@shared/decorators/coercion'; import { basicWidgetConfigComponentsMap } from '@home/components/widget/config/basic/basic-widget-config.module'; import { TimewindowConfigData } from '@home/components/widget/config/timewindow-config-panel.component'; -import { DataKeySettingsFunction } from '@home/components/widget/config/data-keys.component.models'; +import { DataKeySettingsFunction } from '@home/components/widget/lib/settings/common/key/data-keys.component.models'; import { defaultFormProperties, FormProperty } from '@shared/models/dynamic-form.models'; import { takeUntilDestroyed } from '@angular/core/rxjs-interop'; import Timeout = NodeJS.Timeout; diff --git a/ui-ngx/src/app/modules/home/models/widget-component.models.ts b/ui-ngx/src/app/modules/home/models/widget-component.models.ts index b1958cb3db..c599fbf16d 100644 --- a/ui-ngx/src/app/modules/home/models/widget-component.models.ts +++ b/ui-ngx/src/app/modules/home/models/widget-component.models.ts @@ -102,7 +102,7 @@ import { MillisecondsToTimeStringPipe } from '@shared/pipe/milliseconds-to-time- import { SharedTelemetrySubscriber, TelemetrySubscriber } from '@shared/models/telemetry/telemetry.models'; import { UserId } from '@shared/models/id/user-id'; import { UserSettingsService } from '@core/http/user-settings.service'; -import { DataKeySettingsFunction } from '@home/components/widget/config/data-keys.component.models'; +import { DataKeySettingsFunction } from '@home/components/widget/lib/settings/common/key/data-keys.component.models'; import { UtilsService } from '@core/services/utils.service'; import { CompiledTbFunction } from '@shared/models/js-function.models'; import { FormProperty } from '@shared/models/dynamic-form.models'; 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 eb4c15fc18..cf8ba1d150 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 @@ -16,7 +16,7 @@ --> diff --git a/ui-ngx/src/app/shared/components/string-items-list.component.scss b/ui-ngx/src/app/shared/components/string-items-list.component.scss new file mode 100644 index 0000000000..70e487f998 --- /dev/null +++ b/ui-ngx/src/app/shared/components/string-items-list.component.scss @@ -0,0 +1,22 @@ +/** + * Copyright © 2016-2024 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +.tb-string-items-list { + .mat-mdc-standard-chip { + .mdc-evolution-chip__cell--primary, .mat-mdc-chip-action-label { + overflow: hidden; + } + } +} 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 4121a7e8d7..b1447825ed 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,7 @@ /// limitations under the License. /// -import { Component, ElementRef, forwardRef, Input, OnInit, ViewChild } from '@angular/core'; +import { Component, ElementRef, forwardRef, Input, OnInit, ViewChild, ViewEncapsulation } from '@angular/core'; import { AbstractControl, ControlValueAccessor, @@ -38,14 +38,15 @@ export interface StringItemsOption { @Component({ selector: 'tb-string-items-list', templateUrl: './string-items-list.component.html', - styleUrls: [], + styleUrls: ['./string-items-list.component.scss'], providers: [ { provide: NG_VALUE_ACCESSOR, useExisting: forwardRef(() => StringItemsListComponent), multi: true } - ] + ], + encapsulation: ViewEncapsulation.None }) export class StringItemsListComponent implements ControlValueAccessor, OnInit { diff --git a/ui-ngx/src/app/shared/models/widget.models.ts b/ui-ngx/src/app/shared/models/widget.models.ts index bbb462d9af..8b6558a11f 100644 --- a/ui-ngx/src/app/shared/models/widget.models.ts +++ b/ui-ngx/src/app/shared/models/widget.models.ts @@ -42,7 +42,7 @@ import { WidgetConfigComponentData } from '@home/models/widget-component.models' import { ComponentStyle, Font, TimewindowStyle } from '@shared/models/widget-settings.models'; import { NULL_UUID } from '@shared/models/id/has-uuid'; import { HasTenantId, HasVersion } from '@shared/models/entity.models'; -import { DataKeysCallbacks, DataKeySettingsFunction } from '@home/components/widget/config/data-keys.component.models'; +import { DataKeysCallbacks, DataKeySettingsFunction } from '@home/components/widget/lib/settings/common/key/data-keys.component.models'; import { WidgetConfigCallbacks } from '@home/components/widget/config/widget-config.component.models'; import { TbFunction } from '@shared/models/js-function.models'; import { FormProperty, jsonFormSchemaToFormProperties } from '@shared/models/dynamic-form.models'; 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 946f338ddd..7ac6789d6b 100644 --- a/ui-ngx/src/assets/locale/locale.constant-en_US.json +++ b/ui-ngx/src/assets/locale/locale.constant-en_US.json @@ -1349,6 +1349,7 @@ "general": "General", "advanced": "Advanced", "key": "Key", + "keys": "Keys", "label": "Label", "color": "Color", "units": "Special symbol to show next to value", @@ -6869,6 +6870,8 @@ }, "data-layer": { "source": "Source", + "additional-data-keys": "Additional data keys", + "groups": "Groups", "marker": { "latitude-key": "Latitude key", "longitude-key": "Longitude key", diff --git a/ui-ngx/src/typings/leaflet-extend-tb.d.ts b/ui-ngx/src/typings/leaflet-extend-tb.d.ts index 9b8f357c18..d1ba09b66c 100644 --- a/ui-ngx/src/typings/leaflet-extend-tb.d.ts +++ b/ui-ngx/src/typings/leaflet-extend-tb.d.ts @@ -39,7 +39,7 @@ declare module 'leaflet' { class SidebarControl extends Control { constructor(options: SidebarControlOptions); - addPane(pane: JQuery): this; + addPane(pane: JQuery, button: JQuery): this; togglePane(pane: JQuery, button: JQuery): void; } @@ -70,12 +70,33 @@ declare module 'leaflet' { constructor(options: LayersControlOptions); } + interface DataLayer { + toggleGroup(group: string): boolean; + } + + interface GroupData { + title: string; + group: string; + enabled: boolean; + dataLayers: DataLayer[]; + } + + interface GroupsControlOptions extends SidebarPaneControlOptions { + groups: GroupData[]; + } + + class GroupsControl extends SidebarPaneControl { + constructor(options: GroupsControlOptions); + } + function sidebar(options: SidebarControlOptions): SidebarControl; function sidebarPane(options: O): SidebarPaneControl; function layers(options: LayersControlOptions): LayersControl; + function groups(options: GroupsControlOptions): GroupsControl; + namespace TileLayer { interface ChinaProvidersData { From 2e5031faecb57160001b24f562af3aeb696b0542 Mon Sep 17 00:00:00 2001 From: Igor Kulikov Date: Mon, 13 Jan 2025 19:44:52 +0200 Subject: [PATCH 019/345] UI: Marker settings. --- .../widget/lib/maps/leaflet/leaflet-tb.ts | 1 + .../widget/lib/maps/map-data-layer.ts | 16 +-- .../components/widget/lib/maps/map.models.ts | 21 +++- .../home/components/widget/lib/maps/map.scss | 58 +++++++-- .../home/components/widget/lib/maps/map.ts | 52 ++++++++ ...-layer-color-settings-panel.component.html | 69 +++++++++++ ...-layer-color-settings-panel.component.scss | 53 ++++++++ ...ta-layer-color-settings-panel.component.ts | 82 +++++++++++++ .../data-layer-color-settings.component.html | 30 +++++ .../data-layer-color-settings.component.ts | 113 ++++++++++++++++++ .../map/map-data-layer-dialog.component.html | 34 ++++++ .../map/map-data-layer-dialog.component.ts | 25 +++- ...marker-image-settings-panel.component.html | 72 +++++++++++ ...marker-image-settings-panel.component.scss | 53 ++++++++ .../marker-image-settings-panel.component.ts | 101 ++++++++++++++++ .../map/marker-image-settings.component.html | 28 +++++ .../map/marker-image-settings.component.ts | 96 +++++++++++++++ .../common/widget-settings-common.module.ts | 16 +++ .../assets/locale/locale.constant-en_US.json | 20 +++- 19 files changed, 916 insertions(+), 24 deletions(-) create mode 100644 ui-ngx/src/app/modules/home/components/widget/lib/settings/common/map/data-layer-color-settings-panel.component.html create mode 100644 ui-ngx/src/app/modules/home/components/widget/lib/settings/common/map/data-layer-color-settings-panel.component.scss create mode 100644 ui-ngx/src/app/modules/home/components/widget/lib/settings/common/map/data-layer-color-settings-panel.component.ts create mode 100644 ui-ngx/src/app/modules/home/components/widget/lib/settings/common/map/data-layer-color-settings.component.html create mode 100644 ui-ngx/src/app/modules/home/components/widget/lib/settings/common/map/data-layer-color-settings.component.ts create mode 100644 ui-ngx/src/app/modules/home/components/widget/lib/settings/common/map/marker-image-settings-panel.component.html create mode 100644 ui-ngx/src/app/modules/home/components/widget/lib/settings/common/map/marker-image-settings-panel.component.scss create mode 100644 ui-ngx/src/app/modules/home/components/widget/lib/settings/common/map/marker-image-settings-panel.component.ts create mode 100644 ui-ngx/src/app/modules/home/components/widget/lib/settings/common/map/marker-image-settings.component.html create mode 100644 ui-ngx/src/app/modules/home/components/widget/lib/settings/common/map/marker-image-settings.component.ts diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/maps/leaflet/leaflet-tb.ts b/ui-ngx/src/app/modules/home/components/widget/lib/maps/leaflet/leaflet-tb.ts index a4bb547ec7..84b0d2a688 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/maps/leaflet/leaflet-tb.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/maps/leaflet/leaflet-tb.ts @@ -99,6 +99,7 @@ class SidebarPaneControl extends L.Contr this.button = $("
") .attr('class', 'tb-control-button') .attr('href', '#') + .attr('role', 'button') .html('
') .on('click', (e) => { this.toggle(e); diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/maps/map-data-layer.ts b/ui-ngx/src/app/modules/home/components/widget/lib/maps/map-data-layer.ts index 767e44c90c..2b825e09be 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/maps/map-data-layer.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/maps/map-data-layer.ts @@ -16,6 +16,7 @@ import { CirclesDataLayerSettings, + createColorMarkerURI, DataLayerColorSettings, DataLayerColorType, defaultBaseCirclesDataLayerSettings, @@ -487,19 +488,6 @@ export class TbMarkersDataLayer extends TbMapDataLayer` + - ``; - return 'data:image/svg+xml;base64,' + btoa(svg); - } - public createDefaultMarkerIcon(): MarkerIconInfo { const color = this.settings.markerColor.color || '#FE7569'; return this.createColoredMarkerIcon(tinycolor(color)); @@ -509,7 +497,7 @@ export class TbMarkersDataLayer extends TbMapDataLayer { + const svg = `` + + ``; + return 'data:image/svg+xml;base64,' + btoa(svg); +} export enum MapType { geoMap = 'geoMap', @@ -200,7 +214,12 @@ export const defaultBaseMarkersDataLayerSettings: Partial div { + background: $tb-primary-color; // primary color + } } > div { width: 30px; height: 30px; - color: #5B5B5B; + background: rgba(0, 0, 0, 0.54); line-height: 30px; - background-repeat: no-repeat; - background-position: center; + mask-repeat: no-repeat; + mask-position: center; &.tb-layers { - background-image: url('data:image/svg+xml,'); + mask-image: url('data:image/svg+xml,'); } &.tb-groups { - background-image: url('data:image/svg+xml,'); + mask-image: url('data:image/svg+xml,'); } } } 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 b61f5c8afd..973d035683 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,6 +50,7 @@ import { IWidgetSubscription, WidgetSubscriptionOptions } from '@core/api/widget import { widgetType } from '@shared/models/widget.models'; import { EntityDataPageLink } from '@shared/models/query/query.models'; import { CustomTranslatePipe } from '@shared/pipe/custom-translate.pipe'; +import ITooltipsterInstance = JQueryTooltipster.ITooltipsterInstance; export abstract class TbMap { @@ -81,6 +82,8 @@ export abstract class TbMap { private readonly mapResize$: ResizeObserver; + private tooltipInstances: ITooltipsterInstance[] = []; + protected constructor(protected ctx: WidgetContext, protected inputSettings: DeepPartial, protected containerElement: HTMLElement) { @@ -133,6 +136,7 @@ export abstract class TbMap { this.map.panTo(this.defaultCenterPosition); } this.setupDataLayers(); + this.createdControlButtonTooltip(); } private setupDataLayers() { @@ -229,6 +233,51 @@ export abstract class TbMap { } } + private createdControlButtonTooltip() { + import('tooltipster').then(() => { + if ($.tooltipster) { + this.tooltipInstances.forEach((instance) => { + instance.destroy(); + }); + this.tooltipInstances = []; + } + $(this.mapElement) + .find('a[role="button"]:not(.leaflet-pm-action)') + .each((_index, element) => { + let title: string; + if (element.title) { + title = element.title; + $(element).removeAttr('title'); + } else if (element.parentElement.title) { + title = element.parentElement.title; + $(element).parent().removeAttr('title'); + } + const tooltip = $(element).tooltipster( + { + content: title, + theme: 'tooltipster-shadow', + delay: 10, + triggerClose: { + click: true, + tap: true, + scroll: true, + mouseleave: true + }, + side: ['topleft', 'bottomleft'].includes(this.settings.controlsPosition) ? 'right' : 'left', + distance: 2, + trackOrigin: true, + functionBefore: (instance, helper) => { + if (helper.origin.ariaDisabled === 'true' || helper.origin.parentElement.classList.contains('active')) { + return false; + } + }, + } + ); + this.tooltipInstances.push(tooltip.tooltipster('instance')); + }); + }); + } + private update(subscription: IWidgetSubscription) { const dsData = formattedDataFormDatasourceData(subscription.data, undefined, undefined, el => el.datasource.entityId + el.datasource.mapDataIds[0]); @@ -328,6 +377,9 @@ export abstract class TbMap { if (this.map) { this.map.remove(); } + this.tooltipInstances.forEach((instance) => { + instance.destroy(); + }); } public abstract positionToLatLng(position: {x: number; y: number}): L.LatLng; diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/map/data-layer-color-settings-panel.component.html b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/map/data-layer-color-settings-panel.component.html new file mode 100644 index 0000000000..8b19ac16cd --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/map/data-layer-color-settings-panel.component.html @@ -0,0 +1,69 @@ + +
+
widgets.maps.data-layer.color-settings
+
+ + + {{ 'widgets.maps.data-layer.color-type-constant' | translate }} + + + {{ 'widgets.maps.data-layer.color-type-function' | translate }} + + +
+
+
widgets.maps.data-layer.color
+ + +
+
+
+
+ +
+
+ + + +
+
+ + +
+ + +
+
diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/map/data-layer-color-settings-panel.component.scss b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/map/data-layer-color-settings-panel.component.scss new file mode 100644 index 0000000000..a1c9d5bacc --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/map/data-layer-color-settings-panel.component.scss @@ -0,0 +1,53 @@ +/** + * Copyright © 2016-2024 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +@import '../../../../../../../../../scss/constants'; + +.tb-data-layer-color-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-data-layer-color-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-data-layer-color-settings-panel-body { + flex: 1; + display: flex; + flex-direction: column; + min-height: 0; + } + .tb-data-layer-color-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/data-layer-color-settings-panel.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/map/data-layer-color-settings-panel.component.ts new file mode 100644 index 0000000000..ff41d2772b --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/map/data-layer-color-settings-panel.component.ts @@ -0,0 +1,82 @@ +/// +/// Copyright © 2016-2024 The Thingsboard Authors +/// +/// Licensed under the Apache License, Version 2.0 (the "License"); +/// you may not use this file except in compliance with the License. +/// You may obtain a copy of the License at +/// +/// http://www.apache.org/licenses/LICENSE-2.0 +/// +/// Unless required by applicable law or agreed to in writing, software +/// distributed under the License is distributed on an "AS IS" BASIS, +/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +/// See the License for the specific language governing permissions and +/// limitations under the License. +/// + +import { Component, DestroyRef, EventEmitter, Input, OnInit, Output, ViewEncapsulation } from '@angular/core'; +import { PageComponent } from '@shared/components/page.component'; +import { TbPopoverComponent } from '@shared/components/popover.component'; +import { UntypedFormBuilder, UntypedFormGroup } from '@angular/forms'; +import { Store } from '@ngrx/store'; +import { AppState } from '@core/core.state'; +import { WidgetService } from '@core/http/widget.service'; +import { takeUntilDestroyed } from '@angular/core/rxjs-interop'; +import { DataLayerColorSettings, DataLayerColorType } from '@home/components/widget/lib/maps/map.models'; + +@Component({ + selector: 'tb-data-layer-color-settings-panel', + templateUrl: './data-layer-color-settings-panel.component.html', + providers: [], + styleUrls: ['./data-layer-color-settings-panel.component.scss'], + encapsulation: ViewEncapsulation.None +}) +export class DataLayerColorSettingsPanelComponent extends PageComponent implements OnInit { + + @Input() + colorSettings: DataLayerColorSettings; + + @Input() + popover: TbPopoverComponent; + + @Output() + colorSettingsApplied = new EventEmitter(); + + DataLayerColorType = DataLayerColorType; + + colorSettingsFormGroup: UntypedFormGroup; + + functionScopeVariables = this.widgetService.getWidgetScopeVariables(); + + constructor(private fb: UntypedFormBuilder, + private widgetService: WidgetService, + protected store: Store, + private destroyRef: DestroyRef) { + super(store); + } + + ngOnInit(): void { + this.colorSettingsFormGroup = this.fb.group( + { + type: [this.colorSettings?.type || DataLayerColorType.constant, []], + color: [this.colorSettings?.color, []], + colorFunction: [this.colorSettings?.colorFunction, []] + } + ); + this.colorSettingsFormGroup.get('type').valueChanges.pipe( + takeUntilDestroyed(this.destroyRef) + ).subscribe(() => { + setTimeout(() => {this.popover?.updatePosition();}, 0); + }); + } + + cancel() { + this.popover?.hide(); + } + + applyColorSettings() { + const colorSettings: DataLayerColorSettings = this.colorSettingsFormGroup.value; + this.colorSettingsApplied.emit(colorSettings); + } + +} diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/map/data-layer-color-settings.component.html b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/map/data-layer-color-settings.component.html new file mode 100644 index 0000000000..2122c580f3 --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/map/data-layer-color-settings.component.html @@ -0,0 +1,30 @@ + + + +
+
+
+
diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/map/data-layer-color-settings.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/map/data-layer-color-settings.component.ts new file mode 100644 index 0000000000..79249a5cee --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/map/data-layer-color-settings.component.ts @@ -0,0 +1,113 @@ +/// +/// Copyright © 2016-2024 The Thingsboard Authors +/// +/// Licensed under the Apache License, Version 2.0 (the "License"); +/// you may not use this file except in compliance with the License. +/// You may obtain a copy of the License at +/// +/// http://www.apache.org/licenses/LICENSE-2.0 +/// +/// Unless required by applicable law or agreed to in writing, software +/// distributed under the License is distributed on an "AS IS" BASIS, +/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +/// See the License for the specific language governing permissions and +/// limitations under the License. +/// + +import { Component, forwardRef, Input, Renderer2, ViewContainerRef } from '@angular/core'; +import { ControlValueAccessor, NG_VALUE_ACCESSOR } from '@angular/forms'; +import { ComponentStyle } from '@shared/models/widget-settings.models'; +import { MatButton } from '@angular/material/button'; +import { TbPopoverService } from '@shared/components/popover.service'; +import { DataLayerColorSettings, DataLayerColorType } from '@home/components/widget/lib/maps/map.models'; +import { + DataLayerColorSettingsPanelComponent +} from '@home/components/widget/lib/settings/common/map/data-layer-color-settings-panel.component'; + +@Component({ + selector: 'tb-data-layer-color-settings', + templateUrl: './data-layer-color-settings.component.html', + styleUrls: [], + providers: [ + { + provide: NG_VALUE_ACCESSOR, + useExisting: forwardRef(() => DataLayerColorSettingsComponent), + multi: true + } + ] +}) +export class DataLayerColorSettingsComponent implements ControlValueAccessor { + + @Input() + disabled: boolean; + + DataLayerColorType = DataLayerColorType; + + modelValue: DataLayerColorSettings; + + colorStyle: ComponentStyle = {}; + + private propagateChange: (v: any) => void = () => { }; + + constructor(private popoverService: TbPopoverService, + private renderer: Renderer2, + private viewContainerRef: ViewContainerRef) {} + + registerOnChange(fn: any): void { + this.propagateChange = fn; + } + + registerOnTouched(fn: any): void { + } + + setDisabledState(isDisabled: boolean): void { + this.disabled = isDisabled; + this.updateColorStyle(); + } + + writeValue(value: DataLayerColorSettings): void { + if (value) { + this.modelValue = value; + this.updateColorStyle(); + } + } + + openColorSettingsPopup($event: Event, matButton: MatButton) { + if ($event) { + $event.stopPropagation(); + } + const trigger = matButton._elementRef.nativeElement; + if (this.popoverService.hasPopover(trigger)) { + this.popoverService.hidePopover(trigger); + } else { + const ctx: any = { + colorSettings: this.modelValue, + }; + const colorSettingsPanelPopover = this.popoverService.displayPopover(trigger, this.renderer, + this.viewContainerRef, DataLayerColorSettingsPanelComponent, 'left', true, null, + ctx, + {}, + {}, {}, true); + colorSettingsPanelPopover.tbComponentRef.instance.popover = colorSettingsPanelPopover; + colorSettingsPanelPopover.tbComponentRef.instance.colorSettingsApplied.subscribe((colorSettings) => { + colorSettingsPanelPopover.hide(); + this.modelValue = colorSettings; + this.updateColorStyle(); + this.propagateChange(this.modelValue); + }); + } + } + + private updateColorStyle() { + if (!this.disabled && this.modelValue) { + if (this.modelValue.type === DataLayerColorType.constant) { + this.colorStyle = {backgroundColor: this.modelValue.color}; + } else { + this.colorStyle = {}; + } + } else { + this.colorStyle = {}; + } + } + +} diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/map/map-data-layer-dialog.component.html b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/map/map-data-layer-dialog.component.html index 872b94a20a..711a004e4e 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/map/map-data-layer-dialog.component.html +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/map/map-data-layer-dialog.component.html @@ -170,6 +170,40 @@
{{ 'widgets.maps.data-layer.groups' | translate }}
diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/map/map-data-layer-dialog.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/map/map-data-layer-dialog.component.ts index afcb164be8..6b15b49580 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/map/map-data-layer-dialog.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/map/map-data-layer-dialog.component.ts @@ -17,13 +17,13 @@ import { Component, DestroyRef, Inject, ViewEncapsulation } from '@angular/core'; import { DialogComponent } from '@shared/components/dialog.component'; import { - CirclesDataLayerSettings, + CirclesDataLayerSettings, defaultBaseMapDataLayerSettings, MapDataLayerSettings, MapDataLayerType, MapType, MarkersDataLayerSettings, MarkerType, - PolygonsDataLayerSettings + PolygonsDataLayerSettings, ShapeDataLayerSettings } from '@home/components/widget/lib/maps/map.models'; import { Store } from '@ngrx/store'; import { AppState } from '@core/core.state'; @@ -35,7 +35,7 @@ import { DataKeyType } from '@shared/models/telemetry/telemetry.models'; import { takeUntilDestroyed } from '@angular/core/rxjs-interop'; import { EntityType } from '@shared/models/entity-type.models'; import { MapSettingsContext } from '@home/components/widget/lib/settings/common/map/map-settings.component.models'; -import { genNextLabelForDataKeys } from '@core/utils'; +import { genNextLabelForDataKeys, mergeDeepIgnoreArray } from '@core/utils'; export interface MapDataLayerDialogData { settings: MapDataLayerSettings; @@ -92,12 +92,18 @@ export class MapDataLayerDialogComponent extends DialogComponent(this.dataLayerType), this.settings); + this.dataLayerFormGroup = this.fb.group({ dsType: [this.settings.dsType, [Validators.required]], + dsLabel: [this.settings.dsLabel, []], dsDeviceId: [this.settings.dsDeviceId, [Validators.required]], dsEntityAliasId: [this.settings.dsEntityAliasId, [Validators.required]], dsFilterId: [this.settings.dsFilterId, []], additionalDataKeys: [this.settings.additionalDataKeys, []], + label: [this.settings.label, []], + tooltip: [this.settings.tooltip, []], groups: [this.settings.groups, []] }); @@ -119,14 +125,20 @@ export class MapDataLayerDialogComponent extends DialogComponent + + + Link text')", + "tooltip-function": "Tooltip function", + "tooltip-trigger": "Tooltip trigger", + "tooltip-trigger-click": "Show tooltip on click", + "tooltip-trigger-hover": "Show tooltip on hover", + "auto-close-tooltips": "Auto-close tooltips", + "tooltip-offset": "Tooltip offset", + "tooltip-offset-horizontal": "Horizontal", + "tooltip-offset-vertical": "Vertical", "marker": { "latitude-key": "Latitude key", "longitude-key": "Longitude key", @@ -6898,10 +6915,10 @@ "marker-type-image": "Image", "image": "Image", "marker-image": "Marker image", - "marker-image-type-image": "Constant", + "marker-image-type-image": "Image", "marker-image-type-function": "Function", "custom-marker-image-size": "Custom marker image size", - "marker-image-function": "Function", + "marker-image-function": "Marker image function", "marker-images": "Marker images", "marker-offset": "Marker offset", "offset-horizontal": "Horizontal", From 64f15f99df3712ff8b73f008f6bde0f902329da2 Mon Sep 17 00:00:00 2001 From: mpetrov Date: Thu, 16 Jan 2025 16:00:17 +0200 Subject: [PATCH 024/345] Added provisioning device mobile action --- .../mobile-action-editor.component.html | 30 +++++++++++++++ .../action/mobile-action-editor.component.ts | 26 ++++++++++++- .../action/mobile-action-editor.models.ts | 17 +++++++++ .../components/widget/widget.component.ts | 20 ++++++++++ ui-ngx/src/app/shared/models/widget.models.ts | 37 +++++++++++++++++-- .../assets/locale/locale.constant-en_US.json | 10 +++++ 6 files changed, 134 insertions(+), 6 deletions(-) diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/action/mobile-action-editor.component.html b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/action/mobile-action-editor.component.html index fb1afd544d..1e5c3cadd5 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/action/mobile-action-editor.component.html +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/action/mobile-action-editor.component.html @@ -36,6 +36,36 @@
+ +
+
{{ 'widget-action.mobile.provisioning-device.type' | translate }}*
+ + + + {{ widgetMobileProvisionTypeTranslationMap.get(mobileProvisionType[actionType]) | translate }} + + + + warning + + +
+ +
diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/action/mobile-action-editor.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/action/mobile-action-editor.component.ts index e949f765c6..593cedbb27 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/action/mobile-action-editor.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/action/mobile-action-editor.component.ts @@ -24,10 +24,12 @@ import { } from '@angular/forms'; import { coerceBooleanProperty } from '@angular/cdk/coercion'; import { + WidgetMobileProvisionType, WidgetActionType, WidgetMobileActionDescriptor, WidgetMobileActionType, - widgetMobileActionTypeTranslationMap + widgetMobileActionTypeTranslationMap, + widgetMobileProvisionTypeTranslationMap } from '@shared/models/widget.models'; import { CustomActionEditorCompleter } from '@home/components/widget/lib/settings/common/action/custom-action.models'; import { @@ -38,7 +40,8 @@ import { getDefaultProcessImageFunction, getDefaultProcessLaunchResultFunction, getDefaultProcessLocationFunction, - getDefaultProcessQrCodeFunction + getDefaultProcessQrCodeFunction, + getDefaultProvisioningSuccessFunction } from '@home/components/widget/lib/settings/common/action/mobile-action-editor.models'; import { WidgetService } from '@core/http/widget.service'; import { TbFunction } from '@shared/models/js-function.models'; @@ -59,6 +62,9 @@ export class MobileActionEditorComponent implements ControlValueAccessor, OnInit mobileActionTypes = Object.keys(WidgetMobileActionType); mobileActionTypeTranslations = widgetMobileActionTypeTranslationMap; mobileActionType = WidgetMobileActionType; + mobileProvisionType = WidgetMobileProvisionType; + mobileProvisionTypes = Object.keys(WidgetMobileProvisionType); + widgetMobileProvisionTypeTranslationMap = widgetMobileProvisionTypeTranslationMap; customActionEditorCompleter = CustomActionEditorCompleter; @@ -254,6 +260,22 @@ export class MobileActionEditorComponent implements ControlValueAccessor, OnInit this.fb.control(processLocationFunction, [Validators.required]) ); break; + case WidgetMobileActionType.provisioningDevice: + let handleProvisionSuccessFunction = action?.handleProvisionSuccessFunction; + if (changed) { + const defaultProvisioningSuccessFunction = getDefaultProvisioningSuccessFunction(); + if (defaultProvisioningSuccessFunction !== handleProvisionSuccessFunction) { + handleProvisionSuccessFunction = defaultProvisioningSuccessFunction; + } + } + this.mobileActionTypeFormGroup.addControl( + 'handleProvisionSuccessFunction', + this.fb.control(handleProvisionSuccessFunction, [Validators.required]) + ); + this.mobileActionTypeFormGroup.addControl( + 'provisionType', + this.fb.control(null, [Validators.required]) + ); } } this.mobileActionTypeFormGroup.valueChanges.pipe( diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/action/mobile-action-editor.models.ts b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/action/mobile-action-editor.models.ts index 830a907a09..8366334f2c 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/action/mobile-action-editor.models.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/action/mobile-action-editor.models.ts @@ -138,6 +138,15 @@ const processLocationFunction: TbFunction = ' }, 100);\n' + '}'; +const provisioningSuccessFunction: TbFunction = + '// Function body to handle device provision success. \n' + + '// - device - device that was successfully provisioned.\n' + + 'function showDeviceProvisioningSuccess(deviceName, message) {\n' + + ' setTimeout(function() {\n' + + ' widgetContext.dialogs.alert(`Device ` + deviceName + ` was successfully provisioned`, message).subscribe();\n' + + ' }, 100);\n' + + '}\n'; + const handleEmptyResultFunctionTemplate: TbFunction = '// Optional function body to handle empty result. \n' + '// Usually this happens when user cancels the action (for ex. by pressing phone back button). \n\n' + @@ -241,6 +250,8 @@ export const getDefaultProcessQrCodeFunction = () => processQrCodeFunction; export const getDefaultProcessLocationFunction = () => processLocationFunction; +export const getDefaultProvisioningSuccessFunction = () => provisioningSuccessFunction; + export const getDefaultGetLocationFunction = () => getLocationFunctionTemplate; export const getDefaultGetPhoneNumberFunction = () => getPhoneNumberFunctionTemplate; @@ -272,6 +283,9 @@ export const getDefaultHandleEmptyResultFunction = (type: WidgetMobileActionType case WidgetMobileActionType.takeScreenshot: message = 'Take screenshot action was cancelled!'; break; + case WidgetMobileActionType.provisioningDevice: + message = 'Provisioning device was not invoked!'; + break; } return handleEmptyResultFunctionTemplate.replace('--MESSAGE--', message); }; @@ -303,6 +317,9 @@ export const getDefaultHandleErrorFunction = (type: WidgetMobileActionType): TbF case WidgetMobileActionType.takeScreenshot: title = 'Failed to take screenshot'; break; + case WidgetMobileActionType.provisioningDevice: + title = 'Failed to make device provisioning'; + break; } return handleErrorFunctionTemplate.replace('--TITLE--', title); }; diff --git a/ui-ngx/src/app/modules/home/components/widget/widget.component.ts b/ui-ngx/src/app/modules/home/components/widget/widget.component.ts index eaf6b6105e..375646da55 100644 --- a/ui-ngx/src/app/modules/home/components/widget/widget.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/widget.component.ts @@ -1292,6 +1292,26 @@ export class WidgetComponent extends PageComponent implements OnInit, OnChanges, ); } break; + case WidgetMobileActionType.provisioningDevice: + const deviceName = actionResult.device.name; + if (isNotEmptyTbFunction(mobileAction.handleProvisionSuccessFunction)) { + compileTbFunction(this.http, mobileAction.handleProvisionSuccessFunction, 'deviceName', '$event', 'widgetContext', 'entityId', + 'entityName', 'additionalParams', 'entityLabel').subscribe( + { + next: (compiled) => { + try { + compiled.execute(deviceName, $event, this.widgetContext, entityId, entityName, additionalParams, entityLabel); + } catch (e) { + console.error(e); + } + }, + error: (err) => { + console.error(err); + } + } + ); + } + break; case WidgetMobileActionType.scanQrCode: const code = actionResult.code; const format = actionResult.format; diff --git a/ui-ngx/src/app/shared/models/widget.models.ts b/ui-ngx/src/app/shared/models/widget.models.ts index 5950dcf911..f92cffcf1c 100644 --- a/ui-ngx/src/app/shared/models/widget.models.ts +++ b/ui-ngx/src/app/shared/models/widget.models.ts @@ -47,6 +47,7 @@ import { WidgetConfigCallbacks } from '@home/components/widget/config/widget-con import { TbFunction } from '@shared/models/js-function.models'; import { FormProperty, jsonFormSchemaToFormProperties } from '@shared/models/dynamic-form.models'; import { takeUntilDestroyed } from '@angular/core/rxjs-interop'; +import { Device } from '@shared/models/device.models'; export enum widgetType { timeseries = 'timeseries', @@ -585,9 +586,26 @@ export enum WidgetMobileActionType { scanQrCode = 'scanQrCode', makePhoneCall = 'makePhoneCall', getLocation = 'getLocation', - takeScreenshot = 'takeScreenshot' + takeScreenshot = 'takeScreenshot', + provisioningDevice = 'provisioningDevice', } +export enum WidgetMobileProvisionType { + smartConfigEspTouch = 'SmartConfigEspTouch', + smartConfigEspTouchV2 = 'SmartConfigEspTouchV2', + wifi = 'wifi', + softAp = 'SoftAp', +} + +export const widgetMobileProvisionTypeTranslationMap = new Map( + [ + [ WidgetMobileProvisionType.smartConfigEspTouch, 'widget-action.mobile.provisioning-device.smart-config-esp-touch' ], + [ WidgetMobileProvisionType.smartConfigEspTouchV2, 'widget-action.mobile.provisioning-device.smart-config-esp-touch-v2' ], + [ WidgetMobileProvisionType.wifi, 'widget-action.mobile.provisioning-device.wifi' ], + [ WidgetMobileProvisionType.softAp, 'widget-action.mobile.provisioning-device.soft-ap' ] + ] +); + export const widgetActionTypes = Object.keys(WidgetActionType) as WidgetActionType[]; export const widgetActionTypeTranslationMap = new Map( @@ -612,7 +630,8 @@ export const widgetMobileActionTypeTranslationMap = new Map { result?: T; @@ -646,6 +670,10 @@ export interface WidgetMobileActionResult { hasError: boolean; } +export interface ProvisionSuccessDescriptor { + handleProvisionSuccessFunction: TbFunction; +} + export interface ProcessImageDescriptor { processImageFunction: TbFunction; } @@ -674,7 +702,8 @@ export type WidgetMobileActionDescriptors = ProcessImageDescriptor & LaunchMapDescriptor & ScanQrCodeDescriptor & MakePhoneCallDescriptor & - GetLocationDescriptor; + GetLocationDescriptor & + ProvisionSuccessDescriptor; export interface WidgetMobileActionDescriptor extends WidgetMobileActionDescriptors { type: WidgetMobileActionType; 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 a584bb453b..27c85e7263 100644 --- a/ui-ngx/src/assets/locale/locale.constant-en_US.json +++ b/ui-ngx/src/assets/locale/locale.constant-en_US.json @@ -5593,6 +5593,16 @@ "URL": "URL", "url-required": "URL is required.", "mobile": { + "provisioning-device": { + "label": "Provisioning device", + "type": "Provisioning device type", + "select-type": "Select provisioning device type", + "type-required": "Provisioning device type is required", + "smart-config-esp-touch": "Smart Config ESP-Touch", + "smart-config-esp-touch-v2": "Smart Config ESP-Touch v2", + "wifi": "Wi-Fi Provisioning via Bluetooth Low Energy BLE/QR code", + "soft-ap": "SoftAP" + }, "action-type": "Mobile action type", "select-action-type": "Select mobile action type", "action-type-required": "Mobile action type is required", From d3c7c30d35c9f6a818cafd4d9b4c8fa7f30704b1 Mon Sep 17 00:00:00 2001 From: mpetrov Date: Thu, 16 Jan 2025 16:39:21 +0200 Subject: [PATCH 025/345] added provsion type arg --- .../src/app/modules/home/components/widget/widget.component.ts | 3 +++ ui-ngx/src/app/shared/models/widget.models.ts | 1 + 2 files changed, 4 insertions(+) diff --git a/ui-ngx/src/app/modules/home/components/widget/widget.component.ts b/ui-ngx/src/app/modules/home/components/widget/widget.component.ts index 375646da55..22821c3d88 100644 --- a/ui-ngx/src/app/modules/home/components/widget/widget.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/widget.component.ts @@ -1205,6 +1205,9 @@ export class WidgetComponent extends PageComponent implements OnInit, OnChanges, case WidgetMobileActionType.takeScreenshot: argsObservable = of([]); break; + case WidgetMobileActionType.provisioningDevice: + argsObservable = of([mobileAction.provisionType]); + break; case WidgetMobileActionType.mapDirection: case WidgetMobileActionType.mapLocation: argsObservable = compileTbFunction(this.http, mobileAction.getLocationFunction, '$event', 'widgetContext', 'entityId', diff --git a/ui-ngx/src/app/shared/models/widget.models.ts b/ui-ngx/src/app/shared/models/widget.models.ts index f92cffcf1c..0a04ef06b9 100644 --- a/ui-ngx/src/app/shared/models/widget.models.ts +++ b/ui-ngx/src/app/shared/models/widget.models.ts @@ -672,6 +672,7 @@ export interface WidgetMobileActionResult { export interface ProvisionSuccessDescriptor { handleProvisionSuccessFunction: TbFunction; + provisionType: WidgetMobileProvisionType; } export interface ProcessImageDescriptor { From 09d14440facc4fa7c79646621013d5a140980563 Mon Sep 17 00:00:00 2001 From: Igor Kulikov Date: Mon, 20 Jan 2025 19:45:41 +0200 Subject: [PATCH 026/345] UI: Map: Marker shape visualization. --- .../widget/lib/maps/map-data-layer.ts | 144 +++++++++---- .../components/widget/lib/maps/map.models.ts | 60 ++++-- .../home/components/widget/lib/maps/map.scss | 6 + .../widget/lib/maps/marker-shape.models.ts | 143 ++++++++++++ .../map/map-data-layer-dialog.component.html | 13 +- .../map/map-data-layer-dialog.component.ts | 15 +- .../map/marker-shape-settings.component.html | 34 +++ .../map/marker-shape-settings.component.ts | 204 ++++++++++++++++++ .../common/map/marker-shapes.component.html | 38 ++++ .../common/map/marker-shapes.component.scss | 39 ++++ .../common/map/marker-shapes.component.ts | 84 ++++++++ .../common/widget-settings-common.module.ts | 6 + .../assets/locale/locale.constant-en_US.json | 6 +- ui-ngx/src/assets/markers/shape1.svg | 19 ++ ui-ngx/src/assets/markers/shape10.svg | 16 ++ ui-ngx/src/assets/markers/shape2.svg | 17 ++ ui-ngx/src/assets/markers/shape3.svg | 19 ++ ui-ngx/src/assets/markers/shape4.svg | 21 ++ ui-ngx/src/assets/markers/shape5.svg | 20 ++ ui-ngx/src/assets/markers/shape6.svg | 22 ++ ui-ngx/src/assets/markers/shape7.svg | 28 +++ ui-ngx/src/assets/markers/shape8.svg | 17 ++ ui-ngx/src/assets/markers/shape9.svg | 19 ++ 23 files changed, 918 insertions(+), 72 deletions(-) create mode 100644 ui-ngx/src/app/modules/home/components/widget/lib/maps/marker-shape.models.ts create mode 100644 ui-ngx/src/app/modules/home/components/widget/lib/settings/common/map/marker-shape-settings.component.html create mode 100644 ui-ngx/src/app/modules/home/components/widget/lib/settings/common/map/marker-shape-settings.component.ts create mode 100644 ui-ngx/src/app/modules/home/components/widget/lib/settings/common/map/marker-shapes.component.html create mode 100644 ui-ngx/src/app/modules/home/components/widget/lib/settings/common/map/marker-shapes.component.scss create mode 100644 ui-ngx/src/app/modules/home/components/widget/lib/settings/common/map/marker-shapes.component.ts create mode 100644 ui-ngx/src/assets/markers/shape1.svg create mode 100644 ui-ngx/src/assets/markers/shape10.svg create mode 100644 ui-ngx/src/assets/markers/shape2.svg create mode 100644 ui-ngx/src/assets/markers/shape3.svg create mode 100644 ui-ngx/src/assets/markers/shape4.svg create mode 100644 ui-ngx/src/assets/markers/shape5.svg create mode 100644 ui-ngx/src/assets/markers/shape6.svg create mode 100644 ui-ngx/src/assets/markers/shape7.svg create mode 100644 ui-ngx/src/assets/markers/shape8.svg create mode 100644 ui-ngx/src/assets/markers/shape9.svg diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/maps/map-data-layer.ts b/ui-ngx/src/app/modules/home/components/widget/lib/maps/map-data-layer.ts index 0cabfe9fef..a730c74adf 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/maps/map-data-layer.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/maps/map-data-layer.ts @@ -15,8 +15,8 @@ /// import { + BaseMarkerShapeSettings, CirclesDataLayerSettings, - createColorMarkerURI, DataLayerColorSettings, DataLayerColorType, DataLayerPatternSettings, @@ -34,18 +34,22 @@ import { MapStringFunction, MapType, MarkerIconInfo, + MarkerIconSettings, MarkerImageFunction, MarkerImageInfo, MarkerImageSettings, MarkerImageType, MarkersDataLayerSettings, + MarkerShapeSettings, MarkerType, - PolygonsDataLayerSettings, processTooltipTemplate, ShapeDataLayerSettings, + PolygonsDataLayerSettings, + processTooltipTemplate, + ShapeDataLayerSettings, TbCircleData, TbMapDatasource } from '@home/components/widget/lib/maps/map.models'; import { TbMap } from '@home/components/widget/lib/maps/map'; -import { Datasource, FormattedData } from '@shared/models/widget.models'; +import { FormattedData } from '@shared/models/widget.models'; import { forkJoin, Observable, of } from 'rxjs'; import { createLabelFromPattern, @@ -59,13 +63,20 @@ import { parseTbFunction, safeExecuteTbFunction } from '@core/utils'; -import L, { LatLngBounds, PathOptions } from 'leaflet'; +import L, { divIcon, LatLngBounds } from 'leaflet'; import { CompiledTbFunction } from '@shared/models/js-function.models'; -import { catchError, map } from 'rxjs/operators'; +import { catchError, map, switchMap } from 'rxjs/operators'; import tinycolor from 'tinycolor2'; import { WidgetContext } from '@home/models/widget-component.models'; import { ImagePipe } from '@shared/pipe/image.pipe'; import { CustomTranslatePipe } from '@shared/pipe/custom-translate.pipe'; +import { + createColorMarkerIconElement, + createColorMarkerShapeURI, + MarkerShape +} from '@home/components/widget/lib/maps/marker-shape.models'; +import { MatIconRegistry } from '@angular/material/icon'; +import { DomSanitizer } from '@angular/platform-browser'; abstract class TbDataLayerItem> { @@ -317,7 +328,7 @@ export abstract class TbMapDataLayer d.$datasource.mapDataIds.includes(this.mapDataId)); const rawItems = layerData.filter(d => this.isValidLayerData(d)); const toDelete = new Set(Array.from(this.layerItems.keys())); - rawItems.forEach((data, index) => { + rawItems.forEach((data) => { let layerItem = this.layerItems.get(data.entityId); if (layerItem) { layerItem.update(data, dsData); @@ -428,8 +439,10 @@ abstract class MarkerIconProcessor { static fromSettings(dataLayer: TbMarkersDataLayer, settings: MarkersDataLayerSettings): MarkerIconProcessor { switch (settings.markerType) { - case MarkerType.default: - return new ColorMarkerIconProcessor(dataLayer, settings.markerColor); + case MarkerType.shape: + return new ShapeMarkerIconProcessor(dataLayer, settings.markerShape); + case MarkerType.icon: + return new IconMarkerIconProcessor(dataLayer, settings.markerIcon); case MarkerType.image: return new ImageMarkerIconProcessor(dataLayer, settings.markerImage); } @@ -445,46 +458,80 @@ abstract class MarkerIconProcessor { } -class ColorMarkerIconProcessor extends MarkerIconProcessor { +abstract class BaseColorMarkerShapeProcessor extends MarkerIconProcessor { private markerColorFunction: CompiledTbFunction; private defaultMarkerIconInfo: MarkerIconInfo; - constructor(protected dataLayer: TbMarkersDataLayer, - protected settings: DataLayerColorSettings) { + protected constructor(protected dataLayer: TbMarkersDataLayer, + protected settings: S) { super(dataLayer, settings); } public setup(): Observable { - if (this.settings.type === DataLayerColorType.function) { - return parseTbFunction(this.dataLayer.getCtx().http, this.settings.colorFunction, ['data', 'dsData']).pipe( + const colorSettings = this.settings.color; + if (colorSettings.type === DataLayerColorType.function) { + return parseTbFunction(this.dataLayer.getCtx().http, colorSettings.colorFunction, ['data', 'dsData']).pipe( map((parsed) => { this.markerColorFunction = parsed; return null; }) ); } else { - const color = tinycolor(this.settings.color); - this.defaultMarkerIconInfo = this.dataLayer.createColoredMarkerIcon(color); - return of(null) + const color = tinycolor(colorSettings.color); + return this.createMarkerShape(color, this.settings.size).pipe( + map((info) => { + this.defaultMarkerIconInfo = info; + return null; + } + )); } } public createMarkerIcon(data: FormattedData, dsData: FormattedData[]): Observable { - if (this.settings.type === DataLayerColorType.function) { + const colorSettings = this.settings.color; + if (colorSettings.type === DataLayerColorType.function) { const functionColor = safeExecuteTbFunction(this.markerColorFunction, [data, dsData]); let color: tinycolor.Instance; if (isDefinedAndNotNull(functionColor)) { color = tinycolor(functionColor); } else { - color = tinycolor(this.settings.color); + color = tinycolor(colorSettings.color); } - return of(this.dataLayer.createColoredMarkerIcon(color)); + return this.createMarkerShape(color, this.settings.size); } else { return of(this.defaultMarkerIconInfo); } } + + protected abstract createMarkerShape(color: tinycolor.Instance, size: number): Observable; +} + +class ShapeMarkerIconProcessor extends BaseColorMarkerShapeProcessor { + + constructor(protected dataLayer: TbMarkersDataLayer, + protected settings: MarkerShapeSettings) { + super(dataLayer, settings); + } + + protected createMarkerShape(color: tinycolor.Instance, size: number): Observable { + return this.dataLayer.createColoredMarkerShape(this.settings.shape, color, size); + } + +} + +class IconMarkerIconProcessor extends BaseColorMarkerShapeProcessor { + + constructor(protected dataLayer: TbMarkersDataLayer, + protected settings: MarkerIconSettings) { + super(dataLayer, settings); + } + + protected createMarkerShape(color: tinycolor.Instance, size: number): Observable { + return this.dataLayer.createColoredMarkerIcon(this.settings.icon, color, size); + } + } class ImageMarkerIconProcessor extends MarkerIconProcessor { @@ -532,7 +579,7 @@ class ImageMarkerIconProcessor extends MarkerIconProcessor private loadMarkerIconInfo(image: MarkerImageInfo): Observable { if (image && image.url) { return loadImageWithAspect(this.dataLayer.getCtx().$injector.get(ImagePipe), image.url).pipe( - map((aspectImage) => { + switchMap((aspectImage) => { if (aspectImage?.aspect) { let width: number; let height: number; @@ -561,15 +608,15 @@ class ImageMarkerIconProcessor extends MarkerIconProcessor size: [width, height], icon }; - return iconInfo; + return of(iconInfo); } else { return this.dataLayer.createDefaultMarkerIcon(); } }), - catchError(() => of(this.dataLayer.createDefaultMarkerIcon())) + catchError(() => this.dataLayer.createDefaultMarkerIcon()) ); } else { - return of(this.dataLayer.createDefaultMarkerIcon()); + return this.dataLayer.createDefaultMarkerIcon(); } } @@ -644,24 +691,42 @@ export class TbMarkersDataLayer extends TbMapDataLayer { + const color = this.settings.markerShape?.color?.color || '#307FE5'; + return this.createColoredMarkerShape(MarkerShape.markerShape1, tinycolor(color)); } - public createColoredMarkerIcon(color: tinycolor.Instance): MarkerIconInfo { - return { - size: [21, 34], - icon: L.icon({ - iconUrl: createColorMarkerURI(color), - iconSize: [21, 34], - iconAnchor: [21 * this.markerOffset[0], 34 * this.markerOffset[1]], - popupAnchor: [21 * this.tooltipOffset[0], 34 * this.tooltipOffset[1]], - shadowUrl: 'assets/shadow.png', - shadowSize: [40, 37], - shadowAnchor: [12, 35] + public createColoredMarkerShape(shape: MarkerShape, color: tinycolor.Instance, size = 34): Observable { + return createColorMarkerShapeURI(this.getCtx().$injector.get(MatIconRegistry), this.getCtx().$injector.get(DomSanitizer), shape, color).pipe( + map((iconUrl) => { + return { + size: [size, size], + icon: L.icon({ + iconUrl, + iconSize: [size, size], + iconAnchor: [size * this.markerOffset[0], size * this.markerOffset[1]], + popupAnchor: [size * this.tooltipOffset[0], size * this.tooltipOffset[1]] + }) + }; }) - }; + ); + } + + public createColoredMarkerIcon(icon: string, color: tinycolor.Instance, size = 34): Observable { + return createColorMarkerIconElement(this.getCtx().$injector.get(MatIconRegistry), this.getCtx().$injector.get(DomSanitizer), icon, color).pipe( + map((element) => { + return { + size: [size, size], + icon: L.divIcon({ + html: element.outerHTML, + className: 'tb-marker-div-icon', + iconSize: [size, size], + iconAnchor: [size * this.markerOffset[0], size * this.markerOffset[1]], + popupAnchor: [size * this.tooltipOffset[0], size * this.tooltipOffset[1]] + }) + }; + }) + ); } public extractLocation(data: FormattedData): L.LatLng { @@ -757,7 +822,7 @@ abstract class TbShapesDataLayer, dsData: FormattedData[]): L.PathOptions { const fill = this.fillColorProcessor.processColor(data, dsData); const stroke = this.strokeColorProcessor.processColor(data, dsData); - const style: L.PathOptions = { + return { fill: true, fillColor: fill, color: stroke, @@ -765,7 +830,6 @@ abstract class TbShapesDataLayer { - const svg = `` + - ``; - return 'data:image/svg+xml;base64,' + btoa(svg); -} +import { MarkerShape } from '@home/components/widget/lib/maps/marker-shape.models'; export enum MapType { geoMap = 'geoMap', @@ -183,7 +170,8 @@ export const mapDataLayerValidator = (type: MapDataLayerType): ValidatorFn => { }; export enum MarkerType { - default = 'default', + shape = 'shape', + icon = 'icon', image = 'image' } @@ -211,11 +199,25 @@ export interface MarkerImageSettings { images?: string[]; } +export interface BaseMarkerShapeSettings { + size: number; + color: DataLayerColorSettings; +} + +export interface MarkerShapeSettings extends BaseMarkerShapeSettings { + shape: MarkerShape; +} + +export interface MarkerIconSettings extends BaseMarkerShapeSettings { + icon: string; +} + export interface MarkersDataLayerSettings extends MapDataLayerSettings { xKey: DataKey; yKey: DataKey; markerType: MarkerType; - markerColor: DataLayerColorSettings; + markerShape?: MarkerShapeSettings; + markerIcon?: MarkerIconSettings; markerImage?: MarkerImageSettings; markerOffsetX: number; markerOffsetY: number; @@ -267,14 +269,26 @@ export const defaultMarkersDataLayerSettings = (mapType: MapType, functionsOnly } as MarkersDataLayerSettings, defaultBaseMarkersDataLayerSettings as MarkersDataLayerSettings); export const defaultBaseMarkersDataLayerSettings: Partial = mergeDeep({ - markerType: MarkerType.default, - markerColor: { - type: DataLayerColorType.constant, - color: '#307FE5', + markerType: MarkerType.shape, + markerShape: { + shape: MarkerShape.markerShape1, + size: 34, + color: { + type: DataLayerColorType.constant, + color: '#307FE5', + } + }, + markerIcon: { + icon: 'mdi:lightbulb-on', + size: 48, + color: { + type: DataLayerColorType.constant, + color: '#307FE5', + } }, markerImage: { type: MarkerImageType.image, - image: createColorMarkerURI(tinycolor('#307FE5')), + image: '/assets/markers/shape1.svg', imageSize: 34 }, markerOffsetX: 0.5, @@ -720,7 +734,7 @@ export interface MarkerImageInfo { } export interface MarkerIconInfo { - icon: Icon; + icon: L.Icon; size: [number, number]; } diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/maps/map.scss b/ui-ngx/src/app/modules/home/components/widget/lib/maps/map.scss index 65cc297eac..2928f6205a 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/maps/map.scss +++ b/ui-ngx/src/app/modules/home/components/widget/lib/maps/map.scss @@ -95,6 +95,12 @@ } } } + .leaflet-marker-icon { + &.tb-marker-div-icon { + background: none; + border: none; + } + } } .tb-map-sidebar { .tb-layers, .tb-groups { diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/maps/marker-shape.models.ts b/ui-ngx/src/app/modules/home/components/widget/lib/maps/marker-shape.models.ts new file mode 100644 index 0000000000..8de7e3d013 --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/widget/lib/maps/marker-shape.models.ts @@ -0,0 +1,143 @@ +/// +/// Copyright © 2016-2024 The Thingsboard Authors +/// +/// Licensed under the Apache License, Version 2.0 (the "License"); +/// you may not use this file except in compliance with the License. +/// You may obtain a copy of the License at +/// +/// http://www.apache.org/licenses/LICENSE-2.0 +/// +/// Unless required by applicable law or agreed to in writing, software +/// distributed under the License is distributed on an "AS IS" BASIS, +/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +/// See the License for the specific language governing permissions and +/// limitations under the License. +/// + +import tinycolor from 'tinycolor2'; +import { MatIconRegistry } from '@angular/material/icon'; +import { DomSanitizer } from '@angular/platform-browser'; +import { Observable, of, switchMap } from 'rxjs'; +import { catchError, map, take } from 'rxjs/operators'; +import { isSvgIcon, splitIconName } from '@shared/models/icon.models'; +import { Element, Text, G } from '@svgdotjs/svg.js'; + +export enum MarkerShape { + markerShape1 = 'markerShape1', + markerShape2 = 'markerShape2', + markerShape3 = 'markerShape3', + markerShape4 = 'markerShape4', + markerShape5 = 'markerShape5', + markerShape6 = 'markerShape6', + markerShape7 = 'markerShape7', + markerShape8 = 'markerShape8', + markerShape9 = 'markerShape9', + markerShape10 = 'markerShape10' +} + +export const markerShapeMap = new Map( + [ + [MarkerShape.markerShape1, '/assets/markers/shape1.svg'], + [MarkerShape.markerShape2, '/assets/markers/shape2.svg'], + [MarkerShape.markerShape3, '/assets/markers/shape3.svg'], + [MarkerShape.markerShape4, '/assets/markers/shape4.svg'], + [MarkerShape.markerShape5, '/assets/markers/shape5.svg'], + [MarkerShape.markerShape6, '/assets/markers/shape6.svg'], + [MarkerShape.markerShape7, '/assets/markers/shape7.svg'], + [MarkerShape.markerShape8, '/assets/markers/shape8.svg'], + [MarkerShape.markerShape9, '/assets/markers/shape9.svg'], + [MarkerShape.markerShape10, '/assets/markers/shape10.svg'] + ] +); + +const createColorMarkerShape = (iconRegistry: MatIconRegistry, domSanitizer: DomSanitizer, shape: MarkerShape, color: tinycolor.Instance): Observable => { + const markerAssetUrl = markerShapeMap.get(shape); + const safeUrl = domSanitizer.bypassSecurityTrustResourceUrl(markerAssetUrl); + return iconRegistry.getSvgIconFromUrl(safeUrl).pipe( + map((svgElement) => { + const colorElements = Array.from(svgElement.getElementsByClassName('marker-color')); + colorElements.forEach(el => { + el.setAttribute('fill', '#'+color.toHex()); + }); + const strokeElements = Array.from(svgElement.getElementsByClassName('marker-stroke')); + strokeElements.forEach(el => { + el.setAttribute('stroke', '#'+color.toHex()); + }); + return svgElement; + }) + ); +} + + +export const createColorMarkerShapeURI = (iconRegistry: MatIconRegistry, domSanitizer: DomSanitizer, shape: MarkerShape, color: tinycolor.Instance): Observable => { + return createColorMarkerShape(iconRegistry, domSanitizer, shape, color).pipe( + map((svgElement) => { + const svg = svgElement.outerHTML; + return 'data:image/svg+xml;base64,' + btoa(svg); + }) + ); +} + +const createIconElement = (iconRegistry: MatIconRegistry, icon: string, size: number, color: tinycolor.Instance): Observable => { + const isSvg = isSvgIcon(icon); + const iconColor = tinycolor.mix(color, tinycolor('rgba(0,0,0,0.38)')); + if (isSvg) { + const [namespace, iconName] = splitIconName(icon); + return iconRegistry + .getNamedSvgIcon(iconName, namespace) + .pipe( + take(1), + map((svgElement) => { + const element = new Element(svgElement.firstChild); + element.fill('#'+iconColor.toHex()); + //const box = element.bbox(); + const scale = size / 24;//box.height; + element.scale(scale); + return element; + }), + catchError(() => of(null)) + ); + } else { + const iconName = splitIconName(icon)[1]; + const textElement = new Text(document.createElementNS('http://www.w3.org/2000/svg', 'text')); + const fontSetClasses = ( + iconRegistry.getDefaultFontSetClass() + ).filter(className => className.length > 0); + fontSetClasses.forEach(className => textElement.addClass(className)); + textElement.font({size: `${size}px`}); + textElement.attr({ + style: `font-size: ${size}px`, + 'text-anchor': 'start' + }); + textElement.fill('#'+iconColor.toHex()); + const tspan = textElement.tspan(iconName); + tspan.attr({ + 'dominant-baseline': 'hanging' + }); + return of(textElement); + } +} + +export const createColorMarkerIconElement = (iconRegistry: MatIconRegistry, domSanitizer: DomSanitizer, icon: string, color: tinycolor.Instance): Observable => { + return createColorMarkerShape(iconRegistry, domSanitizer, MarkerShape.markerShape6, color).pipe( + switchMap((svgElement) => { + return createIconElement(iconRegistry, icon, 12, color).pipe( + map((iconElement) => { + let elements = svgElement.getElementsByClassName('marker-icon-container'); + if (iconElement && elements.length) { + const iconContainer = new G(elements[0] as SVGGElement); + iconContainer.add(iconElement); + const box = iconElement.bbox(); + iconElement.translate(-box.cx, -box.cy); + } + elements = svgElement.getElementsByClassName('marker-icon-background'); + if (elements.length) { + (elements[0] as SVGGElement).style.display = ''; + } + return svgElement; + }) + ); + }) + ); +} + diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/map/map-data-layer-dialog.component.html b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/map/map-data-layer-dialog.component.html index 9ecaac60a7..b84d61f354 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/map/map-data-layer-dialog.component.html +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/map/map-data-layer-dialog.component.html @@ -181,13 +181,18 @@
{{ 'widgets.maps.data-layer.marker.marker-type' | translate }}
- {{ 'widgets.maps.data-layer.marker.marker-type-default' | translate }} + {{ 'widgets.maps.data-layer.marker.marker-type-shape' | translate }} + {{ 'widgets.maps.data-layer.marker.marker-type-icon' | translate }} {{ 'widgets.maps.data-layer.marker.marker-type-image' | translate }}
-
-
widgets.maps.data-layer.color
- +
+
widgets.maps.data-layer.marker.shape
+ +
+
+
widgets.maps.data-layer.marker.icon
+
widgets.maps.data-layer.marker.image
diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/map/map-data-layer-dialog.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/map/map-data-layer-dialog.component.ts index 6b15b49580..52bec3c42f 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/map/map-data-layer-dialog.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/map/map-data-layer-dialog.component.ts @@ -114,7 +114,8 @@ export class MapDataLayerDialogComponent extends DialogComponent +
+ + +
px
+
+ + +
diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/map/marker-shape-settings.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/map/marker-shape-settings.component.ts new file mode 100644 index 0000000000..cd8ddb25b9 --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/map/marker-shape-settings.component.ts @@ -0,0 +1,204 @@ +/// +/// Copyright © 2016-2024 The Thingsboard Authors +/// +/// Licensed under the Apache License, Version 2.0 (the "License"); +/// you may not use this file except in compliance with the License. +/// You may obtain a copy of the License at +/// +/// http://www.apache.org/licenses/LICENSE-2.0 +/// +/// Unless required by applicable law or agreed to in writing, software +/// distributed under the License is distributed on an "AS IS" BASIS, +/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +/// See the License for the specific language governing permissions and +/// limitations under the License. +/// + +import { + ChangeDetectorRef, + Component, + DestroyRef, + forwardRef, + Input, + OnInit, + Renderer2, + ViewContainerRef +} from '@angular/core'; +import { + ControlValueAccessor, + NG_VALUE_ACCESSOR, + UntypedFormBuilder, + UntypedFormGroup, + Validators +} from '@angular/forms'; +import { MatButton } from '@angular/material/button'; +import { TbPopoverService } from '@shared/components/popover.service'; +import { MarkerIconSettings, MarkerShapeSettings, MarkerType } from '@home/components/widget/lib/maps/map.models'; +import { takeUntilDestroyed } from '@angular/core/rxjs-interop'; +import { Observable } from 'rxjs'; +import { DomSanitizer, SafeHtml, SafeUrl } from '@angular/platform-browser'; +import { MatIconRegistry } from '@angular/material/icon'; +import { + createColorMarkerIconElement, + createColorMarkerShapeURI +} from '@home/components/widget/lib/maps/marker-shape.models'; +import tinycolor from 'tinycolor2'; +import { map, share } from 'rxjs/operators'; +import { MarkerShapesComponent } from '@home/components/widget/lib/settings/common/map/marker-shapes.component'; +import { MaterialIconsComponent } from '@shared/components/material-icons.component'; + +@Component({ + selector: 'tb-marker-shape-settings', + templateUrl: './marker-shape-settings.component.html', + styleUrls: [], + providers: [ + { + provide: NG_VALUE_ACCESSOR, + useExisting: forwardRef(() => MarkerShapeSettingsComponent), + multi: true + } + ] +}) +export class MarkerShapeSettingsComponent implements ControlValueAccessor, OnInit { + + MarkerType = MarkerType; + + @Input() + disabled: boolean; + + @Input() + markerType: MarkerType; + + modelValue: MarkerShapeSettings | MarkerIconSettings; + + public shapeSettingsFormGroup: UntypedFormGroup; + + public shapePreview$: Observable; + public iconPreview$: Observable; + + private propagateChange: (v: any) => void = () => { }; + + constructor(private popoverService: TbPopoverService, + private fb: UntypedFormBuilder, + private destroyRef: DestroyRef, + private iconRegistry: MatIconRegistry, + private domSanitizer: DomSanitizer, + private renderer: Renderer2, + private cd: ChangeDetectorRef, + private viewContainerRef: ViewContainerRef) {} + + ngOnInit(): void { + this.shapeSettingsFormGroup = this.fb.group({ + size: [null, [Validators.required, Validators.min(1)]], + color: [null, [Validators.required]] + }); + if (this.markerType === MarkerType.shape) { + this.shapeSettingsFormGroup.addControl('shape', this.fb.control(null, [Validators.required])); + } + if (this.markerType === MarkerType.icon) { + this.shapeSettingsFormGroup.addControl('icon', this.fb.control(null, [Validators.required])); + } + this.shapeSettingsFormGroup.valueChanges.pipe( + takeUntilDestroyed(this.destroyRef) + ).subscribe(() => { + this.updateModel(); + }); + } + + registerOnChange(fn: any): void { + this.propagateChange = fn; + } + + registerOnTouched(fn: any): void { + } + + setDisabledState(isDisabled: boolean): void { + this.disabled = isDisabled; + if (isDisabled) { + this.shapeSettingsFormGroup.disable({emitEvent: false}); + } else { + this.shapeSettingsFormGroup.enable({emitEvent: false}); + } + } + + writeValue(value: MarkerShapeSettings | MarkerIconSettings): void { + this.modelValue = value; + this.shapeSettingsFormGroup.patchValue( + value, {emitEvent: false} + ); + this.updatePreview(); + } + + openShapePopup($event: Event, matButton: MatButton) { + if ($event) { + $event.stopPropagation(); + } + const trigger = matButton._elementRef.nativeElement; + if (this.popoverService.hasPopover(trigger)) { + this.popoverService.hidePopover(trigger); + } else { + if (this.markerType === MarkerType.shape) { + const ctx: any = { + shape: (this.modelValue as MarkerShapeSettings).shape, + color: this.modelValue.color.color + }; + const markerShapesPopover = this.popoverService.displayPopover(trigger, this.renderer, + this.viewContainerRef, MarkerShapesComponent, 'left', true, null, + ctx, + {}, + {}, {}, true); + markerShapesPopover.tbComponentRef.instance.popover = markerShapesPopover; + markerShapesPopover.tbComponentRef.instance.markerShapeSelected.subscribe((shape) => { + markerShapesPopover.hide(); + this.shapeSettingsFormGroup.get('shape').patchValue( + shape + ); + }); + } else if (this.markerType === MarkerType.icon) { + const ctx: any = { + selectedIcon: (this.modelValue as MarkerIconSettings).icon, + iconClearButton: false + }; + const materialIconsPopover = this.popoverService.displayPopover(trigger, this.renderer, + this.viewContainerRef, MaterialIconsComponent, 'left', true, null, + ctx, + {}, + {}, {}, true); + materialIconsPopover.tbComponentRef.instance.popover = materialIconsPopover; + materialIconsPopover.tbComponentRef.instance.iconSelected.subscribe((icon) => { + materialIconsPopover.hide(); + this.shapeSettingsFormGroup.get('icon').patchValue( + icon + ); + }); + } + } + } + + private updateModel() { + this.modelValue = this.shapeSettingsFormGroup.getRawValue(); + this.propagateChange(this.modelValue); + this.updatePreview(); + } + + private updatePreview() { + const color = this.modelValue.color.color; + if (this.markerType === MarkerType.shape) { + const shape = (this.modelValue as MarkerShapeSettings).shape; + this.shapePreview$ = createColorMarkerShapeURI(this.iconRegistry, this.domSanitizer, shape, tinycolor(color)).pipe( + map((url) => { + return this.domSanitizer.bypassSecurityTrustUrl(url); + }), + share() + ); + } else if (this.markerType === MarkerType.icon) { + const icon = (this.modelValue as MarkerIconSettings).icon; + this.iconPreview$ = createColorMarkerIconElement(this.iconRegistry, this.domSanitizer, icon, tinycolor(color)).pipe( + map((element) => { + return this.domSanitizer.bypassSecurityTrustHtml(element.outerHTML); + }), + share() + ); + } + } +} diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/map/marker-shapes.component.html b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/map/marker-shapes.component.html new file mode 100644 index 0000000000..09d7b654e8 --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/map/marker-shapes.component.html @@ -0,0 +1,38 @@ + +
+
widgets.maps.data-layer.marker.marker-shapes
+
+ + + + +
+
diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/map/marker-shapes.component.scss b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/map/marker-shapes.component.scss new file mode 100644 index 0000000000..3e9dfb9a52 --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/map/marker-shapes.component.scss @@ -0,0 +1,39 @@ +/** + * Copyright © 2016-2024 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +.tb-marker-shapes-panel { + width: 100%; + display: flex; + flex-direction: column; + gap: 16px; + .tb-marker-shapes-title { + font-size: 16px; + font-weight: 500; + line-height: 24px; + letter-spacing: 0.25px; + color: rgba(0, 0, 0, 0.87); + } + button.mat-mdc-button-base.tb-select-shape-button { + width: 42px; + min-width: 42px; + height: 42px; + padding: 4px; + img.tb-marker-shape { + width: 34px; + height: 34px; + object-fit: contain; + } + } +} diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/map/marker-shapes.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/map/marker-shapes.component.ts new file mode 100644 index 0000000000..b225bf535c --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/map/marker-shapes.component.ts @@ -0,0 +1,84 @@ +/// +/// Copyright © 2016-2024 The Thingsboard Authors +/// +/// Licensed under the Apache License, Version 2.0 (the "License"); +/// you may not use this file except in compliance with the License. +/// You may obtain a copy of the License at +/// +/// http://www.apache.org/licenses/LICENSE-2.0 +/// +/// Unless required by applicable law or agreed to in writing, software +/// distributed under the License is distributed on an "AS IS" BASIS, +/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +/// See the License for the specific language governing permissions and +/// limitations under the License. +/// + +import { Component, EventEmitter, Input, OnInit, Output, ViewEncapsulation } from '@angular/core'; +import { PageComponent } from '@shared/components/page.component'; +import { TbPopoverComponent } from '@shared/components/popover.component'; +import { Store } from '@ngrx/store'; +import { AppState } from '@core/core.state'; +import { createColorMarkerShapeURI, MarkerShape } from '@home/components/widget/lib/maps/marker-shape.models'; +import { Observable } from 'rxjs'; +import { DomSanitizer, SafeUrl } from '@angular/platform-browser'; +import { MatIconRegistry } from '@angular/material/icon'; +import tinycolor from 'tinycolor2'; +import { map, share } from 'rxjs/operators'; + +interface MarkerShapeInfo { + shape: MarkerShape; + url$: Observable; +} + +@Component({ + selector: 'tb-marker-shapes', + templateUrl: './marker-shapes.component.html', + providers: [], + styleUrls: ['./marker-shapes.component.scss'], + encapsulation: ViewEncapsulation.None +}) +export class MarkerShapesComponent extends PageComponent implements OnInit { + + @Input() + shape: MarkerShape; + + @Input() + color: string; + + @Input() + popover: TbPopoverComponent; + + @Output() + markerShapeSelected = new EventEmitter(); + + shapes: MarkerShapeInfo[]; + + constructor(protected store: Store, + private iconRegistry: MatIconRegistry, + private domSanitizer: DomSanitizer) { + super(store); + } + + ngOnInit(): void { + this.shapes = (Object.keys(MarkerShape) as MarkerShape[]).map((shape) => { + return { + shape, + url$: createColorMarkerShapeURI(this.iconRegistry, this.domSanitizer, shape, tinycolor(this.color)).pipe( + map((url) => { + return this.domSanitizer.bypassSecurityTrustUrl(url); + }), + share() + ) + }; + }); + } + + cancel() { + this.popover?.hide(); + } + + selectShape(shape: MarkerShape) { + this.markerShapeSelected.emit(shape); + } +} 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 91186e45ac..583f3e399c 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 @@ -219,6 +219,10 @@ import { import { DataLayerPatternSettingsComponent } from '@home/components/widget/lib/settings/common/map/data-layer-pattern-settings.component'; +import { + MarkerShapeSettingsComponent +} from '@home/components/widget/lib/settings/common/map/marker-shape-settings.component'; +import { MarkerShapesComponent } from '@home/components/widget/lib/settings/common/map/marker-shapes.component'; @NgModule({ declarations: [ @@ -295,6 +299,8 @@ import { DataLayerColorSettingsComponent, DataLayerColorSettingsPanelComponent, DataLayerPatternSettingsComponent, + MarkerShapeSettingsComponent, + MarkerShapesComponent, MarkerImageSettingsComponent, MarkerImageSettingsPanelComponent, MapDataLayerDialogComponent, 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 413a409b65..b01a2598d9 100644 --- a/ui-ngx/src/assets/locale/locale.constant-en_US.json +++ b/ui-ngx/src/assets/locale/locale.constant-en_US.json @@ -6911,9 +6911,13 @@ "marker-configuration": "Marker configuration", "remove-marker": "Remove marker", "marker-type": "Marker type", - "marker-type-default": "Default", + "marker-type-shape": "Shape", + "marker-type-icon": "Icon", "marker-type-image": "Image", + "shape": "Shape", + "icon": "Icon", "image": "Image", + "marker-shapes": "Marker shapes", "marker-image": "Marker image", "marker-image-type-image": "Image", "marker-image-type-function": "Function", diff --git a/ui-ngx/src/assets/markers/shape1.svg b/ui-ngx/src/assets/markers/shape1.svg new file mode 100644 index 0000000000..fafd5638e7 --- /dev/null +++ b/ui-ngx/src/assets/markers/shape1.svg @@ -0,0 +1,19 @@ + + + + + + + + + + + + + + + + + + + diff --git a/ui-ngx/src/assets/markers/shape10.svg b/ui-ngx/src/assets/markers/shape10.svg new file mode 100644 index 0000000000..f0d8b68d2f --- /dev/null +++ b/ui-ngx/src/assets/markers/shape10.svg @@ -0,0 +1,16 @@ + + + + + + + + + + + + + + + + diff --git a/ui-ngx/src/assets/markers/shape2.svg b/ui-ngx/src/assets/markers/shape2.svg new file mode 100644 index 0000000000..6777c1810e --- /dev/null +++ b/ui-ngx/src/assets/markers/shape2.svg @@ -0,0 +1,17 @@ + + + + + + + + + + + + + + + + + diff --git a/ui-ngx/src/assets/markers/shape3.svg b/ui-ngx/src/assets/markers/shape3.svg new file mode 100644 index 0000000000..eb2fa563b7 --- /dev/null +++ b/ui-ngx/src/assets/markers/shape3.svg @@ -0,0 +1,19 @@ + + + + + + + + + + + + + + + + + + + diff --git a/ui-ngx/src/assets/markers/shape4.svg b/ui-ngx/src/assets/markers/shape4.svg new file mode 100644 index 0000000000..5303d47ba6 --- /dev/null +++ b/ui-ngx/src/assets/markers/shape4.svg @@ -0,0 +1,21 @@ + + + + + + + + + + + + + + + + + + + + + diff --git a/ui-ngx/src/assets/markers/shape5.svg b/ui-ngx/src/assets/markers/shape5.svg new file mode 100644 index 0000000000..db3c2e0118 --- /dev/null +++ b/ui-ngx/src/assets/markers/shape5.svg @@ -0,0 +1,20 @@ + + + + + + + + + + + + + + + + + + + + diff --git a/ui-ngx/src/assets/markers/shape6.svg b/ui-ngx/src/assets/markers/shape6.svg new file mode 100644 index 0000000000..f4de6d140d --- /dev/null +++ b/ui-ngx/src/assets/markers/shape6.svg @@ -0,0 +1,22 @@ + + + + + + + + + + + + + + + + + + + + + + diff --git a/ui-ngx/src/assets/markers/shape7.svg b/ui-ngx/src/assets/markers/shape7.svg new file mode 100644 index 0000000000..c7df181b78 --- /dev/null +++ b/ui-ngx/src/assets/markers/shape7.svg @@ -0,0 +1,28 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/ui-ngx/src/assets/markers/shape8.svg b/ui-ngx/src/assets/markers/shape8.svg new file mode 100644 index 0000000000..1fecf058d9 --- /dev/null +++ b/ui-ngx/src/assets/markers/shape8.svg @@ -0,0 +1,17 @@ + + + + + + + + + + + + + + + + + diff --git a/ui-ngx/src/assets/markers/shape9.svg b/ui-ngx/src/assets/markers/shape9.svg new file mode 100644 index 0000000000..f44a6b3626 --- /dev/null +++ b/ui-ngx/src/assets/markers/shape9.svg @@ -0,0 +1,19 @@ + + + + + + + + + + + + + + + + + + + From cd8d172e122a572e23f6995344e9f33527199caa Mon Sep 17 00:00:00 2001 From: Igor Kulikov Date: Tue, 21 Jan 2025 19:42:21 +0200 Subject: [PATCH 027/345] UI: Implement map markers clustering. --- .../widget/lib/maps-legacy/leaflet-map.ts | 8 +- .../widget/lib/maps-legacy/markers.ts | 4 +- .../lib/maps/data-layer/circles-data-layer.ts | 120 +++ .../lib/maps/data-layer/map-data-layer.ts | 361 +++++++ .../lib/maps/data-layer/markers-data-layer.ts | 506 +++++++++ .../maps/data-layer/polygons-data-layer.ts | 135 +++ .../lib/maps/data-layer/shapes-data-layer.ts | 52 + .../widget/lib/maps/leaflet/leaflet-tb.ts | 5 + .../widget/lib/maps/map-data-layer.ts | 968 ------------------ .../components/widget/lib/maps/map-layer.ts | 2 +- .../widget/lib/maps/map-widget.component.ts | 2 +- .../widget/lib/maps/map-widget.models.ts | 2 +- .../home/components/widget/lib/maps/map.scss | 11 + .../home/components/widget/lib/maps/map.ts | 26 +- .../lib/maps/{ => models}/map.models.ts | 34 +- .../maps/{ => models}/marker-shape.models.ts | 0 ...ta-layer-color-settings-panel.component.ts | 2 +- .../data-layer-color-settings.component.ts | 2 +- .../data-layer-pattern-settings.component.ts | 2 +- .../map/map-data-layer-dialog.component.html | 4 + .../map/map-data-layer-dialog.component.ts | 3 +- .../map/map-data-layer-row.component.ts | 2 +- .../common/map/map-data-layers.component.ts | 2 +- .../common/map/map-layer-row.component.ts | 2 +- .../map/map-layer-settings-panel.component.ts | 2 +- .../common/map/map-layers.component.ts | 2 +- .../common/map/map-settings.component.ts | 2 +- .../marker-clustering-settings.component.html | 98 ++ .../marker-clustering-settings.component.ts | 156 +++ .../marker-image-settings-panel.component.ts | 2 +- .../map/marker-image-settings.component.ts | 2 +- .../map/marker-shape-settings.component.ts | 4 +- .../common/map/marker-shapes.component.ts | 2 +- .../common/widget-settings-common.module.ts | 4 + .../assets/locale/locale.constant-en_US.json | 16 +- ui-ngx/src/typings/leaflet-extend-tb.d.ts | 4 +- 36 files changed, 1536 insertions(+), 1013 deletions(-) create mode 100644 ui-ngx/src/app/modules/home/components/widget/lib/maps/data-layer/circles-data-layer.ts create mode 100644 ui-ngx/src/app/modules/home/components/widget/lib/maps/data-layer/map-data-layer.ts create mode 100644 ui-ngx/src/app/modules/home/components/widget/lib/maps/data-layer/markers-data-layer.ts create mode 100644 ui-ngx/src/app/modules/home/components/widget/lib/maps/data-layer/polygons-data-layer.ts create mode 100644 ui-ngx/src/app/modules/home/components/widget/lib/maps/data-layer/shapes-data-layer.ts delete mode 100644 ui-ngx/src/app/modules/home/components/widget/lib/maps/map-data-layer.ts rename ui-ngx/src/app/modules/home/components/widget/lib/maps/{ => models}/map.models.ts (97%) rename ui-ngx/src/app/modules/home/components/widget/lib/maps/{ => models}/marker-shape.models.ts (100%) create mode 100644 ui-ngx/src/app/modules/home/components/widget/lib/settings/common/map/marker-clustering-settings.component.html create mode 100644 ui-ngx/src/app/modules/home/components/widget/lib/settings/common/map/marker-clustering-settings.component.ts diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/maps-legacy/leaflet-map.ts b/ui-ngx/src/app/modules/home/components/widget/lib/maps-legacy/leaflet-map.ts index 2a390a39fc..9c522abf94 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/maps-legacy/leaflet-map.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/maps-legacy/leaflet-map.ts @@ -16,9 +16,7 @@ import L, { FeatureGroup, LatLngBounds, LatLngTuple, PointExpression, Projection } from 'leaflet'; import tinycolor from 'tinycolor2'; -import 'leaflet-providers'; -import 'leaflet.markercluster'; -import '@geoman-io/leaflet-geoman-free'; +import '@home/components/widget/lib/maps/leaflet/leaflet-tb'; import { CircleData, @@ -125,10 +123,6 @@ export default abstract class LeafletMap { private initMarkerClusterSettings() { const markerClusteringSettings: WidgetMarkerClusteringSettings = this.options; if (markerClusteringSettings.useClusterMarkers) { - // disabled marker cluster icon - (L as any).MarkerCluster = (L as any).MarkerCluster.extend({ - options: { pmIgnore: true, ...L.Icon.prototype.options } - }); this.clusteringSettings = { spiderfyOnMaxZoom: markerClusteringSettings.spiderfyOnMaxZoom, zoomToBoundsOnClick: markerClusteringSettings.zoomOnClick, diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/maps-legacy/markers.ts b/ui-ngx/src/app/modules/home/components/widget/lib/maps-legacy/markers.ts index d62b2b9217..c642768a4f 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/maps-legacy/markers.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/maps-legacy/markers.ts @@ -52,7 +52,7 @@ export class Marker { this.leafletMarker = L.marker(this.location, { pmIgnore: !settings.draggableMarker, snapIgnore: !snappable, - tbMarkerData: this.data + tbMarkerData: this.data as any }); this.markerOffset = [ @@ -101,7 +101,7 @@ export class Marker { setDataSources(data: FormattedData, dataSources: FormattedData[]) { this.data = data; this.dataSources = dataSources; - this.leafletMarker.options.tbMarkerData = data; + this.leafletMarker.options.tbMarkerData = data as any; } updateMarkerTooltip(data: FormattedData) { diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/maps/data-layer/circles-data-layer.ts b/ui-ngx/src/app/modules/home/components/widget/lib/maps/data-layer/circles-data-layer.ts new file mode 100644 index 0000000000..d53d30e6e5 --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/widget/lib/maps/data-layer/circles-data-layer.ts @@ -0,0 +1,120 @@ +/// +/// Copyright © 2016-2024 The Thingsboard Authors +/// +/// Licensed under the Apache License, Version 2.0 (the "License"); +/// you may not use this file except in compliance with the License. +/// You may obtain a copy of the License at +/// +/// http://www.apache.org/licenses/LICENSE-2.0 +/// +/// Unless required by applicable law or agreed to in writing, software +/// distributed under the License is distributed on an "AS IS" BASIS, +/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +/// See the License for the specific language governing permissions and +/// limitations under the License. +/// + +import { + CirclesDataLayerSettings, + defaultBaseCirclesDataLayerSettings, isJSON, TbCircleData, + TbMapDatasource +} from '@home/components/widget/lib/maps/models/map.models'; +import L from 'leaflet'; +import { FormattedData } from '@shared/models/widget.models'; +import { TbShapesDataLayer } from '@home/components/widget/lib/maps/data-layer/shapes-data-layer'; +import { TbMap } from '@home/components/widget/lib/maps/map'; +import { Observable } from 'rxjs'; +import { isNotEmptyStr } from '@core/utils'; +import { MapDataLayerType, TbDataLayerItem } from '@home/components/widget/lib/maps/data-layer/map-data-layer'; + +class TbCircleDataLayerItem extends TbDataLayerItem { + + private circle: L.Circle; + + constructor(data: FormattedData, + dsData: FormattedData[], + protected settings: CirclesDataLayerSettings, + protected dataLayer: TbCirclesDataLayer) { + super(data, dsData, settings, dataLayer); + } + + protected create(data: FormattedData, dsData: FormattedData[]): L.Layer { + const circleData = this.dataLayer.extractCircleCoordinates(data); + const center = new L.LatLng(circleData.latitude, circleData.longitude); + const style = this.dataLayer.getShapeStyle(data, dsData); + this.circle = L.circle(center, { + radius: circleData.radius, + ...style + }); + this.updateLabel(data, dsData); + return this.circle; + } + + protected createEventListeners(data: FormattedData, _dsData: FormattedData[]): void { + this.dataLayer.getMap().circleClick(this.circle, data.$datasource); + } + + protected unbindLabel() { + this.circle.unbindTooltip(); + } + + protected bindLabel(content: L.Content): void { + this.circle.bindTooltip(content, { className: 'tb-polygon-label', permanent: true, direction: 'center'}) + .openTooltip(this.circle.getLatLng()); + } + + public update(data: FormattedData, dsData: FormattedData[]): void { + const circleData = this.dataLayer.extractCircleCoordinates(data); + const center = new L.LatLng(circleData.latitude, circleData.longitude); + if (!this.circle.getLatLng().equals(center)) { + this.circle.setLatLng(center); + } + if (this.circle.getRadius() !== circleData.radius) { + this.circle.setRadius(circleData.radius); + } + this.updateTooltip(data, dsData); + this.updateLabel(data, dsData); + const style = this.dataLayer.getShapeStyle(data, dsData); + this.circle.setStyle(style); + } +} + +export class TbCirclesDataLayer extends TbShapesDataLayer { + + constructor(protected map: TbMap, + inputSettings: CirclesDataLayerSettings) { + super(map, inputSettings); + } + + public dataLayerType(): MapDataLayerType { + return MapDataLayerType.circle; + } + + protected setupDatasource(datasource: TbMapDatasource): TbMapDatasource { + datasource.dataKeys.push(this.settings.circleKey); + return datasource; + } + + protected defaultBaseSettings(): Partial { + return defaultBaseCirclesDataLayerSettings; + } + + protected doSetup(): Observable { + return super.doSetup(); + } + + protected isValidLayerData(layerData: FormattedData): boolean { + return layerData && isNotEmptyStr(layerData[this.settings.circleKey.label]) && isJSON(layerData[this.settings.circleKey.label]); + } + + protected createLayerItem(data: FormattedData, dsData: FormattedData[]): TbDataLayerItem { + throw new TbCircleDataLayerItem(data, dsData, this.settings, this); + } + + public extractCircleCoordinates(data: FormattedData) { + const circleData: TbCircleData = JSON.parse(data[this.settings.circleKey.label]); + return this.map.convertCircleData(circleData); + } + + +} diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/maps/data-layer/map-data-layer.ts b/ui-ngx/src/app/modules/home/components/widget/lib/maps/data-layer/map-data-layer.ts new file mode 100644 index 0000000000..c14937ef00 --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/widget/lib/maps/data-layer/map-data-layer.ts @@ -0,0 +1,361 @@ +/// +/// Copyright © 2016-2024 The Thingsboard Authors +/// +/// Licensed under the Apache License, Version 2.0 (the "License"); +/// you may not use this file except in compliance with the License. +/// You may obtain a copy of the License at +/// +/// http://www.apache.org/licenses/LICENSE-2.0 +/// +/// Unless required by applicable law or agreed to in writing, software +/// distributed under the License is distributed on an "AS IS" BASIS, +/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +/// See the License for the specific language governing permissions and +/// limitations under the License. +/// + +import { + DataLayerColorSettings, + DataLayerColorType, + DataLayerPatternSettings, + DataLayerPatternType, + DataLayerTooltipTrigger, + MapDataLayerSettings, + mapDataSourceSettingsToDatasource, + MapStringFunction, + MapType, + processTooltipTemplate, + TbMapDatasource +} from '@home/components/widget/lib/maps/models/map.models'; +import { TbMap } from '@home/components/widget/lib/maps/map'; +import { FormattedData } from '@shared/models/widget.models'; +import { forkJoin, Observable, of } from 'rxjs'; +import { + createLabelFromPattern, + guid, + isDefined, + mergeDeepIgnoreArray, + parseTbFunction, + safeExecuteTbFunction +} from '@core/utils'; +import L, { LatLngBounds } from 'leaflet'; +import { CompiledTbFunction } from '@shared/models/js-function.models'; +import { map } from 'rxjs/operators'; +import { WidgetContext } from '@home/models/widget-component.models'; +import { CustomTranslatePipe } from '@shared/pipe/custom-translate.pipe'; + +export abstract class TbDataLayerItem, L extends L.Layer = L.Layer> { + + protected layer: L; + protected tooltip: L.Popup; + + protected constructor(data: FormattedData, + dsData: FormattedData[], + protected settings: S, + protected dataLayer: D) { + this.layer = this.create(data, dsData); + if (this.settings.tooltip?.show) { + this.createTooltip(data.$datasource); + this.updateTooltip(data, dsData); + } + this.createEventListeners(data, dsData); + try { + this.dataLayer.getDataLayerContainer().addLayer(this.layer); + } catch (e) { + console.warn(e); + } + } + + protected abstract create(data: FormattedData, dsData: FormattedData[]): L; + + protected abstract unbindLabel(): void; + + protected abstract bindLabel(content: L.Content): void; + + protected abstract createEventListeners(data: FormattedData, dsData: FormattedData[]): void; + + public abstract update(data: FormattedData, dsData: FormattedData[]): void; + + public remove() { + this.layer.off(); + this.dataLayer.getDataLayerContainer().removeLayer(this.layer); + } + + public getLayer(): L { + return this.layer; + } + + protected updateTooltip(data: FormattedData, dsData: FormattedData[]) { + if (this.settings.tooltip.show) { + let tooltipTemplate = this.dataLayer.dataLayerTooltipProcessor.processPattern(data, dsData); + tooltipTemplate = processTooltipTemplate(tooltipTemplate); + this.tooltip.setContent(tooltipTemplate); + if (this.tooltip.isOpen() && this.tooltip.getElement()) { + this.bindTooltipActions(data.$datasource); + } + } + } + + protected updateLabel(data: FormattedData, dsData: FormattedData[]) { + if (this.settings.label.show) { + this.unbindLabel(); + const label = this.dataLayer.dataLayerLabelProcessor.processPattern(data, dsData); + const labelColor = this.dataLayer.getCtx().widgetConfig.color; + const content: L.Content = `
${label}
`; + this.bindLabel(content); + } + } + + private createTooltip(datasource: TbMapDatasource) { + this.tooltip = L.popup(); + this.layer.bindPopup(this.tooltip, {autoClose: this.settings.tooltip.autoclose, closeOnClick: false}); + if (this.settings.tooltip.trigger === DataLayerTooltipTrigger.hover) { + this.layer.off('click'); + this.layer.on('mouseover', () => { + this.layer.openPopup(); + }); + this.layer.on('mousemove', (e) => { + this.tooltip.setLatLng(e.latlng); + }); + this.layer.on('mouseout', () => { + this.layer.closePopup(); + }); + } + this.layer.on('popupopen', () => { + this.bindTooltipActions(datasource); + (this.layer as any)._popup._closeButton.addEventListener('click', (event: Event) => { + event.preventDefault(); + }); + }); + } + + private bindTooltipActions(datasource: TbMapDatasource) { + const actions = this.tooltip.getElement().getElementsByClassName('tb-custom-action'); + Array.from(actions).forEach( + (element: HTMLElement) => { + const actionName = element.getAttribute('data-action-name'); + this.dataLayer.getMap().tooltipElementClick(element, actionName, datasource); + }); + } + +} + +export enum MapDataLayerType { + marker = 'marker', + polygon = 'polygon', + circle = 'circle' +} + +class DataLayerPatternProcessor { + + private patternFunction: CompiledTbFunction; + private pattern: string; + + constructor(private dataLayer: TbMapDataLayer, + private settings: DataLayerPatternSettings) {} + + public setup(): Observable { + if (this.settings.type === DataLayerPatternType.function) { + return parseTbFunction(this.dataLayer.getCtx().http, this.settings.patternFunction, ['data', 'dsData']).pipe( + map((parsed) => { + this.patternFunction = parsed; + return null; + }) + ); + } else { + this.pattern = this.settings.pattern; + return of(null) + } + } + + public processPattern(data: FormattedData, dsData: FormattedData[]): string { + let pattern: string; + if (this.settings.type === DataLayerPatternType.function) { + pattern = safeExecuteTbFunction(this.patternFunction, [data, dsData]); + } else { + pattern = this.pattern; + } + const text = createLabelFromPattern(pattern, data); + const customTranslate = this.dataLayer.getCtx().$injector.get(CustomTranslatePipe); + return customTranslate.transform(text); + } + +} + +export class DataLayerColorProcessor { + + private colorFunction: CompiledTbFunction; + private color: string; + + constructor(private dataLayer: TbMapDataLayer, + private settings: DataLayerColorSettings) {} + + public setup(): Observable { + if (this.settings.type === DataLayerColorType.function) { + return parseTbFunction(this.dataLayer.getCtx().http, this.settings.colorFunction, ['data', 'dsData']).pipe( + map((parsed) => { + this.colorFunction = parsed; + return null; + }) + ); + } else { + this.color = this.settings.color; + return of(null) + } + } + + public processColor(data: FormattedData, dsData: FormattedData[]): string { + let color: string; + if (this.settings.type === DataLayerColorType.function) { + color = safeExecuteTbFunction(this.colorFunction, [data, dsData]); + } else { + color = this.color; + } + return color; + } + +} + +export abstract class TbMapDataLayer, L extends L.Layer = L.Layer> implements L.TB.DataLayer { + + protected settings: S; + + protected datasource: TbMapDatasource; + + protected mapDataId = guid(); + + protected dataLayerContainer: L.FeatureGroup; + + protected layerItems = new Map>(); + + protected groupsState: {[group: string]: boolean} = {}; + + protected enabled = true; + + public dataLayerLabelProcessor: DataLayerPatternProcessor; + public dataLayerTooltipProcessor: DataLayerPatternProcessor; + + protected constructor(protected map: TbMap, + inputSettings: S) { + this.settings = mergeDeepIgnoreArray({} as S, this.defaultBaseSettings() as S, inputSettings); + if (this.settings.groups?.length) { + this.settings.groups.forEach((group) => { + this.groupsState[group] = true; + }); + } + this.dataLayerContainer = this.createDataLayerContainer(); + this.dataLayerLabelProcessor = this.settings.label.show ? new DataLayerPatternProcessor(this, this.settings.label) : null; + this.dataLayerTooltipProcessor = this.settings.tooltip.show ? new DataLayerPatternProcessor(this, this.settings.tooltip): null; + this.map.getMap().addLayer(this.dataLayerContainer); + } + + public setup(): Observable { + this.datasource = mapDataSourceSettingsToDatasource(this.settings); + this.datasource.dataKeys = this.settings.additionalDataKeys ? [...this.settings.additionalDataKeys] : []; + this.mapDataId = this.datasource.mapDataIds[0]; + this.datasource = this.setupDatasource(this.datasource); + return forkJoin( + [ + this.dataLayerLabelProcessor ? this.dataLayerLabelProcessor.setup() : of(null), + this.dataLayerTooltipProcessor ? this.dataLayerTooltipProcessor.setup() : of(null), + this.doSetup() + ]); + } + + public getDatasource(): TbMapDatasource { + return this.datasource; + } + + public getDataLayerContainer(): L.FeatureGroup { + return this.dataLayerContainer; + } + + public getBounds(): LatLngBounds { + return this.dataLayerContainer.getBounds(); + } + + public isEnabled(): boolean { + return this.enabled; + } + + public getGroups(): string[] { + return this.settings.groups || []; + } + + public toggleGroup(group: string): boolean { + if (isDefined(this.groupsState[group])) { + this.groupsState[group] = !this.groupsState[group]; + const enabled = Object.values(this.groupsState).some(v => v); + if (this.enabled !== enabled) { + this.enabled = enabled; + if (this.enabled) { + this.map.getMap().addLayer(this.dataLayerContainer); + } else { + this.map.getMap().removeLayer(this.dataLayerContainer); + } + return true; + } + } + return false; + } + + public updateData(dsData: FormattedData[]) { + const layerData = dsData.filter(d => d.$datasource.mapDataIds.includes(this.mapDataId)); + const rawItems = layerData.filter(d => this.isValidLayerData(d)); + const toDelete = new Set(Array.from(this.layerItems.keys())); + const updatedItems: TbDataLayerItem[] = []; + rawItems.forEach((data) => { + let layerItem = this.layerItems.get(data.entityId); + if (layerItem) { + layerItem.update(data, dsData); + updatedItems.push(layerItem); + } else { + layerItem = this.createLayerItem(data, dsData); + this.layerItems.set(data.entityId, layerItem); + } + toDelete.delete(data.entityId); + }); + toDelete.forEach((key) => { + const item = this.layerItems.get(key); + item.remove(); + this.layerItems.delete(key); + }); + if (updatedItems.length) { + this.layerItemsUpdated(updatedItems); + } + } + + public getCtx(): WidgetContext { + return this.map.getCtx(); + } + public getMap(): TbMap { + return this.map; + } + + protected createDataLayerContainer(): L.FeatureGroup { + return L.featureGroup(); + } + + protected setupDatasource(datasource: TbMapDatasource): TbMapDatasource { + return datasource; + } + + protected layerItemsUpdated(_updatedItems: TbDataLayerItem[]): void { + } + + protected mapType(): MapType { + return this.map.type(); + } + + public abstract dataLayerType(): MapDataLayerType; + + protected abstract defaultBaseSettings(): Partial; + + protected abstract doSetup(): Observable; + + protected abstract isValidLayerData(layerData: FormattedData): boolean; + + protected abstract createLayerItem(data: FormattedData, dsData: FormattedData[]): TbDataLayerItem; + +} + 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 new file mode 100644 index 0000000000..f97a6e31b3 --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/widget/lib/maps/data-layer/markers-data-layer.ts @@ -0,0 +1,506 @@ +/// +/// Copyright © 2016-2024 The Thingsboard Authors +/// +/// Licensed under the Apache License, Version 2.0 (the "License"); +/// you may not use this file except in compliance with the License. +/// You may obtain a copy of the License at +/// +/// http://www.apache.org/licenses/LICENSE-2.0 +/// +/// Unless required by applicable law or agreed to in writing, software +/// distributed under the License is distributed on an "AS IS" BASIS, +/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +/// See the License for the specific language governing permissions and +/// limitations under the License. +/// + +import { + BaseMarkerShapeSettings, + ClusterMarkerColorFunction, + DataLayerColorType, + defaultBaseMarkersDataLayerSettings, isValidLatLng, + loadImageWithAspect, + MapStringFunction, MapType, + MarkerIconInfo, + MarkerIconSettings, + MarkerImageFunction, + MarkerImageInfo, + MarkerImageSettings, + MarkerImageType, + MarkersDataLayerSettings, + MarkerShapeSettings, + MarkerType, + TbMapDatasource +} from '@home/components/widget/lib/maps/models/map.models'; +import L, { FeatureGroup } from 'leaflet'; +import { FormattedData } from '@shared/models/widget.models'; +import { forkJoin, Observable, of } from 'rxjs'; +import { CompiledTbFunction } from '@shared/models/js-function.models'; +import { isDefined, isDefinedAndNotNull, isEmptyStr, parseTbFunction, safeExecuteTbFunction } from '@core/utils'; +import { catchError, map, switchMap } from 'rxjs/operators'; +import tinycolor from 'tinycolor2'; +import { ImagePipe } from '@shared/pipe/image.pipe'; +import { TbMap } from '@home/components/widget/lib/maps/map'; +import { + createColorMarkerIconElement, + createColorMarkerShapeURI, + MarkerShape +} from '@home/components/widget/lib/maps/models/marker-shape.models'; +import { MatIconRegistry } from '@angular/material/icon'; +import { DomSanitizer } from '@angular/platform-browser'; +import { MapDataLayerType, TbDataLayerItem, TbMapDataLayer } from '@home/components/widget/lib/maps/data-layer/map-data-layer'; + +class TbMarkerDataLayerItem extends TbDataLayerItem { + + private location: L.LatLng; + private marker: L.Marker; + private labelOffset: L.PointTuple; + + constructor(data: FormattedData, + dsData: FormattedData[], + protected settings: MarkersDataLayerSettings, + protected dataLayer: TbMarkersDataLayer) { + super(data, dsData, settings, dataLayer); + } + + protected create(data: FormattedData, dsData: FormattedData[]): L.Marker { + this.location = this.dataLayer.extractLocation(data); + this.marker = L.marker(this.location, { + tbMarkerData: data + }); + + this.updateMarkerIcon(data, dsData); + + return this.marker; + } + + protected createEventListeners(data: FormattedData, _dsData: FormattedData[]): void { + this.dataLayer.getMap().markerClick(this.marker, data.$datasource); + } + + protected unbindLabel() { + this.marker.unbindTooltip(); + } + + protected bindLabel(content: L.Content): void { + this.marker.bindTooltip(content, { className: 'tb-marker-label', permanent: true, direction: 'top', offset: this.labelOffset }); + } + + public update(data: FormattedData, dsData: FormattedData[]): void { + const position = this.dataLayer.extractLocation(data); + if (!this.marker.getLatLng().equals(position)) { + this.location = position; + this.marker.setLatLng(position); + } + this.marker.options.tbMarkerData = data; + this.updateTooltip(data, dsData); + this.updateMarkerIcon(data, dsData); + } + + private updateMarkerIcon(data: FormattedData, dsData: FormattedData[]) { + this.dataLayer.markerIconProcessor.createMarkerIcon(data, dsData).subscribe( + (iconInfo) => { + this.marker.setIcon(iconInfo.icon); + const anchor = iconInfo.icon.options.iconAnchor; + if (anchor && Array.isArray(anchor)) { + this.labelOffset = [iconInfo.size[0] / 2 - anchor[0], 10 - anchor[1]]; + } else { + this.labelOffset = [0, -iconInfo.size[1] * this.dataLayer.markerOffset[1] + 10]; + } + this.updateLabel(data, dsData); + } + ); + } +} + +abstract class MarkerIconProcessor { + + static fromSettings(dataLayer: TbMarkersDataLayer, + settings: MarkersDataLayerSettings): MarkerIconProcessor { + switch (settings.markerType) { + case MarkerType.shape: + return new ShapeMarkerIconProcessor(dataLayer, settings.markerShape); + case MarkerType.icon: + return new IconMarkerIconProcessor(dataLayer, settings.markerIcon); + case MarkerType.image: + return new ImageMarkerIconProcessor(dataLayer, settings.markerImage); + } + } + + protected constructor(protected dataLayer: TbMarkersDataLayer, + protected settings: S) {} + + public abstract setup(): Observable; + + public abstract createMarkerIcon(data: FormattedData, + dsData: FormattedData[]): Observable; + +} + +abstract class BaseColorMarkerShapeProcessor extends MarkerIconProcessor { + + private markerColorFunction: CompiledTbFunction; + + private defaultMarkerIconInfo: MarkerIconInfo; + + protected constructor(protected dataLayer: TbMarkersDataLayer, + protected settings: S) { + super(dataLayer, settings); + } + + public setup(): Observable { + const colorSettings = this.settings.color; + if (colorSettings.type === DataLayerColorType.function) { + return parseTbFunction(this.dataLayer.getCtx().http, colorSettings.colorFunction, ['data', 'dsData']).pipe( + map((parsed) => { + this.markerColorFunction = parsed; + return null; + }) + ); + } else { + const color = tinycolor(colorSettings.color); + return this.createMarkerShape(color, this.settings.size).pipe( + map((info) => { + this.defaultMarkerIconInfo = info; + return null; + } + )); + } + } + + public createMarkerIcon(data: FormattedData, dsData: FormattedData[]): Observable { + const colorSettings = this.settings.color; + if (colorSettings.type === DataLayerColorType.function) { + const functionColor = safeExecuteTbFunction(this.markerColorFunction, [data, dsData]); + let color: tinycolor.Instance; + if (isDefinedAndNotNull(functionColor)) { + color = tinycolor(functionColor); + } else { + color = tinycolor(colorSettings.color); + } + return this.createMarkerShape(color, this.settings.size); + } else { + return of(this.defaultMarkerIconInfo); + } + } + + protected abstract createMarkerShape(color: tinycolor.Instance, size: number): Observable; +} + +class ShapeMarkerIconProcessor extends BaseColorMarkerShapeProcessor { + + constructor(protected dataLayer: TbMarkersDataLayer, + protected settings: MarkerShapeSettings) { + super(dataLayer, settings); + } + + protected createMarkerShape(color: tinycolor.Instance, size: number): Observable { + return this.dataLayer.createColoredMarkerShape(this.settings.shape, color, size); + } + +} + +class IconMarkerIconProcessor extends BaseColorMarkerShapeProcessor { + + constructor(protected dataLayer: TbMarkersDataLayer, + protected settings: MarkerIconSettings) { + super(dataLayer, settings); + } + + protected createMarkerShape(color: tinycolor.Instance, size: number): Observable { + return this.dataLayer.createColoredMarkerIcon(this.settings.icon, color, size); + } + +} + +class ImageMarkerIconProcessor extends MarkerIconProcessor { + + private markerImageFunction: CompiledTbFunction; + + private defaultMarkerIconInfo: MarkerIconInfo; + + constructor(protected dataLayer: TbMarkersDataLayer, + protected settings: MarkerImageSettings) { + super(dataLayer, settings); + } + + public setup(): Observable { + if (this.settings.type === MarkerImageType.function) { + return parseTbFunction(this.dataLayer.getCtx().http, this.settings.imageFunction, ['data', 'images', 'dsData']).pipe( + map((parsed) => { + this.markerImageFunction = parsed; + return null; + }) + ); + } else { + const currentImage: MarkerImageInfo = { + url: this.settings.image, + size: this.settings.imageSize || 34 + }; + return this.loadMarkerIconInfo(currentImage).pipe( + map((iconInfo) => { + this.defaultMarkerIconInfo = iconInfo; + return null; + } + )); + } + } + + public createMarkerIcon(data: FormattedData, dsData: FormattedData[]): Observable { + if (this.settings.type === MarkerImageType.function) { + const currentImage: MarkerImageInfo = safeExecuteTbFunction(this.markerImageFunction, [data, this.settings.images, dsData]); + return this.loadMarkerIconInfo(currentImage); + } else { + return of(this.defaultMarkerIconInfo); + } + } + + private loadMarkerIconInfo(image: MarkerImageInfo): Observable { + if (image && image.url) { + return loadImageWithAspect(this.dataLayer.getCtx().$injector.get(ImagePipe), image.url).pipe( + switchMap((aspectImage) => { + if (aspectImage?.aspect) { + let width: number; + let height: number; + if (aspectImage.aspect > 1) { + width = image.size; + height = image.size / aspectImage.aspect; + } else { + width = image.size * aspectImage.aspect; + height = image.size; + } + let iconAnchor = image.markerOffset; + let popupAnchor = image.tooltipOffset; + if (!iconAnchor) { + iconAnchor = [width * this.dataLayer.markerOffset[0], height * this.dataLayer.markerOffset[1]]; + } + if (!popupAnchor) { + popupAnchor = [width * this.dataLayer.tooltipOffset[0], height * this.dataLayer.tooltipOffset[1]]; + } + const icon = L.icon({ + iconUrl: aspectImage.url, + iconSize: [width, height], + iconAnchor, + popupAnchor + }); + const iconInfo: MarkerIconInfo = { + size: [width, height], + icon + }; + return of(iconInfo); + } else { + return this.dataLayer.createDefaultMarkerIcon(); + } + }), + catchError(() => this.dataLayer.createDefaultMarkerIcon()) + ); + } else { + return this.dataLayer.createDefaultMarkerIcon(); + } + } + +} + +export class TbMarkersDataLayer extends TbMapDataLayer { + + public markerIconProcessor: MarkerIconProcessor; + + public markerOffset: L.LatLngTuple; + public tooltipOffset: L.LatLngTuple; + + private markersClusterContainer: L.MarkerClusterGroup; + private clusterMarkerColorFunction: CompiledTbFunction; + + constructor(protected map: TbMap, + inputSettings: MarkersDataLayerSettings) { + super(map, inputSettings); + } + + public dataLayerType(): MapDataLayerType { + return MapDataLayerType.marker; + } + + protected createDataLayerContainer(): FeatureGroup { + if (this.settings.markerClustering?.enable) { + return this.createMarkersClusterContainer(); + } else { + return super.createDataLayerContainer(); + } + } + + protected setupDatasource(datasource: TbMapDatasource): TbMapDatasource { + datasource.dataKeys.push(this.settings.xKey, this.settings.yKey); + return datasource; + } + + protected defaultBaseSettings(): Partial { + return defaultBaseMarkersDataLayerSettings; + } + + protected doSetup(): Observable { + this.markerOffset = [ + isDefined(this.settings.markerOffsetX) ? this.settings.markerOffsetX : 0.5, + isDefined(this.settings.markerOffsetY) ? this.settings.markerOffsetY : 1, + ]; + this.tooltipOffset = [ + isDefined(this.settings.tooltip?.offsetX) ? this.settings.tooltip?.offsetX : 0, + isDefined(this.settings.tooltip?.offsetY) ? this.settings.tooltip?.offsetY : -1, + ]; + this.markerIconProcessor = MarkerIconProcessor.fromSettings(this, this.settings); + const setup$: Observable[] = []; + if (this.settings.markerClustering?.enable && this.settings.markerClustering.useClusterMarkerColorFunction) { + setup$.push( + parseTbFunction(this.getCtx().http, this.settings.markerClustering.clusterMarkerColorFunction, ['data', 'childCount']).pipe( + map((parsed) => { + this.clusterMarkerColorFunction = parsed; + return null; + }) + ) + ); + } + setup$.push(this.markerIconProcessor.setup()); + return forkJoin(setup$).pipe(map(() => null)); + } + + protected isValidLayerData(layerData: FormattedData): boolean { + return !!this.extractPosition(layerData); + } + + protected createLayerItem(data: FormattedData, dsData: FormattedData[]): TbMarkerDataLayerItem { + return new TbMarkerDataLayerItem(data, dsData, this.settings, this); + } + + protected layerItemsUpdated(updatedItems: TbDataLayerItem[]) { + if (this.settings.markerClustering?.enable) { + this.markersClusterContainer.refreshClusters(updatedItems.map(item => item.getLayer())); + } + super.layerItemsUpdated(updatedItems); + } + + private createMarkersClusterContainer(): L.FeatureGroup { + const markerClusterOptions: L.MarkerClusterGroupOptions = { + spiderfyOnMaxZoom: this.settings.markerClustering?.spiderfyOnMaxZoom, + zoomToBoundsOnClick: this.settings.markerClustering?.zoomOnClick, + showCoverageOnHover: this.settings.markerClustering?.showCoverageOnHover, + removeOutsideVisibleBounds: this.settings.markerClustering?.lazyLoad, + animate: this.settings.markerClustering?.zoomAnimation, + chunkedLoading: this.settings.markerClustering?.chunkedLoad, + pmIgnore: true, + spiderLegPolylineOptions: { + pmIgnore: true + }, + polygonOptions: { + pmIgnore: true + } + }; + if (this.settings.markerClustering?.useClusterMarkerColorFunction) { + markerClusterOptions.iconCreateFunction = (cluster) => { + const childCount = cluster.getChildCount(); + const data = cluster.getAllChildMarkers().map(clusterMarker => clusterMarker.options.tbMarkerData); + const markerColor: string = this.clusterMarkerColorFunction ? + safeExecuteTbFunction(this.clusterMarkerColorFunction, [data, childCount]) : null; + if (isDefinedAndNotNull(markerColor) && tinycolor(markerColor).isValid()) { + const parsedColor = tinycolor(markerColor); + return L.divIcon({ + html: `
` + + `
` + childCount + '
', + iconSize: new L.Point(40, 40), + className: 'tb-cluster-marker-container' + }); + } else { + let c = ' marker-cluster-'; + if (childCount < 10) { + c += 'small'; + } else if (childCount < 100) { + c += 'medium'; + } else { + c += 'large'; + } + return new L.DivIcon({ + html: '
' + childCount + '
', + className: 'marker-cluster' + c, + iconSize: new L.Point(40, 40) + }); + } + } + } + if (this.settings.markerClustering?.maxClusterRadius && this.settings.markerClustering.maxClusterRadius > 0) { + markerClusterOptions.maxClusterRadius = Math.floor(this.settings.markerClustering.maxClusterRadius); + } + if (this.settings.markerClustering?.maxZoom && this.settings.markerClustering.maxZoom >= 0 && this.settings.markerClustering.maxZoom < 19) { + markerClusterOptions.disableClusteringAtZoom = Math.floor(this.settings.markerClustering.maxZoom); + } + this.markersClusterContainer = new L.MarkerClusterGroup(markerClusterOptions); + return this.markersClusterContainer; + } + + private extractPosition(data: FormattedData): {x: number; y: number} { + if (data) { + const xKeyVal = data[this.settings.xKey.label]; + const yKeyVal = data[this.settings.yKey.label]; + switch (this.mapType()) { + case MapType.geoMap: + if (!isValidLatLng(xKeyVal, yKeyVal)) { + return null; + } + break; + case MapType.image: + if (!isDefinedAndNotNull(xKeyVal) || isEmptyStr(xKeyVal) || isNaN(xKeyVal) || !isDefinedAndNotNull(yKeyVal) || isEmptyStr(yKeyVal) || isNaN(yKeyVal)) { + return null; + } + break; + } + return {x: xKeyVal, y: yKeyVal}; + } else { + return null; + } + } + + public createDefaultMarkerIcon(): Observable { + const color = this.settings.markerShape?.color?.color || '#307FE5'; + return this.createColoredMarkerShape(MarkerShape.markerShape1, tinycolor(color)); + } + + public createColoredMarkerShape(shape: MarkerShape, color: tinycolor.Instance, size = 34): Observable { + return createColorMarkerShapeURI(this.getCtx().$injector.get(MatIconRegistry), this.getCtx().$injector.get(DomSanitizer), shape, color).pipe( + map((iconUrl) => { + return { + size: [size, size], + icon: L.icon({ + iconUrl, + iconSize: [size, size], + iconAnchor: [size * this.markerOffset[0], size * this.markerOffset[1]], + popupAnchor: [size * this.tooltipOffset[0], size * this.tooltipOffset[1]] + }) + }; + }) + ); + } + + public createColoredMarkerIcon(icon: string, color: tinycolor.Instance, size = 34): Observable { + return createColorMarkerIconElement(this.getCtx().$injector.get(MatIconRegistry), this.getCtx().$injector.get(DomSanitizer), icon, color).pipe( + map((element) => { + return { + size: [size, size], + icon: L.divIcon({ + html: element.outerHTML, + className: 'tb-marker-div-icon', + iconSize: [size, size], + iconAnchor: [size * this.markerOffset[0], size * this.markerOffset[1]], + popupAnchor: [size * this.tooltipOffset[0], size * this.tooltipOffset[1]] + }) + }; + }) + ); + } + + public extractLocation(data: FormattedData): L.LatLng { + const position = this.extractPosition(data); + if (position) { + return this.map.positionToLatLng(position); + } else { + return null; + } + } +} diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/maps/data-layer/polygons-data-layer.ts b/ui-ngx/src/app/modules/home/components/widget/lib/maps/data-layer/polygons-data-layer.ts new file mode 100644 index 0000000000..de938a2809 --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/widget/lib/maps/data-layer/polygons-data-layer.ts @@ -0,0 +1,135 @@ +/// +/// Copyright © 2016-2024 The Thingsboard Authors +/// +/// Licensed under the Apache License, Version 2.0 (the "License"); +/// you may not use this file except in compliance with the License. +/// You may obtain a copy of the License at +/// +/// http://www.apache.org/licenses/LICENSE-2.0 +/// +/// Unless required by applicable law or agreed to in writing, software +/// distributed under the License is distributed on an "AS IS" BASIS, +/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +/// See the License for the specific language governing permissions and +/// limitations under the License. +/// + +import { + defaultBasePolygonsDataLayerSettings, + isCutPolygon, isJSON, + PolygonsDataLayerSettings, + TbMapDatasource +} from '@home/components/widget/lib/maps/models/map.models'; +import L from 'leaflet'; +import { FormattedData } from '@shared/models/widget.models'; +import { TbShapesDataLayer } from '@home/components/widget/lib/maps/data-layer/shapes-data-layer'; +import { TbMap } from '@home/components/widget/lib/maps/map'; +import { Observable } from 'rxjs'; +import { isNotEmptyStr, isString } from '@core/utils'; +import { MapDataLayerType, TbDataLayerItem } from '@home/components/widget/lib/maps/data-layer/map-data-layer'; + +class TbPolygonDataLayerItem extends TbDataLayerItem { + + private polygonContainer: L.FeatureGroup; + private polygon: L.Polygon; + + constructor(data: FormattedData, + dsData: FormattedData[], + protected settings: PolygonsDataLayerSettings, + protected dataLayer: TbPolygonsDataLayer) { + super(data, dsData, settings, dataLayer); + } + + protected create(data: FormattedData, dsData: FormattedData[]): L.Layer { + const polyData = this.dataLayer.extractPolygonCoordinates(data); + const polyConstructor = isCutPolygon(polyData) || polyData.length !== 2 ? L.polygon : L.rectangle; + const style = this.dataLayer.getShapeStyle(data, dsData); + this.polygon = polyConstructor(polyData, { + ...style + }); + + this.polygonContainer = L.featureGroup(); + this.polygon.addTo(this.polygonContainer); + + this.updateLabel(data, dsData); + return this.polygonContainer; + } + + protected createEventListeners(data: FormattedData, _dsData: FormattedData[]): void { + this.dataLayer.getMap().polygonClick(this.polygonContainer, data.$datasource); + } + + protected unbindLabel() { + this.polygonContainer.unbindTooltip(); + } + + protected bindLabel(content: L.Content): void { + this.polygonContainer.bindTooltip(content, {className: 'tb-polygon-label', permanent: true, direction: 'center'}) + .openTooltip(this.polygonContainer.getBounds().getCenter()); + } + + public update(data: FormattedData, dsData: FormattedData[]): void { + const polyData = this.dataLayer.extractPolygonCoordinates(data); + const style = this.dataLayer.getShapeStyle(data, dsData); + if (isCutPolygon(polyData) || polyData.length !== 2) { + if (this.polygon instanceof L.Rectangle) { + this.polygonContainer.removeLayer(this.polygon); + this.polygon = L.polygon(polyData, { + ...style + }); + this.polygon.addTo(this.polygonContainer); + } else { + this.polygon.setLatLngs(polyData); + } + } else if (polyData.length === 2) { + const bounds = new L.LatLngBounds(polyData); + // @ts-ignore + this.leafletPoly.setBounds(bounds); + } + this.updateTooltip(data, dsData); + this.updateLabel(data, dsData); + this.polygon.setStyle(style); + } +} + +export class TbPolygonsDataLayer extends TbShapesDataLayer { + + constructor(protected map: TbMap, + inputSettings: PolygonsDataLayerSettings) { + super(map, inputSettings); + } + + public dataLayerType(): MapDataLayerType { + return MapDataLayerType.polygon; + } + + protected setupDatasource(datasource: TbMapDatasource): TbMapDatasource { + datasource.dataKeys.push(this.settings.polygonKey); + return datasource; + } + + protected defaultBaseSettings(): Partial { + return defaultBasePolygonsDataLayerSettings; + } + + protected doSetup(): Observable { + return super.doSetup(); + } + + protected isValidLayerData(layerData: FormattedData): boolean { + return layerData && ((isNotEmptyStr(layerData[this.settings.polygonKey.label]) && !isJSON(layerData[this.settings.polygonKey.label]) + || Array.isArray(layerData[this.settings.polygonKey.label]))); + } + + protected createLayerItem(data: FormattedData, dsData: FormattedData[]): TbPolygonDataLayerItem { + return new TbPolygonDataLayerItem(data, dsData, this.settings, this); + } + + public extractPolygonCoordinates(data: FormattedData) { + let rawPolyData = data[this.settings.polygonKey.label]; + if (isString(rawPolyData)) { + rawPolyData = JSON.parse(rawPolyData); + } + return this.map.toPolygonCoordinates(rawPolyData); + } +} diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/maps/data-layer/shapes-data-layer.ts b/ui-ngx/src/app/modules/home/components/widget/lib/maps/data-layer/shapes-data-layer.ts new file mode 100644 index 0000000000..30db8a0ac4 --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/widget/lib/maps/data-layer/shapes-data-layer.ts @@ -0,0 +1,52 @@ +/// +/// Copyright © 2016-2024 The Thingsboard Authors +/// +/// Licensed under the Apache License, Version 2.0 (the "License"); +/// you may not use this file except in compliance with the License. +/// You may obtain a copy of the License at +/// +/// http://www.apache.org/licenses/LICENSE-2.0 +/// +/// Unless required by applicable law or agreed to in writing, software +/// distributed under the License is distributed on an "AS IS" BASIS, +/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +/// See the License for the specific language governing permissions and +/// limitations under the License. +/// + +import { ShapeDataLayerSettings, TbMapDatasource } from '@home/components/widget/lib/maps/models/map.models'; +import L from 'leaflet'; +import { TbMap } from '@home/components/widget/lib/maps/map'; +import { forkJoin, Observable } from 'rxjs'; +import { FormattedData } from '@shared/models/widget.models'; +import { DataLayerColorProcessor, TbMapDataLayer } from '@home/components/widget/lib/maps/data-layer/map-data-layer'; + +export abstract class TbShapesDataLayer> extends TbMapDataLayer { + + public fillColorProcessor: DataLayerColorProcessor; + public strokeColorProcessor: DataLayerColorProcessor; + + protected constructor(protected map: TbMap, + inputSettings: S) { + super(map, inputSettings); + } + + protected doSetup(): Observable { + this.fillColorProcessor = new DataLayerColorProcessor(this, this.settings.fillColor); + this.strokeColorProcessor = new DataLayerColorProcessor(this, this.settings.strokeColor); + return forkJoin([this.fillColorProcessor.setup(), this.strokeColorProcessor.setup()]); + } + + public getShapeStyle(data: FormattedData, dsData: FormattedData[]): L.PathOptions { + const fill = this.fillColorProcessor.processColor(data, dsData); + const stroke = this.strokeColorProcessor.processColor(data, dsData); + return { + fill: true, + fillColor: fill, + color: stroke, + weight: this.settings.strokeWeight, + fillOpacity: 1, + opacity: 1 + }; + } +} diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/maps/leaflet/leaflet-tb.ts b/ui-ngx/src/app/modules/home/components/widget/lib/maps/leaflet/leaflet-tb.ts index 84b0d2a688..0aafc0889c 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/maps/leaflet/leaflet-tb.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/maps/leaflet/leaflet-tb.ts @@ -16,6 +16,11 @@ import L, { Coords, TB, TileLayerOptions } from 'leaflet'; import { guid } from '@core/utils'; +import 'leaflet-providers'; +import '@geoman-io/leaflet-geoman-free'; +import 'leaflet.markercluster'; + +L.MarkerCluster = L.MarkerCluster.mergeOptions({ pmIgnore: true }); class SidebarControl extends L.Control { diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/maps/map-data-layer.ts b/ui-ngx/src/app/modules/home/components/widget/lib/maps/map-data-layer.ts deleted file mode 100644 index a730c74adf..0000000000 --- a/ui-ngx/src/app/modules/home/components/widget/lib/maps/map-data-layer.ts +++ /dev/null @@ -1,968 +0,0 @@ -/// -/// Copyright © 2016-2024 The Thingsboard Authors -/// -/// Licensed under the Apache License, Version 2.0 (the "License"); -/// you may not use this file except in compliance with the License. -/// You may obtain a copy of the License at -/// -/// http://www.apache.org/licenses/LICENSE-2.0 -/// -/// Unless required by applicable law or agreed to in writing, software -/// distributed under the License is distributed on an "AS IS" BASIS, -/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -/// See the License for the specific language governing permissions and -/// limitations under the License. -/// - -import { - BaseMarkerShapeSettings, - CirclesDataLayerSettings, - DataLayerColorSettings, - DataLayerColorType, - DataLayerPatternSettings, - DataLayerPatternType, - DataLayerTooltipTrigger, - defaultBaseCirclesDataLayerSettings, - defaultBaseMarkersDataLayerSettings, - defaultBasePolygonsDataLayerSettings, - isCutPolygon, - isJSON, - isValidLatLng, - loadImageWithAspect, - MapDataLayerSettings, - mapDataSourceSettingsToDatasource, - MapStringFunction, - MapType, - MarkerIconInfo, - MarkerIconSettings, - MarkerImageFunction, - MarkerImageInfo, - MarkerImageSettings, - MarkerImageType, - MarkersDataLayerSettings, - MarkerShapeSettings, - MarkerType, - PolygonsDataLayerSettings, - processTooltipTemplate, - ShapeDataLayerSettings, - TbCircleData, - TbMapDatasource -} from '@home/components/widget/lib/maps/map.models'; -import { TbMap } from '@home/components/widget/lib/maps/map'; -import { FormattedData } from '@shared/models/widget.models'; -import { forkJoin, Observable, of } from 'rxjs'; -import { - createLabelFromPattern, - guid, - isDefined, - isDefinedAndNotNull, - isEmptyStr, - isNotEmptyStr, - isString, - mergeDeepIgnoreArray, - parseTbFunction, - safeExecuteTbFunction -} from '@core/utils'; -import L, { divIcon, LatLngBounds } from 'leaflet'; -import { CompiledTbFunction } from '@shared/models/js-function.models'; -import { catchError, map, switchMap } from 'rxjs/operators'; -import tinycolor from 'tinycolor2'; -import { WidgetContext } from '@home/models/widget-component.models'; -import { ImagePipe } from '@shared/pipe/image.pipe'; -import { CustomTranslatePipe } from '@shared/pipe/custom-translate.pipe'; -import { - createColorMarkerIconElement, - createColorMarkerShapeURI, - MarkerShape -} from '@home/components/widget/lib/maps/marker-shape.models'; -import { MatIconRegistry } from '@angular/material/icon'; -import { DomSanitizer } from '@angular/platform-browser'; - -abstract class TbDataLayerItem> { - - protected layer: L.Layer; - protected tooltip: L.Popup; - - protected constructor(data: FormattedData, - dsData: FormattedData[], - protected settings: S, - protected dataLayer: L) { - this.layer = this.create(data, dsData); - if (this.settings.tooltip?.show) { - this.createTooltip(data.$datasource); - this.updateTooltip(data, dsData); - } - this.createEventListeners(data, dsData); - this.dataLayer.getFeatureGroup().addLayer(this.layer); - } - - protected abstract create(data: FormattedData, dsData: FormattedData[]): L.Layer; - - protected abstract unbindLabel(): void; - - protected abstract bindLabel(content: L.Content): void; - - protected abstract createEventListeners(data: FormattedData, dsData: FormattedData[]): void; - - public abstract update(data: FormattedData, dsData: FormattedData[]): void; - - public remove() { - this.layer.off(); - this.dataLayer.getFeatureGroup().removeLayer(this.layer); - } - - protected updateTooltip(data: FormattedData, dsData: FormattedData[]) { - if (this.settings.tooltip.show) { - let tooltipTemplate = this.dataLayer.dataLayerTooltipProcessor.processPattern(data, dsData); - tooltipTemplate = processTooltipTemplate(tooltipTemplate); - this.tooltip.setContent(tooltipTemplate); - if (this.tooltip.isOpen() && this.tooltip.getElement()) { - this.bindTooltipActions(data.$datasource); - } - } - } - - protected updateLabel(data: FormattedData, dsData: FormattedData[]) { - if (this.settings.label.show) { - this.unbindLabel(); - const label = this.dataLayer.dataLayerLabelProcessor.processPattern(data, dsData); - const labelColor = this.dataLayer.getCtx().widgetConfig.color; - const content: L.Content = `
${label}
`; - this.bindLabel(content); - } - } - - private createTooltip(datasource: TbMapDatasource) { - this.tooltip = L.popup(); - this.layer.bindPopup(this.tooltip, {autoClose: this.settings.tooltip.autoclose, closeOnClick: false}); - if (this.settings.tooltip.trigger === DataLayerTooltipTrigger.hover) { - this.layer.off('click'); - this.layer.on('mouseover', () => { - this.layer.openPopup(); - }); - this.layer.on('mousemove', (e) => { - this.tooltip.setLatLng(e.latlng); - }); - this.layer.on('mouseout', () => { - this.layer.closePopup(); - }); - } - this.layer.on('popupopen', () => { - this.bindTooltipActions(datasource); - (this.layer as any)._popup._closeButton.addEventListener('click', (event: Event) => { - event.preventDefault(); - }); - }); - } - - private bindTooltipActions(datasource: TbMapDatasource) { - const actions = this.tooltip.getElement().getElementsByClassName('tb-custom-action'); - Array.from(actions).forEach( - (element: HTMLElement) => { - const actionName = element.getAttribute('data-action-name'); - this.dataLayer.getMap().tooltipElementClick(element, actionName, datasource); - }); - } - -} - -export enum MapDataLayerType { - marker = 'marker', - polygon = 'polygon', - circle = 'circle' -} - -class DataLayerPatternProcessor { - - private patternFunction: CompiledTbFunction; - private pattern: string; - - constructor(private dataLayer: TbMapDataLayer, - private settings: DataLayerPatternSettings) {} - - public setup(): Observable { - if (this.settings.type === DataLayerPatternType.function) { - return parseTbFunction(this.dataLayer.getCtx().http, this.settings.patternFunction, ['data', 'dsData']).pipe( - map((parsed) => { - this.patternFunction = parsed; - return null; - }) - ); - } else { - this.pattern = this.settings.pattern; - return of(null) - } - } - - public processPattern(data: FormattedData, dsData: FormattedData[]): string { - let pattern: string; - if (this.settings.type === DataLayerPatternType.function) { - pattern = safeExecuteTbFunction(this.patternFunction, [data, dsData]); - } else { - pattern = this.pattern; - } - const text = createLabelFromPattern(pattern, data); - const customTranslate = this.dataLayer.getCtx().$injector.get(CustomTranslatePipe); - return customTranslate.transform(text); - } - -} - -class DataLayerColorProcessor { - - private colorFunction: CompiledTbFunction; - private color: string; - - constructor(private dataLayer: TbMapDataLayer, - private settings: DataLayerColorSettings) {} - - public setup(): Observable { - if (this.settings.type === DataLayerColorType.function) { - return parseTbFunction(this.dataLayer.getCtx().http, this.settings.colorFunction, ['data', 'dsData']).pipe( - map((parsed) => { - this.colorFunction = parsed; - return null; - }) - ); - } else { - this.color = this.settings.color; - return of(null) - } - } - - public processColor(data: FormattedData, dsData: FormattedData[]): string { - let color: string; - if (this.settings.type === DataLayerColorType.function) { - color = safeExecuteTbFunction(this.colorFunction, [data, dsData]); - } else { - color = this.color; - } - return color; - } - -} - -export abstract class TbMapDataLayer> implements L.TB.DataLayer { - - protected settings: S; - - protected datasource: TbMapDatasource; - - protected mapDataId = guid(); - - protected featureGroup = L.featureGroup(); - - protected layerItems = new Map>(); - - protected groupsState: {[group: string]: boolean} = {}; - - protected enabled = true; - - public dataLayerLabelProcessor: DataLayerPatternProcessor; - public dataLayerTooltipProcessor: DataLayerPatternProcessor; - - protected constructor(protected map: TbMap, - inputSettings: S) { - this.settings = mergeDeepIgnoreArray({} as S, this.defaultBaseSettings() as S, inputSettings); - if (this.settings.groups?.length) { - this.settings.groups.forEach((group) => { - this.groupsState[group] = true; - }); - } - this.dataLayerLabelProcessor = this.settings.label.show ? new DataLayerPatternProcessor(this, this.settings.label) : null; - this.dataLayerTooltipProcessor = this.settings.tooltip.show ? new DataLayerPatternProcessor(this, this.settings.tooltip): null; - this.map.getMap().addLayer(this.featureGroup); - } - - public setup(): Observable { - this.datasource = mapDataSourceSettingsToDatasource(this.settings); - this.datasource.dataKeys = this.settings.additionalDataKeys ? [...this.settings.additionalDataKeys] : []; - this.mapDataId = this.datasource.mapDataIds[0]; - this.datasource = this.setupDatasource(this.datasource); - return forkJoin( - [ - this.dataLayerLabelProcessor ? this.dataLayerLabelProcessor.setup() : of(null), - this.dataLayerTooltipProcessor ? this.dataLayerTooltipProcessor.setup() : of(null), - this.doSetup() - ]); - } - - public getDatasource(): TbMapDatasource { - return this.datasource; - } - - public getFeatureGroup(): L.FeatureGroup { - return this.featureGroup; - } - - public getBounds(): LatLngBounds { - return this.featureGroup.getBounds(); - } - - public isEnabled(): boolean { - return this.enabled; - } - - public getGroups(): string[] { - return this.settings.groups || []; - } - - public toggleGroup(group: string): boolean { - if (isDefined(this.groupsState[group])) { - this.groupsState[group] = !this.groupsState[group]; - const enabled = Object.values(this.groupsState).some(v => v); - if (this.enabled !== enabled) { - this.enabled = enabled; - if (this.enabled) { - this.map.getMap().addLayer(this.featureGroup); - } else { - this.map.getMap().removeLayer(this.featureGroup); - } - return true; - } - } - return false; - } - - public updateData(dsData: FormattedData[]) { - const layerData = dsData.filter(d => d.$datasource.mapDataIds.includes(this.mapDataId)); - const rawItems = layerData.filter(d => this.isValidLayerData(d)); - const toDelete = new Set(Array.from(this.layerItems.keys())); - rawItems.forEach((data) => { - let layerItem = this.layerItems.get(data.entityId); - if (layerItem) { - layerItem.update(data, dsData); - } else { - layerItem = this.createLayerItem(data, dsData); - this.layerItems.set(data.entityId, layerItem); - } - toDelete.delete(data.entityId); - }); - toDelete.forEach((key) => { - const item = this.layerItems.get(key); - item.remove(); - this.layerItems.delete(key); - }); - } - - public getCtx(): WidgetContext { - return this.map.getCtx(); - } - public getMap(): TbMap { - return this.map; - } - - protected setupDatasource(datasource: TbMapDatasource): TbMapDatasource { - return datasource; - } - - protected mapType(): MapType { - return this.map.type(); - } - - public abstract dataLayerType(): MapDataLayerType; - - protected abstract defaultBaseSettings(): Partial; - - protected abstract doSetup(): Observable; - - protected abstract isValidLayerData(layerData: FormattedData): boolean; - - protected abstract createLayerItem(data: FormattedData, dsData: FormattedData[]): TbDataLayerItem; - -} - -class TbMarkerDataLayerItem extends TbDataLayerItem { - - private location: L.LatLng; - private marker: L.Marker; - private labelOffset: L.PointTuple; - - constructor(data: FormattedData, - dsData: FormattedData[], - protected settings: MarkersDataLayerSettings, - protected dataLayer: TbMarkersDataLayer) { - super(data, dsData, settings, dataLayer); - } - - protected create(data: FormattedData, dsData: FormattedData[]): L.Layer { - this.location = this.dataLayer.extractLocation(data); - this.marker = L.marker(this.location, { - tbMarkerData: data - }); - - this.updateMarkerIcon(data, dsData); - - return this.marker; - } - - protected createEventListeners(data: FormattedData, dsData: FormattedData[]): void { - this.dataLayer.getMap().markerClick(this.marker, data.$datasource); - } - - protected unbindLabel() { - this.marker.unbindTooltip(); - } - - protected bindLabel(content: L.Content): void { - this.marker.bindTooltip(content, { className: 'tb-marker-label', permanent: true, direction: 'top', offset: this.labelOffset }); - } - - public update(data: FormattedData, dsData: FormattedData[]): void { - const position = this.dataLayer.extractLocation(data); - if (!this.marker.getLatLng().equals(position)) { - this.location = position; - this.marker.setLatLng(position); - } - this.updateTooltip(data, dsData); - this.updateMarkerIcon(data, dsData); - } - - private updateMarkerIcon(data: FormattedData, dsData: FormattedData[]) { - this.dataLayer.markerIconProcessor.createMarkerIcon(data, dsData).subscribe( - (iconInfo) => { - this.marker.setIcon(iconInfo.icon); - const anchor = iconInfo.icon.options.iconAnchor; - if (anchor && Array.isArray(anchor)) { - this.labelOffset = [iconInfo.size[0] / 2 - anchor[0], 10 - anchor[1]]; - } else { - this.labelOffset = [0, -iconInfo.size[1] * this.dataLayer.markerOffset[1] + 10]; - } - this.updateLabel(data, dsData); - } - ); - } -} - -abstract class MarkerIconProcessor { - - static fromSettings(dataLayer: TbMarkersDataLayer, - settings: MarkersDataLayerSettings): MarkerIconProcessor { - switch (settings.markerType) { - case MarkerType.shape: - return new ShapeMarkerIconProcessor(dataLayer, settings.markerShape); - case MarkerType.icon: - return new IconMarkerIconProcessor(dataLayer, settings.markerIcon); - case MarkerType.image: - return new ImageMarkerIconProcessor(dataLayer, settings.markerImage); - } - } - - protected constructor(protected dataLayer: TbMarkersDataLayer, - protected settings: S) {} - - public abstract setup(): Observable; - - public abstract createMarkerIcon(data: FormattedData, - dsData: FormattedData[]): Observable; - -} - -abstract class BaseColorMarkerShapeProcessor extends MarkerIconProcessor { - - private markerColorFunction: CompiledTbFunction; - - private defaultMarkerIconInfo: MarkerIconInfo; - - protected constructor(protected dataLayer: TbMarkersDataLayer, - protected settings: S) { - super(dataLayer, settings); - } - - public setup(): Observable { - const colorSettings = this.settings.color; - if (colorSettings.type === DataLayerColorType.function) { - return parseTbFunction(this.dataLayer.getCtx().http, colorSettings.colorFunction, ['data', 'dsData']).pipe( - map((parsed) => { - this.markerColorFunction = parsed; - return null; - }) - ); - } else { - const color = tinycolor(colorSettings.color); - return this.createMarkerShape(color, this.settings.size).pipe( - map((info) => { - this.defaultMarkerIconInfo = info; - return null; - } - )); - } - } - - public createMarkerIcon(data: FormattedData, dsData: FormattedData[]): Observable { - const colorSettings = this.settings.color; - if (colorSettings.type === DataLayerColorType.function) { - const functionColor = safeExecuteTbFunction(this.markerColorFunction, [data, dsData]); - let color: tinycolor.Instance; - if (isDefinedAndNotNull(functionColor)) { - color = tinycolor(functionColor); - } else { - color = tinycolor(colorSettings.color); - } - return this.createMarkerShape(color, this.settings.size); - } else { - return of(this.defaultMarkerIconInfo); - } - } - - protected abstract createMarkerShape(color: tinycolor.Instance, size: number): Observable; -} - -class ShapeMarkerIconProcessor extends BaseColorMarkerShapeProcessor { - - constructor(protected dataLayer: TbMarkersDataLayer, - protected settings: MarkerShapeSettings) { - super(dataLayer, settings); - } - - protected createMarkerShape(color: tinycolor.Instance, size: number): Observable { - return this.dataLayer.createColoredMarkerShape(this.settings.shape, color, size); - } - -} - -class IconMarkerIconProcessor extends BaseColorMarkerShapeProcessor { - - constructor(protected dataLayer: TbMarkersDataLayer, - protected settings: MarkerIconSettings) { - super(dataLayer, settings); - } - - protected createMarkerShape(color: tinycolor.Instance, size: number): Observable { - return this.dataLayer.createColoredMarkerIcon(this.settings.icon, color, size); - } - -} - -class ImageMarkerIconProcessor extends MarkerIconProcessor { - - private markerImageFunction: CompiledTbFunction; - - private defaultMarkerIconInfo: MarkerIconInfo; - - constructor(protected dataLayer: TbMarkersDataLayer, - protected settings: MarkerImageSettings) { - super(dataLayer, settings); - } - - public setup(): Observable { - if (this.settings.type === MarkerImageType.function) { - return parseTbFunction(this.dataLayer.getCtx().http, this.settings.imageFunction, ['data', 'images', 'dsData']).pipe( - map((parsed) => { - this.markerImageFunction = parsed; - return null; - }) - ); - } else { - const currentImage: MarkerImageInfo = { - url: this.settings.image, - size: this.settings.imageSize || 34 - }; - return this.loadMarkerIconInfo(currentImage).pipe( - map((iconInfo) => { - this.defaultMarkerIconInfo = iconInfo; - return null; - } - )); - } - } - - public createMarkerIcon(data: FormattedData, dsData: FormattedData[]): Observable { - if (this.settings.type === MarkerImageType.function) { - const currentImage: MarkerImageInfo = safeExecuteTbFunction(this.markerImageFunction, [data, this.settings.images, dsData]); - return this.loadMarkerIconInfo(currentImage); - } else { - return of(this.defaultMarkerIconInfo); - } - } - - private loadMarkerIconInfo(image: MarkerImageInfo): Observable { - if (image && image.url) { - return loadImageWithAspect(this.dataLayer.getCtx().$injector.get(ImagePipe), image.url).pipe( - switchMap((aspectImage) => { - if (aspectImage?.aspect) { - let width: number; - let height: number; - if (aspectImage.aspect > 1) { - width = image.size; - height = image.size / aspectImage.aspect; - } else { - width = image.size * aspectImage.aspect; - height = image.size; - } - let iconAnchor = image.markerOffset; - let popupAnchor = image.tooltipOffset; - if (!iconAnchor) { - iconAnchor = [width * this.dataLayer.markerOffset[0], height * this.dataLayer.markerOffset[1]]; - } - if (!popupAnchor) { - popupAnchor = [width * this.dataLayer.tooltipOffset[0], height * this.dataLayer.tooltipOffset[1]]; - } - const icon = L.icon({ - iconUrl: aspectImage.url, - iconSize: [width, height], - iconAnchor, - popupAnchor - }); - const iconInfo: MarkerIconInfo = { - size: [width, height], - icon - }; - return of(iconInfo); - } else { - return this.dataLayer.createDefaultMarkerIcon(); - } - }), - catchError(() => this.dataLayer.createDefaultMarkerIcon()) - ); - } else { - return this.dataLayer.createDefaultMarkerIcon(); - } - } - -} - -export class TbMarkersDataLayer extends TbMapDataLayer { - - public markerIconProcessor: MarkerIconProcessor; - - public markerOffset: L.LatLngTuple; - public tooltipOffset: L.LatLngTuple; - - constructor(protected map: TbMap, - inputSettings: MarkersDataLayerSettings) { - super(map, inputSettings); - } - - public dataLayerType(): MapDataLayerType { - return MapDataLayerType.marker; - } - - protected setupDatasource(datasource: TbMapDatasource): TbMapDatasource { - datasource.dataKeys.push(this.settings.xKey, this.settings.yKey); - return datasource; - } - - protected defaultBaseSettings(): Partial { - return defaultBaseMarkersDataLayerSettings; - } - - protected doSetup(): Observable { - this.markerOffset = [ - isDefined(this.settings.markerOffsetX) ? this.settings.markerOffsetX : 0.5, - isDefined(this.settings.markerOffsetY) ? this.settings.markerOffsetY : 1, - ]; - this.tooltipOffset = [ - isDefined(this.settings.tooltip?.offsetX) ? this.settings.tooltip?.offsetX : 0, - isDefined(this.settings.tooltip?.offsetY) ? this.settings.tooltip?.offsetY : -1, - ]; - - this.markerIconProcessor = MarkerIconProcessor.fromSettings(this, this.settings); - return this.markerIconProcessor.setup(); - } - - protected isValidLayerData(layerData: FormattedData): boolean { - return !!this.extractPosition(layerData); - } - - protected createLayerItem(data: FormattedData, dsData: FormattedData[]): TbMarkerDataLayerItem { - return new TbMarkerDataLayerItem(data, dsData, this.settings, this); - } - - private extractPosition(data: FormattedData): {x: number; y: number} { - if (data) { - const xKeyVal = data[this.settings.xKey.label]; - const yKeyVal = data[this.settings.yKey.label]; - switch (this.mapType()) { - case MapType.geoMap: - if (!isValidLatLng(xKeyVal, yKeyVal)) { - return null; - } - break; - case MapType.image: - if (!isDefinedAndNotNull(xKeyVal) || isEmptyStr(xKeyVal) || isNaN(xKeyVal) || !isDefinedAndNotNull(yKeyVal) || isEmptyStr(yKeyVal) || isNaN(yKeyVal)) { - return null; - } - break; - } - return {x: xKeyVal, y: yKeyVal}; - } else { - return null; - } - } - - public createDefaultMarkerIcon(): Observable { - const color = this.settings.markerShape?.color?.color || '#307FE5'; - return this.createColoredMarkerShape(MarkerShape.markerShape1, tinycolor(color)); - } - - public createColoredMarkerShape(shape: MarkerShape, color: tinycolor.Instance, size = 34): Observable { - return createColorMarkerShapeURI(this.getCtx().$injector.get(MatIconRegistry), this.getCtx().$injector.get(DomSanitizer), shape, color).pipe( - map((iconUrl) => { - return { - size: [size, size], - icon: L.icon({ - iconUrl, - iconSize: [size, size], - iconAnchor: [size * this.markerOffset[0], size * this.markerOffset[1]], - popupAnchor: [size * this.tooltipOffset[0], size * this.tooltipOffset[1]] - }) - }; - }) - ); - } - - public createColoredMarkerIcon(icon: string, color: tinycolor.Instance, size = 34): Observable { - return createColorMarkerIconElement(this.getCtx().$injector.get(MatIconRegistry), this.getCtx().$injector.get(DomSanitizer), icon, color).pipe( - map((element) => { - return { - size: [size, size], - icon: L.divIcon({ - html: element.outerHTML, - className: 'tb-marker-div-icon', - iconSize: [size, size], - iconAnchor: [size * this.markerOffset[0], size * this.markerOffset[1]], - popupAnchor: [size * this.tooltipOffset[0], size * this.tooltipOffset[1]] - }) - }; - }) - ); - } - - public extractLocation(data: FormattedData): L.LatLng { - const position = this.extractPosition(data); - if (position) { - return this.map.positionToLatLng(position); - } else { - return null; - } - } -} - -class TbPolygonDataLayerItem extends TbDataLayerItem { - - private polygonContainer: L.FeatureGroup; - private polygon: L.Polygon; - - constructor(data: FormattedData, - dsData: FormattedData[], - protected settings: PolygonsDataLayerSettings, - protected dataLayer: TbPolygonsDataLayer) { - super(data, dsData, settings, dataLayer); - } - - protected create(data: FormattedData, dsData: FormattedData[]): L.Layer { - const polyData = this.dataLayer.extractPolygonCoordinates(data); - const polyConstructor = isCutPolygon(polyData) || polyData.length !== 2 ? L.polygon : L.rectangle; - const style = this.dataLayer.getShapeStyle(data, dsData); - this.polygon = polyConstructor(polyData, { - ...style - }); - - this.polygonContainer = L.featureGroup(); - this.polygon.addTo(this.polygonContainer); - - this.updateLabel(data, dsData); - return this.polygonContainer; - } - - protected createEventListeners(data: FormattedData, dsData: FormattedData[]): void { - this.dataLayer.getMap().polygonClick(this.polygonContainer, data.$datasource); - } - - protected unbindLabel() { - this.polygonContainer.unbindTooltip(); - } - - protected bindLabel(content: L.Content): void { - this.polygonContainer.bindTooltip(content, {className: 'tb-polygon-label', permanent: true, direction: 'center'}) - .openTooltip(this.polygonContainer.getBounds().getCenter()); - } - - public update(data: FormattedData, dsData: FormattedData[]): void { - const polyData = this.dataLayer.extractPolygonCoordinates(data); - const style = this.dataLayer.getShapeStyle(data, dsData); - if (isCutPolygon(polyData) || polyData.length !== 2) { - if (this.polygon instanceof L.Rectangle) { - this.polygonContainer.removeLayer(this.polygon); - this.polygon = L.polygon(polyData, { - ...style - }); - this.polygon.addTo(this.polygonContainer); - } else { - this.polygon.setLatLngs(polyData); - } - } else if (polyData.length === 2) { - const bounds = new L.LatLngBounds(polyData); - // @ts-ignore - this.leafletPoly.setBounds(bounds); - } - this.updateTooltip(data, dsData); - this.updateLabel(data, dsData); - this.polygon.setStyle(style); - } -} - -abstract class TbShapesDataLayer> extends TbMapDataLayer { - - public fillColorProcessor: DataLayerColorProcessor; - public strokeColorProcessor: DataLayerColorProcessor; - - protected constructor(protected map: TbMap, - inputSettings: S) { - super(map, inputSettings); - } - - protected doSetup(): Observable { - this.fillColorProcessor = new DataLayerColorProcessor(this, this.settings.fillColor); - this.strokeColorProcessor = new DataLayerColorProcessor(this, this.settings.strokeColor); - return forkJoin([this.fillColorProcessor.setup(), this.strokeColorProcessor.setup()]); - } - - public getShapeStyle(data: FormattedData, dsData: FormattedData[]): L.PathOptions { - const fill = this.fillColorProcessor.processColor(data, dsData); - const stroke = this.strokeColorProcessor.processColor(data, dsData); - return { - fill: true, - fillColor: fill, - color: stroke, - weight: this.settings.strokeWeight, - fillOpacity: 1, - opacity: 1 - }; - } -} - -export class TbPolygonsDataLayer extends TbShapesDataLayer { - - constructor(protected map: TbMap, - inputSettings: PolygonsDataLayerSettings) { - super(map, inputSettings); - } - - public dataLayerType(): MapDataLayerType { - return MapDataLayerType.polygon; - } - - protected setupDatasource(datasource: TbMapDatasource): TbMapDatasource { - datasource.dataKeys.push(this.settings.polygonKey); - return datasource; - } - - protected defaultBaseSettings(): Partial { - return defaultBasePolygonsDataLayerSettings; - } - - protected doSetup(): Observable { - return super.doSetup(); - } - - protected isValidLayerData(layerData: FormattedData): boolean { - return layerData && ((isNotEmptyStr(layerData[this.settings.polygonKey.label]) && !isJSON(layerData[this.settings.polygonKey.label]) - || Array.isArray(layerData[this.settings.polygonKey.label]))); - } - - protected createLayerItem(data: FormattedData, dsData: FormattedData[]): TbPolygonDataLayerItem { - return new TbPolygonDataLayerItem(data, dsData, this.settings, this); - } - - public extractPolygonCoordinates(data: FormattedData) { - let rawPolyData = data[this.settings.polygonKey.label]; - if (isString(rawPolyData)) { - rawPolyData = JSON.parse(rawPolyData); - } - return this.map.toPolygonCoordinates(rawPolyData); - } -} - -class TbCircleDataLayerItem extends TbDataLayerItem { - - private circle: L.Circle; - - constructor(data: FormattedData, - dsData: FormattedData[], - protected settings: CirclesDataLayerSettings, - protected dataLayer: TbCirclesDataLayer) { - super(data, dsData, settings, dataLayer); - } - - protected create(data: FormattedData, dsData: FormattedData[]): L.Layer { - const circleData = this.dataLayer.extractCircleCoordinates(data); - const center = new L.LatLng(circleData.latitude, circleData.longitude); - const style = this.dataLayer.getShapeStyle(data, dsData); - this.circle = L.circle(center, { - radius: circleData.radius, - ...style - }); - this.updateLabel(data, dsData); - return this.circle; - } - - protected createEventListeners(data: FormattedData, dsData: FormattedData[]): void { - this.dataLayer.getMap().circleClick(this.circle, data.$datasource); - } - - protected unbindLabel() { - this.circle.unbindTooltip(); - } - - protected bindLabel(content: L.Content): void { - this.circle.bindTooltip(content, { className: 'tb-polygon-label', permanent: true, direction: 'center'}) - .openTooltip(this.circle.getLatLng()); - } - - public update(data: FormattedData, dsData: FormattedData[]): void { - const circleData = this.dataLayer.extractCircleCoordinates(data); - const center = new L.LatLng(circleData.latitude, circleData.longitude); - if (!this.circle.getLatLng().equals(center)) { - this.circle.setLatLng(center); - } - if (this.circle.getRadius() !== circleData.radius) { - this.circle.setRadius(circleData.radius); - } - this.updateTooltip(data, dsData); - this.updateLabel(data, dsData); - const style = this.dataLayer.getShapeStyle(data, dsData); - this.circle.setStyle(style); - } -} - -export class TbCirclesDataLayer extends TbShapesDataLayer { - - constructor(protected map: TbMap, - inputSettings: CirclesDataLayerSettings) { - super(map, inputSettings); - } - - public dataLayerType(): MapDataLayerType { - return MapDataLayerType.circle; - } - - protected setupDatasource(datasource: TbMapDatasource): TbMapDatasource { - datasource.dataKeys.push(this.settings.circleKey); - return datasource; - } - - protected defaultBaseSettings(): Partial { - return defaultBaseCirclesDataLayerSettings; - } - - protected doSetup(): Observable { - return super.doSetup(); - } - - protected isValidLayerData(layerData: FormattedData): boolean { - return layerData && isNotEmptyStr(layerData[this.settings.circleKey.label]) && isJSON(layerData[this.settings.circleKey.label]); - } - - protected createLayerItem(data: FormattedData, dsData: FormattedData[]): TbDataLayerItem { - throw new TbCircleDataLayerItem(data, dsData, this.settings, this); - } - - public extractCircleCoordinates(data: FormattedData) { - const circleData: TbCircleData = JSON.parse(data[this.settings.circleKey.label]); - return this.map.convertCircleData(circleData); - } - - -} diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/maps/map-layer.ts b/ui-ngx/src/app/modules/home/components/widget/lib/maps/map-layer.ts index ddea67c613..05ec4d3c19 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/maps/map-layer.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/maps/map-layer.ts @@ -28,7 +28,7 @@ import { MapProvider, OpenStreetMapLayerSettings, TencentMapLayerSettings -} from '@home/components/widget/lib/maps/map.models'; +} from '@home/components/widget/lib/maps/models/map.models'; import { WidgetContext } from '@home/models/widget-component.models'; import { DeepPartial } from '@shared/models/common'; import { mergeDeep } from '@core/utils'; diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/maps/map-widget.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/maps/map-widget.component.ts index da394f6292..95f5e576c4 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/maps/map-widget.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/maps/map-widget.component.ts @@ -32,7 +32,7 @@ import { WidgetContext } from '@home/models/widget-component.models'; import { Observable } from 'rxjs'; import { backgroundStyle, ComponentStyle, overlayStyle } from '@shared/models/widget-settings.models'; import { TbMap } from '@home/components/widget/lib/maps/map'; -import { MapSetting } from '@home/components/widget/lib/maps/map.models'; +import { MapSetting } from '@home/components/widget/lib/maps/models/map.models'; import { WidgetComponent } from '@home/components/widget/widget.component'; import { ImagePipe } from '@shared/pipe/image.pipe'; import { DomSanitizer } from '@angular/platform-browser'; diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/maps/map-widget.models.ts b/ui-ngx/src/app/modules/home/components/widget/lib/maps/map-widget.models.ts index bc0d790036..30daf48cdc 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/maps/map-widget.models.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/maps/map-widget.models.ts @@ -14,7 +14,7 @@ /// limitations under the License. /// -import { defaultMapSettings, MapSetting } from '@home/components/widget/lib/maps/map.models'; +import { defaultMapSettings, MapSetting } from '@home/components/widget/lib/maps/models/map.models'; import { BackgroundSettings, BackgroundType } from '@shared/models/widget-settings.models'; import { mergeDeep } from '@core/utils'; diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/maps/map.scss b/ui-ngx/src/app/modules/home/components/widget/lib/maps/map.scss index 2928f6205a..b44fcd75b2 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/maps/map.scss +++ b/ui-ngx/src/app/modules/home/components/widget/lib/maps/map.scss @@ -101,6 +101,17 @@ border: none; } } + .tb-cluster-marker-container { + border: none; + background-color: transparent; + } + .tb-cluster-marker-element { + position: absolute; + top: 0; + left: 0; + width: 40px; + height: 40px; + } } .tb-map-sidebar { .tb-layers, .tb-groups { 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 a85406daa9..6531c81839 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 @@ -22,7 +22,8 @@ import { defaultImageMapSettings, GeoMapSettings, ImageMapSettings, - latLngPointToBounds, MapActionHandler, + latLngPointToBounds, + MapActionHandler, MapSetting, MapType, MapZoomAction, @@ -30,26 +31,23 @@ import { parseCenterPosition, TbCircleData, TbMapDatasource -} from '@home/components/widget/lib/maps/map.models'; +} from '@home/components/widget/lib/maps/models/map.models'; import { WidgetContext } from '@home/models/widget-component.models'; import { formattedDataFormDatasourceData, isDefinedAndNotNull, mergeDeepIgnoreArray } from '@core/utils'; import { DeepPartial } from '@shared/models/common'; -import L, { LatLngBounds, LatLngTuple, LeafletMouseEvent, PointExpression, Projection } from 'leaflet'; +import L, { LatLngBounds, LatLngTuple, LeafletMouseEvent, Projection } from 'leaflet'; import { forkJoin, Observable, of } from 'rxjs'; import { TbMapLayer } from '@home/components/widget/lib/maps/map-layer'; import { map, switchMap, tap } from 'rxjs/operators'; import '@home/components/widget/lib/maps/leaflet/leaflet-tb'; -import { - MapDataLayerType, - TbCirclesDataLayer, - TbMapDataLayer, - TbMarkersDataLayer, - TbPolygonsDataLayer -} from '@home/components/widget/lib/maps/map-data-layer'; +import { MapDataLayerType, TbMapDataLayer, } from '@home/components/widget/lib/maps/data-layer/map-data-layer'; import { IWidgetSubscription, WidgetSubscriptionOptions } from '@core/api/widget-api.models'; -import { Datasource, WidgetActionDescriptor, widgetType } from '@shared/models/widget.models'; +import { WidgetActionDescriptor, 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'; +import { TbPolygonsDataLayer } from '@home/components/widget/lib/maps/data-layer/polygons-data-layer'; +import { TbCirclesDataLayer } from '@home/components/widget/lib/maps/data-layer/circles-data-layer'; import ITooltipsterInstance = JQueryTooltipster.ITooltipsterInstance; export abstract class TbMap { @@ -277,7 +275,7 @@ export abstract class TbMap { side: ['topleft', 'bottomleft'].includes(this.settings.controlsPosition) ? 'right' : 'left', distance: 2, trackOrigin: true, - functionBefore: (instance, helper) => { + functionBefore: (_instance, helper) => { if (helper.origin.ariaDisabled === 'true' || helper.origin.parentElement.classList.contains('active')) { return false; } @@ -478,7 +476,9 @@ class TbGeoMap extends TbMap { const map = L.map(this.mapElement, { scrollWheelZoom: this.settings.zoomActions.includes(MapZoomAction.scroll), doubleClickZoom: this.settings.zoomActions.includes(MapZoomAction.doubleClick), - zoomControl: this.settings.zoomActions.includes(MapZoomAction.controlButtons) + zoomControl: this.settings.zoomActions.includes(MapZoomAction.controlButtons), + zoom: this.settings.defaultZoomLevel || DEFAULT_ZOOM_LEVEL, + center: this.defaultCenterPosition }).setView(this.defaultCenterPosition, this.settings.defaultZoomLevel || DEFAULT_ZOOM_LEVEL); return of(map); } diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/maps/map.models.ts b/ui-ngx/src/app/modules/home/components/widget/lib/maps/models/map.models.ts similarity index 97% rename from ui-ngx/src/app/modules/home/components/widget/lib/maps/map.models.ts rename to ui-ngx/src/app/modules/home/components/widget/lib/maps/models/map.models.ts index f6cdd500e1..526c67945b 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/maps/map.models.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/maps/models/map.models.ts @@ -24,7 +24,7 @@ import { TbFunction } from '@shared/models/js-function.models'; import { Observable, Observer, of, switchMap } from 'rxjs'; import { map } from 'rxjs/operators'; import { ImagePipe } from '@shared/pipe/image.pipe'; -import { MarkerShape } from '@home/components/widget/lib/maps/marker-shape.models'; +import { MarkerShape } from '@home/components/widget/lib/maps/models/marker-shape.models'; export enum MapType { geoMap = 'geoMap', @@ -211,6 +211,20 @@ export interface MarkerShapeSettings extends BaseMarkerShapeSettings { export interface MarkerIconSettings extends BaseMarkerShapeSettings { icon: string; } +export interface MarkerClusteringSettings { + enable: boolean; + zoomOnClick: boolean; + maxZoom: number; + maxClusterRadius: number; + zoomAnimation: boolean; + showCoverageOnHover: boolean; + spiderfyOnMaxZoom: boolean; + chunkedLoad: boolean; + lazyLoad: boolean; + useClusterMarkerColorFunction: boolean; + clusterMarkerColorFunction: TbFunction; +} + export interface MarkersDataLayerSettings extends MapDataLayerSettings { xKey: DataKey; @@ -221,6 +235,7 @@ export interface MarkersDataLayerSettings extends MapDataLayerSettings { markerImage?: MarkerImageSettings; markerOffsetX: number; markerOffsetY: number; + markerClustering: MarkerClusteringSettings; } const defaultMarkerLatitudeFunction = 'var value = prevValue || 15.833293;\n' + @@ -292,7 +307,20 @@ export const defaultBaseMarkersDataLayerSettings: Partial, export type MarkerImageFunction = (data: FormattedData, markerImages: string[], dsData: FormattedData[]) => MarkerImageInfo; +export type ClusterMarkerColorFunction = (data: FormattedData[], childCount: number) => string; + export interface TbCircleData { latitude: number; longitude: number; diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/maps/marker-shape.models.ts b/ui-ngx/src/app/modules/home/components/widget/lib/maps/models/marker-shape.models.ts similarity index 100% rename from ui-ngx/src/app/modules/home/components/widget/lib/maps/marker-shape.models.ts rename to ui-ngx/src/app/modules/home/components/widget/lib/maps/models/marker-shape.models.ts diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/map/data-layer-color-settings-panel.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/map/data-layer-color-settings-panel.component.ts index ff41d2772b..d8c64942ea 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/map/data-layer-color-settings-panel.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/map/data-layer-color-settings-panel.component.ts @@ -22,7 +22,7 @@ import { Store } from '@ngrx/store'; import { AppState } from '@core/core.state'; import { WidgetService } from '@core/http/widget.service'; import { takeUntilDestroyed } from '@angular/core/rxjs-interop'; -import { DataLayerColorSettings, DataLayerColorType } from '@home/components/widget/lib/maps/map.models'; +import { DataLayerColorSettings, DataLayerColorType } from '@home/components/widget/lib/maps/models/map.models'; @Component({ selector: 'tb-data-layer-color-settings-panel', diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/map/data-layer-color-settings.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/map/data-layer-color-settings.component.ts index 79249a5cee..0d7643fff1 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/map/data-layer-color-settings.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/map/data-layer-color-settings.component.ts @@ -19,7 +19,7 @@ import { ControlValueAccessor, NG_VALUE_ACCESSOR } from '@angular/forms'; import { ComponentStyle } from '@shared/models/widget-settings.models'; import { MatButton } from '@angular/material/button'; import { TbPopoverService } from '@shared/components/popover.service'; -import { DataLayerColorSettings, DataLayerColorType } from '@home/components/widget/lib/maps/map.models'; +import { DataLayerColorSettings, DataLayerColorType } from '@home/components/widget/lib/maps/models/map.models'; import { DataLayerColorSettingsPanelComponent } from '@home/components/widget/lib/settings/common/map/data-layer-color-settings-panel.component'; diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/map/data-layer-pattern-settings.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/map/data-layer-pattern-settings.component.ts index 204e1db59a..ee00d92043 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/map/data-layer-pattern-settings.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/map/data-layer-pattern-settings.component.ts @@ -32,7 +32,7 @@ import { DataLayerPatternSettings, DataLayerPatternType, DataLayerTooltipSettings, dataLayerTooltipTriggers, dataLayerTooltipTriggerTranslationMap -} from '@home/components/widget/lib/maps/map.models'; +} from '@home/components/widget/lib/maps/models/map.models'; import { coerceBoolean } from '@shared/decorators/coercion'; @Component({ diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/map/map-data-layer-dialog.component.html b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/map/map-data-layer-dialog.component.html index b84d61f354..dc9c069882 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/map/map-data-layer-dialog.component.html +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/map/map-data-layer-dialog.component.html @@ -251,6 +251,10 @@ formControlName="groups">
+ +
', + `
` + childCount + '
', iconSize: new L.Point(40, 40), className: 'tb-cluster-marker-container' }); diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/maps/models/map.models.ts b/ui-ngx/src/app/modules/home/components/widget/lib/maps/models/map.models.ts index 526c67945b..9dd6065ad6 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/maps/models/map.models.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/maps/models/map.models.ts @@ -421,6 +421,31 @@ export const additionalMapDataSourcesToDatasources = (additionalMapDataSources: }); }; +export const additionalMapDataSourceValid = (dataSource: AdditionalMapDataSourceSettings): boolean => { + if (!dataSource.dsType || ![DatasourceType.function, DatasourceType.device, DatasourceType.entity].includes(dataSource.dsType)) { + return false; + } + return !!dataSource.dataKeys?.length; +}; + +export const additionalMapDataSourceValidator: ValidatorFn = (control: AbstractControl): ValidationErrors | null => { + const dataSource: AdditionalMapDataSourceSettings = control.value; + if (!additionalMapDataSourceValid(dataSource)) { + return { + dataSource: true + }; + } + return null; +}; + +export const defaultAdditionalMapDataSourceSettings = (functionsOnly = false): AdditionalMapDataSourceSettings => { + return { + dsType: functionsOnly ? DatasourceType.function : DatasourceType.entity, + dsLabel: functionsOnly ? 'Additional data' : '', + dataKeys: [] + }; +}; + export enum MapControlsPosition { topleft = 'topleft', topright = 'topright', diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/maps/models/marker-shape.models.ts b/ui-ngx/src/app/modules/home/components/widget/lib/maps/models/marker-shape.models.ts index 8de7e3d013..9e8749bba7 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/maps/models/marker-shape.models.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/maps/models/marker-shape.models.ts @@ -58,10 +58,12 @@ const createColorMarkerShape = (iconRegistry: MatIconRegistry, domSanitizer: Dom const colorElements = Array.from(svgElement.getElementsByClassName('marker-color')); colorElements.forEach(el => { el.setAttribute('fill', '#'+color.toHex()); + el.setAttribute('fill-opacity', `${color.getAlpha()}`); }); const strokeElements = Array.from(svgElement.getElementsByClassName('marker-stroke')); strokeElements.forEach(el => { el.setAttribute('stroke', '#'+color.toHex()); + el.setAttribute('stroke-opacity', `${color.getAlpha()}`); }); return svgElement; }) @@ -90,8 +92,8 @@ const createIconElement = (iconRegistry: MatIconRegistry, icon: string, size: nu map((svgElement) => { const element = new Element(svgElement.firstChild); element.fill('#'+iconColor.toHex()); - //const box = element.bbox(); - const scale = size / 24;//box.height; + element.attr('fill-opacity', iconColor.getAlpha()); + const scale = size / 24; element.scale(scale); return element; }), @@ -110,6 +112,7 @@ const createIconElement = (iconRegistry: MatIconRegistry, icon: string, size: nu 'text-anchor': 'start' }); textElement.fill('#'+iconColor.toHex()); + textElement.attr('fill-opacity', iconColor.getAlpha()); const tspan = textElement.tspan(iconName); tspan.attr({ 'dominant-baseline': 'hanging' diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/key/data-key-input.component.html b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/key/data-key-input.component.html index 789ba36855..2349c14472 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/key/data-key-input.component.html +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/key/data-key-input.component.html @@ -15,7 +15,7 @@ limitations under the License. --> - - {{ label ? label : placeholder}} + [class]="{'tb-inline-field': inlineField, + 'tb-suffix-absolute': (inlineField && !keys?.length)}" + [appearance]="inlineField ? 'fill' : appearance" + [subscriptSizing]="inlineField ? 'dynamic' : subscriptSizing"> + {{ label ? label : placeholder}}
+ + warning + - + {{ requiredText }}
diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/key/data-keys.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/key/data-keys.component.ts index 61c45fd574..cd5669292d 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/key/data-keys.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/key/data-keys.component.ts @@ -110,6 +110,10 @@ export class DataKeysComponent implements ControlValueAccessor, OnInit, OnChange @Input() subscriptSizing: SubscriptSizing = 'fixed'; + @Input() + @coerceBoolean() + inlineField = false; + @Input() @coerceBoolean() hideDataKeyLabel: boolean; diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/map/map-data-layer-dialog.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/map/map-data-layer-dialog.component.ts index 98ab235dd7..d00325b0a7 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/map/map-data-layer-dialog.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/map/map-data-layer-dialog.component.ts @@ -176,6 +176,16 @@ export class MapDataLayerDialogComponent extends DialogComponent +
+
+ + + + {{ datasourceTypesTranslations.get(type) | translate }} + + + + + + + + + + +
+ + +
+
+ +
+
+
diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/map/map-data-source-row.component.scss b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/map/map-data-source-row.component.scss new file mode 100644 index 0000000000..e86005c768 --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/map/map-data-source-row.component.scss @@ -0,0 +1,36 @@ +/** + * Copyright © 2016-2024 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +.tb-form-table-row.tb-map-data-source-row { + + .tb-source-field { + flex: 1 1 50%; + display: flex; + gap: 12px; + .tb-ds-type-field, .tb-label-field, .tb-device-field, .tb-entity-alias-field { + flex: 1; + } + } + + .tb-data-keys-field { + flex: 1 1 50%; + } + + .tb-remove-button { + width: 40px; + min-width: 40px; + } +} diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/map/map-data-source-row.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/map/map-data-source-row.component.ts new file mode 100644 index 0000000000..37ba1e0c38 --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/map/map-data-source-row.component.ts @@ -0,0 +1,207 @@ +/// +/// Copyright © 2016-2024 The Thingsboard Authors +/// +/// Licensed under the Apache License, Version 2.0 (the "License"); +/// you may not use this file except in compliance with the License. +/// You may obtain a copy of the License at +/// +/// http://www.apache.org/licenses/LICENSE-2.0 +/// +/// Unless required by applicable law or agreed to in writing, software +/// distributed under the License is distributed on an "AS IS" BASIS, +/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +/// See the License for the specific language governing permissions and +/// limitations under the License. +/// + +import { + ChangeDetectorRef, + Component, + DestroyRef, + EventEmitter, + forwardRef, + Input, + OnInit, + Output, + ViewEncapsulation +} from '@angular/core'; +import { + ControlValueAccessor, + NG_VALUE_ACCESSOR, + UntypedFormBuilder, + UntypedFormGroup, + Validators +} from '@angular/forms'; +import { takeUntilDestroyed } from '@angular/core/rxjs-interop'; +import { AdditionalMapDataSourceSettings } from '@home/components/widget/lib/maps/models/map.models'; +import { DataKey, DatasourceType, datasourceTypeTranslationMap, widgetType } from '@shared/models/widget.models'; +import { EntityType } from '@shared/models/entity-type.models'; +import { DataKeyType } from '@shared/models/telemetry/telemetry.models'; +import { genNextLabelForDataKeys } from '@core/utils'; +import { MapSettingsContext } from '@home/components/widget/lib/settings/common/map/map-settings.component.models'; + +@Component({ + selector: 'tb-map-data-source-row', + templateUrl: './map-data-source-row.component.html', + styleUrls: ['./map-data-source-row.component.scss'], + providers: [ + { + provide: NG_VALUE_ACCESSOR, + useExisting: forwardRef(() => MapDataSourceRowComponent), + multi: true + } + ], + encapsulation: ViewEncapsulation.None +}) +export class MapDataSourceRowComponent implements ControlValueAccessor, OnInit { + + DatasourceType = DatasourceType; + DataKeyType = DataKeyType; + + EntityType = EntityType; + + widgetType = widgetType; + + datasourceTypes: Array = []; + datasourceTypesTranslations = datasourceTypeTranslationMap; + + @Input() + disabled: boolean; + + @Input() + context: MapSettingsContext; + + @Output() + dataSourceRemoved = new EventEmitter(); + + dataSourceFormGroup: UntypedFormGroup; + + generateAdditionalDataKey = this.generateDataKey.bind(this); + + modelValue: AdditionalMapDataSourceSettings; + + private propagateChange = (_val: any) => {}; + + constructor(private fb: UntypedFormBuilder, + private cd: ChangeDetectorRef, + private destroyRef: DestroyRef) { + } + + ngOnInit() { + if (this.context.functionsOnly) { + this.datasourceTypes = [DatasourceType.function]; + } else { + this.datasourceTypes = [DatasourceType.function, DatasourceType.device, DatasourceType.entity]; + } + this.dataSourceFormGroup = this.fb.group({ + dsType: [null, [Validators.required]], + dsLabel: [null, []], + dsDeviceId: [null, [Validators.required]], + dsEntityAliasId: [null, [Validators.required]], + dataKeys: [null, [Validators.required]] + }); + this.dataSourceFormGroup.valueChanges.pipe( + takeUntilDestroyed(this.destroyRef) + ).subscribe( + () => this.updateModel() + ); + this.dataSourceFormGroup.get('dsType').valueChanges.pipe( + takeUntilDestroyed(this.destroyRef) + ).subscribe( + (newDsType: DatasourceType) => this.onDsTypeChanged(newDsType) + ); + } + + registerOnChange(fn: any): void { + this.propagateChange = fn; + } + + registerOnTouched(_fn: any): void { + } + + setDisabledState(isDisabled: boolean): void { + this.disabled = isDisabled; + if (isDisabled) { + this.dataSourceFormGroup.disable({emitEvent: false}); + } else { + this.dataSourceFormGroup.enable({emitEvent: false}); + this.updateValidators(); + } + } + + writeValue(value: AdditionalMapDataSourceSettings): void { + this.modelValue = value; + this.dataSourceFormGroup.patchValue( + { + dsType: value?.dsType, + dsLabel: value?.dsLabel, + dsDeviceId: value?.dsDeviceId, + dsEntityAliasId: value?.dsEntityAliasId, + dataKeys: value?.dataKeys + }, {emitEvent: false} + ); + this.updateValidators(); + this.cd.markForCheck(); + } + + private generateDataKey(key: DataKey): DataKey { + const dataKey = this.context.callbacks.generateDataKey(key.name, key.type, null, false, null); + const dataKeys: DataKey[] = this.dataSourceFormGroup.get('dataKeys').value || []; + dataKey.label = genNextLabelForDataKeys(dataKey.label, dataKeys); + return dataKey; + } + + private onDsTypeChanged(newDsType: DatasourceType) { + let updateModel = false; + const dataKeys: DataKey[] = this.dataSourceFormGroup.get('dataKeys').value; + if (dataKeys?.length) { + for (const key of dataKeys) { + updateModel = this.updateDataKeyToNewDsType(key, newDsType) || updateModel; + } + if (updateModel) { + this.dataSourceFormGroup.get('dataKeys').patchValue(dataKeys, {emitEvent: false}); + } + } + this.updateValidators(); + if (updateModel) { + this.updateModel(); + } + } + + private updateDataKeyToNewDsType(dataKey: DataKey, newDsType: DatasourceType): boolean { + if (newDsType === DatasourceType.function) { + if (dataKey.type !== DataKeyType.function) { + dataKey.type = DataKeyType.function; + return true; + } + } else { + if (dataKey.type === DataKeyType.function) { + dataKey.type = DataKeyType.attribute; + return true; + } + } + return false; + } + + private updateValidators() { + const dsType: DatasourceType = this.dataSourceFormGroup.get('dsType').value; + if (dsType === DatasourceType.function) { + this.dataSourceFormGroup.get('dsLabel').enable({emitEvent: false}); + this.dataSourceFormGroup.get('dsDeviceId').disable({emitEvent: false}); + this.dataSourceFormGroup.get('dsEntityAliasId').disable({emitEvent: false}); + } else if (dsType === DatasourceType.device) { + this.dataSourceFormGroup.get('dsLabel').disable({emitEvent: false}); + this.dataSourceFormGroup.get('dsDeviceId').enable({emitEvent: false}); + this.dataSourceFormGroup.get('dsEntityAliasId').disable({emitEvent: false}); + } else { + this.dataSourceFormGroup.get('dsLabel').disable({emitEvent: false}); + this.dataSourceFormGroup.get('dsDeviceId').disable({emitEvent: false}); + this.dataSourceFormGroup.get('dsEntityAliasId').enable({emitEvent: false}); + } + } + + private updateModel() { + this.modelValue = {...this.modelValue, ...this.dataSourceFormGroup.value}; + this.propagateChange(this.modelValue); + } +} diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/map/map-data-sources.component.html b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/map/map-data-sources.component.html new file mode 100644 index 0000000000..93eb05fb21 --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/map/map-data-sources.component.html @@ -0,0 +1,43 @@ + +
+
+
+
widgets.maps.data-layer.source
+
widgets.maps.data-layer.data-keys
+
+
+
+
+ + +
+
+
+
+ +
+
+ + {{ 'widgets.maps.data-layer.no-datasources' | translate }} + diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/map/map-data-sources.component.scss b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/map/map-data-sources.component.scss new file mode 100644 index 0000000000..1b5bd95d3e --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/map/map-data-sources.component.scss @@ -0,0 +1,36 @@ +/** + * Copyright © 2016-2024 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +.tb-map-data-sources { + .tb-form-table-header-cell { + &.tb-source-header { + flex: 1 1 50%; + } + &.tb-data-keys-header { + flex: 1 1 50%; + } + &.tb-actions-header { + width: 40px; + min-width: 40px; + } + } + + .tb-form-table-body { + tb-map-data-source-row { + overflow: hidden; + } + } +} diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/map/map-data-sources.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/map/map-data-sources.component.ts new file mode 100644 index 0000000000..4e0dea7d5a --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/map/map-data-sources.component.ts @@ -0,0 +1,147 @@ +/// +/// Copyright © 2016-2024 The Thingsboard Authors +/// +/// Licensed under the Apache License, Version 2.0 (the "License"); +/// you may not use this file except in compliance with the License. +/// You may obtain a copy of the License at +/// +/// http://www.apache.org/licenses/LICENSE-2.0 +/// +/// Unless required by applicable law or agreed to in writing, software +/// distributed under the License is distributed on an "AS IS" BASIS, +/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +/// See the License for the specific language governing permissions and +/// limitations under the License. +/// + +import { Component, DestroyRef, forwardRef, Input, OnInit, ViewEncapsulation } from '@angular/core'; +import { + AbstractControl, + ControlValueAccessor, + NG_VALIDATORS, + NG_VALUE_ACCESSOR, + UntypedFormArray, + UntypedFormBuilder, + UntypedFormControl, + UntypedFormGroup, + Validator +} from '@angular/forms'; +import { mergeDeep } from '@core/utils'; +import { takeUntilDestroyed } from '@angular/core/rxjs-interop'; +import { + AdditionalMapDataSourceSettings, + additionalMapDataSourceValid, + additionalMapDataSourceValidator, + defaultAdditionalMapDataSourceSettings +} from '@home/components/widget/lib/maps/models/map.models'; +import { MapSettingsContext } from '@home/components/widget/lib/settings/common/map/map-settings.component.models'; + +@Component({ + selector: 'tb-map-data-sources', + templateUrl: './map-data-sources.component.html', + styleUrls: ['./map-data-sources.component.scss'], + providers: [ + { + provide: NG_VALUE_ACCESSOR, + useExisting: forwardRef(() => MapDataSourcesComponent), + multi: true + }, + { + provide: NG_VALIDATORS, + useExisting: forwardRef(() => MapDataSourcesComponent), + multi: true + } + ], + encapsulation: ViewEncapsulation.None +}) +export class MapDataSourcesComponent implements ControlValueAccessor, OnInit, Validator { + + @Input() + disabled: boolean; + + @Input() + context: MapSettingsContext; + + dataSourcesFormGroup: UntypedFormGroup; + + private propagateChange = (_val: any) => {}; + + constructor(private fb: UntypedFormBuilder, + private destroyRef: DestroyRef) { + } + + ngOnInit() { + this.dataSourcesFormGroup = this.fb.group({ + dataSources: [this.fb.array([]), []] + }); + this.dataSourcesFormGroup.valueChanges.pipe( + takeUntilDestroyed(this.destroyRef) + ).subscribe( + () => { + let dataSources: AdditionalMapDataSourceSettings[] = this.dataSourcesFormGroup.get('dataSources').value; + if (dataSources) { + dataSources = dataSources.filter(dataSource => additionalMapDataSourceValid(dataSource)); + } + this.propagateChange(dataSources); + } + ); + } + + registerOnChange(fn: any): void { + this.propagateChange = fn; + } + + registerOnTouched(fn: any): void { + } + + setDisabledState(isDisabled: boolean): void { + this.disabled = isDisabled; + if (isDisabled) { + this.dataSourcesFormGroup.disable({emitEvent: false}); + } else { + this.dataSourcesFormGroup.enable({emitEvent: false}); + } + } + + writeValue(value: AdditionalMapDataSourceSettings[] | undefined): void { + const dataSources: AdditionalMapDataSourceSettings[] = value || []; + this.dataSourcesFormGroup.setControl('dataSources', this.prepareDataSourcesFormArray(dataSources), {emitEvent: false}); + } + + public validate(c: UntypedFormControl) { + const valid = this.dataSourcesFormGroup.valid; + return valid ? null : { + dataSources: { + valid: false, + }, + }; + } + + dataSourcesFormArray(): UntypedFormArray { + return this.dataSourcesFormGroup.get('dataSources') as UntypedFormArray; + } + + trackByDataSource(index: number, dataSourceControl: AbstractControl): any { + return dataSourceControl; + } + + removeDataSource(index: number) { + (this.dataSourcesFormGroup.get('dataSources') as UntypedFormArray).removeAt(index); + } + + addDataSource() { + const dataSource = mergeDeep({} as AdditionalMapDataSourceSettings, + defaultAdditionalMapDataSourceSettings(this.context.functionsOnly)); + const dataSourcesArray = this.dataSourcesFormGroup.get('dataSources') as UntypedFormArray; + const dataSourceControl = this.fb.control(dataSource, [additionalMapDataSourceValidator]); + dataSourcesArray.push(dataSourceControl); + } + + private prepareDataSourcesFormArray(dataSources: AdditionalMapDataSourceSettings[]): UntypedFormArray { + const dataSourcesControls: Array = []; + dataSources.forEach((dataSource) => { + dataSourcesControls.push(this.fb.control(dataSource, [additionalMapDataSourceValidator])); + }); + return this.fb.array(dataSourcesControls); + } +} 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 dd0b91b11e..34c383aaf5 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 @@ -57,4 +57,12 @@ [context]="context" [mapType]="mapSettingsFormGroup.get('mapType').value">
+
+
+ {{ 'widgets.maps.data-layer.additional-datasources' | translate }} +
+ + +
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 fffa911bdb..c31bddd8f1 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 @@ -226,6 +226,10 @@ import { MarkerShapesComponent } from '@home/components/widget/lib/settings/comm import { MarkerClusteringSettingsComponent } from '@home/components/widget/lib/settings/common/map/marker-clustering-settings.component'; +import { MapDataSourcesComponent } from '@home/components/widget/lib/settings/common/map/map-data-sources.component'; +import { + MapDataSourceRowComponent +} from '@home/components/widget/lib/settings/common/map/map-data-source-row.component'; @NgModule({ declarations: [ @@ -310,6 +314,8 @@ import { MapDataLayerDialogComponent, MapDataLayerRowComponent, MapDataLayersComponent, + MapDataSourceRowComponent, + MapDataSourcesComponent, MapSettingsComponent, EntityAliasSelectComponent, FilterSelectComponent, 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 9397ba5ee5..876bc51d6b 100644 --- a/ui-ngx/src/assets/locale/locale.constant-en_US.json +++ b/ui-ngx/src/assets/locale/locale.constant-en_US.json @@ -6874,6 +6874,11 @@ "data-layer": { "source": "Source", "additional-data-keys": "Additional data keys", + "additional-datasources": "Additional datasources", + "data-keys": "Data keys", + "add-datasource": "Add datasource", + "no-datasources": "No datasources configured", + "remove-datasource": "Remove datasource", "groups": "Groups", "color": "Color", "fill-color": "Fill color", From 787d681f6f881eb7a859e108aa5919cb83dc993f Mon Sep 17 00:00:00 2001 From: Igor Kulikov Date: Wed, 22 Jan 2025 17:17:23 +0200 Subject: [PATCH 029/345] UI: Map - Improve marker shape opacity --- .../components/widget/lib/maps/models/marker-shape.models.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/maps/models/marker-shape.models.ts b/ui-ngx/src/app/modules/home/components/widget/lib/maps/models/marker-shape.models.ts index 9e8749bba7..8b0b31ba50 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/maps/models/marker-shape.models.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/maps/models/marker-shape.models.ts @@ -136,6 +136,7 @@ export const createColorMarkerIconElement = (iconRegistry: MatIconRegistry, domS elements = svgElement.getElementsByClassName('marker-icon-background'); if (elements.length) { (elements[0] as SVGGElement).style.display = ''; + (elements[0] as SVGGElement).setAttribute('fill-opacity', `${color.getAlpha()}`); } return svgElement; }) From 0f848722a2961c27f6236287f9dc712cc3271629 Mon Sep 17 00:00:00 2001 From: Igor Kulikov Date: Wed, 22 Jan 2025 17:25:13 +0200 Subject: [PATCH 030/345] UI: Map - Improve marker shape opacity --- .../widget/lib/maps/models/marker-shape.models.ts | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/maps/models/marker-shape.models.ts b/ui-ngx/src/app/modules/home/components/widget/lib/maps/models/marker-shape.models.ts index 8b0b31ba50..f972582201 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/maps/models/marker-shape.models.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/maps/models/marker-shape.models.ts @@ -82,7 +82,8 @@ export const createColorMarkerShapeURI = (iconRegistry: MatIconRegistry, domSani const createIconElement = (iconRegistry: MatIconRegistry, icon: string, size: number, color: tinycolor.Instance): Observable => { const isSvg = isSvgIcon(icon); - const iconColor = tinycolor.mix(color, tinycolor('rgba(0,0,0,0.38)')); + const iconAlpha = color.getAlpha(); + const iconColor = tinycolor.mix(color.clone().setAlpha(1), tinycolor('rgba(0,0,0,0.38)')); if (isSvg) { const [namespace, iconName] = splitIconName(icon); return iconRegistry @@ -92,7 +93,7 @@ const createIconElement = (iconRegistry: MatIconRegistry, icon: string, size: nu map((svgElement) => { const element = new Element(svgElement.firstChild); element.fill('#'+iconColor.toHex()); - element.attr('fill-opacity', iconColor.getAlpha()); + element.attr('fill-opacity', iconAlpha); const scale = size / 24; element.scale(scale); return element; @@ -112,7 +113,7 @@ const createIconElement = (iconRegistry: MatIconRegistry, icon: string, size: nu 'text-anchor': 'start' }); textElement.fill('#'+iconColor.toHex()); - textElement.attr('fill-opacity', iconColor.getAlpha()); + textElement.attr('fill-opacity', iconAlpha); const tspan = textElement.tspan(iconName); tspan.attr({ 'dominant-baseline': 'hanging' From ab997ea8e82aeddf2dceb3eb600e7ada47734389 Mon Sep 17 00:00:00 2001 From: Igor Kulikov Date: Wed, 22 Jan 2025 18:03:24 +0200 Subject: [PATCH 031/345] UI: minor changes --- ui-ngx/src/app/shared/import-export/import-dialog.component.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/ui-ngx/src/app/shared/import-export/import-dialog.component.ts b/ui-ngx/src/app/shared/import-export/import-dialog.component.ts index 16800ceadc..e4256229c4 100644 --- a/ui-ngx/src/app/shared/import-export/import-dialog.component.ts +++ b/ui-ngx/src/app/shared/import-export/import-dialog.component.ts @@ -78,7 +78,6 @@ export class ImportDialogComponent extends DialogComponent { this.importTypeChanged(); }); - this.importTypeChanged(); } isErrorState(control: UntypedFormControl | null, form: FormGroupDirective | NgForm | null): boolean { From c0eda85e105ed0d6d041220d5ce7e87a7731972f Mon Sep 17 00:00:00 2001 From: Igor Kulikov Date: Thu, 23 Jan 2025 18:00:52 +0200 Subject: [PATCH 032/345] UI: Map: image map. --- .../data/json/system/widget_bundles/maps.json | 1 + .../json/system/widget_types/image_map.json | 55 ++-- .../widget_types/image_map_deprecated.json | 102 ++++++ .../data/json/system/widget_types/map.json | 81 ++++- .../basic/map/map-basic-config.component.ts | 13 + .../lib/maps-legacy/providers/image-map.ts | 15 +- .../lib/maps/data-layer/circles-data-layer.ts | 27 +- .../lib/maps/data-layer/map-data-layer.ts | 23 +- .../lib/maps/data-layer/markers-data-layer.ts | 57 +++- .../maps/data-layer/polygons-data-layer.ts | 33 +- .../components/widget/lib/maps/geo-map.ts | 141 +++++++++ .../components/widget/lib/maps/image-map.ts | 293 ++++++++++++++++++ .../widget/lib/maps/map-widget.component.ts | 8 +- .../widget/lib/maps/map-widget.models.ts | 18 +- .../home/components/widget/lib/maps/map.scss | 23 +- .../home/components/widget/lib/maps/map.ts | 224 ++----------- .../widget/lib/maps/models/map.models.ts | 121 ++++++-- .../common/key/data-key-input.component.html | 2 +- .../common/key/data-keys.component.ts | 2 +- .../image-map-source-settings.component.html | 62 ++++ .../image-map-source-settings.component.ts | 157 ++++++++++ .../map/map-data-layer-dialog.component.html | 11 + .../map/map-data-layer-dialog.component.ts | 18 +- .../common/map/map-settings.component.html | 62 +++- .../common/map/map-settings.component.ts | 81 +++-- .../common/widget-settings-common.module.ts | 4 + .../assets/locale/locale.constant-en_US.json | 29 ++ 27 files changed, 1307 insertions(+), 356 deletions(-) create mode 100644 application/src/main/data/json/system/widget_types/image_map_deprecated.json create mode 100644 ui-ngx/src/app/modules/home/components/widget/lib/maps/geo-map.ts create mode 100644 ui-ngx/src/app/modules/home/components/widget/lib/maps/image-map.ts create mode 100644 ui-ngx/src/app/modules/home/components/widget/lib/settings/common/map/image-map-source-settings.component.html create mode 100644 ui-ngx/src/app/modules/home/components/widget/lib/settings/common/map/image-map-source-settings.component.ts diff --git a/application/src/main/data/json/system/widget_bundles/maps.json b/application/src/main/data/json/system/widget_bundles/maps.json index 07831f4ef4..7060499f24 100644 --- a/application/src/main/data/json/system/widget_bundles/maps.json +++ b/application/src/main/data/json/system/widget_bundles/maps.json @@ -10,6 +10,7 @@ }, "widgetTypeFqns": [ "map", + "image_map", "maps_v2.openstreetmap", "maps_v2.google_maps", "maps_v2.image_map", diff --git a/application/src/main/data/json/system/widget_types/image_map.json b/application/src/main/data/json/system/widget_types/image_map.json index 23cf7f7a14..3330045721 100644 --- a/application/src/main/data/json/system/widget_types/image_map.json +++ b/application/src/main/data/json/system/widget_types/image_map.json @@ -1,36 +1,24 @@ { - "fqn": "maps_v2.image_map", + "fqn": "image_map", "name": "Image Map", - "deprecated": true, + "deprecated": false, "image": "tb-image;/api/images/system/image_map_system_widget_image.png", "description": "Displays the indoor or relative location of the entities on the image map. Useful to display floor maps, smart parking, etc. Entity coordinates are expected to be in the range from 0 to 1. Highly customizable via custom markers, marker tooltips, and widget actions. ", "descriptor": { "type": "latest", "sizeX": 8.5, - "sizeY": 6.5, + "sizeY": 6, "resources": [], - "templateHtml": "", - "templateCss": ".leaflet-zoom-box {\n\tz-index: 9;\n}\n\n.leaflet-pane { z-index: 4; }\n\n.leaflet-tile-pane { z-index: 2; }\n.leaflet-overlay-pane { z-index: 4; }\n.leaflet-shadow-pane { z-index: 5; }\n.leaflet-marker-pane { z-index: 6; }\n.leaflet-tooltip-pane { z-index: 7; }\n.leaflet-popup-pane { z-index: 8; }\n\n.leaflet-map-pane canvas { z-index: 1; }\n.leaflet-map-pane svg { z-index: 2; }\n\n.leaflet-control {\n\tz-index: 9;\n}\n.leaflet-top,\n.leaflet-bottom {\n\tz-index: 11;\n}\n\n.tb-marker-label {\n border: none;\n background: none;\n box-shadow: none;\n}\n\n.tb-marker-label:before {\n border: none;\n background: none;\n}\n", - "controllerScript": "self.onInit = function() {\n self.ctx.map = new TbMapWidgetV2('image-map', false, self.ctx);\n}\n\nself.onDataUpdated = function() {\n self.ctx.map.update();\n}\n\nself.onResize = function() {\n self.ctx.map.resize();\n}\n\nself.actionSources = function() {\n return TbMapWidgetV2.actionSources();\n}\n\nself.onDestroy = function() {\n self.ctx.map.destroy();\n}\n\nself.typeParameters = function() {\n return {\n hasDataPageLink: true\n };\n}", - "settingsSchema": "", - "dataKeySettingsSchema": "", - "settingsDirective": "tb-map-widget-settings-legacy", - "defaultConfig": "{\"datasources\":[{\"type\":\"function\",\"name\":\"First point\",\"entityAliasId\":null,\"filterId\":null,\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"xPos\",\"color\":\"#2196f3\",\"settings\":{},\"_hash\":0.05427416942713381,\"funcBody\":\"var value = prevValue || 0.2;\\nif (time % 5000 < 500) {\\n value += Math.random() * 0.05 - 0.025;\\n}\\nreturn value;\"},{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"yPos\",\"color\":\"#4caf50\",\"settings\":{},\"_hash\":0.680594833308841,\"funcBody\":\"var value = prevValue || 0.3;\\nif (time % 5000 < 500) {\\n value += Math.random() * 0.05 - 0.025;\\n}\\nreturn value;\"},{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"temperature\",\"color\":\"#9c27b0\",\"settings\":{},\"_hash\":0.9430343126300238,\"funcBody\":\"var value = prevValue + Math.random() * 40 - 20;\\nif (value < -60) {\\n\\tvalue = -60;\\n} else if (value > 60) {\\n\\tvalue = 60;\\n}\\nreturn value;\"},{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Type\",\"color\":\"#8bc34a\",\"settings\":{},\"_hash\":0.1784452363910778,\"funcBody\":\"return \\\"colorpin\\\";\",\"units\":null,\"decimals\":null,\"usePostProcessing\":null,\"postFuncBody\":null}]},{\"type\":\"function\",\"name\":\"Second point\",\"entityAliasId\":null,\"filterId\":null,\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"xPos\",\"color\":\"#f44336\",\"settings\":{},\"_hash\":0.05012157428742059,\"funcBody\":\"var value = prevValue || 0.6;\\nif (time % 4000 < 500) {\\n value += Math.random() * 0.05 - 0.025;\\n}\\nreturn value;\"},{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"yPos\",\"color\":\"#ffc107\",\"settings\":{},\"_hash\":0.6742359401617628,\"funcBody\":\"var value = prevValue || 0.7;\\nif (time % 4000 < 500) {\\n value += Math.random() * 0.05 - 0.025;\\n}\\nreturn value;\"},{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"temperature\",\"color\":\"#8bc34a\",\"settings\":{},\"_hash\":0.773875863339494,\"funcBody\":\"var value = prevValue + Math.random() * 40 - 20;\\nif (value < -60) {\\n\\tvalue = -60;\\n} else if (value > 60) {\\n\\tvalue = 60;\\n}\\nreturn value;\"},{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Type\",\"color\":\"#3f51b5\",\"settings\":{},\"_hash\":0.405822538899673,\"funcBody\":\"return \\\"thermometer\\\";\",\"units\":null,\"decimals\":null,\"usePostProcessing\":null,\"postFuncBody\":null}]}],\"timewindow\":{\"realtime\":{\"timewindowMs\":60000}},\"showTitle\":true,\"backgroundColor\":\"#fff\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"8px\",\"settings\":{\"provider\":\"image-map\",\"mapImageUrl\":\"tb-image;/api/images/system/image_map_system_widget_map_image.svg\",\"latKeyName\":\"latitude\",\"lngKeyName\":\"longitude\",\"xPosKeyName\":\"xPos\",\"yPosKeyName\":\"yPos\",\"defaultCenterPosition\":\"0,0\",\"disableScrollZooming\":false,\"disableDoubleClickZooming\":false,\"disableZoomControl\":false,\"fitMapBounds\":true,\"useDefaultCenterPosition\":false,\"mapPageSize\":16384,\"markerOffsetX\":0.5,\"markerOffsetY\":1,\"posFunction\":\"return {x: origXPos, y: origYPos};\",\"draggableMarker\":false,\"showLabel\":true,\"useLabelFunction\":false,\"label\":\"${entityName}\",\"showTooltip\":true,\"showTooltipAction\":\"click\",\"autocloseTooltip\":true,\"useTooltipFunction\":false,\"tooltipPattern\":\"${entityName}

X Pos: ${xPos:2}
Y Pos: ${yPos:2}
Temperature: ${temperature} °C
See advanced settings for details\",\"tooltipOffsetX\":0,\"tooltipOffsetY\":-1,\"color\":\"#fe7569\",\"useColorFunction\":true,\"colorFunction\":\"var type = dsData[dsIndex]['Type'];\\nif (type == 'colorpin') {\\n\\tvar temperature = dsData[dsIndex]['temperature'];\\n\\tif (typeof temperature !== undefined) {\\n\\t var percent = (temperature + 60)/120 * 100;\\n\\t return tinycolor.mix('blue', 'red', percent).toHexString();\\n\\t}\\n\\treturn 'blue';\\n}\\n\",\"useMarkerImageFunction\":true,\"markerImageSize\":34,\"markerImageFunction\":\"var type = dsData[dsIndex]['Type'];\\nif (type == 'thermometer') {\\n\\tvar res = {\\n\\t url: images[0],\\n\\t size: 40\\n\\t}\\n\\tvar temperature = dsData[dsIndex]['temperature'];\\n\\tif (typeof temperature !== undefined) {\\n\\t var percent = (temperature + 60)/120;\\n\\t var index = Math.min(3, Math.floor(4 * percent));\\n\\t res.url = images[index];\\n\\t}\\n\\treturn res;\\n}\",\"markerImages\":[\"tb-image;/api/images/system/map_marker_image_0.png\",\"tb-image;/api/images/system/map_marker_image_1.png\",\"tb-image;/api/images/system/map_marker_image_2.png\",\"tb-image;/api/images/system/map_marker_image_3.png\"],\"showPolygon\":false,\"polygonKeyName\":\"perimeter\",\"editablePolygon\":false,\"showPolygonLabel\":false,\"usePolygonLabelFunction\":false,\"polygonLabel\":\"${entityName}\",\"showPolygonTooltip\":false,\"showPolygonTooltipAction\":\"click\",\"autoClosePolygonTooltip\":true,\"usePolygonTooltipFunction\":false,\"polygonTooltipPattern\":\"${entityName}

TimeStamp: ${ts:7}\",\"polygonColor\":\"#3388ff\",\"polygonOpacity\":0.2,\"usePolygonColorFunction\":false,\"polygonStrokeColor\":\"#3388ff\",\"polygonStrokeOpacity\":1,\"polygonStrokeWeight\":3,\"usePolygonStrokeColorFunction\":false,\"showCircle\":false,\"circleKeyName\":\"perimeter\",\"editableCircle\":false,\"showCircleLabel\":false,\"useCircleLabelFunction\":false,\"circleLabel\":\"${entityName}\",\"showCircleTooltip\":false,\"showCircleTooltipAction\":\"click\",\"autoCloseCircleTooltip\":true,\"useCircleTooltipFunction\":false,\"circleTooltipPattern\":\"${entityName}

TimeStamp: ${ts:7}\",\"circleFillColor\":\"#3388ff\",\"circleFillColorOpacity\":0.2,\"useCircleFillColorFunction\":false,\"circleStrokeColor\":\"#3388ff\",\"circleStrokeOpacity\":1,\"circleStrokeWeight\":3,\"useCircleStrokeColorFunction\":false},\"title\":\"Image Map\",\"dropShadow\":true,\"enableFullscreen\":true,\"titleStyle\":{\"fontSize\":\"16px\",\"fontWeight\":400},\"useDashboardTimewindow\":true,\"showLegend\":false,\"widgetStyle\":{},\"actions\":{}}" + "templateHtml": "\n", + "templateCss": "", + "controllerScript": "self.onInit = function() {\n self.ctx.$scope.mapWidget.onInit();\n};\n\nself.typeParameters = function() {\n return {\n hideDataTab: true,\n hideDataSettings: true,\n previewWidth: '80%',\n embedTitlePanel: true,\n datasourcesOptional: true\n };\n}\n\nself.actionSources = function() {\n return {\n markerClick: {\n name: 'widget-action.marker-click',\n multiple: false\n },\n polygonClick: {\n name: 'widget-action.polygon-click',\n multiple: false\n },\n circleClick: {\n name: 'widget-action.circle-click',\n multiple: false\n },\n tooltipAction: {\n name: 'widget-action.tooltip-tag-action',\n multiple: true\n }\n };\n}\n", + "settingsForm": [], + "dataKeySettingsForm": [], + "settingsDirective": "tb-map-widget-settings", + "hasBasicMode": true, + "basicModeDirective": "tb-map-basic-config", + "defaultConfig": "{\"datasources\":[],\"timewindow\":{\"realtime\":{\"timewindowMs\":60000}},\"showTitle\":true,\"backgroundColor\":\"rgba(0, 0, 0, 0)\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"0px\",\"settings\":{\"mapType\":\"image\",\"layers\":[],\"imageSource\":{\"sourceType\":\"image\",\"url\":\"tb-image;/api/images/system/image_map_system_widget_map_image.svg\",\"entityAliasId\":null,\"entityKey\":null},\"markers\":[{\"dsType\":\"function\",\"dsLabel\":\"First point\",\"dsDeviceId\":null,\"dsEntityAliasId\":null,\"dsFilterId\":null,\"additionalDataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"temperature\",\"color\":\"#2196f3\",\"settings\":{},\"_hash\":0.8239425680406081,\"funcBody\":\"var value = prevValue + Math.random() * 40 - 20;\\nif (value < -60) {\\n\\tvalue = -60;\\n} else if (value > 60) {\\n\\tvalue = 60;\\n}\\nreturn value;\",\"aggregationType\":null,\"units\":null,\"decimals\":null,\"usePostProcessing\":null,\"postFuncBody\":null}],\"label\":{\"show\":true,\"type\":\"pattern\",\"pattern\":\"${entityName}\"},\"tooltip\":{\"show\":true,\"type\":\"pattern\",\"pattern\":\"${entityName}

X Pos: ${xPos:2}
Y Pos: ${yPos:2}
Temperature: ${temperature} °C
See tooltip settings for details\",\"patternFunction\":null,\"trigger\":\"click\",\"autoclose\":true,\"offsetX\":0,\"offsetY\":-1},\"groups\":null,\"xKey\":{\"name\":\"f(x)\",\"label\":\"latitude\",\"type\":\"function\",\"funcBody\":\"var value = prevValue || 0.2;\\nif (time % 5000 < 500) {\\n value += Math.random() * 0.05 - 0.025;\\n}\\nreturn value;\",\"settings\":{},\"color\":\"#2196f3\",\"aggregationType\":null,\"units\":null,\"decimals\":null,\"usePostProcessing\":null,\"postFuncBody\":null},\"yKey\":{\"name\":\"f(x)\",\"label\":\"longitude\",\"type\":\"function\",\"funcBody\":\"var value = prevValue || 0.3;\\nif (time % 5000 < 500) {\\n value += Math.random() * 0.05 - 0.025;\\n}\\nreturn value;\",\"settings\":{},\"color\":\"#2196f3\",\"aggregationType\":null,\"units\":null,\"decimals\":null,\"usePostProcessing\":null,\"postFuncBody\":null},\"markerType\":\"shape\",\"markerShape\":{\"shape\":\"markerShape1\",\"size\":34,\"color\":{\"type\":\"function\",\"color\":\"#307FE5\",\"colorFunction\":\"var temperature = data.temperature;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\\n\"}},\"markerIcon\":{\"icon\":\"mdi:lightbulb-on\",\"size\":34,\"color\":{\"type\":\"constant\",\"color\":\"#307FE5\"}},\"markerImage\":{\"type\":\"image\",\"image\":\"data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9Ii0xOTEuMzUgLTM1MS4xOCAxMDgzLjU4IDE3MzAuNDYiPjxwYXRoIGZpbGwtcnVsZT0iZXZlbm9kZCIgY2xpcC1ydWxlPSJldmVub2RkIiBmaWxsPSIjZmU3NTY5IiBzdHJva2U9IiMwMDAiIHN0cm9rZS13aWR0aD0iMzciIHN0cm9rZS1taXRlcmxpbWl0PSIxMCIgZD0iTTM1MS44MzMgMTM2MC43OGMtMzguNzY2LTE5MC4zLTEwNy4xMTYtMzQ4LjY2NS0xODkuOTAzLTQ5NS40NEMxMDAuNTIzIDc1Ni40NjkgMjkuMzg2IDY1NS45NzgtMzYuNDM0IDU1MC40MDRjLTIxLjk3Mi0zNS4yNDQtNDAuOTM0LTcyLjQ3Ny02Mi4wNDctMTA5LjA1NC00Mi4yMTYtNzMuMTM3LTc2LjQ0NC0xNTcuOTM1LTc0LjI2OS0yNjcuOTMyIDIuMTI1LTEwNy40NzMgMzMuMjA4LTE5My42ODUgNzguMDMtMjY0LjE3M0MtMjEtMjA2LjY5IDEwMi40ODEtMzAxLjc0NSAyNjguMTY0LTMyNi43MjRjMTM1LjQ2Ni0yMC40MjUgMjYyLjQ3NSAxNC4wODIgMzUyLjU0MyA2Ni43NDcgNzMuNiA0My4wMzggMTMwLjU5NiAxMDAuNTI4IDE3My45MiAxNjguMjggNDUuMjIgNzAuNzE2IDc2LjM2IDE1NC4yNiA3OC45NzEgMjYzLjIzMyAxLjMzNyA1NS44My03LjgwNSAxMDcuNTMyLTIwLjY4NCAxNTAuNDE3LTEzLjAzNCA0My40MS0zMy45OTYgNzkuNjk1LTUyLjY0NiAxMTguNDU1LTM2LjQwNiA3NS42NTktODIuMDQ5IDE0NC45ODEtMTI3Ljg1NSAyMTQuMzQ1LTEzNi40MzcgMjA2LjYwNi0yNjQuNDk2IDQxNy4zMS0zMjAuNTggNzA2LjAyOHoiLz48Y2lyY2xlIGZpbGwtcnVsZT0iZXZlbm9kZCIgY2xpcC1ydWxlPSJldmVub2RkIiBjeD0iMzUyLjg5MSIgY3k9IjIyNS43NzkiIHI9IjE4My4zMzIiLz48L3N2Zz4=\",\"imageSize\":34},\"markerOffsetX\":0.5,\"markerOffsetY\":1,\"positionFunction\":\"return {x: origXPos, y: origYPos};\",\"markerClustering\":{\"enable\":false,\"zoomOnClick\":true,\"maxZoom\":null,\"maxClusterRadius\":80,\"zoomAnimation\":true,\"showCoverageOnHover\":true,\"spiderfyOnMaxZoom\":false,\"chunkedLoad\":false,\"lazyLoad\":true,\"useClusterMarkerColorFunction\":false,\"clusterMarkerColorFunction\":null}},{\"dsType\":\"function\",\"dsLabel\":\"Second point\",\"dsDeviceId\":null,\"dsEntityAliasId\":null,\"dsFilterId\":null,\"additionalDataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"temperature\",\"color\":\"#2196f3\",\"settings\":{},\"_hash\":0.7826299113906372,\"funcBody\":\"var value = prevValue + Math.random() * 40 - 20;\\nif (value < -60) {\\n\\tvalue = -60;\\n} else if (value > 60) {\\n\\tvalue = 60;\\n}\\nreturn value;\",\"aggregationType\":null,\"units\":null,\"decimals\":null,\"usePostProcessing\":null,\"postFuncBody\":null}],\"label\":{\"show\":true,\"type\":\"pattern\",\"pattern\":\"${entityName}\"},\"tooltip\":{\"show\":true,\"type\":\"pattern\",\"pattern\":\"${entityName}

X Pos: ${xPos:2}
Y Pos: ${yPos:2}
Temperature: ${temperature} °C
See tooltip settings for details\",\"patternFunction\":null,\"trigger\":\"click\",\"autoclose\":true,\"offsetX\":0,\"offsetY\":-1},\"groups\":null,\"xKey\":{\"name\":\"f(x)\",\"label\":\"latitude\",\"type\":\"function\",\"funcBody\":\"var value = prevValue || 0.6;\\nif (time % 4000 < 500) {\\n value += Math.random() * 0.05 - 0.025;\\n}\\nreturn value;\",\"settings\":{},\"color\":\"#2196f3\",\"aggregationType\":null,\"units\":null,\"decimals\":null,\"usePostProcessing\":null,\"postFuncBody\":null},\"yKey\":{\"name\":\"f(x)\",\"label\":\"longitude\",\"type\":\"function\",\"funcBody\":\"var value = prevValue || 0.7;\\nif (time % 4000 < 500) {\\n value += Math.random() * 0.05 - 0.025;\\n}\\nreturn value;\",\"settings\":{},\"color\":\"#2196f3\",\"aggregationType\":null,\"units\":null,\"decimals\":null,\"usePostProcessing\":null,\"postFuncBody\":null},\"markerType\":\"image\",\"markerShape\":{\"shape\":\"markerShape1\",\"size\":34,\"color\":{\"type\":\"constant\",\"color\":\"#307FE5\"}},\"markerIcon\":{\"icon\":\"\",\"size\":34,\"color\":{\"type\":\"constant\",\"color\":\"#307FE5\"}},\"markerImage\":{\"type\":\"function\",\"image\":\"data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSIzNCIgaGVpZ2h0PSIzNCIgdmlld0JveD0iMCAwIDM0IDM0IiBmaWxsPSJub25lIj4KICA8ZyBmaWx0ZXI9InVybCgjZmlsdGVyMF9iZl84ODE2XzI2Mzg4NykiPgogICAgPHBhdGggZD0iTTE5IDI0LjVDMTcuNDA3NSAyNy40MTI1IDE3IDMzIDE3IDMzQzE3IDMzIDI3LjA4NTggMzIuMTk1NSAzMC45OTkyIDI3LjQ5OThDMzQgMjMuODk5MiAzMS45OTkyIDE5IDI3Ljk5OTIgMTlDMjMuOTk5MyAxOSAyMS4xOTI5IDIwLjQ4OTQgMTkgMjQuNVoiIGZpbGw9ImJsYWNrIiBmaWxsLW9wYWNpdHk9IjAuMjQiLz4KICA8L2c+CiAgPG1hc2sgaWQ9InBhdGgtMi1pbnNpZGUtMV84ODE2XzI2Mzg4NyIgZmlsbD0id2hpdGUiPgogICAgPHBhdGggZmlsbC1ydWxlPSJldmVub2RkIiBjbGlwLXJ1bGU9ImV2ZW5vZGQiIGQ9Ik0yOCAxMS45QzI4LjAwMzcgNS4zMjc2MSAyNC4yOTAyIDAgMTcgMEM5LjcwOTgzIDAgNS45OTYzIDUuMzI3NjEgNiAxMS45QzYuMDA0NzMgMjAuMjkzNyAxNyAzNCAxNyAzNEMxNyAzNCAyNy45OTUzIDIwLjI5MzcgMjggMTEuOVpNMjEuMjUgMTAuNjI1QzIxLjI1IDEyLjk3MjIgMTkuMzQ3MiAxNC44NzUgMTcgMTQuODc1QzE0LjY1MjggMTQuODc1IDEyLjc1IDEyLjk3MjIgMTIuNzUgMTAuNjI1QzEyLjc1IDguMjc3NzkgMTQuNjUyOCA2LjM3NSAxNyA2LjM3NUMxOS4zNDcyIDYuMzc1IDIxLjI1IDguMjc3NzkgMjEuMjUgMTAuNjI1WiIvPgogIDwvbWFzaz4KICA8cGF0aCBmaWxsLXJ1bGU9ImV2ZW5vZGQiIGNsaXAtcnVsZT0iZXZlbm9kZCIgZD0iTTI4IDExLjlDMjguMDAzNyA1LjMyNzYxIDI0LjI5MDIgMCAxNyAwQzkuNzA5ODMgMCA1Ljk5NjMgNS4zMjc2MSA2IDExLjlDNi4wMDQ3MyAyMC4yOTM3IDE3IDM0IDE3IDM0QzE3IDM0IDI3Ljk5NTMgMjAuMjkzNyAyOCAxMS45Wk0yMS4yNSAxMC42MjVDMjEuMjUgMTIuOTcyMiAxOS4zNDcyIDE0Ljg3NSAxNyAxNC44NzVDMTQuNjUyOCAxNC44NzUgMTIuNzUgMTIuOTcyMiAxMi43NSAxMC42MjVDMTIuNzUgOC4yNzc3OSAxNC42NTI4IDYuMzc1IDE3IDYuMzc1QzE5LjM0NzIgNi4zNzUgMjEuMjUgOC4yNzc3OSAyMS4yNSAxMC42MjVaIiBmaWxsPSIjMzA3ZmU1Ii8+CiAgPHBhdGggZD0iTTI4IDExLjlMMjkuMDYyNSAxMS45MDA2TDI4IDExLjlaTTYgMTEuOUw3LjA2MjUgMTEuODk5NEw2IDExLjlaTTE3IDM0TDE2LjE3MTIgMzQuNjY0OUwxNyAzNS42OThMMTcuODI4OCAzNC42NjQ5TDE3IDM0Wk0xNyAxLjA2MjVDMjAuMzY0MSAxLjA2MjUgMjIuODA4NSAyLjI4MDA2IDI0LjQyNzMgNC4xNzUzOUMyNi4wNjUyIDYuMDkzMjMgMjYuOTM5MiA4LjgwMzMxIDI2LjkzNzUgMTEuODk5NEwyOS4wNjI1IDExLjkwMDZDMjkuMDY0NCA4LjQyNDMgMjguMDgzNSA1LjE4NDM4IDI2LjA0MzEgMi43OTUzMkMyMy45ODM1IDAuMzgzNzQyIDIwLjkyNjEgLTEuMDYyNSAxNyAtMS4wNjI1VjEuMDYyNVpNNy4wNjI1IDExLjg5OTRDNy4wNjA3NiA4LjgwMzMxIDcuOTM0NzcgNi4wOTMyMyA5LjU3Mjc0IDQuMTc1MzlDMTEuMTkxNSAyLjI4MDA2IDEzLjYzNTkgMS4wNjI1IDE3IDEuMDYyNVYtMS4wNjI1QzEzLjA3MzkgLTEuMDYyNSAxMC4wMTY1IDAuMzgzNzQxIDcuOTU2ODYgMi43OTUzMkM1LjkxNjQ1IDUuMTg0MzggNC45MzU1NSA4LjQyNDMgNC45Mzc1IDExLjkwMDZMNy4wNjI1IDExLjg5OTRaTTE3IDM0QzE3LjgyODggMzMuMzM1MSAxNy44Mjg4IDMzLjMzNTIgMTcuODI4OCAzMy4zMzUyQzE3LjgyODggMzMuMzM1MiAxNy44Mjg4IDMzLjMzNTEgMTcuODI4NyAzMy4zMzVDMTcuODI4NSAzMy4zMzQ4IDE3LjgyODEgMzMuMzM0MyAxNy44Mjc2IDMzLjMzMzZDMTcuODI2NSAzMy4zMzIzIDE3LjgyNDggMzMuMzMwMSAxNy44MjI0IDMzLjMyNzFDMTcuODE3NiAzMy4zMjEyIDE3LjgxMDMgMzMuMzEyIDE3LjgwMDQgMzMuMjk5NUMxNy43ODA3IDMzLjI3NDcgMTcuNzUwOSAzMy4yMzcxIDE3LjcxMTggMzMuMTg3NEMxNy42MzM1IDMzLjA4NzggMTcuNTE3OCAzMi45Mzk1IDE3LjM3IDMyLjc0NzJDMTcuMDc0MiAzMi4zNjI0IDE2LjY1MDMgMzEuODAxNyAxNi4xNDA5IDMxLjEwMTlDMTUuMTIxNCAyOS43MDE0IDEzLjc2MzQgMjcuNzQ5NSAxMi40MDcxIDI1LjU0MTVDMTEuMDQ5IDIzLjMzMDYgOS43MDM4OCAyMC44ODEzIDguNzAwOTEgMTguNDg0MUM3LjY5MTA5IDE2LjA3MDYgNy4wNjM1NyAxMy43OTIxIDcuMDYyNSAxMS44OTk0TDQuOTM3NSAxMS45MDA2QzQuOTM4OCAxNC4yMDQ4IDUuNjg3NDYgMTYuNzg3MyA2Ljc0MDU4IDE5LjMwNDNDNy44MDA1NSAyMS44Mzc4IDkuMjA1MTYgMjQuMzg4OSAxMC41OTY0IDI2LjY1MzhDMTEuOTg5NSAyOC45MjE2IDEzLjM4MDcgMzAuOTIwOSAxNC40MjI5IDMyLjM1MjZDMTQuOTQ0NCAzMy4wNjg5IDE1LjM3OTUgMzMuNjQ0NiAxNS42ODUxIDM0LjA0MjJDMTUuODM3OSAzNC4yNDEgMTUuOTU4NCAzNC4zOTU0IDE2LjA0MTIgMzQuNTAwN0MxNi4wODI2IDM0LjU1MzQgMTYuMTE0NiAzNC41OTM4IDE2LjEzNjUgMzQuNjIxM0MxNi4xNDc0IDM0LjYzNTEgMTYuMTU1OSAzNC42NDU2IDE2LjE2MTcgMzQuNjUyOUMxNi4xNjQ2IDM0LjY1NjYgMTYuMTY2OCAzNC42NTk0IDE2LjE2ODQgMzQuNjYxNEMxNi4xNjkyIDM0LjY2MjQgMTYuMTY5OSAzNC42NjMyIDE2LjE3MDMgMzQuNjYzN0MxNi4xNzA2IDM0LjY2NCAxNi4xNzA4IDM0LjY2NDMgMTYuMTcwOSAzNC42NjQ1QzE2LjE3MTEgMzQuNjY0NyAxNi4xNzEyIDM0LjY2NDkgMTcgMzRaTTI2LjkzNzUgMTEuODk5NEMyNi45MzY0IDEzLjc5MjEgMjYuMzA4OSAxNi4wNzA2IDI1LjI5OTEgMTguNDg0MUMyNC4yOTYxIDIwLjg4MTMgMjIuOTUxIDIzLjMzMDYgMjEuNTkyOSAyNS41NDE1QzIwLjIzNjYgMjcuNzQ5NSAxOC44Nzg2IDI5LjcwMTQgMTcuODU5MSAzMS4xMDE5QzE3LjM0OTcgMzEuODAxNyAxNi45MjU4IDMyLjM2MjQgMTYuNjMgMzIuNzQ3MkMxNi40ODIyIDMyLjkzOTUgMTYuMzY2NSAzMy4wODc4IDE2LjI4ODIgMzMuMTg3NEMxNi4yNDkxIDMzLjIzNzEgMTYuMjE5MyAzMy4yNzQ3IDE2LjE5OTYgMzMuMjk5NUMxNi4xODk3IDMzLjMxMiAxNi4xODI0IDMzLjMyMTIgMTYuMTc3NiAzMy4zMjcxQzE2LjE3NTIgMzMuMzMwMSAxNi4xNzM1IDMzLjMzMjMgMTYuMTcyNCAzMy4zMzM2QzE2LjE3MTkgMzMuMzM0MyAxNi4xNzE1IDMzLjMzNDggMTYuMTcxMyAzMy4zMzVDMTYuMTcxMiAzMy4zMzUxIDE2LjE3MTIgMzMuMzM1MiAxNi4xNzEyIDMzLjMzNTJDMTYuMTcxMiAzMy4zMzUyIDE2LjE3MTIgMzMuMzM1MSAxNyAzNEMxNy44Mjg4IDM0LjY2NDkgMTcuODI4OSAzNC42NjQ3IDE3LjgyOTEgMzQuNjY0NUMxNy44MjkyIDM0LjY2NDMgMTcuODI5NCAzNC42NjQgMTcuODI5NyAzNC42NjM3QzE3LjgzMDEgMzQuNjYzMiAxNy44MzA4IDM0LjY2MjQgMTcuODMxNiAzNC42NjE0QzE3LjgzMzIgMzQuNjU5NCAxNy44MzU0IDM0LjY1NjYgMTcuODM4MyAzNC42NTI5QzE3Ljg0NDEgMzQuNjQ1NiAxNy44NTI2IDM0LjYzNTEgMTcuODYzNSAzNC42MjEzQzE3Ljg4NTQgMzQuNTkzOCAxNy45MTc0IDM0LjU1MzQgMTcuOTU4OCAzNC41MDA3QzE4LjA0MTYgMzQuMzk1NCAxOC4xNjIxIDM0LjI0MSAxOC4zMTQ5IDM0LjA0MjJDMTguNjIwNSAzMy42NDQ2IDE5LjA1NTYgMzMuMDY4OSAxOS41NzcxIDMyLjM1MjZDMjAuNjE5MyAzMC45MjA5IDIyLjAxMDUgMjguOTIxNiAyMy40MDM2IDI2LjY1MzhDMjQuNzk0OCAyNC4zODg5IDI2LjE5OTUgMjEuODM3OCAyNy4yNTk0IDE5LjMwNDNDMjguMzEyNSAxNi43ODczIDI5LjA2MTIgMTQuMjA0OCAyOS4wNjI1IDExLjkwMDZMMjYuOTM3NSAxMS44OTk0Wk0xNyAxNS45Mzc1QzE5LjkzNCAxNS45Mzc1IDIyLjMxMjUgMTMuNTU5IDIyLjMxMjUgMTAuNjI1SDIwLjE4NzVDMjAuMTg3NSAxMi4zODU0IDE4Ljc2MDQgMTMuODEyNSAxNyAxMy44MTI1VjE1LjkzNzVaTTExLjY4NzUgIDEwLjYyNUMxMS42ODc1IDEzLjU1OSAxNC4wNjYgMTUuOTM3NSAxNyAxNS45Mzc1VjEzLjgxMjVDMTUuMjM5NiAxMy44MTI1IDEzLjgxMjUgMTIuMzg1NCAxMy44MTI1IDEwLjYyNUgxMS42ODc1Wk0xNyA1LjMxMjVDMTQuMDY2IDUuMzEyNSAxMS42ODc1IDcuNjkwOTkgMTEuNjg3NSAxMC42MjVIMTMuODEyNUMxMy44MTI1IDguODY0NTkgMTUuMjM5NiA3LjQzNzUgMTcgNy40Mzc1VjUuMzEyNVpNMjIuMzEyNSAxMC42MjVDMjIuMzEyNSA3LjY5MDk5IDE5LjkzNCA1LjMxMjUgMTcgNS4zMTI1VjcuNDM3NUMxOC43NjA0IDcuNDM3NSAyMC4xODc1IDguODY0NTkgMjAuMTg3NSAxMC42MjVIMjIuMzEyNVoiIGZpbGw9ImJsYWNrIiBmaWxsLW9wYWNpdHk9IjAuMzgiIG1hc2s9InVybCgjcGF0aC0yLWluc2lkZS0xXzg4MTZfMjYzODg3KSIvPgogIDxkZWZzPgogICAgPGZpbHRlciBpZD0iZmlsdGVyMF9iZl84ODE2XzI2Mzg4NyIgeD0iMTIuNzUiIHk9IjE0Ljc1IiB3aWR0aD0iMjMuOTQzNCIgaGVpZ2h0PSIyMi41IiBmaWx0ZXJVbml0cz0idXNlclNwYWNlT25Vc2UiIGNvbG9yLWludGVycG9sYXRpb24tZmlsdGVycz0ic1JHQiI+CiAgICAgIDxmZUZsb29kIGZsb29kLW9wYWNpdHk9IjAiIHJlc3VsdD0iQmFja2dyb3VuZEltYWdlRml4Ii8+CiAgICAgIDxmZUdhdXNzaWFuQmx1ciBpbj0iQmFja2dyb3VuZEltYWdlRml4IiBzdGREZXZpYXRpb249IjIuMTI1Ii8+CiAgICAgIDxmZUNvbXBvc2l0ZSBpbjI9IlNvdXJjZUFscGhhIiBvcGVyYXRvcj0iaW4iIHJlc3VsdD0iZWZmZWN0MV9iYWNrZ3JvdW5kQmx1cl84ODE2XzI2Mzg4NyIvPgogICAgICA8ZmVCbGVuZCBtb2RlPSJub3JtYWwiIGluPSJTb3VyY2VHcmFwaGljIiBpbjI9ImVmZmVjdDFfYmFja2dyb3VuZEJsdXJfODgxNl8yNjM4ODciIHJlc3VsdD0ic2hhcGUiLz4KICAgICAgPGZlR2F1c3NpYW5CbHVyIHN0ZERldmlhdGlvbj0iMC41IiByZXN1bHQ9ImVmZmVjdDJfZm9yZWdyb3VuZEJsdXJfODgxNl8yNjM4ODciLz4KICAgIDwvZmlsdGVyPgogIDwvZGVmcz4KPC9zdmc+\",\"imageSize\":34,\"imageFunction\":\"var res = {\\n url: images[0],\\n size: 40\\n}\\nvar temperature = data.temperature;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120;\\n var index = Math.min(3, Math.floor(4 * percent));\\n res.url = images[index];\\n}\\nreturn res;\\n\",\"images\":[\"tb-image;/api/images/system/map_marker_image_0.png\",\"tb-image;/api/images/system/map_marker_image_1.png\",\"tb-image;/api/images/system/map_marker_image_2.png\",\"tb-image;/api/images/system/map_marker_image_3.png\"]},\"markerOffsetX\":0.5,\"markerOffsetY\":1,\"positionFunction\":\"return {x: origXPos, y: origYPos};\",\"markerClustering\":{\"enable\":false,\"zoomOnClick\":true,\"maxZoom\":null,\"maxClusterRadius\":80,\"zoomAnimation\":true,\"showCoverageOnHover\":true,\"spiderfyOnMaxZoom\":false,\"chunkedLoad\":false,\"lazyLoad\":true,\"useClusterMarkerColorFunction\":false,\"clusterMarkerColorFunction\":null}}],\"polygons\":[],\"circles\":[],\"additionalDataSources\":[],\"controlsPosition\":\"topleft\",\"zoomActions\":[\"scroll\",\"doubleClick\",\"controlButtons\"],\"fitMapBounds\":true,\"useDefaultCenterPosition\":false,\"defaultCenterPosition\":\"0,0\",\"defaultZoomLevel\":null,\"mapPageSize\":16384,\"background\":{\"type\":\"color\",\"color\":\"#fff\",\"overlay\":{\"enabled\":false,\"color\":\"rgba(255,255,255,0.72)\",\"blur\":3}},\"padding\":\"8px\"},\"title\":\"Image Map\",\"useDashboardTimewindow\":true,\"displayTimewindow\":true,\"showTitleIcon\":false,\"titleTooltip\":\"\",\"dropShadow\":true,\"enableFullscreen\":true,\"widgetStyle\":{},\"widgetCss\":\"\",\"titleStyle\":{\"fontSize\":\"16px\",\"fontWeight\":400},\"pageSize\":1024,\"noDataDisplayMessage\":\"\",\"configMode\":\"basic\",\"titleFont\":null,\"titleColor\":null,\"margin\":\"0px\",\"borderRadius\":\"0px\",\"iconSize\":\"24px\",\"titleIcon\":\"map\",\"iconColor\":\"#1F6BDD\",\"actions\":{\"tooltipAction\":[{\"name\":\"testTag\",\"icon\":\"more_horiz\",\"useShowWidgetActionFunction\":null,\"showWidgetActionFunction\":\"return true;\",\"type\":\"custom\",\"customFunction\":\"console.log('It works!!!');\\n\\nconsole.log(entityName);\\n\\nconsole.log(additionalParams);\",\"openInSeparateDialog\":false,\"openInPopover\":false,\"id\":\"f9b4925a-818c-15d2-6220-cf2f317bc7fe\"}]}}" }, - "tags": [ - "building", - "interior", - "venue", - "inside", - "room", - "office", - "manufacturing", - "floor", - "plant", - "storage", - "warehouse", - "depot" - ], "resources": [ { "link": "/api/images/system/image_map_system_widget_image.png", @@ -38,7 +26,7 @@ "type": "IMAGE", "subType": "IMAGE", "fileName": "image_map_system_widget_image.png", - "publicResourceKey": "hDdSISQr6elribOYD6T3uePXZI5WvNtM", + "publicResourceKey": "otJxNhSbraXccAZhnPzmfOIdEXra5Hf5", "mediaType": "image/png", "data": "iVBORw0KGgoAAAANSUhEUgAAAMgAAACgCAMAAAB+IdObAAAC+lBMVEX+/v79/f37+/z////yyUz6+vv4+frMzNn7+/tvz5f39/jy8/Xz8/b4+PmyssX29ve0tMf19fbGxtTDw9LAwNDw8PPR0dwAAADx8fW8vM2trcGkpLrPz9u4uMnBwdHOztrIyNbr6/DFxdO6usv09PSoqL2pqr7y8vLLy9fLy9ju7vLt7u6iormgobjo6O3IyNWwsMPT0967u8uwsLBQZW/s7fG1tca2tsi6urrk5OrKyte+vs2xscS/v8+3t8F3h4/m5uyvr8OrrMBoeoK5ucrAwMHq6u/r7OyursEYGRjo6Ojv8PDV1d/Pz9rMzdLd3eWzs7Pl5ufZ2eK3t7hEWWWyssDl5eXY2NjX1+HT09O/v87Hx8ioqKmfn6AxMDDl6Orc4OLGxsbDw8Smprxyg4slJSS8vLxdb3rn6uvi4unj4+TU1Nu0tLSrq6t1hY1meIHc3Nx7i5IMDAzq6urh4ejg4OCMjIxbbnh2dnbX192cnJxuf4fh5ObV1dXQ0NHPz8/Ly8u2tratra1xgYmIiIhkdIA1NTTa2trV1tlTZ3FFW2YhISHf4Ofg4eK/v7+lpaWHlZxrfIX19fi+vr5qamtXV1cUFBTO1NecnbSBz6KPj5BwcHFRUVIeHh7b2+Tb3ODT2NvN0NXExMuZmZl9jJTbulTpw088PD0uLS0GBgbe3ua/xsqWl7CXo6mjo6OhoaKEk5mTk5N/f39gcns8Ul7X297JytTHyc/JycnCwsKcp66np6eOm6Kut72RkayLmJ98fHy/plmys8OnqbuDg4N6enpXanVNYmw5T1xMTExnX0q9wce0vcFJXmlnZ2dGRkZEQz4CAgLt7fKosbeiprehq7KSnqWWlpeBj5d+c1Kxur+GelXivVDZ2d28vMWFj51oaGipllrR1dh91KF10ZtudohjY2NdXV3IrFolLymy5ci4wMSqs7moqLOHh6SYmKN+fpxvmYBtln5YUkKhoat00ZtJWmywmliy5ce3t8ZveozbvFzZulp3blBe970rAAAf9UlEQVR42rzbeVAbVRzA8d/bN7uJm02WQJImEBIIEAooISCBJKiA3KSWgnJTxVoUpB6F1lItVESLSkVbj06ttbXOVLH17Gir1nGqtjreZx1ndBwddcb7D//wT3dzwIbsZheifv9o0mWTeZ+8t48MBMBXPKsB+VaZQDrKgIRVsxCnmz+HOBlzMuN9NRVEQ4DhmjuVOMAaF+JHwlIZkE5/+pflQ/JTQbIbHwIlmeJBSD8phFQZQboDp7+DOOXn1IF0pZIQBE+BogLxIWwUpBRCTWyCJhKie+90wfIhmQ2QaC57XAgjhFQUQiiiHE1ktw2X9MJCR69G8SGF/zHEFxeiEUJ0lRCMrVXlbs/d0DswAfNpTr9BQXRIKCuVhiCKrayCSCQliGEYEIlAEJPDtfgkk2FUApI0CMF2v7vS25t02eT2WjJ4lrlwsPXAsTd8aiAJfmBM3WhrwKQKZjWZ3Fwme1oGV/CI1WpKcUTKSk/Scb2mkwpynA0NOofNYrFkXWANBNwmq53L1DpoJiJD1uo1+Y5kiC7P6XQmRc7wf6JhaHWIQRn3uFYu1J3EVVbjL8pJ43v/2GdvvFHdUNGd5fF4ymwOLhufK89qDYpeS+YlK1V2KycLqAwpqsBoKaNlMo1M6ahFl6kxZtbx5S9kZriA0ZDARdGlowF7KJPJZPfXpNdYbIakhrRzg+05YunmsI7kDDdfko8iOEAZeO3JvlVZe849j2+dU9edrit7zWm3hxmrAp2dnXPeTA1J0HzGYzeSJEiGCFRFESQCiVgdA4pCJB2zrggtY86sLEsh6UyOyr1m9sCgkYJgo5Yal8uRsd8IlGamIjyH2vRPEB9JopjoY3K7ZGqsQggxQ2KhdBUEo1hSakdJYsLn6jRIqpO3URA3JAMxQoLVWEGm0gUII+W45vajkAhEnzik2KQYAjq9FOTXF+RWFhEXwiQOsSwF4pWCfPnC5bIQIi7EDAlmULC09BAq6VpEEGIO/fvvgkxkXIhGp4EES7crh5RNT255a7cI5KtjB0Am6r+GlKlApnwBZLqra0wEcvP7RGIQs46BBMvIUw5J39aztW0o1pF97GWQS5tKgnTGxCGmPEIWsisCmd77/FZaZM+6mQC5aBmIPnGIWhbCzkO02loU2/VfgmxsXEj+vwEhIX5GAWT27ECsw3jmVZBNHxdSqmP/T0jN9CW522Iht5z5PHEInTiEUAzJGhs/lB0LOXDmJMjGpFIgXWbiEHcekoXQEKp4bLJtTSxk55mnEodQCUPsoBhiGfuubTwWUn/mIMimiQup+xcggaVArmw7EQsh7zwKspllIGTCkNXKIReMDbe1odguelYJRAvSFf4LkErZISRFhpA8tnuiVgTyrAKIMZWOCyEgwTI0S4F83X+DCOTeZ7WKIRiLXuwJQ4w5xFIgA/1aEci7dx4ATDStAQyS5YcgeLIJYsIs6UOR+7C8Wp0gD6HmIeOz7SIQ6u1m3HX3m289MiQ9jE/CkA+2iUD2TrRGILdCqKH+Wo6sPEcA5NLMQ1LGymd7kEhX3wtP5mLc3hhcO7wGC28wFxql+TthCOaL3OH/8eLQPbSCCB4nbq3tKgflIR0sCXLlbLkY5Mv71Cu0GGAb4KZ3Gt8iAE8cbpzibsrXrj+LcPPD5eufuASB/oHGqXd4CO5/5uEHP+Ae0bxh314S8KEu3Fu+5cEPiJHDKw4PAZd6eFvHBCiv0rQUiO3asdlhMcjn58PeO8YRxphubMYb3sJDa1nc9ybuf4eEqSk8smI77noQ4zvugaYHg5CWtdlw6AGc23yC7mnswFv68ea1NGyZxCT/gvANv0LAEnJblwIxJJc/vRuF6+jdcUPkgnnqTCne8e3G50hcshmAfhJPDWBAE/jwJgBiBRpZi4FbMwXrMeAPQpCt/BF111bA8NPrQQj3iOOv80sLgg1kdyBQnt0OsjERCDIYRmbHI5Dc2p62/kOh3fiWi40YY/Xew3jzg3dzqX/sCK7/jdkA+MHckTv4YZM3PBAFwY25bVMYw9nn8ZYr8ebdANu3LED2bt5cD0oig16rHSmAkBCMMBj6WnIjEH1/f/9xtgvxEZ9PD2MAWMFumMV8U/0YQI0PN/EHyTCk/o4oCHys7XqYg+weEIGMtGzWQqic6oqkC7q5amrSdQZbOIPFYrOrfC6Hw5HhLtRHQ1Clyeol4kEQIkLTceUNbf0T2zchLeIjT+3c2I5xRyM0P8Jishzf8CHCx3/E5fcDnvwJhyGwvgPYR7pImiZK3kEw8CFWN9bDjlztAoRj05ggydGxS7Q9ZDCITU2RyDxnykjmFC6fSqWynJskgKAZi81kMulM7MIhSq8prUxHJEXRDGM2GFgCiaRx7Dk4/eEj6z+qx3j87jvWd2Dc8sjaB7QY+h5Z/yNJNK0FI1rBGnfevX7fvu2jKlVh776PnnhixFv66BNrH2jTaB6uxc+3eEdr7zeyWxovmXO5XA/f//BZQzCIFxl5z3kkKcs3auQngeUYmcBFzLj8Fq7uGl1Vag7fkdRw1bp1XM7q6lRdetmpiorUnDSu8/bs2XPk6pM0SWs7y7K8BE0MlvkzEUW1FruMiNy1ykJir8eMWUcl0hKAVAbD4OxerEV1KnshosBrrcSoFVBnAGGvm6IJk6vG9djrW1siEPl8Nl5TU11dVJFafcQo9fP6Ij/FGEsLC+u4l7I0X8NQlPB3E1rabDbfcvtN3NF4EUVePUWqSZKIXCNciGSZzFE3tzg8LEFGQij50frpkhYUDOSbs7HAR3JD06jyQKJA0QwRjFapCJDo19shfqioCebDN3RhwZe8HIQSnpv8df1YybBSyGAABOWpQCJvESMPufz9A3KQNSCQYBDWqfIIB6zmZuSZQyWEMshclAPlWaW/IRLyEHT+4/IQyQpVxSCI4CD1XSWUEghq1aFoiAkkYtPDEG0cCDx7k0bmXV0zlnyTrlF1gzDfo/XtLW1IAYSyF7MQlckNEmk9SiCf3nQgPsSIQ4kOJ3pGYCa9vn58kwKI2VG8H6LzSi4tdZFaAeSlm96DuOGd+zZu3NchKiFUHibq/86d3/UheUil31YY81xJtCSEJog1rAyEfeGojKNxqr19qnEnBpFUHhMIm37sMW2TLKTVEiAhpoBKQqIt0tdPys4IfH5RXAfeN4W5ph4QnRK7x/PKK+0w38i7vWdr5SCtFYUgEiX1ETSW3357C+Qg9cc0cSEb23lI+0YJiD87dzvMR1542eZhwfbbVAAx7c8yg3gXiOuZopG2tg7ZGYHrfokLWYGDrRCFrPJ8U7CmBxb67UIi8g1xZeaa8tmS2J0uWXowZXZxSH1PSQsrCzl6EZ0AxPP07AQs9Ncfzw2EIS/5L5xmx5tiFlYnSKZBVetiNy86qWBHSUmzLOTkVQ8tG2L3HBwaqo+C/HFPGKI96Pf7u7oIiM7CQpwIL3NemtVLgbCsgvK+Em5GqPiQg1d9tXzIqeff3J0LCzXk5zfPX+wpq/xdPeRiiAbkGuz2z4GgmoL+Q7cSshDNdUcTmJHmy4qFT57KonDAVUSTsz1NS4YAWWnSqQYpCOcpKCjokIfAyYvQciGqU5s9KhBUvUsIATsqmRxfBGFAUXOG7jWRPaCASwGk4O01y4Z49ntejILQUZCArRQWZdGCstDMYy99rQ1BrtxdqwACF51cJgSpPJ5TDhDkjIaQsZ/qNVlBWbc8xnUQuNILjnd0cRBSDvLydfrlQzwpICiHEkD4WucgOsZjBEU1tbddMj3EL1xDQcEmQgnkvdsuWTbkxVODIGjdYgh0xixHAwFKKn96or293eQFsBToCUWQh277dHkQQuXZurU/CkIuhoALoiusqQTpkIaCUBN9vRwkYOVnRB2BqCFe9HVfLBuSW58LgtJiIa0qPURldYNk3iydLqUU+F6f6htqb3d7AdzVhDIIvHfjsiEAIANRu1KiJUTVKEjUmZ1rrFudoWvVANRO9W1qb1/JP8A5DyEhbrnHaGkI+vv7739GohBypQeBMCQCAcrug6gcTgSiZWSHG7PxVx43IwHEP4FTLQY5PhEeBQCic/OZ4Ab8qTTk53O4fhaH5C2CEGlELIToTHIzGkH711EgWoUmIvGumjy7t29oyA1ctJMMQtRRkG1acuDKiZ6SDVM77unr63uJp8PNtyNJyPc85HtxiMpDRB8QQCwGLpsjpdiTlFThDFZd1RC8TXNIfQrCmD3f9OQPO5uBjxGFDI93tZzo6Rm550RHx0BfX8lqDQD71elmScg5wUQhVN4iCJWGIoG7dfVgYT5DcTxOp9cwFAIAZJ6zcRQEYtEqm3ceMtG7CUJpnJQIpLzl1lsHjvdkn7hnx46ODRseWz1DEoPXnj4IiyKCxYMQFOvzMKyGG2I4khVApPMglAzimZ1FVbrWIOREby2EylynZVmzMb9OpfIWlhqZyrn9+2dWx8YdNp3+wsplt1sXJYSkpKS4HBZLVjqXLtzvOVE5/zwvWFp1KkjnR9ISytm52lDt5iDDG4YAwFSdlvPaa1UVXFXV5x3RNTQ06EJlFRdbHClcNput28+XxXXT6WSXz8XlM7lNwWIhps7RTL02GKsJlplR7K3kKywNNbpOyYxkIS6fmwKRdFaEEGMqdniDv+YutMwY62oYFMpgI5FML99FUxSFYhJCUEyMrzh6NHVphAJIOuILeOwzZlgUytmFgrktdvugptOpYZhMDx2BGGQhzW+rUSTlEP1iSGYaqegaCbVaV13hHi3k83q9dXoETKcdhSMHTabqnBwjw5jLIk9qk4egm59aBoT2FZMgrDSNUgDRoYWMnRkGPv5PcHR5Pp8PCdrlc+YzDJOFlgB54b5lQLS+YjUIy1cEKYoeDrFrl5ZAfKWmC4pakTCVimGMNUuCXLoMCOUrJkCYMU2rANIgORxjUVFRAAnKzGLyXcULEEoW8tLb2qVDyFgIrQCSKjkcqqjIxJj8+2kUaS413eNfCuShtwuWDlGvLEYgzJzGKoA4dyGpdDqaJHe5VWihQptjKZDc299bOoTwLYJo0vQKIDnSkEILyad1uAXTlLQUCLrtqqVD0GIIowiyjkFS0WVkMMYeWIDoULiUKEjB5DY6Mg1qwf57p1rM0bsxBNnYKyZRFUNU+jRGASRNGvJPc3cCFUUdB3D8959/M7vN7uwywOoCsrggrNzoSrvEsdynEUcEISIi4sXhkWZCJhpalCQdallZSaav0qzsUMvu+7A7u+t1vl69jle9rvf6z86yxzCzLGbW5/GYQeDBd////wyz7kJxFj3iwmm0KyRLNkRt51utV9bTqMY2e//6TefZBzud722bYZDpOPz1dWLIdV93YjxmiD6gkCSjYog2nHZriHeF5MmGJFqXllvXdbRvjkh6YD/R2VE1hYwF98KMOUgC8LllpMNVUlaOAUlEhYEPNpCpRSUZFEOofNpD1SCGhMuGoKWt1rmtrVfbv01Kmt3Uaa0aPE+joagpO2e0jA556ofT3X54VH5EaA+KjAjDicCpovMiFlyWUuBEJyUgRXlX0W41Wc4P1JZSSGQyMV7NrIDsTCBPlA2aWawRmNPNO2e8ped5TiB+HNniPdd5Qq5rxmy8k8r91Nu85BAfpkcKI11AgNr5jr6+2glEa1XVxZ32wXYALuk170FD4FnW2vRHgnREUCRRmJYXG5SVl5f2nU0XqiJXDzF33pkkFZFV6tqbFx4eF0G2yd0OXZ6TLiioMCbUlBmG8N6fPSGfLcdo6qQJUVHxHqbwEF/bU4waAS+Og8W6tKOqvr71AfI04Y6qi6/s7LBSoE8yIwGuSCQs2OKqaCBXOWl3NqQbycVaQkpGrn5RSmSeKUG4OkTIAmQTbUsxG/R8ulmUoPGl5QS9DscclmEQeGBpCAYnir7KmJ7RoCIljTzjRiOUkEaNcE2n1rXcg9a5Re1TP7RWrbX2DdoBtEnFYsj8a1evXn0Y71gMtLkwPCs+Q0OhEPpqTLm9pquhBOjca4R/rbFxgDdVgF8VDof0I7B0amHwxtWQEAZ5M4RLQgjEMLzWkDJ16k4tAqAotSYplxJDyl0PzU3XGmmEyB6pTLwRg/jpGCMqdxrZAIVcX5qhML5nFgZ/9A5HujTEZ7H/+KgkBKhp0hBjvidEp7MVhmZmZofFFBaGBU8VRQmvXPORuryF3BSTr9pxKb6k8/Y+XLSn7Hooan71dh5RiLu2av7GekDsrsub2wGdew9OXN0xf89K/NzW+Qf8liCHIxGkJdWSw69Ebnw27ROiifWEUALWkHv2zLNGKqImEFFT00KiyUU1CblUONJRN6zE1zxXgUqa1bB6H6q+URg2pJ+4DhbvZfEljyL1tUWYPEC0euJaaL0cw+pBwODPsEwIbvI5IYKEMb7RN0Q7zyfEgyxC974hzuBaI2Xkka0cFkLOAyiZX4GB0pCpRQkhr2IK7imCvVMQKm92htyOGXYiIlML/NviKAGZkrIfr/vss+t+LJPpADo+W+0bEqH2hCgxxpnRyBohXCEIX3RDcyfUeIWgpn3cRERuhYliCDDMRGrskCMkRKak/NHm5cubHy2X6QAU30j5hOgjGE/ICkpQsTJROQTIxhUCCDD96CraKwQuaYeJGhYllrlCuIBCjjoWwmgIjwAZ8dm+IWyE3h1yeFfn4daHZtvt9qLFPiHauHS5kPaPAdufoizLXWukBLNbLfieVQBP7XeFXCWEXNKH8QmEsJNphQwxBPngIrTukDVr1uyz2yvsxKAkpJhhyTL3DUHw5I3Nr/DT0HOXVzhH5MmNW9sRrb5m/o2rEBJDtELIyuX3+A/ZIhuiD9KAhJ8QJsLgDulcs8Zur4uIq7XX2g2MO4NlNfkqIl5lwNQ0slOMkQGjm1QztRRNn33nBFrPZBeemf4qZqjiyNJUAwCQWgzkRasVtgD+Q5Y55siGaEERkobQEQnukP2Hm64u+jbNLcsWGRrm/LUwj2SHZZNHKIemkvszXVy/VEalmhd6RbpBo2H5VzHHEnwa5cHYKBiT3iEbwvoLoaQhlNeIUApYQxrP0EgeN13PCjh6FWbFEIbysCFQphEsXPhpW9u2hSPmtIy4ufTCljkLKzQs+GB4njeULJwqCUFJ53Mc4zeElOSxSAkdM4V1ohDtCtGLJ5/qlpbezd1Ez4YNGxzLli17Z0OBYIHgsstm+HWZSNxf0NO1bLirra1tqMCtZ8Yz9/YMbzny1qG3jhx9p63/ssuGPjqUX5htCglRDmH8hKAYDetDk3Wwt/qbd/oXuHQPkwgXR1fbhg3Ci2N4y10v7Dy47WZyy9+xZcaMZQ9va2mpbjm4bdvNvZV3bbl7y9EjW9rIt7uha9ndR48cOXJ0i2B4eNhds2HGOQpeAkpRnl45JIyEMBQCDIhiNL23dD1LdC9ra3Pc0XvwgfwrWBjTsi6QQSsftVg+PTTbQuZlxZwRB+/cWV1d3XuQ8BOiY/2ELFRjN+71Bfedc1fvzVpxAdLGcCOMzeGQDbHRoIgODUWS+341ahc/IUHyIXxi7y13HdJjNLhjY9neso07ihDG8TrkRmnCNYGEtMmHMOMIeS2gkEjOt0CduHnbIUcBWcgFLwKubd5z/LHbdt/22PE9zXaMzchDG1hID8hgbJzfEAq8pQcUcoYBCWhN713DPf0F7xSQQ8nQkSOHXn+xF5Mz+Senjfhk/g41Bq8xCyikawhkcDYWFKmlIeaxQ/jEm4fuvbegZ1nXBRdccO9Qz1DXW3eUcCDC6ueO7T7NY/ex59TYE6IPLKQAZLA2PSiiAg3hSzZve/3unv6hu7suIBb0DHfd/fCcROlthPE1x07zdWwHxuMN6aZkQ646wRByEGu5y9Ez1DZc4Pzm2452dXVtebe3Zdv2XJCFce389yUh719ux+BZI1oYW1u33LLW23hQhKQhxV4hwszp7ulxDHUdebHXwAXw+94waibrQ+KTZuQeEk2AIXKTiLdpxxGSEmH0jIiGAlmcTq8QMrjnNJfbHrttZHdPER7viMhNQK1XCE2NDqEVQxTRup3yHXjHcdcaf37v5Xufd6364zvcc4uXCWGngNvixUBs6K6QDRnJM0eRuz7io31uzdEhhrFDKN0tCiEbHxO/959eKcErXnlefOOxjSMh9BSZkE2bgIPFL2Re2Xp1bVGRfakQUiIbYgSRijwsQjUpU/WPQ5CuUiGk7DZxXi2vwBROXO56qwxjhCiCkVkjqL6e7eT0dXWXbtq0qb2oaIoQIneFqHGHTGeQoNRfSG4gIRBUifTVciF7d4tj0Oy8IGwWx2f3XowTnAyWtNx0o4bjkDuD0dbfGptTrqmrq3u8r6moqLWE53oKqkEK0QZbMUvTzpMfclJFacDNFKrlvS2KMOs1IlBmqzy8a1eiTMjW990jgj0jshUzsaL87yOSk+YJ8rN0keRyMyvcKTa2zsuCz2OmNzaaosl9gE7BqdHCJtbkFBy6SAzRR58Z7BRC5KWF+XrkjCAXUBZZyfft4/yskedfWeGzRjAIKIaPoGBM6u7+gzTL6zOmnZka4nxyM7mQnpyaut2USoomTzCjEekNZwZHhwSTh1tMzi4VLr5Nk8+aedMVGeffNCk0wqjXjjkiMZWW9kFeJuTj99xHrRvdR633riEhIjqQEDja34tkTEd+REcjmqGQmzbJcwedsumV169ebYfRJdaNMueRjVYM4wmh3+hPRDLC/IYEIx/6JNodghTXe2blrU19jEyIfvkfp0n9uVw/vhAurp/9xyGsV0iHtRzkqSoJALm5dXxUyF/XYK8QBGPSP9GNxh0SIgnhkih3iNVarxzygOw5EV+0dbekY/fWi9wdQAUSwsdtOAkhEd4hSxVD9j+1SzYEf/meJOS9L/E4Q4zhT//zEGaeJ2S2dbNiiOXi66fIlmwipxJv75fVYxhfiDl/xj8PoX1CqhVDDlx/wCI/JF887xPy/Mt4vCG5cQPUeEMoaYg61ntqlSiGWK/vA1m4pOwrr46vykowjDOk4YkBHsnIRIIKM714ivCtq/2GUF4hHdYK5cV+0Xr5Soxn7/Fa78378bhDgp8YSFQO+WBteVXH4SX7yPNeEld4hURLQuI8IeutPMiLr7SvX/8gyEJw7LhnpR8D7PPOAEK0aREDvYoh3BISUrW+dsnOSkuHXTkE5XtPLRrkpVYe2GVfrDDD8eKyr9wTayWGcYZowsLfGGhRWCMUf/aSJR9UVfU9sGTJks0dfd4hFO2NiWU4ghZCVl0PCn4jP6KmgLwJRtw3X5xc789vwtg7gyZLEJQgWlt8/szQ0nAhhOX14t8T0CSYUzKuqJkWlWranjqz5sIlXnIme5Tmq3zZXNuzJgECJWelZtY9oBRyJuDVP4nXiR9jNGmyk6kxrDCS+C4/Pz9PZ8ssdInUOQXFxITZdHlZwr4Q8sb06dNNIabG6d7mTXYSftaVUp1ZmqWSMUkAoBxyf86uOlohJITB9EbhtHi8WY8NNVGThV80pGrI0NIMc3byIpYB/zSmJwZKGxfRIKBpPe9iiClmCYYe4TO4wcG8WkSBhN+QK1fVaRRCTHqEV2799bRPyjZjLM4ZBC4ZycUwJubxAZvMxOVjEkARCdGDPP8hFPUhUgxRA7667NeyVkxx0iNBshnGdstAIzpVIauezAClqUXGGOPZy2djy5NPIvCRkpwAY5vzUjDIhRj+hRAEoBiiJ/MUC6DpbcnHmZMNMDbDgAlG0/sLQScaAsohpiniijMideeBzeAjIVkDY+MGHkf/fci0nJwP99er1aqo4isPHJBcCBiTtTA2auAN6n8RklM9V62een6xdc0aBD60yQF9uYGPTtWITPUfcu5KtVqXUrz23DXgi01mIADPDIFciPFUh9TdSlZJVqLlvAOHwReTTEMALlgw3hDqBENUfhY7GZELSUiaxTLLwp0HPlAyggAM9cNobIzm5IdE+XlfzofWFcKIkJBNlkHwlQyBOFpwqkLOAiVIlXNglV2tpiMtlnWzZq0DXxEQiDu6/vsQtSqncjE5aOVGWSwrVqywgK84CAT3jGyI9qSH2MJACa0ia4Qjp5GpZESsrSCRB4GgzpGr8xeiVgg58b8ciISQSjUdTEJW1q4DCR0EZIAaZwh90kPgLBJyCxkRFVnstXNBIhICcq/lFIVEgaKGnJz15WSNkBF5qLYdJLIhIAvehVGYGP6kh0zyE5Kbc2uiRa3mwyyW2bNrpVMkDAJyQfepCYlSgSI6Z215olrNRSZa9q0aFTIdAjJ0DhpfCKMcstNvCAJFVqu1ipwQG6fRQJ/oGtl2n0EmRO83RD4TPdwPylT+Qqrs9otISEZYwoEnE0HCBgG5+b6DJynkzf6P/IZQoCiHVTvpKoA4scMvfe9RmRB23CH0W7+/rgZFSKWixw4xB8FoWRCYdwsWnoQQw3D//eAH5T9Ezzpp42S+cB4EprrgDpCgYzggNtlBBicXMudz8UH2uRkZZqd0o5OWc6KEkBRBQoJBq+UZBiGtgewJz8TKbQjdnj0i7gp/f2XSv7sPjQ5hhNf76vc1WWfNenDd3KaH5g4+OGu2Wink4XMOMSCYl6xgXuydQU4xokaTyST8d32kU7JYERJi2r49eCYjCaEgMHf0y4egptp1+5qKiNaEovbWufZaLZIN0Xz0+QsgSjAbjBI85xQdQoE8iknJR+BSo4uOnsSCt3waAjPnPo1sCCSy6MG+tbNaH1ppXNr60Dp7vVHDIGClIdsWdFeDEwJlISREyWux4JYR0hAcFF2TomWQ0MjQVKmG14r0jD/0wpfelK7nbD3DcUaBwWzgxRs5Pd2ckpK7KCMjOHhRrpkwGAxGnucP/dLluSEwoBMIMcSBh81A8dGROiKvVJD/vS4oMGe8NPBErFN4KUE+LfKR0vAst0hTZKQtLS8vPE6UFhdJZIaGZhaS7Rm/fMSNjAf+G1Rcm7rvZj7gAAAAAElFTkSuQmCC", "public": true @@ -49,7 +37,7 @@ "type": "IMAGE", "subType": "IMAGE", "fileName": "image_map_system_widget_map_image.svg", - "publicResourceKey": "QKRIYhDeBGwjaeIS601VvNLSsvZ25DRj", + "publicResourceKey": "P4hZLRjmo2P1RjGPZ4CrCMxh6xOQFQr5", "mediaType": "image/svg+xml", "data": "PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0iVVRGLTgiPz4KPHN2ZyB3aWR0aD0iMTEzNC41IiBoZWlnaHQ9Ijc2Mi43OCIgdmVyc2lvbj0iMS4xIiB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciPgogPGcgdHJhbnNmb3JtPSJ0cmFuc2xhdGUoLTI3LjA3MSAtMzA3LjkpIj4KICA8ZyBmaWxsPSJub25lIj4KICAgPHBhdGggZD0ibTkwNi4wMyA3MDYuMTMgMy40MjkyIDE3Ljc5Nm0tODgwLjg5IDQxLjEyMWMxNTAuNDQgNi44MzM0IDE0Ni4zOS0yNi4zMzQgMTY2LjQzLTI5LjMyIDM2LjE0NC01LjM4NDggMTE0LjI5LTYuNTI1NCAxNDguMzMtOC42MjM1IDQzLjM3OC0yLjY3MzggMTQxLjc2LTExLjIzMSAxODguODYtMTkuODM0IDM5LjgxMS03LjI3MjggMjIxLjM3LTAuODYyMzUgMzE5LjA3LTAuODYyMzUgNzAuODI3IDAgMTQ2LjkyLTEuNzI0NyAyMTguMTgtMS43MjQ3LTMxLjYyIDAgMTE3Ljg2LTIuNTg3MSA4Ni4yMzYtMi41ODcxbS0yNS4wOTEtNjguMTI2Yy01Mi44IDM0Ljc4NS02NS44OTUgNTEuNzQ5LTk1LjYzOSA4MS40OTMtMjQuOTMxIDI0LjkzMS0xNDAuNC0xOS4xMzktMTc4Ljk0IDM2LjY1LTEyLjI4MSAxNy43NzctNDcuMDAzIDQ2LjU0Ny02NS4xMDggNTkuMDcxLTIwLjEwNSAxMy45MDgtNTYuMDM3IDQ0Ljk1Ny02Ny43NjkgNzMuMDc4LTQuODAxNSAxMS41MDktMTMuMzggMzUuOTkzLTIzLjQ0OSA0Ni4wNjItMTAuNDk3IDEwLjQ5Ny0zOC4zNzcgNi4zODU3LTQ0LjAyMyAxNy42NDgtMTkuMDA1IDM3LjkwOC0yNS40NjUgMTAwLjkyLTY3LjYxOCAxMDIuMDVtMTkuMjgyLTYyNC4wMWMzNC42NTktMS44NzM4IDg0LjAyNyA3LjM5MTMgMTA5LjktNC4yODU0IDEzLjI4Mi01Ljk5NDEgNDEuNDA3LTIuNDYxNCA2Ni44MjktMi4zMjA1IDM1LjMyMiAwLjE5NTc4IDY0LjM4MiAwLjYzNDc3IDEwMS45MiA1LjAyMzIgMjUuMDMgMi45MjY1IDQ0LjY2MyAzNC4yODcgNTguNTI3IDUwLjY0NCAxNy4wOTkgMjAuMTczIDYyLjc2NC0xLjcxNDcgNjYuMzA2IDMyLjEzNCA1LjEwMjcgNDguNzY2LTYuMzI4NCA3OC42MzcgNi4xNDExIDk3LjM0MiAxOS45NjkgMjkuOTU0IDUwLjQ4NiAxNy44NTYgNDQuNjE5IDgzLjk3MW0tNDcyLjQ1LTM3OC43OWM0LjY0MzUgMjMuNzI5IDE1LjA2OSA3Mi43NzYgMTkuMDYxIDEzMC42NCAwLjg3MjA2IDEyLjY0IDUuNDQ3MiAyNC45OTMgNC4yMjIzIDQ1LjI3OC0yLjUxNzIgNDEuNjg4LTE1LjcxNyA0My42NzctMTUuMDkxIDYwLjM2NSAxLjQzMiAzOC4xODIgMzAuNjE0IDkzLjgzNyAzMC42MTQgMTM5LjcgMCAyNC4xODEtMi42Njk2IDExNS4zOSA3LjMzIDEzNS4zOSAwLjE1OTExIDAuMzE4MjEgMTAuMDY1IDM1Ljg4MyAxMC43NzkgNDkuMTU0IDAuOTQzNzggMTcuNTI1LTI0LjQ3OCAzOS40Ny0yOC4wMjcgNDYuNTY3LTUuNDc3NyAxMC45NTUtMzYuOTczIDEwLjg4Mi00MC4xIDI0LjE0Ni0zLjg2ODggMTYuNDE1LTMuODY2MyA0My43OTcgNC4wNDY1IDU5LjQ0MW05Ny4zMzctNjkxLjAxYy01LjAxMzMgMzUuNTE2LTQzLjY1OSAxMS4zMTctNTguNTM5IDIzLjc4MS0yMS4zMyAxNy44NjktNjIuNSAzMS40MzItNzAuMTI0IDM1LjM2Ny0zNS4wODggMTguMTA4LTExMC40Ny0xNS4xNDItMTI1LjYxIDQuMjY4NC0xNS45NTEgMjAuNDQ3LTAuMDczNSA2MS40NjYtOS4xNDY3IDg0LjE0OS02LjAzNTcgMTUuMDg5LTE4Ljg3NyAyMy4wMTctMjcuNDQgMzIuOTI4LTE5Ljc0OCAyMi44NTYtNjkuOTc0IDY5LjgyNC04NC43NTkgMTAwLTcuNDk3NCAxNS4zMDQtMy4yODQzIDQ0LjQyLTMuNDcwNSA2My4zNDMtMC4xMjc5MyAxMi45OTQtMC44MTAxNSAyMy4xMDQgMi40MDM0IDI4LjI3NiA0Ljk2MTYgNy45ODU4IDIzLjcyIDI4LjExMiAyNC4yMzkgNTAuNjExIDAuMjk0MTEgMTIuNzcxIDAuMDEzMyA3OC41OTEgMy4wNDg5IDg3LjY1NSAyLjMxMjYgNi45MDU1IDQuMjIgMjYuNTY1IDEwLjIxNCAzNi41ODcgMTEuMzU0IDE4Ljk4NCA0LjM4NzQgNDAuMTU3IDI3Ljg5NyA1My41MDggMTkuMDUgMTAuODE5IDQ2Ljg3OCAxMi4yMTkgODEuOTI2IDE0LjQ2MSAzMy43MDMgMi4xNTU5IDYxLjUxMi0xLjQzMDQgNzYuOTIxIDYuMTQxMSAxMS41ODUgNS42OTI3IDguNTgxNSAxNy45MzMgMTQuMjk1IDI5LjM2MSA1LjY0MDQgMTEuMjgxIDMxLjUwMyAxMS4xNTYgNDEuODA0IDQzLjQ1NSA3LjYwNTkgMjMuODQ3IDMuMDg1OSA0NC4xNTcgNi43MDc2IDY1Ljg4NyIgc3Ryb2tlPSIjMzY0ZTU5IiBzdHJva2Utd2lkdGg9IjMiLz4KICAgPHBhdGggZD0ibTQzLjI3OCA1MTcuOTVzMjMwLjg1LTMuNjM4IDI1MC4wMS0zLjY1ODdjNy40ODIyLThlLTMgOC42MTk1IDUuMTUxOSAxNC4wMjEgMTEuNDU5IDI0LjU5NiAyOC43MTkgOTMuOTEgMTEyLjk0IDkzLjkxIDExMi45NCIgc3Ryb2tlPSIjMzM2IiBzdHJva2Utd2lkdGg9IjFweCIvPgogICA8cGF0aCBkPSJtMzUuOTYxIDU3Ny43czE2NS41Mi0xLjY4NDUgMjQ4Ljc4LTEuNjg0NWM0Ljk0NzUgMCA3LjcyOTktMi44ODMzIDEwLjUzOC01LjcyOTggOS42NjExLTkuNzk0MiAyNS42MzItMjguNTkgMjUuNjMyLTI4LjU5IiBzdHJva2U9IiMzMzYiIHN0cm9rZS13aWR0aD0iMXB4Ii8+CiAgPC9nPgogIDxwYXRoIGQ9Im0zOC40IDY0MS43MyAzOTMuMzEtNC4yNjg0IiBjb2xvcj0iIzAwMDAwMCIgZmlsbD0iIzMzNiIgc3Ryb2tlPSIjMzM2IiBzdHJva2Utd2lkdGg9IjFweCIvPgogIDxwYXRoIGQ9Im0zOS4wMDkgNzA0LjU0IDQ4NC4xNi02LjcwNzYiIGNvbG9yPSIjMDAwMDAwIiBmaWxsPSIjMzM2IiBzdHJva2U9IiMzMzYiIHN0cm9rZS13aWR0aD0iMXB4Ii8+CiAgPGcgZmlsbD0ibm9uZSIgc3Ryb2tlPSIjMzM2Ij4KICAgPGcgc3Ryb2tlLXdpZHRoPSIxcHgiPgogICAgPHBhdGggZD0ibTMwMy45NiA2ODIuNTkgMTQ2LjggMS44MjkzYzEwLjUzNCAwLjEzMTI3IDE0LjM0NC0yLjYzNzQgMjUuNDg3LTYuMzcyOCAxMC40MTItMy40OTAzIDMxLjQyNC0yLjY5OSA0MS4zODUtMi43NzM4bDQwNS41Ni0zLjA0ODkiIGNvbG9yPSIjMDAwMDAwIi8+CiAgICA8cGF0aCBkPSJtNDI2LjIyIDMxNC44OWMyLjA2NzUgOS4wNTI3IDEuODQxOCA1MS43MjggNi41MDc5IDc0LjgzNSAxLjY3NDggOC4yOTM0IDguNjc1MSAxNC4wNjYgMTAuMDU1IDE0Ljg1OSA0LjkwMTUgMi44MTQ2IDEwLjgxNSA4LjE0OTggMTMuMDQ2IDE2LjA4OCA2Ljc1NzggMjQuMDQ2IDAuODc5NzIgNjguNDUyIDAuODc5NzIgMTEwLjY5IDAgNi4wOTc4IDEuNjYwMSAzMC4xNDctMi4xNTU5IDMzLjk2My0yLjU0MDggMi41NDA4LTAuMjgxNjMgMTIuOTkxLTMuNDM2OCAxNi4xNDRsLTkuODQ5NCA5Ljg0MzFjLTEwLjM2NyAxMC4zNi0xMS41OSA2LjUyNjEtMTcuNzM4IDE4LjgyMy0zLjU2NzcgNy4xMzU0IDUuNDAyNCAyMC42NzIgNy4zNTQzIDI0LjU3NiAxLjkzMjEgMy44NjQzLTEuODQyMiA0Ljc3NzctMS43OTI0IDcuNDQ2MyAwLjI1Mjg2IDEzLjU0NSAyLjI5NzUgMzczLjkzIDIuMjk3NSAzNzMuOTMiIGNvbG9yPSIjMDAwMDAwIi8+CiAgICA8cGF0aCBkPSJtMzY1LjI0IDUxOS43OCA0LjExNiA1MDIuMTUiIGNvbG9yPSIjMDAwMDAwIi8+CiAgICA8cGF0aCBkPSJtMTE2LjUzIDUwNC4xOSAzLjg4MDYgMzEwLjk2IiBjb2xvcj0iIzAwMDAwMCIvPgogICAgPHBhdGggZD0ibTMxNy42OCA1NzYuNDkgMTMwLjE5IDEuNTI0NGM0LjUxMDggMy4yNDE3IDIwLjM0NSA3Ljk2ODUgMjcuNzQ1IDQuMjY4NCAzLjE1NTUtMS41Nzc3IDkuNDE5LTUuMzg4MiAxNC4wMjUtMy45NjM2IDQuMjY3IDEuMzE5OCA2LjAxNjkgMy4xMTYzIDEwLjM2NiAzLjA0ODkgMTAuMzA0LTAuMTU5NzUgMjAuMjEyIDAuMzg3NDEgMzAuNDg5IDAuMzA0ODkgMTc3Ljg5LTEuNDI4MyAzNTYuNTktMi4xMzI1IDUzNC43Ny0zLjA0ODkiIGNvbG9yPSIjMDAwMDAwIi8+CiAgICA8cGF0aCBkPSJtNDc1LjMxIDU4Mi44OWMtMy40NDQyIDExLjM1MS0yLjEwMzQgMTIuNDM0IDMuNjU4NiAyMS4wMzcgMy43OTQ0IDUuNjY1NiA1MC44NjMgMTMuMDM4IDQxLjQ2NSAyNy4xMzUtMTAuNTM3IDE1LjgwNS0yMi44OTctNS40Nzc3LTMzLjg0My0xLjgyOTMtNS40NTI0IDEuODE3NC03LjM0OSA1LjQ1NjMtMy42NTg3IDkuMTQ2NiAyLjgwNjggMi44MDY4IDQuMDQ4IDEuODA0IDYuNTIwMyA1LjEwMDQiIGNvbG9yPSIjMDAwMDAwIi8+CiAgICA8cGF0aCBkPSJtNDMyLjAxIDYzNi44NWM4LjMxOSAxMy4xMSAxOC44NDYgMTQuNjM1IDM1LjY3MiAxNC42MzUgMi45Mzg2IDAgNy44Ny0wLjkzMzcxIDEwLjY3MSAwIDExLjM1OSAzLjc4NjQgMjcuMTk0IDEwLjI3NiAzNi4yMDIgMjEuMTI5IDguMjggOS45NzY2IDEwLjI1MyAyMy44ODMgNy43MDIgMzcuMTA0LTYuMTY5OSAzMS45OC0xNi43MTQgNTYuOTg5LTE5LjA0NCA4Ni41NjktMS4zNDggMTcuMTE5IDQuNTA5NiAyMi41MzUgMTEuMDcxIDMzLjkyOSAxMC42NyAxOC41MjcgOC43MjQ1IDE0LjIgOC41NzE0IDM0LjI4Ni0wLjEzOTYzIDE4LjMxOSAwIDYwLjI2NCAwIDgwLjcxNCIgY29sb3I9IiMwMDAwMDAiLz4KICAgIDxwYXRoIGQ9Im01MjguNTEgNjU4Ljk2Yy0xMC42ODEgMC45MDQ1NC03LjEwOC01LjYwMjYtMTAuODI0LTguMDc5Ni00Ljc4NDUtMy4xODk3LTEyLjIyNy0xLjI1MS0xNi43NjktNS43OTI5LTAuNjY2MTItMC42NjYxMi04LjgwOTctNC4xMDg4LTEwLjE3NC0yLjc0NC04LjM2NDYgOC4zNjQ2LTMuMDQ4OSAyMC41NTItMy4wNDg5IDMzLjUzOGwzLjAyMiAzMzkuNyIgY29sb3I9IiMwMDAwMDAiLz4KICAgIDxwYXRoIGQ9Im01MTcuOTkgNjUxLjAzYy0wLjIyMTcxLTIuNzAxOCAxLjkwMzUtNS41NjIxIDMuMzUzOC03LjAxMjQgMS43OTk0LTEuNzk5NCA2LjkyMjkgMS4wMDQyIDguODQxOC0wLjkxNDY2IDAuMjg3NjUtMC4yODc2NiAwLjg0MzI5LTExLjE2NCAwLjIyODY2LTEzLjU2OC0yLjA2NDgtOC4wNzQyLTIuMDU4LTI4LjY1Ny0yLjA1OC0zOC43MjF2LTczLjE3MyIgY29sb3I9IiMwMDAwMDAiLz4KICAgIDxwYXRoIGQ9Im01MjguNjYgNjc1LjQyLTAuNDU3MzMtMzEuNTU2IiBjb2xvcj0iIzAwMDAwMCIvPgogICAgPHBhdGggZD0ibTc2Ni4zMiA1NzkuNjQgMC40MzExOCAxMy43OThjMy4xMzY0IDQuNjY5MiAzLjAxODIgOS42MDA3IDMuMDE4MiAxNi4zODV2MTU3LjM4IiBjb2xvcj0iIzAwMDAwMCIvPgogICAgPHBhdGggZD0ibTExMjIuOSA3NjUuOTFjLTIwMi4zMSA0LjY5MDUtNDAzLjc0LTEuMTEzOC02MDUuOTUgMy4zNTM5LTEwLjg2NCAwLjI0MDAyLTMuMzYxNS04LjU4NjMtMjguNTM3LTguNTg2MyIgY29sb3I9IiMwMDAwMDAiLz4KICAgIDxwYXRoIGQ9Im04NjAuMDEgNzM3LjA3cy05Ny40NDggMC44NTgwNi0xNDcuNTcgMC44NTgwNmMtNS4yNjg2IDAtNC41MTU1LTguMzI5OS03LjMwMDktOC4zMjk5LTMuOTc0NCAwLTguNjI5MiAwLjAyMDEtMTAuNTA5IDAuMDM1OS0yLjMzNDggMC4wMTk3LTEuODEwOSA4LjM2Ni00LjE0NTggOC4zNjY5LTQ2LjE2OSAwLjAxODgtMTY3LjQxLTEuMzA4LTE3NS4wNS0xLjMwOC00LjQyOTYgMC04LjU3NjMtNi40Mzk3LTEzLjEzMi02LjQzOTdoLTE0LjM5NSIgY29sb3I9IiMwMDAwMDAiLz4KICAgIDxwYXRoIGQ9Im02NzUuMDEgODMxLjE3LTAuNjA5NzgtNTIxLjc3IiBjb2xvcj0iIzAwMDAwMCIvPgogICAgPHBhdGggZD0ibTc5OS40IDMxMy4wNiAxLjIxOTYgNDk1Ljg3IiBjb2xvcj0iIzAwMDAwMCIvPgogICAgPHBhdGggZD0ibTczNi41OSAzMTIuNDUtMS4yMTk2IDcxNi40OSIgY29sb3I9IiMwMDAwMDAiLz4KICAgIDxwYXRoIGQ9Im01MzAuMDMgNjQzLjQ2IDM5Mi4zNy0zLjAxODIiIGNvbG9yPSIjMDAwMDAwIi8+CiAgICA8cGF0aCBkPSJtODU5LjQ1IDMxNC45IDEuMjkzNSA1MDcuOTgiIGNvbG9yPSIjMDAwMDAwIi8+CiAgIDwvZz4KICAgPHBhdGggZD0ibTkyMS41NCAzMTAuNTkgMS43MjQ3IDUzMS43NSIgY29sb3I9IiMwMDAwMDAiIHN0cm9rZS13aWR0aD0iMXB4Ii8+CiAgIDxnIHN0cm9rZS13aWR0aD0iMXB4Ij4KICAgIDxwYXRoIGQ9Im03MzYuMjkgNDUzLjMxIDE4NS42OC0wLjMwNDg5IiBjb2xvcj0iIzAwMDAwMCIvPgogICAgPHBhdGggZD0ibTEwNjAuOCA1MTQuOTdzLTM2My4yOC01LjYyNjItNTQ0LjY1IDIuNTIxOGMtNC4xNzc4IDAuMTg3NjktMTIuNSAxLjA2NzEtMTIuNSAxLjA2NzEtMS41NzEgMC4xMzQxLTIuMDAwOS0yLjMyNS0yLjU5MTYtMy41MDYyLTAuMDk2Ny0wLjE5MzQzLTcuMDYwOC0xLjkzMzQtNy42MjIyLTEuMzcyLTIuODkzMSAyLjg5MzEtNy42MzE3IDQuMjQ4Ny0xMi4xOTYgNC4xMTZsLTExMi4wNS0zLjI1NzgiIGNvbG9yPSIjMDAwMDAwIi8+CiAgICA8cGF0aCBkPSJtMzk5LjgyIDQ3OS42MSAxMS42NDIgNS42MDUzYzIuOTg0MSAxLjQzNjggNi41Mjg4LTAuNDc3MTIgOS45MTcxLTAuNDMxMThsMTI3LjIgMS43MjQ3IiBjb2xvcj0iIzAwMDAwMCIvPgogICAgPHBhdGggZD0ibTUxOS4yNSA1MTcuMTItMC40MzExOS0yMDguNjkiIGNvbG9yPSIjMDAwMDAwIi8+CiAgICA8cGF0aCBkPSJtNDMyLjkzIDM4OS43MWMxMS4wNDUgMCAzNS41MzMgMC42MTkyNyA0Mi41OC0xLjAwNCA4LjQwNTItMS45MzYyIDcuMDY2LTYuOTUzOCAxNC4xOTctNi45NTM4IDcuODA5NSAwIDYuNTQyOSA4LjA2MjQgMjAuMTQyIDguMDYyNCAxMy45OTEgMCA0NC45NzcgMC4zNzg4NiA2My45NCAwLjM3ODg2IDEyLjA4NCAwIDgyLjAwMyAwLjMwNDg5IDkzLjYwMSAwLjMwNDg5IDguNzYwNSAwIDEzLjE2LTIuMjg4MyAyMS4zNDItNy4wMTI0IDcuMTk1Mi00LjE1NDEgMi4wNTQ2LTkuNDkxNCAyMC40MjgtOC44NDE4IDIzLjE0NSAwLjgxODMzIDEyLjY0MyAxNC4wMjUgMzIuMzE4IDE0LjAyNWgxNTAuOTJjMTQuMzMyIDAtNC4xMTkxLTEzLjExIDI5LjI2OS0xMy40MTUiIGNvbG9yPSIjMDAwMDAwIi8+CiAgIDwvZz4KICA8L2c+CiAgPGcgZmlsbD0iIzAwMDAwMCIgZm9udC1mYW1pbHk9IlZlcmRhbmEiIGxldHRlci1zcGFjaW5nPSIwcHgiIHdvcmQtc3BhY2luZz0iMHB4Ij4KICAgPHRleHQgeD0iNTg4LjY3OTU3IiB5PSI3MzUuODA0NjMiIHN0eWxlPSJsaW5lLWhlaWdodDowJSIgeG1sOnNwYWNlPSJwcmVzZXJ2ZSI+PHRzcGFuIHg9IjU4OC42Nzk1NyIgeT0iNzM1LjgwNDYzIiBmb250LXNpemU9IjkuNjU4NHB4IiBzdHlsZT0ibGluZS1oZWlnaHQ6MS4yNSI+TGluY29sbjwvdHNwYW4+PC90ZXh0PgogICA8dGV4dCB4PSI2ODYuMzk4NSIgeT0iNzY1LjYyODQyIiBzdHlsZT0ibGluZS1oZWlnaHQ6MCUiIHhtbDpzcGFjZT0icHJlc2VydmUiPjx0c3BhbiB4PSI2ODYuMzk4NSIgeT0iNzY1LjYyODQyIiBmb250LXNpemU9IjkuNjU4NHB4IiBzdHlsZT0ibGluZS1oZWlnaHQ6MS4yNSI+SGFycnk8L3RzcGFuPjwvdGV4dD4KICAgPHRleHQgdHJhbnNmb3JtPSJyb3RhdGUoOTApIiB4PSI3MDkuODcxODMiIHk9Ii04MDIuMzc3MzgiIHN0eWxlPSJsaW5lLWhlaWdodDowJSIgeG1sOnNwYWNlPSJwcmVzZXJ2ZSI+PHRzcGFuIHg9IjcwOS44NzE4MyIgeT0iLTgwMi4zNzczOCIgZm9udC1zaXplPSI5LjY1ODRweCIgc3R5bGU9ImxpbmUtaGVpZ2h0OjEuMjUiPldvb2RsYXduPC90c3Bhbj48L3RleHQ+CiAgIDx0ZXh0IHRyYW5zZm9ybT0icm90YXRlKDkwKSIgeD0iNTYyLjExOTI2IiB5PSItNzcxLjk2ODE0IiBzdHlsZT0ibGluZS1oZWlnaHQ6MCUiIHhtbDpzcGFjZT0icHJlc2VydmUiPjx0c3BhbiB4PSI1NjIuMTE5MjYiIHk9Ii03NzEuOTY4MTQiIGZvbnQtc2l6ZT0iOS42NTg0cHgiIHN0eWxlPSJsaW5lLWhlaWdodDoxLjI1Ij5FZGdlbW9vcjwvdHNwYW4+PC90ZXh0PgogICA8dGV4dCB0cmFuc2Zvcm09InJvdGF0ZSg5MCkiIHg9IjU5OC4zMDQ4NyIgeT0iLTczOC4zNjY0NiIgc3R5bGU9ImxpbmUtaGVpZ2h0OjAlIiB4bWw6c3BhY2U9InByZXNlcnZlIj48dHNwYW4geD0iNTk4LjMwNDg3IiB5PSItNzM4LjM2NjQ2IiBmb250LXNpemU9IjkuNjU4NHB4IiBzdHlsZT0ibGluZS1oZWlnaHQ6MS4yNSI+T2xpdmVyPC90c3Bhbj48L3RleHQ+CiAgIDx0ZXh0IHRyYW5zZm9ybT0icm90YXRlKDkwKSIgeD0iNTkyLjEyMjg2IiB5PSItNjc3LjIwMzk4IiBzdHlsZT0ibGluZS1oZWlnaHQ6MCUiIHhtbDpzcGFjZT0icHJlc2VydmUiPjx0c3BhbiB4PSI1OTIuMTIyODYiIHk9Ii02NzcuMjAzOTgiIGZvbnQtc2l6ZT0iOS42NTg0cHgiIHN0eWxlPSJsaW5lLWhlaWdodDoxLjI1Ij5IaWxsc2lkZTwvdHNwYW4+PC90ZXh0PgogICA8dGV4dCB0cmFuc2Zvcm09InJvdGF0ZSg5MCkiIHg9IjU5Ny4zMjcwOSIgeT0iLTg2Mi42MTQwNyIgc3R5bGU9ImxpbmUtaGVpZ2h0OjAlIiB4bWw6c3BhY2U9InByZXNlcnZlIj48dHNwYW4geD0iNTk3LjMyNzA5IiB5PSItODYyLjYxNDA3IiBmb250LXNpemU9IjkuNjU4NHB4IiBzdHlsZT0ibGluZS1oZWlnaHQ6MS4yNSI+Um9jazwvdHNwYW4+PC90ZXh0PgogICA8dGV4dCB0cmFuc2Zvcm09InJvdGF0ZSg5MCkiIHg9IjU4Ny4zNzAxOCIgeT0iLTkyNi4xMzY2IiBzdHlsZT0ibGluZS1oZWlnaHQ6MCUiIHhtbDpzcGFjZT0icHJlc2VydmUiPjx0c3BhbiB4PSI1ODcuMzcwMTgiIHk9Ii05MjYuMTM2NiIgZm9udC1zaXplPSI5LjY1ODRweCIgc3R5bGU9ImxpbmUtaGVpZ2h0OjEuMjUiPldlYmI8L3RzcGFuPjwvdGV4dD4KICAgPHRleHQgeD0iODcxLjE2MTAxIiB5PSI2MzcuNTc1MiIgc3R5bGU9ImxpbmUtaGVpZ2h0OjAlIiB4bWw6c3BhY2U9InByZXNlcnZlIj48dHNwYW4geD0iODcxLjE2MTAxIiB5PSI2MzcuNTc1MiIgZm9udC1zaXplPSI5LjY1ODRweCIgc3R5bGU9ImxpbmUtaGVpZ2h0OjEuMjUiPkNlbnRyYWw8L3RzcGFuPjwvdGV4dD4KICAgPHRleHQgeD0iODczLjgzMjI4IiB5PSI1NzcuMDMyNDciIHN0eWxlPSJsaW5lLWhlaWdodDowJSIgeG1sOnNwYWNlPSJwcmVzZXJ2ZSI+PHRzcGFuIHg9Ijg3My44MzIyOCIgeT0iNTc3LjAzMjQ3IiBmb250LXNpemU9IjkuNjU4NHB4IiBzdHlsZT0ibGluZS1oZWlnaHQ6MS4yNSI+MTN0aDwvdHNwYW4+PC90ZXh0PgogICA8dGV4dCB4PSI4NzUuOTY2NDkiIHk9IjUxMC4yNjE4MSIgc3R5bGU9ImxpbmUtaGVpZ2h0OjAlIiB4bWw6c3BhY2U9InByZXNlcnZlIj48dHNwYW4geD0iODc1Ljk2NjQ5IiB5PSI1MTAuMjYxODEiIGZvbnQtc2l6ZT0iOS42NTg0cHgiIHN0eWxlPSJsaW5lLWhlaWdodDoxLjI1Ij4yMXN0PC90c3Bhbj48L3RleHQ+CiAgIDx0ZXh0IHg9Ijg4MS4zMTY1OSIgeT0iNDUwLjE5ODc2IiBzdHlsZT0ibGluZS1oZWlnaHQ6MCUiIHhtbDpzcGFjZT0icHJlc2VydmUiPjx0c3BhbiB4PSI4ODEuMzE2NTkiIHk9IjQ1MC4xOTg3NiIgZm9udC1zaXplPSI5LjY1ODRweCIgc3R5bGU9ImxpbmUtaGVpZ2h0OjEuMjUiPjI5dGg8L3RzcGFuPjwvdGV4dD4KICAgPHRleHQgeD0iNjE1Ljc5MjQ4IiB5PSIzODcuNzQ3MTYiIHN0eWxlPSJsaW5lLWhlaWdodDowJSIgeG1sOnNwYWNlPSJwcmVzZXJ2ZSI+PHRzcGFuIHg9IjYxNS43OTI0OCIgeT0iMzg3Ljc0NzE2IiBmb250LXNpemU9IjkuNjU4NHB4IiBzdHlsZT0ibGluZS1oZWlnaHQ6MS4yNSI+Mzd0aDwvdHNwYW4+PC90ZXh0PgogICA8dGV4dCB4PSI0ODQuNjkwMzciIHk9IjQ4MS42NTI4NiIgc3R5bGU9ImxpbmUtaGVpZ2h0OjAlIiB4bWw6c3BhY2U9InByZXNlcnZlIj48dHNwYW4geD0iNDg0LjY5MDM3IiB5PSI0ODEuNjUyODYiIGZvbnQtc2l6ZT0iOS42NTg0cHgiIHN0eWxlPSJsaW5lLWhlaWdodDoxLjI1Ij4yNXRoPC90c3Bhbj48L3RleHQ+CiAgIDx0ZXh0IHg9IjU2My4wNDY3NSIgeT0iNTEzLjM2MTMzIiBzdHlsZT0ibGluZS1oZWlnaHQ6MCUiIHhtbDpzcGFjZT0icHJlc2VydmUiPjx0c3BhbiB4PSI1NjMuMDQ2NzUiIHk9IjUxMy4zNjEzMyIgZm9udC1zaXplPSI5LjY1ODRweCIgc3R5bGU9ImxpbmUtaGVpZ2h0OjEuMjUiPjIxc3Q8L3RzcGFuPjwvdGV4dD4KICAgPHRleHQgeD0iNTY1Ljk3MTUiIHk9IjU3Ny44OTQ4NCIgc3R5bGU9ImxpbmUtaGVpZ2h0OjAlIiB4bWw6c3BhY2U9InByZXNlcnZlIj48dHNwYW4geD0iNTY1Ljk3MTUiIHk9IjU3Ny44OTQ4NCIgZm9udC1zaXplPSI5LjY1ODRweCIgc3R5bGU9ImxpbmUtaGVpZ2h0OjEuMjUiPjEzdGg8L3RzcGFuPjwvdGV4dD4KICAgPHRleHQgdHJhbnNmb3JtPSJyb3RhdGUoOTApIiB4PSI0MzMuNTgwNzUiIHk9Ii00NjAuNzMzMTIiIHN0eWxlPSJsaW5lLWhlaWdodDowJSIgeG1sOnNwYWNlPSJwcmVzZXJ2ZSI+PHRzcGFuIHg9IjQzMy41ODA3NSIgeT0iLTQ2MC43MzMxMiIgZm9udC1zaXplPSI5LjY1ODRweCIgc3R5bGU9ImxpbmUtaGVpZ2h0OjEuMjUiPkFtaWRvbjwvdHNwYW4+PC90ZXh0PgogICA8dGV4dCB0cmFuc2Zvcm09InJvdGF0ZSg5MCkiIHg9IjQwNS41MzA5OCIgeT0iLTUyMy41NDAxNiIgc3R5bGU9ImxpbmUtaGVpZ2h0OjAlIiB4bWw6c3BhY2U9InByZXNlcnZlIj48dHNwYW4geD0iNDA1LjUzMDk4IiB5PSItNTIzLjU0MDE2IiBmb250LXNpemU9IjkuNjU4NHB4IiBzdHlsZT0ibGluZS1oZWlnaHQ6MS4yNSI+QXJrYW5zYXM8L3RzcGFuPjwvdGV4dD4KICAgPHRleHQgdHJhbnNmb3JtPSJyb3RhdGUoOTApIiB4PSI3NDUuNDg0NjIiIHk9Ii0zNzIuNTg1OTQiIHN0eWxlPSJsaW5lLWhlaWdodDowJSIgeG1sOnNwYWNlPSJwcmVzZXJ2ZSI+PHRzcGFuIHg9Ijc0NS40ODQ2MiIgeT0iLTM3Mi41ODU5NCIgZm9udC1zaXplPSI5LjY1ODRweCIgc3R5bGU9ImxpbmUtaGVpZ2h0OjEuMjUiPldlc3Q8L3RzcGFuPjwvdGV4dD4KICAgPHRleHQgdHJhbnNmb3JtPSJyb3RhdGUoOTApIiB4PSI1OTYuNzI4MzMiIHk9Ii01MzEuMjU5MjgiIHN0eWxlPSJsaW5lLWhlaWdodDowJSIgeG1sOnNwYWNlPSJwcmVzZXJ2ZSI+PHRzcGFuIHg9IjU5Ni43MjgzMyIgeT0iLTUzMS4yNTkyOCIgZm9udC1zaXplPSI5LjY1ODRweCIgc3R5bGU9ImxpbmUtaGVpZ2h0OjEuMjUiPldhY288L3RzcGFuPjwvdGV4dD4KICAgPHRleHQgdHJhbnNmb3JtPSJyb3RhdGUoOTApIiB4PSI1OTUuNDM0ODEiIHk9Ii0xMjIuNTAyOTUiIHN0eWxlPSJsaW5lLWhlaWdodDowJSIgeG1sOnNwYWNlPSJwcmVzZXJ2ZSI+PHRzcGFuIHg9IjU5NS40MzQ4MSIgeT0iLTEyMi41MDI5NSIgZm9udC1zaXplPSI5LjY1ODRweCIgc3R5bGU9ImxpbmUtaGVpZ2h0OjEuMjUiPk1hemllPC90c3Bhbj48L3RleHQ+CiAgIDx0ZXh0IHRyYW5zZm9ybT0icm90YXRlKDQ1KSIgeD0iNjk1Ljc3Mjk1IiB5PSIxNjIuMDY4NzciIHN0eWxlPSJsaW5lLWhlaWdodDowJSIgeG1sOnNwYWNlPSJwcmVzZXJ2ZSI+PHRzcGFuIHg9IjY5NS43NzI5NSIgeT0iMTYyLjA2ODc3IiBmb250LXNpemU9IjkuNjU4NHB4IiBzdHlsZT0ibGluZS1oZWlnaHQ6MS4yNSI+Wm9vPC90c3Bhbj48L3RleHQ+CiAgIDx0ZXh0IHg9IjI0MC41ODk5NyIgeT0iNTc0LjQ0NTQzIiBzdHlsZT0ibGluZS1oZWlnaHQ6MCUiIHhtbDpzcGFjZT0icHJlc2VydmUiPjx0c3BhbiB4PSIyNDAuNTg5OTciIHk9IjU3NC40NDU0MyIgZm9udC1zaXplPSI5LjY1ODRweCIgc3R5bGU9ImxpbmUtaGVpZ2h0OjEuMjUiPjEzdGg8L3RzcGFuPjwvdGV4dD4KICAgPHRleHQgeD0iMjA2LjAzMTc1IiB5PSI1MTEuNjM2NjMiIHN0eWxlPSJsaW5lLWhlaWdodDowJSIgeG1sOnNwYWNlPSJwcmVzZXJ2ZSI+PHRzcGFuIHg9IjIwNi4wMzE3NSIgeT0iNTExLjYzNjYzIiBmb250LXNpemU9IjkuNjU4NHB4IiBzdHlsZT0ibGluZS1oZWlnaHQ6MS4yNSI+MjFzdDwvdHNwYW4+PC90ZXh0PgogICA8dGV4dCB0cmFuc2Zvcm09InJvdGF0ZSg5MCkiIHg9IjYyMC40NDMxMiIgeT0iLTUwNi42ODIxOSIgc3R5bGU9ImxpbmUtaGVpZ2h0OjAlIiB4bWw6c3BhY2U9InByZXNlcnZlIj48dHNwYW4geD0iNjIwLjQ0MzEyIiB5PSItNTA2LjY4MjE5IiBmb250LXNpemU9IjkuNjU4NHB4IiBzdHlsZT0ibGluZS1oZWlnaHQ6MS4yNSI+TmltczwvdHNwYW4+PC90ZXh0PgogICA8dGV4dCB4PSIzNzAuMjE2ODYiIHk9IjY5OC44NDAwOSIgc3R5bGU9ImxpbmUtaGVpZ2h0OjAlIiB4bWw6c3BhY2U9InByZXNlcnZlIj48dHNwYW4geD0iMzcwLjIxNjg2IiB5PSI2OTguODQwMDkiIGZvbnQtc2l6ZT0iOS42NTg0cHgiIHN0eWxlPSJsaW5lLWhlaWdodDoxLjI1Ij5NYXBsZTwvdHNwYW4+PC90ZXh0PgogICA8dGV4dCB4PSIzODQuMDg0MiIgeT0iNjgwLjg1MTM4IiBzdHlsZT0ibGluZS1oZWlnaHQ6MCUiIHhtbDpzcGFjZT0icHJlc2VydmUiPjx0c3BhbiB4PSIzODQuMDg0MiIgeT0iNjgwLjg1MTM4IiBmb250LXNpemU9IjkuNjU4NHB4IiBzdHlsZT0ibGluZS1oZWlnaHQ6MS4yNSI+RG91Z2xhczwvdHNwYW4+PC90ZXh0PgogIDwvZz4KICA8cGF0aCBkPSJtMzY3LjkxIDEwMTBoMjYzLjAyIiBjb2xvcj0iIzAwMDAwMCIgZmlsbD0ibm9uZSIgc3Ryb2tlPSIjMzM2IiBzdHJva2Utd2lkdGg9IjFweCIvPgogIDxnIGZpbGw9IiMwMDAwMDAiIGZvbnQtZmFtaWx5PSJWZXJkYW5hIiBsZXR0ZXItc3BhY2luZz0iMHB4IiB3b3JkLXNwYWNpbmc9IjBweCI+CiAgIDx0ZXh0IHRyYW5zZm9ybT0icm90YXRlKDkwKSIgeD0iNzM2LjI2NzQ2IiB5PSItNDMzLjEzNzc2IiBzdHlsZT0ibGluZS1oZWlnaHQ6MCUiIHhtbDpzcGFjZT0icHJlc2VydmUiPjx0c3BhbiB4PSI3MzYuMjY3NDYiIHk9Ii00MzMuMTM3NzYiIGZvbnQtc2l6ZT0iOS42NTg0cHgiIHN0eWxlPSJsaW5lLWhlaWdodDoxLjI1Ij5NZXJpZGlhbjwvdHNwYW4+PC90ZXh0PgogICA8dGV4dCB4PSI1NzIuODMyMTUiIHk9IjY0MC4yMDUyNiIgc3R5bGU9ImxpbmUtaGVpZ2h0OjAlIiB4bWw6c3BhY2U9InByZXNlcnZlIj48dHNwYW4geD0iNTcyLjgzMjE1IiB5PSI2NDAuMjA1MjYiIGZvbnQtc2l6ZT0iOS42NTg0cHgiIHN0eWxlPSJsaW5lLWhlaWdodDoxLjI1Ij5DZW50cmFsPC90c3Bhbj48L3RleHQ+CiAgIDx0ZXh0IHg9IjU3NS4wODk2NiIgeT0iNjcwLjkwMzUiIHN0eWxlPSJsaW5lLWhlaWdodDowJSIgeG1sOnNwYWNlPSJwcmVzZXJ2ZSI+PHRzcGFuIHg9IjU3NS4wODk2NiIgeT0iNjcwLjkwMzUiIGZvbnQtc2l6ZT0iOS42NTg0cHgiIHN0eWxlPSJsaW5lLWhlaWdodDoxLjI1Ij5Eb3VnbGFzPC90c3Bhbj48L3RleHQ+CiAgIDx0ZXh0IHg9IjQ5OS40ODk2MiIgeT0iMTAwOC42MDY5IiBzdHlsZT0ibGluZS1oZWlnaHQ6MCUiIHhtbDpzcGFjZT0icHJlc2VydmUiPjx0c3BhbiB4PSI0OTkuNDg5NjIiIHk9IjEwMDguNjA2OSIgZm9udC1zaXplPSI5LjY1ODRweCIgc3R5bGU9ImxpbmUtaGVpZ2h0OjEuMjUiPjQ3dGg8L3RzcGFuPjwvdGV4dD4KICAgPHRleHQgeD0iMjE2LjY0NTQzIiB5PSI3MjUuOTgyOTciIHN0eWxlPSJsaW5lLWhlaWdodDowJSIgeG1sOnNwYWNlPSJwcmVzZXJ2ZSI+PHRzcGFuIHg9IjIxNi42NDU0MyIgeT0iNzI1Ljk4Mjk3IiBmb250LXNpemU9IjkuNjU4NHB4IiBzdHlsZT0ibGluZS1oZWlnaHQ6MS4yNSI+S2VsbG9nZzwvdHNwYW4+PC90ZXh0PgogICA8dGV4dCB0cmFuc2Zvcm09InJvdGF0ZSg5MCkiIHg9Ijc3NC44NzU2MSIgeT0iLTUwOC4xODk3MyIgc3R5bGU9ImxpbmUtaGVpZ2h0OjAlIiB4bWw6c3BhY2U9InByZXNlcnZlIj48dHNwYW4geD0iNzc0Ljg3NTYxIiB5PSItNTA4LjE4OTczIiBmb250LXNpemU9IjkuNjU4NHB4IiBzdHlsZT0ibGluZS1oZWlnaHQ6MS4yNSI+TWNDbGVhbjwvdHNwYW4+PC90ZXh0PgogIDwvZz4KICA8cGF0aCB0cmFuc2Zvcm09InRyYW5zbGF0ZSgwIDI4Ny4zNikiIGQ9Im0zNjQuMTYgNjU4LjQzIDI5OS41MS0xLjAxMDJjNi40OTg3LTAuMDIxOSA2Ljk3NzIgOS4yNTQxIDE2LjU5NiA5LjM5MjUgMTIuMDU0IDAuMTczMzkgMjkuMTExLTAuNTM1NzIgNTQuMTE0LTAuMzAxMSIgY29sb3I9IiMwMDAwMDAiIGZpbGw9Im5vbmUiIHN0cm9rZT0iIzMzNiIgc3Ryb2tlLXdpZHRoPSIxcHgiLz4KICA8dGV4dCB4PSIzNzMuOTkzMDQiIHk9Ijk0NC4zNTc1NCIgZmlsbD0iIzAwMDAwMCIgZm9udC1mYW1pbHk9IlZlcmRhbmEiIGxldHRlci1zcGFjaW5nPSIwcHgiIHdvcmQtc3BhY2luZz0iMHB4IiBzdHlsZT0ibGluZS1oZWlnaHQ6MCUiIHhtbDpzcGFjZT0icHJlc2VydmUiPjx0c3BhbiB4PSIzNzMuOTkzMDQiIHk9Ijk0NC4zNTc1NCIgZm9udC1zaXplPSI5LjY1ODRweCIgc3R5bGU9ImxpbmUtaGVpZ2h0OjEuMjUiPk1hY0FydGh1cjwvdHNwYW4+PC90ZXh0PgogIDx0ZXh0IHRyYW5zZm9ybT0icm90YXRlKDkwKSIgeD0iNzgwLjg0NjA3IiB5PSItNDkwLjI0NTk3IiBmaWxsPSIjMDAwMDAwIiBmb250LWZhbWlseT0iVmVyZGFuYSIgbGV0dGVyLXNwYWNpbmc9IjBweCIgd29yZC1zcGFjaW5nPSIwcHgiIHN0eWxlPSJsaW5lLWhlaWdodDowJSIgeG1sOnNwYWNlPSJwcmVzZXJ2ZSI+PHRzcGFuIHg9Ijc4MC44NDYwNyIgeT0iLTQ5MC4yNDU5NyIgZm9udC1zaXplPSI5LjY1ODRweCIgc3R5bGU9ImxpbmUtaGVpZ2h0OjEuMjUiPlNlbmVjYTwvdHNwYW4+PC90ZXh0PgogIDxwYXRoIHRyYW5zZm9ybT0idHJhbnNsYXRlKDAgMjg3LjM2KSIgZD0ibTM2Ny43IDUzNy4yMSAxNDEuMjgtMS4wMTAyYzYuNDktMC4wNDY0IDEyLjc4MSA3LjIzNTQgMTkuMTkzIDcuMzIzNiA1NS45MjQgMC43Njg5IDE1OC42OS0wLjE3MzMzIDIzNi41MS0xLjAxMDIgNy44Mzk2LTAuMDg0MyAyMi42MzEtMTkuODU0IDMwLjMwNS0yMC40NTYgMjIuMjY2LTEuMzUxOCA0NS4xNzktMC41MDUwNyA2Ny42OC0wLjUwNTA3IDE2LjE0Ny0wLjYzMjQxIDMuNjEwMiAyMC43MDggMjYuNzY5IDIwLjcwOGwyNDMuNDUtMS4wMTAyIiBjb2xvcj0iIzAwMDAwMCIgZmlsbD0ibm9uZSIgc3Ryb2tlPSIjMzM2IiBzdHJva2Utd2lkdGg9IjFweCIvPgogIDx0ZXh0IHg9IjY4NS4yMDgxMyIgeT0iODI3LjUzMDgyIiBmaWxsPSIjMDAwMDAwIiBmb250LWZhbWlseT0iVmVyZGFuYSIgbGV0dGVyLXNwYWNpbmc9IjBweCIgd29yZC1zcGFjaW5nPSIwcHgiIHN0eWxlPSJsaW5lLWhlaWdodDowJSIgeG1sOnNwYWNlPSJwcmVzZXJ2ZSI+PHRzcGFuIHg9IjY4NS4yMDgxMyIgeT0iODI3LjUzMDgyIiBmb250LXNpemU9IjkuNjU4NHB4IiBzdHlsZT0ibGluZS1oZWlnaHQ6MS4yNSI+UGF3bmVlPC90c3Bhbj48L3RleHQ+CiAgPHBhdGggdHJhbnNmb3JtPSJ0cmFuc2xhdGUoMCAyODcuMzYpIiBkPSJtNTU0LjI5IDcyMS40My00LjI4NTctMTc4LjIxLTIuODU3MS00NDAuNzEtMC4zNTcxNC03OS4yODYiIGNvbG9yPSIjMDAwMDAwIiBmaWxsPSJub25lIiBzdHJva2U9IiMzMzYiIHN0cm9rZS13aWR0aD0iMXB4Ii8+CiAgPHRleHQgdHJhbnNmb3JtPSJyb3RhdGUoOTApIiB4PSI1MjkuNjI1MzEiIHk9Ii01NTAuODQ3NzgiIGZpbGw9IiMwMDAwMDAiIGZvbnQtZmFtaWx5PSJWZXJkYW5hIiBsZXR0ZXItc3BhY2luZz0iMHB4IiB3b3JkLXNwYWNpbmc9IjBweCIgc3R5bGU9ImxpbmUtaGVpZ2h0OjAlIiB4bWw6c3BhY2U9InByZXNlcnZlIj48dHNwYW4geD0iNTI5LjYyNTMxIiB5PSItNTUwLjg0Nzc4IiBmb250LXNpemU9IjkuNjU4NHB4IiBzdHlsZT0ibGluZS1oZWlnaHQ6MS4yNSI+QnJvYWR3YXk8L3RzcGFuPjwvdGV4dD4KIDwvZz4KPC9zdmc+Cg==", "public": true @@ -98,5 +86,20 @@ "data": "iVBORw0KGgoAAAANSUhEUgAAAB4AAAB/CAYAAAD4mHJdAAAABHNCSVQICAgIfAhkiAAAAAlwSFlzAAACWAAAAlgB7MGOJQAAABl0RVh0U29mdHdhcmUAd3d3Lmlua3NjYXBlLm9yZ5vuPBoAAAxNSURBVGiB7Zp7kFTllcB/5/a93dMz3T0PemYIDgoCPhZ5iaD4wNkFjQjRRMlLTNbSlKlyzZpobSVbFRPUbNVWSRCWuKvlxqybtbIrukp4SATZCAgospEBgeElj4EZ5t3d0+++37d/9O2ZnqEHQZzZSlXfqlMz/c253+875zvfOefeHuH8L6u83P+AwH0lJZ4pbrenEiCVSnYmEsndGl4NhSKvAJkLmPPcV0VFxZjKivKPv77wXr274WN9uvm0PnHihD5y9IhuPNioN216Vy+Yf6eurAj8b2Vl5aXnM6d8loLf7w9apvHhyy//29jZ9fW0t7fpdWtWN7Wdao4qpaiqDpbdXF9fV1paKpu3bGbxk08eSWXU9ZFIpOPirC33v7xs+TIdiUT0Pz239NjeaTOTHXXjdb4cuP6W5DOLFx/7aNdH+oknfqQryv0vXZTFfr8/GKyqaN7XeMhc//ba6NSfPFXqS6fESJ29jdGAX69+9KHY9OnTyxbec08mHInWhsPhzsHmNs4FNgxdf+NNN5sAh3/7n40dCxeKedUsOr6x8CzdsnBEQu9sPABwzTWTTMNQ9eec+1x/FDEuGTduHABXtreOKutJYyiFqq4tqD+5O3wJQF1dHSij7nODtdZuj9cLgMfGOpcuQInSFoDldqNFez43eCivIrgILoKL4CK4CC6Ci+AiuAgugovgIrgILoKL4CK4CC6Ci+A/B7B5vor6Mz4PNnbRYAAtoCQLUMMFVobuBWOALWdjVIGxiwbbZC3WkrXWLqAzJBZrR5T0LWTgdSHfdF1YcIlG57t8oM5nfov1OcCKPmDW1Rfi2IsA5yI5F9WFXF0o0i8arARwggsBu4BbhwaM6g0ujXY+9b+GLqrzLR5E5wsH2ziB5QRXoW8lCy3mosH553iwlDlEe9znai2DpMyhAJ+PxUNTJMhZm51+WM9xvsWFXD2kx0nl9rjQ4oYC3C+4BoEMnasl39Vn6wxRdcqbXApXpwupWBcEVgLKGLw6DU1w5bkaCjcChcYuHozuLYtqEFfroXC1TZ67GcbjlEuZWjSIHr6ozjZ7/y/VSWOLdgJIF9zjQl3JFwDOXn1lsYDOULm6X+YaROcLB6s8+LC2tzqvoc+Wx0L2nT/6wlIm5y6LQ9bs5TLXsO5x7jG192lxuJq9bCOg0aIRGcYEkt9lCsPp6lxlMsBlFE4ghcYuGoxznHKFYNjKYq7Zy5XFYW32lMtCBGzbLlwWLwB83m/2NNC44R0iFaP503+8jO1UqHz5wiwW0aNzvysgdPJTQr/7dFD9fHD+vecN9vl8NaYpv546ZeqCBx98CMhGbPXEqZRfcTWmyySTjuO2TMora/B4Sji+832OnWoGYMGCBez88IMfHD50eExG6Yd6enraBjJcAwf8fv+Vbsv1Pz9f/NT1y1esQCnNPz6zeGuy6WBN+MRRrwp1YMR6MOIJMqEuOj49xNFd2zh5aD9SVpr44PCJXVOmXXvpHfPm4fP7rtz98Z/usSz3+lQq1e/fnvuFSHl5+VjTNLb96lfPj6yv/0t2bN/eufJnj+37Uql1c/1Xv8WM279CaZn/rJcBGoj1hNm+7k22rF5JcyK1edp3Hps0bfq0yj0Ne/jFL55pVopZ3d3dx88C19bWlqVS8Z2Lf/7U1XNvu51Vb72x7/irz9fUBEcEv/03PyFYPRJDgZHt9XpvzG8QlAFnWppY+S9LaOnsaPPOWdhxx7z5V320cydLl/7yE2+pb+bp06dj/VxtWbJ03h13zr/r7rtZu2bNwVP/9cKYMiHwtW8+QNAbwOiOIN09SCiChCKQL+EIKhxBhcN4EGpGjuJww66yxNH9gePac+zGm26sikQiNY379/kSydT63uCqrCybXB6oeuS+RYvYv29f/OTKFz1+dIlXXFQrCznRjNhkRfdJzmIMEAExsqbUmh68holWGXf43deMg6NHJ+5btKjkgw8//IFh8lJnZ88nBoBWxpPf+e53DYC1Ly5bVSb6Mo8WSrQgx5uRY6cHSDMcz0q/vx/PSTNeJXi04EOPfe93L70JcP/99xu2bfwUwPT5fNU+n++rM2fO5P3332+uS3V9y9KCG8FSmtjRo3iN0uz+qqylemDnLhpDQDsFJGrHMG2F2xAyGi5Nhr65Y8f21unTZ9T4yrz3KqVHGC4X91x33XUmwN7N775nApbuk90nD5BpbUbaWqG9Dd3eju5o6y/t7dDehrS1kmltYffJ/ViA25nDBcbeLZs2AUyaNNkyDL5minDL7Nm3opSiNtQ0yUQwESydlXg6xc70Sf5CewliYSD9TqHu/anpIMUnJIiLjSVCGjAFTA21odNTlFLMunEWO7bvuMUUjKkTrriCvXv3RDyiJxpacGVXSc56W2uO6DhtKkmFFsocHchmtKhoukURNrJPG5YDdAEuDYaAV/TVjY0HesaNG+8Tw5hmuC1zFEBLS0urkQ3QPtFgILgQTC0IkAZSgEJQCClnTBwdF4KBOPf2iQBnzrS2GYaBZblGmWK4/ADxWCzqoS85iDOZDFiMS2ddV5Kz2EkGhgwECYLOzqOzxy0W7YkAiBgBw7btFIC3tMw/2JsrnS9OI5B2pPdt0AC9gdVZZxkBANu2k0Ymk2kDCI6oqsw1c/nNu8rVW8l+2ZFCkxRNzMhKUjQpNBlnv23nXfbAeTRQHayudMBtBlod6OrqZNz4CeVprcKqd4KsZBxgGk1KNEmBmGiijsScsZRo0s4CMnn3284CMqJCY8aOCXR2dqK0PmBokQ3r1q7D7/dLq7tyY8axMCOatDNZFqhJiCbuWNsLNrJjCUcnt4C0ZOew0WTQnDYr3/X5fLJx4wZE5B1DKVm1a9dHAIyYesPYjEBa+vYwJZAUSAgkHAtjookaWcl9Togm4eim8u5PS9YDNVNmXg7QsLsBreX3RjgcPtzW1rarubmZ+QvumtahXJvzrUzmWRvrZ61yxNnvPKuTA6xvt13bvjxv/tSW5mY6Ozt3hkKhoy4Ar6ek6dChg4vm3nY7oZJAJnG4oUIQESdD5Ud0v30XSBlZC1OGdjyTA/darwK3LcxcPm585ZJnl9ATinwvnkweNgC6wuF1x44d27R3714WfOWucZGrb3g7kee+eJ6LewPLcXU0bzwuuf2G3P3NoyevnzP3tsv3NOylqenkHzvD4fWQ197aikeW/nJJd1dnJ4//9On57V+a8Hoib7K4kQeUAWL0D7RcsJ2oqHv9wUcfu7Orq5MVK5Z3KS0P53j96lsgEPjyiKqKtW/891uu2tpalvzDMxsTW96s9yhMC8HUOCkxm07JO/fZk5A9dkmDTOSqWe/99fcfmRPtifHY3z6a6Q5F7gyFQhsKggFGjKh4wFviffG11153T59xHVu3bg3968/+7g9V3ae+0Zv0kX49l3ISjA2ccpe/NXvR9+uvnX5tRdOpJv7+xz9OxnpiD3d0d/97PqcXrLWeBcwGLnv11d96n3j88QVPPf108KHvPUwmk+HttWu71q96Y0dozzajJBUfXyqMA4gpfShmeY54JkzX19/6VzfMmDmjMpPOsOqtVbzyym9alz23fM23Fy1KACeAP4rIBwCitb4MeAQY5SxEt7a2qIaGBn70wx+OTKXTc5Y+t8w1d85cdN5KtdbYSqGVImPbJOIxotEo6/+wniXPPmsH/L4Ny5etaJk46Rqprq7JPTgooBn4Z9FaPw9UAHR1dSnbTsuZMy1GMpnItLZ2GFu3bq5d/fvVc0ZUjZB7F36d2fW3MmHCFZguF0pr0uk0Bxsb2bL5PV5fuZLuUEjfdffdG2+66ebW6mCVLvP5qa4OAoYEg8Gcg7tNIAIEADHdJnbcxmNZ6UQ05nK7TT1x4sRYRVV1/FTTqdLVa9bywgsvEImESKfSAFiWhT9QzqhL6rh25g3UjbokPnJkTaKkxFRaa8NtGbaIy+Up8eS2VgEx0VpXO66+HKfdbW9vV93d7RKNJl3xeNQOd4d1Mp0i3B3yRCKRsmgiYSVTaa9orS23lfR5vany8vKYLxCIeyxLKqoqtddbKh6PSVVVtQ4Gg5IHPQI8nx9ck4CbgSuBarJnvARsiUai4XBPmGQyqbWGRCxh2VrZAKYYLtNjZUyXSxsuU6oqyg1fwO91nhUSzvQdwB5gm4h8UvA4OYsoByYDY4EaoBLwAN7sYiDvZ4LsqUo60uNIK3AY2CMioYGM/wPREY0iGUY58wAAAABJRU5ErkJggg==", "public": true } + ], + "scada": false, + "tags": [ + "building", + "interior", + "venue", + "inside", + "room", + "office", + "manufacturing", + "floor", + "plant", + "storage", + "warehouse", + "depot" ] } \ No newline at end of file diff --git a/application/src/main/data/json/system/widget_types/image_map_deprecated.json b/application/src/main/data/json/system/widget_types/image_map_deprecated.json new file mode 100644 index 0000000000..23cf7f7a14 --- /dev/null +++ b/application/src/main/data/json/system/widget_types/image_map_deprecated.json @@ -0,0 +1,102 @@ +{ + "fqn": "maps_v2.image_map", + "name": "Image Map", + "deprecated": true, + "image": "tb-image;/api/images/system/image_map_system_widget_image.png", + "description": "Displays the indoor or relative location of the entities on the image map. Useful to display floor maps, smart parking, etc. Entity coordinates are expected to be in the range from 0 to 1. Highly customizable via custom markers, marker tooltips, and widget actions. ", + "descriptor": { + "type": "latest", + "sizeX": 8.5, + "sizeY": 6.5, + "resources": [], + "templateHtml": "", + "templateCss": ".leaflet-zoom-box {\n\tz-index: 9;\n}\n\n.leaflet-pane { z-index: 4; }\n\n.leaflet-tile-pane { z-index: 2; }\n.leaflet-overlay-pane { z-index: 4; }\n.leaflet-shadow-pane { z-index: 5; }\n.leaflet-marker-pane { z-index: 6; }\n.leaflet-tooltip-pane { z-index: 7; }\n.leaflet-popup-pane { z-index: 8; }\n\n.leaflet-map-pane canvas { z-index: 1; }\n.leaflet-map-pane svg { z-index: 2; }\n\n.leaflet-control {\n\tz-index: 9;\n}\n.leaflet-top,\n.leaflet-bottom {\n\tz-index: 11;\n}\n\n.tb-marker-label {\n border: none;\n background: none;\n box-shadow: none;\n}\n\n.tb-marker-label:before {\n border: none;\n background: none;\n}\n", + "controllerScript": "self.onInit = function() {\n self.ctx.map = new TbMapWidgetV2('image-map', false, self.ctx);\n}\n\nself.onDataUpdated = function() {\n self.ctx.map.update();\n}\n\nself.onResize = function() {\n self.ctx.map.resize();\n}\n\nself.actionSources = function() {\n return TbMapWidgetV2.actionSources();\n}\n\nself.onDestroy = function() {\n self.ctx.map.destroy();\n}\n\nself.typeParameters = function() {\n return {\n hasDataPageLink: true\n };\n}", + "settingsSchema": "", + "dataKeySettingsSchema": "", + "settingsDirective": "tb-map-widget-settings-legacy", + "defaultConfig": "{\"datasources\":[{\"type\":\"function\",\"name\":\"First point\",\"entityAliasId\":null,\"filterId\":null,\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"xPos\",\"color\":\"#2196f3\",\"settings\":{},\"_hash\":0.05427416942713381,\"funcBody\":\"var value = prevValue || 0.2;\\nif (time % 5000 < 500) {\\n value += Math.random() * 0.05 - 0.025;\\n}\\nreturn value;\"},{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"yPos\",\"color\":\"#4caf50\",\"settings\":{},\"_hash\":0.680594833308841,\"funcBody\":\"var value = prevValue || 0.3;\\nif (time % 5000 < 500) {\\n value += Math.random() * 0.05 - 0.025;\\n}\\nreturn value;\"},{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"temperature\",\"color\":\"#9c27b0\",\"settings\":{},\"_hash\":0.9430343126300238,\"funcBody\":\"var value = prevValue + Math.random() * 40 - 20;\\nif (value < -60) {\\n\\tvalue = -60;\\n} else if (value > 60) {\\n\\tvalue = 60;\\n}\\nreturn value;\"},{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Type\",\"color\":\"#8bc34a\",\"settings\":{},\"_hash\":0.1784452363910778,\"funcBody\":\"return \\\"colorpin\\\";\",\"units\":null,\"decimals\":null,\"usePostProcessing\":null,\"postFuncBody\":null}]},{\"type\":\"function\",\"name\":\"Second point\",\"entityAliasId\":null,\"filterId\":null,\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"xPos\",\"color\":\"#f44336\",\"settings\":{},\"_hash\":0.05012157428742059,\"funcBody\":\"var value = prevValue || 0.6;\\nif (time % 4000 < 500) {\\n value += Math.random() * 0.05 - 0.025;\\n}\\nreturn value;\"},{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"yPos\",\"color\":\"#ffc107\",\"settings\":{},\"_hash\":0.6742359401617628,\"funcBody\":\"var value = prevValue || 0.7;\\nif (time % 4000 < 500) {\\n value += Math.random() * 0.05 - 0.025;\\n}\\nreturn value;\"},{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"temperature\",\"color\":\"#8bc34a\",\"settings\":{},\"_hash\":0.773875863339494,\"funcBody\":\"var value = prevValue + Math.random() * 40 - 20;\\nif (value < -60) {\\n\\tvalue = -60;\\n} else if (value > 60) {\\n\\tvalue = 60;\\n}\\nreturn value;\"},{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Type\",\"color\":\"#3f51b5\",\"settings\":{},\"_hash\":0.405822538899673,\"funcBody\":\"return \\\"thermometer\\\";\",\"units\":null,\"decimals\":null,\"usePostProcessing\":null,\"postFuncBody\":null}]}],\"timewindow\":{\"realtime\":{\"timewindowMs\":60000}},\"showTitle\":true,\"backgroundColor\":\"#fff\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"8px\",\"settings\":{\"provider\":\"image-map\",\"mapImageUrl\":\"tb-image;/api/images/system/image_map_system_widget_map_image.svg\",\"latKeyName\":\"latitude\",\"lngKeyName\":\"longitude\",\"xPosKeyName\":\"xPos\",\"yPosKeyName\":\"yPos\",\"defaultCenterPosition\":\"0,0\",\"disableScrollZooming\":false,\"disableDoubleClickZooming\":false,\"disableZoomControl\":false,\"fitMapBounds\":true,\"useDefaultCenterPosition\":false,\"mapPageSize\":16384,\"markerOffsetX\":0.5,\"markerOffsetY\":1,\"posFunction\":\"return {x: origXPos, y: origYPos};\",\"draggableMarker\":false,\"showLabel\":true,\"useLabelFunction\":false,\"label\":\"${entityName}\",\"showTooltip\":true,\"showTooltipAction\":\"click\",\"autocloseTooltip\":true,\"useTooltipFunction\":false,\"tooltipPattern\":\"${entityName}

X Pos: ${xPos:2}
Y Pos: ${yPos:2}
Temperature: ${temperature} °C
See advanced settings for details\",\"tooltipOffsetX\":0,\"tooltipOffsetY\":-1,\"color\":\"#fe7569\",\"useColorFunction\":true,\"colorFunction\":\"var type = dsData[dsIndex]['Type'];\\nif (type == 'colorpin') {\\n\\tvar temperature = dsData[dsIndex]['temperature'];\\n\\tif (typeof temperature !== undefined) {\\n\\t var percent = (temperature + 60)/120 * 100;\\n\\t return tinycolor.mix('blue', 'red', percent).toHexString();\\n\\t}\\n\\treturn 'blue';\\n}\\n\",\"useMarkerImageFunction\":true,\"markerImageSize\":34,\"markerImageFunction\":\"var type = dsData[dsIndex]['Type'];\\nif (type == 'thermometer') {\\n\\tvar res = {\\n\\t url: images[0],\\n\\t size: 40\\n\\t}\\n\\tvar temperature = dsData[dsIndex]['temperature'];\\n\\tif (typeof temperature !== undefined) {\\n\\t var percent = (temperature + 60)/120;\\n\\t var index = Math.min(3, Math.floor(4 * percent));\\n\\t res.url = images[index];\\n\\t}\\n\\treturn res;\\n}\",\"markerImages\":[\"tb-image;/api/images/system/map_marker_image_0.png\",\"tb-image;/api/images/system/map_marker_image_1.png\",\"tb-image;/api/images/system/map_marker_image_2.png\",\"tb-image;/api/images/system/map_marker_image_3.png\"],\"showPolygon\":false,\"polygonKeyName\":\"perimeter\",\"editablePolygon\":false,\"showPolygonLabel\":false,\"usePolygonLabelFunction\":false,\"polygonLabel\":\"${entityName}\",\"showPolygonTooltip\":false,\"showPolygonTooltipAction\":\"click\",\"autoClosePolygonTooltip\":true,\"usePolygonTooltipFunction\":false,\"polygonTooltipPattern\":\"${entityName}

TimeStamp: ${ts:7}\",\"polygonColor\":\"#3388ff\",\"polygonOpacity\":0.2,\"usePolygonColorFunction\":false,\"polygonStrokeColor\":\"#3388ff\",\"polygonStrokeOpacity\":1,\"polygonStrokeWeight\":3,\"usePolygonStrokeColorFunction\":false,\"showCircle\":false,\"circleKeyName\":\"perimeter\",\"editableCircle\":false,\"showCircleLabel\":false,\"useCircleLabelFunction\":false,\"circleLabel\":\"${entityName}\",\"showCircleTooltip\":false,\"showCircleTooltipAction\":\"click\",\"autoCloseCircleTooltip\":true,\"useCircleTooltipFunction\":false,\"circleTooltipPattern\":\"${entityName}

TimeStamp: ${ts:7}\",\"circleFillColor\":\"#3388ff\",\"circleFillColorOpacity\":0.2,\"useCircleFillColorFunction\":false,\"circleStrokeColor\":\"#3388ff\",\"circleStrokeOpacity\":1,\"circleStrokeWeight\":3,\"useCircleStrokeColorFunction\":false},\"title\":\"Image Map\",\"dropShadow\":true,\"enableFullscreen\":true,\"titleStyle\":{\"fontSize\":\"16px\",\"fontWeight\":400},\"useDashboardTimewindow\":true,\"showLegend\":false,\"widgetStyle\":{},\"actions\":{}}" + }, + "tags": [ + "building", + "interior", + "venue", + "inside", + "room", + "office", + "manufacturing", + "floor", + "plant", + "storage", + "warehouse", + "depot" + ], + "resources": [ + { + "link": "/api/images/system/image_map_system_widget_image.png", + "title": "\"Image Map\" system widget image", + "type": "IMAGE", + "subType": "IMAGE", + "fileName": "image_map_system_widget_image.png", + "publicResourceKey": "hDdSISQr6elribOYD6T3uePXZI5WvNtM", + "mediaType": "image/png", + "data": "iVBORw0KGgoAAAANSUhEUgAAAMgAAACgCAMAAAB+IdObAAAC+lBMVEX+/v79/f37+/z////yyUz6+vv4+frMzNn7+/tvz5f39/jy8/Xz8/b4+PmyssX29ve0tMf19fbGxtTDw9LAwNDw8PPR0dwAAADx8fW8vM2trcGkpLrPz9u4uMnBwdHOztrIyNbr6/DFxdO6usv09PSoqL2pqr7y8vLLy9fLy9ju7vLt7u6iormgobjo6O3IyNWwsMPT0967u8uwsLBQZW/s7fG1tca2tsi6urrk5OrKyte+vs2xscS/v8+3t8F3h4/m5uyvr8OrrMBoeoK5ucrAwMHq6u/r7OyursEYGRjo6Ojv8PDV1d/Pz9rMzdLd3eWzs7Pl5ufZ2eK3t7hEWWWyssDl5eXY2NjX1+HT09O/v87Hx8ioqKmfn6AxMDDl6Orc4OLGxsbDw8Smprxyg4slJSS8vLxdb3rn6uvi4unj4+TU1Nu0tLSrq6t1hY1meIHc3Nx7i5IMDAzq6urh4ejg4OCMjIxbbnh2dnbX192cnJxuf4fh5ObV1dXQ0NHPz8/Ly8u2tratra1xgYmIiIhkdIA1NTTa2trV1tlTZ3FFW2YhISHf4Ofg4eK/v7+lpaWHlZxrfIX19fi+vr5qamtXV1cUFBTO1NecnbSBz6KPj5BwcHFRUVIeHh7b2+Tb3ODT2NvN0NXExMuZmZl9jJTbulTpw088PD0uLS0GBgbe3ua/xsqWl7CXo6mjo6OhoaKEk5mTk5N/f39gcns8Ul7X297JytTHyc/JycnCwsKcp66np6eOm6Kut72RkayLmJ98fHy/plmys8OnqbuDg4N6enpXanVNYmw5T1xMTExnX0q9wce0vcFJXmlnZ2dGRkZEQz4CAgLt7fKosbeiprehq7KSnqWWlpeBj5d+c1Kxur+GelXivVDZ2d28vMWFj51oaGipllrR1dh91KF10ZtudohjY2NdXV3IrFolLymy5ci4wMSqs7moqLOHh6SYmKN+fpxvmYBtln5YUkKhoat00ZtJWmywmliy5ce3t8ZveozbvFzZulp3blBe970rAAAf9UlEQVR42rzbeVAbVRzA8d/bN7uJm02WQJImEBIIEAooISCBJKiA3KSWgnJTxVoUpB6F1lItVESLSkVbj06ttbXOVLH17Gir1nGqtjreZx1ndBwddcb7D//wT3dzwIbsZheifv9o0mWTeZ+8t48MBMBXPKsB+VaZQDrKgIRVsxCnmz+HOBlzMuN9NRVEQ4DhmjuVOMAaF+JHwlIZkE5/+pflQ/JTQbIbHwIlmeJBSD8phFQZQboDp7+DOOXn1IF0pZIQBE+BogLxIWwUpBRCTWyCJhKie+90wfIhmQ2QaC57XAgjhFQUQiiiHE1ktw2X9MJCR69G8SGF/zHEFxeiEUJ0lRCMrVXlbs/d0DswAfNpTr9BQXRIKCuVhiCKrayCSCQliGEYEIlAEJPDtfgkk2FUApI0CMF2v7vS25t02eT2WjJ4lrlwsPXAsTd8aiAJfmBM3WhrwKQKZjWZ3Fwme1oGV/CI1WpKcUTKSk/Scb2mkwpynA0NOofNYrFkXWANBNwmq53L1DpoJiJD1uo1+Y5kiC7P6XQmRc7wf6JhaHWIQRn3uFYu1J3EVVbjL8pJ43v/2GdvvFHdUNGd5fF4ymwOLhufK89qDYpeS+YlK1V2KycLqAwpqsBoKaNlMo1M6ahFl6kxZtbx5S9kZriA0ZDARdGlowF7KJPJZPfXpNdYbIakhrRzg+05YunmsI7kDDdfko8iOEAZeO3JvlVZe849j2+dU9edrit7zWm3hxmrAp2dnXPeTA1J0HzGYzeSJEiGCFRFESQCiVgdA4pCJB2zrggtY86sLEsh6UyOyr1m9sCgkYJgo5Yal8uRsd8IlGamIjyH2vRPEB9JopjoY3K7ZGqsQggxQ2KhdBUEo1hSakdJYsLn6jRIqpO3URA3JAMxQoLVWEGm0gUII+W45vajkAhEnzik2KQYAjq9FOTXF+RWFhEXwiQOsSwF4pWCfPnC5bIQIi7EDAlmULC09BAq6VpEEGIO/fvvgkxkXIhGp4EES7crh5RNT255a7cI5KtjB0Am6r+GlKlApnwBZLqra0wEcvP7RGIQs46BBMvIUw5J39aztW0o1pF97GWQS5tKgnTGxCGmPEIWsisCmd77/FZaZM+6mQC5aBmIPnGIWhbCzkO02loU2/VfgmxsXEj+vwEhIX5GAWT27ECsw3jmVZBNHxdSqmP/T0jN9CW522Iht5z5PHEInTiEUAzJGhs/lB0LOXDmJMjGpFIgXWbiEHcekoXQEKp4bLJtTSxk55mnEodQCUPsoBhiGfuubTwWUn/mIMimiQup+xcggaVArmw7EQsh7zwKspllIGTCkNXKIReMDbe1odguelYJRAvSFf4LkErZISRFhpA8tnuiVgTyrAKIMZWOCyEgwTI0S4F83X+DCOTeZ7WKIRiLXuwJQ4w5xFIgA/1aEci7dx4ATDStAQyS5YcgeLIJYsIs6UOR+7C8Wp0gD6HmIeOz7SIQ6u1m3HX3m289MiQ9jE/CkA+2iUD2TrRGILdCqKH+Wo6sPEcA5NLMQ1LGymd7kEhX3wtP5mLc3hhcO7wGC28wFxql+TthCOaL3OH/8eLQPbSCCB4nbq3tKgflIR0sCXLlbLkY5Mv71Cu0GGAb4KZ3Gt8iAE8cbpzibsrXrj+LcPPD5eufuASB/oHGqXd4CO5/5uEHP+Ae0bxh314S8KEu3Fu+5cEPiJHDKw4PAZd6eFvHBCiv0rQUiO3asdlhMcjn58PeO8YRxphubMYb3sJDa1nc9ybuf4eEqSk8smI77noQ4zvugaYHg5CWtdlw6AGc23yC7mnswFv68ea1NGyZxCT/gvANv0LAEnJblwIxJJc/vRuF6+jdcUPkgnnqTCne8e3G50hcshmAfhJPDWBAE/jwJgBiBRpZi4FbMwXrMeAPQpCt/BF111bA8NPrQQj3iOOv80sLgg1kdyBQnt0OsjERCDIYRmbHI5Dc2p62/kOh3fiWi40YY/Xew3jzg3dzqX/sCK7/jdkA+MHckTv4YZM3PBAFwY25bVMYw9nn8ZYr8ebdANu3LED2bt5cD0oig16rHSmAkBCMMBj6WnIjEH1/f/9xtgvxEZ9PD2MAWMFumMV8U/0YQI0PN/EHyTCk/o4oCHys7XqYg+weEIGMtGzWQqic6oqkC7q5amrSdQZbOIPFYrOrfC6Hw5HhLtRHQ1Clyeol4kEQIkLTceUNbf0T2zchLeIjT+3c2I5xRyM0P8Jishzf8CHCx3/E5fcDnvwJhyGwvgPYR7pImiZK3kEw8CFWN9bDjlztAoRj05ggydGxS7Q9ZDCITU2RyDxnykjmFC6fSqWynJskgKAZi81kMulM7MIhSq8prUxHJEXRDGM2GFgCiaRx7Dk4/eEj6z+qx3j87jvWd2Dc8sjaB7QY+h5Z/yNJNK0FI1rBGnfevX7fvu2jKlVh776PnnhixFv66BNrH2jTaB6uxc+3eEdr7zeyWxovmXO5XA/f//BZQzCIFxl5z3kkKcs3auQngeUYmcBFzLj8Fq7uGl1Vag7fkdRw1bp1XM7q6lRdetmpiorUnDSu8/bs2XPk6pM0SWs7y7K8BE0MlvkzEUW1FruMiNy1ykJir8eMWUcl0hKAVAbD4OxerEV1KnshosBrrcSoFVBnAGGvm6IJk6vG9djrW1siEPl8Nl5TU11dVJFafcQo9fP6Ij/FGEsLC+u4l7I0X8NQlPB3E1rabDbfcvtN3NF4EUVePUWqSZKIXCNciGSZzFE3tzg8LEFGQij50frpkhYUDOSbs7HAR3JD06jyQKJA0QwRjFapCJDo19shfqioCebDN3RhwZe8HIQSnpv8df1YybBSyGAABOWpQCJvESMPufz9A3KQNSCQYBDWqfIIB6zmZuSZQyWEMshclAPlWaW/IRLyEHT+4/IQyQpVxSCI4CD1XSWUEghq1aFoiAkkYtPDEG0cCDx7k0bmXV0zlnyTrlF1gzDfo/XtLW1IAYSyF7MQlckNEmk9SiCf3nQgPsSIQ4kOJ3pGYCa9vn58kwKI2VG8H6LzSi4tdZFaAeSlm96DuOGd+zZu3NchKiFUHibq/86d3/UheUil31YY81xJtCSEJog1rAyEfeGojKNxqr19qnEnBpFUHhMIm37sMW2TLKTVEiAhpoBKQqIt0tdPys4IfH5RXAfeN4W5ph4QnRK7x/PKK+0w38i7vWdr5SCtFYUgEiX1ETSW3357C+Qg9cc0cSEb23lI+0YJiD87dzvMR1542eZhwfbbVAAx7c8yg3gXiOuZopG2tg7ZGYHrfokLWYGDrRCFrPJ8U7CmBxb67UIi8g1xZeaa8tmS2J0uWXowZXZxSH1PSQsrCzl6EZ0AxPP07AQs9Ncfzw2EIS/5L5xmx5tiFlYnSKZBVetiNy86qWBHSUmzLOTkVQ8tG2L3HBwaqo+C/HFPGKI96Pf7u7oIiM7CQpwIL3NemtVLgbCsgvK+Em5GqPiQg1d9tXzIqeff3J0LCzXk5zfPX+wpq/xdPeRiiAbkGuz2z4GgmoL+Q7cSshDNdUcTmJHmy4qFT57KonDAVUSTsz1NS4YAWWnSqQYpCOcpKCjokIfAyYvQciGqU5s9KhBUvUsIATsqmRxfBGFAUXOG7jWRPaCASwGk4O01y4Z49ntejILQUZCArRQWZdGCstDMYy99rQ1BrtxdqwACF51cJgSpPJ5TDhDkjIaQsZ/qNVlBWbc8xnUQuNILjnd0cRBSDvLydfrlQzwpICiHEkD4WucgOsZjBEU1tbddMj3EL1xDQcEmQgnkvdsuWTbkxVODIGjdYgh0xixHAwFKKn96or293eQFsBToCUWQh277dHkQQuXZurU/CkIuhoALoiusqQTpkIaCUBN9vRwkYOVnRB2BqCFe9HVfLBuSW58LgtJiIa0qPURldYNk3iydLqUU+F6f6htqb3d7AdzVhDIIvHfjsiEAIANRu1KiJUTVKEjUmZ1rrFudoWvVANRO9W1qb1/JP8A5DyEhbrnHaGkI+vv7739GohBypQeBMCQCAcrug6gcTgSiZWSHG7PxVx43IwHEP4FTLQY5PhEeBQCic/OZ4Ab8qTTk53O4fhaH5C2CEGlELIToTHIzGkH711EgWoUmIvGumjy7t29oyA1ctJMMQtRRkG1acuDKiZ6SDVM77unr63uJp8PNtyNJyPc85HtxiMpDRB8QQCwGLpsjpdiTlFThDFZd1RC8TXNIfQrCmD3f9OQPO5uBjxGFDI93tZzo6Rm550RHx0BfX8lqDQD71elmScg5wUQhVN4iCJWGIoG7dfVgYT5DcTxOp9cwFAIAZJ6zcRQEYtEqm3ceMtG7CUJpnJQIpLzl1lsHjvdkn7hnx46ODRseWz1DEoPXnj4IiyKCxYMQFOvzMKyGG2I4khVApPMglAzimZ1FVbrWIOREby2EylynZVmzMb9OpfIWlhqZyrn9+2dWx8YdNp3+wsplt1sXJYSkpKS4HBZLVjqXLtzvOVE5/zwvWFp1KkjnR9ISytm52lDt5iDDG4YAwFSdlvPaa1UVXFXV5x3RNTQ06EJlFRdbHClcNput28+XxXXT6WSXz8XlM7lNwWIhps7RTL02GKsJlplR7K3kKywNNbpOyYxkIS6fmwKRdFaEEGMqdniDv+YutMwY62oYFMpgI5FML99FUxSFYhJCUEyMrzh6NHVphAJIOuILeOwzZlgUytmFgrktdvugptOpYZhMDx2BGGQhzW+rUSTlEP1iSGYaqegaCbVaV13hHi3k83q9dXoETKcdhSMHTabqnBwjw5jLIk9qk4egm59aBoT2FZMgrDSNUgDRoYWMnRkGPv5PcHR5Pp8PCdrlc+YzDJOFlgB54b5lQLS+YjUIy1cEKYoeDrFrl5ZAfKWmC4pakTCVimGMNUuCXLoMCOUrJkCYMU2rANIgORxjUVFRAAnKzGLyXcULEEoW8tLb2qVDyFgIrQCSKjkcqqjIxJj8+2kUaS413eNfCuShtwuWDlGvLEYgzJzGKoA4dyGpdDqaJHe5VWihQptjKZDc299bOoTwLYJo0vQKIDnSkEILyad1uAXTlLQUCLrtqqVD0GIIowiyjkFS0WVkMMYeWIDoULiUKEjB5DY6Mg1qwf57p1rM0bsxBNnYKyZRFUNU+jRGASRNGvJPc3cCFUUdB3D8959/M7vN7uwywOoCsrggrNzoSrvEsdynEUcEISIi4sXhkWZCJhpalCQdallZSaav0qzsUMvu+7A7u+t1vl69jle9rvf6z86yxzCzLGbW5/GYQeDBd////wyz7kJxFj3iwmm0KyRLNkRt51utV9bTqMY2e//6TefZBzud722bYZDpOPz1dWLIdV93YjxmiD6gkCSjYog2nHZriHeF5MmGJFqXllvXdbRvjkh6YD/R2VE1hYwF98KMOUgC8LllpMNVUlaOAUlEhYEPNpCpRSUZFEOofNpD1SCGhMuGoKWt1rmtrVfbv01Kmt3Uaa0aPE+joagpO2e0jA556ofT3X54VH5EaA+KjAjDicCpovMiFlyWUuBEJyUgRXlX0W41Wc4P1JZSSGQyMV7NrIDsTCBPlA2aWawRmNPNO2e8ped5TiB+HNniPdd5Qq5rxmy8k8r91Nu85BAfpkcKI11AgNr5jr6+2glEa1XVxZ32wXYALuk170FD4FnW2vRHgnREUCRRmJYXG5SVl5f2nU0XqiJXDzF33pkkFZFV6tqbFx4eF0G2yd0OXZ6TLiioMCbUlBmG8N6fPSGfLcdo6qQJUVHxHqbwEF/bU4waAS+Og8W6tKOqvr71AfI04Y6qi6/s7LBSoE8yIwGuSCQs2OKqaCBXOWl3NqQbycVaQkpGrn5RSmSeKUG4OkTIAmQTbUsxG/R8ulmUoPGl5QS9DscclmEQeGBpCAYnir7KmJ7RoCIljTzjRiOUkEaNcE2n1rXcg9a5Re1TP7RWrbX2DdoBtEnFYsj8a1evXn0Y71gMtLkwPCs+Q0OhEPpqTLm9pquhBOjca4R/rbFxgDdVgF8VDof0I7B0amHwxtWQEAZ5M4RLQgjEMLzWkDJ16k4tAqAotSYplxJDyl0PzU3XGmmEyB6pTLwRg/jpGCMqdxrZAIVcX5qhML5nFgZ/9A5HujTEZ7H/+KgkBKhp0hBjvidEp7MVhmZmZofFFBaGBU8VRQmvXPORuryF3BSTr9pxKb6k8/Y+XLSn7Hooan71dh5RiLu2av7GekDsrsub2wGdew9OXN0xf89K/NzW+Qf8liCHIxGkJdWSw69Ebnw27ROiifWEUALWkHv2zLNGKqImEFFT00KiyUU1CblUONJRN6zE1zxXgUqa1bB6H6q+URg2pJ+4DhbvZfEljyL1tUWYPEC0euJaaL0cw+pBwODPsEwIbvI5IYKEMb7RN0Q7zyfEgyxC974hzuBaI2Xkka0cFkLOAyiZX4GB0pCpRQkhr2IK7imCvVMQKm92htyOGXYiIlML/NviKAGZkrIfr/vss+t+LJPpADo+W+0bEqH2hCgxxpnRyBohXCEIX3RDcyfUeIWgpn3cRERuhYliCDDMRGrskCMkRKak/NHm5cubHy2X6QAU30j5hOgjGE/ICkpQsTJROQTIxhUCCDD96CraKwQuaYeJGhYllrlCuIBCjjoWwmgIjwAZ8dm+IWyE3h1yeFfn4daHZtvt9qLFPiHauHS5kPaPAdufoizLXWukBLNbLfieVQBP7XeFXCWEXNKH8QmEsJNphQwxBPngIrTukDVr1uyz2yvsxKAkpJhhyTL3DUHw5I3Nr/DT0HOXVzhH5MmNW9sRrb5m/o2rEBJDtELIyuX3+A/ZIhuiD9KAhJ8QJsLgDulcs8Zur4uIq7XX2g2MO4NlNfkqIl5lwNQ0slOMkQGjm1QztRRNn33nBFrPZBeemf4qZqjiyNJUAwCQWgzkRasVtgD+Q5Y55siGaEERkobQEQnukP2Hm64u+jbNLcsWGRrm/LUwj2SHZZNHKIemkvszXVy/VEalmhd6RbpBo2H5VzHHEnwa5cHYKBiT3iEbwvoLoaQhlNeIUApYQxrP0EgeN13PCjh6FWbFEIbysCFQphEsXPhpW9u2hSPmtIy4ufTCljkLKzQs+GB4njeULJwqCUFJ53Mc4zeElOSxSAkdM4V1ohDtCtGLJ5/qlpbezd1Ez4YNGxzLli17Z0OBYIHgsstm+HWZSNxf0NO1bLirra1tqMCtZ8Yz9/YMbzny1qG3jhx9p63/ssuGPjqUX5htCglRDmH8hKAYDetDk3Wwt/qbd/oXuHQPkwgXR1fbhg3Ci2N4y10v7Dy47WZyy9+xZcaMZQ9va2mpbjm4bdvNvZV3bbl7y9EjW9rIt7uha9ndR48cOXJ0i2B4eNhds2HGOQpeAkpRnl45JIyEMBQCDIhiNL23dD1LdC9ra3Pc0XvwgfwrWBjTsi6QQSsftVg+PTTbQuZlxZwRB+/cWV1d3XuQ8BOiY/2ELFRjN+71Bfedc1fvzVpxAdLGcCOMzeGQDbHRoIgODUWS+341ahc/IUHyIXxi7y13HdJjNLhjY9neso07ihDG8TrkRmnCNYGEtMmHMOMIeS2gkEjOt0CduHnbIUcBWcgFLwKubd5z/LHbdt/22PE9zXaMzchDG1hID8hgbJzfEAq8pQcUcoYBCWhN713DPf0F7xSQQ8nQkSOHXn+xF5Mz+Senjfhk/g41Bq8xCyikawhkcDYWFKmlIeaxQ/jEm4fuvbegZ1nXBRdccO9Qz1DXW3eUcCDC6ueO7T7NY/ex59TYE6IPLKQAZLA2PSiiAg3hSzZve/3unv6hu7suIBb0DHfd/fCcROlthPE1x07zdWwHxuMN6aZkQ646wRByEGu5y9Ez1DZc4Pzm2452dXVtebe3Zdv2XJCFce389yUh719ux+BZI1oYW1u33LLW23hQhKQhxV4hwszp7ulxDHUdebHXwAXw+94waibrQ+KTZuQeEk2AIXKTiLdpxxGSEmH0jIiGAlmcTq8QMrjnNJfbHrttZHdPER7viMhNQK1XCE2NDqEVQxTRup3yHXjHcdcaf37v5Xufd6364zvcc4uXCWGngNvixUBs6K6QDRnJM0eRuz7io31uzdEhhrFDKN0tCiEbHxO/959eKcErXnlefOOxjSMh9BSZkE2bgIPFL2Re2Xp1bVGRfakQUiIbYgSRijwsQjUpU/WPQ5CuUiGk7DZxXi2vwBROXO56qwxjhCiCkVkjqL6e7eT0dXWXbtq0qb2oaIoQIneFqHGHTGeQoNRfSG4gIRBUifTVciF7d4tj0Oy8IGwWx2f3XowTnAyWtNx0o4bjkDuD0dbfGptTrqmrq3u8r6moqLWE53oKqkEK0QZbMUvTzpMfclJFacDNFKrlvS2KMOs1IlBmqzy8a1eiTMjW990jgj0jshUzsaL87yOSk+YJ8rN0keRyMyvcKTa2zsuCz2OmNzaaosl9gE7BqdHCJtbkFBy6SAzRR58Z7BRC5KWF+XrkjCAXUBZZyfft4/yskedfWeGzRjAIKIaPoGBM6u7+gzTL6zOmnZka4nxyM7mQnpyaut2USoomTzCjEekNZwZHhwSTh1tMzi4VLr5Nk8+aedMVGeffNCk0wqjXjjkiMZWW9kFeJuTj99xHrRvdR633riEhIjqQEDja34tkTEd+REcjmqGQmzbJcwedsumV169ebYfRJdaNMueRjVYM4wmh3+hPRDLC/IYEIx/6JNodghTXe2blrU19jEyIfvkfp0n9uVw/vhAurp/9xyGsV0iHtRzkqSoJALm5dXxUyF/XYK8QBGPSP9GNxh0SIgnhkih3iNVarxzygOw5EV+0dbekY/fWi9wdQAUSwsdtOAkhEd4hSxVD9j+1SzYEf/meJOS9L/E4Q4zhT//zEGaeJ2S2dbNiiOXi66fIlmwipxJv75fVYxhfiDl/xj8PoX1CqhVDDlx/wCI/JF887xPy/Mt4vCG5cQPUeEMoaYg61ntqlSiGWK/vA1m4pOwrr46vykowjDOk4YkBHsnIRIIKM714ivCtq/2GUF4hHdYK5cV+0Xr5Soxn7/Fa78378bhDgp8YSFQO+WBteVXH4SX7yPNeEld4hURLQuI8IeutPMiLr7SvX/8gyEJw7LhnpR8D7PPOAEK0aREDvYoh3BISUrW+dsnOSkuHXTkE5XtPLRrkpVYe2GVfrDDD8eKyr9wTayWGcYZowsLfGGhRWCMUf/aSJR9UVfU9sGTJks0dfd4hFO2NiWU4ghZCVl0PCn4jP6KmgLwJRtw3X5xc789vwtg7gyZLEJQgWlt8/szQ0nAhhOX14t8T0CSYUzKuqJkWlWranjqz5sIlXnIme5Tmq3zZXNuzJgECJWelZtY9oBRyJuDVP4nXiR9jNGmyk6kxrDCS+C4/Pz9PZ8ssdInUOQXFxITZdHlZwr4Q8sb06dNNIabG6d7mTXYSftaVUp1ZmqWSMUkAoBxyf86uOlohJITB9EbhtHi8WY8NNVGThV80pGrI0NIMc3byIpYB/zSmJwZKGxfRIKBpPe9iiClmCYYe4TO4wcG8WkSBhN+QK1fVaRRCTHqEV2799bRPyjZjLM4ZBC4ZycUwJubxAZvMxOVjEkARCdGDPP8hFPUhUgxRA7667NeyVkxx0iNBshnGdstAIzpVIauezAClqUXGGOPZy2djy5NPIvCRkpwAY5vzUjDIhRj+hRAEoBiiJ/MUC6DpbcnHmZMNMDbDgAlG0/sLQScaAsohpiniijMideeBzeAjIVkDY+MGHkf/fci0nJwP99er1aqo4isPHJBcCBiTtTA2auAN6n8RklM9V62een6xdc0aBD60yQF9uYGPTtWITPUfcu5KtVqXUrz23DXgi01mIADPDIFciPFUh9TdSlZJVqLlvAOHwReTTEMALlgw3hDqBENUfhY7GZELSUiaxTLLwp0HPlAyggAM9cNobIzm5IdE+XlfzofWFcKIkJBNlkHwlQyBOFpwqkLOAiVIlXNglV2tpiMtlnWzZq0DXxEQiDu6/vsQtSqncjE5aOVGWSwrVqywgK84CAT3jGyI9qSH2MJACa0ia4Qjp5GpZESsrSCRB4GgzpGr8xeiVgg58b8ciISQSjUdTEJW1q4DCR0EZIAaZwh90kPgLBJyCxkRFVnstXNBIhICcq/lFIVEgaKGnJz15WSNkBF5qLYdJLIhIAvehVGYGP6kh0zyE5Kbc2uiRa3mwyyW2bNrpVMkDAJyQfepCYlSgSI6Z215olrNRSZa9q0aFTIdAjJ0DhpfCKMcstNvCAJFVqu1ipwQG6fRQJ/oGtl2n0EmRO83RD4TPdwPylT+Qqrs9otISEZYwoEnE0HCBgG5+b6DJynkzf6P/IZQoCiHVTvpKoA4scMvfe9RmRB23CH0W7+/rgZFSKWixw4xB8FoWRCYdwsWnoQQw3D//eAH5T9Ezzpp42S+cB4EprrgDpCgYzggNtlBBicXMudz8UH2uRkZZqd0o5OWc6KEkBRBQoJBq+UZBiGtgewJz8TKbQjdnj0i7gp/f2XSv7sPjQ5hhNf76vc1WWfNenDd3KaH5g4+OGu2Wink4XMOMSCYl6xgXuydQU4xokaTyST8d32kU7JYERJi2r49eCYjCaEgMHf0y4egptp1+5qKiNaEovbWufZaLZIN0Xz0+QsgSjAbjBI85xQdQoE8iknJR+BSo4uOnsSCt3waAjPnPo1sCCSy6MG+tbNaH1ppXNr60Dp7vVHDIGClIdsWdFeDEwJlISREyWux4JYR0hAcFF2TomWQ0MjQVKmG14r0jD/0wpfelK7nbD3DcUaBwWzgxRs5Pd2ckpK7KCMjOHhRrpkwGAxGnucP/dLluSEwoBMIMcSBh81A8dGROiKvVJD/vS4oMGe8NPBErFN4KUE+LfKR0vAst0hTZKQtLS8vPE6UFhdJZIaGZhaS7Rm/fMSNjAf+G1Rcm7rvZj7gAAAAAElFTkSuQmCC", + "public": true + }, + { + "link": "/api/images/system/image_map_system_widget_map_image.svg", + "title": "\"Image Map\" system widget map image", + "type": "IMAGE", + "subType": "IMAGE", + "fileName": "image_map_system_widget_map_image.svg", + "publicResourceKey": "QKRIYhDeBGwjaeIS601VvNLSsvZ25DRj", + "mediaType": "image/svg+xml", + "data": "PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0iVVRGLTgiPz4KPHN2ZyB3aWR0aD0iMTEzNC41IiBoZWlnaHQ9Ijc2Mi43OCIgdmVyc2lvbj0iMS4xIiB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciPgogPGcgdHJhbnNmb3JtPSJ0cmFuc2xhdGUoLTI3LjA3MSAtMzA3LjkpIj4KICA8ZyBmaWxsPSJub25lIj4KICAgPHBhdGggZD0ibTkwNi4wMyA3MDYuMTMgMy40MjkyIDE3Ljc5Nm0tODgwLjg5IDQxLjEyMWMxNTAuNDQgNi44MzM0IDE0Ni4zOS0yNi4zMzQgMTY2LjQzLTI5LjMyIDM2LjE0NC01LjM4NDggMTE0LjI5LTYuNTI1NCAxNDguMzMtOC42MjM1IDQzLjM3OC0yLjY3MzggMTQxLjc2LTExLjIzMSAxODguODYtMTkuODM0IDM5LjgxMS03LjI3MjggMjIxLjM3LTAuODYyMzUgMzE5LjA3LTAuODYyMzUgNzAuODI3IDAgMTQ2LjkyLTEuNzI0NyAyMTguMTgtMS43MjQ3LTMxLjYyIDAgMTE3Ljg2LTIuNTg3MSA4Ni4yMzYtMi41ODcxbS0yNS4wOTEtNjguMTI2Yy01Mi44IDM0Ljc4NS02NS44OTUgNTEuNzQ5LTk1LjYzOSA4MS40OTMtMjQuOTMxIDI0LjkzMS0xNDAuNC0xOS4xMzktMTc4Ljk0IDM2LjY1LTEyLjI4MSAxNy43NzctNDcuMDAzIDQ2LjU0Ny02NS4xMDggNTkuMDcxLTIwLjEwNSAxMy45MDgtNTYuMDM3IDQ0Ljk1Ny02Ny43NjkgNzMuMDc4LTQuODAxNSAxMS41MDktMTMuMzggMzUuOTkzLTIzLjQ0OSA0Ni4wNjItMTAuNDk3IDEwLjQ5Ny0zOC4zNzcgNi4zODU3LTQ0LjAyMyAxNy42NDgtMTkuMDA1IDM3LjkwOC0yNS40NjUgMTAwLjkyLTY3LjYxOCAxMDIuMDVtMTkuMjgyLTYyNC4wMWMzNC42NTktMS44NzM4IDg0LjAyNyA3LjM5MTMgMTA5LjktNC4yODU0IDEzLjI4Mi01Ljk5NDEgNDEuNDA3LTIuNDYxNCA2Ni44MjktMi4zMjA1IDM1LjMyMiAwLjE5NTc4IDY0LjM4MiAwLjYzNDc3IDEwMS45MiA1LjAyMzIgMjUuMDMgMi45MjY1IDQ0LjY2MyAzNC4yODcgNTguNTI3IDUwLjY0NCAxNy4wOTkgMjAuMTczIDYyLjc2NC0xLjcxNDcgNjYuMzA2IDMyLjEzNCA1LjEwMjcgNDguNzY2LTYuMzI4NCA3OC42MzcgNi4xNDExIDk3LjM0MiAxOS45NjkgMjkuOTU0IDUwLjQ4NiAxNy44NTYgNDQuNjE5IDgzLjk3MW0tNDcyLjQ1LTM3OC43OWM0LjY0MzUgMjMuNzI5IDE1LjA2OSA3Mi43NzYgMTkuMDYxIDEzMC42NCAwLjg3MjA2IDEyLjY0IDUuNDQ3MiAyNC45OTMgNC4yMjIzIDQ1LjI3OC0yLjUxNzIgNDEuNjg4LTE1LjcxNyA0My42NzctMTUuMDkxIDYwLjM2NSAxLjQzMiAzOC4xODIgMzAuNjE0IDkzLjgzNyAzMC42MTQgMTM5LjcgMCAyNC4xODEtMi42Njk2IDExNS4zOSA3LjMzIDEzNS4zOSAwLjE1OTExIDAuMzE4MjEgMTAuMDY1IDM1Ljg4MyAxMC43NzkgNDkuMTU0IDAuOTQzNzggMTcuNTI1LTI0LjQ3OCAzOS40Ny0yOC4wMjcgNDYuNTY3LTUuNDc3NyAxMC45NTUtMzYuOTczIDEwLjg4Mi00MC4xIDI0LjE0Ni0zLjg2ODggMTYuNDE1LTMuODY2MyA0My43OTcgNC4wNDY1IDU5LjQ0MW05Ny4zMzctNjkxLjAxYy01LjAxMzMgMzUuNTE2LTQzLjY1OSAxMS4zMTctNTguNTM5IDIzLjc4MS0yMS4zMyAxNy44NjktNjIuNSAzMS40MzItNzAuMTI0IDM1LjM2Ny0zNS4wODggMTguMTA4LTExMC40Ny0xNS4xNDItMTI1LjYxIDQuMjY4NC0xNS45NTEgMjAuNDQ3LTAuMDczNSA2MS40NjYtOS4xNDY3IDg0LjE0OS02LjAzNTcgMTUuMDg5LTE4Ljg3NyAyMy4wMTctMjcuNDQgMzIuOTI4LTE5Ljc0OCAyMi44NTYtNjkuOTc0IDY5LjgyNC04NC43NTkgMTAwLTcuNDk3NCAxNS4zMDQtMy4yODQzIDQ0LjQyLTMuNDcwNSA2My4zNDMtMC4xMjc5MyAxMi45OTQtMC44MTAxNSAyMy4xMDQgMi40MDM0IDI4LjI3NiA0Ljk2MTYgNy45ODU4IDIzLjcyIDI4LjExMiAyNC4yMzkgNTAuNjExIDAuMjk0MTEgMTIuNzcxIDAuMDEzMyA3OC41OTEgMy4wNDg5IDg3LjY1NSAyLjMxMjYgNi45MDU1IDQuMjIgMjYuNTY1IDEwLjIxNCAzNi41ODcgMTEuMzU0IDE4Ljk4NCA0LjM4NzQgNDAuMTU3IDI3Ljg5NyA1My41MDggMTkuMDUgMTAuODE5IDQ2Ljg3OCAxMi4yMTkgODEuOTI2IDE0LjQ2MSAzMy43MDMgMi4xNTU5IDYxLjUxMi0xLjQzMDQgNzYuOTIxIDYuMTQxMSAxMS41ODUgNS42OTI3IDguNTgxNSAxNy45MzMgMTQuMjk1IDI5LjM2MSA1LjY0MDQgMTEuMjgxIDMxLjUwMyAxMS4xNTYgNDEuODA0IDQzLjQ1NSA3LjYwNTkgMjMuODQ3IDMuMDg1OSA0NC4xNTcgNi43MDc2IDY1Ljg4NyIgc3Ryb2tlPSIjMzY0ZTU5IiBzdHJva2Utd2lkdGg9IjMiLz4KICAgPHBhdGggZD0ibTQzLjI3OCA1MTcuOTVzMjMwLjg1LTMuNjM4IDI1MC4wMS0zLjY1ODdjNy40ODIyLThlLTMgOC42MTk1IDUuMTUxOSAxNC4wMjEgMTEuNDU5IDI0LjU5NiAyOC43MTkgOTMuOTEgMTEyLjk0IDkzLjkxIDExMi45NCIgc3Ryb2tlPSIjMzM2IiBzdHJva2Utd2lkdGg9IjFweCIvPgogICA8cGF0aCBkPSJtMzUuOTYxIDU3Ny43czE2NS41Mi0xLjY4NDUgMjQ4Ljc4LTEuNjg0NWM0Ljk0NzUgMCA3LjcyOTktMi44ODMzIDEwLjUzOC01LjcyOTggOS42NjExLTkuNzk0MiAyNS42MzItMjguNTkgMjUuNjMyLTI4LjU5IiBzdHJva2U9IiMzMzYiIHN0cm9rZS13aWR0aD0iMXB4Ii8+CiAgPC9nPgogIDxwYXRoIGQ9Im0zOC40IDY0MS43MyAzOTMuMzEtNC4yNjg0IiBjb2xvcj0iIzAwMDAwMCIgZmlsbD0iIzMzNiIgc3Ryb2tlPSIjMzM2IiBzdHJva2Utd2lkdGg9IjFweCIvPgogIDxwYXRoIGQ9Im0zOS4wMDkgNzA0LjU0IDQ4NC4xNi02LjcwNzYiIGNvbG9yPSIjMDAwMDAwIiBmaWxsPSIjMzM2IiBzdHJva2U9IiMzMzYiIHN0cm9rZS13aWR0aD0iMXB4Ii8+CiAgPGcgZmlsbD0ibm9uZSIgc3Ryb2tlPSIjMzM2Ij4KICAgPGcgc3Ryb2tlLXdpZHRoPSIxcHgiPgogICAgPHBhdGggZD0ibTMwMy45NiA2ODIuNTkgMTQ2LjggMS44MjkzYzEwLjUzNCAwLjEzMTI3IDE0LjM0NC0yLjYzNzQgMjUuNDg3LTYuMzcyOCAxMC40MTItMy40OTAzIDMxLjQyNC0yLjY5OSA0MS4zODUtMi43NzM4bDQwNS41Ni0zLjA0ODkiIGNvbG9yPSIjMDAwMDAwIi8+CiAgICA8cGF0aCBkPSJtNDI2LjIyIDMxNC44OWMyLjA2NzUgOS4wNTI3IDEuODQxOCA1MS43MjggNi41MDc5IDc0LjgzNSAxLjY3NDggOC4yOTM0IDguNjc1MSAxNC4wNjYgMTAuMDU1IDE0Ljg1OSA0LjkwMTUgMi44MTQ2IDEwLjgxNSA4LjE0OTggMTMuMDQ2IDE2LjA4OCA2Ljc1NzggMjQuMDQ2IDAuODc5NzIgNjguNDUyIDAuODc5NzIgMTEwLjY5IDAgNi4wOTc4IDEuNjYwMSAzMC4xNDctMi4xNTU5IDMzLjk2My0yLjU0MDggMi41NDA4LTAuMjgxNjMgMTIuOTkxLTMuNDM2OCAxNi4xNDRsLTkuODQ5NCA5Ljg0MzFjLTEwLjM2NyAxMC4zNi0xMS41OSA2LjUyNjEtMTcuNzM4IDE4LjgyMy0zLjU2NzcgNy4xMzU0IDUuNDAyNCAyMC42NzIgNy4zNTQzIDI0LjU3NiAxLjkzMjEgMy44NjQzLTEuODQyMiA0Ljc3NzctMS43OTI0IDcuNDQ2MyAwLjI1Mjg2IDEzLjU0NSAyLjI5NzUgMzczLjkzIDIuMjk3NSAzNzMuOTMiIGNvbG9yPSIjMDAwMDAwIi8+CiAgICA8cGF0aCBkPSJtMzY1LjI0IDUxOS43OCA0LjExNiA1MDIuMTUiIGNvbG9yPSIjMDAwMDAwIi8+CiAgICA8cGF0aCBkPSJtMTE2LjUzIDUwNC4xOSAzLjg4MDYgMzEwLjk2IiBjb2xvcj0iIzAwMDAwMCIvPgogICAgPHBhdGggZD0ibTMxNy42OCA1NzYuNDkgMTMwLjE5IDEuNTI0NGM0LjUxMDggMy4yNDE3IDIwLjM0NSA3Ljk2ODUgMjcuNzQ1IDQuMjY4NCAzLjE1NTUtMS41Nzc3IDkuNDE5LTUuMzg4MiAxNC4wMjUtMy45NjM2IDQuMjY3IDEuMzE5OCA2LjAxNjkgMy4xMTYzIDEwLjM2NiAzLjA0ODkgMTAuMzA0LTAuMTU5NzUgMjAuMjEyIDAuMzg3NDEgMzAuNDg5IDAuMzA0ODkgMTc3Ljg5LTEuNDI4MyAzNTYuNTktMi4xMzI1IDUzNC43Ny0zLjA0ODkiIGNvbG9yPSIjMDAwMDAwIi8+CiAgICA8cGF0aCBkPSJtNDc1LjMxIDU4Mi44OWMtMy40NDQyIDExLjM1MS0yLjEwMzQgMTIuNDM0IDMuNjU4NiAyMS4wMzcgMy43OTQ0IDUuNjY1NiA1MC44NjMgMTMuMDM4IDQxLjQ2NSAyNy4xMzUtMTAuNTM3IDE1LjgwNS0yMi44OTctNS40Nzc3LTMzLjg0My0xLjgyOTMtNS40NTI0IDEuODE3NC03LjM0OSA1LjQ1NjMtMy42NTg3IDkuMTQ2NiAyLjgwNjggMi44MDY4IDQuMDQ4IDEuODA0IDYuNTIwMyA1LjEwMDQiIGNvbG9yPSIjMDAwMDAwIi8+CiAgICA8cGF0aCBkPSJtNDMyLjAxIDYzNi44NWM4LjMxOSAxMy4xMSAxOC44NDYgMTQuNjM1IDM1LjY3MiAxNC42MzUgMi45Mzg2IDAgNy44Ny0wLjkzMzcxIDEwLjY3MSAwIDExLjM1OSAzLjc4NjQgMjcuMTk0IDEwLjI3NiAzNi4yMDIgMjEuMTI5IDguMjggOS45NzY2IDEwLjI1MyAyMy44ODMgNy43MDIgMzcuMTA0LTYuMTY5OSAzMS45OC0xNi43MTQgNTYuOTg5LTE5LjA0NCA4Ni41NjktMS4zNDggMTcuMTE5IDQuNTA5NiAyMi41MzUgMTEuMDcxIDMzLjkyOSAxMC42NyAxOC41MjcgOC43MjQ1IDE0LjIgOC41NzE0IDM0LjI4Ni0wLjEzOTYzIDE4LjMxOSAwIDYwLjI2NCAwIDgwLjcxNCIgY29sb3I9IiMwMDAwMDAiLz4KICAgIDxwYXRoIGQ9Im01MjguNTEgNjU4Ljk2Yy0xMC42ODEgMC45MDQ1NC03LjEwOC01LjYwMjYtMTAuODI0LTguMDc5Ni00Ljc4NDUtMy4xODk3LTEyLjIyNy0xLjI1MS0xNi43NjktNS43OTI5LTAuNjY2MTItMC42NjYxMi04LjgwOTctNC4xMDg4LTEwLjE3NC0yLjc0NC04LjM2NDYgOC4zNjQ2LTMuMDQ4OSAyMC41NTItMy4wNDg5IDMzLjUzOGwzLjAyMiAzMzkuNyIgY29sb3I9IiMwMDAwMDAiLz4KICAgIDxwYXRoIGQ9Im01MTcuOTkgNjUxLjAzYy0wLjIyMTcxLTIuNzAxOCAxLjkwMzUtNS41NjIxIDMuMzUzOC03LjAxMjQgMS43OTk0LTEuNzk5NCA2LjkyMjkgMS4wMDQyIDguODQxOC0wLjkxNDY2IDAuMjg3NjUtMC4yODc2NiAwLjg0MzI5LTExLjE2NCAwLjIyODY2LTEzLjU2OC0yLjA2NDgtOC4wNzQyLTIuMDU4LTI4LjY1Ny0yLjA1OC0zOC43MjF2LTczLjE3MyIgY29sb3I9IiMwMDAwMDAiLz4KICAgIDxwYXRoIGQ9Im01MjguNjYgNjc1LjQyLTAuNDU3MzMtMzEuNTU2IiBjb2xvcj0iIzAwMDAwMCIvPgogICAgPHBhdGggZD0ibTc2Ni4zMiA1NzkuNjQgMC40MzExOCAxMy43OThjMy4xMzY0IDQuNjY5MiAzLjAxODIgOS42MDA3IDMuMDE4MiAxNi4zODV2MTU3LjM4IiBjb2xvcj0iIzAwMDAwMCIvPgogICAgPHBhdGggZD0ibTExMjIuOSA3NjUuOTFjLTIwMi4zMSA0LjY5MDUtNDAzLjc0LTEuMTEzOC02MDUuOTUgMy4zNTM5LTEwLjg2NCAwLjI0MDAyLTMuMzYxNS04LjU4NjMtMjguNTM3LTguNTg2MyIgY29sb3I9IiMwMDAwMDAiLz4KICAgIDxwYXRoIGQ9Im04NjAuMDEgNzM3LjA3cy05Ny40NDggMC44NTgwNi0xNDcuNTcgMC44NTgwNmMtNS4yNjg2IDAtNC41MTU1LTguMzI5OS03LjMwMDktOC4zMjk5LTMuOTc0NCAwLTguNjI5MiAwLjAyMDEtMTAuNTA5IDAuMDM1OS0yLjMzNDggMC4wMTk3LTEuODEwOSA4LjM2Ni00LjE0NTggOC4zNjY5LTQ2LjE2OSAwLjAxODgtMTY3LjQxLTEuMzA4LTE3NS4wNS0xLjMwOC00LjQyOTYgMC04LjU3NjMtNi40Mzk3LTEzLjEzMi02LjQzOTdoLTE0LjM5NSIgY29sb3I9IiMwMDAwMDAiLz4KICAgIDxwYXRoIGQ9Im02NzUuMDEgODMxLjE3LTAuNjA5NzgtNTIxLjc3IiBjb2xvcj0iIzAwMDAwMCIvPgogICAgPHBhdGggZD0ibTc5OS40IDMxMy4wNiAxLjIxOTYgNDk1Ljg3IiBjb2xvcj0iIzAwMDAwMCIvPgogICAgPHBhdGggZD0ibTczNi41OSAzMTIuNDUtMS4yMTk2IDcxNi40OSIgY29sb3I9IiMwMDAwMDAiLz4KICAgIDxwYXRoIGQ9Im01MzAuMDMgNjQzLjQ2IDM5Mi4zNy0zLjAxODIiIGNvbG9yPSIjMDAwMDAwIi8+CiAgICA8cGF0aCBkPSJtODU5LjQ1IDMxNC45IDEuMjkzNSA1MDcuOTgiIGNvbG9yPSIjMDAwMDAwIi8+CiAgIDwvZz4KICAgPHBhdGggZD0ibTkyMS41NCAzMTAuNTkgMS43MjQ3IDUzMS43NSIgY29sb3I9IiMwMDAwMDAiIHN0cm9rZS13aWR0aD0iMXB4Ii8+CiAgIDxnIHN0cm9rZS13aWR0aD0iMXB4Ij4KICAgIDxwYXRoIGQ9Im03MzYuMjkgNDUzLjMxIDE4NS42OC0wLjMwNDg5IiBjb2xvcj0iIzAwMDAwMCIvPgogICAgPHBhdGggZD0ibTEwNjAuOCA1MTQuOTdzLTM2My4yOC01LjYyNjItNTQ0LjY1IDIuNTIxOGMtNC4xNzc4IDAuMTg3NjktMTIuNSAxLjA2NzEtMTIuNSAxLjA2NzEtMS41NzEgMC4xMzQxLTIuMDAwOS0yLjMyNS0yLjU5MTYtMy41MDYyLTAuMDk2Ny0wLjE5MzQzLTcuMDYwOC0xLjkzMzQtNy42MjIyLTEuMzcyLTIuODkzMSAyLjg5MzEtNy42MzE3IDQuMjQ4Ny0xMi4xOTYgNC4xMTZsLTExMi4wNS0zLjI1NzgiIGNvbG9yPSIjMDAwMDAwIi8+CiAgICA8cGF0aCBkPSJtMzk5LjgyIDQ3OS42MSAxMS42NDIgNS42MDUzYzIuOTg0MSAxLjQzNjggNi41Mjg4LTAuNDc3MTIgOS45MTcxLTAuNDMxMThsMTI3LjIgMS43MjQ3IiBjb2xvcj0iIzAwMDAwMCIvPgogICAgPHBhdGggZD0ibTUxOS4yNSA1MTcuMTItMC40MzExOS0yMDguNjkiIGNvbG9yPSIjMDAwMDAwIi8+CiAgICA8cGF0aCBkPSJtNDMyLjkzIDM4OS43MWMxMS4wNDUgMCAzNS41MzMgMC42MTkyNyA0Mi41OC0xLjAwNCA4LjQwNTItMS45MzYyIDcuMDY2LTYuOTUzOCAxNC4xOTctNi45NTM4IDcuODA5NSAwIDYuNTQyOSA4LjA2MjQgMjAuMTQyIDguMDYyNCAxMy45OTEgMCA0NC45NzcgMC4zNzg4NiA2My45NCAwLjM3ODg2IDEyLjA4NCAwIDgyLjAwMyAwLjMwNDg5IDkzLjYwMSAwLjMwNDg5IDguNzYwNSAwIDEzLjE2LTIuMjg4MyAyMS4zNDItNy4wMTI0IDcuMTk1Mi00LjE1NDEgMi4wNTQ2LTkuNDkxNCAyMC40MjgtOC44NDE4IDIzLjE0NSAwLjgxODMzIDEyLjY0MyAxNC4wMjUgMzIuMzE4IDE0LjAyNWgxNTAuOTJjMTQuMzMyIDAtNC4xMTkxLTEzLjExIDI5LjI2OS0xMy40MTUiIGNvbG9yPSIjMDAwMDAwIi8+CiAgIDwvZz4KICA8L2c+CiAgPGcgZmlsbD0iIzAwMDAwMCIgZm9udC1mYW1pbHk9IlZlcmRhbmEiIGxldHRlci1zcGFjaW5nPSIwcHgiIHdvcmQtc3BhY2luZz0iMHB4Ij4KICAgPHRleHQgeD0iNTg4LjY3OTU3IiB5PSI3MzUuODA0NjMiIHN0eWxlPSJsaW5lLWhlaWdodDowJSIgeG1sOnNwYWNlPSJwcmVzZXJ2ZSI+PHRzcGFuIHg9IjU4OC42Nzk1NyIgeT0iNzM1LjgwNDYzIiBmb250LXNpemU9IjkuNjU4NHB4IiBzdHlsZT0ibGluZS1oZWlnaHQ6MS4yNSI+TGluY29sbjwvdHNwYW4+PC90ZXh0PgogICA8dGV4dCB4PSI2ODYuMzk4NSIgeT0iNzY1LjYyODQyIiBzdHlsZT0ibGluZS1oZWlnaHQ6MCUiIHhtbDpzcGFjZT0icHJlc2VydmUiPjx0c3BhbiB4PSI2ODYuMzk4NSIgeT0iNzY1LjYyODQyIiBmb250LXNpemU9IjkuNjU4NHB4IiBzdHlsZT0ibGluZS1oZWlnaHQ6MS4yNSI+SGFycnk8L3RzcGFuPjwvdGV4dD4KICAgPHRleHQgdHJhbnNmb3JtPSJyb3RhdGUoOTApIiB4PSI3MDkuODcxODMiIHk9Ii04MDIuMzc3MzgiIHN0eWxlPSJsaW5lLWhlaWdodDowJSIgeG1sOnNwYWNlPSJwcmVzZXJ2ZSI+PHRzcGFuIHg9IjcwOS44NzE4MyIgeT0iLTgwMi4zNzczOCIgZm9udC1zaXplPSI5LjY1ODRweCIgc3R5bGU9ImxpbmUtaGVpZ2h0OjEuMjUiPldvb2RsYXduPC90c3Bhbj48L3RleHQ+CiAgIDx0ZXh0IHRyYW5zZm9ybT0icm90YXRlKDkwKSIgeD0iNTYyLjExOTI2IiB5PSItNzcxLjk2ODE0IiBzdHlsZT0ibGluZS1oZWlnaHQ6MCUiIHhtbDpzcGFjZT0icHJlc2VydmUiPjx0c3BhbiB4PSI1NjIuMTE5MjYiIHk9Ii03NzEuOTY4MTQiIGZvbnQtc2l6ZT0iOS42NTg0cHgiIHN0eWxlPSJsaW5lLWhlaWdodDoxLjI1Ij5FZGdlbW9vcjwvdHNwYW4+PC90ZXh0PgogICA8dGV4dCB0cmFuc2Zvcm09InJvdGF0ZSg5MCkiIHg9IjU5OC4zMDQ4NyIgeT0iLTczOC4zNjY0NiIgc3R5bGU9ImxpbmUtaGVpZ2h0OjAlIiB4bWw6c3BhY2U9InByZXNlcnZlIj48dHNwYW4geD0iNTk4LjMwNDg3IiB5PSItNzM4LjM2NjQ2IiBmb250LXNpemU9IjkuNjU4NHB4IiBzdHlsZT0ibGluZS1oZWlnaHQ6MS4yNSI+T2xpdmVyPC90c3Bhbj48L3RleHQ+CiAgIDx0ZXh0IHRyYW5zZm9ybT0icm90YXRlKDkwKSIgeD0iNTkyLjEyMjg2IiB5PSItNjc3LjIwMzk4IiBzdHlsZT0ibGluZS1oZWlnaHQ6MCUiIHhtbDpzcGFjZT0icHJlc2VydmUiPjx0c3BhbiB4PSI1OTIuMTIyODYiIHk9Ii02NzcuMjAzOTgiIGZvbnQtc2l6ZT0iOS42NTg0cHgiIHN0eWxlPSJsaW5lLWhlaWdodDoxLjI1Ij5IaWxsc2lkZTwvdHNwYW4+PC90ZXh0PgogICA8dGV4dCB0cmFuc2Zvcm09InJvdGF0ZSg5MCkiIHg9IjU5Ny4zMjcwOSIgeT0iLTg2Mi42MTQwNyIgc3R5bGU9ImxpbmUtaGVpZ2h0OjAlIiB4bWw6c3BhY2U9InByZXNlcnZlIj48dHNwYW4geD0iNTk3LjMyNzA5IiB5PSItODYyLjYxNDA3IiBmb250LXNpemU9IjkuNjU4NHB4IiBzdHlsZT0ibGluZS1oZWlnaHQ6MS4yNSI+Um9jazwvdHNwYW4+PC90ZXh0PgogICA8dGV4dCB0cmFuc2Zvcm09InJvdGF0ZSg5MCkiIHg9IjU4Ny4zNzAxOCIgeT0iLTkyNi4xMzY2IiBzdHlsZT0ibGluZS1oZWlnaHQ6MCUiIHhtbDpzcGFjZT0icHJlc2VydmUiPjx0c3BhbiB4PSI1ODcuMzcwMTgiIHk9Ii05MjYuMTM2NiIgZm9udC1zaXplPSI5LjY1ODRweCIgc3R5bGU9ImxpbmUtaGVpZ2h0OjEuMjUiPldlYmI8L3RzcGFuPjwvdGV4dD4KICAgPHRleHQgeD0iODcxLjE2MTAxIiB5PSI2MzcuNTc1MiIgc3R5bGU9ImxpbmUtaGVpZ2h0OjAlIiB4bWw6c3BhY2U9InByZXNlcnZlIj48dHNwYW4geD0iODcxLjE2MTAxIiB5PSI2MzcuNTc1MiIgZm9udC1zaXplPSI5LjY1ODRweCIgc3R5bGU9ImxpbmUtaGVpZ2h0OjEuMjUiPkNlbnRyYWw8L3RzcGFuPjwvdGV4dD4KICAgPHRleHQgeD0iODczLjgzMjI4IiB5PSI1NzcuMDMyNDciIHN0eWxlPSJsaW5lLWhlaWdodDowJSIgeG1sOnNwYWNlPSJwcmVzZXJ2ZSI+PHRzcGFuIHg9Ijg3My44MzIyOCIgeT0iNTc3LjAzMjQ3IiBmb250LXNpemU9IjkuNjU4NHB4IiBzdHlsZT0ibGluZS1oZWlnaHQ6MS4yNSI+MTN0aDwvdHNwYW4+PC90ZXh0PgogICA8dGV4dCB4PSI4NzUuOTY2NDkiIHk9IjUxMC4yNjE4MSIgc3R5bGU9ImxpbmUtaGVpZ2h0OjAlIiB4bWw6c3BhY2U9InByZXNlcnZlIj48dHNwYW4geD0iODc1Ljk2NjQ5IiB5PSI1MTAuMjYxODEiIGZvbnQtc2l6ZT0iOS42NTg0cHgiIHN0eWxlPSJsaW5lLWhlaWdodDoxLjI1Ij4yMXN0PC90c3Bhbj48L3RleHQ+CiAgIDx0ZXh0IHg9Ijg4MS4zMTY1OSIgeT0iNDUwLjE5ODc2IiBzdHlsZT0ibGluZS1oZWlnaHQ6MCUiIHhtbDpzcGFjZT0icHJlc2VydmUiPjx0c3BhbiB4PSI4ODEuMzE2NTkiIHk9IjQ1MC4xOTg3NiIgZm9udC1zaXplPSI5LjY1ODRweCIgc3R5bGU9ImxpbmUtaGVpZ2h0OjEuMjUiPjI5dGg8L3RzcGFuPjwvdGV4dD4KICAgPHRleHQgeD0iNjE1Ljc5MjQ4IiB5PSIzODcuNzQ3MTYiIHN0eWxlPSJsaW5lLWhlaWdodDowJSIgeG1sOnNwYWNlPSJwcmVzZXJ2ZSI+PHRzcGFuIHg9IjYxNS43OTI0OCIgeT0iMzg3Ljc0NzE2IiBmb250LXNpemU9IjkuNjU4NHB4IiBzdHlsZT0ibGluZS1oZWlnaHQ6MS4yNSI+Mzd0aDwvdHNwYW4+PC90ZXh0PgogICA8dGV4dCB4PSI0ODQuNjkwMzciIHk9IjQ4MS42NTI4NiIgc3R5bGU9ImxpbmUtaGVpZ2h0OjAlIiB4bWw6c3BhY2U9InByZXNlcnZlIj48dHNwYW4geD0iNDg0LjY5MDM3IiB5PSI0ODEuNjUyODYiIGZvbnQtc2l6ZT0iOS42NTg0cHgiIHN0eWxlPSJsaW5lLWhlaWdodDoxLjI1Ij4yNXRoPC90c3Bhbj48L3RleHQ+CiAgIDx0ZXh0IHg9IjU2My4wNDY3NSIgeT0iNTEzLjM2MTMzIiBzdHlsZT0ibGluZS1oZWlnaHQ6MCUiIHhtbDpzcGFjZT0icHJlc2VydmUiPjx0c3BhbiB4PSI1NjMuMDQ2NzUiIHk9IjUxMy4zNjEzMyIgZm9udC1zaXplPSI5LjY1ODRweCIgc3R5bGU9ImxpbmUtaGVpZ2h0OjEuMjUiPjIxc3Q8L3RzcGFuPjwvdGV4dD4KICAgPHRleHQgeD0iNTY1Ljk3MTUiIHk9IjU3Ny44OTQ4NCIgc3R5bGU9ImxpbmUtaGVpZ2h0OjAlIiB4bWw6c3BhY2U9InByZXNlcnZlIj48dHNwYW4geD0iNTY1Ljk3MTUiIHk9IjU3Ny44OTQ4NCIgZm9udC1zaXplPSI5LjY1ODRweCIgc3R5bGU9ImxpbmUtaGVpZ2h0OjEuMjUiPjEzdGg8L3RzcGFuPjwvdGV4dD4KICAgPHRleHQgdHJhbnNmb3JtPSJyb3RhdGUoOTApIiB4PSI0MzMuNTgwNzUiIHk9Ii00NjAuNzMzMTIiIHN0eWxlPSJsaW5lLWhlaWdodDowJSIgeG1sOnNwYWNlPSJwcmVzZXJ2ZSI+PHRzcGFuIHg9IjQzMy41ODA3NSIgeT0iLTQ2MC43MzMxMiIgZm9udC1zaXplPSI5LjY1ODRweCIgc3R5bGU9ImxpbmUtaGVpZ2h0OjEuMjUiPkFtaWRvbjwvdHNwYW4+PC90ZXh0PgogICA8dGV4dCB0cmFuc2Zvcm09InJvdGF0ZSg5MCkiIHg9IjQwNS41MzA5OCIgeT0iLTUyMy41NDAxNiIgc3R5bGU9ImxpbmUtaGVpZ2h0OjAlIiB4bWw6c3BhY2U9InByZXNlcnZlIj48dHNwYW4geD0iNDA1LjUzMDk4IiB5PSItNTIzLjU0MDE2IiBmb250LXNpemU9IjkuNjU4NHB4IiBzdHlsZT0ibGluZS1oZWlnaHQ6MS4yNSI+QXJrYW5zYXM8L3RzcGFuPjwvdGV4dD4KICAgPHRleHQgdHJhbnNmb3JtPSJyb3RhdGUoOTApIiB4PSI3NDUuNDg0NjIiIHk9Ii0zNzIuNTg1OTQiIHN0eWxlPSJsaW5lLWhlaWdodDowJSIgeG1sOnNwYWNlPSJwcmVzZXJ2ZSI+PHRzcGFuIHg9Ijc0NS40ODQ2MiIgeT0iLTM3Mi41ODU5NCIgZm9udC1zaXplPSI5LjY1ODRweCIgc3R5bGU9ImxpbmUtaGVpZ2h0OjEuMjUiPldlc3Q8L3RzcGFuPjwvdGV4dD4KICAgPHRleHQgdHJhbnNmb3JtPSJyb3RhdGUoOTApIiB4PSI1OTYuNzI4MzMiIHk9Ii01MzEuMjU5MjgiIHN0eWxlPSJsaW5lLWhlaWdodDowJSIgeG1sOnNwYWNlPSJwcmVzZXJ2ZSI+PHRzcGFuIHg9IjU5Ni43MjgzMyIgeT0iLTUzMS4yNTkyOCIgZm9udC1zaXplPSI5LjY1ODRweCIgc3R5bGU9ImxpbmUtaGVpZ2h0OjEuMjUiPldhY288L3RzcGFuPjwvdGV4dD4KICAgPHRleHQgdHJhbnNmb3JtPSJyb3RhdGUoOTApIiB4PSI1OTUuNDM0ODEiIHk9Ii0xMjIuNTAyOTUiIHN0eWxlPSJsaW5lLWhlaWdodDowJSIgeG1sOnNwYWNlPSJwcmVzZXJ2ZSI+PHRzcGFuIHg9IjU5NS40MzQ4MSIgeT0iLTEyMi41MDI5NSIgZm9udC1zaXplPSI5LjY1ODRweCIgc3R5bGU9ImxpbmUtaGVpZ2h0OjEuMjUiPk1hemllPC90c3Bhbj48L3RleHQ+CiAgIDx0ZXh0IHRyYW5zZm9ybT0icm90YXRlKDQ1KSIgeD0iNjk1Ljc3Mjk1IiB5PSIxNjIuMDY4NzciIHN0eWxlPSJsaW5lLWhlaWdodDowJSIgeG1sOnNwYWNlPSJwcmVzZXJ2ZSI+PHRzcGFuIHg9IjY5NS43NzI5NSIgeT0iMTYyLjA2ODc3IiBmb250LXNpemU9IjkuNjU4NHB4IiBzdHlsZT0ibGluZS1oZWlnaHQ6MS4yNSI+Wm9vPC90c3Bhbj48L3RleHQ+CiAgIDx0ZXh0IHg9IjI0MC41ODk5NyIgeT0iNTc0LjQ0NTQzIiBzdHlsZT0ibGluZS1oZWlnaHQ6MCUiIHhtbDpzcGFjZT0icHJlc2VydmUiPjx0c3BhbiB4PSIyNDAuNTg5OTciIHk9IjU3NC40NDU0MyIgZm9udC1zaXplPSI5LjY1ODRweCIgc3R5bGU9ImxpbmUtaGVpZ2h0OjEuMjUiPjEzdGg8L3RzcGFuPjwvdGV4dD4KICAgPHRleHQgeD0iMjA2LjAzMTc1IiB5PSI1MTEuNjM2NjMiIHN0eWxlPSJsaW5lLWhlaWdodDowJSIgeG1sOnNwYWNlPSJwcmVzZXJ2ZSI+PHRzcGFuIHg9IjIwNi4wMzE3NSIgeT0iNTExLjYzNjYzIiBmb250LXNpemU9IjkuNjU4NHB4IiBzdHlsZT0ibGluZS1oZWlnaHQ6MS4yNSI+MjFzdDwvdHNwYW4+PC90ZXh0PgogICA8dGV4dCB0cmFuc2Zvcm09InJvdGF0ZSg5MCkiIHg9IjYyMC40NDMxMiIgeT0iLTUwNi42ODIxOSIgc3R5bGU9ImxpbmUtaGVpZ2h0OjAlIiB4bWw6c3BhY2U9InByZXNlcnZlIj48dHNwYW4geD0iNjIwLjQ0MzEyIiB5PSItNTA2LjY4MjE5IiBmb250LXNpemU9IjkuNjU4NHB4IiBzdHlsZT0ibGluZS1oZWlnaHQ6MS4yNSI+TmltczwvdHNwYW4+PC90ZXh0PgogICA8dGV4dCB4PSIzNzAuMjE2ODYiIHk9IjY5OC44NDAwOSIgc3R5bGU9ImxpbmUtaGVpZ2h0OjAlIiB4bWw6c3BhY2U9InByZXNlcnZlIj48dHNwYW4geD0iMzcwLjIxNjg2IiB5PSI2OTguODQwMDkiIGZvbnQtc2l6ZT0iOS42NTg0cHgiIHN0eWxlPSJsaW5lLWhlaWdodDoxLjI1Ij5NYXBsZTwvdHNwYW4+PC90ZXh0PgogICA8dGV4dCB4PSIzODQuMDg0MiIgeT0iNjgwLjg1MTM4IiBzdHlsZT0ibGluZS1oZWlnaHQ6MCUiIHhtbDpzcGFjZT0icHJlc2VydmUiPjx0c3BhbiB4PSIzODQuMDg0MiIgeT0iNjgwLjg1MTM4IiBmb250LXNpemU9IjkuNjU4NHB4IiBzdHlsZT0ibGluZS1oZWlnaHQ6MS4yNSI+RG91Z2xhczwvdHNwYW4+PC90ZXh0PgogIDwvZz4KICA8cGF0aCBkPSJtMzY3LjkxIDEwMTBoMjYzLjAyIiBjb2xvcj0iIzAwMDAwMCIgZmlsbD0ibm9uZSIgc3Ryb2tlPSIjMzM2IiBzdHJva2Utd2lkdGg9IjFweCIvPgogIDxnIGZpbGw9IiMwMDAwMDAiIGZvbnQtZmFtaWx5PSJWZXJkYW5hIiBsZXR0ZXItc3BhY2luZz0iMHB4IiB3b3JkLXNwYWNpbmc9IjBweCI+CiAgIDx0ZXh0IHRyYW5zZm9ybT0icm90YXRlKDkwKSIgeD0iNzM2LjI2NzQ2IiB5PSItNDMzLjEzNzc2IiBzdHlsZT0ibGluZS1oZWlnaHQ6MCUiIHhtbDpzcGFjZT0icHJlc2VydmUiPjx0c3BhbiB4PSI3MzYuMjY3NDYiIHk9Ii00MzMuMTM3NzYiIGZvbnQtc2l6ZT0iOS42NTg0cHgiIHN0eWxlPSJsaW5lLWhlaWdodDoxLjI1Ij5NZXJpZGlhbjwvdHNwYW4+PC90ZXh0PgogICA8dGV4dCB4PSI1NzIuODMyMTUiIHk9IjY0MC4yMDUyNiIgc3R5bGU9ImxpbmUtaGVpZ2h0OjAlIiB4bWw6c3BhY2U9InByZXNlcnZlIj48dHNwYW4geD0iNTcyLjgzMjE1IiB5PSI2NDAuMjA1MjYiIGZvbnQtc2l6ZT0iOS42NTg0cHgiIHN0eWxlPSJsaW5lLWhlaWdodDoxLjI1Ij5DZW50cmFsPC90c3Bhbj48L3RleHQ+CiAgIDx0ZXh0IHg9IjU3NS4wODk2NiIgeT0iNjcwLjkwMzUiIHN0eWxlPSJsaW5lLWhlaWdodDowJSIgeG1sOnNwYWNlPSJwcmVzZXJ2ZSI+PHRzcGFuIHg9IjU3NS4wODk2NiIgeT0iNjcwLjkwMzUiIGZvbnQtc2l6ZT0iOS42NTg0cHgiIHN0eWxlPSJsaW5lLWhlaWdodDoxLjI1Ij5Eb3VnbGFzPC90c3Bhbj48L3RleHQ+CiAgIDx0ZXh0IHg9IjQ5OS40ODk2MiIgeT0iMTAwOC42MDY5IiBzdHlsZT0ibGluZS1oZWlnaHQ6MCUiIHhtbDpzcGFjZT0icHJlc2VydmUiPjx0c3BhbiB4PSI0OTkuNDg5NjIiIHk9IjEwMDguNjA2OSIgZm9udC1zaXplPSI5LjY1ODRweCIgc3R5bGU9ImxpbmUtaGVpZ2h0OjEuMjUiPjQ3dGg8L3RzcGFuPjwvdGV4dD4KICAgPHRleHQgeD0iMjE2LjY0NTQzIiB5PSI3MjUuOTgyOTciIHN0eWxlPSJsaW5lLWhlaWdodDowJSIgeG1sOnNwYWNlPSJwcmVzZXJ2ZSI+PHRzcGFuIHg9IjIxNi42NDU0MyIgeT0iNzI1Ljk4Mjk3IiBmb250LXNpemU9IjkuNjU4NHB4IiBzdHlsZT0ibGluZS1oZWlnaHQ6MS4yNSI+S2VsbG9nZzwvdHNwYW4+PC90ZXh0PgogICA8dGV4dCB0cmFuc2Zvcm09InJvdGF0ZSg5MCkiIHg9Ijc3NC44NzU2MSIgeT0iLTUwOC4xODk3MyIgc3R5bGU9ImxpbmUtaGVpZ2h0OjAlIiB4bWw6c3BhY2U9InByZXNlcnZlIj48dHNwYW4geD0iNzc0Ljg3NTYxIiB5PSItNTA4LjE4OTczIiBmb250LXNpemU9IjkuNjU4NHB4IiBzdHlsZT0ibGluZS1oZWlnaHQ6MS4yNSI+TWNDbGVhbjwvdHNwYW4+PC90ZXh0PgogIDwvZz4KICA8cGF0aCB0cmFuc2Zvcm09InRyYW5zbGF0ZSgwIDI4Ny4zNikiIGQ9Im0zNjQuMTYgNjU4LjQzIDI5OS41MS0xLjAxMDJjNi40OTg3LTAuMDIxOSA2Ljk3NzIgOS4yNTQxIDE2LjU5NiA5LjM5MjUgMTIuMDU0IDAuMTczMzkgMjkuMTExLTAuNTM1NzIgNTQuMTE0LTAuMzAxMSIgY29sb3I9IiMwMDAwMDAiIGZpbGw9Im5vbmUiIHN0cm9rZT0iIzMzNiIgc3Ryb2tlLXdpZHRoPSIxcHgiLz4KICA8dGV4dCB4PSIzNzMuOTkzMDQiIHk9Ijk0NC4zNTc1NCIgZmlsbD0iIzAwMDAwMCIgZm9udC1mYW1pbHk9IlZlcmRhbmEiIGxldHRlci1zcGFjaW5nPSIwcHgiIHdvcmQtc3BhY2luZz0iMHB4IiBzdHlsZT0ibGluZS1oZWlnaHQ6MCUiIHhtbDpzcGFjZT0icHJlc2VydmUiPjx0c3BhbiB4PSIzNzMuOTkzMDQiIHk9Ijk0NC4zNTc1NCIgZm9udC1zaXplPSI5LjY1ODRweCIgc3R5bGU9ImxpbmUtaGVpZ2h0OjEuMjUiPk1hY0FydGh1cjwvdHNwYW4+PC90ZXh0PgogIDx0ZXh0IHRyYW5zZm9ybT0icm90YXRlKDkwKSIgeD0iNzgwLjg0NjA3IiB5PSItNDkwLjI0NTk3IiBmaWxsPSIjMDAwMDAwIiBmb250LWZhbWlseT0iVmVyZGFuYSIgbGV0dGVyLXNwYWNpbmc9IjBweCIgd29yZC1zcGFjaW5nPSIwcHgiIHN0eWxlPSJsaW5lLWhlaWdodDowJSIgeG1sOnNwYWNlPSJwcmVzZXJ2ZSI+PHRzcGFuIHg9Ijc4MC44NDYwNyIgeT0iLTQ5MC4yNDU5NyIgZm9udC1zaXplPSI5LjY1ODRweCIgc3R5bGU9ImxpbmUtaGVpZ2h0OjEuMjUiPlNlbmVjYTwvdHNwYW4+PC90ZXh0PgogIDxwYXRoIHRyYW5zZm9ybT0idHJhbnNsYXRlKDAgMjg3LjM2KSIgZD0ibTM2Ny43IDUzNy4yMSAxNDEuMjgtMS4wMTAyYzYuNDktMC4wNDY0IDEyLjc4MSA3LjIzNTQgMTkuMTkzIDcuMzIzNiA1NS45MjQgMC43Njg5IDE1OC42OS0wLjE3MzMzIDIzNi41MS0xLjAxMDIgNy44Mzk2LTAuMDg0MyAyMi42MzEtMTkuODU0IDMwLjMwNS0yMC40NTYgMjIuMjY2LTEuMzUxOCA0NS4xNzktMC41MDUwNyA2Ny42OC0wLjUwNTA3IDE2LjE0Ny0wLjYzMjQxIDMuNjEwMiAyMC43MDggMjYuNzY5IDIwLjcwOGwyNDMuNDUtMS4wMTAyIiBjb2xvcj0iIzAwMDAwMCIgZmlsbD0ibm9uZSIgc3Ryb2tlPSIjMzM2IiBzdHJva2Utd2lkdGg9IjFweCIvPgogIDx0ZXh0IHg9IjY4NS4yMDgxMyIgeT0iODI3LjUzMDgyIiBmaWxsPSIjMDAwMDAwIiBmb250LWZhbWlseT0iVmVyZGFuYSIgbGV0dGVyLXNwYWNpbmc9IjBweCIgd29yZC1zcGFjaW5nPSIwcHgiIHN0eWxlPSJsaW5lLWhlaWdodDowJSIgeG1sOnNwYWNlPSJwcmVzZXJ2ZSI+PHRzcGFuIHg9IjY4NS4yMDgxMyIgeT0iODI3LjUzMDgyIiBmb250LXNpemU9IjkuNjU4NHB4IiBzdHlsZT0ibGluZS1oZWlnaHQ6MS4yNSI+UGF3bmVlPC90c3Bhbj48L3RleHQ+CiAgPHBhdGggdHJhbnNmb3JtPSJ0cmFuc2xhdGUoMCAyODcuMzYpIiBkPSJtNTU0LjI5IDcyMS40My00LjI4NTctMTc4LjIxLTIuODU3MS00NDAuNzEtMC4zNTcxNC03OS4yODYiIGNvbG9yPSIjMDAwMDAwIiBmaWxsPSJub25lIiBzdHJva2U9IiMzMzYiIHN0cm9rZS13aWR0aD0iMXB4Ii8+CiAgPHRleHQgdHJhbnNmb3JtPSJyb3RhdGUoOTApIiB4PSI1MjkuNjI1MzEiIHk9Ii01NTAuODQ3NzgiIGZpbGw9IiMwMDAwMDAiIGZvbnQtZmFtaWx5PSJWZXJkYW5hIiBsZXR0ZXItc3BhY2luZz0iMHB4IiB3b3JkLXNwYWNpbmc9IjBweCIgc3R5bGU9ImxpbmUtaGVpZ2h0OjAlIiB4bWw6c3BhY2U9InByZXNlcnZlIj48dHNwYW4geD0iNTI5LjYyNTMxIiB5PSItNTUwLjg0Nzc4IiBmb250LXNpemU9IjkuNjU4NHB4IiBzdHlsZT0ibGluZS1oZWlnaHQ6MS4yNSI+QnJvYWR3YXk8L3RzcGFuPjwvdGV4dD4KIDwvZz4KPC9zdmc+Cg==", + "public": true + }, + { + "link": "/api/images/system/map_marker_image_0.png", + "title": "Map marker image 0", + "type": "IMAGE", + "subType": "IMAGE", + "fileName": "map_marker_image_0.png", + "publicResourceKey": "CdCrVxsjA4EAiFaXK4a7K2MZFMeEuGeD", + "mediaType": "image/png", + "data": "iVBORw0KGgoAAAANSUhEUgAAAB4AAAB/CAYAAAD4mHJdAAAABHNCSVQICAgIfAhkiAAAAAlwSFlzAAACWAAAAlgB7MGOJQAAABl0RVh0U29mdHdhcmUAd3d3Lmlua3NjYXBlLm9yZ5vuPBoAAAwgSURBVGiB7Zt5cBT3lce/v18fc89oRoPEIRBCHIUxp2ywCAgIxLExvoidZIFNxXE2VXHirIO3aqtSseM43qpNeZfYKecox3bhpJykYgdjDkU2mBAB5vCamMNYAgQyURBCoxnNPd39O/aP7hGSEUR24L/uqqf+zfR77/Pe69/Rv6kWwcgPLRIJfZUAa7xez2xd90QBwDSNZKlkHJHAK+l09mUA7BP4vPpRUVExMVoRef+L998njxx9X57vPi/PnTsnO850yPaT7XLXrrflqjtWymhF+HA0Gp0wEp/kHymEQqG4ptJDGzf+um5RUxMSiV7Z3Lyt88L5nozgHJWj4pGmpqZav99PWve04onHHuswmViQzWb7ruZX+Udgv8/z3A+f/NGye1evxssvb+wo5PMfTZs6bfqcuXNHL7hlweh58+ZVAOTUpk2b0p9dvjyqqmrs/b8ejpUMc+unzjgUCsXjsYruE+2n1JY/NedM0zCi0VjA7/d7/f4AAgE//H4/vF4fOjvP9h5695C/oaEhcN/q1SyTzVdnMpnklXzTq4EplUsXfmaRCgC7du3cOn78+KfGj59Add3z1Md1vV7vqPa2D1sA4MYbZ6qUiqVX9X21i4TQcfX19QCA6urquN/vn0kAPRQKpYbTnzRpUhgAampqAEFrPjVYSql7fD4AgK5r2tV0AcDj8WkAoOk6JJGeTw2+nocLdsEu2AW7YBfsgl2wC3bBLtgFu2AX7IJdsAt2wS7YBbtgF+yCXbALdsEu2AW7YBfsgl2wC76mh/ppjIQgXVloPxVSBRV0rBe455P6+kTKBYF3tonxY/IWarry7DvI298Tgp0PR9RzACaN1NeIS100+EdvKXW3cMZvF8wCK10Sq2it2NAzakmukP/wmoP/KuId3BRUMg5uCfCSNVSKVn1rNto7Un8jLrUVqJ4Fi2eEQiEYBzOsy3SYL37TNQdzi8Q5FxkqJIQBsNLlYMGF/zqAJWBxSEogDAY+DJibYqTuRg4WFgO3OKhCYTExbKk5G/mbkSPP2DQhLA5IO/NhSz1MMP882BDgnAFQwdiVSs2vPVhYDIJLUMkBgw1favM6lJoZDDAYhKbAYsOX+rqAhcXAuQSIAKzhSy2vS8YmB7NYH4WCfM7kw5VaWtdpOO3bfWZJZVXgPxMX898bVsm6RhkTIseX29yyIErm/J5z5vwr6pvmsLYjBgeDwSpVJS/OmT1n1de+9qANZgLc4q9Dyj2qQhUhSSUAUCL7GBcchCymTEYBYNWqVXj30MGHT586PZEJ+WAul7ts8bjspd9QKDRNU2nz4z94YtI3H3oI+XwB//3j/9m77eRUUJ9/0eh4APGoDz6vCi4ksgUTmYyBC4k8RLGwtzF+EGu+tHqRqqrYtm0rXnzhhQ7G5cpsNnvyiuBIJFKnqvSd55772eilS5fhwIH9ye+/dPaEf1T9otW3T8GtiyYgGNBBymYEgLSbvakidu8/h01vnkYhcab1gcVs5tx5c6PHjh7DU0/9qFsINPb3939UZg28X11dXR0Qwtr9g8efqGtc+Bn89re/O7FhR9BXNaFm+n98uxHTZ1SDKQqKAihweZlITUVtXQwNs8fg+Bmzdk+bnmPdf/7bwsbGeO2ECaED+9/5XCxWuTGbzVpDwJpGNtx+28o77rr7bmzZsu3k7z+cMlHzeiPrvnoTwtVhFAVQHAZY4HBEoiAAeDXUjI/gyJGeQEd6TFj2tHYuXNgYy2azVe0fngiWDLNloHNFo4FZkXDsoTVr1+KD4x8U/3Ci1qP5PV7N74FeFUbClKDEriy57A5JANL5a68hnqoINL8OAPqbXbNp7clTxTVr1/oOHjr0MFXxq2Qy9wEFACnoY//6la9QAHj+9Q/eUL2RWkVXoWgqkhZBypRImkDKBFIWkLIk+h1JWdL+zrmeNCWSDFB0DYquQvWG637TcnozAKxbt45yTr8PAGowGBwVDAbvmT9/Pvbu3dddijV9WdUUUE0BUQm6kwaCYe+ljK/w8ruUdsYCBLlMEUQhoJoCygWM+LIvHTx4sGfevIbqYMD3BSFkJVUUrG5oaFABoPXwhd1UVUBVahtpKtoOnEV/gSHHgBwDso5c6XO6yNF24CNQTbV9qBRUUenuwz1/BoCZM2dplOJeSggWL1myFEII9IeXziIKBVUUW1QKo2Ci41Anei9kkWcY6Ex5R8qfc0wi0ZPF6QNnYeQNB2j7IQpFOtg0WwiBxoWNIBKLVQI6Z8rUqTh69FiWaFNmEIWgLFShoM5TZbIzgVxvFp6ID5rfA6JQgBAIxsGLJkrpAsycAcH4gN1gX0QPTW9vP5Grr58cJJTOpbqmjgWAnp6ei4QSEEJAKAGh1BbHCS2DLAFmMAgmICwObjDnyYMMAtJL9oN89vRc7KWUQtOUsSqhSggA8sWivSEh9qBxTiCEAGRwQARUVaB67Hf5pZAQlA0Ayrq2LTCogVyhlLURNEw55yYABP2+4ED3vHSClBKQ9jiFdHqvEBCMQzAOKYSt6/RqSGnbDPJRbgT93hAAcM4NyhjrBYDKylhswEEZJgYJFxDchnGTwSqasIomuMnsIDiH5GKIzUAQTsCVlZUxB9xLIUVbKpVEff3kiLTMfimEA7HP5bZgHMJ07mnJAiuaYEXT3jcZDMLkTgBD7exgBKRp9NfVTQwnk0kIKduoJGRH8/ZmhMNh4skc3DnEkDlAi4GbtjDDguVAmZM1M6yB68JyKsCGBqD373s7GAySnTt3gBDyFhWCvPHee/8HAJhTU5g0BMg4uMXBTT4AZSUTrGjBKpiwCnablQbDbZuyfTmAuRPMegA4euQopCRbaCaTOd2XSLzX3d2Nu+64bR7PnP3LJSCDMBm4YW9FWcmyQYMytsW+Zpfdsm1MdimAdMc7K29bMedCdzeSyeS76XT6jLNI4PGf/+w5aLqOu25IjOOWKcSg0jJjcLZ2ecsZD5TdybqsOxC0ZYpbJ58frek6nn/+eVBJHgecjXkqk2nu7Ozcdfz4cdx556rJN5C3m8v3jBt2xpdnazjysawNy5lUbKkrbmtZsWL5pGNHj6Or62+7k5lMy5CFNRQKTfN6tAMvvvhSRe3EOqx/4oXXLvia7qO6CsVZrey5154KB5YpKSG5tHs+5/ZsZnEIk6Ei1fLH73373i/09fXi0fWPpgyTLchkMqeGgAEgHA5/vjJWsf2PmzYr1dXV+K8fP7vjLxduWkY8ilpetQZPg+UJxh63lzqlNDi7gTa3fuPraz6bzxXw79/5FutP51am0+kdZdaQ/2kzDKNDUci51179w8pbP3er8sAD6+pnVCWy+/fs21LAqBnlMT50qJXFLq2a2L/5gaVy7N133j69u7sb67/7iFHIFf4tlU6/Ppg1kLGU8hYAywBMeOWV33gfXb9+1Q+ffDL+4Ne/AcYY/tS8PbV5++4Dhy+MopY2ZrLiidQDgDBSp5TS+Y7psS65ZOHsW26++eYosxje2PwGNm586eKzz/x027+sXWsBOAfgbULIQQAgUspaAA8BGAfnsamrq4u0tZ0Q333kkdGmZS3f8JNnlBXLV0AOilRKCS7sWYlxjlKxgHw+j5Y3W/C/Tz/NQ6Hgjp9seKZ31py5ajwe4wAtz9zdAH5OpJTPAqgEgL5USkpu4eLFHloqFXniYh9t3bunauuWrStisSi5//4vYnHTEkyZOhWqokBICcuy0N7ehr2trXjt1VeRzqTl3ffc81bjgsZELF4pQ6EAqa4eI6UEicfj5dhTKoCikynx6Bop5C14dJ2XcjmouipvvGFGoSJaWfr738/7tmzdjl/88pfIZjKwnH2SpmkIhSMYW1ODhvmNGFcztjhudFXR69Wgck58Hg+XEorH5ylDJYA8kVKOckpdB0ADIBOJhOzv70OhUFILuTzPZLNcSE6SfSlvJp0O5A1DN0qGDxLS4/OUAh6PGQqHC5XxeJEQgkgoRH1+L/wBP6LRuIjH4+Uf8gSAUwB+MbhzzQSwCMA0p/QUQADgNJ/PJ/v7+wnnnFiWkJZhKCYzKADoqiZUXeW67iGcSxKPx2QoFAo7AybnuE8COAZgHyHkxGXjeFAQEQCzANQCqAIQBeAH4AXgcex052w45TMcyQHIAOgBcBbAUUJI5uOM/wcaHmf3g9UM7QAAAABJRU5ErkJggg==", + "public": true + }, + { + "link": "/api/images/system/map_marker_image_1.png", + "title": "Map marker image 1", + "type": "IMAGE", + "subType": "IMAGE", + "fileName": "map_marker_image_1.png", + "publicResourceKey": "DF3fuPXua9Vi3o3d9Nz2I1LXDTwEs2Tv", + "mediaType": "image/png", + "data": "iVBORw0KGgoAAAANSUhEUgAAAB4AAAB/CAYAAAD4mHJdAAAABHNCSVQICAgIfAhkiAAAAAlwSFlzAAACWAAAAlgB7MGOJQAAABl0RVh0U29mdHdhcmUAd3d3Lmlua3NjYXBlLm9yZ5vuPBoAAA3vSURBVGiB7Vt7cFzVef+dc+/d90OrJyO/JSO/4ncxxfULMCYIAyEW08amJJgmM4GmnZjJdNq4gcSGzLQxk3bsaWcaaIHyR8CJrWAbpjgG/AhINsbYxkaSDY6xJFvSrrS7Wu3uvfecr3+cu1pbXhkJs/4nujNndufec77f+d7fd+4uw8gvIxwOfocBaz0e91yXyx0BgKyZiWUz5kcEvBKPJ18EYI+C5rWvkpKSyZGS8LGHGtbQR8ePUUdnB50/f57OfnqWWlpbaN++39O99fdQpCR0NBKJTBwJTfZFE4LBYLmh8+YXXvifKctWrEBPTze9+cbu8/3JVMoWNjwer3/ZsuUTvV4P239gP36yceNZW9CtyWQyei262hcB+7zurU/99Ge3r1nTgJdfevFsqr8/Wlc3rWbGzFkV8+fPr1iwYEEJgLadO3cmbr/jjohh6KXHPjxamsmar39pjoPBYHl5aUnnqZY2/b1Dh9LdPd39kUgk6PP5PD6fH36/Dz6fDx6PF+fOfdZ9+pPTgbq6Ou+aBx+0k/0DVYlEIjYcbX4tYM5pxeK/WKIDwM7Gxt0TJox/dtLESXC53JuHzvV4PBVHDjfvAYDZs+fonMsV16R9rYeM8XG1tbUAgMrKsrDP659DRJ5gMNhbaH5NTU0IAMaPHw9IPv5LAxORy+31AgBcLsO41lwAcLu9BgAYLheIkftLAxfzGgMeAx4DHgMeAx4DHgMeAx4DHgMeAx4DHgMeAx4D/lME1ke7gDF8ltbOHe3W923oEwYi1jxftWfZWgAziwacZkd2pfyN96XN5IIu7dMtIKA9/TI+zqCnFps2Alg5UlojFnVqIHZUlO2sl4RyC4CU+SEEylux8Z/iyc7mrxw4U7UnYwvGpXMYKIgNGdwXC/76C48oRw3sDWfnCgIkARJXcpwbvpA1e6T0Rq5jDr8EAHKA6OpjUOJwfeXAJAEhAXAGgEPKq+dIMVJqowDO4RAAC0rHV21u5LijAJaABAOIAY5Oh15iFMgj1zEpcUuuXjpIWeCouxjAtnIZcGKA5AVFbRfazPUC50QrKe8+Qy8qiqjBYIODA5DgBd1pBO9WRg9sy7yOhXBca+icYrgTOUGOiKnIVdCdisAxJGBTPsYW0nHRrJqgfNmGVtiqaeR1xchF7Vgz40q/BUNmISlcL7CUgJAMnOUiVwEdF0PURIAAVHaC8ucbAiwcQAb1KQpwXMjFrhtYMcOVO8lhOB457ujcKZd9hBguSYwcelTupKyaQWKYJFEU4xJw/Dhfcw29ilSBcNjEoTucFnSnkeOOvvTJpcVC1cYoGB5NAGEQTukjMAzHoghJghyWCRjenYoTuZjKx8xJiwU4LrSZ6waWpIoBjTuRqxDHRUkSUMWAJAZp6QU5FqOw65HHapG3bGVcBTZXDI5VnFaFgBL1yC34uoBJqEJeIwD2MMY1ilZidAFEMlDOqm9UdpJ0ZawumI+LU9ArwhyqWxyNz14XsBAMUnLVH0ttGB0XococdCGWE3XhOV85MF1WV2OY3omK0S2SkxgYAZYYJoAUpcqEEjG/Ru80isA1ysMXYNCnCum4aKUPgTu90w3sFinXL6nO/MadCAhiKloxBjFMeSuK0S1Kylv1cE1bUVoYyHwhoI6bCswpjjuxK5u2G2lcti2jzNCRTluioHEVw52EBA5/2LKsLBL+h2gs/o+Fjpa+MqtmjCbkqQJSYFF3T3zRsPMvA75i7UiBA4FApa6z5+fNnbd6/frHADghk7QdlhAHdMY0KXkZAHAuozaRMDRtKYMdAYDVq1fjcHPTD860nZlsS3qsv7+/+6pNDr0RDAanGTrf85Onnq75/uNPIJ1O4+dbnj34Ot6B4eFLqksqUeEvgcflAREhZabR09+Li/EorLQ4eFv317D2oW8t0XUdu3a9jud/9auztqD6ZDLZOixwOByeouv8D1u3brtpxYrb0XS4Kfbj3//8VHC8d0nDLXfj67OWIeQJgDGADfoOAxHQl05i14l92PHBXiTPp/c/OrFh9vwF8yMnjp/A5s2bOqXEbX19fX+8CriqqspvmunDTz/10xkr71qFnY07Tr1i7aqsLg2Vb6h/GOPCpdAYgTPlNLmF5AzpvBRp74viX3a/hO6+ge47+hZG61fVTz9y+DCee27Lx15fYFFHR8cAcNkPuw2DPXfP1+vvvf+BB7Br967WX9Mbk70eCn33zlWoCrsgKAFBCdgy/2nLBCyZgCUSMGUSpkzC0G1MrKzE0XMt/la9I0QnM+cWL15cmkwmK1tOnwpksuabg8YVifjnhEOlj69dtw6nT51Kv2q96fYG4fG7gbJwFhn7cxicIJgEZwAfEiokGASpWG1KhvIwg1/91ti1N9DEJ7ZOzKxdt87T1Nz8A67jv2Kx/o85AJDk//zXjzzCAeA/D7zU6PZjkkuXcBuEjN2OrGiHabfDFB2w7HZYoh3mVaMDWWdu1m6Hy5Bw6RIuP6b87+HXdgDAww8/zIXgGwFADwQCFYFA4BuLFi3CoUN/6LRmyL/y6gSXTtC4QDTVgQo/B5iEJFJ6Rt64lI6Vfi3JYBFHd1JA5wIunUNIQvpr/C+bm5u65s9fWBnwe9dISWVc0/DNhQsX6gDwTuuhd3WNYOSGTjjSehGp7EVYsguWuJQfssu51wVTXIIpLsGWlzBgXsSRM5dg6Hk6uk787Zb39gHA7NlzDM7xoM4Yli5fvgJSSiRmmbP9HNA0Qm4D6axEc6uJ6eOzuCloQuOOjlneqiUx2BK4lDBwut2DTFaHoXFYGilaHEjMMOdKKXHb4tvw/nvvL9UZ+Lyb6+pw/PjxpOZhsziX0DigcYLG1QaEBD69ZKA7wRHx2/C7BDSNwEi9AEmZGmJJA/1Z9SJM12hwvcYBzgmaj89obW3pr62dGmCcz+cuQ68GgEtdl7oYU40CZwSeW+As1rmy5KzNkbY1WILDlOp71ubgnKA7czVO4NyhwQhcFS7o6urq5pzDMLRqnXEtCACpdCrFHOHlAsTgYEq0nCnj0jnBY6i8KCTLBxbmzB2yPkczmU4lAYAxHtKFECYAPeDzBQZD4GU+motMueXklECWc7QkSaVDGoTAVetz8AGfLwQAQoisbtt2N4BJZaVlpZQjkntdS8w5UFOFni0YLMGhWfny1rbVPVuoOVKyK9ZeTrMsUl7qAHdzkPyktzeG2tqbw8KihCQlPjVUl2hLBkswmDZD1mJIWxwDWTXSFkfWUs8sZ64QzlqHjiRA2tQ7ZcqUYCwWgyT6hBNjb+3ZvQehUIi52tje3M6FyHHIYNkOqM2RsTjS2cuAs+pe1uYKPLcBkduA+m60sH1+v5/t3fsWGGP/x6VkjR98cAQAMNc7bXJepAyWzWHaimjW4siYDGmTY8DkGMhqapgcaVM9yw5ugMOyeX4DkmGub1otABz/6DiI2O94IpE4E+3p+aCzsxP333PfAvOi2G8JBtMRbU68GZMj44Ao0BzXmgOsRk7spq1oWILB6rQP3nt3/byLnZ2IxWKH4/H4pxoAeFzuC21tretW3rUKnk5mtWiflzAGxhgDQ66IYyrnOnqzBFfDZjAdLk1HMnkpMWRNLldmFomamtrIL/71F+iPJ/8mnc2e4QDQm0jsOXfu3L6TJ0/ivtX3T607M26P6SzMWI5eB7ktPHLPc/MV5xwTjpe9sfLOu2pOHD+JCxc+fyeWSLyZdzCoWsvjNpqef/6F8KTJU/DDLT/a3jM90eDWCS5dqmDvxF7NCRSAOikQhCuMUXHMEDjm3v7jb/+oIRrtxpMbnuzNmvatiUSi7QpgAAiFQneXlZbs3rGjUauorMSmLc+8dShy7HbDELqeA3bC4GCScHxWSMDOgVuaPb2t+t3vPfK9O1P9A/j7v3vC7ov318fj8bdyWFf8YCSbzZ7VNHb+tVdfrV911ypt/bcfq52J2uTBg+//LhWwZ0nJYTtWf6WrcccDGFgLdn5nwkPVD9Q/MLOzsxNPbvhhNpUc+G5vPL7jcqxBjonozwEsBzD5lVde9jy5YcPqTZufKX90/WOwbRv7330nsffDt08dSB41EkZyHPfwmwBAZuTFsBm48GeuWfai2oUzp02fFjKzJhp3NuLFF/+765e//Pfd31q71gLwGYC3GWNNAMCIaBKAJwBUO3uQnZ2d/MyZNv1vn/j+LUuXLq/Z/MyzCIfDTmxW8Y+IVFyWqjKRQkDYNqKxGDb97GkcOXLk7LZt/9F8c12dqKqqYM4LYALQCWAbI6J/A1AGgKK9vSBhoa8vEe+N9TwejcZYU1MTfrN9O6puqkJDw0NYtnwFpk6dCsZUMrFtG22trTiw/11s3/4aotEo1jQ04NZFt6KsrJTCoZKtJaWRiGG4KBKJ5BJWnw4gDedAx+0yMJCywLnQGWOSMabV1NbikUfX40J7B367sxFbt25DMhGHZZkgAC7DhWAojOpx4zF3wS0YP64aVZUVYCoQSN2la4bhIsNlcOS73H5GRBUAHgcwBYABAD09PZROp1gq2V8WTybq4vH4xEQ8oSWSSfSnUkinM7As9RdUw9Dh9XoR8PsQCgYRCodESTj0x1Aw2OrxBXsDgYBdXl6eM2IB4CyAbZcb12wASwBMB1Dq7C4ACJZIJHstM5PWdC2TTmcom80wEtySAFwupum6wbxeDxeCuT0et8/v94UBTTrSJABRAKcAHGCMnbrKjy/bRBjAHAATAFQ5NuAF4IFqAtyOKzKo83MLgAkgA2AAQB+ADgCfAzjBGIsPxfh/6wbDK7xbMFYAAAAASUVORK5CYII=", + "public": true + }, + { + "link": "/api/images/system/map_marker_image_2.png", + "title": "Map marker image 2", + "type": "IMAGE", + "subType": "IMAGE", + "fileName": "map_marker_image_2.png", + "publicResourceKey": "rz5SFAw2Sg5T2EyXNdwLycoDwf4QbMiZ", + "mediaType": "image/png", + "data": "iVBORw0KGgoAAAANSUhEUgAAAB4AAAB/CAYAAAD4mHJdAAAABHNCSVQICAgIfAhkiAAAAAlwSFlzAAACWAAAAlgB7MGOJQAAABl0RVh0U29mdHdhcmUAd3d3Lmlua3NjYXBlLm9yZ5vuPBoAAAyUSURBVGiB7Zp7kFRVesB/5/S9PdMz/ZoHMwo4MICDuoGVIYICIuzGcn0vC+oWGuNjs8mua9ySP4wpgyaiVVupbHYTsLJmNT7WNXExwqqzrq8g4oNxdXUgyEMQARmZd3fPTE/3vfd8+ePenhlgBsFlrFSqb9Wpvn3vd77f+b7zne87ffsqjv+wE4nYDQqWl5aWfDUcLqkAyOUHunID+Q8EnkilMo8C7gnoPPaRTCYnVyQT71+1bKl80PK+HGw9KPv27ZPde3bLjp075NVXX5FLL7lYKpLx9yoqKuqOR6f6PIFYLFZtW7r54YcfqV+4aBEdHe3ywm+e39eb6etzPZfS0kj5woUX1EUipWrj6xtZedddu11P5mYymc5j6Q19HrgsUrL67r/7+8VLly7j8cce3d3X29vZ0DB9yplnfWXcrFmzxjU2NiaBXevWrUsv/trXKmzbqnz/9+9VDuTyz35hi2OxWHV1ZbJ1245d1ltvvpFtb293Kyoq7LKystKysnLKy8soKyujtDTCxx/vSW3fsT3c0NAQWbpkiZvp7a9Np9Ndo+nWxwJrLYvmzV9gAaxbt/75urrxd592Wp0Oh0tWHSkbiUQSv3unuQlgxoyZltZm0TF1H+umUnrC1KlTAaipqUpESmMzFIRjsVj3SPJTpkyJA0ycOBGMnviFwSISLolEAAiHbftYsgAlJREbwA6HESUlXxg8lkcRXAQXwUVwEVwEF8FFcBH8/xhsnZC0ksw49eQPI5mmNtP54ccAIvqgqbz4aYn8zYoTUXXcFnueyZ8eXtleZt75iQnpU0VUvYiqB5mvu5p+XH9w8RtgnJMOLut/7rd4+fpRBcS52hz65csnHdxQ8clZnyuT3NV40sHRUnfq58mUWFJ70sEn+yiCi+AiuAgugovgIrgILoKL4CK4CC6Ci+D/Q+Djf/higk8Jzs0IMjIGYDGAp0AUeBbiHf3Xs/HGAHyYlYaRX0EYC4txNeIFugvWHyXzua8cnDjYGMBoQIFhRFfLmLjaCxqAw8iuHing/nCwGlLuMrKrveNfnccPFnyLtQ8c0a1jElye8sGFAYwUSCN54Q8GB4ljKKpHkBmLOZbB4FLgjhLVYxNcDFnkMXJUj03m0kOKR0sgYzLHRvlwpcDYI7oaGYvl5HB4ZRrJ1cf9fP5E/5NwQUKM7uoTOI4/ql38kmgUOCMnEHMCL819sag2jJJAxgIs+HNY6PGlpUxXDQWXw5dXjxH8SFZBPf7SyqKrMQLKG7b/OkpmTBJI0BSjbwTGYo6Ni5+ZjMJDj1wkxmQ5iV+VsBh9BzImKbNQFhWjp8wx21c7dKIV9A94IxaJsdplZt9574JQVcUdpr3rzlEHdzLASslpg19EofLMMa3dc0Z9c9YMXT+s7/GCo9FojWWph87+6tmX3XTTzT7XA/F4xutXr4fyOuQZVQUQ0tLphY1nlcn5YqgAuOyyy3inefOtH+36aLJr5Obe3t72o4w68kIsFptuW7pp5d33TPne928hm83yLz+6b9PVb/4niRK9QNfUoquqUaUREEEG+jGd7Zi2Dnpy3qYHGr7OFdcsX2BZFs899ywP/fznu11PLslkMjtHBScSiXrL0m+uXr3mlEWLFrN58+auxD+u2HZWhb0gcvkyShZ/Ax2N+70KPcVvJpMm999NZJ99mi1dzsb3rviLGbNmz6rY0rKFVavubTWG83p6ej4psAbfr66trS03xtlw98p76s+bN5+nnvzFtouevK/s1AnJM+I/vB37j6aDziJeCtxhzUkhTgoYwJpchz3zbJI7fj/pzA829f6iR/bPPW9e9aS6utjbb715YWVl1SOZTMY5DGzb6scXf+OSS6+48kqanntu55+99shkOyLx8uuvIjSuDEzq6Ob5TdzgPJ9GhT2sCbV4W1vK57R+FP9lOrT33PnzKjOZTM2OD7dFB3L5FwaDq6KifGYiXvn95ddey4fbtmWv2fhIiVUqpbpMEao2SH4fiKCMgAbRggSuVkKwEQz22q4iVKtQEYUtJvzdlvX6+bq67PJrr41sbm6+VVv8W1dX7/9oADH6b//0+us1QO/jD6xPhGWSCgsqLJj8PsTdjzj7Ma7fxDkAzn5wjry+H3H2YfL7UGGDCguJEqnPPf3YOoDrrrtOe56+C8CKRqPjotHoN+fMmcObb7zRelsk9W1lC4QFCRlM9yfoKnsoEgOLVWCxDLfYBRwwnXmwDIQVyoMbo6lrfrq5+dCsxsbaaHlkqTFSpUMhvjV79mwLwHvjldewBGxQlqBswXn3Y6T/EDhtiNOGuG2I2444QXPb/WtOGzhtmL7PcN7di7IFFegiJDq3+ZVXAWbMmGlrzRJLKc6/4IJFGGO4MdQ+gxAQEn/2LcH0u+Sa27HO0IRq/V+MSqnBOUZARMAD75DB2w4mq8AKWkggpPiOtJ3dYgznzTuPt996+3xLoc8+vaGBlpaWzFybrygtqCPgeODtcTFtBl1hUBHfGgl+wNGv8FIayWjE6KCfD1UhBVqotPWZO3Zs7506dVpUaT1Lh21rPED7oUNtKH8OUYLSoHTwWRiEAsmBDIA4gCPIAJh8YL3lyw7vi5JAJ7QdamvXWmPbofGW0qEYQL4/0zeYjdTRTQ0Oxp9/Svx9jvKAkBocsCh1dP9AZ76vNwOglI5bnuflAaukPBo9bM8UpMIjvxeiWAUbATHK3/yNJM/h30vKozEAz/Ny2nXddoCKyqrKwc5GDYFMUJmM8peLqyCvkH6FZP1zXP+eGBXIFvQcrquyqroyALdrxGzv7u5i6rTTE3lX0gUL/DIYPPfwFDh+k5xCBhSS1Ui/9s9zQ/cLz0rEGxqEGMWAK92T6yfHu7q6MCLbtSj1UtPzTcTjcfW0E3t5EBSkv0FgPgAMQgtWa/9azpcZHICrhvR48B+52CvRaFS9/PJLKKVe1Mao9e+++zsAtk9rnIwbLBFHIQ5IACWvkJxGBjSSDeDZ4HxAIznty+SV38chGIA/PXumzZoK0PJBCyLq1zqdTn/U2dHxbmtrKxddfmXj1r7QRr9jMH/5Ye4d8OdV+odZ3F+AqyG3F/oFelr62PQnl14667PWVrq6ut5JpVJ7giLBygfWrMYOh3ll/pLx4iojR7p3QMGgpQX4kPUE8OFuF0chrjIvzL78VDsc5sEHH0SLWkmQLuhOp5v27t376tatW7nk8iun/UN8VhM5BblASS5w53BowdXD4L7Lg8EG7Z6SM36z+MILp25p2cqBA/s3dKXTLxRSBeDvtUpL7M0PPfRwYtLken791z9Y++fevmWE/WJBIelbgJbDtz4mePblBksrcPU/ubVrF65Yuayzs50Vt6/ozuXduel0etdhYIB4PH5RVWXy+WeeWR8aV1PDz+6/56W//PDFxbpELGULgwVEcwSYoWXkKExOuatqGl9b8p3vfb2vt5/b/uoWtyfVe0kqlXqpwDpql1lVlbwhUhr52VNPrQ3PPuccNm16PbXrR3f+9pvm0NV+pWEwhQKIqKHnm57iV9nydc6Smxc1zm5MHvj0AHfecUeuv7f/u509PY8N5wyCReRcYCEw6YknHi9bcfvtl9276r7qG2+6Gdd12bhhQ/rghhe3TdmywT4l2zkhEeIUgJTLZ62RygPbT5/rlv/xvLOmnzE9ns/lWb9uPY8++u9tP/3JPzd9e/nyLLAXeE0ptRlAicgk4BZgfDAGc/DgQb1790fWrT+45Zz58xdMue+++0kkk/5N8RO2iPiZ0BiMCMbz8FyXzq4u7l91L5ub3969Zs2/Np/eMM2rrT21YKQBPgPWKBFZAyQA093drTzPobu7uyPV3XNbR2enam5uZu3atdTW1LDsqqtYeMEipk2b5m8GANd12bVzJ69vfI2n1/6Kjo5OvrVsKefOPZeqqkpJJCtXJ5OJinBpRJLxeOF3bI8FZIAYoEN2SHmeJ6GQ2CiMUipUP2UK199wI59+2sp/rVvP6tVryKRTOE4eAcJ2mFg8wfgJE5nZeA4TJ4yntmYcSimUUsaydMi2wxIKKTXM6n4lIuMCV08m2O52dHSQzfbpvkxvZSqTbkinUnWpVDqUzvTS29dHNpvFcfy6aNsWkUgp0fJyYrEYiUTcSybin8RjiZ2lZeXd0WjUra6uDg2L/z3A6uHBNQNYAEwHqvAXTTl4Kp3O9HhOvk+FGMhmHXHdHGLEE8CytNY6rCKRsPY8VRoOh8tisfIkhFxgIAB2AtuA15VS20ZcTsEgEsBM4DTgFKASiAClQAnBig7EC8/8BoAc0AekgE+B/cAWpVTqSMb/AlY1WXIncMcxAAAAAElFTkSuQmCC", + "public": true + }, + { + "link": "/api/images/system/map_marker_image_3.png", + "title": "Map marker image 3", + "type": "IMAGE", + "subType": "IMAGE", + "fileName": "map_marker_image_3.png", + "publicResourceKey": "KfPfTuvKCeAnmTcKcrvZQHfdU0TPArWY", + "mediaType": "image/png", + "data": "iVBORw0KGgoAAAANSUhEUgAAAB4AAAB/CAYAAAD4mHJdAAAABHNCSVQICAgIfAhkiAAAAAlwSFlzAAACWAAAAlgB7MGOJQAAABl0RVh0U29mdHdhcmUAd3d3Lmlua3NjYXBlLm9yZ5vuPBoAAAxNSURBVGiB7Zp7kFTllcB/5/a93dMz3T0PemYIDgoCPhZ5iaD4wNkFjQjRRMlLTNbSlKlyzZpobSVbFRPUbNVWSRCWuKvlxqybtbIrukp4SATZCAgospEBgeElj4EZ5t3d0+++37d/9O2ZnqEHQZzZSlXfqlMz/c253+875zvfOefeHuH8L6u83P+AwH0lJZ4pbrenEiCVSnYmEsndGl4NhSKvAJkLmPPcV0VFxZjKivKPv77wXr274WN9uvm0PnHihD5y9IhuPNioN216Vy+Yf6eurAj8b2Vl5aXnM6d8loLf7w9apvHhyy//29jZ9fW0t7fpdWtWN7Wdao4qpaiqDpbdXF9fV1paKpu3bGbxk08eSWXU9ZFIpOPirC33v7xs+TIdiUT0Pz239NjeaTOTHXXjdb4cuP6W5DOLFx/7aNdH+oknfqQryv0vXZTFfr8/GKyqaN7XeMhc//ba6NSfPFXqS6fESJ29jdGAX69+9KHY9OnTyxbec08mHInWhsPhzsHmNs4FNgxdf+NNN5sAh3/7n40dCxeKedUsOr6x8CzdsnBEQu9sPABwzTWTTMNQ9eec+1x/FDEuGTduHABXtreOKutJYyiFqq4tqD+5O3wJQF1dHSij7nODtdZuj9cLgMfGOpcuQInSFoDldqNFez43eCivIrgILoKL4CK4CC6Ci+AiuAgugovgIrgILoKL4CK4CC6Ci+A/B7B5vor6Mz4PNnbRYAAtoCQLUMMFVobuBWOALWdjVIGxiwbbZC3WkrXWLqAzJBZrR5T0LWTgdSHfdF1YcIlG57t8oM5nfov1OcCKPmDW1Rfi2IsA5yI5F9WFXF0o0i8arARwggsBu4BbhwaM6g0ujXY+9b+GLqrzLR5E5wsH2ziB5QRXoW8lCy3mosH553iwlDlEe9znai2DpMyhAJ+PxUNTJMhZm51+WM9xvsWFXD2kx0nl9rjQ4oYC3C+4BoEMnasl39Vn6wxRdcqbXApXpwupWBcEVgLKGLw6DU1w5bkaCjcChcYuHozuLYtqEFfroXC1TZ67GcbjlEuZWjSIHr6ozjZ7/y/VSWOLdgJIF9zjQl3JFwDOXn1lsYDOULm6X+YaROcLB6s8+LC2tzqvoc+Wx0L2nT/6wlIm5y6LQ9bs5TLXsO5x7jG192lxuJq9bCOg0aIRGcYEkt9lCsPp6lxlMsBlFE4ghcYuGoxznHKFYNjKYq7Zy5XFYW32lMtCBGzbLlwWLwB83m/2NNC44R0iFaP503+8jO1UqHz5wiwW0aNzvysgdPJTQr/7dFD9fHD+vecN9vl8NaYpv546ZeqCBx98CMhGbPXEqZRfcTWmyySTjuO2TMora/B4Sji+832OnWoGYMGCBez88IMfHD50eExG6Yd6enraBjJcAwf8fv+Vbsv1Pz9f/NT1y1esQCnNPz6zeGuy6WBN+MRRrwp1YMR6MOIJMqEuOj49xNFd2zh5aD9SVpr44PCJXVOmXXvpHfPm4fP7rtz98Z/usSz3+lQq1e/fnvuFSHl5+VjTNLb96lfPj6yv/0t2bN/eufJnj+37Uql1c/1Xv8WM279CaZn/rJcBGoj1hNm+7k22rF5JcyK1edp3Hps0bfq0yj0Ne/jFL55pVopZ3d3dx88C19bWlqVS8Z2Lf/7U1XNvu51Vb72x7/irz9fUBEcEv/03PyFYPRJDgZHt9XpvzG8QlAFnWppY+S9LaOnsaPPOWdhxx7z5V320cydLl/7yE2+pb+bp06dj/VxtWbJ03h13zr/r7rtZu2bNwVP/9cKYMiHwtW8+QNAbwOiOIN09SCiChCKQL+EIKhxBhcN4EGpGjuJww66yxNH9gePac+zGm26sikQiNY379/kSydT63uCqrCybXB6oeuS+RYvYv29f/OTKFz1+dIlXXFQrCznRjNhkRfdJzmIMEAExsqbUmh68holWGXf43deMg6NHJ+5btKjkgw8//IFh8lJnZ88nBoBWxpPf+e53DYC1Ly5bVSb6Mo8WSrQgx5uRY6cHSDMcz0q/vx/PSTNeJXi04EOPfe93L70JcP/99xu2bfwUwPT5fNU+n++rM2fO5P3332+uS3V9y9KCG8FSmtjRo3iN0uz+qqylemDnLhpDQDsFJGrHMG2F2xAyGi5Nhr65Y8f21unTZ9T4yrz3KqVHGC4X91x33XUmwN7N775nApbuk90nD5BpbUbaWqG9Dd3eju5o6y/t7dDehrS1kmltYffJ/ViA25nDBcbeLZs2AUyaNNkyDL5minDL7Nm3opSiNtQ0yUQwESydlXg6xc70Sf5CewliYSD9TqHu/anpIMUnJIiLjSVCGjAFTA21odNTlFLMunEWO7bvuMUUjKkTrriCvXv3RDyiJxpacGVXSc56W2uO6DhtKkmFFsocHchmtKhoukURNrJPG5YDdAEuDYaAV/TVjY0HesaNG+8Tw5hmuC1zFEBLS0urkQ3QPtFgILgQTC0IkAZSgEJQCClnTBwdF4KBOPf2iQBnzrS2GYaBZblGmWK4/ADxWCzqoS85iDOZDFiMS2ddV5Kz2EkGhgwECYLOzqOzxy0W7YkAiBgBw7btFIC3tMw/2JsrnS9OI5B2pPdt0AC9gdVZZxkBANu2k0Ymk2kDCI6oqsw1c/nNu8rVW8l+2ZFCkxRNzMhKUjQpNBlnv23nXfbAeTRQHayudMBtBlod6OrqZNz4CeVprcKqd4KsZBxgGk1KNEmBmGiijsScsZRo0s4CMnn3284CMqJCY8aOCXR2dqK0PmBokQ3r1q7D7/dLq7tyY8axMCOatDNZFqhJiCbuWNsLNrJjCUcnt4C0ZOew0WTQnDYr3/X5fLJx4wZE5B1DKVm1a9dHAIyYesPYjEBa+vYwJZAUSAgkHAtjookaWcl9Togm4eim8u5PS9YDNVNmXg7QsLsBreX3RjgcPtzW1rarubmZ+QvumtahXJvzrUzmWRvrZ61yxNnvPKuTA6xvt13bvjxv/tSW5mY6Ozt3hkKhoy4Ar6ek6dChg4vm3nY7oZJAJnG4oUIQESdD5Ud0v30XSBlZC1OGdjyTA/darwK3LcxcPm585ZJnl9ATinwvnkweNgC6wuF1x44d27R3714WfOWucZGrb3g7kee+eJ6LewPLcXU0bzwuuf2G3P3NoyevnzP3tsv3NOylqenkHzvD4fWQ197aikeW/nJJd1dnJ4//9On57V+a8Hoib7K4kQeUAWL0D7RcsJ2oqHv9wUcfu7Orq5MVK5Z3KS0P53j96lsgEPjyiKqKtW/891uu2tpalvzDMxsTW96s9yhMC8HUOCkxm07JO/fZk5A9dkmDTOSqWe/99fcfmRPtifHY3z6a6Q5F7gyFQhsKggFGjKh4wFviffG11153T59xHVu3bg3968/+7g9V3ae+0Zv0kX49l3ISjA2ccpe/NXvR9+uvnX5tRdOpJv7+xz9OxnpiD3d0d/97PqcXrLWeBcwGLnv11d96n3j88QVPPf108KHvPUwmk+HttWu71q96Y0dozzajJBUfXyqMA4gpfShmeY54JkzX19/6VzfMmDmjMpPOsOqtVbzyym9alz23fM23Fy1KACeAP4rIBwCitb4MeAQY5SxEt7a2qIaGBn70wx+OTKXTc5Y+t8w1d85cdN5KtdbYSqGVImPbJOIxotEo6/+wniXPPmsH/L4Ny5etaJk46Rqprq7JPTgooBn4Z9FaPw9UAHR1dSnbTsuZMy1GMpnItLZ2GFu3bq5d/fvVc0ZUjZB7F36d2fW3MmHCFZguF0pr0uk0Bxsb2bL5PV5fuZLuUEjfdffdG2+66ebW6mCVLvP5qa4OAoYEg8Gcg7tNIAIEADHdJnbcxmNZ6UQ05nK7TT1x4sRYRVV1/FTTqdLVa9bywgsvEImESKfSAFiWhT9QzqhL6rh25g3UjbokPnJkTaKkxFRaa8NtGbaIy+Up8eS2VgEx0VpXO66+HKfdbW9vV93d7RKNJl3xeNQOd4d1Mp0i3B3yRCKRsmgiYSVTaa9orS23lfR5vany8vKYLxCIeyxLKqoqtddbKh6PSVVVtQ4Gg5IHPQI8nx9ck4CbgSuBarJnvARsiUai4XBPmGQyqbWGRCxh2VrZAKYYLtNjZUyXSxsuU6oqyg1fwO91nhUSzvQdwB5gm4h8UvA4OYsoByYDY4EaoBLwAN7sYiDvZ4LsqUo60uNIK3AY2CMioYGM/wPREY0iGUY58wAAAABJRU5ErkJggg==", + "public": true + } + ] +} \ No newline at end of file diff --git a/application/src/main/data/json/system/widget_types/map.json b/application/src/main/data/json/system/widget_types/map.json index 8019b104fb..e4aba10c61 100644 --- a/application/src/main/data/json/system/widget_types/map.json +++ b/application/src/main/data/json/system/widget_types/map.json @@ -2,24 +2,87 @@ "fqn": "map", "name": "Map", "deprecated": false, - "image": null, - "description": null, + "image": "tb-image;/api/images/system/openstreet_map_system_widget_image.png", + "description": "Displays the location of the entities on Map. Allows to choose among existing tile providers or configure own server. Some providers require additional licenses. Highly customizable via custom markers, marker tooltips, and widget actions. ", "descriptor": { "type": "latest", - "sizeX": 7.5, - "sizeY": 5.5, + "sizeX": 8.5, + "sizeY": 6, "resources": [], "templateHtml": "\n", - "templateCss": "#container {\n overflow: auto;\n}\n\n.tbDatasource-container {\n margin: 5px;\n padding: 8px;\n}\n\n.tbDatasource-title {\n font-size: 1.200rem;\n font-weight: 500;\n padding-bottom: 10px;\n}\n\n.tbDatasource-table {\n width: 100%;\n box-shadow: 0 0 10px #ccc;\n border-collapse: collapse;\n white-space: nowrap;\n font-size: 1.000rem;\n color: #757575;\n}\n\n.tbDatasource-table td {\n position: relative;\n border-top: 1px solid rgba(0, 0, 0, 0.12);\n border-bottom: 1px solid rgba(0, 0, 0, 0.12);\n padding: 0px 18px;\n box-sizing: border-box;\n}", - "controllerScript": "self.onInit = function() {\n self.ctx.$scope.mapWidget.onInit();\n};\n\nself.typeParameters = function() {\n return {\n hideDataTab: true,\n hideDataSettings: true,\n previewWidth: '80%',\n embedTitlePanel: true,\n datasourcesOptional: true\n };\n}\n", + "templateCss": "", + "controllerScript": "self.onInit = function() {\n self.ctx.$scope.mapWidget.onInit();\n};\n\nself.typeParameters = function() {\n return {\n hideDataTab: true,\n hideDataSettings: true,\n previewWidth: '80%',\n embedTitlePanel: true,\n datasourcesOptional: true\n };\n}\n\nself.actionSources = function() {\n return {\n markerClick: {\n name: 'widget-action.marker-click',\n multiple: false\n },\n polygonClick: {\n name: 'widget-action.polygon-click',\n multiple: false\n },\n circleClick: {\n name: 'widget-action.circle-click',\n multiple: false\n },\n tooltipAction: {\n name: 'widget-action.tooltip-tag-action',\n multiple: true\n }\n };\n}\n", "settingsForm": [], "dataKeySettingsForm": [], "settingsDirective": "tb-map-widget-settings", "hasBasicMode": true, "basicModeDirective": "tb-map-basic-config", - "defaultConfig": "{\"datasources\":[],\"timewindow\":{\"realtime\":{\"timewindowMs\":60000}},\"showTitle\":true,\"backgroundColor\":\"rgba(0, 0, 0, 0)\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"0px\",\"settings\":{},\"title\":\"Map\",\"useDashboardTimewindow\":true,\"displayTimewindow\":true,\"showTitleIcon\":false,\"titleTooltip\":\"\",\"dropShadow\":true,\"enableFullscreen\":true,\"widgetStyle\":{},\"widgetCss\":\"\",\"titleStyle\":{\"fontSize\":\"16px\",\"fontWeight\":400},\"pageSize\":1024,\"noDataDisplayMessage\":\"\",\"configMode\":\"basic\",\"titleFont\":null,\"titleColor\":null,\"margin\":\"0px\",\"borderRadius\":\"0px\",\"iconSize\":\"24px\",\"titleIcon\":\"map\",\"iconColor\":\"#1F6BDD\",\"actions\":{}}" + "defaultConfig": "{\"datasources\":[],\"timewindow\":{\"realtime\":{\"timewindowMs\":60000}},\"showTitle\":true,\"backgroundColor\":\"rgba(0, 0, 0, 0)\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"0px\",\"settings\":{\"mapType\":\"geoMap\",\"layers\":[{\"provider\":\"openstreet\",\"layerType\":\"OpenStreetMap.Mapnik\"},{\"provider\":\"openstreet\",\"layerType\":\"OpenStreetMap.HOT\"},{\"provider\":\"openstreet\",\"layerType\":\"Esri.WorldStreetMap\"},{\"provider\":\"openstreet\",\"layerType\":\"Esri.WorldTopoMap\"},{\"provider\":\"openstreet\",\"layerType\":\"Esri.WorldImagery\"},{\"provider\":\"openstreet\",\"layerType\":\"CartoDB.Positron\"},{\"provider\":\"openstreet\",\"layerType\":\"CartoDB.DarkMatter\"}],\"imageSourceType\":null,\"imageUrl\":null,\"imageEntityAlias\":null,\"imageUrlAttribute\":null,\"markers\":[{\"dsType\":\"function\",\"dsLabel\":\"First point\",\"dsDeviceId\":null,\"dsEntityAliasId\":null,\"dsFilterId\":null,\"additionalDataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"temperature\",\"color\":\"#2196f3\",\"settings\":{},\"_hash\":0.8239425680406081,\"funcBody\":\"var value = prevValue + Math.random() * 40 - 20;\\nif (value < -60) {\\n\\tvalue = -60;\\n} else if (value > 60) {\\n\\tvalue = 60;\\n}\\nreturn value;\",\"aggregationType\":null,\"units\":null,\"decimals\":null,\"usePostProcessing\":null,\"postFuncBody\":null}],\"label\":{\"show\":true,\"type\":\"pattern\",\"pattern\":\"${entityName}\"},\"tooltip\":{\"show\":true,\"trigger\":\"click\",\"autoclose\":true,\"type\":\"pattern\",\"pattern\":\"${entityName}

Latitude: ${latitude:7}
Longitude: ${longitude:7}
Temperature: ${temperature} °C
See tooltip settings for details\",\"offsetX\":0,\"offsetY\":-1},\"groups\":null,\"xKey\":{\"name\":\"f(x)\",\"label\":\"latitude\",\"type\":\"function\",\"funcBody\":\"var value = prevValue || 15.833293;\\nif (time % 500 < 500) {\\n value += Math.random() * 0.05 - 0.025;\\n}\\nreturn value;\",\"settings\":{},\"color\":\"#2196f3\"},\"yKey\":{\"name\":\"f(x)\",\"label\":\"longitude\",\"type\":\"function\",\"funcBody\":\"var value = prevValue || -90.454350;\\nif (time % 500 < 500) {\\n value += Math.random() * 0.05 - 0.025;\\n}\\nreturn value;\",\"settings\":{},\"color\":\"#2196f3\"},\"markerType\":\"shape\",\"markerShape\":{\"shape\":\"markerShape1\",\"size\":34,\"color\":{\"type\":\"function\",\"color\":\"#307FE5\",\"colorFunction\":\"var temperature = data.temperature;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\\n\"}},\"markerIcon\":{\"icon\":\"mdi:lightbulb-on\",\"size\":34,\"color\":{\"type\":\"constant\",\"color\":\"#307FE5\"}},\"markerImage\":{\"type\":\"image\",\"image\":\"data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9Ii0xOTEuMzUgLTM1MS4xOCAxMDgzLjU4IDE3MzAuNDYiPjxwYXRoIGZpbGwtcnVsZT0iZXZlbm9kZCIgY2xpcC1ydWxlPSJldmVub2RkIiBmaWxsPSIjZmU3NTY5IiBzdHJva2U9IiMwMDAiIHN0cm9rZS13aWR0aD0iMzciIHN0cm9rZS1taXRlcmxpbWl0PSIxMCIgZD0iTTM1MS44MzMgMTM2MC43OGMtMzguNzY2LTE5MC4zLTEwNy4xMTYtMzQ4LjY2NS0xODkuOTAzLTQ5NS40NEMxMDAuNTIzIDc1Ni40NjkgMjkuMzg2IDY1NS45NzgtMzYuNDM0IDU1MC40MDRjLTIxLjk3Mi0zNS4yNDQtNDAuOTM0LTcyLjQ3Ny02Mi4wNDctMTA5LjA1NC00Mi4yMTYtNzMuMTM3LTc2LjQ0NC0xNTcuOTM1LTc0LjI2OS0yNjcuOTMyIDIuMTI1LTEwNy40NzMgMzMuMjA4LTE5My42ODUgNzguMDMtMjY0LjE3M0MtMjEtMjA2LjY5IDEwMi40ODEtMzAxLjc0NSAyNjguMTY0LTMyNi43MjRjMTM1LjQ2Ni0yMC40MjUgMjYyLjQ3NSAxNC4wODIgMzUyLjU0MyA2Ni43NDcgNzMuNiA0My4wMzggMTMwLjU5NiAxMDAuNTI4IDE3My45MiAxNjguMjggNDUuMjIgNzAuNzE2IDc2LjM2IDE1NC4yNiA3OC45NzEgMjYzLjIzMyAxLjMzNyA1NS44My03LjgwNSAxMDcuNTMyLTIwLjY4NCAxNTAuNDE3LTEzLjAzNCA0My40MS0zMy45OTYgNzkuNjk1LTUyLjY0NiAxMTguNDU1LTM2LjQwNiA3NS42NTktODIuMDQ5IDE0NC45ODEtMTI3Ljg1NSAyMTQuMzQ1LTEzNi40MzcgMjA2LjYwNi0yNjQuNDk2IDQxNy4zMS0zMjAuNTggNzA2LjAyOHoiLz48Y2lyY2xlIGZpbGwtcnVsZT0iZXZlbm9kZCIgY2xpcC1ydWxlPSJldmVub2RkIiBjeD0iMzUyLjg5MSIgY3k9IjIyNS43NzkiIHI9IjE4My4zMzIiLz48L3N2Zz4=\",\"imageSize\":34},\"markerOffsetX\":0.5,\"markerOffsetY\":1},{\"dsType\":\"function\",\"dsLabel\":\"Second point\",\"dsDeviceId\":null,\"dsEntityAliasId\":null,\"dsFilterId\":null,\"additionalDataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"temperature\",\"color\":\"#2196f3\",\"settings\":{},\"_hash\":0.7826299113906372,\"funcBody\":\"var value = prevValue + Math.random() * 40 - 20;\\nif (value < -60) {\\n\\tvalue = -60;\\n} else if (value > 60) {\\n\\tvalue = 60;\\n}\\nreturn value;\",\"aggregationType\":null,\"units\":null,\"decimals\":null,\"usePostProcessing\":null,\"postFuncBody\":null}],\"label\":{\"show\":true,\"type\":\"pattern\",\"pattern\":\"${entityName}\"},\"tooltip\":{\"show\":true,\"trigger\":\"click\",\"autoclose\":true,\"type\":\"pattern\",\"pattern\":\"${entityName}

Latitude: ${latitude:7}
Longitude: ${longitude:7}
Temperature: ${temperature} °C
See tooltip settings for details\",\"offsetX\":0,\"offsetY\":-1},\"groups\":null,\"xKey\":{\"name\":\"f(x)\",\"label\":\"latitude\",\"type\":\"function\",\"funcBody\":\"var value = prevValue || 14.450463;\\nif (time % 500 < 500) {\\n value += Math.random() * 0.05 - 0.025;\\n}\\nreturn value;\",\"settings\":{},\"color\":\"#2196f3\",\"aggregationType\":null,\"units\":null,\"decimals\":null,\"usePostProcessing\":null,\"postFuncBody\":null},\"yKey\":{\"name\":\"f(x)\",\"label\":\"longitude\",\"type\":\"function\",\"funcBody\":\"var value = prevValue || -84.845334;\\nif (time % 500 < 500) {\\n value += Math.random() * 0.05 - 0.025;\\n}\\nreturn value;\",\"settings\":{},\"color\":\"#2196f3\",\"aggregationType\":null,\"units\":null,\"decimals\":null,\"usePostProcessing\":null,\"postFuncBody\":null},\"markerType\":\"image\",\"markerShape\":{\"shape\":\"markerShape1\",\"size\":34,\"color\":{\"type\":\"constant\",\"color\":\"#307FE5\"}},\"markerIcon\":{\"icon\":\"\",\"size\":34,\"color\":{\"type\":\"constant\",\"color\":\"#307FE5\"}},\"markerImage\":{\"type\":\"function\",\"image\":\"data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSIzNCIgaGVpZ2h0PSIzNCIgdmlld0JveD0iMCAwIDM0IDM0IiBmaWxsPSJub25lIj4KICA8ZyBmaWx0ZXI9InVybCgjZmlsdGVyMF9iZl84ODE2XzI2Mzg4NykiPgogICAgPHBhdGggZD0iTTE5IDI0LjVDMTcuNDA3NSAyNy40MTI1IDE3IDMzIDE3IDMzQzE3IDMzIDI3LjA4NTggMzIuMTk1NSAzMC45OTkyIDI3LjQ5OThDMzQgMjMuODk5MiAzMS45OTkyIDE5IDI3Ljk5OTIgMTlDMjMuOTk5MyAxOSAyMS4xOTI5IDIwLjQ4OTQgMTkgMjQuNVoiIGZpbGw9ImJsYWNrIiBmaWxsLW9wYWNpdHk9IjAuMjQiLz4KICA8L2c+CiAgPG1hc2sgaWQ9InBhdGgtMi1pbnNpZGUtMV84ODE2XzI2Mzg4NyIgZmlsbD0id2hpdGUiPgogICAgPHBhdGggZmlsbC1ydWxlPSJldmVub2RkIiBjbGlwLXJ1bGU9ImV2ZW5vZGQiIGQ9Ik0yOCAxMS45QzI4LjAwMzcgNS4zMjc2MSAyNC4yOTAyIDAgMTcgMEM5LjcwOTgzIDAgNS45OTYzIDUuMzI3NjEgNiAxMS45QzYuMDA0NzMgMjAuMjkzNyAxNyAzNCAxNyAzNEMxNyAzNCAyNy45OTUzIDIwLjI5MzcgMjggMTEuOVpNMjEuMjUgMTAuNjI1QzIxLjI1IDEyLjk3MjIgMTkuMzQ3MiAxNC44NzUgMTcgMTQuODc1QzE0LjY1MjggMTQuODc1IDEyLjc1IDEyLjk3MjIgMTIuNzUgMTAuNjI1QzEyLjc1IDguMjc3NzkgMTQuNjUyOCA2LjM3NSAxNyA2LjM3NUMxOS4zNDcyIDYuMzc1IDIxLjI1IDguMjc3NzkgMjEuMjUgMTAuNjI1WiIvPgogIDwvbWFzaz4KICA8cGF0aCBmaWxsLXJ1bGU9ImV2ZW5vZGQiIGNsaXAtcnVsZT0iZXZlbm9kZCIgZD0iTTI4IDExLjlDMjguMDAzNyA1LjMyNzYxIDI0LjI5MDIgMCAxNyAwQzkuNzA5ODMgMCA1Ljk5NjMgNS4zMjc2MSA2IDExLjlDNi4wMDQ3MyAyMC4yOTM3IDE3IDM0IDE3IDM0QzE3IDM0IDI3Ljk5NTMgMjAuMjkzNyAyOCAxMS45Wk0yMS4yNSAxMC42MjVDMjEuMjUgMTIuOTcyMiAxOS4zNDcyIDE0Ljg3NSAxNyAxNC44NzVDMTQuNjUyOCAxNC44NzUgMTIuNzUgMTIuOTcyMiAxMi43NSAxMC42MjVDMTIuNzUgOC4yNzc3OSAxNC42NTI4IDYuMzc1IDE3IDYuMzc1QzE5LjM0NzIgNi4zNzUgMjEuMjUgOC4yNzc3OSAyMS4yNSAxMC42MjVaIiBmaWxsPSIjMzA3ZmU1Ii8+CiAgPHBhdGggZD0iTTI4IDExLjlMMjkuMDYyNSAxMS45MDA2TDI4IDExLjlaTTYgMTEuOUw3LjA2MjUgMTEuODk5NEw2IDExLjlaTTE3IDM0TDE2LjE3MTIgMzQuNjY0OUwxNyAzNS42OThMMTcuODI4OCAzNC42NjQ5TDE3IDM0Wk0xNyAxLjA2MjVDMjAuMzY0MSAxLjA2MjUgMjIuODA4NSAyLjI4MDA2IDI0LjQyNzMgNC4xNzUzOUMyNi4wNjUyIDYuMDkzMjMgMjYuOTM5MiA4LjgwMzMxIDI2LjkzNzUgMTEuODk5NEwyOS4wNjI1IDExLjkwMDZDMjkuMDY0NCA4LjQyNDMgMjguMDgzNSA1LjE4NDM4IDI2LjA0MzEgMi43OTUzMkMyMy45ODM1IDAuMzgzNzQyIDIwLjkyNjEgLTEuMDYyNSAxNyAtMS4wNjI1VjEuMDYyNVpNNy4wNjI1IDExLjg5OTRDNy4wNjA3NiA4LjgwMzMxIDcuOTM0NzcgNi4wOTMyMyA5LjU3Mjc0IDQuMTc1MzlDMTEuMTkxNSAyLjI4MDA2IDEzLjYzNTkgMS4wNjI1IDE3IDEuMDYyNVYtMS4wNjI1QzEzLjA3MzkgLTEuMDYyNSAxMC4wMTY1IDAuMzgzNzQxIDcuOTU2ODYgMi43OTUzMkM1LjkxNjQ1IDUuMTg0MzggNC45MzU1NSA4LjQyNDMgNC45Mzc1IDExLjkwMDZMNy4wNjI1IDExLjg5OTRaTTE3IDM0QzE3LjgyODggMzMuMzM1MSAxNy44Mjg4IDMzLjMzNTIgMTcuODI4OCAzMy4zMzUyQzE3LjgyODggMzMuMzM1MiAxNy44Mjg4IDMzLjMzNTEgMTcuODI4NyAzMy4zMzVDMTcuODI4NSAzMy4zMzQ4IDE3LjgyODEgMzMuMzM0MyAxNy44Mjc2IDMzLjMzMzZDMTcuODI2NSAzMy4zMzIzIDE3LjgyNDggMzMuMzMwMSAxNy44MjI0IDMzLjMyNzFDMTcuODE3NiAzMy4zMjEyIDE3LjgxMDMgMzMuMzEyIDE3LjgwMDQgMzMuMjk5NUMxNy43ODA3IDMzLjI3NDcgMTcuNzUwOSAzMy4yMzcxIDE3LjcxMTggMzMuMTg3NEMxNy42MzM1IDMzLjA4NzggMTcuNTE3OCAzMi45Mzk1IDE3LjM3IDMyLjc0NzJDMTcuMDc0MiAzMi4zNjI0IDE2LjY1MDMgMzEuODAxNyAxNi4xNDA5IDMxLjEwMTlDMTUuMTIxNCAyOS43MDE0IDEzLjc2MzQgMjcuNzQ5NSAxMi40MDcxIDI1LjU0MTVDMTEuMDQ5IDIzLjMzMDYgOS43MDM4OCAyMC44ODEzIDguNzAwOTEgMTguNDg0MUM3LjY5MTA5IDE2LjA3MDYgNy4wNjM1NyAxMy43OTIxIDcuMDYyNSAxMS44OTk0TDQuOTM3NSAxMS45MDA2QzQuOTM4OCAxNC4yMDQ4IDUuNjg3NDYgMTYuNzg3MyA2Ljc0MDU4IDE5LjMwNDNDNy44MDA1NSAyMS44Mzc4IDkuMjA1MTYgMjQuMzg4OSAxMC41OTY0IDI2LjY1MzhDMTEuOTg5NSAyOC45MjE2IDEzLjM4MDcgMzAuOTIwOSAxNC40MjI5IDMyLjM1MjZDMTQuOTQ0NCAzMy4wNjg5IDE1LjM3OTUgMzMuNjQ0NiAxNS42ODUxIDM0LjA0MjJDMTUuODM3OSAzNC4yNDEgMTUuOTU4NCAzNC4zOTU0IDE2LjA0MTIgMzQuNTAwN0MxNi4wODI2IDM0LjU1MzQgMTYuMTE0NiAzNC41OTM4IDE2LjEzNjUgMzQuNjIxM0MxNi4xNDc0IDM0LjYzNTEgMTYuMTU1OSAzNC42NDU2IDE2LjE2MTcgMzQuNjUyOUMxNi4xNjQ2IDM0LjY1NjYgMTYuMTY2OCAzNC42NTk0IDE2LjE2ODQgMzQuNjYxNEMxNi4xNjkyIDM0LjY2MjQgMTYuMTY5OSAzNC42NjMyIDE2LjE3MDMgMzQuNjYzN0MxNi4xNzA2IDM0LjY2NCAxNi4xNzA4IDM0LjY2NDMgMTYuMTcwOSAzNC42NjQ1QzE2LjE3MTEgMzQuNjY0NyAxNi4xNzEyIDM0LjY2NDkgMTcgMzRaTTI2LjkzNzUgMTEuODk5NEMyNi45MzY0IDEzLjc5MjEgMjYuMzA4OSAxNi4wNzA2IDI1LjI5OTEgMTguNDg0MUMyNC4yOTYxIDIwLjg4MTMgMjIuOTUxIDIzLjMzMDYgMjEuNTkyOSAyNS41NDE1QzIwLjIzNjYgMjcuNzQ5NSAxOC44Nzg2IDI5LjcwMTQgMTcuODU5MSAzMS4xMDE5QzE3LjM0OTcgMzEuODAxNyAxNi45MjU4IDMyLjM2MjQgMTYuNjMgMzIuNzQ3MkMxNi40ODIyIDMyLjkzOTUgMTYuMzY2NSAzMy4wODc4IDE2LjI4ODIgMzMuMTg3NEMxNi4yNDkxIDMzLjIzNzEgMTYuMjE5MyAzMy4yNzQ3IDE2LjE5OTYgMzMuMjk5NUMxNi4xODk3IDMzLjMxMiAxNi4xODI0IDMzLjMyMTIgMTYuMTc3NiAzMy4zMjcxQzE2LjE3NTIgMzMuMzMwMSAxNi4xNzM1IDMzLjMzMjMgMTYuMTcyNCAzMy4zMzM2QzE2LjE3MTkgMzMuMzM0MyAxNi4xNzE1IDMzLjMzNDggMTYuMTcxMyAzMy4zMzVDMTYuMTcxMiAzMy4zMzUxIDE2LjE3MTIgMzMuMzM1MiAxNi4xNzEyIDMzLjMzNTJDMTYuMTcxMiAzMy4zMzUyIDE2LjE3MTIgMzMuMzM1MSAxNyAzNEMxNy44Mjg4IDM0LjY2NDkgMTcuODI4OSAzNC42NjQ3IDE3LjgyOTEgMzQuNjY0NUMxNy44MjkyIDM0LjY2NDMgMTcuODI5NCAzNC42NjQgMTcuODI5NyAzNC42NjM3QzE3LjgzMDEgMzQuNjYzMiAxNy44MzA4IDM0LjY2MjQgMTcuODMxNiAzNC42NjE0QzE3LjgzMzIgMzQuNjU5NCAxNy44MzU0IDM0LjY1NjYgMTcuODM4MyAzNC42NTI5QzE3Ljg0NDEgMzQuNjQ1NiAxNy44NTI2IDM0LjYzNTEgMTcuODYzNSAzNC42MjEzQzE3Ljg4NTQgMzQuNTkzOCAxNy45MTc0IDM0LjU1MzQgMTcuOTU4OCAzNC41MDA3QzE4LjA0MTYgMzQuMzk1NCAxOC4xNjIxIDM0LjI0MSAxOC4zMTQ5IDM0LjA0MjJDMTguNjIwNSAzMy42NDQ2IDE5LjA1NTYgMzMuMDY4OSAxOS41NzcxIDMyLjM1MjZDMjAuNjE5MyAzMC45MjA5IDIyLjAxMDUgMjguOTIxNiAyMy40MDM2IDI2LjY1MzhDMjQuNzk0OCAyNC4zODg5IDI2LjE5OTUgMjEuODM3OCAyNy4yNTk0IDE5LjMwNDNDMjguMzEyNSAxNi43ODczIDI5LjA2MTIgMTQuMjA0OCAyOS4wNjI1IDExLjkwMDZMMjYuOTM3NSAxMS44OTk0Wk0xNyAxNS45Mzc1QzE5LjkzNCAxNS45Mzc1IDIyLjMxMjUgMTMuNTU5IDIyLjMxMjUgMTAuNjI1SDIwLjE4NzVDMjAuMTg3NSAxMi4zODU0IDE4Ljc2MDQgMTMuODEyNSAxNyAxMy44MTI1VjE1LjkzNzVaTTExLjY4NzUgIDEwLjYyNUMxMS42ODc1IDEzLjU1OSAxNC4wNjYgMTUuOTM3NSAxNyAxNS45Mzc1VjEzLjgxMjVDMTUuMjM5NiAxMy44MTI1IDEzLjgxMjUgMTIuMzg1NCAxMy44MTI1IDEwLjYyNUgxMS42ODc1Wk0xNyA1LjMxMjVDMTQuMDY2IDUuMzEyNSAxMS42ODc1IDcuNjkwOTkgMTEuNjg3NSAxMC42MjVIMTMuODEyNUMxMy44MTI1IDguODY0NTkgMTUuMjM5NiA3LjQzNzUgMTcgNy40Mzc1VjUuMzEyNVpNMjIuMzEyNSAxMC42MjVDMjIuMzEyNSA3LjY5MDk5IDE5LjkzNCA1LjMxMjUgMTcgNS4zMTI1VjcuNDM3NUMxOC43NjA0IDcuNDM3NSAyMC4xODc1IDguODY0NTkgMjAuMTg3NSAxMC42MjVIMjIuMzEyNVoiIGZpbGw9ImJsYWNrIiBmaWxsLW9wYWNpdHk9IjAuMzgiIG1hc2s9InVybCgjcGF0aC0yLWluc2lkZS0xXzg4MTZfMjYzODg3KSIvPgogIDxkZWZzPgogICAgPGZpbHRlciBpZD0iZmlsdGVyMF9iZl84ODE2XzI2Mzg4NyIgeD0iMTIuNzUiIHk9IjE0Ljc1IiB3aWR0aD0iMjMuOTQzNCIgaGVpZ2h0PSIyMi41IiBmaWx0ZXJVbml0cz0idXNlclNwYWNlT25Vc2UiIGNvbG9yLWludGVycG9sYXRpb24tZmlsdGVycz0ic1JHQiI+CiAgICAgIDxmZUZsb29kIGZsb29kLW9wYWNpdHk9IjAiIHJlc3VsdD0iQmFja2dyb3VuZEltYWdlRml4Ii8+CiAgICAgIDxmZUdhdXNzaWFuQmx1ciBpbj0iQmFja2dyb3VuZEltYWdlRml4IiBzdGREZXZpYXRpb249IjIuMTI1Ii8+CiAgICAgIDxmZUNvbXBvc2l0ZSBpbjI9IlNvdXJjZUFscGhhIiBvcGVyYXRvcj0iaW4iIHJlc3VsdD0iZWZmZWN0MV9iYWNrZ3JvdW5kQmx1cl84ODE2XzI2Mzg4NyIvPgogICAgICA8ZmVCbGVuZCBtb2RlPSJub3JtYWwiIGluPSJTb3VyY2VHcmFwaGljIiBpbjI9ImVmZmVjdDFfYmFja2dyb3VuZEJsdXJfODgxNl8yNjM4ODciIHJlc3VsdD0ic2hhcGUiLz4KICAgICAgPGZlR2F1c3NpYW5CbHVyIHN0ZERldmlhdGlvbj0iMC41IiByZXN1bHQ9ImVmZmVjdDJfZm9yZWdyb3VuZEJsdXJfODgxNl8yNjM4ODciLz4KICAgIDwvZmlsdGVyPgogIDwvZGVmcz4KPC9zdmc+\",\"imageSize\":34,\"imageFunction\":\"var res = {\\n url: images[0],\\n size: 40\\n}\\nvar temperature = data.temperature;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120;\\n var index = Math.min(3, Math.floor(4 * percent));\\n res.url = images[index];\\n}\\nreturn res;\\n\",\"images\":[\"tb-image;/api/images/system/map_marker_image_0.png\",\"tb-image;/api/images/system/map_marker_image_1.png\",\"tb-image;/api/images/system/map_marker_image_2.png\",\"tb-image;/api/images/system/map_marker_image_3.png\"]},\"markerOffsetX\":0.5,\"markerOffsetY\":1}],\"polygons\":[],\"circles\":[],\"additionalDataSources\":[],\"controlsPosition\":\"topleft\",\"zoomActions\":[\"scroll\",\"doubleClick\",\"controlButtons\"],\"fitMapBounds\":true,\"useDefaultCenterPosition\":false,\"defaultCenterPosition\":\"0,0\",\"defaultZoomLevel\":null,\"mapPageSize\":16384,\"background\":{\"type\":\"color\",\"color\":\"#fff\",\"overlay\":{\"enabled\":false,\"color\":\"rgba(255,255,255,0.72)\",\"blur\":3}},\"padding\":\"8px\"},\"title\":\"Map\",\"useDashboardTimewindow\":true,\"displayTimewindow\":true,\"showTitleIcon\":false,\"titleTooltip\":\"\",\"dropShadow\":true,\"enableFullscreen\":true,\"widgetStyle\":{},\"widgetCss\":\"\",\"titleStyle\":{\"fontSize\":\"16px\",\"fontWeight\":400},\"pageSize\":1024,\"noDataDisplayMessage\":\"\",\"configMode\":\"basic\",\"titleFont\":null,\"titleColor\":null,\"margin\":\"0px\",\"borderRadius\":\"0px\",\"iconSize\":\"24px\",\"titleIcon\":\"map\",\"iconColor\":\"#1F6BDD\",\"actions\":{\"tooltipAction\":[{\"name\":\"testTag\",\"icon\":\"more_horiz\",\"useShowWidgetActionFunction\":null,\"showWidgetActionFunction\":\"return true;\",\"type\":\"custom\",\"customFunction\":\"console.log('It works!!!');\\n\\nconsole.log(entityName);\\n\\nconsole.log(additionalParams);\",\"openInSeparateDialog\":false,\"openInPopover\":false,\"id\":\"f9b4925a-818c-15d2-6220-cf2f317bc7fe\"}]}}" }, - "resources": [], + "resources": [ + { + "link": "/api/images/system/map_marker_image_0.png", + "title": "Map marker image 0", + "type": "IMAGE", + "subType": "IMAGE", + "fileName": "map_marker_image_0.png", + "publicResourceKey": "CdCrVxsjA4EAiFaXK4a7K2MZFMeEuGeD", + "mediaType": "image/png", + "data": "iVBORw0KGgoAAAANSUhEUgAAAB4AAAB/CAYAAAD4mHJdAAAABHNCSVQICAgIfAhkiAAAAAlwSFlzAAACWAAAAlgB7MGOJQAAABl0RVh0U29mdHdhcmUAd3d3Lmlua3NjYXBlLm9yZ5vuPBoAAAwgSURBVGiB7Zt5cBT3lce/v18fc89oRoPEIRBCHIUxp2ywCAgIxLExvoidZIFNxXE2VXHirIO3aqtSseM43qpNeZfYKecox3bhpJykYgdjDkU2mBAB5vCamMNYAgQyURBCoxnNPd39O/aP7hGSEUR24L/uqqf+zfR77/Pe69/Rv6kWwcgPLRIJfZUAa7xez2xd90QBwDSNZKlkHJHAK+l09mUA7BP4vPpRUVExMVoRef+L998njxx9X57vPi/PnTsnO850yPaT7XLXrrflqjtWymhF+HA0Gp0wEp/kHymEQqG4ptJDGzf+um5RUxMSiV7Z3Lyt88L5nozgHJWj4pGmpqZav99PWve04onHHuswmViQzWb7ruZX+Udgv8/z3A+f/NGye1evxssvb+wo5PMfTZs6bfqcuXNHL7hlweh58+ZVAOTUpk2b0p9dvjyqqmrs/b8ejpUMc+unzjgUCsXjsYruE+2n1JY/NedM0zCi0VjA7/d7/f4AAgE//H4/vF4fOjvP9h5695C/oaEhcN/q1SyTzVdnMpnklXzTq4EplUsXfmaRCgC7du3cOn78+KfGj59Add3z1Md1vV7vqPa2D1sA4MYbZ6qUiqVX9X21i4TQcfX19QCA6urquN/vn0kAPRQKpYbTnzRpUhgAampqAEFrPjVYSql7fD4AgK5r2tV0AcDj8WkAoOk6JJGeTw2+nocLdsEu2AW7YBfsgl2wC3bBLtgFu2AX7IJdsAt2wS7YBbtgF+yCXbALdsEu2AW7YBfsgl2wC76mh/ppjIQgXVloPxVSBRV0rBe455P6+kTKBYF3tonxY/IWarry7DvI298Tgp0PR9RzACaN1NeIS100+EdvKXW3cMZvF8wCK10Sq2it2NAzakmukP/wmoP/KuId3BRUMg5uCfCSNVSKVn1rNto7Un8jLrUVqJ4Fi2eEQiEYBzOsy3SYL37TNQdzi8Q5FxkqJIQBsNLlYMGF/zqAJWBxSEogDAY+DJibYqTuRg4WFgO3OKhCYTExbKk5G/mbkSPP2DQhLA5IO/NhSz1MMP882BDgnAFQwdiVSs2vPVhYDIJLUMkBgw1favM6lJoZDDAYhKbAYsOX+rqAhcXAuQSIAKzhSy2vS8YmB7NYH4WCfM7kw5VaWtdpOO3bfWZJZVXgPxMX898bVsm6RhkTIseX29yyIErm/J5z5vwr6pvmsLYjBgeDwSpVJS/OmT1n1de+9qANZgLc4q9Dyj2qQhUhSSUAUCL7GBcchCymTEYBYNWqVXj30MGHT586PZEJ+WAul7ts8bjspd9QKDRNU2nz4z94YtI3H3oI+XwB//3j/9m77eRUUJ9/0eh4APGoDz6vCi4ksgUTmYyBC4k8RLGwtzF+EGu+tHqRqqrYtm0rXnzhhQ7G5cpsNnvyiuBIJFKnqvSd55772eilS5fhwIH9ye+/dPaEf1T9otW3T8GtiyYgGNBBymYEgLSbvakidu8/h01vnkYhcab1gcVs5tx5c6PHjh7DU0/9qFsINPb3939UZg28X11dXR0Qwtr9g8efqGtc+Bn89re/O7FhR9BXNaFm+n98uxHTZ1SDKQqKAihweZlITUVtXQwNs8fg+Bmzdk+bnmPdf/7bwsbGeO2ECaED+9/5XCxWuTGbzVpDwJpGNtx+28o77rr7bmzZsu3k7z+cMlHzeiPrvnoTwtVhFAVQHAZY4HBEoiAAeDXUjI/gyJGeQEd6TFj2tHYuXNgYy2azVe0fngiWDLNloHNFo4FZkXDsoTVr1+KD4x8U/3Ci1qP5PV7N74FeFUbClKDEriy57A5JANL5a68hnqoINL8OAPqbXbNp7clTxTVr1/oOHjr0MFXxq2Qy9wEFACnoY//6la9QAHj+9Q/eUL2RWkVXoWgqkhZBypRImkDKBFIWkLIk+h1JWdL+zrmeNCWSDFB0DYquQvWG637TcnozAKxbt45yTr8PAGowGBwVDAbvmT9/Pvbu3dddijV9WdUUUE0BUQm6kwaCYe+ljK/w8ruUdsYCBLlMEUQhoJoCygWM+LIvHTx4sGfevIbqYMD3BSFkJVUUrG5oaFABoPXwhd1UVUBVahtpKtoOnEV/gSHHgBwDso5c6XO6yNF24CNQTbV9qBRUUenuwz1/BoCZM2dplOJeSggWL1myFEII9IeXziIKBVUUW1QKo2Ci41Anei9kkWcY6Ex5R8qfc0wi0ZPF6QNnYeQNB2j7IQpFOtg0WwiBxoWNIBKLVQI6Z8rUqTh69FiWaFNmEIWgLFShoM5TZbIzgVxvFp6ID5rfA6JQgBAIxsGLJkrpAsycAcH4gN1gX0QPTW9vP5Grr58cJJTOpbqmjgWAnp6ei4QSEEJAKAGh1BbHCS2DLAFmMAgmICwObjDnyYMMAtJL9oN89vRc7KWUQtOUsSqhSggA8sWivSEh9qBxTiCEAGRwQARUVaB67Hf5pZAQlA0Ayrq2LTCogVyhlLURNEw55yYABP2+4ED3vHSClBKQ9jiFdHqvEBCMQzAOKYSt6/RqSGnbDPJRbgT93hAAcM4NyhjrBYDKylhswEEZJgYJFxDchnGTwSqasIomuMnsIDiH5GKIzUAQTsCVlZUxB9xLIUVbKpVEff3kiLTMfimEA7HP5bZgHMJ07mnJAiuaYEXT3jcZDMLkTgBD7exgBKRp9NfVTQwnk0kIKduoJGRH8/ZmhMNh4skc3DnEkDlAi4GbtjDDguVAmZM1M6yB68JyKsCGBqD373s7GAySnTt3gBDyFhWCvPHee/8HAJhTU5g0BMg4uMXBTT4AZSUTrGjBKpiwCnablQbDbZuyfTmAuRPMegA4euQopCRbaCaTOd2XSLzX3d2Nu+64bR7PnP3LJSCDMBm4YW9FWcmyQYMytsW+Zpfdsm1MdimAdMc7K29bMedCdzeSyeS76XT6jLNI4PGf/+w5aLqOu25IjOOWKcSg0jJjcLZ2ecsZD5TdybqsOxC0ZYpbJ58frek6nn/+eVBJHgecjXkqk2nu7Ozcdfz4cdx556rJN5C3m8v3jBt2xpdnazjysawNy5lUbKkrbmtZsWL5pGNHj6Or62+7k5lMy5CFNRQKTfN6tAMvvvhSRe3EOqx/4oXXLvia7qO6CsVZrey5154KB5YpKSG5tHs+5/ZsZnEIk6Ei1fLH73373i/09fXi0fWPpgyTLchkMqeGgAEgHA5/vjJWsf2PmzYr1dXV+K8fP7vjLxduWkY8ilpetQZPg+UJxh63lzqlNDi7gTa3fuPraz6bzxXw79/5FutP51am0+kdZdaQ/2kzDKNDUci51179w8pbP3er8sAD6+pnVCWy+/fs21LAqBnlMT50qJXFLq2a2L/5gaVy7N133j69u7sb67/7iFHIFf4tlU6/Ppg1kLGU8hYAywBMeOWV33gfXb9+1Q+ffDL+4Ne/AcYY/tS8PbV5++4Dhy+MopY2ZrLiidQDgDBSp5TS+Y7psS65ZOHsW26++eYosxje2PwGNm586eKzz/x027+sXWsBOAfgbULIQQAgUspaAA8BGAfnsamrq4u0tZ0Q333kkdGmZS3f8JNnlBXLV0AOilRKCS7sWYlxjlKxgHw+j5Y3W/C/Tz/NQ6Hgjp9seKZ31py5ajwe4wAtz9zdAH5OpJTPAqgEgL5USkpu4eLFHloqFXniYh9t3bunauuWrStisSi5//4vYnHTEkyZOhWqokBICcuy0N7ehr2trXjt1VeRzqTl3ffc81bjgsZELF4pQ6EAqa4eI6UEicfj5dhTKoCikynx6Bop5C14dJ2XcjmouipvvGFGoSJaWfr738/7tmzdjl/88pfIZjKwnH2SpmkIhSMYW1ODhvmNGFcztjhudFXR69Wgck58Hg+XEorH5ylDJYA8kVKOckpdB0ADIBOJhOzv70OhUFILuTzPZLNcSE6SfSlvJp0O5A1DN0qGDxLS4/OUAh6PGQqHC5XxeJEQgkgoRH1+L/wBP6LRuIjH4+Uf8gSAUwB+MbhzzQSwCMA0p/QUQADgNJ/PJ/v7+wnnnFiWkJZhKCYzKADoqiZUXeW67iGcSxKPx2QoFAo7AybnuE8COAZgHyHkxGXjeFAQEQCzANQCqAIQBeAH4AXgcex052w45TMcyQHIAOgBcBbAUUJI5uOM/wcaHmf3g9UM7QAAAABJRU5ErkJggg==", + "public": true + }, + { + "link": "/api/images/system/map_marker_image_1.png", + "title": "Map marker image 1", + "type": "IMAGE", + "subType": "IMAGE", + "fileName": "map_marker_image_1.png", + "publicResourceKey": "DF3fuPXua9Vi3o3d9Nz2I1LXDTwEs2Tv", + "mediaType": "image/png", + "data": "iVBORw0KGgoAAAANSUhEUgAAAB4AAAB/CAYAAAD4mHJdAAAABHNCSVQICAgIfAhkiAAAAAlwSFlzAAACWAAAAlgB7MGOJQAAABl0RVh0U29mdHdhcmUAd3d3Lmlua3NjYXBlLm9yZ5vuPBoAAA3vSURBVGiB7Vt7cFzVef+dc+/d90OrJyO/JSO/4ncxxfULMCYIAyEW08amJJgmM4GmnZjJdNq4gcSGzLQxk3bsaWcaaIHyR8CJrWAbpjgG/AhINsbYxkaSDY6xJFvSrrS7Wu3uvfecr3+cu1pbXhkJs/4nujNndufec77f+d7fd+4uw8gvIxwOfocBaz0e91yXyx0BgKyZiWUz5kcEvBKPJ18EYI+C5rWvkpKSyZGS8LGHGtbQR8ePUUdnB50/f57OfnqWWlpbaN++39O99fdQpCR0NBKJTBwJTfZFE4LBYLmh8+YXXvifKctWrEBPTze9+cbu8/3JVMoWNjwer3/ZsuUTvV4P239gP36yceNZW9CtyWQyei262hcB+7zurU/99Ge3r1nTgJdfevFsqr8/Wlc3rWbGzFkV8+fPr1iwYEEJgLadO3cmbr/jjohh6KXHPjxamsmar39pjoPBYHl5aUnnqZY2/b1Dh9LdPd39kUgk6PP5PD6fH36/Dz6fDx6PF+fOfdZ9+pPTgbq6Ou+aBx+0k/0DVYlEIjYcbX4tYM5pxeK/WKIDwM7Gxt0TJox/dtLESXC53JuHzvV4PBVHDjfvAYDZs+fonMsV16R9rYeM8XG1tbUAgMrKsrDP659DRJ5gMNhbaH5NTU0IAMaPHw9IPv5LAxORy+31AgBcLsO41lwAcLu9BgAYLheIkftLAxfzGgMeAx4DHgMeAx4DHgMeAx4DHgMeAx4DHgMeAx4D/lME1ke7gDF8ltbOHe3W923oEwYi1jxftWfZWgAziwacZkd2pfyN96XN5IIu7dMtIKA9/TI+zqCnFps2Alg5UlojFnVqIHZUlO2sl4RyC4CU+SEEylux8Z/iyc7mrxw4U7UnYwvGpXMYKIgNGdwXC/76C48oRw3sDWfnCgIkARJXcpwbvpA1e6T0Rq5jDr8EAHKA6OpjUOJwfeXAJAEhAXAGgEPKq+dIMVJqowDO4RAAC0rHV21u5LijAJaABAOIAY5Oh15iFMgj1zEpcUuuXjpIWeCouxjAtnIZcGKA5AVFbRfazPUC50QrKe8+Qy8qiqjBYIODA5DgBd1pBO9WRg9sy7yOhXBca+icYrgTOUGOiKnIVdCdisAxJGBTPsYW0nHRrJqgfNmGVtiqaeR1xchF7Vgz40q/BUNmISlcL7CUgJAMnOUiVwEdF0PURIAAVHaC8ucbAiwcQAb1KQpwXMjFrhtYMcOVO8lhOB457ujcKZd9hBguSYwcelTupKyaQWKYJFEU4xJw/Dhfcw29ilSBcNjEoTucFnSnkeOOvvTJpcVC1cYoGB5NAGEQTukjMAzHoghJghyWCRjenYoTuZjKx8xJiwU4LrSZ6waWpIoBjTuRqxDHRUkSUMWAJAZp6QU5FqOw65HHapG3bGVcBTZXDI5VnFaFgBL1yC34uoBJqEJeIwD2MMY1ilZidAFEMlDOqm9UdpJ0ZawumI+LU9ArwhyqWxyNz14XsBAMUnLVH0ttGB0XococdCGWE3XhOV85MF1WV2OY3omK0S2SkxgYAZYYJoAUpcqEEjG/Ru80isA1ysMXYNCnCum4aKUPgTu90w3sFinXL6nO/MadCAhiKloxBjFMeSuK0S1Kylv1cE1bUVoYyHwhoI6bCswpjjuxK5u2G2lcti2jzNCRTluioHEVw52EBA5/2LKsLBL+h2gs/o+Fjpa+MqtmjCbkqQJSYFF3T3zRsPMvA75i7UiBA4FApa6z5+fNnbd6/frHADghk7QdlhAHdMY0KXkZAHAuozaRMDRtKYMdAYDVq1fjcHPTD860nZlsS3qsv7+/+6pNDr0RDAanGTrf85Onnq75/uNPIJ1O4+dbnj34Ot6B4eFLqksqUeEvgcflAREhZabR09+Li/EorLQ4eFv317D2oW8t0XUdu3a9jud/9auztqD6ZDLZOixwOByeouv8D1u3brtpxYrb0XS4Kfbj3//8VHC8d0nDLXfj67OWIeQJgDGADfoOAxHQl05i14l92PHBXiTPp/c/OrFh9vwF8yMnjp/A5s2bOqXEbX19fX+8CriqqspvmunDTz/10xkr71qFnY07Tr1i7aqsLg2Vb6h/GOPCpdAYgTPlNLmF5AzpvBRp74viX3a/hO6+ge47+hZG61fVTz9y+DCee27Lx15fYFFHR8cAcNkPuw2DPXfP1+vvvf+BB7Br967WX9Mbk70eCn33zlWoCrsgKAFBCdgy/2nLBCyZgCUSMGUSpkzC0G1MrKzE0XMt/la9I0QnM+cWL15cmkwmK1tOnwpksuabg8YVifjnhEOlj69dtw6nT51Kv2q96fYG4fG7gbJwFhn7cxicIJgEZwAfEiokGASpWG1KhvIwg1/91ti1N9DEJ7ZOzKxdt87T1Nz8A67jv2Kx/o85AJDk//zXjzzCAeA/D7zU6PZjkkuXcBuEjN2OrGiHabfDFB2w7HZYoh3mVaMDWWdu1m6Hy5Bw6RIuP6b87+HXdgDAww8/zIXgGwFADwQCFYFA4BuLFi3CoUN/6LRmyL/y6gSXTtC4QDTVgQo/B5iEJFJ6Rt64lI6Vfi3JYBFHd1JA5wIunUNIQvpr/C+bm5u65s9fWBnwe9dISWVc0/DNhQsX6gDwTuuhd3WNYOSGTjjSehGp7EVYsguWuJQfssu51wVTXIIpLsGWlzBgXsSRM5dg6Hk6uk787Zb39gHA7NlzDM7xoM4Yli5fvgJSSiRmmbP9HNA0Qm4D6axEc6uJ6eOzuCloQuOOjlneqiUx2BK4lDBwut2DTFaHoXFYGilaHEjMMOdKKXHb4tvw/nvvL9UZ+Lyb6+pw/PjxpOZhsziX0DigcYLG1QaEBD69ZKA7wRHx2/C7BDSNwEi9AEmZGmJJA/1Z9SJM12hwvcYBzgmaj89obW3pr62dGmCcz+cuQ68GgEtdl7oYU40CZwSeW+As1rmy5KzNkbY1WILDlOp71ubgnKA7czVO4NyhwQhcFS7o6urq5pzDMLRqnXEtCACpdCrFHOHlAsTgYEq0nCnj0jnBY6i8KCTLBxbmzB2yPkczmU4lAYAxHtKFECYAPeDzBQZD4GU+motMueXklECWc7QkSaVDGoTAVetz8AGfLwQAQoisbtt2N4BJZaVlpZQjkntdS8w5UFOFni0YLMGhWfny1rbVPVuoOVKyK9ZeTrMsUl7qAHdzkPyktzeG2tqbw8KihCQlPjVUl2hLBkswmDZD1mJIWxwDWTXSFkfWUs8sZ64QzlqHjiRA2tQ7ZcqUYCwWgyT6hBNjb+3ZvQehUIi52tje3M6FyHHIYNkOqM2RsTjS2cuAs+pe1uYKPLcBkduA+m60sH1+v5/t3fsWGGP/x6VkjR98cAQAMNc7bXJepAyWzWHaimjW4siYDGmTY8DkGMhqapgcaVM9yw5ugMOyeX4DkmGub1otABz/6DiI2O94IpE4E+3p+aCzsxP333PfAvOi2G8JBtMRbU68GZMj44Ao0BzXmgOsRk7spq1oWILB6rQP3nt3/byLnZ2IxWKH4/H4pxoAeFzuC21tretW3rUKnk5mtWiflzAGxhgDQ66IYyrnOnqzBFfDZjAdLk1HMnkpMWRNLldmFomamtrIL/71F+iPJ/8mnc2e4QDQm0jsOXfu3L6TJ0/ivtX3T607M26P6SzMWI5eB7ktPHLPc/MV5xwTjpe9sfLOu2pOHD+JCxc+fyeWSLyZdzCoWsvjNpqef/6F8KTJU/DDLT/a3jM90eDWCS5dqmDvxF7NCRSAOikQhCuMUXHMEDjm3v7jb/+oIRrtxpMbnuzNmvatiUSi7QpgAAiFQneXlZbs3rGjUauorMSmLc+8dShy7HbDELqeA3bC4GCScHxWSMDOgVuaPb2t+t3vPfK9O1P9A/j7v3vC7ov318fj8bdyWFf8YCSbzZ7VNHb+tVdfrV911ypt/bcfq52J2uTBg+//LhWwZ0nJYTtWf6WrcccDGFgLdn5nwkPVD9Q/MLOzsxNPbvhhNpUc+G5vPL7jcqxBjonozwEsBzD5lVde9jy5YcPqTZufKX90/WOwbRv7330nsffDt08dSB41EkZyHPfwmwBAZuTFsBm48GeuWfai2oUzp02fFjKzJhp3NuLFF/+765e//Pfd31q71gLwGYC3GWNNAMCIaBKAJwBUO3uQnZ2d/MyZNv1vn/j+LUuXLq/Z/MyzCIfDTmxW8Y+IVFyWqjKRQkDYNqKxGDb97GkcOXLk7LZt/9F8c12dqKqqYM4LYALQCWAbI6J/A1AGgKK9vSBhoa8vEe+N9TwejcZYU1MTfrN9O6puqkJDw0NYtnwFpk6dCsZUMrFtG22trTiw/11s3/4aotEo1jQ04NZFt6KsrJTCoZKtJaWRiGG4KBKJ5BJWnw4gDedAx+0yMJCywLnQGWOSMabV1NbikUfX40J7B367sxFbt25DMhGHZZkgAC7DhWAojOpx4zF3wS0YP64aVZUVYCoQSN2la4bhIsNlcOS73H5GRBUAHgcwBYABAD09PZROp1gq2V8WTybq4vH4xEQ8oSWSSfSnUkinM7As9RdUw9Dh9XoR8PsQCgYRCodESTj0x1Aw2OrxBXsDgYBdXl6eM2IB4CyAbZcb12wASwBMB1Dq7C4ACJZIJHstM5PWdC2TTmcom80wEtySAFwupum6wbxeDxeCuT0et8/v94UBTTrSJABRAKcAHGCMnbrKjy/bRBjAHAATAFQ5NuAF4IFqAtyOKzKo83MLgAkgA2AAQB+ADgCfAzjBGIsPxfh/6wbDK7xbMFYAAAAASUVORK5CYII=", + "public": true + }, + { + "link": "/api/images/system/map_marker_image_2.png", + "title": "Map marker image 2", + "type": "IMAGE", + "subType": "IMAGE", + "fileName": "map_marker_image_2.png", + "publicResourceKey": "rz5SFAw2Sg5T2EyXNdwLycoDwf4QbMiZ", + "mediaType": "image/png", + "data": "iVBORw0KGgoAAAANSUhEUgAAAB4AAAB/CAYAAAD4mHJdAAAABHNCSVQICAgIfAhkiAAAAAlwSFlzAAACWAAAAlgB7MGOJQAAABl0RVh0U29mdHdhcmUAd3d3Lmlua3NjYXBlLm9yZ5vuPBoAAAyUSURBVGiB7Zp7kFRVesB/5/S9PdMz/ZoHMwo4MICDuoGVIYICIuzGcn0vC+oWGuNjs8mua9ySP4wpgyaiVVupbHYTsLJmNT7WNXExwqqzrq8g4oNxdXUgyEMQARmZd3fPTE/3vfd8+ePenhlgBsFlrFSqb9Wpvn3vd77f+b7zne87ffsqjv+wE4nYDQqWl5aWfDUcLqkAyOUHunID+Q8EnkilMo8C7gnoPPaRTCYnVyQT71+1bKl80PK+HGw9KPv27ZPde3bLjp075NVXX5FLL7lYKpLx9yoqKuqOR6f6PIFYLFZtW7r54YcfqV+4aBEdHe3ywm+e39eb6etzPZfS0kj5woUX1EUipWrj6xtZedddu11P5mYymc5j6Q19HrgsUrL67r/7+8VLly7j8cce3d3X29vZ0DB9yplnfWXcrFmzxjU2NiaBXevWrUsv/trXKmzbqnz/9+9VDuTyz35hi2OxWHV1ZbJ1245d1ltvvpFtb293Kyoq7LKystKysnLKy8soKyujtDTCxx/vSW3fsT3c0NAQWbpkiZvp7a9Np9Ndo+nWxwJrLYvmzV9gAaxbt/75urrxd592Wp0Oh0tWHSkbiUQSv3unuQlgxoyZltZm0TF1H+umUnrC1KlTAaipqUpESmMzFIRjsVj3SPJTpkyJA0ycOBGMnviFwSISLolEAAiHbftYsgAlJREbwA6HESUlXxg8lkcRXAQXwUVwEVwEF8FFcBH8/xhsnZC0ksw49eQPI5mmNtP54ccAIvqgqbz4aYn8zYoTUXXcFnueyZ8eXtleZt75iQnpU0VUvYiqB5mvu5p+XH9w8RtgnJMOLut/7rd4+fpRBcS52hz65csnHdxQ8clZnyuT3NV40sHRUnfq58mUWFJ70sEn+yiCi+AiuAgugovgIrgILoKL4CK4CC6Ci+D/Q+Djf/higk8Jzs0IMjIGYDGAp0AUeBbiHf3Xs/HGAHyYlYaRX0EYC4txNeIFugvWHyXzua8cnDjYGMBoQIFhRFfLmLjaCxqAw8iuHing/nCwGlLuMrKrveNfnccPFnyLtQ8c0a1jElye8sGFAYwUSCN54Q8GB4ljKKpHkBmLOZbB4FLgjhLVYxNcDFnkMXJUj03m0kOKR0sgYzLHRvlwpcDYI7oaGYvl5HB4ZRrJ1cf9fP5E/5NwQUKM7uoTOI4/ql38kmgUOCMnEHMCL819sag2jJJAxgIs+HNY6PGlpUxXDQWXw5dXjxH8SFZBPf7SyqKrMQLKG7b/OkpmTBJI0BSjbwTGYo6Ni5+ZjMJDj1wkxmQ5iV+VsBh9BzImKbNQFhWjp8wx21c7dKIV9A94IxaJsdplZt9574JQVcUdpr3rzlEHdzLASslpg19EofLMMa3dc0Z9c9YMXT+s7/GCo9FojWWph87+6tmX3XTTzT7XA/F4xutXr4fyOuQZVQUQ0tLphY1nlcn5YqgAuOyyy3inefOtH+36aLJr5Obe3t72o4w68kIsFptuW7pp5d33TPne928hm83yLz+6b9PVb/4niRK9QNfUoquqUaUREEEG+jGd7Zi2Dnpy3qYHGr7OFdcsX2BZFs899ywP/fznu11PLslkMjtHBScSiXrL0m+uXr3mlEWLFrN58+auxD+u2HZWhb0gcvkyShZ/Ax2N+70KPcVvJpMm999NZJ99mi1dzsb3rviLGbNmz6rY0rKFVavubTWG83p6ej4psAbfr66trS03xtlw98p76s+bN5+nnvzFtouevK/s1AnJM+I/vB37j6aDziJeCtxhzUkhTgoYwJpchz3zbJI7fj/pzA829f6iR/bPPW9e9aS6utjbb715YWVl1SOZTMY5DGzb6scXf+OSS6+48kqanntu55+99shkOyLx8uuvIjSuDEzq6Ob5TdzgPJ9GhT2sCbV4W1vK57R+FP9lOrT33PnzKjOZTM2OD7dFB3L5FwaDq6KifGYiXvn95ddey4fbtmWv2fhIiVUqpbpMEao2SH4fiKCMgAbRggSuVkKwEQz22q4iVKtQEYUtJvzdlvX6+bq67PJrr41sbm6+VVv8W1dX7/9oADH6b//0+us1QO/jD6xPhGWSCgsqLJj8PsTdjzj7Ma7fxDkAzn5wjry+H3H2YfL7UGGDCguJEqnPPf3YOoDrrrtOe56+C8CKRqPjotHoN+fMmcObb7zRelsk9W1lC4QFCRlM9yfoKnsoEgOLVWCxDLfYBRwwnXmwDIQVyoMbo6lrfrq5+dCsxsbaaHlkqTFSpUMhvjV79mwLwHvjldewBGxQlqBswXn3Y6T/EDhtiNOGuG2I2444QXPb/WtOGzhtmL7PcN7di7IFFegiJDq3+ZVXAWbMmGlrzRJLKc6/4IJFGGO4MdQ+gxAQEn/2LcH0u+Sa27HO0IRq/V+MSqnBOUZARMAD75DB2w4mq8AKWkggpPiOtJ3dYgznzTuPt996+3xLoc8+vaGBlpaWzFybrygtqCPgeODtcTFtBl1hUBHfGgl+wNGv8FIayWjE6KCfD1UhBVqotPWZO3Zs7506dVpUaT1Lh21rPED7oUNtKH8OUYLSoHTwWRiEAsmBDIA4gCPIAJh8YL3lyw7vi5JAJ7QdamvXWmPbofGW0qEYQL4/0zeYjdTRTQ0Oxp9/Svx9jvKAkBocsCh1dP9AZ76vNwOglI5bnuflAaukPBo9bM8UpMIjvxeiWAUbATHK3/yNJM/h30vKozEAz/Ny2nXddoCKyqrKwc5GDYFMUJmM8peLqyCvkH6FZP1zXP+eGBXIFvQcrquyqroyALdrxGzv7u5i6rTTE3lX0gUL/DIYPPfwFDh+k5xCBhSS1Ui/9s9zQ/cLz0rEGxqEGMWAK92T6yfHu7q6MCLbtSj1UtPzTcTjcfW0E3t5EBSkv0FgPgAMQgtWa/9azpcZHICrhvR48B+52CvRaFS9/PJLKKVe1Mao9e+++zsAtk9rnIwbLBFHIQ5IACWvkJxGBjSSDeDZ4HxAIznty+SV38chGIA/PXumzZoK0PJBCyLq1zqdTn/U2dHxbmtrKxddfmXj1r7QRr9jMH/5Ye4d8OdV+odZ3F+AqyG3F/oFelr62PQnl14667PWVrq6ut5JpVJ7giLBygfWrMYOh3ll/pLx4iojR7p3QMGgpQX4kPUE8OFuF0chrjIvzL78VDsc5sEHH0SLWkmQLuhOp5v27t376tatW7nk8iun/UN8VhM5BblASS5w53BowdXD4L7Lg8EG7Z6SM36z+MILp25p2cqBA/s3dKXTLxRSBeDvtUpL7M0PPfRwYtLken791z9Y++fevmWE/WJBIelbgJbDtz4mePblBksrcPU/ubVrF65Yuayzs50Vt6/ozuXduel0etdhYIB4PH5RVWXy+WeeWR8aV1PDz+6/56W//PDFxbpELGULgwVEcwSYoWXkKExOuatqGl9b8p3vfb2vt5/b/uoWtyfVe0kqlXqpwDpql1lVlbwhUhr52VNPrQ3PPuccNm16PbXrR3f+9pvm0NV+pWEwhQKIqKHnm57iV9nydc6Smxc1zm5MHvj0AHfecUeuv7f/u509PY8N5wyCReRcYCEw6YknHi9bcfvtl9276r7qG2+6Gdd12bhhQ/rghhe3TdmywT4l2zkhEeIUgJTLZ62RygPbT5/rlv/xvLOmnzE9ns/lWb9uPY8++u9tP/3JPzd9e/nyLLAXeE0ptRlAicgk4BZgfDAGc/DgQb1790fWrT+45Zz58xdMue+++0kkk/5N8RO2iPiZ0BiMCMbz8FyXzq4u7l91L5ub3969Zs2/Np/eMM2rrT21YKQBPgPWKBFZAyQA093drTzPobu7uyPV3XNbR2enam5uZu3atdTW1LDsqqtYeMEipk2b5m8GANd12bVzJ69vfI2n1/6Kjo5OvrVsKefOPZeqqkpJJCtXJ5OJinBpRJLxeOF3bI8FZIAYoEN2SHmeJ6GQ2CiMUipUP2UK199wI59+2sp/rVvP6tVryKRTOE4eAcJ2mFg8wfgJE5nZeA4TJ4yntmYcSimUUsaydMi2wxIKKTXM6n4lIuMCV08m2O52dHSQzfbpvkxvZSqTbkinUnWpVDqUzvTS29dHNpvFcfy6aNsWkUgp0fJyYrEYiUTcSybin8RjiZ2lZeXd0WjUra6uDg2L/z3A6uHBNQNYAEwHqvAXTTl4Kp3O9HhOvk+FGMhmHXHdHGLEE8CytNY6rCKRsPY8VRoOh8tisfIkhFxgIAB2AtuA15VS20ZcTsEgEsBM4DTgFKASiAClQAnBig7EC8/8BoAc0AekgE+B/cAWpVTqSMb/AlY1WXIncMcxAAAAAElFTkSuQmCC", + "public": true + }, + { + "link": "/api/images/system/map_marker_image_3.png", + "title": "Map marker image 3", + "type": "IMAGE", + "subType": "IMAGE", + "fileName": "map_marker_image_3.png", + "publicResourceKey": "KfPfTuvKCeAnmTcKcrvZQHfdU0TPArWY", + "mediaType": "image/png", + "data": "iVBORw0KGgoAAAANSUhEUgAAAB4AAAB/CAYAAAD4mHJdAAAABHNCSVQICAgIfAhkiAAAAAlwSFlzAAACWAAAAlgB7MGOJQAAABl0RVh0U29mdHdhcmUAd3d3Lmlua3NjYXBlLm9yZ5vuPBoAAAxNSURBVGiB7Zp7kFTllcB/5/a93dMz3T0PemYIDgoCPhZ5iaD4wNkFjQjRRMlLTNbSlKlyzZpobSVbFRPUbNVWSRCWuKvlxqybtbIrukp4SATZCAgospEBgeElj4EZ5t3d0+++37d/9O2ZnqEHQZzZSlXfqlMz/c253+875zvfOefeHuH8L6u83P+AwH0lJZ4pbrenEiCVSnYmEsndGl4NhSKvAJkLmPPcV0VFxZjKivKPv77wXr274WN9uvm0PnHihD5y9IhuPNioN216Vy+Yf6eurAj8b2Vl5aXnM6d8loLf7w9apvHhyy//29jZ9fW0t7fpdWtWN7Wdao4qpaiqDpbdXF9fV1paKpu3bGbxk08eSWXU9ZFIpOPirC33v7xs+TIdiUT0Pz239NjeaTOTHXXjdb4cuP6W5DOLFx/7aNdH+oknfqQryv0vXZTFfr8/GKyqaN7XeMhc//ba6NSfPFXqS6fESJ29jdGAX69+9KHY9OnTyxbec08mHInWhsPhzsHmNs4FNgxdf+NNN5sAh3/7n40dCxeKedUsOr6x8CzdsnBEQu9sPABwzTWTTMNQ9eec+1x/FDEuGTduHABXtreOKutJYyiFqq4tqD+5O3wJQF1dHSij7nODtdZuj9cLgMfGOpcuQInSFoDldqNFez43eCivIrgILoKL4CK4CC6Ci+AiuAgugovgIrgILoKL4CK4CC6Ci+A/B7B5vor6Mz4PNnbRYAAtoCQLUMMFVobuBWOALWdjVIGxiwbbZC3WkrXWLqAzJBZrR5T0LWTgdSHfdF1YcIlG57t8oM5nfov1OcCKPmDW1Rfi2IsA5yI5F9WFXF0o0i8arARwggsBu4BbhwaM6g0ujXY+9b+GLqrzLR5E5wsH2ziB5QRXoW8lCy3mosH553iwlDlEe9znai2DpMyhAJ+PxUNTJMhZm51+WM9xvsWFXD2kx0nl9rjQ4oYC3C+4BoEMnasl39Vn6wxRdcqbXApXpwupWBcEVgLKGLw6DU1w5bkaCjcChcYuHozuLYtqEFfroXC1TZ67GcbjlEuZWjSIHr6ozjZ7/y/VSWOLdgJIF9zjQl3JFwDOXn1lsYDOULm6X+YaROcLB6s8+LC2tzqvoc+Wx0L2nT/6wlIm5y6LQ9bs5TLXsO5x7jG192lxuJq9bCOg0aIRGcYEkt9lCsPp6lxlMsBlFE4ghcYuGoxznHKFYNjKYq7Zy5XFYW32lMtCBGzbLlwWLwB83m/2NNC44R0iFaP503+8jO1UqHz5wiwW0aNzvysgdPJTQr/7dFD9fHD+vecN9vl8NaYpv546ZeqCBx98CMhGbPXEqZRfcTWmyySTjuO2TMora/B4Sji+832OnWoGYMGCBez88IMfHD50eExG6Yd6enraBjJcAwf8fv+Vbsv1Pz9f/NT1y1esQCnNPz6zeGuy6WBN+MRRrwp1YMR6MOIJMqEuOj49xNFd2zh5aD9SVpr44PCJXVOmXXvpHfPm4fP7rtz98Z/usSz3+lQq1e/fnvuFSHl5+VjTNLb96lfPj6yv/0t2bN/eufJnj+37Uql1c/1Xv8WM279CaZn/rJcBGoj1hNm+7k22rF5JcyK1edp3Hps0bfq0yj0Ne/jFL55pVopZ3d3dx88C19bWlqVS8Z2Lf/7U1XNvu51Vb72x7/irz9fUBEcEv/03PyFYPRJDgZHt9XpvzG8QlAFnWppY+S9LaOnsaPPOWdhxx7z5V320cydLl/7yE2+pb+bp06dj/VxtWbJ03h13zr/r7rtZu2bNwVP/9cKYMiHwtW8+QNAbwOiOIN09SCiChCKQL+EIKhxBhcN4EGpGjuJww66yxNH9gePac+zGm26sikQiNY379/kSydT63uCqrCybXB6oeuS+RYvYv29f/OTKFz1+dIlXXFQrCznRjNhkRfdJzmIMEAExsqbUmh68holWGXf43deMg6NHJ+5btKjkgw8//IFh8lJnZ88nBoBWxpPf+e53DYC1Ly5bVSb6Mo8WSrQgx5uRY6cHSDMcz0q/vx/PSTNeJXi04EOPfe93L70JcP/99xu2bfwUwPT5fNU+n++rM2fO5P3332+uS3V9y9KCG8FSmtjRo3iN0uz+qqylemDnLhpDQDsFJGrHMG2F2xAyGi5Nhr65Y8f21unTZ9T4yrz3KqVHGC4X91x33XUmwN7N775nApbuk90nD5BpbUbaWqG9Dd3eju5o6y/t7dDehrS1kmltYffJ/ViA25nDBcbeLZs2AUyaNNkyDL5minDL7Nm3opSiNtQ0yUQwESydlXg6xc70Sf5CewliYSD9TqHu/anpIMUnJIiLjSVCGjAFTA21odNTlFLMunEWO7bvuMUUjKkTrriCvXv3RDyiJxpacGVXSc56W2uO6DhtKkmFFsocHchmtKhoukURNrJPG5YDdAEuDYaAV/TVjY0HesaNG+8Tw5hmuC1zFEBLS0urkQ3QPtFgILgQTC0IkAZSgEJQCClnTBwdF4KBOPf2iQBnzrS2GYaBZblGmWK4/ADxWCzqoS85iDOZDFiMS2ddV5Kz2EkGhgwECYLOzqOzxy0W7YkAiBgBw7btFIC3tMw/2JsrnS9OI5B2pPdt0AC9gdVZZxkBANu2k0Ymk2kDCI6oqsw1c/nNu8rVW8l+2ZFCkxRNzMhKUjQpNBlnv23nXfbAeTRQHayudMBtBlod6OrqZNz4CeVprcKqd4KsZBxgGk1KNEmBmGiijsScsZRo0s4CMnn3284CMqJCY8aOCXR2dqK0PmBokQ3r1q7D7/dLq7tyY8axMCOatDNZFqhJiCbuWNsLNrJjCUcnt4C0ZOew0WTQnDYr3/X5fLJx4wZE5B1DKVm1a9dHAIyYesPYjEBa+vYwJZAUSAgkHAtjookaWcl9Togm4eim8u5PS9YDNVNmXg7QsLsBreX3RjgcPtzW1rarubmZ+QvumtahXJvzrUzmWRvrZ61yxNnvPKuTA6xvt13bvjxv/tSW5mY6Ozt3hkKhoy4Ar6ek6dChg4vm3nY7oZJAJnG4oUIQESdD5Ud0v30XSBlZC1OGdjyTA/darwK3LcxcPm585ZJnl9ATinwvnkweNgC6wuF1x44d27R3714WfOWucZGrb3g7kee+eJ6LewPLcXU0bzwuuf2G3P3NoyevnzP3tsv3NOylqenkHzvD4fWQ197aikeW/nJJd1dnJ4//9On57V+a8Hoib7K4kQeUAWL0D7RcsJ2oqHv9wUcfu7Orq5MVK5Z3KS0P53j96lsgEPjyiKqKtW/891uu2tpalvzDMxsTW96s9yhMC8HUOCkxm07JO/fZk5A9dkmDTOSqWe/99fcfmRPtifHY3z6a6Q5F7gyFQhsKggFGjKh4wFviffG11153T59xHVu3bg3968/+7g9V3ae+0Zv0kX49l3ISjA2ccpe/NXvR9+uvnX5tRdOpJv7+xz9OxnpiD3d0d/97PqcXrLWeBcwGLnv11d96n3j88QVPPf108KHvPUwmk+HttWu71q96Y0dozzajJBUfXyqMA4gpfShmeY54JkzX19/6VzfMmDmjMpPOsOqtVbzyym9alz23fM23Fy1KACeAP4rIBwCitb4MeAQY5SxEt7a2qIaGBn70wx+OTKXTc5Y+t8w1d85cdN5KtdbYSqGVImPbJOIxotEo6/+wniXPPmsH/L4Ny5etaJk46Rqprq7JPTgooBn4Z9FaPw9UAHR1dSnbTsuZMy1GMpnItLZ2GFu3bq5d/fvVc0ZUjZB7F36d2fW3MmHCFZguF0pr0uk0Bxsb2bL5PV5fuZLuUEjfdffdG2+66ebW6mCVLvP5qa4OAoYEg8Gcg7tNIAIEADHdJnbcxmNZ6UQ05nK7TT1x4sRYRVV1/FTTqdLVa9bywgsvEImESKfSAFiWhT9QzqhL6rh25g3UjbokPnJkTaKkxFRaa8NtGbaIy+Up8eS2VgEx0VpXO66+HKfdbW9vV93d7RKNJl3xeNQOd4d1Mp0i3B3yRCKRsmgiYSVTaa9orS23lfR5vany8vKYLxCIeyxLKqoqtddbKh6PSVVVtQ4Gg5IHPQI8nx9ck4CbgSuBarJnvARsiUai4XBPmGQyqbWGRCxh2VrZAKYYLtNjZUyXSxsuU6oqyg1fwO91nhUSzvQdwB5gm4h8UvA4OYsoByYDY4EaoBLwAN7sYiDvZ4LsqUo60uNIK3AY2CMioYGM/wPREY0iGUY58wAAAABJRU5ErkJggg==", + "public": true + }, + { + "link": "/api/images/system/openstreet_map_system_widget_image.png", + "title": "\"OpenStreet Map\" system widget image", + "type": "IMAGE", + "subType": "IMAGE", + "fileName": "openstreet_map_system_widget_image.png", + "publicResourceKey": "Uyd9JmVKUcCF6PchzgNnAOjx9WvVN5ZE", + "mediaType": "image/png", + "data": "iVBORw0KGgoAAAANSUhEUgAAAMgAAACgCAMAAAB+IdObAAAC91BMVEXx7uip0t7////18/Cq0t708fDyyUzw7Oa92bHy8e5vz5fw7efx7+7y7+rx7uzz8+/9/fz08e3z8Oz8+/rBrLzx7OoAAAD5+PXPv8r6+fjOvsnVyNHv6ub39vTYzNPt6eW/2rPSw83t7OnRwcz39fLx7+rq5eLp4+Hr6uev1N/28/Hs5+Tn4N/Lusbo5+TaztTd09fL38DGtcPu6url5OPf1djJt8TY19Tu6+Xl3t7W4umslKrXytHa5efI3ePh2doXFxbh4N+52N7h7Nioj6bp4+Xf7tWcnbbm3uLb0dfMy8fEscCbmpjMu8htbW3g19nTxc/s5ujg2Nzf3drVxs7Crr6AgH/U1NGTkpD08/Lb2tikiqODhIJYWFcuLi6wr61ycnD01su+qbqOjYxISUji7fHR4efr7uPO3uLk3N3R0M17enmvma7g6Oiy1uDd1NrGxcG6vrOtrKmmpaLY6NC0nrGioaBOT07i2ty6preVdpSJi4pkZWQNDQ3r8/Xp7ODl7d3z0cR0dXRoaWjc0NSnssbCwb/E3Lm4uLdcXVxTU1I1NTP2+Pbb6+/j29+3orSihp/W6O3c6NXEs8K1tbGXlpWLaoqIh4YmJiXc3dqousyzsrB8fXxhYWDy5N3Hztbz3NOnw9L0wLAeHx7S0c/1zL28u7mpqKbm6um+2uKozdvVztbNzcu5s8SZepeegpw+Pz3y6eLG2NypyNbV5Muju8u5wrSen5ycgJuRcZHt9Oq/vrx3eHfm6+W0vsv1x7eehZ/IxsWepLvr7u7C3ub2sp+afpnQ197S4sfN4cOrrMH07uvDvszj6OHAxbz1rJblv0/k29ugr8OppaXU2+KTnJ7JxdGzqLywobb1uaadpKbVtVC5ydKCYIGCzqF+c05tR2y80LKB0KJtln68o1RNRTLy8eHIrVqIe1Sy5cjQvMimsrXMpKt91KH1pY6mk1ZYUkLRzMjoxLvxz2hvZkq0mEXHtJ+ol5LAso7UuGetmFi0nleznVd4V/r5AAAo9UlEQVR42rSXa2vTUBjHTzSgw7C80IH3S0VtUIuXQSUlkdVpvcx5H5udOnWJQZco4r0l6uamGwatlw3mNO6F2qxWoXOoMN20xYoUhNHaWeYn8JXfwCftujaus8PLD5q2aUjOL//nOTlFE66HQp4QjkuswnGswuL2gB0foXvA4Xjfg/1Cj8MB+35D4yfsr+lub2xsbG/v7MLyQoWuT0DXFVmW2TlzOE4cEu9LvBx4CyYZOt+DSrdB471jIM/ZP4HI39PV3dnZ3t6u23R35VG5jtSEqLrcruJ3vc3umgYYeX15sxrAjSqZkXeBxvtOLA+fDEf8vRDY5NEJoUQs6q5vqm9qqi8m9raocrUgCGdVO/6rSk/XsAZI5b3yJ8Mh/ygdkBlbh0qKuN3lTU1tBIhE5WZd5DtEYlTRc+jqcQCNWH7SlfU/dPR0cgSOXDGmvDfMhZs6CMLakpBrwoLQkqktQ9sD0OX5GVqAWAkDKA37H0A4o7JBZ2Niwt2b4J72EoSpudJirejo+B6g8NF0j89D0pit5xBCAsyEIo79J/RoGoH0rIajgMDYB3o6PVyAgNoqtpi9QwHwyEHPbz0olhbNKMmKrUuRjmAXWczA/9IBEGunkvCKSOiYvF6NwnPRCH2OjQHOT0Fpzm1diFLw4rgjoai/U8IRlY0AJmZ8DKDfsVH0xGJ6SgzKMHP6MjSCgI0FPIZT7wpHYZoHvmWseB6aS9O34wZRBngw4XN7YA5HjskiVkwk4JpohGW7Z0M4GdgxPTw8i7Msy7E0E1IidNpLlkOwi9Y0mo7S0h+IhCgNqiwIJkzO0urOWVix4NSXhkB2L0RGcowFlyTJ4xlkmaDAKtwFmmGYOk8KTZZZjhsK0cAXP80D48sF8Z4UctXdQkIsJnSCoZy9nutx3ZOIncQwMxph4RWDhkmwS/ivHknv5/2rSKI0wtGvowzgUbxRIeyROX9c5AA6TPu/gE0UXuMSUZSAAgusgBQ0gcJRK2xNZktwdH0NpDsEpwwjC9OfP/f1PXqIksw6tBhlYdbsAD7aA0TObyVnfbP5tl9gGH9dnTPo/XDhwytZHRyMcBGOsfU7W6O06uz30zQ1PpHm/UPy/dp6AjhQWVFmKampvEcQXg03kl4E46wgaFmnpi8XPejrO7js60QY3dJD04wdYk+SZaFJAgIKnaZpW0jfjVm+VT65X/XGS4NfSvnwazkyGGH8fs4/GImAiJ+PttJ0nqlPknSRt0rLNVmuPcxYqgvftb27VnGvur4EnBASDQuuLsfwEtiuVvfG1CCbOgdb0vegSGfS4hlgsnj5NpTFm6SG2SRIhjTAY8dzm+08ucnn89l85teyHI8H6+I0/QxEGDEe4fpVVW2V+3lwA5HMgI3gFKXp92WKWUPdSoPCK2cDDFFrLWs7sbnygPVJeROIANld35murEDM9b2joYrTKVqyZElRiqMlc2aQS+YhA2JS5A18EiQ826PwvM1mu0EuBBHnerIORMJeZ4RWB+VVg6JoC3OlX1S/Kl8AkWfDPSIxesBSCjuGm0yG7EGkXGGVfWGx6qzlHYhUW8xmc8OuwlSJs5leH56z8IDLFRBqybt7uXD5UhBZPK9o7cm2ebX7Dsx4UlPzBGUxJSWiMQjgKX0swyK7ncSOjTfJ03oiBWTp9k2tQW/panLLbdmvi0Q5Z1xtTYrE47SWvgUZjkyevHIdMjCkfAcRNxOs2kcQZyoqKs1PKw+XmRYhIDuUgWERSnW5rsUSVRVrObXF+vJJ072yXe6ywh/uO0t3ua3WhhOGXrcHTgVARfSadC+E0oGs2nwRkTvJTTd8x30kuee5bW5BED74dpLFdT7vgmDQ6VT7eYUejPM0reFGD2DdZODqrUkog1dRFFZggwVV+tRrssDGYiEKM7/bjb0uCa7DHR17qyp2gUjJMfgLcHhz89PCve4TRdceW601bYYbtwG4aDbDuidpkTFZc+nSkQIyjYnMQQE9DMN41yAjLyYnmf/xIhqBU36yYnY/SYVxHH/2tIZnB/DRAxyPcdIIkMKXLBLZyii2XszsPaNZSFJYrs3M1UU5IF2QuLaoFq50DXpbs82K2WaZ66JN1qy1bqp1031X/QX9zgHkRehtfTblyJFz9nl+39/vAY7D/qpBOrmATg4mch1KoxHH17O0yJev46a39gPmY4Gh2osHzUalcXSjXNnao7x7xmy+fR+l8W4Wcc9L0KkjHfJ4AVook1umg3tLE1BSKoFcp0MaSHlmK9C2Wb8/BlLuyqIU8fTpq02vvjx/jQrTJCRry6NktLqedjXeeWu/1W5uHZKahkxVamWncUzpaK9Stj54fSCzRTbP403cri+UPukFPOgv8Mz6E2WIw0Ga9DW6mg4amBvoF3Q9e7wFRBI0da2nabm9+H5L7YjdTnc69MpDMLXUt+B3AC5TVZFTEcDmRgJ0LEanYwfQf6ERL8pLJT0/65uabkiZXvQLtikUn+ZFNq2nARlF2QGpMgdLTVZPJrM1H+36EPon3LOVBTzSs2t9U9OrG78qiGSpQqFYXduV2l8NTpkO0QJI+FHecc5rXFlSAyK54aLnV9Xvn0X/gCe/Rn3II8ncfbuaju/98QQVgN7QoADWSiTrVyQqcs8JvQgqCXT0+5RGVUNDxYYdDTtWZY8tL0oR8vtt6K+gPd5QX31RGn+8Mj2wcjlFI6cT5aVq3TWFwOpGiURiEL9RGPQpE2NFVEHUG2NCY3e1paEBFl9ddTR7ALvTf4Tc6C9o7KvPrYTfLfR8pT8eaiwQn4UijdNzc9AcIvtPSBIYpiNOpeZj9/mNFKDTlVLUE6pCDx6SbqOlbBUSNiCjog6hrIn73/L0By2Wez/QmNPXiBZlg3pJEqcPUVRn9305BYCMgOlSj16pPqSXW2BB5ILJqqNH69QoP6WLVpai37KI4f0LNWZj6A+4gzIxzM3VqtVCe5SlLSI+OEPRrQZR4Mzhg1KquaWjdnSoR/mio6PFYj58u1ncTCv05etQPmQsazIRTvarQAlLyp9p61tYDg2vTYnyK1EhxiQoA9HDuF+hqEtI3JmJOCVi1SjUTVEQqdt329rO1F4YQIdeXt+rH2XNA5abzXKoiMiHfRaUB2Y8fL49vJH9TXPEpwYmF4q4O69zqev04MJ1vZc28UUED3WdQrE0WYwZZ2quUbLzYq+7Tra1HTaN3kfoQKDafO6ca8Biab2MUtTlK0kpUS1muI1WIuMJZhZBgeChBJVyBHMlMo5hCGFliBomRBSpHx7WkuFQUVGsmHDe2WKMOUIYTLTMYhUmJYWaBJZ9TDx44hsbmwGP6v2K8kZR415GB1HoNiVwDkRaqA4zQp2B6uZRPQKM9wdS/7luF51HhFNxbAnTRhjDOWsrt5IZD1oPsCvJ9XD4PMPzly+Fw3c5d/Hh/g6XINI3fvBC/9D0VGyk91w3V8z2tuGA42X/YdZgVUVP8igvzsTKO+84xc/fTghWg2K1EZ7zzUhQJrr20d7eZipw5nWnlOpuRujWgUvo+Y0Hl1BPj8NFCwMHRvySXSgPpD3YA6vPclEDDgxgTZTCjsv47sQw6RzFi1UBDNUiEy3EEBVF3kXn7AcnSMx0/dvz6CR38B0+G5bi7rOYVzF4EcrHE2eyF3z3EhnyVdco9lcLHmMoB5v/dO+3kOH6SajL3goo4TjY9Jw1o723HBbLduSx2TxoydK8M4sxj6raOXL/Isfx/cyNM5yMNOOhcb6U6ecgLzKsKtb0M37oEVGkfcrP9U+Pt/iZmOsyc34Sn+3ltIc6GKxijXljNTbzRJLkFBKw7P/+XTF4xxeBVOXgqRfxUiI+lIVl+3a3DchfERnLcQx70YVvRCcA/mIPj1ZyOFy7CJGgFEQQVk2NBJmitEjRcNDs6LBh7+1RLIg4WO3IAI9V/J6a8rLBnYN1looTg8tWr0YCEh+MpCQPRZENMK++RyIwbxcSS4jEKJHPWQsyWFYesgmgijK0kJW1ASIrYVT8rfOYYIxbHJyO2LHLrJWRfntSRGMlGRWpJNbp5iFOG3rgSIkcusBiFVMi0VcvB5aUA2JzRCIz98ZSHoZTD+G5tcLcpcegWfIQzxApLc1SXVtes1/08MJxzY6yteqcZPFWEybNVmy3Sgl3HZvCDGlz4deXCQm4cFIET0wyVLJHgsXD40HCRc3spGmYCCIPpFtHWhnQxotS99wpfklLV2g00/XxeGMyWKc0CMHYVSQjXlikT8jVm3soA3VZ+doaUcQtXNlYVbakAmXBbgxGrRMGjjt0xHWkjSGXwhAwljyPBod4LikyZQhaXYcTIucmjlj3bhyZDgZd76bcIPL87Brl3laGa7fOT61k9o0ajSYWj8caJYKK+iF4rAOP1FvYwtGyUb7POSXbINTYBni8YAJU76pD2WgJIZgHI4gWK5NxmCFaYR+BBxnBDGLwcKWXMLCTxMVoYTs3N3k6BK01UtmHiYlgbQm8spTBuVNLqIDbTf/k1UxCmwijOP74hmGM48RZYjMz2RyzSZqYGNtYsRBFBWMsiTV1xYpVkKhQd1slqCDuWHFBQZDiehFFEHdREUUREY8ePHgRFBT0oFffJGkS25ombfR3mTLNYX553/t/bybDsLNuMWxWMIUeJ1taUn4rDMqJnIh774CFl0wwmAUoAnkRYCEQhT66NLsfQBTzjV96cHOQg82O6ONHrfw4Kp9aHz/evn1qEw6QK0+dOriMLeTGX4dFDH8dxoljYoi1xlrQJZZmBibcnlyL9L7N5bLgKnTIyZOA7MbwLRLuAh3WG23WEoGAH4YgNyueWrn2xK3JW9/Mu1+4FTwBCzcug4qxqiZTfBogDofVE9O0NJTSxFFiU1Zk4arnNwHx2SxNkKMlJ/Jn96sJFsAfTGqaFmIdzUkYgpd40bdPncv11v21tyYfOIDVOIC3UVVRj4N7xAMFmGnBL1+0wurouWenKOGV3iF+fz0gLltneILbjQ9v3BCWBwyKKiIHIhrSqF9bIMBAefZgOc6dO+iFEhioFg/O7XL/1vCHUinQUW/Uj6MoytCwZ+vL+2n0iPgpm2uOMU9QTkN/QiridwBjtQKCIl4YymSMJC2lYUT44yZTMwsDkesBuR6jaRTp2DnbMLZ7+vLjTz70jLOZrm+Y0LVt9jGjca4sswNLjECRfYEElIWTSBaJg2poOgEl4HbeFh30Y7IfgO4wUaJIUbNpY482dtv09Ovgts7O6bHuFzHn7E4jYsGSDIE3UL5JGIXMPJzJ7FhGJKicR4/elIrsw2cMfhhcJIEi2zooUaBau2mqp3vs8cBrF3p07uw8HmhtPTnOiDTIchMMhWopt+IX4Ph7pqHhzIyZxAWVsufEiROziqlqwTavh8FxWgCong4KV1ak20D19EzY2bhKOI4i05tbdY77maSqtmgsDEWXKaxHwrTgtP610lHI4TMEOXOYSJXV4hHArD271xZr6iy2+UDssheo4IY5Ri9WZAp1nN6/wdPbq4ssj+kezhcak4hEIo0wNCmTiqNec2NzC85ixdi3M4AQkmkgSEOGEKiACf2/OQfGroX9ewrIFlagLDdehCkqfv0kRXU9gN7ebW1zjcnpptZW+frOKMNYIpHKojGhOXUDv7fLEg5EcA7XN5wo5ETqCJI9wDDwhk1t+6AMduwSUTAaqTwCB+d7zUV8OKc5WiJQIYXSs9PqZVl2as4U1ELkXdzU1gXlcCTzmx2XBXQunLehDwsMnmR0uiIwDJj6kCwn07UQCWJceaE8jpQ8INEeM3+QTkZg+IxMpFHGMmObtziGXg32gbvE5VIPj1P2/gcR9tOnficSzlCyLQxenEpSDFRAWusfSntLRXTR/yDyqZ9I1OlMNnoAAHfzYKVzwL4OVQt6oMi380WReBP8B5EB1XA252819KG9chwJWS32ivhNKHjEAP67CN6pYTHy4NBeFQ5Myjyc75svrxFta/7fIp5ocyPTL9KrIq32JYPgKywtU3cHC5XiBWDjDhvFDV+EjaY0jOwRwah9NaRW5TysjUm5ufIFyt3Tn+PZcBulCxiqFAlpWrAeRojcJyJc2JvPKzkBlWPArNxptsXiKBMMtLTSXW2BcYbqRILJKAsjpbC0uMcXGMQvq16oAio4+0Xc1hPp7Ok0d3dY5m4IB09qNFeNCJvywohhVRb6eJ/NK1mur0rEeO+GOd5B++Jh85NOmt5w7NixJ7TYJ8J//vr1Mz+YSDBaaPRpUAsRKIr0okhKlh1Vicy9122Oh32+cHzBDZoedwNFjhdFPo9GPvcX8dpjWqDvfslRk6xnSkT2PseH+E5ZZqsScdyzmONx2neXOTYbRa4fi22zF5fWV13kK4r8oeF02qdZg2qoCRxpexBqQsn3f/PtBcabwlavBgPEx5mbps++YaJNcUysyIblbWOKzT46yx8iCacW8uTmEVWWcbaoDSVJKwpvmX2pd1AVHIDZZXBb02NoUaTzcGVEEqkuK+RgPR4WakXUUrwm4f2FVCpVfaK7DQZaRxBF0WgURTf8XSSN6fJPsMoh6EO48CMlp2BYcBwUKSOiqvCPiKr2wp74/LXVb2JhePw/EU7IwvV76yKoqumCCFhMVkBE6h+JpGsggg9hERuZKAlQxOWaq1r7RHywz1TfhH/xY5XCD2wuroYinhqI8O1rVqxYsYTsmOmCAiK/UdkHxYpAoxqJqAumHuYhhzLTaKuhCFsTESMvSRKffXeCJz79jQsXL9URyQ0g8fiP86tAUAixeTsJkfQzvNlMFl3hfbUTgWS6FiI2UeSURQvJoi1H7pAr7Rd38VPx/YmlAnC2Q+fOHvn+WuBPXzx7ULJNXU8mrt98sX0ZuZjJLFFqKWKHCmDKi0wkCL9iMVmxY5Iy8axEFm0n9AxCOODougfSzMxrsvohr6y5Sq4dJq11T8m6i4R/eIXYoHoYZkQVSQyeVyLn1kXqEJroIosVV8PFMUQ6qhydgR66CBH5hz/5OpvgmzmDXFpBFrZLVqVOkhZdGY7H1M2blw0m4uiAinA6AOGQUg96yhjawPHtz/SCSDkRN794R/tm3ocikBMB8/xfR/Eg4uUvXkEa2hWLUqcMU+SOJJ0qFRH6UqtCEW2BWzTQBQwGEQDoYz3dMSPNt08wTaHprMg1xYb9rqzfRVAEtQ1L6wgnLTrFZxSRmpAhWRHegyLKsEUUci6TE8mcI0QEndZkCCqB/l3LmcY0VkVx/OTdPPHZ9rWviAUKrV2gTSltbVltFYhtUqAUStkEBATiAigIsqpRwaCIASIDSAbUKIqJymhiIiOuH9yNSzQmxn2Lxpi4G7cPnkdboFCgrfGXmTedxwzc/z3n3HvufefdpJCC5Co2+Al7O/tsVfkYoVr0Wjog5PYXUmSTXorqf5pKuYGSXMzQF59jkqTfIKQahiSS5o6gEFHiOSkXN/dTsphTFXQtNdXa8kVAyBctJ4NPSKrkaRAN5WfTttlTc7202+mcoQOJNTD0S70+3yYKKa2Q443hLhTSW57Y3NIy7fNJvMUP9zK075y66RtOStJlzdP3NyfKdoX48ou9iRAlUoJw0m1FlP5+1BFUghVd3LYQI0QnpIcuXSw129kZZypNz9pstSiE852uLbfP2HxUYsUgTVfgHEFJXHMafqKYmcXyPImNaUfXSpSkMMxzEr6ej0O3w1+QSCXKaSpqi6BXohCGoxkc5amVL8/a4cu67W9SkAfRgYvnuUWbq2mm2umjy10224aLFtGlp2trnW73mMNRNcpUjKJRNmiD3Uo3mRXyWWH5IMM8n4RCbLRezby1f4OqqjRDBFHCkIZ60tgsIoiEavlkV8gn05Rk+8XfaIVY6blBuqrJZR3F1rpttk0bzdDZm0Tbk9o753DMa+i5UppWDDKGnhn6JYViPpndyPPZTvtSnpQwQl6IFsIQVFXGECCk4BKTfwj81y0QisJI34V/+ibg3z6LDmetcAx3MgbpvLN9vJDZeRoZnffRTfPzVrnjlI+2C2m6dtBssNO0U6Fo6rW95PO5Ts+k++RMkr4AhYT3v041EsO0KyULyk7Qta2tPX1QiDHZAdHS5LK9hEJKafO8i1Y4nVaWzuDo2VNOZx4Gx8bzGOtuVJba3j6If/S+NFZLC+U06xq023tKGb1ef0BIrio8PJUPP/roGzo4DCJ8bBn6vGtrT6aHu1YLJbHE8MjFTfMDsA8HXRt2vA0/S0FKHw5DI0SbkpJCSTDU1QeFKFRZQTNsF7Y1Pvro+/73HzUeahLD+DLkL62tDTVST+8N9qcpX3IpRE2gdTzBhvKtYhn6KFKoXdLpF/cJuVKVBojp5XuWcwSA5lACgPxQm4jo7mUoeXptzdNI5RTvmOQLLBp0jwjgf8QgoSRbbdPF5xRPt01ihv+AFsJ57f2SEpPgWZ57nn320YcB0b0hh0Mgt/mhYNnbfYJIsKz3k6COu4ZwQvxfSaeo21paPv3qwzM+/OrTlpbbcK2yX8g2zwZpDI7Jb6QdOnAZoIjwaCnqyeIvUconXxYP8SnK/wjqSPTe9cMZIX64y5tIpe8T8qhRmZkFAlPJyy+/p4cA1fOZcCjLqgyRFBc+hKJK2u4655y72kr42vf/EwOVOPz122fs8vbXw/uru1+bgQhYj8iZ/DU7ldp8ti3By8WlCfA/gpWg3q/PCOdrLyURhQn56AI4yIj10UNtYlFACCKToBQZUdbIs+B/AovJn6JuuwvtEcbb9+9bED76PlwYSYpqVAcRKdJE2HXNUmkqBfD/cOetEkkLxsc+fmiRUHtN8j7AQ1dHaoPVmhBZR1Wk+1iBIC+AmCgwRrc1fO211FZLqPm//LKjpGWSMkCAtNL33w8ciRLJJGcr4SACjSbyolyXnKyAWDBq+AIdXRSCL6faPg02/veVS9r+DH7+tG1nCJbLjcBz/hURuib3bB0gSgfWho0odYJQqZji0AqT5FsgWtIysTgnF/9LeVY0Y+/0V4G2/3F/84Md9/8W+MtX06HJS6FJC71v+kwEZzk7CxCHIrCpN1JRkcm3QJV1WNs0yRaIlmSkEhCzEY4lkSr+MOBXd23hELlWHBDyYTEl4dsp33014KqI4S7PNmcqz1YExVrkRgsf1YVwGLeoohyDRQZAcwRD7hZBNPtngaZ/00KlfCShpoMmwT0PlDFvBuQIi4AyOTtbXhQW6HAURbsGObpxRAs1oUzZ6AgzScYhFnk7aAM+cbzjhoB93i6mEs+bn5fDHiEPQSQSdMo9IfdgK4BGMDQkbniB8IgRTszwc7woIGRX56wRkMq8cEOEkhytAczJIYd07DGJSCxmIki5g2oJxsiPq3dQd6x+vRMjd9x62zV3wg5XQRSYTrSVcdqE7mai1xMiZMlepIBYKiGEfXD7OhpuB4OBcMSgZbVusCTv7ljuCMkQ+9f8ejEnEu0P9us/Dc6CPxY/VhzKVT6doC6/5pozz9vjWNH4dXrr0ytabd3T3czJErLe5m1bI/4J7zoxPXl9FyGAyHcHLacdkKYeQM4eK9++lfvS89lapOp56yiYzcIgbIIypIQR142Pj5eJw0ATGaiT02cE+fA39KsA0yeo/Gs+P3fnJXn1hZdFI0TW/+6SUOvteFrf4SG395nqGtASZdcvd5Ut4wfeIcw7hcSCU025ubmWpiYAeF41+NIceuZnTnjnJYdWm213uxZh8sEyobAec1W1sM7TBwVdXQnGbIt6/MnlrTaxWG3SM2Lx2iQrFr/QKpCKqBQctsIIBsyb5yK33rgbIceTIVt/l2fgaf11DWznGt1cx+Z3T031da0/GRSiVBWG5rrNjz9DFjcLoEeBfFwEhZ85AOx2rWHR7XK1o5CVLuFUM3QvqNs6TkB9Q4NXqFb4x1vVk9eJxes5J3PE1w0M5IvVtw3dvsykUBPoW/v4dYJ6CmXwSgQ750hEIYUMvNs/tfLuu93ijgZ2mCUddY1Tff0TXUgdEQffpw+lnJubbre7cHOzQLC4LcQCuZ8pUcjzWtfHLpcL/11hW53wkgGhv9u/9IIJQDc5LBQmmcY9ePiJ2FOfk9MsXBmoF6NxSjrLxITqKj6QNN7wCPXEuQFC8a6/Sh1FsA/050xN1fc3kAUPWSKkdank+r6+8YAQKWyTF0ohT23ydVGbToAxBbKoBOPHfNCc0tp4IaXQUMcLeUEo6NYvPVKQ3716fSfGi3q8tWNlUt2JQh7LZx/0qoXr3d2dfjGm8VPf7hPybSe+4BXkWogBaVeXXp1E/H2kr5HkEKLPIX1bL6yZGk92PBLa9LNCkHZeiHYzGWB0rF2hOA2QMMYH/PNaZtHlKkyF/K2264SX5AtN3YynTJk/PrnVqcfAH//JXzcgHkIhHWLkEtMlW+pOpRjDPef+MJPgJFJGXRsScjlER+P6wBoASVermeAEglxMdmBEEG4RKGh3ZWQYy3njNH28YQckm5/77VoCY6deGoPV8fFJ4ZReWNIt62r2+Fe8w8MmoVDbUMAK2sTovHVCcf6S16NuHh5ezWf4ObH71/AIGaZSgjJuvQmiZD090RP5IIUMEGXALkU1xx2KwEtOq3ZBmhoNsH1iUGIimJiMEj+erpXBe6iYEQfx+xkxm29ixSIAjkq/65s9Or65YTlkkFcKIFoGhCgkujKL6J5MiEAYzu7JQbliXknogmDuAIiMum16j5DpumCEnHdTbI/Z8mE/nCEJezCcrOQ0+G8UXXn4sv2S3Xj/9K47KOmr55776kUxP/hEpCLohhC0oPnplZOydC0n2rvqKoIYoQmhpbvumes4fCNFiM4VcqwyKgXiRSwuaezcsYdpqqO1sftBGY+WhJpi0USZybshAE6lIgFNuFDimaYRAGxBJFKoB4M7EG/ftcTvoMSJiHgnvJ2Q5WkbLxETMjBlMplum5KVnZjwyDpWV0NBUhmlECFJZwAYcrK7YYkmu2iUMDkAIj6/FB3cE/oxkAMPSygO4oX1bJW0dsLQ9Q11XkJIsxeF+G9nB5qV9c0Ny8tPhjb3ISpcra3AMCK6cSKHIHRjPkv0gLCgHBhSEoOhq48QCINQKY/xYfItHsCghbjRe7uYrk7gT03j08X8bhNyiXbAA4XNJ5eXh/3Akxll3Vmrp3V1jWFIRxlN0CALZWUL/q3rC+F6HZQNPblqKFlaX+8wcBCGDN+y+/uMf4qXqdfvhrhhx/tNJ1DIegnhUbehjpzbZShEcF3X8vLtukC4q4wQDcMPtnZ4GIbtLqEZMWlsKCsbaG2dKCnxYHzk3zZM1hf86mGT+ECYbBX/VbxFSZ67F+KGGfD2j09B/e3blRZEPOTpH7hkWdY/AFDmeeTJB3fXu9Hw2EpHxyRvkT4+QIbQIredbJ3Qaoc8kwsLfZ0lT3powyU5ZH+cpuC++TolkV76AMSNWL20lM9PJ1v8RURI2ZBHnS575BFATvRBkAJFJkTBar2JZRgpaVztYlBI920vrDzSOqHOuc7j8ZR4Ov0n+w2GOhMN+3BQiISDu/Fl8DixOPZNVRzZRkYgHKM5Kvs2X+dtZIEjj7R1r5aQAW/bCeGJtomJDpYsDU8NGRonDYacEinsIw+PN0jBu5e+eDPExy3lGogOwTVl/oTjhSAZge6ox3GLwd+kqw6viDzXRwwmAyEGKMjSvZNrKbWMFIAgTWm8JdRJj39wX5zhnmfNgii5BunLFxwjhM3YraXgIEPMjx8LDKGxbEaO4CIGr6k71Mi32U0cHojTJOUWiJY3r+HpgtiQignCJ7pwpfyWkSKLQq4o3OuwcqTGuGPrx/EUqDionM+CaKlHGQjEigg3yIIhnbXzmDn8MLbS1Ozs0pHMYKk/xJ4wplWkQtQUBEwC8VN5VOpZaJYrKnUC0evcA5cKYsmra8urdA5rKcSgpJ6X0meCeMlyAHKUFrOZ46QAMcwmgnl5UfLZVmsmxEb+NdeUQdzkwdEkGTKLXsdKfe7Smx+P+mkMxEdm34MQN1Y4hrmNjXIOQSVhCTqhCY1wDMdIUSgi4skAcFSzCMSDXFHwPwmxnC6d8dm4AHs7zyUkIYQkDJubtdliFXJ56KmE+X8S0mSmhTabrmKuB4XY7c7BW0ZnUcao0+ks6GHIldlEbp8brL6YjJ6qrSIz1YSc7XaPzs1Z2SSIh3dSIT6KFHAkL7kIa3MNjjKMneMW0/I2mwxzt0D5bJV8TLqRlTSnMOfZbFgOtmgwzNaSvGwyg2V5VpttMU7fKqiA+FAojxEiJDRDxnxCZlFaOshduWn1jdXAKULcG1x7aWY57bTZbKfo0naDYTRPuOi21TqTmmyoLU6LGB0QF7lKOJqxaoIs+mzMBjeYzOnmfO6xXLALyZyba7c7aQZbrWmia9sNZNHGnna57KnsnM020xSnEEWcQlLT4Gja5TYU0uOSb+RyPS7OMcoljanAOW9bdHO1TiwqtPscG4O0vN2gstN0U7J70ceikQZr440RR7S5CYRRIYBjaOrxoZLTp0c5TsFx1dUc164T2Z092a9z7tNCmq529tTaaKbn+UEs/Wx/vimVZXs3Z+McfhFFdG+iEcKELxmi2QwjNDGZuAP43HJ6l6TBUrwUJrEBkiBOEizHW4OjyUg1oXHmyggeQ2CJwoYIwJ29vZhySYNw996XyVVaN2laBBlSGqnOnrfytZJQyuLJwBkQP4LDhy2+SxHCA1KyTXp6uhSK5EqImrCJ6u67bwbLLYAt50Ep7opeXhVA6GXb/yNnmtHUztBmlWp+hJBZQuSzGlo6aFclK3OV/815E14PdD1aheGCzzsKayrT4D+RDYdhrxqcTTRnq1SniW6TXNljrwSctHoUXIwJfyHs494P4CA6FZZm/RdyjYeOoMxge2JTj8s1RjKbyKlFhmkfcbtPV8coROA40NURV47KqlSUHD9pjgSITDJpT754Q+OaOUXsabZFzDZ6erG6XyiNdYg3wy5HrBx1qXL4LxTmQmSqSQ89slnhqh0kG5nVi2aGmetVtNuJCGLjSk3lQed6TgAH0CTDf8GYVwQRYYmLVDc1DTpdmDPpFmcYprfHbp8lEKtraTQH+urx+x6I0BJVEfwHkvMqHJZCJRyAIGfPAdgI8RECDJLkYhkGYiEzV1E9o9GYD3z/++AglvjLR5UWvrNGVCo8kGvkQGk6Ic48EgQwFEVSFCOK7Q13+QjLuhQa1TsJ+wL+0ghRIh+JV0dNYEjRWYyZiipL2j4l24u4YGxnpWp0sdkiNdtqTZX7WB6fRVXzTlrYyc4RxuAseSXEh2Xv+cFmVU1lZni/SRnpjgEKVckW/HL0MW5FXPgEkzAsYsvVqCxBz9HxXXaz4GCQyI0QBwW55vCB2FKDLlaDPyQyaZWq1NTju0y047V4kanr9VpZuoFBMTqNRlWJ90dUVqsGIm0U5SoS4tqZskT0iFsUusNdMQtd8Ri0raHhmxfiuW6pbkuGpCPoptY8tJM5l68R/WCfSRLM6Lyxo5QrDmlTpkUOh+IwHivkdr6BgpMePgVIb8AHXVNa2SPv6gPBxhYMLAyUQSqa5LlL982HeQqIHYEZdRyGWVGYddiXSuEY0h/jtQqGFh7Tc8DwQi5JZ04MrfK93ZrF6oa0KQNQka2ES996fHdWrsjLVl0ZV4plSThqkVKVrIKIFB57lqd6OguQrobH1gwgbGj2jjfKtPr+8f6LL25rVmcI3mVlA/BddiXc/dauSVTYrfFRA8eSAZFIPS4e2ccAaWhbeMxvEBkWGhoZmey2pZXxd4UXd+SwGbA2sF4GluzsF1988bndHDzemVCZd7wMJrIQ5XGuNZWfCXD9uqszh4Csf4HFSJ+S9Y33r11Xz6KQgGFRyOOPP7DjAlhBHh+lFXAMYkiMZJK0iuP6TvvC6rC3oGN1ZSJHBAb/IzhgybauX+o4ccLb0c/CrhA0rSAg4763vr/y5vvuizDXSznRMQtcy3EGSQQqUXxQivHY85lwOazPAoER618AgCMcXmg1GyDYG+98h0L2ppA/6zIff/zeAzMLRwgdrovkc9KMPVOf5RgZ4kTqXxwjVydGn1AkAAAAAElFTkSuQmCC", + "public": true + } + ], "scada": false, - "tags": null + "tags": [ + "mapping", + "gps", + "navigation", + "geolocation", + "satellite", + "directions" + ] } \ No newline at end of file diff --git a/ui-ngx/src/app/modules/home/components/widget/config/basic/map/map-basic-config.component.ts b/ui-ngx/src/app/modules/home/components/widget/config/basic/map/map-basic-config.component.ts index 74c57519b7..34cca1f8ff 100644 --- a/ui-ngx/src/app/modules/home/components/widget/config/basic/map/map-basic-config.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/config/basic/map/map-basic-config.component.ts @@ -46,6 +46,19 @@ export class MapBasicConfigComponent extends BasicWidgetConfigComponent { return this.mapWidgetConfigForm; } + protected setupDefaults(configData: WidgetConfigComponentData) { + const settings = configData.config.settings as MapWidgetSettings; + if (settings?.markers?.length) { + settings.markers = []; + } + if (settings?.polygons?.length) { + settings.polygons = []; + } + if (settings?.circles?.length) { + settings.circles = []; + } + } + protected onConfigSet(configData: WidgetConfigComponentData) { const settings: MapWidgetSettings = mergeDeepIgnoreArray({} as MapWidgetSettings, mapWidgetDefaultSettings, configData.config.settings as MapWidgetSettings); diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/maps-legacy/providers/image-map.ts b/ui-ngx/src/app/modules/home/components/widget/lib/maps-legacy/providers/image-map.ts index 7d3dfdf3f4..fcac189aab 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/maps-legacy/providers/image-map.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/maps-legacy/providers/image-map.ts @@ -127,32 +127,33 @@ export class ImageMap extends LeafletMap { return this.imageFromAlias(result); } - private imageFromUrl(url: string): Observable { + private imageFromUrl(url: string, update = false): Observable { return loadImageWithAspect(this.ctx.$injector.get(ImagePipe), url).pipe( switchMap( aspectImage => { if (aspectImage) { return of({ imageUrl: aspectImage.url, aspect: aspectImage.aspect, - update: false + update }); } else { - return this.imageFromUrl(defaultImageMapProviderSettings.mapImageUrl); + return this.imageFromUrl(defaultImageMapProviderSettings.mapImageUrl, update); } } ), - catchError(() => this.imageFromUrl(defaultImageMapProviderSettings.mapImageUrl)) + catchError(() => this.imageFromUrl(defaultImageMapProviderSettings.mapImageUrl, update)) ); } private imageFromAlias(alias: Observable<[DataSet, boolean]>): Observable { return alias.pipe( switchMap(res => { + const update = res[1]; const url = res[0][0][1]; const mapImage: MapImage = { imageUrl: null, aspect: null, - update: res[1] + update }; return loadImageWithAspect(this.ctx.$injector.get(ImagePipe), url).pipe( switchMap((aspectImage) => { @@ -161,10 +162,10 @@ export class ImageMap extends LeafletMap { mapImage.imageUrl = aspectImage.url; return of(mapImage); } else { - return this.imageFromUrl(defaultImageMapProviderSettings.mapImageUrl); + return this.imageFromUrl(defaultImageMapProviderSettings.mapImageUrl, update); } }), - catchError(() => this.imageFromUrl(defaultImageMapProviderSettings.mapImageUrl)) + catchError(() => this.imageFromUrl(defaultImageMapProviderSettings.mapImageUrl, update)) ); }) ); diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/maps/data-layer/circles-data-layer.ts b/ui-ngx/src/app/modules/home/components/widget/lib/maps/data-layer/circles-data-layer.ts index d53d30e6e5..313af2c4fb 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/maps/data-layer/circles-data-layer.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/maps/data-layer/circles-data-layer.ts @@ -30,6 +30,7 @@ import { MapDataLayerType, TbDataLayerItem } from '@home/components/widget/lib/m class TbCircleDataLayerItem extends TbDataLayerItem { private circle: L.Circle; + private circleStyle: L.PathOptions; constructor(data: FormattedData, dsData: FormattedData[], @@ -41,10 +42,10 @@ class TbCircleDataLayerItem extends TbDataLayerItem, dsData: FormattedData[]): L.Layer { const circleData = this.dataLayer.extractCircleCoordinates(data); const center = new L.LatLng(circleData.latitude, circleData.longitude); - const style = this.dataLayer.getShapeStyle(data, dsData); + this.circleStyle = this.dataLayer.getShapeStyle(data, dsData); this.circle = L.circle(center, { radius: circleData.radius, - ...style + ...this.circleStyle }); this.updateLabel(data, dsData); return this.circle; @@ -63,7 +64,19 @@ class TbCircleDataLayerItem extends TbDataLayerItem, dsData: FormattedData[]): void { + protected doUpdate(data: FormattedData, dsData: FormattedData[]): void { + this.circleStyle = this.dataLayer.getShapeStyle(data, dsData); + this.updateCircleShape(data); + this.updateTooltip(data, dsData); + this.updateLabel(data, dsData); + this.circle.setStyle(this.circleStyle); + } + + protected doInvalidateCoordinates(data: FormattedData, _dsData: FormattedData[]): void { + this.updateCircleShape(data); + } + + private updateCircleShape(data: FormattedData) { const circleData = this.dataLayer.extractCircleCoordinates(data); const center = new L.LatLng(circleData.latitude, circleData.longitude); if (!this.circle.getLatLng().equals(center)) { @@ -72,10 +85,6 @@ class TbCircleDataLayerItem extends TbDataLayerItem { - return defaultBaseCirclesDataLayerSettings; + protected defaultBaseSettings(map: TbMap): Partial { + return defaultBaseCirclesDataLayerSettings(map.type()); } protected doSetup(): Observable { diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/maps/data-layer/map-data-layer.ts b/ui-ngx/src/app/modules/home/components/widget/lib/maps/data-layer/map-data-layer.ts index c14937ef00..6d52838751 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/maps/data-layer/map-data-layer.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/maps/data-layer/map-data-layer.ts @@ -48,11 +48,13 @@ export abstract class TbDataLayerItem; protected constructor(data: FormattedData, dsData: FormattedData[], protected settings: S, protected dataLayer: D) { + this.data = data; this.layer = this.create(data, dsData); if (this.settings.tooltip?.show) { this.createTooltip(data.$datasource); @@ -68,13 +70,24 @@ export abstract class TbDataLayerItem, dsData: FormattedData[]): L; + protected abstract doUpdate(data: FormattedData, dsData: FormattedData[]): void; + + protected abstract doInvalidateCoordinates(data: FormattedData, dsData: FormattedData[]): void; + protected abstract unbindLabel(): void; protected abstract bindLabel(content: L.Content): void; protected abstract createEventListeners(data: FormattedData, dsData: FormattedData[]): void; - public abstract update(data: FormattedData, dsData: FormattedData[]): void; + public invalidateCoordinates(): void { + this.doInvalidateCoordinates(this.data, this.dataLayer.getMap().getData()); + } + + public update(data: FormattedData, dsData: FormattedData[]): void { + this.data = data; + this.doUpdate(data, dsData); + } public remove() { this.layer.off(); @@ -237,7 +250,7 @@ export abstract class TbMapDataLayer, inputSettings: S) { - this.settings = mergeDeepIgnoreArray({} as S, this.defaultBaseSettings() as S, inputSettings); + this.settings = mergeDeepIgnoreArray({} as S, this.defaultBaseSettings(map) as S, inputSettings); if (this.settings.groups?.length) { this.settings.groups.forEach((group) => { this.groupsState[group] = true; @@ -325,6 +338,10 @@ export abstract class TbMapDataLayer item.invalidateCoordinates()); + } + public getCtx(): WidgetContext { return this.map.getCtx(); } @@ -349,7 +366,7 @@ export abstract class TbMapDataLayer; + protected abstract defaultBaseSettings(map: TbMap): Partial; protected abstract doSetup(): Observable; 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 56c45164dc..0a8c4e1983 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 @@ -18,15 +18,18 @@ import { BaseMarkerShapeSettings, ClusterMarkerColorFunction, DataLayerColorType, - defaultBaseMarkersDataLayerSettings, isValidLatLng, + defaultBaseMarkersDataLayerSettings, + isValidLatLng, loadImageWithAspect, - MapStringFunction, MapType, + MapStringFunction, + MapType, MarkerIconInfo, MarkerIconSettings, MarkerImageFunction, MarkerImageInfo, MarkerImageSettings, MarkerImageType, + MarkerPositionFunction, MarkersDataLayerSettings, MarkerShapeSettings, MarkerType, @@ -48,7 +51,12 @@ import { } from '@home/components/widget/lib/maps/models/marker-shape.models'; import { MatIconRegistry } from '@angular/material/icon'; import { DomSanitizer } from '@angular/platform-browser'; -import { MapDataLayerType, TbDataLayerItem, TbMapDataLayer } from '@home/components/widget/lib/maps/data-layer/map-data-layer'; +import { + MapDataLayerType, + TbDataLayerItem, + TbMapDataLayer +} from '@home/components/widget/lib/maps/data-layer/map-data-layer'; +import { TbImageMap } from '@home/components/widget/lib/maps/image-map'; class TbMarkerDataLayerItem extends TbDataLayerItem { @@ -64,7 +72,7 @@ class TbMarkerDataLayerItem extends TbDataLayerItem, dsData: FormattedData[]): L.Marker { - this.location = this.dataLayer.extractLocation(data); + this.location = this.dataLayer.extractLocation(data, dsData); this.marker = L.marker(this.location, { tbMarkerData: data }); @@ -86,15 +94,23 @@ class TbMarkerDataLayerItem extends TbDataLayerItem, dsData: FormattedData[]): void { - const position = this.dataLayer.extractLocation(data); + protected doUpdate(data: FormattedData, dsData: FormattedData[]): void { + this.marker.options.tbMarkerData = data; + this.updateMarkerPosition(data, dsData); + this.updateTooltip(data, dsData); + this.updateMarkerIcon(data, dsData); + } + + protected doInvalidateCoordinates(data: FormattedData, dsData: FormattedData[]): void { + this.updateMarkerPosition(data, dsData); + } + + private updateMarkerPosition(data: FormattedData, dsData: FormattedData[]) { + const position = this.dataLayer.extractLocation(data, dsData); if (!this.marker.getLatLng().equals(position)) { this.location = position; this.marker.setLatLng(position); } - this.marker.options.tbMarkerData = data; - this.updateTooltip(data, dsData); - this.updateMarkerIcon(data, dsData); } private updateMarkerIcon(data: FormattedData, dsData: FormattedData[]) { @@ -310,6 +326,7 @@ export class TbMarkersDataLayer extends TbMapDataLayer; + private positionFunction: CompiledTbFunction; constructor(protected map: TbMap, inputSettings: MarkersDataLayerSettings) { @@ -333,8 +350,8 @@ export class TbMarkersDataLayer extends TbMapDataLayer { - return defaultBaseMarkersDataLayerSettings; + protected defaultBaseSettings(map: TbMap): Partial { + return defaultBaseMarkersDataLayerSettings(map.type()); } protected doSetup(): Observable { @@ -358,6 +375,16 @@ export class TbMarkersDataLayer extends TbMapDataLayer(this.getCtx().http, this.settings.positionFunction, ['origXPos', 'origYPos', 'data', 'dsData', 'aspect']).pipe( + map((parsed) => { + this.positionFunction = parsed; + return null; + }) + ) + ); + } setup$.push(this.markerIconProcessor.setup()); return forkJoin(setup$).pipe(map(() => null)); } @@ -496,9 +523,13 @@ export class TbMarkersDataLayer extends TbMapDataLayer): L.LatLng { - const position = this.extractPosition(data); + public extractLocation(data: FormattedData, dsData: FormattedData[]): L.LatLng { + let position = this.extractPosition(data); if (position) { + if (this.map.type() === MapType.image && this.positionFunction) { + const imageMap = this.map as TbImageMap; + position = this.positionFunction.execute(position.x, position.y, data, dsData, imageMap.getAspect()) || {x: 0, y: 0}; + } return this.map.positionToLatLng(position); } else { return null; diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/maps/data-layer/polygons-data-layer.ts b/ui-ngx/src/app/modules/home/components/widget/lib/maps/data-layer/polygons-data-layer.ts index de938a2809..419a3e0d4c 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/maps/data-layer/polygons-data-layer.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/maps/data-layer/polygons-data-layer.ts @@ -32,6 +32,7 @@ class TbPolygonDataLayerItem extends TbDataLayerItem, dsData: FormattedData[], @@ -43,9 +44,9 @@ class TbPolygonDataLayerItem extends TbDataLayerItem, dsData: FormattedData[]): L.Layer { const polyData = this.dataLayer.extractPolygonCoordinates(data); const polyConstructor = isCutPolygon(polyData) || polyData.length !== 2 ? L.polygon : L.rectangle; - const style = this.dataLayer.getShapeStyle(data, dsData); + this.polygonStyle = this.dataLayer.getShapeStyle(data, dsData); this.polygon = polyConstructor(polyData, { - ...style + ...this.polygonStyle }); this.polygonContainer = L.featureGroup(); @@ -68,14 +69,25 @@ class TbPolygonDataLayerItem extends TbDataLayerItem, dsData: FormattedData[]): void { + protected doUpdate(data: FormattedData, dsData: FormattedData[]): void { + this.polygonStyle = this.dataLayer.getShapeStyle(data, dsData); + this.updatePolygonShape(data); + this.updateTooltip(data, dsData); + this.updateLabel(data, dsData); + this.polygon.setStyle(this.polygonStyle); + } + + protected doInvalidateCoordinates(data: FormattedData, _dsData: FormattedData[]): void { + this.updatePolygonShape(data); + } + + private updatePolygonShape(data: FormattedData) { const polyData = this.dataLayer.extractPolygonCoordinates(data); - const style = this.dataLayer.getShapeStyle(data, dsData); if (isCutPolygon(polyData) || polyData.length !== 2) { if (this.polygon instanceof L.Rectangle) { this.polygonContainer.removeLayer(this.polygon); this.polygon = L.polygon(polyData, { - ...style + ...this.polygonStyle }); this.polygon.addTo(this.polygonContainer); } else { @@ -83,13 +95,10 @@ class TbPolygonDataLayerItem extends TbDataLayerItem { @@ -108,8 +117,8 @@ export class TbPolygonsDataLayer extends TbShapesDataLayer { - return defaultBasePolygonsDataLayerSettings; + protected defaultBaseSettings(map: TbMap): Partial { + return defaultBasePolygonsDataLayerSettings(map.type()); } protected doSetup(): Observable { diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/maps/geo-map.ts b/ui-ngx/src/app/modules/home/components/widget/lib/maps/geo-map.ts new file mode 100644 index 0000000000..ee5baeb109 --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/widget/lib/maps/geo-map.ts @@ -0,0 +1,141 @@ +/// +/// Copyright © 2016-2024 The Thingsboard Authors +/// +/// Licensed under the Apache License, Version 2.0 (the "License"); +/// you may not use this file except in compliance with the License. +/// You may obtain a copy of the License at +/// +/// http://www.apache.org/licenses/LICENSE-2.0 +/// +/// Unless required by applicable law or agreed to in writing, software +/// distributed under the License is distributed on an "AS IS" BASIS, +/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +/// See the License for the specific language governing permissions and +/// limitations under the License. +/// + +import { + DEFAULT_ZOOM_LEVEL, + defaultGeoMapSettings, + GeoMapSettings, latLngPointToBounds, + MapZoomAction, TbCircleData +} from '@home/components/widget/lib/maps/models/map.models'; +import { WidgetContext } from '@home/models/widget-component.models'; +import { DeepPartial } from '@shared/models/common'; +import { forkJoin, Observable, of } from 'rxjs'; +import L, { LatLngBounds, LatLngTuple } from 'leaflet'; +import { map, tap } from 'rxjs/operators'; +import { TbMapLayer } from '@home/components/widget/lib/maps/map-layer'; +import { TbMap } from '@home/components/widget/lib/maps/map'; + +export class TbGeoMap extends TbMap { + + constructor(protected ctx: WidgetContext, + protected inputSettings: DeepPartial, + protected containerElement: HTMLElement) { + super(ctx, inputSettings, containerElement); + } + + protected defaultSettings(): GeoMapSettings { + return defaultGeoMapSettings; + } + + protected createMap(): Observable { + const map = L.map(this.mapElement, { + scrollWheelZoom: this.settings.zoomActions.includes(MapZoomAction.scroll), + doubleClickZoom: this.settings.zoomActions.includes(MapZoomAction.doubleClick), + zoomControl: this.settings.zoomActions.includes(MapZoomAction.controlButtons), + zoom: this.settings.defaultZoomLevel || DEFAULT_ZOOM_LEVEL, + center: this.defaultCenterPosition + }).setView(this.defaultCenterPosition, this.settings.defaultZoomLevel || DEFAULT_ZOOM_LEVEL); + return of(map); + } + + protected onResize(): void {} + + protected fitBounds(bounds: LatLngBounds) { + if (bounds.isValid()) { + if (!this.settings.fitMapBounds && this.settings.defaultZoomLevel) { + this.map.setZoom(this.settings.defaultZoomLevel, { animate: false }); + if (this.settings.useDefaultCenterPosition) { + this.map.panTo(this.defaultCenterPosition, { animate: false }); + } + else { + this.map.panTo(bounds.getCenter()); + } + } else { + this.map.once('zoomend', () => { + let minZoom = this.settings.minZoomLevel; + if (this.settings.defaultZoomLevel) { + minZoom = Math.max(minZoom, this.settings.defaultZoomLevel); + } + if (this.map.getZoom() > minZoom) { + this.map.setZoom(minZoom, { animate: false }); + } + }); + if (this.settings.useDefaultCenterPosition) { + bounds = bounds.extend(this.defaultCenterPosition); + } + this.map.fitBounds(bounds, { padding: [50, 50], animate: false }); + this.map.invalidateSize(); + } + } + } + + protected doSetupControls(): Observable { + return this.loadLayers().pipe( + tap((layers: L.TB.LayerData[]) => { + if (layers.length) { + const layer = layers[0]; + layer.layer.addTo(this.map); + this.map.attributionControl.setPrefix(layer.attributionPrefix); + if (layers.length > 1) { + const sidebar = this.getSidebar(); + L.TB.layers({ + layers, + sidebar, + position: this.settings.controlsPosition, + uiClass: 'tb-layers', + paneTitle: this.ctx.translate.instant('widgets.maps.layer.map-layers'), + buttonTitle: this.ctx.translate.instant('widgets.maps.layer.layers'), + }).addTo(this.map); + } + } + }) + ); + + } + + private loadLayers(): Observable { + const layers = this.settings.layers.map(settings => TbMapLayer.fromSettings(this.ctx, settings)); + return forkJoin(layers.map(layer => layer.loadLayer(this.map))).pipe( + map((layersData) => { + return layersData.filter(l => l !== null); + }) + ); + } + + public positionToLatLng(position: {x: number; y: number}): L.LatLng { + return L.latLng(position.x, position.y) as L.LatLng; + } + + public toPolygonCoordinates(expression: (LatLngTuple | LatLngTuple[] | LatLngTuple[][])[]): any { + return (expression).map((el) => { + if (!Array.isArray(el[0]) && el.length === 2) { + return el; + } else if (Array.isArray(el) && el.length) { + return this.toPolygonCoordinates(el as LatLngTuple[] | LatLngTuple[][]); + } else { + return null; + } + }).filter(el => !!el); + } + + public convertCircleData(circle: TbCircleData): TbCircleData { + const centerPoint = latLngPointToBounds(new L.LatLng(circle.latitude, circle.longitude), this.southWest, this.northEast); + circle.latitude = centerPoint.lat; + circle.longitude = centerPoint.lng; + return circle; + } + +} diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/maps/image-map.ts b/ui-ngx/src/app/modules/home/components/widget/lib/maps/image-map.ts new file mode 100644 index 0000000000..e781f1c62a --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/widget/lib/maps/image-map.ts @@ -0,0 +1,293 @@ +/// +/// Copyright © 2016-2024 The Thingsboard Authors +/// +/// Licensed under the Apache License, Version 2.0 (the "License"); +/// you may not use this file except in compliance with the License. +/// You may obtain a copy of the License at +/// +/// http://www.apache.org/licenses/LICENSE-2.0 +/// +/// Unless required by applicable law or agreed to in writing, software +/// distributed under the License is distributed on an "AS IS" BASIS, +/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +/// See the License for the specific language governing permissions and +/// limitations under the License. +/// + +import { + defaultImageMapSettings, + defaultImageMapSourceSettings, + ImageMapSettings, + imageMapSourceSettingsToDatasource, + ImageSourceType, + loadImageWithAspect, + MapZoomAction, + TbCircleData +} from '@home/components/widget/lib/maps/models/map.models'; +import { WidgetContext } from '@home/models/widget-component.models'; +import { DeepPartial } from '@shared/models/common'; +import { Observable, of, ReplaySubject, switchMap } from 'rxjs'; +import L, { LatLngBounds, LatLngLiteral, LatLngTuple } from 'leaflet'; +import { TbMap } from '@home/components/widget/lib/maps/map'; +import { ImagePipe } from '@shared/pipe/image.pipe'; +import { catchError } from 'rxjs/operators'; +import { DataSet, widgetType } from '@shared/models/widget.models'; +import { WidgetSubscriptionOptions } from '@core/api/widget-api.models'; +import { isNotEmptyStr } from '@core/utils'; +import { EntityDataPageLink } from '@shared/models/query/query.models'; + +interface ImageLayerData { + imageUrl: string; + aspect: number; + update?: boolean; +} + +export class TbImageMap extends TbMap { + + private maxZoom: number; + private width: number; + private height: number; + private imageLayerData: ImageLayerData; + private initMapSubject: ReplaySubject; + + private imageOverlay: L.ImageOverlay; + + constructor(protected ctx: WidgetContext, + protected inputSettings: DeepPartial, + protected containerElement: HTMLElement) { + super(ctx, inputSettings, containerElement); + } + + protected defaultSettings(): ImageMapSettings { + return defaultImageMapSettings; + } + + protected createMap(): Observable { + this.maxZoom = 4; + this.width = 0; + this.height = 0; + this.imageLayerData = { + imageUrl: null, + aspect: 0 + }; + this.initMapSubject = new ReplaySubject(); + this.loadImageLayerData().subscribe((data) => { + this.imageLayerData = data; + if (this.imageLayerData.update) { + this.onResize(true); + } else { + this.onResize(); + this.initMapSubject.next(this.map); + this.initMapSubject.complete(); + } + }); + return this.initMapSubject.asObservable(); + } + + protected onResize(updateImage?: boolean): void { + let width = this.mapElement.clientWidth; + if (width > 0 && this.imageLayerData.aspect) { + let height = Math.round(width / this.imageLayerData.aspect); + const imageMapHeight = this.mapElement.clientHeight; + if (imageMapHeight > 0 && height > imageMapHeight) { + height = imageMapHeight; + width = Math.round(height * this.imageLayerData.aspect); + } + width *= this.maxZoom; + const prevWidth = this.width; + const prevHeight = this.height; + if (this.width !== width || updateImage) { + this.width = width; + this.height = Math.round(width / this.imageLayerData.aspect); + if (!this.map) { + this.doCreateMap(updateImage); + } else { + const lastCenterPos = this.latLngToPoint(this.map.getCenter()); + lastCenterPos.x /= prevWidth; + lastCenterPos.y /= prevHeight; + this.updateMaxBounds(updateImage, lastCenterPos); + (this.map as any)._enforcingBounds = true; + this.map.invalidateSize(false); + (this.map as any)._enforcingBounds = false; + this.invalidateDataLayersCoordinates(); + } + } + } + } + + protected fitBounds(_bounds: LatLngBounds) {} + + public positionToLatLng(position: {x: number; y: number}): L.LatLng { + return this.pointToLatLng( + position.x * this.width, + position.y * this.height); + } + + public pointToLatLng(x: number, y: number): L.LatLng { + return L.CRS.Simple.pointToLatLng({ x, y } as L.PointExpression, this.maxZoom - 1); + } + + private latLngToPoint(latLng: LatLngLiteral): L.Point { + return L.CRS.Simple.latLngToPoint(latLng, this.maxZoom - 1); + } + + public toPolygonCoordinates(expression: (LatLngTuple | LatLngTuple[] | LatLngTuple[][])[]): any { + return (expression).map((el) => { + if (!Array.isArray(el[0]) && !Array.isArray(el[1]) && el.length === 2) { + return this.pointToLatLng( + el[0] * this.width, + el[1] * this.height + ); + } else if (Array.isArray(el) && el.length) { + return this.toPolygonCoordinates(el as LatLngTuple[] | LatLngTuple[][]); + } else { + return null; + } + }).filter(el => !!el); + } + + public convertCircleData(circle: TbCircleData): TbCircleData { + const centerPoint = this.pointToLatLng(circle.latitude * this.width, circle.longitude * this.height); + circle.latitude = centerPoint.lat; + circle.longitude = centerPoint.lng; + circle.radius = circle.radius * this.width; + return circle; + } + + public getAspect(): number { + return this.imageLayerData.aspect; + } + + private doCreateMap(updateImage?: boolean) { + if (!this.map && this.imageLayerData.aspect > 0) { + const center = this.pointToLatLng(this.width / 2, this.height / 2); + this.map = L.map(this.mapElement, { + scrollWheelZoom: this.settings.zoomActions.includes(MapZoomAction.scroll), + doubleClickZoom: this.settings.zoomActions.includes(MapZoomAction.doubleClick), + zoomControl: this.settings.zoomActions.includes(MapZoomAction.controlButtons), + minZoom: 1, + maxZoom: this.maxZoom, + zoom: 1, + center, + crs: L.CRS.Simple, + attributionControl: false + }); + this.updateMaxBounds(updateImage); + } + } + + private updateMaxBounds(updateImage?: boolean, lastCenterPos?: L.Point) { + const w = this.width; + const h = this.height; + this.southWest = this.pointToLatLng(0, h); + this.northEast = this.pointToLatLng(w, 0); + const bounds = new L.LatLngBounds(this.southWest, this.northEast); + + if (updateImage && this.imageOverlay) { + this.imageOverlay.remove(); + this.imageOverlay = null; + } + + if (this.imageOverlay) { + this.imageOverlay.setBounds(bounds); + } else { + this.imageOverlay = L.imageOverlay(this.imageLayerData.imageUrl, bounds).addTo(this.map); + } + const padding = 200 * this.maxZoom; + const southWest = this.pointToLatLng(-padding, h + padding); + const northEast = this.pointToLatLng(w + padding, -padding); + const maxBounds = new L.LatLngBounds(southWest, northEast); + (this.map as any)._enforcingBounds = true; + this.map.setMaxBounds(maxBounds); + if (lastCenterPos) { + lastCenterPos.x *= w; + lastCenterPos.y *= h; + const center = this.pointToLatLng(lastCenterPos.x, lastCenterPos.y); + this.map.panTo(center, { animate: false }); + } + (this.map as any)._enforcingBounds = false; + } + + private loadImageLayerData(): Observable { + const imageSource = this.settings.imageSource; + if (imageSource.sourceType === ImageSourceType.image) { + return this.imageFromUrl(imageSource.url); + } else { + const datasource = imageMapSourceSettingsToDatasource(imageSource); + const result = new ReplaySubject<[DataSet, boolean]>(); + let isUpdate = false; + const imageUrlSubscriptionOptions: WidgetSubscriptionOptions = { + datasources: [datasource], + hasDataPageLink: true, + singleEntity: true, + useDashboardTimewindow: false, + type: widgetType.latest, + callbacks: { + onDataUpdated: (subscription) => { + if (isNotEmptyStr(subscription.data[0]?.data[0]?.[1])) { + result.next([subscription.data[0].data, isUpdate]); + } else { + result.next([[[0, imageSource.url]], isUpdate]); + } + isUpdate = true; + } + } + }; + this.ctx.subscriptionApi.createSubscription(imageUrlSubscriptionOptions, true).subscribe((subscription) => { + const pageLink: EntityDataPageLink = { + page: 0, + pageSize: 1, + textSearch: null, + dynamic: true + }; + subscription.subscribeAllForPaginatedData(pageLink, null); + }); + return this.imageFromEntityData(result); + } + } + + private imageFromUrl(url: string, update = false): Observable { + return loadImageWithAspect(this.ctx.$injector.get(ImagePipe), url).pipe( + switchMap( aspectImage => { + if (aspectImage) { + return of({ + imageUrl: aspectImage.url, + aspect: aspectImage.aspect, + update + }); + } else { + return this.imageFromUrl(defaultImageMapSourceSettings.url, update); + } + } + ), + catchError(() => this.imageFromUrl(defaultImageMapSourceSettings.url, update)) + ); + } + + private imageFromEntityData(entityData: Observable<[DataSet, boolean]>): Observable { + return entityData.pipe( + switchMap(res => { + const update = res[1]; + const url = res[0][0][1]; + const layerData: ImageLayerData = { + imageUrl: null, + aspect: null, + update + }; + return loadImageWithAspect(this.ctx.$injector.get(ImagePipe), url).pipe( + switchMap((aspectImage) => { + if (aspectImage) { + layerData.aspect = aspectImage.aspect; + layerData.imageUrl = aspectImage.url; + return of(layerData); + } else { + return this.imageFromUrl(defaultImageMapSourceSettings.url, update); + } + }), + catchError(() => this.imageFromUrl(defaultImageMapSourceSettings.url, update)) + ); + }) + ); + } + +} diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/maps/map-widget.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/maps/map-widget.component.ts index 95f5e576c4..baab86f783 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/maps/map-widget.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/maps/map-widget.component.ts @@ -27,7 +27,11 @@ import { ViewChild, ViewEncapsulation } from '@angular/core'; -import { mapWidgetDefaultSettings, MapWidgetSettings } from '@home/components/widget/lib/maps/map-widget.models'; +import { + createMap, + mapWidgetDefaultSettings, + MapWidgetSettings +} from '@home/components/widget/lib/maps/map-widget.models'; import { WidgetContext } from '@home/models/widget-component.models'; import { Observable } from 'rxjs'; import { backgroundStyle, ComponentStyle, overlayStyle } from '@shared/models/widget-settings.models'; @@ -88,7 +92,7 @@ export class MapWidgetComponent implements OnInit, OnDestroy { const borderRadius = this.ctx.$widgetElement.css('borderRadius'); this.overlayStyle = {...this.overlayStyle, ...{borderRadius}}; this.cd.detectChanges(); - this.map = TbMap.fromSettings(this.ctx, this.settings, this.mapElement.nativeElement); + this.map = createMap(this.ctx, this.settings, this.mapElement.nativeElement); } } diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/maps/map-widget.models.ts b/ui-ngx/src/app/modules/home/components/widget/lib/maps/map-widget.models.ts index 30daf48cdc..49953335f2 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/maps/map-widget.models.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/maps/map-widget.models.ts @@ -14,9 +14,14 @@ /// limitations under the License. /// -import { defaultMapSettings, MapSetting } from '@home/components/widget/lib/maps/models/map.models'; +import { defaultMapSettings, MapSetting, MapType } from '@home/components/widget/lib/maps/models/map.models'; import { BackgroundSettings, BackgroundType } from '@shared/models/widget-settings.models'; import { mergeDeep } from '@core/utils'; +import { WidgetContext } from '@home/models/widget-component.models'; +import { DeepPartial } from '@shared/models/common'; +import { TbMap } from '@home/components/widget/lib/maps/map'; +import { TbGeoMap } from '@home/components/widget/lib/maps/geo-map'; +import { TbImageMap } from '@home/components/widget/lib/maps/image-map'; export interface MapWidgetSettings extends MapSetting { background: BackgroundSettings; @@ -36,3 +41,14 @@ export const mapWidgetDefaultSettings: MapWidgetSettings = }, padding: '8px' } as MapWidgetSettings); + +export const createMap = (ctx: WidgetContext, + inputSettings: DeepPartial, + mapElement: HTMLElement): TbMap => { + switch (inputSettings.mapType) { + case MapType.geoMap: + return new TbGeoMap(ctx, inputSettings, mapElement); + case MapType.image: + return new TbImageMap(ctx, inputSettings, mapElement); + } +} diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/maps/map.scss b/ui-ngx/src/app/modules/home/components/widget/lib/maps/map.scss index b44fcd75b2..aabdf47810 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/maps/map.scss +++ b/ui-ngx/src/app/modules/home/components/widget/lib/maps/map.scss @@ -50,15 +50,20 @@ border-bottom: none; position: relative; background: transparent; - &:hover { - border-bottom: none; - color: $tb-primary-color; // primary color - &:before { - content: ""; - position: absolute; - inset: 0; - border-radius: 50%; - background-color: rgba(0, 105, 92, 0.10); + &.leaflet-disabled { + color: rgba(0, 0, 0, 0.18); + } + &:not(.leaflet-disabled) { + &:hover { + border-bottom: none; + color: $tb-primary-color; // primary color + &:before { + content: ""; + position: absolute; + inset: 0; + border-radius: 50%; + background-color: rgba(0, 105, 92, 0.10); + } } } &:first-child { 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 6531c81839..350aeaf85d 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 @@ -17,16 +17,8 @@ import { additionalMapDataSourcesToDatasources, BaseMapSettings, - DEFAULT_ZOOM_LEVEL, - defaultGeoMapSettings, - defaultImageMapSettings, - GeoMapSettings, - ImageMapSettings, - latLngPointToBounds, MapActionHandler, - MapSetting, MapType, - MapZoomAction, mergeMapDatasources, parseCenterPosition, TbCircleData, @@ -37,12 +29,11 @@ import { formattedDataFormDatasourceData, isDefinedAndNotNull, mergeDeepIgnoreAr import { DeepPartial } from '@shared/models/common'; import L, { LatLngBounds, LatLngTuple, LeafletMouseEvent, Projection } from 'leaflet'; import { forkJoin, Observable, of } from 'rxjs'; -import { TbMapLayer } from '@home/components/widget/lib/maps/map-layer'; -import { map, switchMap, tap } from 'rxjs/operators'; +import { switchMap } from 'rxjs/operators'; import '@home/components/widget/lib/maps/leaflet/leaflet-tb'; import { MapDataLayerType, TbMapDataLayer, } from '@home/components/widget/lib/maps/data-layer/map-data-layer'; import { IWidgetSubscription, WidgetSubscriptionOptions } from '@core/api/widget-api.models'; -import { WidgetActionDescriptor, widgetType } from '@shared/models/widget.models'; +import { FormattedData, WidgetActionDescriptor, 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'; @@ -52,17 +43,6 @@ import ITooltipsterInstance = JQueryTooltipster.ITooltipsterInstance; export abstract class TbMap { - static fromSettings(ctx: WidgetContext, - inputSettings: DeepPartial, - mapElement: HTMLElement): TbMap { - switch (inputSettings.mapType) { - case MapType.geoMap: - return new TbGeoMap(ctx, inputSettings, mapElement); - case MapType.image: - return new TbImageMap(ctx, inputSettings, mapElement); - } - } - protected settings: S; protected map: L.Map; @@ -73,6 +53,7 @@ export abstract class TbMap { protected northEast = new L.LatLng(Projection.SphericalMercator['MAX_LATITUDE'], 180); protected dataLayers: TbMapDataLayer[]; + protected dsData: FormattedData[]; protected mapElement: HTMLElement; @@ -122,7 +103,9 @@ export abstract class TbMap { } private setupControls(): Observable { - this.map.zoomControl.setPosition(this.settings.controlsPosition); + if (this.map.zoomControl) { + this.map.zoomControl.setPosition(this.settings.controlsPosition); + } return this.doSetupControls(); } @@ -288,9 +271,9 @@ export abstract class TbMap { } private update(subscription: IWidgetSubscription) { - const dsData = formattedDataFormDatasourceData(subscription.data, + this.dsData = formattedDataFormDatasourceData(subscription.data, undefined, undefined, el => el.datasource.entityId + el.datasource.mapDataIds[0]); - this.dataLayers.forEach(dl => dl.updateData(dsData)); + this.dataLayers.forEach(dl => dl.updateData(this.dsData)); this.updateBounds(); } @@ -306,7 +289,6 @@ export abstract class TbMap { if (dataLayersBounds.length) { bounds = new L.LatLngBounds(null, null); dataLayersBounds.forEach(b => bounds.extend(b)); - const mapBounds = this.map.getBounds(); if (bounds.isValid() && this.settings.fitMapBounds && !mapBounds.contains(bounds)) { if (!this.ignoreUpdateBounds) { @@ -317,35 +299,6 @@ export abstract class TbMap { } } - private fitBounds(bounds: LatLngBounds) { - if (bounds.isValid()) { - if (!this.settings.fitMapBounds && this.settings.defaultZoomLevel) { - this.map.setZoom(this.settings.defaultZoomLevel, { animate: false }); - if (this.settings.useDefaultCenterPosition) { - this.map.panTo(this.defaultCenterPosition, { animate: false }); - } - else { - this.map.panTo(bounds.getCenter()); - } - } else { - this.map.once('zoomend', () => { - let minZoom = this.settings.minZoomLevel; - if (this.settings.defaultZoomLevel) { - minZoom = Math.max(minZoom, this.settings.defaultZoomLevel); - } - if (this.map.getZoom() > minZoom) { - this.map.setZoom(minZoom, { animate: false }); - } - }); - if (this.settings.useDefaultCenterPosition) { - bounds = bounds.extend(this.defaultCenterPosition); - } - this.map.fitBounds(bounds, { padding: [50, 50], animate: false }); - this.map.invalidateSize(); - } - } - } - private loadActions(name: string): { [name: string]: MapActionHandler } { const descriptors = this.ctx.actionsApi.getActionDescriptors(name); const actions: { [name: string]: MapActionHandler } = {}; @@ -373,10 +326,16 @@ export abstract class TbMap { protected abstract onResize(): void; + protected abstract fitBounds(bounds: LatLngBounds): void; + protected doSetupControls(): Observable { return of(null); } + protected invalidateDataLayersCoordinates(): void { + this.dataLayers.forEach(dl => dl.invalidateCoordinates()); + } + protected getSidebar(): L.TB.SidebarControl { if (!this.sidebar) { this.sidebar = L.TB.sidebar({ @@ -392,6 +351,10 @@ export abstract class TbMap { return this.ctx; } + public getData(): FormattedData[] { + return this.dsData; + } + public getMap(): L.Map { return this.map; } @@ -459,154 +422,3 @@ export abstract class TbMap { public abstract convertCircleData(circle: TbCircleData): TbCircleData; } - -class TbGeoMap extends TbMap { - - constructor(protected ctx: WidgetContext, - protected inputSettings: DeepPartial, - protected containerElement: HTMLElement) { - super(ctx, inputSettings, containerElement); - } - - protected defaultSettings(): GeoMapSettings { - return defaultGeoMapSettings; - } - - protected createMap(): Observable { - const map = L.map(this.mapElement, { - scrollWheelZoom: this.settings.zoomActions.includes(MapZoomAction.scroll), - doubleClickZoom: this.settings.zoomActions.includes(MapZoomAction.doubleClick), - zoomControl: this.settings.zoomActions.includes(MapZoomAction.controlButtons), - zoom: this.settings.defaultZoomLevel || DEFAULT_ZOOM_LEVEL, - center: this.defaultCenterPosition - }).setView(this.defaultCenterPosition, this.settings.defaultZoomLevel || DEFAULT_ZOOM_LEVEL); - return of(map); - } - - protected onResize(): void {} - - protected doSetupControls(): Observable { - return this.loadLayers().pipe( - tap((layers: L.TB.LayerData[]) => { - if (layers.length) { - const layer = layers[0]; - layer.layer.addTo(this.map); - this.map.attributionControl.setPrefix(layer.attributionPrefix); - if (layers.length > 1) { - const sidebar = this.getSidebar(); - L.TB.layers({ - layers, - sidebar, - position: this.settings.controlsPosition, - uiClass: 'tb-layers', - paneTitle: this.ctx.translate.instant('widgets.maps.layer.map-layers'), - buttonTitle: this.ctx.translate.instant('widgets.maps.layer.layers'), - }).addTo(this.map); - } - } - }) - ); - - } - - private loadLayers(): Observable { - const layers = this.settings.layers.map(settings => TbMapLayer.fromSettings(this.ctx, settings)); - return forkJoin(layers.map(layer => layer.loadLayer(this.map))).pipe( - map((layersData) => { - return layersData.filter(l => l !== null); - }) - ); - } - - public positionToLatLng(position: {x: number; y: number}): L.LatLng { - return L.latLng(position.x, position.y) as L.LatLng; - } - - public toPolygonCoordinates(expression: (LatLngTuple | LatLngTuple[] | LatLngTuple[][])[]): any { - return (expression).map((el) => { - if (!Array.isArray(el[0]) && el.length === 2) { - return el; - } else if (Array.isArray(el) && el.length) { - return this.toPolygonCoordinates(el as LatLngTuple[] | LatLngTuple[][]); - } else { - return null; - } - }).filter(el => !!el); - } - - public convertCircleData(circle: TbCircleData): TbCircleData { - const centerPoint = latLngPointToBounds(new L.LatLng(circle.latitude, circle.longitude), this.southWest, this.northEast); - circle.latitude = centerPoint.lat; - circle.longitude = centerPoint.lng; - return circle; - } - -} - -class TbImageMap extends TbMap { - - private maxZoom = 4; - - private width = 0; - private height = 0; - - constructor(protected ctx: WidgetContext, - protected inputSettings: DeepPartial, - protected mapElement: HTMLElement) { - super(ctx, inputSettings, mapElement); - } - - protected defaultSettings(): ImageMapSettings { - return defaultImageMapSettings; - } - - protected createMap(): Observable { - const map = L.map(this.mapElement, { - scrollWheelZoom: this.settings.zoomActions.includes(MapZoomAction.scroll), - doubleClickZoom: this.settings.zoomActions.includes(MapZoomAction.doubleClick), - zoomControl: this.settings.zoomActions.includes(MapZoomAction.controlButtons), - minZoom: 1, - maxZoom: this.maxZoom, - zoom: 1, - crs: L.CRS.Simple, - attributionControl: false - }).setView(this.defaultCenterPosition, this.settings.defaultZoomLevel || DEFAULT_ZOOM_LEVEL); - return of(map); - } - - protected onResize(): void {} - - public positionToLatLng(position: {x: number; y: number}): L.LatLng { - return this.pointToLatLng( - position.x * this.width, - position.y * this.height); - } - - public pointToLatLng(x: number, y: number): L.LatLng { - return L.CRS.Simple.pointToLatLng({ x, y } as L.PointExpression, this.maxZoom - 1); - } - - public toPolygonCoordinates(expression: (LatLngTuple | LatLngTuple[] | LatLngTuple[][])[]): any { - return (expression).map((el) => { - if (!Array.isArray(el[0]) && !Array.isArray(el[1]) && el.length === 2) { - return this.pointToLatLng( - el[0] * this.width, - el[1] * this.height - ); - } else if (Array.isArray(el) && el.length) { - return this.toPolygonCoordinates(el as LatLngTuple[] | LatLngTuple[][]); - } else { - return null; - } - }).filter(el => !!el); - } - - public convertCircleData(circle: TbCircleData): TbCircleData { - const centerPoint = this.pointToLatLng(circle.latitude * this.width, circle.longitude * this.height); - circle.latitude = centerPoint.lat; - circle.longitude = centerPoint.lng; - circle.radius = circle.radius * this.width; - return circle; - } - -} diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/maps/models/map.models.ts b/ui-ngx/src/app/modules/home/components/widget/lib/maps/models/map.models.ts index 9dd6065ad6..09f6f0c488 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/maps/models/map.models.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/maps/models/map.models.ts @@ -16,7 +16,7 @@ import { DataKey, Datasource, DatasourceType, FormattedData } from '@shared/models/widget.models'; import { DataKeyType } from '@shared/models/telemetry/telemetry.models'; -import { guid, hashCode, isDefinedAndNotNull, isString, mergeDeep } from '@core/utils'; +import { guid, hashCode, isDefinedAndNotNull, isNotEmptyStr, isString, mergeDeep } from '@core/utils'; import { AbstractControl, ValidationErrors, ValidatorFn } from '@angular/forms'; import { materialColors } from '@shared/models/material.models'; import L from 'leaflet'; @@ -96,7 +96,7 @@ export interface MapDataLayerSettings extends MapDataSourceSettings { groups?: string[]; } -export const defaultBaseDataLayerSettings: Partial = { +export const defaultBaseDataLayerSettings = (mapType: MapType): Partial => ({ label: { show: true, type: DataLayerPatternType.pattern, @@ -107,11 +107,13 @@ export const defaultBaseDataLayerSettings: Partial = { trigger: DataLayerTooltipTrigger.click, autoclose: true, type: DataLayerPatternType.pattern, - pattern: '${entityName}

Latitude: ${latitude:7}
Longitude: ${longitude:7}
Temperature: ${temperature} °C
See tooltip settings for details', + pattern: mapType === MapType.geoMap ? + '${entityName}

Latitude: ${latitude:7}
Longitude: ${longitude:7}
Temperature: ${temperature} °C
See tooltip settings for details' + : '${entityName}

X Pos: ${xPos:2}
Y Pos: ${yPos:2}
Temperature: ${temperature} °C
See tooltip settings for details', offsetX: 0, offsetY: -1 } -} +}) export type MapDataLayerType = 'markers' | 'polygons' | 'circles'; @@ -235,6 +237,7 @@ export interface MarkersDataLayerSettings extends MapDataLayerSettings { markerImage?: MarkerImageSettings; markerOffsetX: number; markerOffsetY: number; + positionFunction?: TbFunction; markerClustering: MarkerClusteringSettings; } @@ -281,9 +284,9 @@ export const defaultMarkersDataLayerSettings = (mapType: MapType, functionsOnly settings: {}, color: materialColors[0].value } -} as MarkersDataLayerSettings, defaultBaseMarkersDataLayerSettings as MarkersDataLayerSettings); +} as MarkersDataLayerSettings, defaultBaseMarkersDataLayerSettings(mapType) as MarkersDataLayerSettings); -export const defaultBaseMarkersDataLayerSettings: Partial = mergeDeep({ +export const defaultBaseMarkersDataLayerSettings = (mapType: MapType): Partial => mergeDeep({ markerType: MarkerType.shape, markerShape: { shape: MarkerShape.markerShape1, @@ -308,6 +311,7 @@ export const defaultBaseMarkersDataLayerSettings: Partial mergeDeep({ +export const defaultPolygonsDataLayerSettings = (mapType: MapType, functionsOnly = false): PolygonsDataLayerSettings => mergeDeep({ dsType: functionsOnly ? DatasourceType.function : DatasourceType.entity, dsLabel: functionsOnly ? 'First polygon' : '', polygonKey: { @@ -343,9 +347,9 @@ export const defaultPolygonsDataLayerSettings = (functionsOnly = false): Polygon settings: {}, color: materialColors[0].value } -} as PolygonsDataLayerSettings, defaultBasePolygonsDataLayerSettings as PolygonsDataLayerSettings); +} as PolygonsDataLayerSettings, defaultBasePolygonsDataLayerSettings(mapType) as PolygonsDataLayerSettings); -export const defaultBasePolygonsDataLayerSettings: Partial = mergeDeep({ +export const defaultBasePolygonsDataLayerSettings = (mapType: MapType): Partial => mergeDeep({ fillColor: { type: DataLayerColorType.constant, color: 'rgba(51,136,255,0.2)', @@ -355,14 +359,14 @@ export const defaultBasePolygonsDataLayerSettings: Partial, defaultBaseDataLayerSettings, +} as Partial, defaultBaseDataLayerSettings(mapType), {label: {show: false}, tooltip: {show: false, pattern: '${entityName}

TimeStamp: ${ts:7}'}} as Partial) export interface CirclesDataLayerSettings extends ShapeDataLayerSettings { circleKey: DataKey; } -export const defaultCirclesDataLayerSettings = (functionsOnly = false): CirclesDataLayerSettings => mergeDeep({ +export const defaultCirclesDataLayerSettings = (mapType: MapType, functionsOnly = false): CirclesDataLayerSettings => mergeDeep({ dsType: functionsOnly ? DatasourceType.function : DatasourceType.entity, dsLabel: functionsOnly ? 'First circle' : '', circleKey: { @@ -372,9 +376,9 @@ export const defaultCirclesDataLayerSettings = (functionsOnly = false): CirclesD settings: {}, color: materialColors[0].value } -} as CirclesDataLayerSettings, defaultBaseCirclesDataLayerSettings as CirclesDataLayerSettings); +} as CirclesDataLayerSettings, defaultBaseCirclesDataLayerSettings(mapType) as CirclesDataLayerSettings); -export const defaultBaseCirclesDataLayerSettings: Partial = mergeDeep({ +export const defaultBaseCirclesDataLayerSettings = (mapType: MapType): Partial => mergeDeep({ fillColor: { type: DataLayerColorType.constant, color: 'rgba(51,136,255,0.2)', @@ -384,7 +388,7 @@ export const defaultBaseCirclesDataLayerSettings: Partial, defaultBaseDataLayerSettings, +} as Partial, defaultBaseDataLayerSettings(mapType), {label: {show: false}, tooltip: {show: false, pattern: '${entityName}

TimeStamp: ${ts:7}'}} as Partial) export const defaultMapDataLayerSettings = (mapType: MapType, dataLayerType: MapDataLayerType, functionsOnly = false): MapDataLayerSettings => { @@ -392,20 +396,20 @@ export const defaultMapDataLayerSettings = (mapType: MapType, dataLayerType: Map case 'markers': return defaultMarkersDataLayerSettings(mapType, functionsOnly); case 'polygons': - return defaultPolygonsDataLayerSettings(functionsOnly); + return defaultPolygonsDataLayerSettings(mapType, functionsOnly); case 'circles': - return defaultCirclesDataLayerSettings(functionsOnly); + return defaultCirclesDataLayerSettings(mapType, functionsOnly); } }; -export const defaultBaseMapDataLayerSettings = (dataLayerType: MapDataLayerType): T => { +export const defaultBaseMapDataLayerSettings = (mapType: MapType, dataLayerType: MapDataLayerType): T => { switch (dataLayerType) { case 'markers': - return defaultBaseMarkersDataLayerSettings as T; + return defaultBaseMarkersDataLayerSettings(mapType) as T; case 'polygons': - return defaultBasePolygonsDataLayerSettings as T; + return defaultBasePolygonsDataLayerSettings(mapType) as T; case 'circles': - return defaultBaseCirclesDataLayerSettings as T; + return defaultBaseCirclesDataLayerSettings(mapType) as T; } } @@ -453,12 +457,33 @@ export enum MapControlsPosition { bottomright = 'bottomright' } +export const mapControlPositions = Object.keys(MapControlsPosition) as MapControlsPosition[]; + +export const mapControlsPositionTranslationMap = new Map( + [ + [MapControlsPosition.topleft, 'widgets.maps.control.position-topleft'], + [MapControlsPosition.topright, 'widgets.maps.control.position-topright'], + [MapControlsPosition.bottomleft, 'widgets.maps.control.position-bottomleft'], + [MapControlsPosition.bottomright, 'widgets.maps.control.position-bottomright'] + ] +); + export enum MapZoomAction { scroll = 'scroll', doubleClick = 'doubleClick', controlButtons = 'controlButtons' } +export const mapZoomActions = Object.keys(MapZoomAction) as MapZoomAction[]; + +export const mapZoomActionTranslationMap = new Map( + [ + [MapZoomAction.scroll, 'widgets.maps.control.zoom-scroll'], + [MapZoomAction.doubleClick, 'widgets.maps.control.zoom-double-click'], + [MapZoomAction.controlButtons, 'widgets.maps.control.zoom-control-buttons'] + ] +); + export interface BaseMapSettings { mapType: MapType; markers: MarkersDataLayerSettings[]; @@ -756,20 +781,57 @@ export const defaultGeoMapSettings: GeoMapSettings = { export enum ImageSourceType { image = 'image', - attribute = 'attribute' + entityKey = 'entityKey' +} + +export interface ImageMapSourceSettings { + sourceType: ImageSourceType; + url?: string; + entityAliasId?: string; + entityKey?: DataKey; } +export const imageMapSourceSettingsValid = (imageSource: ImageMapSourceSettings): boolean => { + if (!imageSource?.sourceType) { + return false; + } else if (imageSource.sourceType === ImageSourceType.image) { + return isNotEmptyStr(imageSource.url); + } else { + return isNotEmptyStr(imageSource.entityAliasId) && !!imageSource.entityKey; + } +} + +export const imageMapSourceSettingsValidator: ValidatorFn = (control: AbstractControl): ValidationErrors | null => { + const imageSource: ImageMapSourceSettings = control.value; + if (!imageMapSourceSettingsValid(imageSource)) { + return { + imageMapSource: true + }; + } + return null; +}; + +export const defaultImageMapSourceSettings: ImageMapSourceSettings = { + sourceType: ImageSourceType.image, + url: 'data:image/svg+xml;base64,PHN2ZyBpZD0ic3ZnMiIgeG1sbnM6cmRmPSJodHRwOi8vd3d3LnczLm9yZy8xOTk5LzAyLzIyLXJkZi1zeW50YXgtbnMjIiB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIGhlaWdodD0iMTAwIiB3aWR0aD0iMTAwIiB2ZXJzaW9uPSIxLjEiIHhtbG5zOmNjPSJodHRwOi8vY3JlYXRpdmVjb21tb25zLm9yZy9ucyMiIHhtbG5zOmRjPSJodHRwOi8vcHVybC5vcmcvZGMvZWxlbWVudHMvMS4xLyIgdmlld0JveD0iMCAwIDEwMCAxMDAiPgogPGcgaWQ9ImxheWVyMSIgdHJhbnNmb3JtPSJ0cmFuc2xhdGUoMCAtOTUyLjM2KSI+CiAgPHJlY3QgaWQ9InJlY3Q0Njg0IiBzdHJva2UtbGluZWpvaW49InJvdW5kIiBoZWlnaHQ9Ijk5LjAxIiB3aWR0aD0iOTkuMDEiIHN0cm9rZT0iIzAwMCIgc3Ryb2tlLWxpbmVjYXA9InJvdW5kIiB5PSI5NTIuODYiIHg9Ii40OTUwNSIgc3Ryb2tlLXdpZHRoPSIuOTkwMTAiIGZpbGw9IiNlZWUiLz4KICA8dGV4dCBpZD0idGV4dDQ2ODYiIHN0eWxlPSJ3b3JkLXNwYWNpbmc6MHB4O2xldHRlci1zcGFjaW5nOjBweDt0ZXh0LWFuY2hvcjptaWRkbGU7dGV4dC1hbGlnbjpjZW50ZXIiIGZvbnQtd2VpZ2h0PSJib2xkIiB4bWw6c3BhY2U9InByZXNlcnZlIiBmb250LXNpemU9IjEwcHgiIGxpbmUtaGVpZ2h0PSIxMjUlIiB5PSI5NzAuNzI4MDkiIHg9IjQ5LjM5NjQ3NyIgZm9udC1mYW1pbHk9IlJvYm90byIgZmlsbD0iIzY2NjY2NiI+PHRzcGFuIGlkPSJ0c3BhbjQ2OTAiIHg9IjUwLjY0NjQ3NyIgeT0iOTcwLjcyODA5Ij5JbWFnZSBiYWNrZ3JvdW5kIDwvdHNwYW4+PHRzcGFuIGlkPSJ0c3BhbjQ2OTIiIHg9IjQ5LjM5NjQ3NyIgeT0iOTgzLjIyODA5Ij5pcyBub3QgY29uZmlndXJlZDwvdHNwYW4+PC90ZXh0PgogIDxyZWN0IGlkPSJyZWN0NDY5NCIgc3Ryb2tlLWxpbmVqb2luPSJyb3VuZCIgaGVpZ2h0PSIxOS4zNiIgd2lkdGg9IjY5LjM2IiBzdHJva2U9IiMwMDAiIHN0cm9rZS1saW5lY2FwPSJyb3VuZCIgeT0iOTkyLjY4IiB4PSIxNS4zMiIgc3Ryb2tlLXdpZHRoPSIuNjM5ODYiIGZpbGw9Im5vbmUiLz4KIDwvZz4KPC9zdmc+Cg==', +}; + +export const imageMapSourceSettingsToDatasource = (settings: ImageMapSourceSettings): Datasource => { + return { + type: DatasourceType.entity, + name: '', + entityAliasId: settings.entityAliasId, + dataKeys: [settings.entityKey] + }; +}; + export interface ImageMapSettings extends BaseMapSettings { - imageSourceType?: ImageSourceType; - imageUrl?: string; - imageEntityAlias?: string; - imageUrlAttribute?: string; + imageSource?: ImageMapSourceSettings; } export const defaultImageMapSettings: ImageMapSettings = { mapType: MapType.image, - imageSourceType: ImageSourceType.image, - imageUrl: 'data:image/svg+xml;base64,PHN2ZyBpZD0ic3ZnMiIgeG1sbnM6cmRmPSJodHRwOi8vd3d3LnczLm9yZy8xOTk5LzAyLzIyLXJkZi1zeW50YXgtbnMjIiB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIGhlaWdodD0iMTAwIiB3aWR0aD0iMTAwIiB2ZXJzaW9uPSIxLjEiIHhtbG5zOmNjPSJodHRwOi8vY3JlYXRpdmVjb21tb25zLm9yZy9ucyMiIHhtbG5zOmRjPSJodHRwOi8vcHVybC5vcmcvZGMvZWxlbWVudHMvMS4xLyIgdmlld0JveD0iMCAwIDEwMCAxMDAiPgogPGcgaWQ9ImxheWVyMSIgdHJhbnNmb3JtPSJ0cmFuc2xhdGUoMCAtOTUyLjM2KSI+CiAgPHJlY3QgaWQ9InJlY3Q0Njg0IiBzdHJva2UtbGluZWpvaW49InJvdW5kIiBoZWlnaHQ9Ijk5LjAxIiB3aWR0aD0iOTkuMDEiIHN0cm9rZT0iIzAwMCIgc3Ryb2tlLWxpbmVjYXA9InJvdW5kIiB5PSI5NTIuODYiIHg9Ii40OTUwNSIgc3Ryb2tlLXdpZHRoPSIuOTkwMTAiIGZpbGw9IiNlZWUiLz4KICA8dGV4dCBpZD0idGV4dDQ2ODYiIHN0eWxlPSJ3b3JkLXNwYWNpbmc6MHB4O2xldHRlci1zcGFjaW5nOjBweDt0ZXh0LWFuY2hvcjptaWRkbGU7dGV4dC1hbGlnbjpjZW50ZXIiIGZvbnQtd2VpZ2h0PSJib2xkIiB4bWw6c3BhY2U9InByZXNlcnZlIiBmb250LXNpemU9IjEwcHgiIGxpbmUtaGVpZ2h0PSIxMjUlIiB5PSI5NzAuNzI4MDkiIHg9IjQ5LjM5NjQ3NyIgZm9udC1mYW1pbHk9IlJvYm90byIgZmlsbD0iIzY2NjY2NiI+PHRzcGFuIGlkPSJ0c3BhbjQ2OTAiIHg9IjUwLjY0NjQ3NyIgeT0iOTcwLjcyODA5Ij5JbWFnZSBiYWNrZ3JvdW5kIDwvdHNwYW4+PHRzcGFuIGlkPSJ0c3BhbjQ2OTIiIHg9IjQ5LjM5NjQ3NyIgeT0iOTgzLjIyODA5Ij5pcyBub3QgY29uZmlndXJlZDwvdHNwYW4+PC90ZXh0PgogIDxyZWN0IGlkPSJyZWN0NDY5NCIgc3Ryb2tlLWxpbmVqb2luPSJyb3VuZCIgaGVpZ2h0PSIxOS4zNiIgd2lkdGg9IjY5LjM2IiBzdHJva2U9IiMwMDAiIHN0cm9rZS1saW5lY2FwPSJyb3VuZCIgeT0iOTkyLjY4IiB4PSIxNS4zMiIgc3Ryb2tlLXdpZHRoPSIuNjM5ODYiIGZpbGw9Im5vbmUiLz4KIDwvZz4KPC9zdmc+Cg==', + imageSource: mergeDeep({} as ImageMapSourceSettings, defaultImageMapSourceSettings), ...mergeDeep({} as BaseMapSettings, defaultBaseMapSettings) } @@ -799,6 +861,9 @@ export type MarkerImageFunction = (data: FormattedData, markerI export type ClusterMarkerColorFunction = (data: FormattedData[], childCount: number) => string; +export type MarkerPositionFunction = (origXPos: number, origYPos: number, data: FormattedData, + dsData: FormattedData[], aspect: number) => { x: number, y: number }; + export interface TbCircleData { latitude: number; longitude: number; diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/key/data-key-input.component.html b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/key/data-key-input.component.html index 2349c14472..90e0ed7328 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/key/data-key-input.component.html +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/key/data-key-input.component.html @@ -15,7 +15,7 @@ limitations under the License. --> - DataKey = (key) => key; + generateKey: (key: DataKey) => DataKey; private requiredValue: boolean; get required(): boolean { diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/map/image-map-source-settings.component.html b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/map/image-map-source-settings.component.html new file mode 100644 index 0000000000..6229b6696e --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/map/image-map-source-settings.component.html @@ -0,0 +1,62 @@ + +
+
+
+ {{ 'widgets.maps.image.image-source' | translate }} +
+ + {{ 'widgets.maps.image.image-source-image' | translate }} + {{ 'widgets.maps.image.image-source-entity-key' | translate }} + +
+ + + +
+
widgets.maps.image.source-entity-alias
+ + +
+
+
widgets.maps.image.image-url-key
+ + +
+
+
diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/map/image-map-source-settings.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/map/image-map-source-settings.component.ts new file mode 100644 index 0000000000..13dc2b839e --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/map/image-map-source-settings.component.ts @@ -0,0 +1,157 @@ +/// +/// Copyright © 2016-2024 The Thingsboard Authors +/// +/// Licensed under the Apache License, Version 2.0 (the "License"); +/// you may not use this file except in compliance with the License. +/// You may obtain a copy of the License at +/// +/// http://www.apache.org/licenses/LICENSE-2.0 +/// +/// Unless required by applicable law or agreed to in writing, software +/// distributed under the License is distributed on an "AS IS" BASIS, +/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +/// See the License for the specific language governing permissions and +/// limitations under the License. +/// + +import { Component, DestroyRef, forwardRef, Input, OnInit, ViewEncapsulation } from '@angular/core'; +import { + ControlValueAccessor, + NG_VALIDATORS, + NG_VALUE_ACCESSOR, + UntypedFormBuilder, + UntypedFormControl, + UntypedFormGroup, + Validator, + Validators +} from '@angular/forms'; +import { takeUntilDestroyed } from '@angular/core/rxjs-interop'; +import { ImageMapSourceSettings, ImageSourceType } from '@home/components/widget/lib/maps/models/map.models'; +import { DataKey, DatasourceType, widgetType } from '@shared/models/widget.models'; +import { MapSettingsContext } from '@home/components/widget/lib/settings/common/map/map-settings.component.models'; +import { DataKeyType } from '@shared/models/telemetry/telemetry.models'; + +@Component({ + selector: 'tb-image-map-source-settings', + templateUrl: './image-map-source-settings.component.html', + styleUrls: [], + providers: [ + { + provide: NG_VALUE_ACCESSOR, + useExisting: forwardRef(() => ImageMapSourceSettingsComponent), + multi: true + }, + { + provide: NG_VALIDATORS, + useExisting: forwardRef(() => ImageMapSourceSettingsComponent), + multi: true + } + ] +}) +export class ImageMapSourceSettingsComponent implements OnInit, ControlValueAccessor, Validator { + + ImageSourceType = ImageSourceType; + DatasourceType = DatasourceType; + widgetType = widgetType; + DataKeyType = DataKeyType; + + @Input() + disabled: boolean; + + @Input() + context: MapSettingsContext; + + private modelValue: ImageMapSourceSettings; + + private propagateChange = null; + + public imageMapSourceFormGroup: UntypedFormGroup; + + constructor(private fb: UntypedFormBuilder, + private destroyRef: DestroyRef) { + } + + ngOnInit(): void { + this.imageMapSourceFormGroup = this.fb.group({ + sourceType: [null, [Validators.required]], + url: [null, [Validators.required]], + entityAliasId: [null, [Validators.required]], + entityKey: [null, [Validators.required]] + }); + this.imageMapSourceFormGroup.valueChanges.pipe( + takeUntilDestroyed(this.destroyRef) + ).subscribe(() => { + this.updateModel(); + }); + this.imageMapSourceFormGroup.get('sourceType').valueChanges.pipe( + takeUntilDestroyed(this.destroyRef) + ).subscribe(() => { + this.updateValidators(); + }); + } + + registerOnChange(fn: any): void { + this.propagateChange = fn; + } + + registerOnTouched(_fn: any): void { + } + + setDisabledState(isDisabled: boolean): void { + this.disabled = isDisabled; + if (isDisabled) { + this.imageMapSourceFormGroup.disable({emitEvent: false}); + } else { + this.imageMapSourceFormGroup.enable({emitEvent: false}); + this.updateValidators(); + } + } + + writeValue(value: ImageMapSourceSettings): void { + this.modelValue = value; + this.imageMapSourceFormGroup.patchValue( + value, {emitEvent: false} + ); + this.updateValidators(); + } + + editKey() { + const entityKey: DataKey = this.imageMapSourceFormGroup.get('entityKey').value; + this.context.editKey(entityKey, + null, this.imageMapSourceFormGroup.get('entityAliasId').value).subscribe( + (updatedDataKey) => { + if (updatedDataKey) { + this.imageMapSourceFormGroup.get('entityKey').patchValue(updatedDataKey); + } + } + ); + } + + public validate(c: UntypedFormControl) { + const valid = this.imageMapSourceFormGroup.valid; + return valid ? null : { + imageMapSource: { + valid: false, + }, + }; + } + + + private updateValidators() { + const sourceType: ImageSourceType = this.imageMapSourceFormGroup.get('sourceType').value; + if (sourceType === ImageSourceType.image) { + this.imageMapSourceFormGroup.get('url').enable({emitEvent: false}); + this.imageMapSourceFormGroup.get('entityAliasId').disable({emitEvent: false}); + this.imageMapSourceFormGroup.get('entityKey').disable({emitEvent: false}); + } else { + this.imageMapSourceFormGroup.get('url').disable({emitEvent: false}); + this.imageMapSourceFormGroup.get('entityAliasId').enable({emitEvent: false}); + this.imageMapSourceFormGroup.get('entityKey').enable({emitEvent: false}); + } + } + + private updateModel() { + this.modelValue = this.imageMapSourceFormGroup.getRawValue(); + this.propagateChange(this.modelValue); + } +} diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/map/map-data-layer-dialog.component.html b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/map/map-data-layer-dialog.component.html index dc9c069882..8742eff68c 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/map/map-data-layer-dialog.component.html +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/map/map-data-layer-dialog.component.html @@ -214,6 +214,17 @@
+
+
widgets.maps.data-layer.marker.position-conversion
+ + +
diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/map/map-data-layer-dialog.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/map/map-data-layer-dialog.component.ts index d00325b0a7..b57f92fc35 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/map/map-data-layer-dialog.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/map/map-data-layer-dialog.component.ts @@ -17,13 +17,15 @@ import { Component, DestroyRef, Inject, ViewEncapsulation } from '@angular/core'; import { DialogComponent } from '@shared/components/dialog.component'; import { - CirclesDataLayerSettings, defaultBaseMapDataLayerSettings, + CirclesDataLayerSettings, + defaultBaseMapDataLayerSettings, MapDataLayerSettings, MapDataLayerType, MapType, MarkersDataLayerSettings, MarkerType, - PolygonsDataLayerSettings, ShapeDataLayerSettings + PolygonsDataLayerSettings, + ShapeDataLayerSettings } from '@home/components/widget/lib/maps/models/map.models'; import { Store } from '@ngrx/store'; import { AppState } from '@core/core.state'; @@ -36,6 +38,8 @@ import { takeUntilDestroyed } from '@angular/core/rxjs-interop'; import { EntityType } from '@shared/models/entity-type.models'; import { MapSettingsContext } from '@home/components/widget/lib/settings/common/map/map-settings.component.models'; import { genNextLabelForDataKeys, mergeDeepIgnoreArray } from '@core/utils'; +import { MapProviders } from '@home/components/widget/lib/maps-legacy/map-models'; +import { WidgetService } from '@core/http/widget.service'; export interface MapDataLayerDialogData { settings: MapDataLayerSettings; @@ -76,6 +80,8 @@ export class MapDataLayerDialogComponent extends DialogComponent, @@ -83,6 +89,7 @@ export class MapDataLayerDialogComponent extends DialogComponent, private fb: FormBuilder, + private widgetService: WidgetService, private destroyRef: DestroyRef) { super(store, router, dialogRef); @@ -93,7 +100,7 @@ export class MapDataLayerDialogComponent extends DialogComponent(this.dataLayerType), this.settings); + defaultBaseMapDataLayerSettings(this.mapType, this.dataLayerType), this.settings); this.dataLayerFormGroup = this.fb.group({ dsType: [this.settings.dsType, [Validators.required]], @@ -119,6 +126,9 @@ export class MapDataLayerDialogComponent extends DialogComponent
+ formControlName="layers"> + + +
@@ -65,4 +70,59 @@ [context]="context">
+
+
+ {{ 'widgets.maps.control.map-controls' | translate }} +
+
+
widgets.maps.control.position
+ + + + {{ mapControlsPositionTranslationMap.get(position) | translate }} + + + +
+
+
widgets.maps.control.zoom-actions
+ + + {{ mapZoomActionTranslationMap.get(action) | translate }} + + +
+
+
+
+ {{ 'widgets.maps.common.common-map-settings' | translate }} +
+ +
+ + {{ 'widgets.maps.common.fit-map-bounds' | translate }} + +
+
+ + {{ 'widgets.maps.common.default-map-center-position' | translate }} + + + + +
+
+
widgets.maps.common.default-map-zoom-level
+ + + +
+
+
+
widgets.maps.common.entities-limit
+ + + +
+
diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/map/map-settings.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/map/map-settings.component.ts index e2abdee4ff..3b5c57ecbe 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/map/map-settings.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/map/map-settings.component.ts @@ -22,9 +22,20 @@ import { UntypedFormBuilder, UntypedFormControl, UntypedFormGroup, - Validator + Validator, + Validators } from '@angular/forms'; -import { ImageSourceType, MapDataLayerType, MapSetting, MapType } from '@home/components/widget/lib/maps/models/map.models'; +import { + defaultImageMapSourceSettings, + ImageMapSourceSettings, imageMapSourceSettingsValidator, + mapControlPositions, + mapControlsPositionTranslationMap, + MapDataLayerType, + MapSetting, + MapType, + mapZoomActions, + mapZoomActionTranslationMap +} from '@home/components/widget/lib/maps/models/map.models'; import { takeUntilDestroyed } from '@angular/core/rxjs-interop'; import { merge, Observable } from 'rxjs'; import { coerceBoolean } from '@shared/decorators/coercion'; @@ -36,7 +47,7 @@ import { DataKeyConfigDialogComponent, DataKeyConfigDialogData } from '@home/components/widget/lib/settings/common/key/data-key-config-dialog.component'; -import { deepClone } from '@core/utils'; +import { deepClone, mergeDeep } from '@core/utils'; import { MatDialog } from '@angular/material/dialog'; @Component({ @@ -58,6 +69,14 @@ import { MatDialog } from '@angular/material/dialog'; }) export class MapSettingsComponent implements OnInit, ControlValueAccessor, Validator { + mapControlPositions = mapControlPositions; + + mapZoomActions = mapZoomActions; + + mapControlsPositionTranslationMap = mapControlsPositionTranslationMap; + + mapZoomActionTranslationMap = mapZoomActionTranslationMap; + MapType = MapType; @Input() @@ -105,10 +124,7 @@ export class MapSettingsComponent implements OnInit, ControlValueAccessor, Valid this.mapSettingsFormGroup = this.fb.group({ mapType: [null, []], layers: [null, []], - imageSourceType: [null, []], - imageUrl: [null, []], - imageEntityAlias: [null, []], - imageUrlAttribute: [null, []], + imageSource: [null, [imageMapSourceSettingsValidator]], markers: [null, []], polygons: [null, []], circles: [null, []], @@ -118,8 +134,8 @@ export class MapSettingsComponent implements OnInit, ControlValueAccessor, Valid fitMapBounds: [null, []], useDefaultCenterPosition: [null, []], defaultCenterPosition: [null, []], - defaultZoomLevel: [null, []], - mapPageSize: [null, []] + defaultZoomLevel: [null, [Validators.min(0), Validators.max(20)]], + mapPageSize: [null, [Validators.min(1), Validators.required]] }); this.mapSettingsFormGroup.valueChanges.pipe( takeUntilDestroyed(this.destroyRef) @@ -127,12 +143,17 @@ export class MapSettingsComponent implements OnInit, ControlValueAccessor, Valid this.updateModel(); }); merge(this.mapSettingsFormGroup.get('mapType').valueChanges, - this.mapSettingsFormGroup.get('imageSourceType').valueChanges + this.mapSettingsFormGroup.get('useDefaultCenterPosition').valueChanges ).pipe( takeUntilDestroyed(this.destroyRef) ).subscribe(() => { this.updateValidators(); }); + this.mapSettingsFormGroup.get('mapType').valueChanges.pipe( + takeUntilDestroyed(this.destroyRef) + ).subscribe((mapType: MapType) => { + this.mapTypeChanged(mapType); + }); } registerOnChange(fn: any): void { @@ -160,7 +181,7 @@ export class MapSettingsComponent implements OnInit, ControlValueAccessor, Valid this.updateValidators(); } - public validate(c: UntypedFormControl) { + public validate(_c: UntypedFormControl) { const valid = this.mapSettingsFormGroup.valid; return valid ? null : { mapSettings: { @@ -171,24 +192,34 @@ export class MapSettingsComponent implements OnInit, ControlValueAccessor, Valid private updateValidators() { const mapType: MapType = this.mapSettingsFormGroup.get('mapType').value; - const imageSourceType: ImageSourceType = this.mapSettingsFormGroup.get('imageSourceType').value; if (mapType === MapType.geoMap) { this.mapSettingsFormGroup.get('layers').enable({emitEvent: false}); - this.mapSettingsFormGroup.get('imageSourceType').disable({emitEvent: false}); - this.mapSettingsFormGroup.get('imageUrl').disable({emitEvent: false}); - this.mapSettingsFormGroup.get('imageEntityAlias').disable({emitEvent: false}); - this.mapSettingsFormGroup.get('imageUrlAttribute').disable({emitEvent: false}); + this.mapSettingsFormGroup.get('imageSource').disable({emitEvent: false}); + this.mapSettingsFormGroup.get('fitMapBounds').enable({emitEvent: false}); + this.mapSettingsFormGroup.get('useDefaultCenterPosition').enable({emitEvent: false}); + const useDefaultCenterPosition: boolean = this.mapSettingsFormGroup.get('useDefaultCenterPosition').value; + if (useDefaultCenterPosition) { + this.mapSettingsFormGroup.get('defaultCenterPosition').enable({emitEvent: false}); + } else { + this.mapSettingsFormGroup.get('defaultCenterPosition').disable({emitEvent: false}); + } + this.mapSettingsFormGroup.get('defaultZoomLevel').enable({emitEvent: false}); } else { this.mapSettingsFormGroup.get('layers').disable({emitEvent: false}); - this.mapSettingsFormGroup.get('imageSourceType').enable({emitEvent: false}); - if (imageSourceType === ImageSourceType.image) { - this.mapSettingsFormGroup.get('imageUrl').enable({emitEvent: false}); - this.mapSettingsFormGroup.get('imageEntityAlias').disable({emitEvent: false}); - this.mapSettingsFormGroup.get('imageUrlAttribute').disable({emitEvent: false}); - } else { - this.mapSettingsFormGroup.get('imageUrl').disable({emitEvent: false}); - this.mapSettingsFormGroup.get('imageEntityAlias').enable({emitEvent: false}); - this.mapSettingsFormGroup.get('imageUrlAttribute').enable({emitEvent: false}); + this.mapSettingsFormGroup.get('imageSource').enable({emitEvent: false}); + this.mapSettingsFormGroup.get('fitMapBounds').disable({emitEvent: false}); + this.mapSettingsFormGroup.get('useDefaultCenterPosition').disable({emitEvent: false}); + this.mapSettingsFormGroup.get('defaultCenterPosition').disable({emitEvent: false}); + this.mapSettingsFormGroup.get('defaultZoomLevel').disable({emitEvent: false}); + } + } + + private mapTypeChanged(mapType: MapType): void { + if (mapType === MapType.image) { + let imageSource: ImageMapSourceSettings = this.mapSettingsFormGroup.get('imageSource').value; + if (!imageSource?.sourceType) { + imageSource = mergeDeep({} as ImageMapSourceSettings, defaultImageMapSourceSettings); + this.mapSettingsFormGroup.get('imageSource').patchValue(imageSource); } } } 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 c31bddd8f1..8dd52cb1fe 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 @@ -230,6 +230,9 @@ 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 { + ImageMapSourceSettingsComponent +} from '@home/components/widget/lib/settings/common/map/image-map-source-settings.component'; @NgModule({ declarations: [ @@ -303,6 +306,7 @@ import { MapLayerSettingsPanelComponent, MapLayerRowComponent, MapLayersComponent, + ImageMapSourceSettingsComponent, DataLayerColorSettingsComponent, DataLayerColorSettingsPanelComponent, DataLayerPatternSettingsComponent, 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 e96d92d6b0..ad986ea403 100644 --- a/ui-ngx/src/assets/locale/locale.constant-en_US.json +++ b/ui-ngx/src/assets/locale/locale.constant-en_US.json @@ -7548,6 +7548,33 @@ "map": "Map", "image": "Image" }, + "image": { + "image-source": "Image source", + "image-source-image": "Image", + "image-source-entity-key": "Entity key", + "source-entity-alias": "Source entity alias", + "image-url-key": "Image URL key", + "image-url-key-required": "Image URL key is required" + }, + "control": { + "map-controls": "Map controls", + "position": "Position", + "position-topleft": "Top-left", + "position-topright": "Top-right", + "position-bottomleft": "Bottom-left", + "position-bottomright": "Bottom-right", + "zoom-actions": "Zoom actions", + "zoom-scroll": "Scroll", + "zoom-double-click": "Double click", + "zoom-control-buttons": "Control buttons" + }, + "common": { + "common-map-settings": "Common map settings", + "fit-map-bounds": "Fit map bounds to cover all markers", + "default-map-center-position": "Default map center position", + "default-map-zoom-level": "Default map zoom level", + "entities-limit": "Limit of entities to load" + }, "layer": { "label": "Label", "layer": "Layer", @@ -7667,6 +7694,8 @@ "marker-offset": "Marker offset", "offset-horizontal": "Horizontal", "offset-vertical": "Vertical", + "position-conversion": "Position conversion", + "position-conversion-function": "Position conversion function, should return x,y coordinates as double from 0 to 1 each", "clustering": { "use-map-markers-clustering": "Use map markers clustering", "zoom-on-cluster-click": "Zoom when clicking on a cluster", From 5d3cafc65d3ccb9725e70854ac59bd071e5a9401 Mon Sep 17 00:00:00 2001 From: Igor Kulikov Date: Fri, 24 Jan 2025 20:48:12 +0200 Subject: [PATCH 033/345] UI: Map layers drag mode. --- .../lib/maps/data-layer/circles-data-layer.ts | 57 ++++++++- .../lib/maps/data-layer/map-data-layer.ts | 114 ++++++++++++++++- .../lib/maps/data-layer/markers-data-layer.ts | 115 ++++++++++++++---- .../maps/data-layer/polygons-data-layer.ts | 78 ++++++++++-- .../components/widget/lib/maps/geo-map.ts | 63 ++++++++-- .../components/widget/lib/maps/image-map.ts | 79 +++++++++--- .../home/components/widget/lib/maps/map.scss | 27 ++++ .../home/components/widget/lib/maps/map.ts | 86 +++++++++++-- .../widget/lib/maps/models/map.models.ts | 71 ++++++++++- .../map/map-data-layer-dialog.component.html | 16 +++ .../map/map-data-layer-dialog.component.ts | 27 +++- .../assets/locale/locale.constant-en_US.json | 15 ++- 12 files changed, 666 insertions(+), 82 deletions(-) diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/maps/data-layer/circles-data-layer.ts b/ui-ngx/src/app/modules/home/components/widget/lib/maps/data-layer/circles-data-layer.ts index 313af2c4fb..f1c364c66f 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/maps/data-layer/circles-data-layer.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/maps/data-layer/circles-data-layer.ts @@ -31,6 +31,7 @@ class TbCircleDataLayerItem extends TbDataLayerItem, dsData: FormattedData[], @@ -45,7 +46,8 @@ class TbCircleDataLayerItem extends TbDataLayerItem { + this.editing = true; + }); + this.circle.on('pm:dragend', () => { + this.saveCircleCoordinates(); + this.editing = false; + }); + } + + protected disableDrag(): void { + this.circle.pm.disableLayerDrag(); + this.circle.off('pm:dragstart'); + this.circle.off('pm:dragend'); + } + + private saveCircleCoordinates() { + const center = this.circle.getLatLng(); + const radius = this.circle.getRadius(); + this.dataLayer.saveCircleCoordinates(this.data, center, radius); + } + private updateCircleShape(data: FormattedData) { + if (this.editing) { + return; + } const circleData = this.dataLayer.extractCircleCoordinates(data); const center = new L.LatLng(circleData.latitude, circleData.longitude); if (!this.circle.getLatLng().equals(center)) { @@ -117,13 +157,22 @@ export class TbCirclesDataLayer extends TbShapesDataLayer, dsData: FormattedData[]): TbDataLayerItem { - throw new TbCircleDataLayerItem(data, dsData, this.settings, this); + return new TbCircleDataLayerItem(data, dsData, this.settings, this); } public extractCircleCoordinates(data: FormattedData) { const circleData: TbCircleData = JSON.parse(data[this.settings.circleKey.label]); - return this.map.convertCircleData(circleData); + return this.map.circleDataToCoordinates(circleData); } - + public saveCircleCoordinates(data: FormattedData, center: L.LatLng, radius: number): void { + const converted = this.map.coordinatesToCircleData(center, radius); + const circleData = [ + { + dataKey: this.settings.circleKey, + value: converted + } + ]; + this.map.saveItemData(data.$datasource, circleData).subscribe(); + } } diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/maps/data-layer/map-data-layer.ts b/ui-ngx/src/app/modules/home/components/widget/lib/maps/data-layer/map-data-layer.ts index 6d52838751..269d8fc294 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/maps/data-layer/map-data-layer.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/maps/data-layer/map-data-layer.ts @@ -17,6 +17,7 @@ import { DataLayerColorSettings, DataLayerColorType, + DataLayerEditAction, DataLayerPatternSettings, DataLayerPatternType, DataLayerTooltipTrigger, @@ -63,6 +64,7 @@ export abstract class TbDataLayerItem, dsData: FormattedData[]): void; + protected abstract addItemClass(clazz: string): void; + + protected abstract removeItemClass(clazz: string): void; + + protected abstract enableDrag(): void; + + protected abstract disableDrag(): void; + + protected enableEdit(): void { + if (this.dataLayer.isHoverable()) { + this.addItemClass('tb-hoverable'); + } + if (this.dataLayer.isDragEnabled()) { + this.enableDrag(); + this.addItemClass('tb-draggable'); + } + } + + protected disableEdit(): void { + if (this.dataLayer.isHoverable()) { + this.removeItemClass('tb-hoverable'); + } + if (this.dataLayer.isDragEnabled()) { + this.disableDrag(); + this.removeItemClass('tb-draggable'); + } + } + public invalidateCoordinates(): void { this.doInvalidateCoordinates(this.data, this.dataLayer.getMap().getData()); } + public editModeUpdated() { + if (this.dataLayer.isEditMode()) { + this.enableEdit(); + } else { + this.disableEdit(); + } + } + public update(data: FormattedData, dsData: FormattedData[]): void { this.data = data; this.doUpdate(data, dsData); @@ -245,6 +283,17 @@ export abstract class TbMapDataLayer { @@ -291,6 +352,38 @@ export abstract class TbMapDataLayer item.editModeUpdated()); + } + public abstract dataLayerType(): MapDataLayerType; protected abstract defaultBaseSettings(map: TbMap): Partial; 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 0a8c4e1983..8f7753b31e 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 @@ -60,9 +60,10 @@ import { TbImageMap } from '@home/components/widget/lib/maps/image-map'; class TbMarkerDataLayerItem extends TbDataLayerItem { - private location: L.LatLng; private marker: L.Marker; private labelOffset: L.PointTuple; + private iconClassList: string[]; + private moving = false; constructor(data: FormattedData, dsData: FormattedData[], @@ -72,8 +73,9 @@ class TbMarkerDataLayerItem extends TbDataLayerItem, dsData: FormattedData[]): L.Marker { - this.location = this.dataLayer.extractLocation(data, dsData); - this.marker = L.marker(this.location, { + this.iconClassList = []; + const location = this.dataLayer.extractLocation(data, dsData); + this.marker = L.marker(location, { tbMarkerData: data }); @@ -96,26 +98,69 @@ class TbMarkerDataLayerItem extends TbDataLayerItem, dsData: FormattedData[]): void { this.marker.options.tbMarkerData = data; - this.updateMarkerPosition(data, dsData); + this.updateMarkerLocation(data, dsData); this.updateTooltip(data, dsData); this.updateMarkerIcon(data, dsData); } protected doInvalidateCoordinates(data: FormattedData, dsData: FormattedData[]): void { - this.updateMarkerPosition(data, dsData); + this.updateMarkerLocation(data, dsData); } - private updateMarkerPosition(data: FormattedData, dsData: FormattedData[]) { - const position = this.dataLayer.extractLocation(data, dsData); - if (!this.marker.getLatLng().equals(position)) { - this.location = position; - this.marker.setLatLng(position); + protected addItemClass(clazz: string): void { + if (!this.iconClassList.includes(clazz)) { + this.iconClassList.push(clazz); + this.marker.options.icon.options.className = this.updateIconClasses(this.marker.options.icon.options.className); + if ((this.marker as any)._icon) { + L.DomUtil.addClass((this.marker as any)._icon, clazz); + } + } + } + + protected removeItemClass(clazz: string): void { + const index = this.iconClassList.indexOf(clazz); + if (index !== -1) { + this.iconClassList.splice(index, 1); + this.marker.options.icon.options.className = this.updateIconClasses(this.marker.options.icon.options.className); + if ((this.marker as any)._icon) { + L.DomUtil.removeClass((this.marker as any)._icon, clazz); + } + } + } + + protected enableDrag(): void { + this.marker.options.draggable = true; + this.marker.on('dragstart', () => { + this.moving = true; + }); + this.marker.on('dragend', () => { + this.saveMarkerLocation(); + this.moving = false; + }); + } + + protected disableDrag(): void { + this.marker.options.draggable = false; + this.marker.off('dragstart'); + this.marker.off('dragend'); + } + + private saveMarkerLocation() { + const location = this.marker.getLatLng(); + this.dataLayer.saveMarkerLocation(this.data, location); + } + + private updateMarkerLocation(data: FormattedData, dsData: FormattedData[]) { + const location = this.dataLayer.extractLocation(data, dsData); + if (!this.marker.getLatLng().equals(location) && !this.moving) { + this.marker.setLatLng(location); } } private updateMarkerIcon(data: FormattedData, dsData: FormattedData[]) { this.dataLayer.markerIconProcessor.createMarkerIcon(data, dsData).subscribe( (iconInfo) => { + iconInfo.icon.options.className = this.updateIconClasses(iconInfo.icon.options.className); this.marker.setIcon(iconInfo.icon); const anchor = iconInfo.icon.options.iconAnchor; if (anchor && Array.isArray(anchor)) { @@ -124,9 +169,23 @@ class TbMarkerDataLayerItem extends TbDataLayerItem { + if (!classes.includes(clazz)) { + classes.push(clazz); + } + }); + return classes.join(' '); + } } abstract class MarkerIconProcessor { @@ -390,7 +449,7 @@ export class TbMarkersDataLayer extends TbMapDataLayer): boolean { - return !!this.extractPosition(layerData); + return !!this.extractLocationData(layerData); } protected createLayerItem(data: FormattedData, dsData: FormattedData[]): TbMarkerDataLayerItem { @@ -412,13 +471,7 @@ export class TbMarkersDataLayer extends TbMapDataLayer { @@ -463,7 +516,7 @@ export class TbMarkersDataLayer extends TbMapDataLayer): {x: number; y: number} { + private extractLocationData(data: FormattedData): {x: number; y: number} { if (data) { const xKeyVal = data[this.settings.xKey.label]; const yKeyVal = data[this.settings.yKey.label]; @@ -524,15 +577,31 @@ export class TbMarkersDataLayer extends TbMapDataLayer, dsData: FormattedData[]): L.LatLng { - let position = this.extractPosition(data); - if (position) { + let locationData = this.extractLocationData(data); + if (locationData) { if (this.map.type() === MapType.image && this.positionFunction) { const imageMap = this.map as TbImageMap; - position = this.positionFunction.execute(position.x, position.y, data, dsData, imageMap.getAspect()) || {x: 0, y: 0}; + locationData = this.positionFunction.execute(locationData.x, locationData.y, data, dsData, imageMap.getAspect()) || {x: 0, y: 0}; } - return this.map.positionToLatLng(position); + return this.map.locationDataToLatLng(locationData); } else { return null; } } + + public saveMarkerLocation(data: FormattedData, position: L.LatLng): void { + const converted = this.map.latLngToLocationData(position); + const locationData = [ + { + dataKey: this.settings.xKey, + value: converted.x + }, + { + dataKey: this.settings.yKey, + value: converted.y + } + ]; + this.map.saveItemData(data.$datasource, locationData).subscribe(); + } + } diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/maps/data-layer/polygons-data-layer.ts b/ui-ngx/src/app/modules/home/components/widget/lib/maps/data-layer/polygons-data-layer.ts index 419a3e0d4c..789795149c 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/maps/data-layer/polygons-data-layer.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/maps/data-layer/polygons-data-layer.ts @@ -18,7 +18,7 @@ import { defaultBasePolygonsDataLayerSettings, isCutPolygon, isJSON, PolygonsDataLayerSettings, - TbMapDatasource + TbMapDatasource, TbPolyData, TbPolygonCoordinates, TbPolygonRawCoordinates } from '@home/components/widget/lib/maps/models/map.models'; import L from 'leaflet'; import { FormattedData } from '@shared/models/widget.models'; @@ -33,6 +33,7 @@ class TbPolygonDataLayerItem extends TbDataLayerItem, dsData: FormattedData[], @@ -45,8 +46,9 @@ class TbPolygonDataLayerItem extends TbDataLayerItem { + this.editing = true; + }); + this.polygon.on('pm:dragend', () => { + this.savePolygonCoordinates(); + this.editing = false; + }); + } + + protected disableDrag(): void { + this.polygon.pm.disableLayerDrag(); + this.polygon.off('pm:dragstart'); + this.polygon.off('pm:dragend'); + } + + private savePolygonCoordinates() { + let coordinates: TbPolygonCoordinates = this.polygon.getLatLngs(); + if (coordinates.length === 1) { + coordinates = coordinates[0] as TbPolygonCoordinates; + } + if (this.polygon instanceof L.Rectangle && !isCutPolygon(coordinates)) { + const bounds = this.polygon.getBounds(); + const boundsArray = [bounds.getNorthWest(), bounds.getNorthEast(), bounds.getSouthWest(), bounds.getSouthEast()]; + if (coordinates.every(point => boundsArray.find(boundPoint => boundPoint.equals(point as L.LatLng)) !== undefined)) { + coordinates = [bounds.getNorthWest(), bounds.getSouthEast()]; + } + } + this.dataLayer.savePolygonCoordinates(this.data, coordinates); + } + private updatePolygonShape(data: FormattedData) { - const polyData = this.dataLayer.extractPolygonCoordinates(data); + if (this.editing) { + return; + } + const polyData = this.dataLayer.extractPolygonCoordinates(data) as TbPolyData; if (isCutPolygon(polyData) || polyData.length !== 2) { if (this.polygon instanceof L.Rectangle) { this.polygonContainer.removeLayer(this.polygon); this.polygon = L.polygon(polyData, { - ...this.polygonStyle + ...this.polygonStyle, + snapIgnore: !this.dataLayer.isSnappable() }); this.polygon.addTo(this.polygonContainer); + this.editModeUpdated(); } else { this.polygon.setLatLngs(polyData); } } else if (polyData.length === 2) { - const bounds = new L.LatLngBounds(polyData); + const bounds = new L.LatLngBounds(polyData as L.LatLngTuple[]); (this.polygon as L.Rectangle).setBounds(bounds); } } @@ -134,11 +185,22 @@ export class TbPolygonsDataLayer extends TbShapesDataLayer) { + public extractPolygonCoordinates(data: FormattedData): TbPolygonRawCoordinates { let rawPolyData = data[this.settings.polygonKey.label]; if (isString(rawPolyData)) { rawPolyData = JSON.parse(rawPolyData); } - return this.map.toPolygonCoordinates(rawPolyData); + return this.map.polygonDataToCoordinates(rawPolyData); + } + + public savePolygonCoordinates(data: FormattedData, coordinates: TbPolygonCoordinates): void { + const converted = this.map.coordinatesToPolygonData(coordinates); + const polygonData = [ + { + dataKey: this.settings.polygonKey, + value: converted + } + ]; + this.map.saveItemData(data.$datasource, polygonData).subscribe(); } } diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/maps/geo-map.ts b/ui-ngx/src/app/modules/home/components/widget/lib/maps/geo-map.ts index ee5baeb109..746d576fe2 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/maps/geo-map.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/maps/geo-map.ts @@ -15,15 +15,22 @@ /// import { + checkLngLat, DEFAULT_ZOOM_LEVEL, defaultGeoMapSettings, - GeoMapSettings, latLngPointToBounds, - MapZoomAction, TbCircleData + GeoMapSettings, + latLngPointToBounds, + MapZoomAction, + TbCircleData, + TbPolygonCoordinate, + TbPolygonCoordinates, + TbPolygonRawCoordinate, + TbPolygonRawCoordinates } from '@home/components/widget/lib/maps/models/map.models'; import { WidgetContext } from '@home/models/widget-component.models'; import { DeepPartial } from '@shared/models/common'; import { forkJoin, Observable, of } from 'rxjs'; -import L, { LatLngBounds, LatLngTuple } from 'leaflet'; +import L from 'leaflet'; import { map, tap } from 'rxjs/operators'; import { TbMapLayer } from '@home/components/widget/lib/maps/map-layer'; import { TbMap } from '@home/components/widget/lib/maps/map'; @@ -53,7 +60,7 @@ export class TbGeoMap extends TbMap { protected onResize(): void {} - protected fitBounds(bounds: LatLngBounds) { + protected fitBounds(bounds: L.LatLngBounds) { if (bounds.isValid()) { if (!this.settings.fitMapBounds && this.settings.defaultZoomLevel) { this.map.setZoom(this.settings.defaultZoomLevel, { animate: false }); @@ -115,27 +122,63 @@ export class TbGeoMap extends TbMap { ); } - public positionToLatLng(position: {x: number; y: number}): L.LatLng { + public locationDataToLatLng(position: {x: number; y: number}): L.LatLng { return L.latLng(position.x, position.y) as L.LatLng; } - public toPolygonCoordinates(expression: (LatLngTuple | LatLngTuple[] | LatLngTuple[][])[]): any { - return (expression).map((el) => { - if (!Array.isArray(el[0]) && el.length === 2) { + public latLngToLocationData(position: L.LatLng): {x: number; y: number} { + position = position ? checkLngLat(position, this.southWest, this.northEast, 0) : {lat: null, lng: null} as L.LatLng; + return { + x: position.lat, + y: position.lng + } + } + + public polygonDataToCoordinates(expression: TbPolygonRawCoordinates): TbPolygonRawCoordinates { + return (expression).map((el: TbPolygonRawCoordinate) => { + if (!Array.isArray(el[0]) && !Array.isArray(el[1]) && el.length === 2) { return el; } else if (Array.isArray(el) && el.length) { - return this.toPolygonCoordinates(el as LatLngTuple[] | LatLngTuple[][]); + return this.polygonDataToCoordinates(el as TbPolygonRawCoordinates) as TbPolygonRawCoordinate; } else { return null; } }).filter(el => !!el); } - public convertCircleData(circle: TbCircleData): TbCircleData { + public coordinatesToPolygonData(coordinates: TbPolygonCoordinates): TbPolygonRawCoordinates { + if (coordinates.length) { + return coordinates.map((point: TbPolygonCoordinate) => { + if (Array.isArray(point)) { + return this.coordinatesToPolygonData(point) as TbPolygonRawCoordinate; + } else { + const convertPoint = checkLngLat(point, this.southWest, this.northEast); + return [convertPoint.lat, convertPoint.lng]; + } + }); + } + return []; + } + + public circleDataToCoordinates(circle: TbCircleData): TbCircleData { const centerPoint = latLngPointToBounds(new L.LatLng(circle.latitude, circle.longitude), this.southWest, this.northEast); circle.latitude = centerPoint.lat; circle.longitude = centerPoint.lng; return circle; } + public coordinatesToCircleData(center: L.LatLng, radius: number): TbCircleData { + let circleData: TbCircleData = null; + if (center) { + const position = checkLngLat(center, this.southWest, this.northEast); + circleData = { + latitude: position.lat, + longitude: position.lng, + radius + }; + } + return circleData; + } + + } diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/maps/image-map.ts b/ui-ngx/src/app/modules/home/components/widget/lib/maps/image-map.ts index e781f1c62a..6bd2e24afa 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/maps/image-map.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/maps/image-map.ts @@ -15,6 +15,7 @@ /// import { + calculateNewPointCoordinate, defaultImageMapSettings, defaultImageMapSourceSettings, ImageMapSettings, @@ -22,12 +23,12 @@ import { ImageSourceType, loadImageWithAspect, MapZoomAction, - TbCircleData + TbCircleData, TbPolygonCoordinate, TbPolygonCoordinates, TbPolygonRawCoordinate, TbPolygonRawCoordinates } from '@home/components/widget/lib/maps/models/map.models'; import { WidgetContext } from '@home/models/widget-component.models'; import { DeepPartial } from '@shared/models/common'; import { Observable, of, ReplaySubject, switchMap } from 'rxjs'; -import L, { LatLngBounds, LatLngLiteral, LatLngTuple } from 'leaflet'; +import L from 'leaflet'; import { TbMap } from '@home/components/widget/lib/maps/map'; import { ImagePipe } from '@shared/pipe/image.pipe'; import { catchError } from 'rxjs/operators'; @@ -115,38 +116,62 @@ export class TbImageMap extends TbMap { } } - protected fitBounds(_bounds: LatLngBounds) {} + protected fitBounds(_bounds: L.LatLngBounds) {} - public positionToLatLng(position: {x: number; y: number}): L.LatLng { + public locationDataToLatLng(position: {x: number; y: number}): L.LatLng { return this.pointToLatLng( position.x * this.width, position.y * this.height); } - public pointToLatLng(x: number, y: number): L.LatLng { - return L.CRS.Simple.pointToLatLng({ x, y } as L.PointExpression, this.maxZoom - 1); - } - - private latLngToPoint(latLng: LatLngLiteral): L.Point { - return L.CRS.Simple.latLngToPoint(latLng, this.maxZoom - 1); + public latLngToLocationData(position: L.LatLng): {x: number; y: number} { + if (!position) { + return { + x: null, + y: null + }; + } + const point = this.latLngToPoint(position); + const posX = calculateNewPointCoordinate(point.x, this.width); + const posY = calculateNewPointCoordinate(point.y, this.height); + return { + x: posX, + y: posY + }; } - public toPolygonCoordinates(expression: (LatLngTuple | LatLngTuple[] | LatLngTuple[][])[]): any { - return (expression).map((el) => { + public polygonDataToCoordinates(expression: TbPolygonRawCoordinates): TbPolygonRawCoordinates { + return expression.map((el: TbPolygonRawCoordinate) => { if (!Array.isArray(el[0]) && !Array.isArray(el[1]) && el.length === 2) { - return this.pointToLatLng( + const latLng = this.pointToLatLng( el[0] * this.width, el[1] * this.height ); + return [latLng.lat, latLng.lng] as TbPolygonRawCoordinate; } else if (Array.isArray(el) && el.length) { - return this.toPolygonCoordinates(el as LatLngTuple[] | LatLngTuple[][]); + return this.polygonDataToCoordinates(el as TbPolygonRawCoordinates) as TbPolygonRawCoordinate; } else { return null; } }).filter(el => !!el); } - public convertCircleData(circle: TbCircleData): TbCircleData { + public coordinatesToPolygonData(coordinates: TbPolygonCoordinates): TbPolygonRawCoordinates { + if (coordinates.length) { + return coordinates.map((point: TbPolygonCoordinate) => { + if (Array.isArray(point)) { + return this.coordinatesToPolygonData(point) as TbPolygonRawCoordinate; + } else { + const pos = this.latLngToPoint(point); + return [calculateNewPointCoordinate(pos.x, this.width), calculateNewPointCoordinate(pos.y, this.height)]; + } + }); + } else { + return []; + } + } + + public circleDataToCoordinates(circle: TbCircleData): TbCircleData { const centerPoint = this.pointToLatLng(circle.latitude * this.width, circle.longitude * this.height); circle.latitude = centerPoint.lat; circle.longitude = centerPoint.lng; @@ -154,10 +179,34 @@ export class TbImageMap extends TbMap { return circle; } + public coordinatesToCircleData(center: L.LatLng, radius: number): TbCircleData { + let circleData: TbCircleData = null; + if (center) { + const point = this.latLngToPoint(center); + const posX = calculateNewPointCoordinate(point.x, this.width); + const posY = calculateNewPointCoordinate(point.y, this.height); + const convertedRadius = calculateNewPointCoordinate(radius, this.width); + circleData = { + latitude: posX, + longitude: posY, + radius: convertedRadius + }; + } + return circleData; + } + public getAspect(): number { return this.imageLayerData.aspect; } + private pointToLatLng(x: number, y: number): L.LatLng { + return L.CRS.Simple.pointToLatLng({ x, y } as L.PointExpression, this.maxZoom - 1); + } + + private latLngToPoint(latLng: L.LatLngLiteral): L.Point { + return L.CRS.Simple.latLngToPoint(latLng, this.maxZoom - 1); + } + private doCreateMap(updateImage?: boolean) { if (!this.map && this.imageLayerData.aspect > 0) { const center = this.pointToLatLng(this.width / 2, this.height / 2); diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/maps/map.scss b/ui-ngx/src/app/modules/home/components/widget/lib/maps/map.scss index aabdf47810..238d91f03f 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/maps/map.scss +++ b/ui-ngx/src/app/modules/home/components/widget/lib/maps/map.scss @@ -15,6 +15,9 @@ */ @import '../../../../../../../scss/constants'; +//$map-element-hover-color: #307FE5; +$map-element-hover-color: rgba(0,0,0,0.56); + .tb-map-layout { display: flex; width: 100%; @@ -104,6 +107,30 @@ &.tb-marker-div-icon { background: none; border: none; + &.tb-draggable { + cursor: move; + } + &.tb-hoverable { + svg { + transition: filter 0.2s; + } + &:hover { + svg { + filter: drop-shadow( 0 0 4px $map-element-hover-color); + } + } + } + } + } + img.leaflet-marker-icon, path { + &.tb-draggable { + cursor: move; + } + &.tb-hoverable { + transition: filter 0.2s; + &:hover { + filter: drop-shadow( 0 0 4px $map-element-hover-color); + } } } .tb-cluster-marker-container { 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 350aeaf85d..672682591c 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 @@ -17,17 +17,18 @@ import { additionalMapDataSourcesToDatasources, BaseMapSettings, + DataKeyValuePair, MapActionHandler, MapType, mergeMapDatasources, parseCenterPosition, TbCircleData, - TbMapDatasource + TbMapDatasource, TbPolygonCoordinates, TbPolygonRawCoordinates } from '@home/components/widget/lib/maps/models/map.models'; import { WidgetContext } from '@home/models/widget-component.models'; import { formattedDataFormDatasourceData, isDefinedAndNotNull, mergeDeepIgnoreArray } from '@core/utils'; import { DeepPartial } from '@shared/models/common'; -import L, { LatLngBounds, LatLngTuple, LeafletMouseEvent, Projection } from 'leaflet'; +import L from 'leaflet'; import { forkJoin, Observable, of } from 'rxjs'; import { switchMap } from 'rxjs/operators'; import '@home/components/widget/lib/maps/leaflet/leaflet-tb'; @@ -39,6 +40,9 @@ import { CustomTranslatePipe } from '@shared/pipe/custom-translate.pipe'; import { TbMarkersDataLayer } from '@home/components/widget/lib/maps/data-layer/markers-data-layer'; import { TbPolygonsDataLayer } from '@home/components/widget/lib/maps/data-layer/polygons-data-layer'; import { TbCirclesDataLayer } from '@home/components/widget/lib/maps/data-layer/circles-data-layer'; +import { AttributeService } from '@core/http/attribute.service'; +import { AttributeData, AttributeScope, DataKeyType, LatestTelemetry } from '@shared/models/telemetry/telemetry.models'; +import { EntityId } from '@shared/models/id/entity-id'; import ITooltipsterInstance = JQueryTooltipster.ITooltipsterInstance; export abstract class TbMap { @@ -49,8 +53,8 @@ export abstract class TbMap { protected defaultCenterPosition: [number, number]; protected ignoreUpdateBounds = false; - protected southWest = new L.LatLng(-Projection.SphericalMercator['MAX_LATITUDE'], -180); - protected northEast = new L.LatLng(Projection.SphericalMercator['MAX_LATITUDE'], 180); + protected southWest = new L.LatLng(-L.Projection.SphericalMercator['MAX_LATITUDE'], -180); + protected northEast = new L.LatLng(L.Projection.SphericalMercator['MAX_LATITUDE'], 180); protected dataLayers: TbMapDataLayer[]; protected dsData: FormattedData[]; @@ -128,6 +132,7 @@ export abstract class TbMap { this.map.panTo(this.defaultCenterPosition); } this.setupDataLayers(); + this.setupEditMode(); this.createdControlButtonTooltip(); } @@ -225,6 +230,13 @@ export abstract class TbMap { } } + private setupEditMode() { + const dragEnabled = this.dataLayers.some(dl => dl.isDragEnabled()); + if (dragEnabled) { + //this.map.pm.enableGlobalDragMode(); + } + } + private createdControlButtonTooltip() { import('tooltipster').then(() => { if ($.tooltipster) { @@ -326,7 +338,7 @@ export abstract class TbMap { protected abstract onResize(): void; - protected abstract fitBounds(bounds: LatLngBounds): void; + protected abstract fitBounds(bounds: L.LatLngBounds): void; protected doSetupControls(): Observable { return of(null); @@ -375,7 +387,7 @@ export abstract class TbMap { public markerClick(marker: L.Layer, datasource: TbMapDatasource): void { if (Object.keys(this.markerClickActions).length) { - marker.on('click', (event: LeafletMouseEvent) => { + marker.on('click', (event: L.LeafletMouseEvent) => { for (const action in this.markerClickActions) { this.markerClickActions[action](event.originalEvent, datasource); } @@ -385,7 +397,7 @@ export abstract class TbMap { public polygonClick(polygon: L.Layer, datasource: TbMapDatasource): void { if (Object.keys(this.polygonClickActions).length) { - polygon.on('click', (event: LeafletMouseEvent) => { + polygon.on('click', (event: L.LeafletMouseEvent) => { for (const action in this.polygonClickActions) { this.polygonClickActions[action](event.originalEvent, datasource); } @@ -395,7 +407,7 @@ export abstract class TbMap { public circleClick(circle: L.Layer, datasource: TbMapDatasource): void { if (Object.keys(this.circleClickActions).length) { - circle.on('click', (event: LeafletMouseEvent) => { + circle.on('click', (event: L.LeafletMouseEvent) => { for (const action in this.circleClickActions) { this.circleClickActions[action](event.originalEvent, datasource); } @@ -403,6 +415,50 @@ export abstract class TbMap { } } + public saveItemData(datasource: TbMapDatasource, data: DataKeyValuePair[]): Observable { + const attributeService = this.ctx.$injector.get(AttributeService); + const attributes: AttributeData[] = []; + const timeseries: AttributeData[] = []; + const entityId: EntityId = { + entityType: datasource.entityType, + id: datasource.entityId + }; + data.forEach(pair => { + const key = pair.dataKey; + if (key.type === DataKeyType.attribute) { + attributes.push({ + key: key.name, + value: pair.value + }); + } else if (key.type === DataKeyType.timeseries) { + timeseries.push({ + key: key.name, + value: pair.value + }); + } + }); + const observables: Observable[] = []; + if (timeseries.length) { + observables.push(attributeService.saveEntityTimeseries( + entityId, + LatestTelemetry.LATEST_TELEMETRY, + timeseries + )); + } + if (attributes.length) { + observables.push(attributeService.saveEntityAttributes( + entityId, + AttributeScope.SERVER_SCOPE, + attributes + )); + } + if (observables.length) { + return forkJoin(observables); + } else { + return of(null); + } + } + public destroy() { if (this.mapResize$) { this.mapResize$.disconnect(); @@ -415,10 +471,18 @@ export abstract class TbMap { }); } - public abstract positionToLatLng(position: {x: number; y: number}): L.LatLng; + public abstract locationDataToLatLng(position: {x: number; y: number}): L.LatLng; + + public abstract latLngToLocationData(position: L.LatLng): {x: number; y: number}; + + public abstract polygonDataToCoordinates(coordinates: TbPolygonRawCoordinates): TbPolygonRawCoordinates; + + public abstract coordinatesToPolygonData(coordinates: TbPolygonCoordinates): TbPolygonRawCoordinates; + + public abstract circleDataToCoordinates(circle: TbCircleData): TbCircleData; + + public abstract coordinatesToCircleData(center: L.LatLng, radius: number): TbCircleData; - public abstract toPolygonCoordinates(expression: (LatLngTuple | LatLngTuple[] | LatLngTuple[][])[]): any; - public abstract convertCircleData(circle: TbCircleData): TbCircleData; } diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/maps/models/map.models.ts b/ui-ngx/src/app/modules/home/components/widget/lib/maps/models/map.models.ts index 09f6f0c488..1f43e83c85 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/maps/models/map.models.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/maps/models/map.models.ts @@ -19,7 +19,7 @@ import { DataKeyType } from '@shared/models/telemetry/telemetry.models'; import { guid, hashCode, isDefinedAndNotNull, isNotEmptyStr, isString, mergeDeep } from '@core/utils'; import { AbstractControl, ValidationErrors, ValidatorFn } from '@angular/forms'; import { materialColors } from '@shared/models/material.models'; -import L from 'leaflet'; +import L, { LatLngExpression } from 'leaflet'; import { TbFunction } from '@shared/models/js-function.models'; import { Observable, Observer, of, switchMap } from 'rxjs'; import { map } from 'rxjs/operators'; @@ -89,11 +89,35 @@ export interface DataLayerTooltipSettings extends DataLayerPatternSettings { offsetY: number; } +export enum DataLayerEditAction { + add = 'add', + edit = 'edit', + move = 'move', + remove = 'remove' +} + +export const dataLayerEditActions = Object.keys(DataLayerEditAction) as DataLayerEditAction[]; + +export const dataLayerEditActionTranslationMap = new Map( + [ + [DataLayerEditAction.add, 'widgets.maps.data-layer.action-add'], + [DataLayerEditAction.edit, 'widgets.maps.data-layer.action-edit'], + [DataLayerEditAction.move, 'widgets.maps.data-layer.action-move'], + [DataLayerEditAction.remove, 'widgets.maps.data-layer.action-remove'] + ] +); + +export interface DataLayerEditSettings { + enabledActions: DataLayerEditAction[]; + snappable: boolean; +} + export interface MapDataLayerSettings extends MapDataSourceSettings { additionalDataKeys?: DataKey[]; label: DataLayerPatternSettings; tooltip: DataLayerTooltipSettings; groups?: string[]; + edit: DataLayerEditSettings; } export const defaultBaseDataLayerSettings = (mapType: MapType): Partial => ({ @@ -112,6 +136,10 @@ export const defaultBaseDataLayerSettings = (mapType: MapType): Partial${entityName}

X Pos: ${xPos:2}
Y Pos: ${yPos:2}
Temperature: ${temperature} °C
See tooltip settings for details', offsetX: 0, offsetY: -1 + }, + edit: { + enabledActions: [], + snappable: false } }) @@ -864,12 +892,23 @@ export type ClusterMarkerColorFunction = (data: FormattedData[] export type MarkerPositionFunction = (origXPos: number, origYPos: number, data: FormattedData, dsData: FormattedData[], aspect: number) => { x: number, y: number }; +export type TbPolygonRawCoordinate = L.LatLngTuple | L.LatLngTuple[] | L.LatLngTuple[][]; +export type TbPolygonRawCoordinates = TbPolygonRawCoordinate[]; +export type TbPolyData = L.LatLngTuple[] | L.LatLngTuple[][] | L.LatLngTuple[][][]; +export type TbPolygonCoordinate = L.LatLng | L.LatLng[] | L.LatLng[][]; +export type TbPolygonCoordinates = TbPolygonCoordinate[]; + export interface TbCircleData { latitude: number; longitude: number; radius: number; } +export type DataKeyValuePair = { + dataKey: DataKey; + value: any; +} + export const isJSON = (data: string): boolean => { try { const parseData = JSON.parse(data); @@ -892,7 +931,7 @@ export const isValidLongitude = (longitude: any): boolean => export const isValidLatLng = (latitude: any, longitude: any): boolean => isValidLatitude(latitude) && isValidLongitude(longitude); -export const isCutPolygon = (data): boolean => { +export const isCutPolygon = (data: TbPolygonCoordinates | TbPolygonRawCoordinates): boolean => { return data.length > 1 && Array.isArray(data[0]) && (Array.isArray(data[0][0]) || data[0][0] instanceof L.LatLng); } @@ -1069,3 +1108,31 @@ export const processTooltipTemplate = (template: string): string => { return template; } + +export function calculateNewPointCoordinate(coordinate: number, imageSize: number): number { + let pointCoordinate = coordinate / imageSize; + if (pointCoordinate < 0) { + pointCoordinate = 0; + } else if (pointCoordinate > 1) { + pointCoordinate = 1; + } + return pointCoordinate; +} + +export function checkLngLat(point: L.LatLng, southWest: L.LatLng, northEast: L.LatLng, offset = 0): L.LatLng { + const maxLngMap = northEast.lng - offset; + const minLngMap = southWest.lng + offset; + const maxLatMap = northEast.lat - offset; + const minLatMap = southWest.lat + offset; + if (point.lng > maxLngMap) { + point.lng = maxLngMap; + } else if (point.lng < minLngMap) { + point.lng = minLngMap; + } + if (point.lat > maxLatMap) { + point.lat = maxLatMap; + } else if (point.lat < minLatMap) { + point.lat = minLatMap; + } + return point; +} diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/map/map-data-layer-dialog.component.html b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/map/map-data-layer-dialog.component.html index 8742eff68c..cc17006027 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/map/map-data-layer-dialog.component.html +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/map/map-data-layer-dialog.component.html @@ -262,6 +262,22 @@ formControlName="groups">
+
+
{{ dataLayerEditTitle | translate }}
+
+
widgets.maps.data-layer.edit-instruments
+ + + {{ dataLayerEditActionTranslationMap.get(action) | translate }} + + +
+
+ + {{ 'widgets.maps.data-layer.enable-snapping' | translate }} + +
+
diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/map/map-data-layer-dialog.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/map/map-data-layer-dialog.component.ts index b57f92fc35..fe37953422 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/map/map-data-layer-dialog.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/map/map-data-layer-dialog.component.ts @@ -18,10 +18,12 @@ import { Component, DestroyRef, Inject, ViewEncapsulation } from '@angular/core' import { DialogComponent } from '@shared/components/dialog.component'; import { CirclesDataLayerSettings, + DataLayerEditAction, dataLayerEditActions, + dataLayerEditActionTranslationMap, defaultBaseMapDataLayerSettings, MapDataLayerSettings, MapDataLayerType, - MapType, + MapType, mapZoomActions, mapZoomActionTranslationMap, MarkersDataLayerSettings, MarkerType, PolygonsDataLayerSettings, @@ -71,6 +73,10 @@ export class MapDataLayerDialogComponent extends DialogComponent = []; datasourceTypesTranslations = datasourceTypeTranslationMap; + dataLayerEditTitle: string; + dataLayerEditActions: Array = []; + dataLayerEditActionTranslationMap = dataLayerEditActionTranslationMap; + dataLayerFormGroup: UntypedFormGroup; settings = this.data.settings; @@ -111,13 +117,19 @@ export class MapDataLayerDialogComponent extends DialogComponent Date: Mon, 27 Jan 2025 10:41:29 +0200 Subject: [PATCH 034/345] UI: Map - handle layer snapping --- .../lib/maps/data-layer/circles-data-layer.ts | 3 ++ .../lib/maps/data-layer/map-data-layer.ts | 1 + .../lib/maps/data-layer/markers-data-layer.ts | 45 ++++++++++++++----- .../maps/data-layer/polygons-data-layer.ts | 3 ++ 4 files changed, 40 insertions(+), 12 deletions(-) diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/maps/data-layer/circles-data-layer.ts b/ui-ngx/src/app/modules/home/components/widget/lib/maps/data-layer/circles-data-layer.ts index f1c364c66f..caccea87a7 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/maps/data-layer/circles-data-layer.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/maps/data-layer/circles-data-layer.ts @@ -91,6 +91,9 @@ class TbCircleDataLayerItem extends TbDataLayerItem { this.editing = true; diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/maps/data-layer/map-data-layer.ts b/ui-ngx/src/app/modules/home/components/widget/lib/maps/data-layer/map-data-layer.ts index 269d8fc294..f08fd0bca8 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/maps/data-layer/map-data-layer.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/maps/data-layer/map-data-layer.ts @@ -314,6 +314,7 @@ export abstract class TbMapDataLayer { - this.moving = true; - }); - this.marker.on('dragend', () => { - this.saveMarkerLocation(); - this.moving = false; - }); + if (this.settings.markerClustering?.enable) { + this.marker.options.draggable = true; + this.marker.on('dragstart', () => { + this.moving = true; + }); + this.marker.on('dragend', () => { + this.saveMarkerLocation(); + this.moving = false; + }); + } else { + this.marker.pm.setOptions({ + snappable: this.dataLayer.isSnappable() + }); + this.marker.pm.enableLayerDrag(); + this.marker.on('pm:dragstart', () => { + this.moving = true; + }); + this.marker.on('pm:dragend', () => { + this.saveMarkerLocation(); + this.moving = false; + }); + } } protected disableDrag(): void { - this.marker.options.draggable = false; - this.marker.off('dragstart'); - this.marker.off('dragend'); + if (this.settings.markerClustering?.enable) { + this.marker.options.draggable = false; + this.marker.off('dragstart'); + this.marker.off('dragend'); + } else { + this.marker.pm.disableLayerDrag(); + this.marker.off('pm:dragstart'); + this.marker.off('pm:dragend'); + } } private saveMarkerLocation() { diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/maps/data-layer/polygons-data-layer.ts b/ui-ngx/src/app/modules/home/components/widget/lib/maps/data-layer/polygons-data-layer.ts index 789795149c..b16952a317 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/maps/data-layer/polygons-data-layer.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/maps/data-layer/polygons-data-layer.ts @@ -96,6 +96,9 @@ class TbPolygonDataLayerItem extends TbDataLayerItem { this.editing = true; From 86b5378d592de789afff096e387bb328389c48a7 Mon Sep 17 00:00:00 2001 From: Viacheslav Klimov Date: Mon, 27 Jan 2025 15:16:50 +0200 Subject: [PATCH 035/345] EDQS (#3196) * Experiments with CSV * CSV Loader v1 * EDQ tests * Volatile variables instead of final * Improvements * updated loader with new entities * Fix double memory usage issue * Basic data structures and load * Minor improvements * Snappy + Large String reuse * added EntityFields classes for each entity * Basic implementation * Minor improvements to KeyFilters * implemented RepositoryUtils.checkKeyFilters * Generic query implementation * New structure * Refactoring and few processors implementation * extended DeviceData with shared/client attributes and device profile * Minor refactoring of attribute scopes * DeviceTypeFilter support * Strong types of fields for each entity data class * DeviceType and AssetType filters * EntityView and Edge queries * Relations Query * Relation Query Implementation * Update EDQS module version * Sync with EDQS via Kafka * EDQS: major refactoring * EDQS API requests via Kafka * EDQS: full sync with the database * Refactoring for EDQS sync * EDQS: major refactoring and new features * EDQS refactoring, count query support, fix tests * EDQS: refactoring for query processors * Fix EDQS pom version * Cleanup edqs.yml * EDQS: tenant partitioning strategy; refactoring * EDQS: latest events queue * EDQS: support for monolith setup; RocksDB; other improvements * EDQS: merge sync and events topics, introduce state topic * EDQS: dynamic repartitioning * implemented entity data query filters for edqs * EdqsEntityQueryControllerTest - use in-memory queue * edqs-filter fixes, added test * EDQS: blob entity support * EdqsEntityQueryControllerTest - use in-memory queue * Use DummyEdqsService when disabled * Fixes for EDQS * Refactoring for EDQS tests * Fix edqs requests partitioning * EDQS: Fix for attributes handling * Fix attributes saving in EntityServiceTest * EDQS: refactoring, fixes * Minor refactoring for query processor * added ownerName/ownerType support * fixed relation query processor * fixed EntityServiceTest * refactoring * added support for parentId for relation query result * Get rid of EntityNameFetcher * Add fixme for relation query processor * db restore with select all edqs fields * fixed entity deletion * fixed FieldUtils with new EntityFields * dao method renamed * EDQS: instance groups with same partitions; automatic sync; multiple fixes * Refactoring for EDQS sync * EDQS: refactoring * Fix startup with Kafka * fixed EntityQueryControllerTest * fixed EdqsEntityServiceTest * Separate queue admin for EDQS request template * Implement new EDQS partitioning strategy * EDQS: multiple fixes and refactoring * Add mock EdqsRocksDb beans to tests * added edqs stats for inmemory/grafana * fixed filter tests * Update todos * Refactoring for QueueConfig * Improvements and refactoring for EDQS consumers * implemented TODOs * test fixes * Consume state topic up to end offsets * edqs stats refactoring * EDQS: cleanup on partitions removal; refactoring * EDQS: minor refactoring * EDQS: remove CSV loader --------- Co-authored-by: Andrii Shvaika Co-authored-by: dashevchenko --- application/pom.xml | 4 + .../controller/EntityQueryController.java | 11 + .../edge/EdgeEventSourcingListener.java | 5 + .../service/edqs/DefaultEdqsService.java | 340 ++++++++++ .../server/service/edqs/EdqsDataLoader.java | 539 ++++++++++++++++ .../server/service/edqs/EdqsListener.java | 61 ++ .../server/service/edqs/EdqsSyncService.java | 275 ++++++++ .../service/edqs/KafkaEdqsSyncService.java | 47 ++ .../service/edqs/LocalEdqsSyncService.java | 35 + .../entitiy/EntityStateSourcingListener.java | 5 + .../queue/DefaultTbCoreConsumerService.java | 27 +- .../queue/DefaultTbEdgeConsumerService.java | 16 +- .../DefaultTbRuleEngineConsumerService.java | 2 +- .../TbRuleEngineQueueConsumerManager.java | 5 +- .../state/DefaultDeviceStateService.java | 31 +- .../sync/tenant/TenantExportService.java | 224 +++++++ .../transport/TbCoreTransportApiService.java | 3 +- .../service/ws/DefaultWebSocketService.java | 1 + .../src/main/resources/thingsboard.yml | 23 + .../EdqsEntityQueryControllerTest.java | 67 ++ .../controller/EntityQueryControllerTest.java | 167 +++-- .../discovery/HashPartitionServiceTest.java | 16 +- .../entitiy/EdqsEntityServiceTest.java | 72 +++ .../service/entitiy}/EntityServiceTest.java | 609 ++++++++++-------- .../TbRuleEngineQueueConsumerManagerTest.java | 11 +- .../state/DefaultDeviceStateServiceTest.java | 2 +- .../resources/application-test.properties | 5 +- .../src/test/resources/logback-test.xml | 2 +- .../server/queue/TbQueueRequestTemplate.java | 2 + .../server/queue/TbQueueResponseTemplate.java | 10 +- .../server/common/data/ObjectType.java | 102 +++ .../server/common/data/alarm/AlarmType.java | 27 + .../server/common/data/edqs/AttributeKv.java | 67 ++ .../server/common/data/edqs/EdqsEvent.java | 34 + .../common/data/edqs/EdqsEventType.java | 21 + .../server/common/data/edqs/EdqsObject.java | 28 + .../common/data/edqs/EdqsSyncRequest.java | 24 + .../server/common/data/edqs/Entity.java | 62 ++ .../server/common/data/edqs/LatestTsKv.java | 62 ++ .../common/data/edqs/ToCoreEdqsMsg.java | 32 + .../common/data/edqs/ToCoreEdqsRequest.java | 38 ++ .../edqs/fields/AbstractEntityFields.java | 65 ++ .../data/edqs/fields/ApiUsageStateFields.java | 58 ++ .../common/data/edqs/fields/AssetFields.java | 58 ++ .../data/edqs/fields/AssetProfileFields.java | 35 + .../data/edqs/fields/CustomerFields.java | 55 ++ .../data/edqs/fields/DashboardFields.java | 33 + .../common/data/edqs/fields/DeviceFields.java | 58 ++ .../data/edqs/fields/DeviceProfileFields.java | 38 ++ .../common/data/edqs/fields/EdgeFields.java | 43 ++ .../common/data/edqs/fields/EntityFields.java | 171 +++++ .../data/edqs/fields/EntityIdFields.java | 36 ++ .../data/edqs/fields/EntityViewFields.java | 30 + .../common/data/edqs/fields/FieldsUtil.java | 298 +++++++++ .../data/edqs/fields/GenericFields.java | 41 ++ .../data/edqs/fields/ProfileAwareFields.java | 26 + .../data/edqs/fields/QueueStatsFields.java | 42 ++ .../data/edqs/fields/RuleChainFields.java | 38 ++ .../data/edqs/fields/RuleNodeFields.java | 43 ++ .../common/data/edqs/fields/TenantFields.java | 62 ++ .../data/edqs/fields/TenantProfileFields.java | 36 ++ .../common/data/edqs/fields/UserFields.java | 48 ++ .../data/edqs/fields/WidgetTypeFields.java | 30 + .../data/edqs/fields/WidgetsBundleFields.java | 30 + .../common/data/edqs/query/EdqsRequest.java | 38 ++ .../common/data/edqs/query/EdqsResponse.java | 35 + .../common/data/edqs/query/QueryResult.java | 41 ++ .../common/data/id/UserAuthSettingsId.java | 6 +- .../data/page/BasePageDataIterable.java | 9 +- .../common/data/page/PageDataIterable.java | 5 + .../common/data/permission/QueryContext.java | 14 +- .../common/data/query/DynamicValue.java | 2 - .../common/data/query/EntityCountQuery.java | 2 + .../server/common/data/query/EntityData.java | 15 +- .../server/common/data/queue/QueueConfig.java | 12 + .../server/common/data/queue/QueueStats.java | 8 +- .../common/data/relation/EntityRelation.java | 15 +- .../common/data/util/CollectionsUtil.java | 8 + common/edqs/pom.xml | 97 +++ .../server/edqs/data/ApiUsageStateData.java | 46 ++ .../server/edqs/data/AssetData.java | 36 ++ .../server/edqs/data/BaseEntityData.java | 180 ++++++ .../server/edqs/data/CustomerData.java | 62 ++ .../server/edqs/data/DeviceData.java | 86 +++ .../server/edqs/data/EntityData.java | 65 ++ .../server/edqs/data/EntityGroupData.java | 58 ++ .../server/edqs/data/EntityProfileData.java | 39 ++ .../server/edqs/data/GenericData.java | 38 ++ .../server/edqs/data/ProfileAwareData.java | 28 + .../server/edqs/data/RelationData.java | 26 + .../server/edqs/data/RelationInfo.java | 26 + .../server/edqs/data/RelationsRepo.java | 62 ++ .../server/edqs/data/TenantData.java | 34 + .../edqs/data/dp/AbstractDataPoint.java | 56 ++ .../server/edqs/data/dp/BoolDataPoint.java | 46 ++ .../edqs/data/dp/CompressedJsonDataPoint.java | 31 + .../data/dp/CompressedStringDataPoint.java | 64 ++ .../server/edqs/data/dp/DataPoint.java | 40 ++ .../server/edqs/data/dp/DoubleDataPoint.java | 46 ++ .../server/edqs/data/dp/JsonDataPoint.java | 47 ++ .../server/edqs/data/dp/LongDataPoint.java | 50 ++ .../server/edqs/data/dp/StringDataPoint.java | 51 ++ .../server/edqs/load/TenantRepoLoader.java | 144 +++++ .../server/edqs/processor/EdqsConverter.java | 183 ++++++ .../server/edqs/processor/EdqsProcessor.java | 266 ++++++++ .../server/edqs/processor/EdqsProducer.java | 83 +++ .../server/edqs/query/DataKey.java | 22 + .../server/edqs/query/EdqsCountQuery.java | 30 + .../server/edqs/query/EdqsDataQuery.java | 59 ++ .../server/edqs/query/EdqsFilter.java | 23 + .../server/edqs/query/EdqsQuery.java | 30 + .../server/edqs/query/SortableEntityData.java | 40 ++ .../AbstractEntityGroupQueryProcessor.java | 75 +++ ...stractEntityProfileNameQueryProcessor.java | 52 ++ .../AbstractEntityProfileQueryProcessor.java | 62 ++ .../AbstractEntitySearchQueryProcessor.java | 66 ++ .../processor/AbstractQueryProcessor.java | 129 ++++ .../AbstractRelationQueryProcessor.java | 260 ++++++++ .../AbstractSimpleQueryProcessor.java | 99 +++ ...bstractSingleEntityTypeQueryProcessor.java | 172 +++++ .../ApiUsageStateQueryProcessor.java | 88 +++ .../processor/AssetSearchQueryProcessor.java | 59 ++ .../processor/AssetTypeQueryProcessor.java | 47 ++ .../query/processor/CombinedPermissions.java | 25 + .../processor/DeviceSearchQueryProcessor.java | 59 ++ .../processor/DeviceTypeQueryProcessor.java | 47 ++ .../processor/EdgeTypeQueryProcessor.java | 42 ++ .../EdgeTypeSearchQueryProcessor.java | 44 ++ .../EntitiesByGroupNameQueryProcessor.java | 121 ++++ .../EntitiesByGroupQueryProcessor.java | 96 +++ .../EntityGroupListQueryProcessor.java | 84 +++ .../EntityGroupNameQueryProcessor.java | 83 +++ .../processor/EntityListQueryProcessor.java | 94 +++ .../processor/EntityNameQueryProcessor.java | 41 ++ .../query/processor/EntityQueryProcessor.java | 28 + .../EntityQueryProcessorFactory.java | 50 ++ .../processor/EntityTypeQueryProcessor.java | 35 + .../EntityViewSearchQueryProcessor.java | 44 ++ .../EntityViewTypeQueryProcessor.java | 42 ++ .../query/processor/GroupPermissions.java | 29 + .../edqs/query/processor/Permissions.java | 24 + .../processor/RelationQueryPermissions.java | 33 + .../processor/RelationQueryProcessor.java | 84 +++ .../SchedulerEventQueryProcessor.java | 37 ++ .../processor/SingleEntityQueryProcessor.java | 87 +++ .../StateEntityOwnerQueryProcessor.java | 91 +++ .../server/edqs/repo/EdqRepository.java | 44 ++ .../edqs/repo/InMemoryEdqRepository.java | 91 +++ .../server/edqs/repo/KeyDictionary.java | 40 ++ .../server/edqs/repo/TbBytePool.java | 39 ++ .../server/edqs/repo/TbStringPool.java | 37 ++ .../server/edqs/repo/TenantRepo.java | 584 +++++++++++++++++ .../server/edqs/state/EdqsStateService.java | 32 + .../edqs/state/KafkaEdqsStateService.java | 187 ++++++ .../edqs/state/LocalEdqsStateService.java | 81 +++ .../server/edqs/stats/EdqsStatsService.java | 91 +++ .../edqs/util/EdqsPartitionService.java | 40 ++ .../server/edqs/util/EdqsRocksDb.java | 51 ++ .../server/edqs/util/RepositoryUtils.java | 424 ++++++++++++ .../server/edqs/util/TbRocksDb.java | 68 ++ .../server/edqs/util/VersionsStore.java | 48 ++ .../server/common/msg/edqs/EdqsService.java | 47 ++ .../server/common/msg/queue/ServiceType.java | 3 +- .../common/msg/queue/TopicPartitionInfo.java | 22 +- common/pom.xml | 1 + common/proto/src/main/proto/queue.proto | 57 ++ common/queue/pom.xml | 4 + .../AbstractTbQueueConsumerTemplate.java | 12 +- .../common/DefaultTbQueueRequestTemplate.java | 20 +- .../DefaultTbQueueResponseTemplate.java | 14 +- .../consumer/MainQueueConsumerManager.java | 18 +- .../queue/common/consumer}/QueueEvent.java | 2 +- .../consumer}/TbQueueConsumerManagerTask.java | 2 +- .../common/consumer}/TbQueueConsumerTask.java | 12 +- .../DefaultTbServiceInfoProvider.java | 10 + .../queue/discovery/HashPartitionService.java | 76 ++- .../queue/discovery/PartitionService.java | 6 +- .../queue/discovery/ZkDiscoveryService.java | 3 + .../discovery/event/PartitionChangeEvent.java | 19 +- .../server/queue/edqs/EdqsComponent.java | 29 + .../server/queue/edqs/EdqsConfig.java | 55 ++ .../server/queue/edqs/EdqsQueue.java | 36 ++ .../server/queue/edqs/EdqsQueueFactory.java | 35 + .../queue/edqs/InMemoryEdqsComponent.java | 26 + .../queue/edqs/InMemoryEdqsQueueFactory.java | 77 +++ .../server/queue/edqs/KafkaEdqsComponent.java | 28 + .../queue/edqs/KafkaEdqsQueueFactory.java | 122 ++++ .../queue/environment/DistributedLock.java | 24 + .../environment/DistributedLockService.java | 22 + .../DummyDistributedLockService.java | 53 ++ .../environment/ZkDistributedLockService.java | 62 ++ .../server/queue/kafka/KafkaTbQueueMsg.java | 8 +- .../server/queue/kafka/TbKafkaAdmin.java | 8 +- .../queue/kafka/TbKafkaConsumerTemplate.java | 89 ++- .../queue/kafka/TbKafkaProducerTemplate.java | 22 +- .../queue/kafka/TbKafkaTopicConfigs.java | 15 + .../provider/EdqsClientQueueFactory.java | 31 + .../InMemoryMonolithQueueFactory.java | 49 +- .../provider/KafkaMonolithQueueFactory.java | 50 +- .../provider/KafkaTbCoreQueueFactory.java | 48 ++ .../KafkaTbRuleEngineQueueFactory.java | 19 + .../queue/provider/TbCoreQueueFactory.java | 2 +- .../provider/TbQueueProducerProvider.java | 2 +- .../provider/TbRuleEngineQueueFactory.java | 2 +- .../server/queue/util/PropertyUtils.java | 3 +- .../DefaultTbQueueRequestTemplateTest.java | 8 +- .../common/stats/DummyMessagesStats.java | 54 ++ .../server/common/stats/StatsType.java | 3 +- .../thingsboard/common/util/JacksonUtil.java | 4 + .../DefaultClusterVersionControlService.java | 2 +- .../java/org/thingsboard/server/dao/Dao.java | 12 +- .../org/thingsboard/server/dao/DaoUtil.java | 5 + .../server/dao/TenantEntityDao.java | 18 +- .../server/dao/asset/AssetDao.java | 3 +- .../dao/asset/AssetProfileServiceImpl.java | 7 +- .../server/dao/asset/BaseAssetService.java | 2 +- .../server/dao/attributes/AttributesDao.java | 5 + .../dao/attributes/BaseAttributesService.java | 45 +- .../attributes/CachedAttributesService.java | 20 +- .../server/dao/customer/CustomerDao.java | 2 +- .../server/dao/dashboard/DashboardDao.java | 2 +- .../dao/dashboard/DashboardServiceImpl.java | 2 +- .../server/dao/device/DeviceDao.java | 2 +- .../dao/device/DeviceProfileServiceImpl.java | 7 +- .../dao/dictionary/KeyDictionaryDao.java | 5 + .../thingsboard/server/dao/edge/EdgeDao.java | 2 +- .../server/dao/entity/BaseEntityService.java | 73 +++ .../server/dao/entity/EntityDaoRegistry.java | 31 +- .../dao/entityview/EntityViewServiceImpl.java | 2 +- .../dao/eventsourcing/SaveEntityEvent.java | 1 + .../server/dao/model/ModelConstants.java | 2 + .../dao/model/sql/AlarmTypeCompositeKey.java | 33 + .../server/dao/model/sql/AlarmTypeEntity.java | 65 ++ .../model/sqlts/latest/TsKvLatestEntity.java | 9 + .../notification/NotificationTargetDao.java | 2 +- .../server/dao/ota/OtaPackageDao.java | 2 + .../dao/queue/BaseQueueStatsService.java | 8 +- .../server/dao/relation/RelationDao.java | 4 + .../server/dao/rule/BaseRuleChainService.java | 12 +- .../server/dao/rule/RuleChainDao.java | 2 +- .../dao/sql/alarm/AlarmCommentRepository.java | 6 +- .../server/dao/sql/alarm/AlarmRepository.java | 2 + .../dao/sql/alarm/AlarmTypeRepository.java | 30 + .../dao/sql/alarm/EntityAlarmRepository.java | 4 + .../dao/sql/alarm/JpaAlarmCommentDao.java | 15 +- .../server/dao/sql/alarm/JpaAlarmDao.java | 14 +- .../server/dao/sql/alarm/JpaAlarmTypeDao.java | 46 ++ .../dao/sql/alarm/JpaEntityAlarmDao.java | 46 ++ .../dao/sql/asset/AssetProfileRepository.java | 5 + .../server/dao/sql/asset/AssetRepository.java | 6 + .../server/dao/sql/asset/JpaAssetDao.java | 17 + .../dao/sql/asset/JpaAssetProfileDao.java | 20 +- .../dao/sql/attributes/JpaAttributeDao.java | 10 + .../dao/sql/customer/CustomerRepository.java | 6 + .../dao/sql/customer/JpaCustomerDao.java | 17 + .../sql/dashboard/DashboardRepository.java | 5 + .../dao/sql/dashboard/JpaDashboardDao.java | 17 + .../device/DeviceCredentialsRepository.java | 6 + .../sql/device/DeviceProfileRepository.java | 6 + .../dao/sql/device/DeviceRepository.java | 6 + .../sql/device/JpaDeviceCredentialsDao.java | 16 +- .../server/dao/sql/device/JpaDeviceDao.java | 17 + .../dao/sql/device/JpaDeviceProfileDao.java | 20 +- .../server/dao/sql/edge/EdgeRepository.java | 5 + .../server/dao/sql/edge/JpaEdgeDao.java | 17 + .../sql/entityview/EntityViewRepository.java | 5 + .../dao/sql/entityview/JpaEntityViewDao.java | 22 +- .../notification/JpaNotificationRuleDao.java | 14 +- .../JpaNotificationTargetDao.java | 11 + .../JpaNotificationTemplateDao.java | 14 +- .../server/dao/sql/ota/JpaOtaPackageDao.java | 19 +- .../dao/sql/ota/JpaOtaPackageInfoDao.java | 1 + .../dao/sql/ota/OtaPackageRepository.java | 6 + .../query/DefaultAlarmQueryRepository.java | 12 +- .../query/DefaultEntityQueryRepository.java | 35 +- .../sql/query/DefaultQueryLogComponent.java | 2 +- .../dao/sql/query/DummyEdqsService.java | 64 ++ .../dao/sql/query/EntityKeyMapping.java | 30 +- .../dao/sql/query/QueryLogComponent.java | 2 +- ...QueryContext.java => SqlQueryContext.java} | 9 +- .../server/dao/sql/queue/JpaQueueDao.java | 14 +- .../dao/sql/queue/JpaQueueStatsDao.java | 6 + .../dao/sql/queue/QueueStatsRepository.java | 5 + .../dao/sql/relation/JpaRelationDao.java | 7 + .../dao/sql/relation/RelationRepository.java | 3 + .../dao/sql/resource/JpaTbResourceDao.java | 9 +- .../server/dao/sql/rpc/JpaRpcDao.java | 14 +- .../server/dao/sql/rule/JpaRuleChainDao.java | 17 + .../server/dao/sql/rule/JpaRuleNodeDao.java | 14 +- .../dao/sql/rule/RuleChainRepository.java | 4 + .../dao/sql/rule/RuleNodeRepository.java | 3 + .../sql/settings/AdminSettingsRepository.java | 4 + .../dao/sql/settings/JpaAdminSettingsDao.java | 17 +- .../server/dao/sql/tenant/JpaTenantDao.java | 12 + .../dao/sql/tenant/JpaTenantProfileDao.java | 6 + .../sql/tenant/TenantProfileRepository.java | 5 + .../dao/sql/tenant/TenantRepository.java | 5 + .../usagerecord/ApiUsageStateRepository.java | 10 + .../sql/usagerecord/JpaApiUsageStateDao.java | 19 + .../dao/sql/user/JpaUserAuthSettingsDao.java | 25 +- .../dao/sql/user/JpaUserCredentialsDao.java | 16 +- .../server/dao/sql/user/JpaUserDao.java | 17 + .../dao/sql/user/JpaUserSettingsDao.java | 17 +- .../sql/user/UserAuthSettingsRepository.java | 5 + .../sql/user/UserCredentialsRepository.java | 6 + .../server/dao/sql/user/UserRepository.java | 6 + .../dao/sql/user/UserSettingsRepository.java | 5 + .../dao/sql/widget/JpaWidgetTypeDao.java | 20 +- .../dao/sql/widget/JpaWidgetsBundleDao.java | 22 +- .../dao/sql/widget/WidgetTypeRepository.java | 5 + .../sql/widget/WidgetsBundleRepository.java | 5 + .../widget/WidgetsBundleWidgetRepository.java | 7 + .../dao/sqlts/AbstractSqlTimeseriesDao.java | 2 +- .../CachedRedisSqlTimeseriesLatestDao.java | 9 + .../dao/sqlts/SqlTimeseriesLatestDao.java | 8 + .../sqlts/dictionary/JpaKeyDictionaryDao.java | 8 + .../dictionary/KeyDictionaryRepository.java | 5 + .../sqlts/latest/TsKvLatestRepository.java | 5 + .../server/dao/tenant/TenantDao.java | 5 +- .../dao/timeseries/BaseTimeseriesService.java | 33 +- .../CassandraBaseTimeseriesLatestDao.java | 9 + .../dao/timeseries/TimeseriesLatestDao.java | 6 + .../dao/usagerecord/ApiUsageStateDao.java | 4 +- .../usagerecord/ApiUsageStateServiceImpl.java | 8 + .../thingsboard/server/dao/user/UserDao.java | 2 +- .../dao/service/AbstractServiceTest.java | 3 +- .../dao/service/EntityDaoRegistryTest.java | 31 + .../query/DefaultQueryLogComponentTest.java | 6 +- edqs/pom.xml | 260 ++++++++ .../edqs/DummyQueueRoutingInfoService.java | 33 + .../edqs/DummyTenantRoutingInfoService.java | 30 + .../edqs/ThingsboardEdqsApplication.java | 119 ++++ edqs/src/main/resources/edqs.yml | 364 +++++++++++ edqs/src/main/resources/logback.xml | 38 ++ .../server/edqs/repo/AbstractEDQTest.java | 296 +++++++++ .../edqs/repo/ApiUsageStateFilterTest.java | 106 +++ .../edqs/repo/AssetSearchQueryFilterTest.java | 223 +++++++ .../server/edqs/repo/AssetTypeFilterTest.java | 187 ++++++ .../repo/DeviceSearchQueryFilterTest.java | 241 +++++++ .../edqs/repo/DeviceTypeFilterTest.java | 142 ++++ .../edqs/repo/EdgeSearchQueryFilterTest.java | 167 +++++ .../server/edqs/repo/EdgeTypeFilterTest.java | 177 +++++ .../repo/EntitiesByGroupIdFilterTest.java | 161 +++++ .../repo/EntitiesByGroupNameFilterTest.java | 140 ++++ .../edqs/repo/EntityGroupListFilterTest.java | 189 ++++++ .../edqs/repo/EntityGroupNameFilterTest.java | 186 ++++++ .../edqs/repo/EntityListFilterTest.java | 202 ++++++ .../edqs/repo/EntityNameFilterTest.java | 132 ++++ .../edqs/repo/EntityTypeFilterTest.java | 147 +++++ .../repo/EntityViewSearchQueryFilterTest.java | 221 +++++++ .../edqs/repo/EntityViewTypeFilterTest.java | 177 +++++ .../edqs/repo/RelationsQueryFilterTest.java | 233 +++++++ .../server/edqs/repo/RepositoryUtilsTest.java | 434 +++++++++++++ .../edqs/repo/SchedulerEventFilterTest.java | 129 ++++ .../edqs/repo/SingleEntityFilterTest.java | 176 +++++ .../edqs/repo/StateEntityOwnerFilterTest.java | 81 +++ edqs/src/test/resources/edq-test.properties | 2 + pom.xml | 18 + 358 files changed, 18294 insertions(+), 675 deletions(-) create mode 100644 application/src/main/java/org/thingsboard/server/service/edqs/DefaultEdqsService.java create mode 100644 application/src/main/java/org/thingsboard/server/service/edqs/EdqsDataLoader.java create mode 100644 application/src/main/java/org/thingsboard/server/service/edqs/EdqsListener.java create mode 100644 application/src/main/java/org/thingsboard/server/service/edqs/EdqsSyncService.java create mode 100644 application/src/main/java/org/thingsboard/server/service/edqs/KafkaEdqsSyncService.java create mode 100644 application/src/main/java/org/thingsboard/server/service/edqs/LocalEdqsSyncService.java create mode 100644 application/src/main/java/org/thingsboard/server/service/sync/tenant/TenantExportService.java create mode 100644 application/src/test/java/org/thingsboard/server/controller/EdqsEntityQueryControllerTest.java create mode 100644 application/src/test/java/org/thingsboard/server/service/entitiy/EdqsEntityServiceTest.java rename {dao/src/test/java/org/thingsboard/server/dao/service => application/src/test/java/org/thingsboard/server/service/entitiy}/EntityServiceTest.java (87%) create mode 100644 common/data/src/main/java/org/thingsboard/server/common/data/ObjectType.java create mode 100644 common/data/src/main/java/org/thingsboard/server/common/data/alarm/AlarmType.java create mode 100644 common/data/src/main/java/org/thingsboard/server/common/data/edqs/AttributeKv.java create mode 100644 common/data/src/main/java/org/thingsboard/server/common/data/edqs/EdqsEvent.java create mode 100644 common/data/src/main/java/org/thingsboard/server/common/data/edqs/EdqsEventType.java create mode 100644 common/data/src/main/java/org/thingsboard/server/common/data/edqs/EdqsObject.java create mode 100644 common/data/src/main/java/org/thingsboard/server/common/data/edqs/EdqsSyncRequest.java create mode 100644 common/data/src/main/java/org/thingsboard/server/common/data/edqs/Entity.java create mode 100644 common/data/src/main/java/org/thingsboard/server/common/data/edqs/LatestTsKv.java create mode 100644 common/data/src/main/java/org/thingsboard/server/common/data/edqs/ToCoreEdqsMsg.java create mode 100644 common/data/src/main/java/org/thingsboard/server/common/data/edqs/ToCoreEdqsRequest.java create mode 100644 common/data/src/main/java/org/thingsboard/server/common/data/edqs/fields/AbstractEntityFields.java create mode 100644 common/data/src/main/java/org/thingsboard/server/common/data/edqs/fields/ApiUsageStateFields.java create mode 100644 common/data/src/main/java/org/thingsboard/server/common/data/edqs/fields/AssetFields.java create mode 100644 common/data/src/main/java/org/thingsboard/server/common/data/edqs/fields/AssetProfileFields.java create mode 100644 common/data/src/main/java/org/thingsboard/server/common/data/edqs/fields/CustomerFields.java create mode 100644 common/data/src/main/java/org/thingsboard/server/common/data/edqs/fields/DashboardFields.java create mode 100644 common/data/src/main/java/org/thingsboard/server/common/data/edqs/fields/DeviceFields.java create mode 100644 common/data/src/main/java/org/thingsboard/server/common/data/edqs/fields/DeviceProfileFields.java create mode 100644 common/data/src/main/java/org/thingsboard/server/common/data/edqs/fields/EdgeFields.java create mode 100644 common/data/src/main/java/org/thingsboard/server/common/data/edqs/fields/EntityFields.java create mode 100644 common/data/src/main/java/org/thingsboard/server/common/data/edqs/fields/EntityIdFields.java create mode 100644 common/data/src/main/java/org/thingsboard/server/common/data/edqs/fields/EntityViewFields.java create mode 100644 common/data/src/main/java/org/thingsboard/server/common/data/edqs/fields/FieldsUtil.java create mode 100644 common/data/src/main/java/org/thingsboard/server/common/data/edqs/fields/GenericFields.java create mode 100644 common/data/src/main/java/org/thingsboard/server/common/data/edqs/fields/ProfileAwareFields.java create mode 100644 common/data/src/main/java/org/thingsboard/server/common/data/edqs/fields/QueueStatsFields.java create mode 100644 common/data/src/main/java/org/thingsboard/server/common/data/edqs/fields/RuleChainFields.java create mode 100644 common/data/src/main/java/org/thingsboard/server/common/data/edqs/fields/RuleNodeFields.java create mode 100644 common/data/src/main/java/org/thingsboard/server/common/data/edqs/fields/TenantFields.java create mode 100644 common/data/src/main/java/org/thingsboard/server/common/data/edqs/fields/TenantProfileFields.java create mode 100644 common/data/src/main/java/org/thingsboard/server/common/data/edqs/fields/UserFields.java create mode 100644 common/data/src/main/java/org/thingsboard/server/common/data/edqs/fields/WidgetTypeFields.java create mode 100644 common/data/src/main/java/org/thingsboard/server/common/data/edqs/fields/WidgetsBundleFields.java create mode 100644 common/data/src/main/java/org/thingsboard/server/common/data/edqs/query/EdqsRequest.java create mode 100644 common/data/src/main/java/org/thingsboard/server/common/data/edqs/query/EdqsResponse.java create mode 100644 common/data/src/main/java/org/thingsboard/server/common/data/edqs/query/QueryResult.java rename dao/src/main/java/org/thingsboard/server/dao/sql/query/QuerySecurityContext.java => common/data/src/main/java/org/thingsboard/server/common/data/permission/QueryContext.java (77%) create mode 100644 common/edqs/pom.xml create mode 100644 common/edqs/src/main/java/org/thingsboard/server/edqs/data/ApiUsageStateData.java create mode 100644 common/edqs/src/main/java/org/thingsboard/server/edqs/data/AssetData.java create mode 100644 common/edqs/src/main/java/org/thingsboard/server/edqs/data/BaseEntityData.java create mode 100644 common/edqs/src/main/java/org/thingsboard/server/edqs/data/CustomerData.java create mode 100644 common/edqs/src/main/java/org/thingsboard/server/edqs/data/DeviceData.java create mode 100644 common/edqs/src/main/java/org/thingsboard/server/edqs/data/EntityData.java create mode 100644 common/edqs/src/main/java/org/thingsboard/server/edqs/data/EntityGroupData.java create mode 100644 common/edqs/src/main/java/org/thingsboard/server/edqs/data/EntityProfileData.java create mode 100644 common/edqs/src/main/java/org/thingsboard/server/edqs/data/GenericData.java create mode 100644 common/edqs/src/main/java/org/thingsboard/server/edqs/data/ProfileAwareData.java create mode 100644 common/edqs/src/main/java/org/thingsboard/server/edqs/data/RelationData.java create mode 100644 common/edqs/src/main/java/org/thingsboard/server/edqs/data/RelationInfo.java create mode 100644 common/edqs/src/main/java/org/thingsboard/server/edqs/data/RelationsRepo.java create mode 100644 common/edqs/src/main/java/org/thingsboard/server/edqs/data/TenantData.java create mode 100644 common/edqs/src/main/java/org/thingsboard/server/edqs/data/dp/AbstractDataPoint.java create mode 100644 common/edqs/src/main/java/org/thingsboard/server/edqs/data/dp/BoolDataPoint.java create mode 100644 common/edqs/src/main/java/org/thingsboard/server/edqs/data/dp/CompressedJsonDataPoint.java create mode 100644 common/edqs/src/main/java/org/thingsboard/server/edqs/data/dp/CompressedStringDataPoint.java create mode 100644 common/edqs/src/main/java/org/thingsboard/server/edqs/data/dp/DataPoint.java create mode 100644 common/edqs/src/main/java/org/thingsboard/server/edqs/data/dp/DoubleDataPoint.java create mode 100644 common/edqs/src/main/java/org/thingsboard/server/edqs/data/dp/JsonDataPoint.java create mode 100644 common/edqs/src/main/java/org/thingsboard/server/edqs/data/dp/LongDataPoint.java create mode 100644 common/edqs/src/main/java/org/thingsboard/server/edqs/data/dp/StringDataPoint.java create mode 100644 common/edqs/src/main/java/org/thingsboard/server/edqs/load/TenantRepoLoader.java create mode 100644 common/edqs/src/main/java/org/thingsboard/server/edqs/processor/EdqsConverter.java create mode 100644 common/edqs/src/main/java/org/thingsboard/server/edqs/processor/EdqsProcessor.java create mode 100644 common/edqs/src/main/java/org/thingsboard/server/edqs/processor/EdqsProducer.java create mode 100644 common/edqs/src/main/java/org/thingsboard/server/edqs/query/DataKey.java create mode 100644 common/edqs/src/main/java/org/thingsboard/server/edqs/query/EdqsCountQuery.java create mode 100644 common/edqs/src/main/java/org/thingsboard/server/edqs/query/EdqsDataQuery.java create mode 100644 common/edqs/src/main/java/org/thingsboard/server/edqs/query/EdqsFilter.java create mode 100644 common/edqs/src/main/java/org/thingsboard/server/edqs/query/EdqsQuery.java create mode 100644 common/edqs/src/main/java/org/thingsboard/server/edqs/query/SortableEntityData.java create mode 100644 common/edqs/src/main/java/org/thingsboard/server/edqs/query/processor/AbstractEntityGroupQueryProcessor.java create mode 100644 common/edqs/src/main/java/org/thingsboard/server/edqs/query/processor/AbstractEntityProfileNameQueryProcessor.java create mode 100644 common/edqs/src/main/java/org/thingsboard/server/edqs/query/processor/AbstractEntityProfileQueryProcessor.java create mode 100644 common/edqs/src/main/java/org/thingsboard/server/edqs/query/processor/AbstractEntitySearchQueryProcessor.java create mode 100644 common/edqs/src/main/java/org/thingsboard/server/edqs/query/processor/AbstractQueryProcessor.java create mode 100644 common/edqs/src/main/java/org/thingsboard/server/edqs/query/processor/AbstractRelationQueryProcessor.java create mode 100644 common/edqs/src/main/java/org/thingsboard/server/edqs/query/processor/AbstractSimpleQueryProcessor.java create mode 100644 common/edqs/src/main/java/org/thingsboard/server/edqs/query/processor/AbstractSingleEntityTypeQueryProcessor.java create mode 100644 common/edqs/src/main/java/org/thingsboard/server/edqs/query/processor/ApiUsageStateQueryProcessor.java create mode 100644 common/edqs/src/main/java/org/thingsboard/server/edqs/query/processor/AssetSearchQueryProcessor.java create mode 100644 common/edqs/src/main/java/org/thingsboard/server/edqs/query/processor/AssetTypeQueryProcessor.java create mode 100644 common/edqs/src/main/java/org/thingsboard/server/edqs/query/processor/CombinedPermissions.java create mode 100644 common/edqs/src/main/java/org/thingsboard/server/edqs/query/processor/DeviceSearchQueryProcessor.java create mode 100644 common/edqs/src/main/java/org/thingsboard/server/edqs/query/processor/DeviceTypeQueryProcessor.java create mode 100644 common/edqs/src/main/java/org/thingsboard/server/edqs/query/processor/EdgeTypeQueryProcessor.java create mode 100644 common/edqs/src/main/java/org/thingsboard/server/edqs/query/processor/EdgeTypeSearchQueryProcessor.java create mode 100644 common/edqs/src/main/java/org/thingsboard/server/edqs/query/processor/EntitiesByGroupNameQueryProcessor.java create mode 100644 common/edqs/src/main/java/org/thingsboard/server/edqs/query/processor/EntitiesByGroupQueryProcessor.java create mode 100644 common/edqs/src/main/java/org/thingsboard/server/edqs/query/processor/EntityGroupListQueryProcessor.java create mode 100644 common/edqs/src/main/java/org/thingsboard/server/edqs/query/processor/EntityGroupNameQueryProcessor.java create mode 100644 common/edqs/src/main/java/org/thingsboard/server/edqs/query/processor/EntityListQueryProcessor.java create mode 100644 common/edqs/src/main/java/org/thingsboard/server/edqs/query/processor/EntityNameQueryProcessor.java create mode 100644 common/edqs/src/main/java/org/thingsboard/server/edqs/query/processor/EntityQueryProcessor.java create mode 100644 common/edqs/src/main/java/org/thingsboard/server/edqs/query/processor/EntityQueryProcessorFactory.java create mode 100644 common/edqs/src/main/java/org/thingsboard/server/edqs/query/processor/EntityTypeQueryProcessor.java create mode 100644 common/edqs/src/main/java/org/thingsboard/server/edqs/query/processor/EntityViewSearchQueryProcessor.java create mode 100644 common/edqs/src/main/java/org/thingsboard/server/edqs/query/processor/EntityViewTypeQueryProcessor.java create mode 100644 common/edqs/src/main/java/org/thingsboard/server/edqs/query/processor/GroupPermissions.java create mode 100644 common/edqs/src/main/java/org/thingsboard/server/edqs/query/processor/Permissions.java create mode 100644 common/edqs/src/main/java/org/thingsboard/server/edqs/query/processor/RelationQueryPermissions.java create mode 100644 common/edqs/src/main/java/org/thingsboard/server/edqs/query/processor/RelationQueryProcessor.java create mode 100644 common/edqs/src/main/java/org/thingsboard/server/edqs/query/processor/SchedulerEventQueryProcessor.java create mode 100644 common/edqs/src/main/java/org/thingsboard/server/edqs/query/processor/SingleEntityQueryProcessor.java create mode 100644 common/edqs/src/main/java/org/thingsboard/server/edqs/query/processor/StateEntityOwnerQueryProcessor.java create mode 100644 common/edqs/src/main/java/org/thingsboard/server/edqs/repo/EdqRepository.java create mode 100644 common/edqs/src/main/java/org/thingsboard/server/edqs/repo/InMemoryEdqRepository.java create mode 100644 common/edqs/src/main/java/org/thingsboard/server/edqs/repo/KeyDictionary.java create mode 100644 common/edqs/src/main/java/org/thingsboard/server/edqs/repo/TbBytePool.java create mode 100644 common/edqs/src/main/java/org/thingsboard/server/edqs/repo/TbStringPool.java create mode 100644 common/edqs/src/main/java/org/thingsboard/server/edqs/repo/TenantRepo.java create mode 100644 common/edqs/src/main/java/org/thingsboard/server/edqs/state/EdqsStateService.java create mode 100644 common/edqs/src/main/java/org/thingsboard/server/edqs/state/KafkaEdqsStateService.java create mode 100644 common/edqs/src/main/java/org/thingsboard/server/edqs/state/LocalEdqsStateService.java create mode 100644 common/edqs/src/main/java/org/thingsboard/server/edqs/stats/EdqsStatsService.java create mode 100644 common/edqs/src/main/java/org/thingsboard/server/edqs/util/EdqsPartitionService.java create mode 100644 common/edqs/src/main/java/org/thingsboard/server/edqs/util/EdqsRocksDb.java create mode 100644 common/edqs/src/main/java/org/thingsboard/server/edqs/util/RepositoryUtils.java create mode 100644 common/edqs/src/main/java/org/thingsboard/server/edqs/util/TbRocksDb.java create mode 100644 common/edqs/src/main/java/org/thingsboard/server/edqs/util/VersionsStore.java create mode 100644 common/message/src/main/java/org/thingsboard/server/common/msg/edqs/EdqsService.java rename {application/src/main/java/org/thingsboard/server/service/queue => common/queue/src/main/java/org/thingsboard/server/queue/common}/consumer/MainQueueConsumerManager.java (95%) rename {application/src/main/java/org/thingsboard/server/service/queue/ruleengine => common/queue/src/main/java/org/thingsboard/server/queue/common/consumer}/QueueEvent.java (92%) rename {application/src/main/java/org/thingsboard/server/service/queue/ruleengine => common/queue/src/main/java/org/thingsboard/server/queue/common/consumer}/TbQueueConsumerManagerTask.java (96%) rename {application/src/main/java/org/thingsboard/server/service/queue/ruleengine => common/queue/src/main/java/org/thingsboard/server/queue/common/consumer}/TbQueueConsumerTask.java (89%) create mode 100644 common/queue/src/main/java/org/thingsboard/server/queue/edqs/EdqsComponent.java create mode 100644 common/queue/src/main/java/org/thingsboard/server/queue/edqs/EdqsConfig.java create mode 100644 common/queue/src/main/java/org/thingsboard/server/queue/edqs/EdqsQueue.java create mode 100644 common/queue/src/main/java/org/thingsboard/server/queue/edqs/EdqsQueueFactory.java create mode 100644 common/queue/src/main/java/org/thingsboard/server/queue/edqs/InMemoryEdqsComponent.java create mode 100644 common/queue/src/main/java/org/thingsboard/server/queue/edqs/InMemoryEdqsQueueFactory.java create mode 100644 common/queue/src/main/java/org/thingsboard/server/queue/edqs/KafkaEdqsComponent.java create mode 100644 common/queue/src/main/java/org/thingsboard/server/queue/edqs/KafkaEdqsQueueFactory.java create mode 100644 common/queue/src/main/java/org/thingsboard/server/queue/environment/DistributedLock.java create mode 100644 common/queue/src/main/java/org/thingsboard/server/queue/environment/DistributedLockService.java create mode 100644 common/queue/src/main/java/org/thingsboard/server/queue/environment/DummyDistributedLockService.java create mode 100644 common/queue/src/main/java/org/thingsboard/server/queue/environment/ZkDistributedLockService.java create mode 100644 common/queue/src/main/java/org/thingsboard/server/queue/provider/EdqsClientQueueFactory.java create mode 100644 common/stats/src/main/java/org/thingsboard/server/common/stats/DummyMessagesStats.java create mode 100644 dao/src/main/java/org/thingsboard/server/dao/model/sql/AlarmTypeCompositeKey.java create mode 100644 dao/src/main/java/org/thingsboard/server/dao/model/sql/AlarmTypeEntity.java create mode 100644 dao/src/main/java/org/thingsboard/server/dao/sql/alarm/AlarmTypeRepository.java create mode 100644 dao/src/main/java/org/thingsboard/server/dao/sql/alarm/JpaAlarmTypeDao.java create mode 100644 dao/src/main/java/org/thingsboard/server/dao/sql/alarm/JpaEntityAlarmDao.java create mode 100644 dao/src/main/java/org/thingsboard/server/dao/sql/query/DummyEdqsService.java rename dao/src/main/java/org/thingsboard/server/dao/sql/query/{QueryContext.java => SqlQueryContext.java} (94%) create mode 100644 edqs/pom.xml create mode 100644 edqs/src/main/java/org/thingsboard/server/edqs/DummyQueueRoutingInfoService.java create mode 100644 edqs/src/main/java/org/thingsboard/server/edqs/DummyTenantRoutingInfoService.java create mode 100644 edqs/src/main/java/org/thingsboard/server/edqs/ThingsboardEdqsApplication.java create mode 100644 edqs/src/main/resources/edqs.yml create mode 100644 edqs/src/main/resources/logback.xml create mode 100644 edqs/src/test/java/org/thingsboard/server/edqs/repo/AbstractEDQTest.java create mode 100644 edqs/src/test/java/org/thingsboard/server/edqs/repo/ApiUsageStateFilterTest.java create mode 100644 edqs/src/test/java/org/thingsboard/server/edqs/repo/AssetSearchQueryFilterTest.java create mode 100644 edqs/src/test/java/org/thingsboard/server/edqs/repo/AssetTypeFilterTest.java create mode 100644 edqs/src/test/java/org/thingsboard/server/edqs/repo/DeviceSearchQueryFilterTest.java create mode 100644 edqs/src/test/java/org/thingsboard/server/edqs/repo/DeviceTypeFilterTest.java create mode 100644 edqs/src/test/java/org/thingsboard/server/edqs/repo/EdgeSearchQueryFilterTest.java create mode 100644 edqs/src/test/java/org/thingsboard/server/edqs/repo/EdgeTypeFilterTest.java create mode 100644 edqs/src/test/java/org/thingsboard/server/edqs/repo/EntitiesByGroupIdFilterTest.java create mode 100644 edqs/src/test/java/org/thingsboard/server/edqs/repo/EntitiesByGroupNameFilterTest.java create mode 100644 edqs/src/test/java/org/thingsboard/server/edqs/repo/EntityGroupListFilterTest.java create mode 100644 edqs/src/test/java/org/thingsboard/server/edqs/repo/EntityGroupNameFilterTest.java create mode 100644 edqs/src/test/java/org/thingsboard/server/edqs/repo/EntityListFilterTest.java create mode 100644 edqs/src/test/java/org/thingsboard/server/edqs/repo/EntityNameFilterTest.java create mode 100644 edqs/src/test/java/org/thingsboard/server/edqs/repo/EntityTypeFilterTest.java create mode 100644 edqs/src/test/java/org/thingsboard/server/edqs/repo/EntityViewSearchQueryFilterTest.java create mode 100644 edqs/src/test/java/org/thingsboard/server/edqs/repo/EntityViewTypeFilterTest.java create mode 100644 edqs/src/test/java/org/thingsboard/server/edqs/repo/RelationsQueryFilterTest.java create mode 100644 edqs/src/test/java/org/thingsboard/server/edqs/repo/RepositoryUtilsTest.java create mode 100644 edqs/src/test/java/org/thingsboard/server/edqs/repo/SchedulerEventFilterTest.java create mode 100644 edqs/src/test/java/org/thingsboard/server/edqs/repo/SingleEntityFilterTest.java create mode 100644 edqs/src/test/java/org/thingsboard/server/edqs/repo/StateEntityOwnerFilterTest.java create mode 100644 edqs/src/test/resources/edq-test.properties diff --git a/application/pom.xml b/application/pom.xml index d97f6d92e3..9bdfc7c754 100644 --- a/application/pom.xml +++ b/application/pom.xml @@ -124,6 +124,10 @@ org.thingsboard.common edge-api + + org.thingsboard.common + edqs + org.thingsboard dao diff --git a/application/src/main/java/org/thingsboard/server/controller/EntityQueryController.java b/application/src/main/java/org/thingsboard/server/controller/EntityQueryController.java index 505244539b..f28d485b30 100644 --- a/application/src/main/java/org/thingsboard/server/controller/EntityQueryController.java +++ b/application/src/main/java/org/thingsboard/server/controller/EntityQueryController.java @@ -20,6 +20,7 @@ import io.swagger.v3.oas.annotations.media.Schema; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.http.ResponseEntity; import org.springframework.security.access.prepost.PreAuthorize; +import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.RequestBody; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RequestMethod; @@ -27,6 +28,7 @@ import org.springframework.web.bind.annotation.RequestParam; import org.springframework.web.bind.annotation.ResponseBody; import org.springframework.web.bind.annotation.RestController; import org.springframework.web.context.request.async.DeferredResult; +import org.thingsboard.server.common.data.edqs.ToCoreEdqsRequest; import org.thingsboard.server.common.data.exception.ThingsboardException; import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.common.data.id.UserId; @@ -38,6 +40,7 @@ import org.thingsboard.server.common.data.query.EntityCountQuery; import org.thingsboard.server.common.data.query.EntityData; import org.thingsboard.server.common.data.query.EntityDataPageLink; import org.thingsboard.server.common.data.query.EntityDataQuery; +import org.thingsboard.server.common.msg.edqs.EdqsService; import org.thingsboard.server.config.annotations.ApiOperation; import org.thingsboard.server.queue.util.TbCoreComponent; import org.thingsboard.server.service.query.EntityQueryService; @@ -55,6 +58,8 @@ public class EntityQueryController extends BaseController { @Autowired private EntityQueryService entityQueryService; + @Autowired + private EdqsService edqsService; private static final int MAX_PAGE_SIZE = 100; @@ -133,4 +138,10 @@ public class EntityQueryController extends BaseController { return entityQueryService.getKeysByQuery(getCurrentUser(), tenantId, query, isTimeseries, isAttributes, scope); } + @PreAuthorize("hasAnyAuthority('SYS_ADMIN')") + @PostMapping("/edqs/system/request") + public void processSystemEdqsRequest(@RequestBody ToCoreEdqsRequest request) { + edqsService.processSystemRequest(request); + } + } diff --git a/application/src/main/java/org/thingsboard/server/service/edge/EdgeEventSourcingListener.java b/application/src/main/java/org/thingsboard/server/service/edge/EdgeEventSourcingListener.java index 9317c51a64..c7e95dbbc8 100644 --- a/application/src/main/java/org/thingsboard/server/service/edge/EdgeEventSourcingListener.java +++ b/application/src/main/java/org/thingsboard/server/service/edge/EdgeEventSourcingListener.java @@ -82,6 +82,11 @@ public class EdgeEventSourcingListener { @TransactionalEventListener(fallbackExecution = true) public void handleEvent(SaveEntityEvent event) { + if (Boolean.FALSE.equals(event.getBroadcastEvent())) { + log.trace("Ignoring event {}", event); + return; + } + try { if (!isValidSaveEntityEventForEdgeProcessing(event)) { return; diff --git a/application/src/main/java/org/thingsboard/server/service/edqs/DefaultEdqsService.java b/application/src/main/java/org/thingsboard/server/service/edqs/DefaultEdqsService.java new file mode 100644 index 0000000000..7582d036e2 --- /dev/null +++ b/application/src/main/java/org/thingsboard/server/service/edqs/DefaultEdqsService.java @@ -0,0 +1,340 @@ +/** + * Copyright © 2016-2024 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.service.edqs; + +import com.google.common.util.concurrent.Futures; +import com.google.common.util.concurrent.ListenableFuture; +import com.google.common.util.concurrent.MoreExecutors; +import com.google.protobuf.ByteString; +import jakarta.annotation.PostConstruct; +import jakarta.annotation.PreDestroy; +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; +import lombok.RequiredArgsConstructor; +import lombok.SneakyThrows; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; +import org.springframework.context.annotation.Lazy; +import org.springframework.stereotype.Service; +import org.thingsboard.common.util.JacksonUtil; +import org.thingsboard.common.util.ThingsBoardExecutors; +import org.thingsboard.server.cluster.TbClusterService; +import org.thingsboard.server.common.data.AttributeScope; +import org.thingsboard.server.common.data.EntityType; +import org.thingsboard.server.common.data.ObjectType; +import org.thingsboard.server.common.data.edqs.EdqsEventType; +import org.thingsboard.server.common.data.edqs.EdqsObject; +import org.thingsboard.server.common.data.edqs.EdqsSyncRequest; +import org.thingsboard.server.common.data.edqs.Entity; +import org.thingsboard.server.common.data.edqs.ToCoreEdqsMsg; +import org.thingsboard.server.common.data.edqs.ToCoreEdqsRequest; +import org.thingsboard.server.common.data.edqs.query.EdqsRequest; +import org.thingsboard.server.common.data.edqs.query.EdqsResponse; +import org.thingsboard.server.common.data.id.CustomerId; +import org.thingsboard.server.common.data.id.EntityId; +import org.thingsboard.server.common.data.id.TenantId; +import org.thingsboard.server.common.data.kv.BaseAttributeKvEntry; +import org.thingsboard.server.common.data.kv.JsonDataEntry; +import org.thingsboard.server.common.data.kv.KvEntry; +import org.thingsboard.server.common.msg.edqs.EdqsService; +import org.thingsboard.server.common.msg.queue.ServiceType; +import org.thingsboard.server.dao.attributes.AttributesService; +import org.thingsboard.server.edqs.processor.EdqsConverter; +import org.thingsboard.server.edqs.processor.EdqsProducer; +import org.thingsboard.server.edqs.util.EdqsPartitionService; +import org.thingsboard.server.gen.transport.TransportProtos; +import org.thingsboard.server.gen.transport.TransportProtos.EdqsEventMsg; +import org.thingsboard.server.gen.transport.TransportProtos.EdqsRequestMsg; +import org.thingsboard.server.gen.transport.TransportProtos.FromEdqsMsg; +import org.thingsboard.server.gen.transport.TransportProtos.ToCoreNotificationMsg; +import org.thingsboard.server.gen.transport.TransportProtos.ToEdqsCoreServiceMsg; +import org.thingsboard.server.gen.transport.TransportProtos.ToEdqsMsg; +import org.thingsboard.server.queue.TbQueueRequestTemplate; +import org.thingsboard.server.queue.common.TbProtoQueueMsg; +import org.thingsboard.server.queue.discovery.HashPartitionService; +import org.thingsboard.server.queue.edqs.EdqsQueue; +import org.thingsboard.server.queue.environment.DistributedLock; +import org.thingsboard.server.queue.environment.DistributedLockService; +import org.thingsboard.server.queue.provider.EdqsClientQueueFactory; +import org.thingsboard.server.queue.util.AfterStartUp; + +import java.util.UUID; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.TimeUnit; + +@Service +@RequiredArgsConstructor +@Slf4j +@ConditionalOnProperty(value = "queue.edqs.sync_enabled", havingValue = "true") +public class DefaultEdqsService implements EdqsService { + + private final EdqsClientQueueFactory queueFactory; + private final EdqsConverter edqsConverter; + private final EdqsSyncService edqsSyncService; + private final DistributedLockService distributedLockService; + private final AttributesService attributesService; + private final EdqsPartitionService edqsPartitionService; + @Autowired @Lazy + private TbClusterService clusterService; + @Autowired @Lazy + private HashPartitionService hashPartitionService; + + @Value("${queue.edqs.api_enabled:false}") + private Boolean apiEnabled; + + private EdqsProducer eventsProducer; + private TbQueueRequestTemplate, TbProtoQueueMsg> requestTemplate; + private ExecutorService executor; + private DistributedLock syncLock; + + @PostConstruct + private void init() { + executor = ThingsBoardExecutors.newWorkStealingPool(12, getClass()); + eventsProducer = EdqsProducer.builder() + .queue(EdqsQueue.EVENTS) + .partitionService(edqsPartitionService) + .producer(queueFactory.createEdqsMsgProducer(EdqsQueue.EVENTS)) + .build(); + if (apiEnabled) { + apiEnabled = null; + } + + requestTemplate = queueFactory.createEdqsRequestTemplate(); + requestTemplate.init(); + syncLock = distributedLockService.getLock("edqs_sync"); + } + + @AfterStartUp(order = AfterStartUp.REGULAR_SERVICE) + public void onStartUp() { + executor.submit(() -> { + try { + EdqsSyncState syncState = getSyncState(); + if (edqsSyncService.isSyncNeeded() || syncState == null || syncState.getStatus() != EdqsSyncStatus.FINISHED) { + if (hashPartitionService.isSystemPartitionMine(ServiceType.TB_CORE)) { + processSystemRequest(ToCoreEdqsRequest.builder() + .syncRequest(new EdqsSyncRequest()) + .build()); + } + } else { // only if topic/RocksDB is not empty and sync is finished + if (apiEnabled == null) { + log.info("EDQS is already synced, enabling API"); + apiEnabled = true; + } else { + log.info("EDQS is already synced"); + } + } + } catch (Throwable e) { + log.error("Failed to start EDQS service", e); + } + }); + } + + @Override + public void processSystemRequest(ToCoreEdqsRequest request) { + log.info("Processing system request {}", request); + if (request.getSyncRequest() != null) { + saveSyncState(EdqsSyncStatus.REQUESTED); + } + broadcast(request.toInternalMsg()); + } + + @Override + public void processSystemMsg(ToCoreEdqsMsg msg) { + executor.submit(() -> { + log.info("Processing system msg {}", msg); + try { + if (msg.getApiEnabled() != null) { + apiEnabled = msg.getApiEnabled(); + } + + if (msg.getSyncRequest() != null) { + syncLock.lock(); + try { + EdqsSyncState syncState = getSyncState(); + if (syncState != null && syncState.getStatus() == EdqsSyncStatus.FINISHED) { + log.info("EDQS sync is already finished"); + return; + } + + saveSyncState(EdqsSyncStatus.STARTED); + edqsSyncService.sync(); + + saveSyncState(EdqsSyncStatus.FINISHED); + if (apiEnabled == null) { + broadcast(ToCoreEdqsMsg.builder() + .apiEnabled(Boolean.TRUE) + .build()); + } + } catch (Exception e) { + log.error("Failed to complete sync", e); + saveSyncState(EdqsSyncStatus.FAILED); + } finally { + syncLock.unlock(); + } + } + } catch (Throwable e) { + log.error("Failed to process msg {}", msg, e); + } + }); + } + + @Override + public void onUpdate(TenantId tenantId, EntityId entityId, Object entity) { + EntityType entityType = entityId.getEntityType(); + ObjectType objectType = ObjectType.fromEntityType(entityType); + if (!isEdqsType(tenantId, objectType)) { + log.trace("[{}][{}] Ignoring update event, type {} not supported", tenantId, entityId, entityType); + return; + } + onUpdate(tenantId, objectType, edqsConverter.toEntity(entityType, entity)); + } + + @Override + public void onUpdate(TenantId tenantId, ObjectType objectType, EdqsObject object) { + processEvent(tenantId, objectType, EdqsEventType.UPDATED, object); + } + + @Override + public void onDelete(TenantId tenantId, EntityId entityId) { + EntityType entityType = entityId.getEntityType(); + ObjectType objectType = ObjectType.fromEntityType(entityType); + if (!isEdqsType(tenantId, objectType)) { + log.trace("[{}][{}] Ignoring deletion event, type {} not supported", tenantId, entityId, entityType); + return; + } + onDelete(tenantId, objectType, new Entity(entityType, entityId.getId(), Long.MAX_VALUE)); + } + + @Override + public void onDelete(TenantId tenantId, ObjectType objectType, EdqsObject object) { + processEvent(tenantId, objectType, EdqsEventType.DELETED, object); + } + + @Override + public ListenableFuture processRequest(TenantId tenantId, CustomerId customerId, EdqsRequest request) { + var requestMsg = newEdqsMsg(tenantId) + .setRequestMsg(EdqsRequestMsg.newBuilder() + .setValue(JacksonUtil.toString(request)) + .build()); + if (customerId != null && !customerId.isNullUid()) { + requestMsg.setCustomerIdMSB(customerId.getId().getMostSignificantBits()); + requestMsg.setCustomerIdLSB(customerId.getId().getLeastSignificantBits()); + } + + Integer partition = edqsPartitionService.resolvePartition(tenantId); + ListenableFuture> resultFuture = requestTemplate.send(new TbProtoQueueMsg<>(UUID.randomUUID(), requestMsg.build()), partition); + return Futures.transform(resultFuture, msg -> { + TransportProtos.EdqsResponseMsg responseMsg = msg.getValue().getResponseMsg(); + return JacksonUtil.fromString(responseMsg.getValue(), EdqsResponse.class); + }, MoreExecutors.directExecutor()); + } + + @Override + public boolean isApiEnabled() { + return Boolean.TRUE.equals(apiEnabled); + } + + protected void processEvent(TenantId tenantId, ObjectType objectType, EdqsEventType eventType, EdqsObject object) { + executor.submit(() -> { + try { + String key = object.key(); + Long version = object.version(); + EdqsEventMsg.Builder eventMsg = EdqsEventMsg.newBuilder() + .setKey(key) + .setObjectType(objectType.name()) + .setData(ByteString.copyFrom(edqsConverter.serialize(objectType, object))) + .setEventType(eventType.name()); + if (version != null) { + eventMsg.setVersion(version); + } + eventsProducer.send(tenantId, objectType, key, newEdqsMsg(tenantId) + .setEventMsg(eventMsg) + .build()); + } catch (Throwable e) { + log.error("[{}] Failed to push {} event for {} {}", tenantId, eventType, objectType, object, e); + } + }); + } + + private boolean isEdqsType(TenantId tenantId, ObjectType objectType) { + if (objectType == null) { + return false; + } + if (!tenantId.isSysTenantId()) { + return ObjectType.edqsTypes.contains(objectType); + } else { + return ObjectType.edqsSystemTypes.contains(objectType); + } + } + + private void broadcast(ToCoreEdqsMsg msg) { + clusterService.broadcastToCore(ToCoreNotificationMsg.newBuilder() + .setToEdqsCoreServiceMsg(ToEdqsCoreServiceMsg.newBuilder() + .setValue(ByteString.copyFrom(JacksonUtil.writeValueAsBytes(msg)))) + .build()); + } + + private static ToEdqsMsg.Builder newEdqsMsg(TenantId tenantId) { + return ToEdqsMsg.newBuilder() + .setTenantIdMSB(tenantId.getId().getMostSignificantBits()) + .setTenantIdLSB(tenantId.getId().getLeastSignificantBits()) + .setTs(System.currentTimeMillis()); + } + + @PreDestroy + private void preDestroy() { + executor.shutdown(); + eventsProducer.stop(); + requestTemplate.stop(); + } + + @SneakyThrows + private EdqsSyncState getSyncState() { + EdqsSyncState state = attributesService.find(TenantId.SYS_TENANT_ID, TenantId.SYS_TENANT_ID, AttributeScope.SERVER_SCOPE, "edqsSyncState").get(30, TimeUnit.SECONDS) + .flatMap(KvEntry::getJsonValue) + .map(value -> JacksonUtil.fromString(value, EdqsSyncState.class)) + .orElse(null); + log.info("getSyncState = {}", state); + return state; + } + + @SneakyThrows + private void saveSyncState(EdqsSyncStatus status) { + EdqsSyncState state = new EdqsSyncState(status); + log.info("saveSyncState {}", state); + attributesService.save(TenantId.SYS_TENANT_ID, TenantId.SYS_TENANT_ID, AttributeScope.SERVER_SCOPE, new BaseAttributeKvEntry( + new JsonDataEntry("edqsSyncState", JacksonUtil.toString(state)), + System.currentTimeMillis())).get(30, TimeUnit.SECONDS); + } + + @Data + @AllArgsConstructor + @NoArgsConstructor + private static class EdqsSyncState { + private EdqsSyncStatus status; + } + + private enum EdqsSyncStatus { + REQUESTED, + STARTED, + FINISHED, + FAILED + } + +} diff --git a/application/src/main/java/org/thingsboard/server/service/edqs/EdqsDataLoader.java b/application/src/main/java/org/thingsboard/server/service/edqs/EdqsDataLoader.java new file mode 100644 index 0000000000..69a3f6108d --- /dev/null +++ b/application/src/main/java/org/thingsboard/server/service/edqs/EdqsDataLoader.java @@ -0,0 +1,539 @@ +/** + * Copyright © 2016-2024 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.service.edqs; + +import com.fasterxml.jackson.databind.MappingIterator; +import com.fasterxml.jackson.dataformat.csv.CsvMapper; +import com.fasterxml.jackson.dataformat.csv.CsvSchema; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.thingsboard.common.util.ThingsBoardThreadFactory; +import org.thingsboard.server.common.data.ApiUsageState; +import org.thingsboard.server.common.data.AttributeScope; +import org.thingsboard.server.common.data.Customer; +import org.thingsboard.server.common.data.Dashboard; +import org.thingsboard.server.common.data.Device; +import org.thingsboard.server.common.data.DeviceProfile; +import org.thingsboard.server.common.data.DeviceProfileType; +import org.thingsboard.server.common.data.EntityType; +import org.thingsboard.server.common.data.EntityView; +import org.thingsboard.server.common.data.ObjectType; +import org.thingsboard.server.common.data.StringUtils; +import org.thingsboard.server.common.data.Tenant; +import org.thingsboard.server.common.data.User; +import org.thingsboard.server.common.data.asset.Asset; +import org.thingsboard.server.common.data.asset.AssetProfile; +import org.thingsboard.server.common.data.converter.Converter; +import org.thingsboard.server.common.data.converter.ConverterType; +import org.thingsboard.server.common.data.edge.Edge; +import org.thingsboard.server.common.data.edqs.AttributeKv; +import org.thingsboard.server.common.data.edqs.LatestTsKv; +import org.thingsboard.server.common.data.group.EntityGroup; +import org.thingsboard.server.common.data.id.ApiUsageStateId; +import org.thingsboard.server.common.data.id.AssetId; +import org.thingsboard.server.common.data.id.AssetProfileId; +import org.thingsboard.server.common.data.id.ConverterId; +import org.thingsboard.server.common.data.id.CustomerId; +import org.thingsboard.server.common.data.id.DashboardId; +import org.thingsboard.server.common.data.id.DeviceId; +import org.thingsboard.server.common.data.id.DeviceProfileId; +import org.thingsboard.server.common.data.id.EdgeId; +import org.thingsboard.server.common.data.id.EntityGroupId; +import org.thingsboard.server.common.data.id.EntityId; +import org.thingsboard.server.common.data.id.EntityIdFactory; +import org.thingsboard.server.common.data.id.EntityViewId; +import org.thingsboard.server.common.data.id.IntegrationId; +import org.thingsboard.server.common.data.id.RoleId; +import org.thingsboard.server.common.data.id.RuleChainId; +import org.thingsboard.server.common.data.id.SchedulerEventId; +import org.thingsboard.server.common.data.id.TenantId; +import org.thingsboard.server.common.data.id.TenantProfileId; +import org.thingsboard.server.common.data.id.UserId; +import org.thingsboard.server.common.data.id.WidgetTypeId; +import org.thingsboard.server.common.data.id.WidgetsBundleId; +import org.thingsboard.server.common.data.integration.Integration; +import org.thingsboard.server.common.data.integration.IntegrationType; +import org.thingsboard.server.common.data.kv.AttributeKvEntry; +import org.thingsboard.server.common.data.kv.BaseAttributeKvEntry; +import org.thingsboard.server.common.data.kv.BasicTsKvEntry; +import org.thingsboard.server.common.data.kv.BooleanDataEntry; +import org.thingsboard.server.common.data.kv.DoubleDataEntry; +import org.thingsboard.server.common.data.kv.JsonDataEntry; +import org.thingsboard.server.common.data.kv.KvEntry; +import org.thingsboard.server.common.data.kv.LongDataEntry; +import org.thingsboard.server.common.data.kv.StringDataEntry; +import org.thingsboard.server.common.data.relation.EntityRelation; +import org.thingsboard.server.common.data.relation.RelationTypeGroup; +import org.thingsboard.server.common.data.role.Role; +import org.thingsboard.server.common.data.role.RoleType; +import org.thingsboard.server.common.data.rule.RuleChain; +import org.thingsboard.server.common.data.scheduler.SchedulerEvent; +import org.thingsboard.server.common.data.widget.WidgetType; +import org.thingsboard.server.common.data.widget.WidgetsBundle; +import org.thingsboard.server.common.msg.edqs.EdqsService; +import org.thingsboard.server.edqs.processor.EdqsConverter; + +import java.io.FileReader; +import java.util.Map; +import java.util.UUID; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; +import java.util.function.Consumer; + +import static org.thingsboard.common.util.JacksonUtil.toJsonNode; + + +@RequiredArgsConstructor +@Slf4j +//@Service +public class EdqsDataLoader { + + private final EdqsService edqsService; + private final EdqsConverter edqsConverter; + + public final static TenantId MAIN = TenantId.fromUUID(UUID.fromString("2a209df0-c7ff-11ea-a3e0-f321b0429d60")); + + private final String folder = "/home/viacheslav/Downloads/schwarz"; + + private ExecutorService executor = Executors.newFixedThreadPool(5, ThingsBoardThreadFactory.forName("edqs-publisher")); + +// @AfterStartUp(order = 100) + public void load() throws Exception { + loadCustomers(); + loadDeviceProfile(); + loadDevices(); + loadAssets(); + loadEdges(); + loadEntityViews(); + loadTenants(); + loadUsers(); + loadDashboards(); + loadRuleChains(); + loadWidgetType(); + loadWidgetBundle(); + loadConverters(); + loadIntegrations(); + loadSchedulerEvents(); + loadRoles(); + loadApiUsageStates(); + loadAssetProfile(); + loadEntityGroups(); + loadRelations(); + + loadAttributes(); + loadTs(); + } + + private void loadCustomers() throws Exception { + load("customer.csv", (values) -> { + Customer customer = new Customer(); + customer.setTitle(values.get("title")); + customer.setId(new CustomerId(UUID.fromString(values.get("id")))); + customer.setCreatedTime(Long.parseLong(values.get("created_time"))); + customer.setTenantId(tenantId(values.get("tenant_id"))); + var parentCustomerId = values.get("parent_customer_id"); + if (StringUtils.isNotEmpty(parentCustomerId)) { + customer.setParentCustomerId(new CustomerId(UUID.fromString(parentCustomerId))); + } + edqsService.onUpdate(customer.getTenantId(), customer.getId(), customer); + }); + } + + private void loadDevices() throws Exception { + load("device.csv", (values) -> { + Device device = new Device(); + device.setType(values.get("type")); + device.setName(values.get("name")); + device.setLabel(values.get("label")); + device.setId(new DeviceId(uuid(values.get("id")))); + device.setCreatedTime(parseLong(values.get("created_time"))); + device.setCustomerId(customerId(values.get("customer_id"))); + device.setTenantId(tenantId(values.get("tenant_id"))); + device.setDeviceProfileId(new DeviceProfileId(uuid(values.get("device_profile_id")))); + device.setAdditionalInfo(toJsonNode(values.get("additional_info"))); + + edqsService.onUpdate(device.getTenantId(), device.getId(), device); + }); + } + + private void loadAssets() throws Exception { + load("asset.csv", (values) -> { + Asset asset = new Asset(); + asset.setType(values.get("type")); + asset.setName(values.get("name")); + asset.setLabel(values.get("label")); + asset.setId(new AssetId(uuid(values.get("id")))); + asset.setCreatedTime(parseLong(values.get("created_time"))); + asset.setCustomerId(customerId(values.get("customer_id"))); + asset.setTenantId(tenantId(values.get("tenant_id"))); + asset.setAssetProfileId(new AssetProfileId(uuid(values.get("asset_profile_id")))); + asset.setAdditionalInfo(toJsonNode(values.get("additional_info"))); + + edqsService.onUpdate(asset.getTenantId(), asset.getId(), asset); + }); + } + + private void loadEdges() throws Exception { + load("edge.csv", (values) -> { + Edge edge = new Edge(); + edge.setId(new EdgeId(uuid(values.get("id")))); + edge.setCreatedTime(parseLong(values.get("created_time"))); + edge.setType(values.get("type")); + edge.setName(values.get("name")); + edge.setLabel(values.get("label")); + edge.setCustomerId(customerId(values.get("customer_id"))); + edge.setTenantId(tenantId(values.get("tenant_id"))); + edge.setAdditionalInfo(toJsonNode(values.get("additional_info"))); + + edqsService.onUpdate(edge.getTenantId(), edge.getId(), edge); + }); + } + + private void loadEntityViews() throws Exception { + load("entity_view.csv", (values) -> { + EntityView entityView = new EntityView(); + entityView.setId(new EntityViewId(uuid(values.get("id")))); + entityView.setCreatedTime(parseLong(values.get("created_time"))); + entityView.setType(values.get("type")); + entityView.setName(values.get("name")); + entityView.setCustomerId(customerId(values.get("customer_id"))); + entityView.setTenantId(tenantId(values.get("tenant_id"))); + entityView.setAdditionalInfo(toJsonNode(values.get("additional_info"))); + + edqsService.onUpdate(entityView.getTenantId(), entityView.getId(), entityView); + }); + } + + private void loadTenants() throws Exception { + load("tenant.csv", (values) -> { + Tenant tenant = new Tenant(); + tenant.setId(new TenantId(uuid(values.get("id")))); + tenant.setCreatedTime(parseLong(values.get("created_time"))); + tenant.setEmail(values.get("email")); + tenant.setTitle(values.get("title")); + tenant.setCountry(values.get("country")); + tenant.setState(values.get("state")); + tenant.setCity(values.get("city")); + tenant.setAddress(values.get("address")); + tenant.setAddress2(values.get("address2")); + tenant.setZip(values.get("zip")); + tenant.setPhone(values.get("phone")); + tenant.setRegion(values.get("region")); + tenant.setTenantProfileId(new TenantProfileId(uuid(values.get("tenant_profile_id")))); + tenant.setAdditionalInfo(toJsonNode(values.get("additional_info"))); + edqsService.onUpdate(MAIN, tenant.getId(), tenant); + }); + } + + private void loadUsers() throws Exception { + load("user.csv", (values) -> { + User user = new User(); + user.setId(new UserId(uuid(values.get("id")))); + user.setCreatedTime(parseLong(values.get("created_time"))); + user.setTenantId(tenantId(values.get("tenant_id"))); + user.setFirstName(values.get("first_name")); + user.setLastName(values.get("last_name")); + user.setEmail(values.get("email")); + user.setPhone(values.get("phone")); + user.setAdditionalInfo(toJsonNode(values.get("additional_info"))); + + edqsService.onUpdate(user.getTenantId(), user.getId(), user); + }); + } + + private void loadDashboards() throws Exception { + load("dashboard.csv", (values) -> { + Dashboard dashboard = new Dashboard(); + dashboard.setId(new DashboardId(uuid(values.get("id")))); + dashboard.setCreatedTime(parseLong(values.get("created_time"))); + dashboard.setTenantId(tenantId(values.get("tenant_id"))); + dashboard.setTitle(values.get("title")); + + edqsService.onUpdate(dashboard.getTenantId(), dashboard.getId(), dashboard); + }); + } + + private void loadEntityGroups() throws Exception { + load("entity_group.csv", (values) -> { + EntityGroup entityGroup = new EntityGroup(); + entityGroup.setId(new EntityGroupId(uuid(values.get("id")))); + entityGroup.setCreatedTime(parseLong(values.get("created_time"))); + entityGroup.setName(values.get("name")); + entityGroup.setOwnerId(entityId(values.get("owner_type"), values.get("owner_id"))); + entityGroup.setType(EntityType.valueOf(values.get("type"))); + edqsService.onUpdate(MAIN, entityGroup.getId(), entityGroup); + }); + } + + private void loadRelations() throws Exception { + load("relation.csv", (values) -> { + EntityRelation entityRelation = new EntityRelation(); + entityRelation.setFrom(entityId(values.get("from_type"), values.get("from_id"))); + entityRelation.setTo(entityId(values.get("to_type"), values.get("to_id"))); + entityRelation.setTypeGroup(RelationTypeGroup.valueOf(values.get("relation_type_group"))); + entityRelation.setType(values.get("relation_type")); + edqsService.onUpdate(MAIN, ObjectType.RELATION, entityRelation); + }); + } + + private void loadRuleChains() throws Exception { + load("rule_chain.csv", (values) -> { + RuleChain ruleChain = new RuleChain(); + ruleChain.setId(new RuleChainId(uuid(values.get("id")))); + ruleChain.setCreatedTime(parseLong(values.get("created_time"))); + ruleChain.setName(values.get("name")); + ruleChain.setTenantId(tenantId(values.get("tenant_id"))); + ruleChain.setAdditionalInfo(toJsonNode(values.get("additional_info"))); + + edqsService.onUpdate(ruleChain.getTenantId(), ruleChain.getId(), ruleChain); + }); + } + + private void loadWidgetType() throws Exception { + load("widget_type.csv", (values) -> { + WidgetType widgetType = new WidgetType(); + widgetType.setId(new WidgetTypeId(uuid(values.get("id")))); + widgetType.setCreatedTime(parseLong(values.get("created_time"))); + widgetType.setName(values.get("name")); + widgetType.setTenantId(tenantId(values.get("tenant_id"))); + + edqsService.onUpdate(widgetType.getTenantId(), widgetType.getId(), widgetType); + }); + } + + private void loadWidgetBundle() throws Exception { + load("widgets_bundle.csv", (values) -> { + WidgetsBundle widgetsBundle = new WidgetsBundle(); + widgetsBundle.setId(new WidgetsBundleId(uuid(values.get("id")))); + widgetsBundle.setCreatedTime(parseLong(values.get("created_time"))); + widgetsBundle.setTitle(values.get("title")); + widgetsBundle.setTenantId(tenantId(values.get("tenant_id"))); + + edqsService.onUpdate(widgetsBundle.getTenantId(), widgetsBundle.getId(), widgetsBundle); + }); + } + + private void loadConverters() throws Exception { + load("converter.csv", (values) -> { + Converter converter = new Converter(); + converter.setId(new ConverterId(uuid(values.get("id")))); + converter.setCreatedTime(parseLong(values.get("created_time"))); + converter.setName(values.get("name")); + converter.setType(ConverterType.valueOf(values.get("type"))); + converter.setTenantId(tenantId(values.get("tenant_id"))); + converter.setEdgeTemplate(parseBoolean(values.get("is_edge_template"))); + converter.setAdditionalInfo(toJsonNode(values.get("additional_info"))); + + edqsService.onUpdate(converter.getTenantId(), converter.getId(), converter); + }); + } + + private void loadIntegrations() throws Exception { + load("integration.csv", (values) -> { + Integration integration = new Integration(); + integration.setId(new IntegrationId(uuid(values.get("id")))); + integration.setCreatedTime(parseLong(values.get("created_time"))); + integration.setName(values.get("name")); + integration.setType(IntegrationType.valueOf(values.get("type"))); + integration.setTenantId(tenantId(values.get("tenant_id"))); + integration.setEdgeTemplate(parseBoolean(values.get("is_edge_template"))); + integration.setAdditionalInfo(toJsonNode(values.get("additional_info"))); + + edqsService.onUpdate(integration.getTenantId(), integration.getId(), integration); + }); + } + + private void loadSchedulerEvents() throws Exception { + load("scheduler_event.csv", (values) -> { + SchedulerEvent schedulerEvent = new SchedulerEvent(); + schedulerEvent.setId(new SchedulerEventId(uuid(values.get("id")))); + schedulerEvent.setCreatedTime(parseLong(values.get("created_time"))); + schedulerEvent.setName(values.get("name")); + schedulerEvent.setType(values.get("type")); + schedulerEvent.setTenantId(tenantId(values.get("tenant_id"))); + schedulerEvent.setConfiguration(toJsonNode(values.get("configuration"))); + schedulerEvent.setSchedule(toJsonNode(values.get("schedule"))); + schedulerEvent.setOriginatorId(entityId(values.get("originator_type"), values.get("originator_id"))); + schedulerEvent.setAdditionalInfo(toJsonNode(values.get("additional_info"))); + + edqsService.onUpdate(schedulerEvent.getTenantId(), schedulerEvent.getId(), schedulerEvent); + }); + } + + private void loadRoles() throws Exception { + load("role.csv", (values) -> { + Role role = new Role(); + role.setId(new RoleId(uuid(values.get("id")))); + role.setCreatedTime(parseLong(values.get("created_time"))); + role.setName(values.get("name")); + role.setType(RoleType.valueOf(values.get("type"))); + role.setTenantId(tenantId(values.get("tenant_id"))); + role.setAdditionalInfo(toJsonNode(values.get("additional_info"))); + + edqsService.onUpdate(role.getTenantId(), role.getId(), role); + }); + } + + private void loadApiUsageStates() throws Exception { + load("api_usage_state.csv", (values) -> { + ApiUsageState apiUsageState = new ApiUsageState(); + apiUsageState.setId(new ApiUsageStateId(uuid(values.get("id")))); + apiUsageState.setCreatedTime(parseLong(values.get("created_time"))); + apiUsageState.setEntityId(entityId(values.get("entity_type"), values.get("entity_id"))); + apiUsageState.setTenantId(tenantId(values.get("tenant_id"))); + + edqsService.onUpdate(apiUsageState.getTenantId(), apiUsageState.getId(), apiUsageState); + }); + } + + private void loadDeviceProfile() throws Exception { + load("device_profile.csv", (values) -> { + DeviceProfile deviceProfile = new DeviceProfile(); + deviceProfile.setId(new DeviceProfileId(uuid(values.get("id")))); + deviceProfile.setCreatedTime(parseLong(values.get("created_time"))); + deviceProfile.setName(values.get("name")); + deviceProfile.setType(DeviceProfileType.valueOf(values.get("type"))); + deviceProfile.setTenantId(tenantId(values.get("tenant_id"))); + + edqsService.onUpdate(deviceProfile.getTenantId(), deviceProfile.getId(), deviceProfile); + }); + } + + private void loadAssetProfile() throws Exception { + load("asset_profile.csv", (values) -> { + AssetProfile assetProfile = new AssetProfile(); + assetProfile.setId(new AssetProfileId(uuid(values.get("id")))); + assetProfile.setCreatedTime(parseLong(values.get("created_time"))); + assetProfile.setName(values.get("name")); + assetProfile.setTenantId(tenantId(values.get("tenant_id"))); + + edqsService.onUpdate(assetProfile.getTenantId(), assetProfile.getId(), assetProfile); + }); + } + + private void loadAttributes() throws Exception { + load("attribute.csv", (values) -> { + EntityId entityId = EntityIdFactory.getByTypeAndId(values.get("entity_type"), values.get("entity_id")); + long ts = parseLong(values.get("last_update_ts")); + AttributeScope scope = AttributeScope.valueOf(values.get("attribute_type")); + String key = values.get("attribute_key"); + KvEntry kvEntry; + if (StringUtils.isNotEmpty(values.get("bool_v"))) { + kvEntry = new BooleanDataEntry(key, "t".equals(values.get("bool_v"))); + } else if (StringUtils.isNotEmpty(values.get("str_v"))) { + kvEntry = new StringDataEntry(key, values.get("str_v")); + } else if (StringUtils.isNotEmpty(values.get("long_v"))) { + kvEntry = new LongDataEntry(key, parseLong(values.get("long_v"))); + } else if (StringUtils.isNotEmpty(values.get("dbl_v"))) { + kvEntry = new DoubleDataEntry(key, Double.parseDouble(values.get("dbl_v"))); + } else if (StringUtils.isNotEmpty(values.get("json_v"))) { + kvEntry = new JsonDataEntry(key, values.get("json_v")); + } else { + kvEntry = new StringDataEntry(key, ""); + } + AttributeKvEntry attributeKvEntry = new BaseAttributeKvEntry(ts, kvEntry); + AttributeKv attributeKv = new AttributeKv(entityId, scope, attributeKvEntry, 0); + edqsService.onUpdate(MAIN, ObjectType.ATTRIBUTE_KV, attributeKv); + }); + } + + private void loadTs() throws Exception { + load("ts_kv.csv", (values) -> { + var entityTypeStr = values.get("find_entity_type"); + if (StringUtils.isEmpty(entityTypeStr)) { + return; + } + EntityId entityId = EntityIdFactory.getByTypeAndId(values.get("find_entity_type"), values.get("entity_id")); + long ts = parseLong(values.get("ts")); + String key = values.get("key"); + KvEntry kvEntry; + if (StringUtils.isNotEmpty(values.get("bool_v"))) { + kvEntry = new BooleanDataEntry(key, "t".equals(values.get("bool_v"))); + } else if (StringUtils.isNotEmpty(values.get("str_v"))) { + kvEntry = new StringDataEntry(key, values.get("str_v")); + } else if (StringUtils.isNotEmpty(values.get("long_v"))) { + kvEntry = new LongDataEntry(key, parseLong(values.get("long_v"))); + } else if (StringUtils.isNotEmpty(values.get("dbl_v"))) { + kvEntry = new DoubleDataEntry(key, Double.parseDouble(values.get("dbl_v"))); + } else if (StringUtils.isNotEmpty(values.get("json_v"))) { + kvEntry = new JsonDataEntry(key, values.get("json_v")); + } else { + kvEntry = new StringDataEntry(key, ""); + } + BasicTsKvEntry tsKvEntry = new BasicTsKvEntry(ts, kvEntry); + edqsService.onUpdate(MAIN, ObjectType.LATEST_TS_KV, new LatestTsKv(entityId, tsKvEntry, 0L)); + }); + } + + private void load(String file, Consumer> function) throws Exception { + Executors.newSingleThreadExecutor(ThingsBoardThreadFactory.forName("loader-" + file)).submit(() -> { + try { + long ts = System.currentTimeMillis(); + CsvSchema schema = CsvSchema.emptySchema().withHeader().withColumnSeparator('|'); + CsvMapper mapper = new CsvMapper(); + MappingIterator> it = mapper + .readerFor(Map.class) + .with(schema) + .readValues(new FileReader(folder + "/" + file)); + + int success = 0; + int failure = 0; + while (it.hasNextValue()) { + Map row = it.nextValue(); + try { + function.accept(row); + success++; + if (success % 1000 == 0) { + log.info("Loaded [{}] from [{}]", success, file); + } + } catch (Exception e) { + log.error("Failed to parse str: [{}]", row, e); + failure++; + } + } + log.info("Loaded [{}] from [{}] in {}ms. Failures {}", success, file, (System.currentTimeMillis() - ts), failure); + } catch (Throwable t) { + log.error("Failed to load data from [{}]", file, t); + } + }); + } + + private static TenantId tenantId(String id) { + return TenantId.fromUUID(UUID.fromString(id)); + } + + private static CustomerId customerId(String id) { + var c = new CustomerId(UUID.fromString(id)); + return c.isNullUid() ? null : c; + } + + private static EntityId entityId(String type, String id) { + return EntityIdFactory.getByTypeAndId(type, id); + } + + private static UUID uuid(String id) { + return UUID.fromString(id); + } + + private static long parseLong(String time) { + return Long.parseLong(time); + } + + private static boolean parseBoolean(String value) { + return Boolean.parseBoolean(value); + } + +} diff --git a/application/src/main/java/org/thingsboard/server/service/edqs/EdqsListener.java b/application/src/main/java/org/thingsboard/server/service/edqs/EdqsListener.java new file mode 100644 index 0000000000..c4ce0c7a7e --- /dev/null +++ b/application/src/main/java/org/thingsboard/server/service/edqs/EdqsListener.java @@ -0,0 +1,61 @@ +/** + * Copyright © 2016-2024 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.service.edqs; + +import lombok.RequiredArgsConstructor; +import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; +import org.springframework.stereotype.Service; +import org.springframework.transaction.event.TransactionalEventListener; +import org.thingsboard.server.common.data.ObjectType; +import org.thingsboard.server.common.data.audit.ActionType; +import org.thingsboard.server.common.msg.edqs.EdqsService; +import org.thingsboard.server.dao.eventsourcing.DeleteEntityEvent; +import org.thingsboard.server.dao.eventsourcing.RelationActionEvent; +import org.thingsboard.server.dao.eventsourcing.SaveEntityEvent; + +@Service +@RequiredArgsConstructor +@ConditionalOnProperty(value = "queue.edqs.sync_enabled", havingValue = "true") +public class EdqsListener { + + private final EdqsService edqsService; + + @TransactionalEventListener(fallbackExecution = true) + public void onUpdate(SaveEntityEvent event) { + if (event.getEntityId() == null || event.getEntity() == null) { + return; + } + edqsService.onUpdate(event.getTenantId(), event.getEntityId(), event.getEntity()); + } + + @TransactionalEventListener(fallbackExecution = true) + public void onDelete(DeleteEntityEvent event) { + if (event.getEntityId() == null) { + return; + } + edqsService.onDelete(event.getTenantId(), event.getEntityId()); + } + + @TransactionalEventListener(fallbackExecution = true) + public void handleEvent(RelationActionEvent relationEvent) { + if (relationEvent.getActionType() == ActionType.RELATION_ADD_OR_UPDATE) { + edqsService.onUpdate(relationEvent.getTenantId(), ObjectType.RELATION, relationEvent.getRelation()); + } else if (relationEvent.getActionType() == ActionType.RELATION_DELETED) { + edqsService.onDelete(relationEvent.getTenantId(), ObjectType.RELATION, relationEvent.getRelation()); + } + } + +} diff --git a/application/src/main/java/org/thingsboard/server/service/edqs/EdqsSyncService.java b/application/src/main/java/org/thingsboard/server/service/edqs/EdqsSyncService.java new file mode 100644 index 0000000000..8e12da56e0 --- /dev/null +++ b/application/src/main/java/org/thingsboard/server/service/edqs/EdqsSyncService.java @@ -0,0 +1,275 @@ +/** + * Copyright © 2016-2024 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.service.edqs; + +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.context.annotation.Lazy; +import org.thingsboard.server.common.data.AttributeScope; +import org.thingsboard.server.common.data.EntityType; +import org.thingsboard.server.common.data.ObjectType; +import org.thingsboard.server.common.data.edqs.AttributeKv; +import org.thingsboard.server.common.data.edqs.EdqsEventType; +import org.thingsboard.server.common.data.edqs.EdqsObject; +import org.thingsboard.server.common.data.edqs.Entity; +import org.thingsboard.server.common.data.edqs.LatestTsKv; +import org.thingsboard.server.common.data.edqs.fields.EntityFields; +import org.thingsboard.server.common.data.edqs.fields.TenantFields; +import org.thingsboard.server.common.data.id.EntityId; +import org.thingsboard.server.common.data.id.EntityIdFactory; +import org.thingsboard.server.common.data.id.TenantId; +import org.thingsboard.server.common.data.page.PageDataIterable; +import org.thingsboard.server.common.data.page.SortOrder; +import org.thingsboard.server.common.data.relation.EntityRelation; +import org.thingsboard.server.common.data.relation.RelationTypeGroup; +import org.thingsboard.server.dao.Dao; +import org.thingsboard.server.dao.attributes.AttributesDao; +import org.thingsboard.server.dao.dictionary.KeyDictionaryDao; +import org.thingsboard.server.dao.entity.EntityDaoRegistry; +import org.thingsboard.server.dao.group.EntityGroupDao; +import org.thingsboard.server.dao.model.sql.AttributeKvEntity; +import org.thingsboard.server.dao.model.sqlts.dictionary.KeyDictionaryEntry; +import org.thingsboard.server.dao.model.sqlts.latest.TsKvLatestEntity; +import org.thingsboard.server.dao.relation.RelationDao; +import org.thingsboard.server.dao.tenant.TenantDao; +import org.thingsboard.server.dao.timeseries.TimeseriesLatestDao; + +import java.util.EnumSet; +import java.util.Map; +import java.util.Set; +import java.util.UUID; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.atomic.AtomicInteger; + +import static org.thingsboard.server.common.data.ObjectType.API_USAGE_STATE; +import static org.thingsboard.server.common.data.ObjectType.ASSET; +import static org.thingsboard.server.common.data.ObjectType.ASSET_PROFILE; +import static org.thingsboard.server.common.data.ObjectType.ATTRIBUTE_KV; +import static org.thingsboard.server.common.data.ObjectType.BLOB_ENTITY; +import static org.thingsboard.server.common.data.ObjectType.CONVERTER; +import static org.thingsboard.server.common.data.ObjectType.CUSTOMER; +import static org.thingsboard.server.common.data.ObjectType.DASHBOARD; +import static org.thingsboard.server.common.data.ObjectType.DEVICE; +import static org.thingsboard.server.common.data.ObjectType.DEVICE_PROFILE; +import static org.thingsboard.server.common.data.ObjectType.EDGE; +import static org.thingsboard.server.common.data.ObjectType.ENTITY_GROUP; +import static org.thingsboard.server.common.data.ObjectType.ENTITY_VIEW; +import static org.thingsboard.server.common.data.ObjectType.INTEGRATION; +import static org.thingsboard.server.common.data.ObjectType.LATEST_TS_KV; +import static org.thingsboard.server.common.data.ObjectType.QUEUE_STATS; +import static org.thingsboard.server.common.data.ObjectType.RELATION; +import static org.thingsboard.server.common.data.ObjectType.ROLE; +import static org.thingsboard.server.common.data.ObjectType.RULE_CHAIN; +import static org.thingsboard.server.common.data.ObjectType.SCHEDULER_EVENT; +import static org.thingsboard.server.common.data.ObjectType.TENANT; +import static org.thingsboard.server.common.data.ObjectType.TENANT_PROFILE; +import static org.thingsboard.server.common.data.ObjectType.USER; +import static org.thingsboard.server.common.data.ObjectType.WIDGETS_BUNDLE; +import static org.thingsboard.server.common.data.ObjectType.WIDGET_TYPE; + +@Slf4j +public abstract class EdqsSyncService { + + @Autowired + private EntityDaoRegistry entityDaoRegistry; + @Autowired + private TenantDao tenantDao; + @Autowired + private AttributesDao attributesDao; + @Autowired + private KeyDictionaryDao keyDictionaryDao; + @Autowired + private RelationDao relationDao; + @Autowired + private EntityGroupDao entityGroupDao; + @Autowired + private TimeseriesLatestDao timeseriesLatestDao; + @Autowired + @Lazy + private DefaultEdqsService edqsService; + + private final ConcurrentHashMap entityInfoMap = new ConcurrentHashMap<>(); + private final ConcurrentHashMap keys = new ConcurrentHashMap<>(); + + private final Map counters = new ConcurrentHashMap<>(); + + public static final Set edqsTenantTypes = EnumSet.of( + TENANT_PROFILE, CUSTOMER, DEVICE_PROFILE, DEVICE, ASSET_PROFILE, ASSET, EDGE, ENTITY_VIEW, USER, DASHBOARD, + RULE_CHAIN, WIDGET_TYPE, WIDGETS_BUNDLE, CONVERTER, INTEGRATION, SCHEDULER_EVENT, ROLE, + BLOB_ENTITY, API_USAGE_STATE, QUEUE_STATS + ); + + public abstract boolean isSyncNeeded(); + + public void sync() { + log.info("Synchronizing data to EDQS"); + long startTs = System.currentTimeMillis(); + counters.clear(); + + syncTenants(); + syncTenantEntities(); + syncEntityGroups(); + syncRelations(); + loadKeyDictionary(); + syncAttributes(); + syncLatestTimeseries(); + + counters.clear(); + log.info("Finishing synchronizing data to EDQS in {} ms", (System.currentTimeMillis() - startTs)); + } + + private void process(TenantId tenantId, ObjectType type, EdqsObject object) { + AtomicInteger counter = counters.computeIfAbsent(type, t -> new AtomicInteger()); + if (counter.incrementAndGet() % 10000 == 0) { + log.info("Processed {} {} objects", counter.get(), type); + } + edqsService.processEvent(tenantId, type, EdqsEventType.UPDATED, object); + } + + private void syncTenants() { + log.info("Synchronizing tenants to EDQS"); + long ts = System.currentTimeMillis(); + var tenants = new PageDataIterable<>(tenantDao::findAllFields, 10000); + for (EntityFields entityFields : tenants) { + TenantId tenantId = TenantId.fromUUID(entityFields.getId()); + entityInfoMap.put(entityFields.getId(), new EntityIdInfo(EntityType.TENANT, tenantId)); + process(tenantId, TENANT, new Entity(EntityType.TENANT, entityFields)); + } + process(TenantId.SYS_TENANT_ID, TENANT, new Entity(EntityType.TENANT, new TenantFields(TenantId.SYS_TENANT_ID.getId(), Long.MAX_VALUE))); + log.info("Finished synchronizing tenants to EDQS in {} ms", (System.currentTimeMillis() - ts)); + } + + private void syncTenantEntities() { + for (ObjectType type : edqsTenantTypes) { + log.info("Synchronizing tenant {} entities to EDQS", type); + long ts = System.currentTimeMillis(); + EntityType entityType = type.toEntityType(); + Dao dao = entityDaoRegistry.getDao(entityType); + var entities = new PageDataIterable<>(dao::findAllFields, 10000); + for (EntityFields entityFields : entities) { + TenantId tenantId = TenantId.fromUUID(entityFields.getTenantId()); + entityInfoMap.put(entityFields.getId(), new EntityIdInfo(entityType, tenantId)); + process(tenantId, type, new Entity(type.toEntityType(), entityFields)); + } + log.info("Finished synchronizing tenant {} entities to EDQS in {} ms", type, (System.currentTimeMillis() - ts)); + } + } + + private void syncEntityGroups() { + log.info("Synchronizing entity groups to EDQS"); + long ts = System.currentTimeMillis(); + var entityGroups = new PageDataIterable<>(entityGroupDao::findAllFields, 10000); + for (EntityFields groupFields : entityGroups) { + EntityIdInfo entityIdInfo = entityInfoMap.get(groupFields.getOwnerId()); + if (entityIdInfo != null) { + entityInfoMap.put(groupFields.getId(), new EntityIdInfo(EntityType.ENTITY_GROUP, entityIdInfo.tenantId())); + process(entityIdInfo.tenantId(), ENTITY_GROUP, new Entity(EntityType.ENTITY_GROUP, groupFields)); + } else { + log.info("Entity group owner not found: " + groupFields.getOwnerId()); + } + } + log.info("Finished synchronizing entity groups to EDQS in {} ms", (System.currentTimeMillis() - ts)); + } + + private void syncRelations() { + log.info("Synchronizing relations to EDQS"); + long ts = System.currentTimeMillis(); + var relations = new PageDataIterable<>(relationDao::findAll, 10000); + for (EntityRelation relation : relations) { + if (relation.getTypeGroup() == RelationTypeGroup.COMMON || relation.getTypeGroup() == RelationTypeGroup.FROM_ENTITY_GROUP) { + EntityIdInfo entityIdInfo = entityInfoMap.get(relation.getFrom().getId()); + if (entityIdInfo != null) { + process(entityIdInfo.tenantId(), RELATION, relation); + } else { + log.info("Relation from entity not found: " + relation.getFrom()); + } + } + } + log.info("Finished synchronizing relations to EDQS in {} ms", (System.currentTimeMillis() - ts)); + } + + private void loadKeyDictionary() { + log.info("Loading key dictionary"); + long ts = System.currentTimeMillis(); + var keyDictionaryEntries = new PageDataIterable<>(keyDictionaryDao::findAll, 10000); + for (KeyDictionaryEntry keyDictionaryEntry : keyDictionaryEntries) { + keys.put(keyDictionaryEntry.getKeyId(), keyDictionaryEntry.getKey()); + } + log.info("Finished loading key dictionary in {} ms", (System.currentTimeMillis() - ts)); + } + + private void syncAttributes() { + log.info("Synchronizing attributes to EDQS"); + long ts = System.currentTimeMillis(); + var attributes = new PageDataIterable<>(attributesDao::findAll, 10000); + for (AttributeKvEntity attribute : attributes) { + attribute.setStrKey(getStrKeyOrFetchFromDb(attribute.getId().getAttributeKey())); + UUID entityId = attribute.getId().getEntityId(); + EntityIdInfo entityIdInfo = entityInfoMap.get(entityId); + if (entityIdInfo == null) { + log.debug("Skipping attribute with entity UUID {} as it is not found in entityInfoMap", entityId); + continue; + } + AttributeKv attributeKv = new AttributeKv( + EntityIdFactory.getByTypeAndUuid(entityIdInfo.entityType(), entityId), + AttributeScope.valueOf(attribute.getId().getAttributeType()), + attribute.toData(), + attribute.getVersion()); + process(entityIdInfo.tenantId(), ATTRIBUTE_KV, attributeKv); + } + log.info("Finished synchronizing attributes to EDQS in {} ms", (System.currentTimeMillis() - ts)); + } + + private void syncLatestTimeseries() { + log.info("Synchronizing latest timeseries to EDQS"); + long ts = System.currentTimeMillis(); + var tsKvLatestEntities = new PageDataIterable<>(pageLink -> timeseriesLatestDao.findAllLatest(pageLink), 10000); + for (TsKvLatestEntity tsKvLatestEntity : tsKvLatestEntities) { + try { + String strKey = getStrKeyOrFetchFromDb(tsKvLatestEntity.getKey()); + if (strKey == null) { + log.debug("Skipping latest timeseries with key {} as it is not found in key dictionary", tsKvLatestEntity.getKey()); + continue; + } + tsKvLatestEntity.setStrKey(strKey); + UUID entityUuid = tsKvLatestEntity.getEntityId(); + EntityIdInfo entityIdInfo = entityInfoMap.get(entityUuid); + if (entityIdInfo != null) { + EntityId entityId = EntityIdFactory.getByTypeAndUuid(entityIdInfo.entityType(), entityUuid); + LatestTsKv latestTsKv = new LatestTsKv(entityId, tsKvLatestEntity.toData(), tsKvLatestEntity.getVersion()); + process(entityIdInfo.tenantId(), LATEST_TS_KV, latestTsKv); + } + } catch (Exception e) { + log.error("Failed to sync latest timeseries: {}", tsKvLatestEntity, e); + } + } + log.info("Finished synchronizing latest timeseries to EDQS in {} ms", (System.currentTimeMillis() - ts)); + } + + private String getStrKeyOrFetchFromDb(int key) { + String strKey = keys.get(key); + if (strKey != null) { + return strKey; + } else { + strKey = keyDictionaryDao.getKey(key); + keys.putIfAbsent(key, strKey); + } + return strKey; + } + + public record EntityIdInfo(EntityType entityType, TenantId tenantId) {} + +} diff --git a/application/src/main/java/org/thingsboard/server/service/edqs/KafkaEdqsSyncService.java b/application/src/main/java/org/thingsboard/server/service/edqs/KafkaEdqsSyncService.java new file mode 100644 index 0000000000..d51d87ed01 --- /dev/null +++ b/application/src/main/java/org/thingsboard/server/service/edqs/KafkaEdqsSyncService.java @@ -0,0 +1,47 @@ +/** + * Copyright © 2016-2024 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.service.edqs; + +import jakarta.annotation.PostConstruct; +import lombok.RequiredArgsConstructor; +import org.springframework.boot.autoconfigure.condition.ConditionalOnExpression; +import org.springframework.stereotype.Service; +import org.thingsboard.server.queue.edqs.EdqsQueue; +import org.thingsboard.server.queue.kafka.TbKafkaAdmin; +import org.thingsboard.server.queue.kafka.TbKafkaSettings; + +import java.util.Collections; + +@Service +@RequiredArgsConstructor +@ConditionalOnExpression("'${queue.edqs.sync_enabled:true}' == 'true' && '${queue.type:null}' == 'kafka'") +public class KafkaEdqsSyncService extends EdqsSyncService { + + private final TbKafkaSettings kafkaSettings; + private TbKafkaAdmin kafkaAdmin; + + @PostConstruct + private void init() { + kafkaAdmin = new TbKafkaAdmin(kafkaSettings, Collections.emptyMap()); + } + + @Override + public boolean isSyncNeeded() { + return kafkaAdmin.isTopicEmpty(EdqsQueue.STATE.getTopic()); + } + + +} diff --git a/application/src/main/java/org/thingsboard/server/service/edqs/LocalEdqsSyncService.java b/application/src/main/java/org/thingsboard/server/service/edqs/LocalEdqsSyncService.java new file mode 100644 index 0000000000..924bf94830 --- /dev/null +++ b/application/src/main/java/org/thingsboard/server/service/edqs/LocalEdqsSyncService.java @@ -0,0 +1,35 @@ +/** + * Copyright © 2016-2024 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.service.edqs; + +import lombok.RequiredArgsConstructor; +import org.springframework.boot.autoconfigure.condition.ConditionalOnExpression; +import org.springframework.stereotype.Service; +import org.thingsboard.server.edqs.util.EdqsRocksDb; + +@Service +@RequiredArgsConstructor +@ConditionalOnExpression("'${queue.edqs.sync_enabled:true}' == 'true' && '${queue.type:null}' == 'in-memory'") +public class LocalEdqsSyncService extends EdqsSyncService { + + private final EdqsRocksDb db; + + @Override + public boolean isSyncNeeded() { + return db.isNew(); + } + +} diff --git a/application/src/main/java/org/thingsboard/server/service/entitiy/EntityStateSourcingListener.java b/application/src/main/java/org/thingsboard/server/service/entitiy/EntityStateSourcingListener.java index 80368c085f..3cc999df29 100644 --- a/application/src/main/java/org/thingsboard/server/service/entitiy/EntityStateSourcingListener.java +++ b/application/src/main/java/org/thingsboard/server/service/entitiy/EntityStateSourcingListener.java @@ -72,6 +72,11 @@ public class EntityStateSourcingListener { @TransactionalEventListener(fallbackExecution = true) public void handleEvent(SaveEntityEvent event) { + if (Boolean.FALSE.equals(event.getBroadcastEvent())) { + log.trace("Ignoring event {}", event); + return; + } + TenantId tenantId = event.getTenantId(); EntityId entityId = event.getEntityId(); if (entityId == null) { diff --git a/application/src/main/java/org/thingsboard/server/service/queue/DefaultTbCoreConsumerService.java b/application/src/main/java/org/thingsboard/server/service/queue/DefaultTbCoreConsumerService.java index 3aaa36b068..1b4bfe7c71 100644 --- a/application/src/main/java/org/thingsboard/server/service/queue/DefaultTbCoreConsumerService.java +++ b/application/src/main/java/org/thingsboard/server/service/queue/DefaultTbCoreConsumerService.java @@ -20,7 +20,6 @@ import com.google.common.util.concurrent.ListeningExecutorService; import com.google.common.util.concurrent.MoreExecutors; import jakarta.annotation.PostConstruct; import jakarta.annotation.PreDestroy; -import lombok.Data; import lombok.Getter; import lombok.Setter; import lombok.extern.slf4j.Slf4j; @@ -35,6 +34,7 @@ import org.thingsboard.common.util.ThingsBoardThreadFactory; import org.thingsboard.server.actors.ActorSystemContext; import org.thingsboard.server.common.data.JavaSerDesUtil; import org.thingsboard.server.common.data.alarm.AlarmInfo; +import org.thingsboard.server.common.data.edqs.ToCoreEdqsMsg; import org.thingsboard.server.common.data.event.ErrorEvent; import org.thingsboard.server.common.data.event.Event; import org.thingsboard.server.common.data.event.LifecycleEvent; @@ -47,6 +47,7 @@ import org.thingsboard.server.common.data.queue.QueueConfig; import org.thingsboard.server.common.data.rpc.RpcError; import org.thingsboard.server.common.msg.MsgType; import org.thingsboard.server.common.msg.TbActorMsg; +import org.thingsboard.server.common.msg.edqs.EdqsService; import org.thingsboard.server.common.msg.notification.NotificationRuleProcessor; import org.thingsboard.server.common.msg.queue.ServiceType; import org.thingsboard.server.common.msg.queue.TbCallback; @@ -78,6 +79,7 @@ import org.thingsboard.server.gen.transport.TransportProtos.ToUsageStatsServiceM import org.thingsboard.server.gen.transport.TransportProtos.TransportToDeviceActorMsg; import org.thingsboard.server.queue.TbQueueConsumer; import org.thingsboard.server.queue.common.TbProtoQueueMsg; +import org.thingsboard.server.queue.common.consumer.MainQueueConsumerManager; import org.thingsboard.server.queue.common.consumer.QueueConsumerManager; import org.thingsboard.server.queue.discovery.PartitionService; import org.thingsboard.server.queue.discovery.QueueKey; @@ -89,7 +91,6 @@ import org.thingsboard.server.service.notification.NotificationSchedulerService; import org.thingsboard.server.service.ota.OtaPackageStateService; import org.thingsboard.server.service.profile.TbAssetProfileCache; import org.thingsboard.server.service.profile.TbDeviceProfileCache; -import org.thingsboard.server.service.queue.consumer.MainQueueConsumerManager; import org.thingsboard.server.service.queue.processing.AbstractConsumerService; import org.thingsboard.server.service.queue.processing.IdMsgPair; import org.thingsboard.server.service.resource.TbImageService; @@ -147,9 +148,10 @@ public class DefaultTbCoreConsumerService extends AbstractConsumerService, CoreQueueConfig> mainConsumer; + private MainQueueConsumerManager, QueueConfig> mainConsumer; private QueueConsumerManager> usageStatsConsumer; private QueueConsumerManager> firmwareStatesConsumer; @@ -175,7 +177,8 @@ public class DefaultTbCoreConsumerService extends AbstractConsumerService, CoreQueueConfig>builder() + this.mainConsumer = MainQueueConsumerManager., QueueConfig>builder() .queueKey(new QueueKey(ServiceType.TB_CORE)) - .config(CoreQueueConfig.of(consumerPerPartition, (int) pollInterval)) + .config(QueueConfig.of(consumerPerPartition, pollInterval)) .msgPackProcessor(this::processMsgs) .consumerCreator((config, partitionId) -> queueFactory.createToCoreMsgConsumer()) .consumerExecutor(consumersExecutor) @@ -251,7 +255,7 @@ public class DefaultTbCoreConsumerService extends AbstractConsumerService> msgs, TbQueueConsumer> consumer, CoreQueueConfig config) throws Exception { + private void processMsgs(List> msgs, TbQueueConsumer> consumer, QueueConfig config) throws Exception { List> orderedMsgList = msgs.stream().map(msg -> new IdMsgPair<>(UUID.randomUUID(), msg)).toList(); ConcurrentMap> pendingMap = orderedMsgList.stream().collect( Collectors.toConcurrentMap(IdMsgPair::getUuid, IdMsgPair::getMsg)); @@ -396,6 +400,9 @@ public class DefaultTbCoreConsumerService extends AbstractConsumerService, EdgeQueueConfig> mainConsumer; + private MainQueueConsumerManager, QueueConfig> mainConsumer; public DefaultTbEdgeConsumerService(TbCoreQueueFactory tbCoreQueueFactory, ActorSystemContext actorContext, StatsFactory statsFactory, EdgeContextComponent edgeCtx) { @@ -102,9 +102,9 @@ public class DefaultTbEdgeConsumerService extends AbstractConsumerService, EdgeQueueConfig>builder() + this.mainConsumer = MainQueueConsumerManager., QueueConfig>builder() .queueKey(new QueueKey(ServiceType.TB_CORE).withQueueName(DataConstants.EDGE_QUEUE_NAME)) - .config(EdgeQueueConfig.of(consumerPerPartition, pollInterval)) + .config(QueueConfig.of(consumerPerPartition, pollInterval)) .msgPackProcessor(this::processMsgs) .consumerCreator((config, partitionId) -> queueFactory.createEdgeMsgConsumer()) .consumerExecutor(consumersExecutor) @@ -130,7 +130,7 @@ public class DefaultTbEdgeConsumerService extends AbstractConsumerService> msgs, TbQueueConsumer> consumer, EdgeQueueConfig edgeQueueConfig) throws InterruptedException { + private void processMsgs(List> msgs, TbQueueConsumer> consumer, QueueConfig edgeQueueConfig) throws InterruptedException { List> orderedMsgList = msgs.stream().map(msg -> new IdMsgPair<>(UUID.randomUUID(), msg)).toList(); ConcurrentMap> pendingMap = orderedMsgList.stream().collect( Collectors.toConcurrentMap(IdMsgPair::getUuid, IdMsgPair::getMsg)); @@ -323,10 +323,4 @@ public class DefaultTbEdgeConsumerService extends AbstractConsumerService { + event.getNewPartitions().forEach((queueKey, partitions) -> { if (partitionService.isManagedByCurrentService(queueKey.getTenantId())) { var consumer = getConsumer(queueKey).orElseGet(() -> { Queue config = queueService.findQueueByTenantIdAndName(queueKey.getTenantId(), queueKey.getQueueName()); diff --git a/application/src/main/java/org/thingsboard/server/service/queue/ruleengine/TbRuleEngineQueueConsumerManager.java b/application/src/main/java/org/thingsboard/server/service/queue/ruleengine/TbRuleEngineQueueConsumerManager.java index c2823d3c00..3636eb05af 100644 --- a/application/src/main/java/org/thingsboard/server/service/queue/ruleengine/TbRuleEngineQueueConsumerManager.java +++ b/application/src/main/java/org/thingsboard/server/service/queue/ruleengine/TbRuleEngineQueueConsumerManager.java @@ -33,11 +33,14 @@ import org.thingsboard.server.common.msg.queue.TopicPartitionInfo; import org.thingsboard.server.gen.transport.TransportProtos.ToRuleEngineMsg; import org.thingsboard.server.queue.TbQueueConsumer; import org.thingsboard.server.queue.common.TbProtoQueueMsg; +import org.thingsboard.server.queue.common.consumer.QueueEvent; +import org.thingsboard.server.queue.common.consumer.TbQueueConsumerManagerTask; +import org.thingsboard.server.queue.common.consumer.TbQueueConsumerTask; import org.thingsboard.server.queue.discovery.QueueKey; import org.thingsboard.server.service.queue.TbMsgPackCallback; import org.thingsboard.server.service.queue.TbMsgPackProcessingContext; import org.thingsboard.server.service.queue.TbRuleEngineConsumerStats; -import org.thingsboard.server.service.queue.consumer.MainQueueConsumerManager; +import org.thingsboard.server.queue.common.consumer.MainQueueConsumerManager; import org.thingsboard.server.service.queue.processing.TbRuleEngineProcessingDecision; import org.thingsboard.server.service.queue.processing.TbRuleEngineProcessingResult; import org.thingsboard.server.service.queue.processing.TbRuleEngineProcessingStrategy; diff --git a/application/src/main/java/org/thingsboard/server/service/state/DefaultDeviceStateService.java b/application/src/main/java/org/thingsboard/server/service/state/DefaultDeviceStateService.java index c58cb4fd5a..d3d0421297 100644 --- a/application/src/main/java/org/thingsboard/server/service/state/DefaultDeviceStateService.java +++ b/application/src/main/java/org/thingsboard/server/service/state/DefaultDeviceStateService.java @@ -251,7 +251,7 @@ public class DefaultDeviceStateService extends AbstractPartitionBasedService= deviceState.getLastActivityTime()) { deviceState.setLastInactivityAlarmTime(0L); - save(deviceId, INACTIVITY_ALARM_TIME, 0L); + save(state.getTenantId(), deviceId, INACTIVITY_ALARM_TIME, 0L); } } } @@ -583,7 +583,7 @@ public class DefaultDeviceStateService extends AbstractPartitionBasedService>> customExporters; + private Map relatedEntitiesExporters; + + private static final Set RELATED = EnumSet.of(EVENT, RELATION, ATTRIBUTE_KV, LATEST_TS_KV); + + @PostConstruct + private void init() { + relatedEntitiesExporters = Map.of( + RELATION, this::exportRelations, + EVENT, this::exportEvents, // todo: query by tenant + ATTRIBUTE_KV, this::exportAttributes, + LATEST_TS_KV, this::exportLatestTelemetry + ); + customExporters = Map.of( + AUDIT_LOG, this::exportAuditLogs + ); + } + + public void exportTenant(TenantId tenantId, ExportConfig config, BiConsumer processor) { + log.info("[{}] Exporting tenant", tenantId); + Tenant tenant = tenantDao.findById(TenantId.SYS_TENANT_ID, tenantId.getId()); + if (tenant == null) { + throw new IllegalArgumentException("Tenant with id " + tenantId + " not found"); + } + + Set objectTypes = config.getIncludedObjectTypes(); + if (objectTypes.contains(TENANT)) { + exportEntity(tenantId, TENANT, tenant, config, processor); + } + + for (ObjectType type : objectTypes) { + if (RELATED.contains(type) || type == TENANT) { + continue; + } + log.debug("[{}] Exporting {} entities", tenantId, type); + if (!customExporters.containsKey(type)) { + TenantEntityDao dao = entityDaoRegistry.getTenantEntityDao(type); + var entities = new PageDataIterable<>(pageLink -> dao.findAllByTenantId(tenantId, pageLink), 100); + for (Object entity : entities) { + exportEntity(tenantId, type, entity, config, processor); + } + } else { + customExporters.get(type).accept(tenantId, processor); + } + } + } + + private void exportEntity(TenantId tenantId, ObjectType type, Object entity, ExportConfig config, BiConsumer processor) { + processor.accept(type, entity); + if (entity instanceof HasId hasId && hasId.getId() instanceof EntityId entityId) { + relatedEntitiesExporters.forEach((relatedEntityType, exporter) -> { + if (config.getIncludedObjectTypes().contains(relatedEntityType)) { + exporter.export(tenantId, entityId, processor); + } + }); + } + } + + private Map getPartitions(String table) { + List partitionsStartTime = partitioningRepository.fetchPartitions(table).stream().sorted().toList(); + if (partitionsStartTime.isEmpty()) { + return Collections.emptyMap(); + } + + Map partitions = new HashMap<>(); + for (int i = 0; i < partitionsStartTime.size(); i++) { + Long startTime = partitionsStartTime.get(i); + Long endTime; + if (partitionsStartTime.size() - 1 == i) { + endTime = System.currentTimeMillis(); + } else { + endTime = partitionsStartTime.get(i + 1) - 1; + } + partitions.put(startTime, endTime); + } + return partitions; + } + + private void exportAuditLogs(TenantId tenantId, BiConsumer processor) { + Map partitions = getPartitions(ModelConstants.AUDIT_LOG_TABLE_NAME); + partitions.forEach((startTime, endTime) -> { + PageDataIterable auditLogs = new PageDataIterable<>(pageLink -> { + return auditLogDao.findAuditLogsByTenantId(tenantId.getId(), null, new TimePageLink(pageLink, startTime, endTime)); + }, 512); + for (AuditLog auditLog : auditLogs) { + processor.accept(AUDIT_LOG, auditLog); + } + }); + } + + private void exportAttributes(TenantId tenantId, EntityId entityId, BiConsumer processor) { + for (AttributeScope attributeScope : AttributeScope.values()) { + List attributes = attributesDao.findAll(tenantId, entityId, attributeScope); + for (AttributeKvEntry entry : attributes) { + AttributeKv attributeKv = new AttributeKv(entityId, attributeScope, entry, entry.getVersion()); + processor.accept(ATTRIBUTE_KV, attributeKv); + } + } + } + + private void exportRelations(TenantId tenantId, EntityId entityId, BiConsumer processor) { + List relations = relationDao.findAllByFrom(tenantId, entityId); + for (EntityRelation relation : relations) { + processor.accept(RELATION, relation); + } + } + + @SneakyThrows + private void exportLatestTelemetry(TenantId tenantId, EntityId entityId, BiConsumer processor) { + List latestTelemetry = timeseriesLatestDao.findAllLatest(tenantId, entityId).get(30, TimeUnit.SECONDS); + for (TsKvEntry tsKvEntry : latestTelemetry) { + LatestTsKv latestTsKv = new LatestTsKv(entityId, tsKvEntry, tsKvEntry.getVersion()); + processor.accept(LATEST_TS_KV, latestTsKv); + } + } + + private void exportEvents(TenantId tenantId, EntityId entityId, BiConsumer processor) { + for (EventType eventType : EventType.values()) { + Map partitions = getPartitions(eventType.getTable()); + partitions.forEach((startTime, endTime) -> { + PageDataIterable events = new PageDataIterable<>(pageLink -> { + return eventDao.findEvents(tenantId.getId(), entityId.getId(), eventType, new TimePageLink(pageLink, startTime, endTime)); + }, 512); + for (Event event : events) { + processor.accept(EVENT, event); + } + }); + } + } + + private interface Exporter { + + void export(TenantId tenantId, EntityId entityId, BiConsumer processor); + + } + + @Data + public static class ExportConfig { + + private Set includedObjectTypes; + + } + +} diff --git a/application/src/main/java/org/thingsboard/server/service/transport/TbCoreTransportApiService.java b/application/src/main/java/org/thingsboard/server/service/transport/TbCoreTransportApiService.java index b38fbf4fec..c0360ede8b 100644 --- a/application/src/main/java/org/thingsboard/server/service/transport/TbCoreTransportApiService.java +++ b/application/src/main/java/org/thingsboard/server/service/transport/TbCoreTransportApiService.java @@ -93,7 +93,8 @@ public class TbCoreTransportApiService { @AfterStartUp(order = AfterStartUp.REGULAR_SERVICE) public void onApplicationEvent(ApplicationReadyEvent applicationReadyEvent) { log.info("Received application ready event. Starting polling for events."); - transportApiTemplate.init(transportApiService); + transportApiTemplate.subscribe(); + transportApiTemplate.launch(transportApiService); } @PreDestroy 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 34fd4a426f..2bd39b8c09 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 @@ -229,6 +229,7 @@ public class DefaultWebSocketService implements WebSocketService { } catch (TbRateLimitsException e) { log.debug("{} Failed to handle WS cmd: {}", sessionRef, cmd, e); } catch (Exception e) { + sendError(sessionRef, cmd.getCmdId(), SubscriptionErrorCode.INTERNAL_ERROR, e.getMessage()); log.error("{} Failed to handle WS cmd: {}", sessionRef, cmd, e); } } diff --git a/application/src/main/resources/thingsboard.yml b/application/src/main/resources/thingsboard.yml index f29cc55197..9b182ce3a7 100644 --- a/application/src/main/resources/thingsboard.yml +++ b/application/src/main/resources/thingsboard.yml @@ -1615,6 +1615,12 @@ queue: edge: "${TB_QUEUE_KAFKA_EDGE_TOPIC_PROPERTIES:retention.ms:604800000;segment.bytes:52428800;retention.bytes:1048576000;partitions:1;min.insync.replicas:1}" # Kafka properties for Edge event topic edge-event: "${TB_QUEUE_KAFKA_EDGE_EVENT_TOPIC_PROPERTIES:retention.ms:2592000000;segment.bytes:52428800;retention.bytes:1048576000;partitions:1;min.insync.replicas:1}" + # Kafka properties for EDQS events topics. Partitions number must be the same as queue.edqs.partitions + edqs-events: "${TB_QUEUE_KAFKA_EDQS_EVENTS_TOPIC_PROPERTIES:retention.ms:604800000;segment.bytes:52428800;retention.bytes:1048576000;partitions:12;min.insync.replicas:1}" + # Kafka properties for EDQS requests topic (default: 3 minutes retention). Partitions number must be the same as queue.edqs.partitions + edqs-requests: "${TB_QUEUE_KAFKA_EDQS_REQUESTS_TOPIC_PROPERTIES:retention.ms:180000;segment.bytes:52428800;retention.bytes:1048576000;partitions:12;min.insync.replicas:1}" + # Kafka properties for EDQS state topic (infinite retention, compaction). Partitions number must be the same as queue.edqs.partitions + edqs-state: "${TB_QUEUE_KAFKA_EDQS_LATEST_EVENTS_TOPIC_PROPERTIES:retention.ms:-1;segment.bytes:52428800;retention.bytes:1048576000;partitions:12;min.insync.replicas:1;cleanup.policy:compact}" consumer-stats: # Prints lag between consumer group offset and last messages offset in Kafka topics enabled: "${TB_QUEUE_KAFKA_CONSUMER_STATS_ENABLED:true}" @@ -1688,6 +1694,23 @@ queue: enabled: "${TB_HOUSEKEEPER_STATS_ENABLED:true}" # Statistics printing interval for Housekeeper print-interval-ms: "${TB_HOUSEKEEPER_STATS_PRINT_INTERVAL_MS:60000}" + edqs: + sync_enabled: "${TB_EDQS_SYNC_ENABLED:true}" # FIXME: disable by default before release + api_enabled: "${TB_EDQS_API_ENABLED:true}" # FIXME: disable by default before release + mode: "${TB_EDQS_MODE:local}" # local or remote + local: + rocksdb_path: "${TB_EDQS_ROCKSDB_PATH:/tmp/edqs-backup}" + partitions: "${TB_EDQS_PARTITIONS:12}" + requests_topic: "${TB_EDQS_REQUESTS_TOPIC:edqs.requests}" + responses_topic: "${TB_EDQS_RESPONSES_TOPIC:edqs.responses}" + poll_interval: "${TB_EDQS_POLL_INTERVAL_MS:125}" + max_pending_requests: "${TB_EDQS_MAX_PENDING_REQUESTS:10000}" + max_request_timeout: "${TB_EDQS_MAX_REQUEST_TIMEOUT:10000}" + stats: + # Enable/disable statistics for EDQS service + enabled: "${TB_EDQS_STATS_ENABLED:true}" + # Statistics printing interval for EDQS + print-interval-ms: "${TB_EDQS_STATS_PRINT_INTERVAL_MS:60000}" vc: # Default topic name diff --git a/application/src/test/java/org/thingsboard/server/controller/EdqsEntityQueryControllerTest.java b/application/src/test/java/org/thingsboard/server/controller/EdqsEntityQueryControllerTest.java new file mode 100644 index 0000000000..daa1c92c1c --- /dev/null +++ b/application/src/test/java/org/thingsboard/server/controller/EdqsEntityQueryControllerTest.java @@ -0,0 +1,67 @@ +/** + * Copyright © 2016-2024 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.controller; + +import org.junit.Before; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.mock.mockito.MockBean; +import org.springframework.test.context.TestPropertySource; +import org.thingsboard.server.common.data.page.PageData; +import org.thingsboard.server.common.data.query.EntityCountQuery; +import org.thingsboard.server.common.data.query.EntityData; +import org.thingsboard.server.common.data.query.EntityDataQuery; +import org.thingsboard.server.common.msg.edqs.EdqsService; +import org.thingsboard.server.dao.service.DaoSqlTest; +import org.thingsboard.server.edqs.util.EdqsRocksDb; + +import java.util.concurrent.TimeUnit; + +import static org.awaitility.Awaitility.await; + +@DaoSqlTest +@TestPropertySource(properties = { + "queue.type=kafka", // uncomment to use Kafka + "queue.kafka.bootstrap.servers=10.7.1.254:9092", + "queue.edqs.sync_enabled=true", + "queue.edqs.api_enabled=true", + "queue.edqs.mode=local" +}) +public class EdqsEntityQueryControllerTest extends EntityQueryControllerTest { + + @Autowired + private EdqsService edqsService; + + @MockBean + private EdqsRocksDb edqsRocksDb; + + @Before + public void before() { + await().atMost(TIMEOUT, TimeUnit.SECONDS).until(() -> edqsService.isApiEnabled()); + } + + @Override + protected PageData findByQueryAndCheck(EntityDataQuery query, int expectedResultSize) { + return await().atMost(TIMEOUT, TimeUnit.SECONDS).until(() -> findByQuery(query), + result -> result.getTotalElements() == expectedResultSize); + } + + @Override + protected Long countByQueryAndCheck(EntityCountQuery query, long expectedResult) { + return await().atMost(TIMEOUT, TimeUnit.SECONDS).until(() -> countByQuery(query), + result -> result == expectedResult); + } + +} 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 5d04b16dfa..ad554f11ba 100644 --- a/application/src/test/java/org/thingsboard/server/controller/EntityQueryControllerTest.java +++ b/application/src/test/java/org/thingsboard/server/controller/EntityQueryControllerTest.java @@ -18,12 +18,14 @@ package org.thingsboard.server.controller; import com.fasterxml.jackson.core.type.TypeReference; import com.fasterxml.jackson.databind.JsonNode; import org.awaitility.Awaitility; +import com.fasterxml.jackson.databind.node.ObjectNode; import org.junit.After; import org.junit.Assert; import org.junit.Before; import org.junit.Test; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.test.web.servlet.ResultActions; +import org.testcontainers.shaded.org.apache.commons.lang3.RandomStringUtils; import org.thingsboard.common.util.JacksonUtil; import org.thingsboard.server.common.data.Customer; import org.thingsboard.server.common.data.Dashboard; @@ -49,6 +51,7 @@ import org.thingsboard.server.common.data.query.EntityDataQuery; import org.thingsboard.server.common.data.query.EntityDataSortOrder; import org.thingsboard.server.common.data.query.EntityKey; import org.thingsboard.server.common.data.query.EntityKeyType; +import org.thingsboard.server.common.data.query.EntityKeyValueType; import org.thingsboard.server.common.data.query.EntityListFilter; import org.thingsboard.server.common.data.query.EntityTypeFilter; import org.thingsboard.server.common.data.query.FilterPredicateValue; @@ -73,6 +76,7 @@ import java.util.concurrent.TimeUnit; import java.util.stream.Collectors; import static org.assertj.core.api.Assertions.assertThat; +import static org.awaitility.Awaitility.await; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; @DaoSqlTest @@ -130,36 +134,25 @@ public class EntityQueryControllerTest extends AbstractControllerTest { filter.setDeviceNameFilter(""); EntityCountQuery countQuery = new EntityCountQuery(filter); - - Long count = doPostWithResponse("/api/entitiesQuery/count", countQuery, Long.class); - Assert.assertEquals(97, count.longValue()); + countByQueryAndCheck(countQuery, 97); filter.setDeviceTypes(List.of("unknown")); - count = doPostWithResponse("/api/entitiesQuery/count", countQuery, Long.class); - Assert.assertEquals(0, count.longValue()); + countByQueryAndCheck(countQuery, 0); filter.setDeviceTypes(List.of("default")); filter.setDeviceNameFilter("Device1"); - - count = doPostWithResponse("/api/entitiesQuery/count", countQuery, Long.class); - Assert.assertEquals(11, count.longValue()); + countByQueryAndCheck(countQuery, 11); EntityListFilter entityListFilter = new EntityListFilter(); entityListFilter.setEntityType(EntityType.DEVICE); entityListFilter.setEntityList(devices.stream().map(Device::getId).map(DeviceId::toString).collect(Collectors.toList())); - countQuery = new EntityCountQuery(entityListFilter); - - count = doPostWithResponse("/api/entitiesQuery/count", countQuery, Long.class); - Assert.assertEquals(97, count.longValue()); + countByQueryAndCheck(countQuery, 97); EntityTypeFilter filter2 = new EntityTypeFilter(); filter2.setEntityType(EntityType.DEVICE); - - EntityCountQuery countQuery2 = new EntityCountQuery(filter2); - - Long count2 = doPostWithResponse("/api/entitiesQuery/count", countQuery2, Long.class); - Assert.assertEquals(97, count2.longValue()); + countQuery = new EntityCountQuery(filter2); + countByQueryAndCheck(countQuery, 97); } @Test @@ -169,14 +162,15 @@ public class EntityQueryControllerTest extends AbstractControllerTest { EntityTypeFilter allDeviceFilter = new EntityTypeFilter(); allDeviceFilter.setEntityType(EntityType.DEVICE); EntityCountQuery query = new EntityCountQuery(allDeviceFilter); - Long initialCount = doPostWithResponse("/api/entitiesQuery/count", query, Long.class); + Long initialCount = countByQuery(query); loginTenantAdmin(); List devices = new ArrayList<>(); + String devicePrefix = "Device" + RandomStringUtils.random(5); for (int i = 0; i < 97; i++) { Device device = new Device(); - device.setName("Device" + i); + device.setName(devicePrefix + i); device.setType("default"); device.setLabel("testLabel" + (int) (Math.random() * 1000)); devices.add(doPost("/api/device", device, Device.class)); @@ -189,31 +183,23 @@ public class EntityQueryControllerTest extends AbstractControllerTest { loginSysAdmin(); EntityCountQuery countQuery = new EntityCountQuery(filter); - - Long count = doPostWithResponse("/api/entitiesQuery/count", countQuery, Long.class); - Assert.assertEquals(97, count.longValue()); + countByQueryAndCheck(countQuery, initialCount + 97); filter.setDeviceType("unknown"); - count = doPostWithResponse("/api/entitiesQuery/count", countQuery, Long.class); - Assert.assertEquals(0, count.longValue()); + countByQueryAndCheck(countQuery, 0); filter.setDeviceType("default"); - filter.setDeviceNameFilter("Device1"); - - count = doPostWithResponse("/api/entitiesQuery/count", countQuery, Long.class); - Assert.assertEquals(11, count.longValue()); + filter.setDeviceNameFilter(devicePrefix + "1"); + countByQueryAndCheck(countQuery, 11); EntityListFilter entityListFilter = new EntityListFilter(); entityListFilter.setEntityType(EntityType.DEVICE); entityListFilter.setEntityList(devices.stream().map(Device::getId).map(DeviceId::toString).collect(Collectors.toList())); countQuery = new EntityCountQuery(entityListFilter); + countByQueryAndCheck(countQuery, 97); - count = doPostWithResponse("/api/entitiesQuery/count", countQuery, Long.class); - Assert.assertEquals(97, count.longValue()); - - Long count2 = doPostWithResponse("/api/entitiesQuery/count", query, Long.class); - Assert.assertEquals(initialCount + 97, count2.longValue()); + countByQueryAndCheck(query, initialCount + 97); } @Test @@ -371,11 +357,7 @@ public class EntityQueryControllerTest extends AbstractControllerTest { EntityDataQuery query = new EntityDataQuery(filter, pageLink, entityFields, null, null); - PageData data = - doPostWithTypedResponse("/api/entitiesQuery/find", query, new TypeReference>() { - }); - - Assert.assertEquals(97, data.getTotalElements()); + PageData data = findByQueryAndCheck(query, 97); Assert.assertEquals(10, data.getTotalPages()); Assert.assertTrue(data.hasNext()); Assert.assertEquals(10, data.getData().size()); @@ -383,8 +365,7 @@ public class EntityQueryControllerTest extends AbstractControllerTest { List loadedEntities = new ArrayList<>(data.getData()); while (data.hasNext()) { query = query.next(); - data = doPostWithTypedResponse("/api/entitiesQuery/find", query, new TypeReference>() { - }); + data = findByQuery(query); loadedEntities.addAll(data.getData()); } Assert.assertEquals(97, loadedEntities.size()); @@ -406,8 +387,7 @@ public class EntityQueryControllerTest extends AbstractControllerTest { pageLink = new EntityDataPageLink(10, 0, "device1", sortOrder); query = new EntityDataQuery(filter, pageLink, entityFields, null, null); - data = doPostWithTypedResponse("/api/entitiesQuery/find", query, new TypeReference>() { - }); + data = findByQuery(query); Assert.assertEquals(11, data.getTotalElements()); Assert.assertEquals("Device19", data.getData().get(0).getLatest().get(EntityKeyType.ENTITY_FIELD).get("name").getValue()); @@ -423,9 +403,7 @@ public class EntityQueryControllerTest extends AbstractControllerTest { EntityDataQuery query2 = new EntityDataQuery(filter2, pageLink2, entityFields2, null, null); - PageData data2 = - doPostWithTypedResponse("/api/entitiesQuery/find", query2, new TypeReference>() { - }); + PageData data2 = findByQuery(query2); Assert.assertEquals(97, data2.getTotalElements()); Assert.assertEquals(10, data2.getTotalPages()); @@ -473,20 +451,15 @@ public class EntityQueryControllerTest extends AbstractControllerTest { List entityFields = Collections.singletonList(new EntityKey(EntityKeyType.ENTITY_FIELD, "name")); EntityDataQuery query = new EntityDataQuery(filter, pageLink, entityFields, null, null); - - PageData data = doPostWithTypedResponse("/api/entitiesQuery/find", query, new TypeReference<>() {}); - - Assert.assertEquals(87, data.getTotalElements()); + findByQueryAndCheck(query, 87); filter.setFilters(List.of(new RelationEntityTypeFilter("NOT_CONTAINS", List.of(EntityType.DEVICE), false))); query = new EntityDataQuery(filter, pageLink, entityFields, null, null); - data = doPostWithTypedResponse("/api/entitiesQuery/find", query, new TypeReference<>() {}); - Assert.assertEquals(10, data.getTotalElements()); + findByQueryAndCheck(query, 10); filter.setFilters(List.of(new RelationEntityTypeFilter("NOT_CONTAINS", List.of(EntityType.DEVICE), true))); query = new EntityDataQuery(filter, pageLink, entityFields, null, null); - data = doPostWithTypedResponse("/api/entitiesQuery/find", query, new TypeReference<>() {}); - Assert.assertEquals(87, data.getTotalElements()); + findByQueryAndCheck(query, 87); } private EntityRelation createFromRelation(Device mainDevice, Device device, String relationType) { @@ -531,14 +504,12 @@ public class EntityQueryControllerTest extends AbstractControllerTest { List latestValues = Collections.singletonList(new EntityKey(EntityKeyType.ATTRIBUTE, "temperature")); EntityDataQuery query = new EntityDataQuery(filter, pageLink, entityFields, latestValues, null); - PageData data = doPostWithTypedResponse("/api/entitiesQuery/find", query, new TypeReference>() { - }); + PageData data = findByQueryAndCheck(query, 67); List loadedEntities = new ArrayList<>(data.getData()); while (data.hasNext()) { query = query.next(); - data = doPostWithTypedResponse("/api/entitiesQuery/find", query, new TypeReference>() { - }); + data = findByQuery(query); loadedEntities.addAll(data.getData()); } Assert.assertEquals(67, loadedEntities.size()); @@ -551,6 +522,7 @@ public class EntityQueryControllerTest extends AbstractControllerTest { pageLink = new EntityDataPageLink(10, 0, null, sortOrder); KeyFilter highTemperatureFilter = new KeyFilter(); highTemperatureFilter.setKey(new EntityKey(EntityKeyType.ATTRIBUTE, "temperature")); + highTemperatureFilter.setValueType(EntityKeyValueType.NUMERIC); NumericFilterPredicate predicate = new NumericFilterPredicate(); predicate.setValue(FilterPredicateValue.fromDouble(45)); predicate.setOperation(NumericFilterPredicate.NumericOperation.GREATER); @@ -559,13 +531,11 @@ public class EntityQueryControllerTest extends AbstractControllerTest { query = new EntityDataQuery(filter, pageLink, entityFields, latestValues, keyFilters); - data = doPostWithTypedResponse("/api/entitiesQuery/find", query, new TypeReference>() { - }); + data = findByQuery(query); loadedEntities = new ArrayList<>(data.getData()); while (data.hasNext()) { query = query.next(); - data = doPostWithTypedResponse("/api/entitiesQuery/find", query, new TypeReference>() { - }); + data = findByQuery(query); loadedEntities.addAll(data.getData()); } Assert.assertEquals(highTemperatures.size(), loadedEntities.size()); @@ -604,6 +574,7 @@ public class EntityQueryControllerTest extends AbstractControllerTest { KeyFilter highTemperatureFilter = new KeyFilter(); highTemperatureFilter.setKey(new EntityKey(EntityKeyType.SERVER_ATTRIBUTE, "alarmActiveTime")); + highTemperatureFilter.setValueType(EntityKeyValueType.NUMERIC); NumericFilterPredicate predicate = new NumericFilterPredicate(); DynamicValue dynamicValue = @@ -627,16 +598,16 @@ public class EntityQueryControllerTest extends AbstractControllerTest { EntityDataQuery query = new EntityDataQuery(filter, pageLink, entityFields, latestValues, keyFilters); - Awaitility.await() + await() .alias("data by query") .atMost(TIMEOUT, TimeUnit.SECONDS) .until(() -> { - var data = doPostWithTypedResponse("/api/entitiesQuery/find", query, new TypeReference>() {}); + var data = findByQuery(query); var loadedEntities = new ArrayList<>(data.getData()); return loadedEntities.size() == numOfDevices; }); - var data = doPostWithTypedResponse("/api/entitiesQuery/find", query, new TypeReference>() {}); + var data = findByQuery(query); var loadedEntities = new ArrayList<>(data.getData()); Assert.assertEquals(numOfDevices, loadedEntities.size()); @@ -694,11 +665,7 @@ public class EntityQueryControllerTest extends AbstractControllerTest { EntityDataQuery query = new EntityDataQuery(entityTypeFilter, pageLink, entityFields, null, null); - PageData data = - doPostWithTypedResponse("/api/entitiesQuery/find", query, new TypeReference>() { - }); - - Assert.assertEquals(97, data.getTotalElements()); + PageData data = findByQueryAndCheck(query, 97); Assert.assertEquals(10, data.getTotalPages()); Assert.assertTrue(data.hasNext()); Assert.assertEquals(10, data.getData().size()); @@ -712,9 +679,7 @@ public class EntityQueryControllerTest extends AbstractControllerTest { }); EntityCountQuery countQuery = new EntityCountQuery(entityTypeFilter); - - Long count = doPostWithResponse("/api/entitiesQuery/count", countQuery, Long.class); - Assert.assertEquals(97, count.longValue()); + countByQueryAndCheck(countQuery, 97); } @Test @@ -742,28 +707,28 @@ public class EntityQueryControllerTest extends AbstractControllerTest { KeyFilter activeAlarmTimeToLongFilter = getServerAttributeNumericGreaterThanKeyFilter("alarmActiveTime", 30); KeyFilter tenantOwnerNameFilter = getEntityFieldStringEqualToKeyFilter("ownerName", TEST_TENANT_NAME); KeyFilter wrongOwnerNameFilter = getEntityFieldStringEqualToKeyFilter("ownerName", "wrongName"); - KeyFilter tenantOwnerTypeFilter = getEntityFieldStringEqualToKeyFilter("ownerType", "TENANT"); + KeyFilter tenantOwnerTypeFilter = getEntityFieldStringEqualToKeyFilter("ownerType", "TENANT"); KeyFilter customerOwnerTypeFilter = getEntityFieldStringEqualToKeyFilter("ownerType", "CUSTOMER"); // all devices with ownerName = TEST TENANT - EntityCountQuery query = new EntityCountQuery(filter, List.of(activeAlarmTimeFilter, tenantOwnerNameFilter)); - checkEntitiesCount(query, numOfDevices); + EntityCountQuery query = new EntityCountQuery(filter, List.of(activeAlarmTimeFilter, tenantOwnerNameFilter)); + countByQueryAndCheck(query, numOfDevices); // all devices with ownerName = TEST TENANT - EntityCountQuery activeAlarmTimeToLongQuery = new EntityCountQuery(filter, List.of(activeAlarmTimeToLongFilter, tenantOwnerNameFilter)); - checkEntitiesCount(activeAlarmTimeToLongQuery, 0); + EntityCountQuery activeAlarmTimeToLongQuery = new EntityCountQuery(filter, List.of(activeAlarmTimeToLongFilter, tenantOwnerNameFilter)); + countByQueryAndCheck(activeAlarmTimeToLongQuery, 0); // all devices with wrong ownerName EntityCountQuery wrongTenantNameQuery = new EntityCountQuery(filter, List.of(activeAlarmTimeFilter, wrongOwnerNameFilter)); - checkEntitiesCount(wrongTenantNameQuery, 0); + countByQueryAndCheck(wrongTenantNameQuery, 0); // all devices with owner type = TENANT EntityCountQuery tenantEntitiesQuery = new EntityCountQuery(filter, List.of(activeAlarmTimeFilter, tenantOwnerTypeFilter)); - checkEntitiesCount(tenantEntitiesQuery, numOfDevices); + countByQueryAndCheck(tenantEntitiesQuery, numOfDevices); // all devices with owner type = CUSTOMER EntityCountQuery customerEntitiesQuery = new EntityCountQuery(filter, List.of(activeAlarmTimeFilter, customerOwnerTypeFilter)); - checkEntitiesCount(customerEntitiesQuery, 0); + countByQueryAndCheck(customerEntitiesQuery, 0); } @Test @@ -790,7 +755,7 @@ public class EntityQueryControllerTest extends AbstractControllerTest { KeyFilter activeAlarmTimeFilter = getServerAttributeNumericGreaterThanKeyFilter("alarmActiveTime", 5); KeyFilter tenantOwnerNameFilter = getEntityFieldStringEqualToKeyFilter("ownerName", TEST_TENANT_NAME); KeyFilter wrongOwnerNameFilter = getEntityFieldStringEqualToKeyFilter("ownerName", "wrongName"); - KeyFilter tenantOwnerTypeFilter = getEntityFieldStringEqualToKeyFilter("ownerType", "TENANT"); + KeyFilter tenantOwnerTypeFilter = getEntityFieldStringEqualToKeyFilter("ownerType", "TENANT"); KeyFilter customerOwnerTypeFilter = getEntityFieldStringEqualToKeyFilter("ownerType", "CUSTOMER"); EntityDataSortOrder sortOrder = new EntityDataSortOrder( @@ -874,18 +839,18 @@ public class EntityQueryControllerTest extends AbstractControllerTest { } private void checkEntitiesByQuery(EntityDataQuery query, int expectedNumOfDevices, String expectedOwnerName, String expectedOwnerType) throws Exception { - Awaitility.await() + await() .alias("data by query") .atMost(30, TimeUnit.SECONDS) .until(() -> { - var data = doPostWithTypedResponse("/api/entitiesQuery/find", query, new TypeReference>() {}); + var data = findByQuery(query); var loadedEntities = new ArrayList<>(data.getData()); return loadedEntities.size() == expectedNumOfDevices; }); if (expectedNumOfDevices == 0) { return; } - var data = doPostWithTypedResponse("/api/entitiesQuery/find", query, new TypeReference>() {}); + var data = findByQuery(query); var loadedEntities = new ArrayList<>(data.getData()); Assert.assertEquals(expectedNumOfDevices, loadedEntities.size()); @@ -898,25 +863,36 @@ public class EntityQueryControllerTest extends AbstractControllerTest { String alarmActiveTime = entity.getLatest().get(EntityKeyType.ATTRIBUTE).getOrDefault("alarmActiveTime", new TsValue(0, "-1")).getValue(); Assert.assertEquals("Device" + i, name); - Assert.assertEquals( expectedOwnerName, ownerName); - Assert.assertEquals( expectedOwnerType, ownerType); + Assert.assertEquals(expectedOwnerName, ownerName); + Assert.assertEquals(expectedOwnerType, ownerType); Assert.assertEquals("1" + i, alarmActiveTime); } } - private void checkEntitiesCount(EntityCountQuery query, int expectedNumOfDevices) { - Awaitility.await() - .alias("count by query") - .atMost(30, TimeUnit.SECONDS) - .until(() -> { - var count = doPost("/api/entitiesQuery/count", query, Integer.class); - return count == expectedNumOfDevices; - }); - } + protected PageData findByQuery(EntityDataQuery query) throws Exception { + return doPostWithTypedResponse("/api/entitiesQuery/find", query, new TypeReference<>() {}); + } + + protected PageData findByQueryAndCheck(EntityDataQuery query, int expectedResultSize) throws Exception { + PageData result = findByQuery(query); + assertThat(result.getTotalElements()).isEqualTo(expectedResultSize); + return result; + } + + protected Long countByQuery(EntityCountQuery countQuery) throws Exception { + return doPostWithResponse("/api/entitiesQuery/count", countQuery, Long.class); + } + + protected Long countByQueryAndCheck(EntityCountQuery query, long expectedResult) throws Exception { + Long result = countByQuery(query); + assertThat(result).isEqualTo(expectedResult); + return result; + } private KeyFilter getEntityFieldStringEqualToKeyFilter(String keyName, String value) { KeyFilter tenantOwnerNameFilter = new KeyFilter(); tenantOwnerNameFilter.setKey(new EntityKey(EntityKeyType.ENTITY_FIELD, keyName)); + tenantOwnerNameFilter.setValueType(EntityKeyValueType.STRING); StringFilterPredicate ownerNamePredicate = new StringFilterPredicate(); ownerNamePredicate.setValue(FilterPredicateValue.fromString(value)); ownerNamePredicate.setOperation(StringFilterPredicate.StringOperation.EQUAL); @@ -927,6 +903,7 @@ public class EntityQueryControllerTest extends AbstractControllerTest { private KeyFilter getServerAttributeNumericGreaterThanKeyFilter(String attribute, int value) { KeyFilter numericFilter = new KeyFilter(); numericFilter.setKey(new EntityKey(EntityKeyType.SERVER_ATTRIBUTE, attribute)); + numericFilter.setValueType(EntityKeyValueType.NUMERIC); NumericFilterPredicate predicate = new NumericFilterPredicate(); predicate.setValue(FilterPredicateValue.fromDouble(value)); predicate.setOperation(NumericFilterPredicate.NumericOperation.GREATER); diff --git a/application/src/test/java/org/thingsboard/server/queue/discovery/HashPartitionServiceTest.java b/application/src/test/java/org/thingsboard/server/queue/discovery/HashPartitionServiceTest.java index 52cbaa4add..0a4009e8b2 100644 --- a/application/src/test/java/org/thingsboard/server/queue/discovery/HashPartitionServiceTest.java +++ b/application/src/test/java/org/thingsboard/server/queue/discovery/HashPartitionServiceTest.java @@ -153,7 +153,7 @@ public class HashPartitionServiceTest { for (int queueIndex = 0; queueIndex < queueCount; queueIndex++) { QueueKey queueKey = new QueueKey(ServiceType.TB_RULE_ENGINE, "queue" + queueIndex, tenantId); for (int partition = 0; partition < partitionCount; partition++) { - ServiceInfo serviceInfo = partitionService.resolveByPartitionIdx(services, queueKey, partition, Collections.emptyMap()); + ServiceInfo serviceInfo = partitionService.resolveByPartitionIdx(services, queueKey, partition, Collections.emptyMap()).get(0); String serviceId = serviceInfo.getServiceId(); map.put(serviceId, map.get(serviceId) + 1); } @@ -308,7 +308,7 @@ public class HashPartitionServiceTest { partitionService_common.recalculatePartitions(commonRuleEngine, List.of(dedicatedRuleEngine)); verifyPartitionChangeEvent(event -> { QueueKey queueKey = new QueueKey(ServiceType.TB_RULE_ENGINE, DataConstants.MAIN_QUEUE_NAME, TenantId.SYS_TENANT_ID); - return event.getPartitionsMap().get(queueKey).size() == systemQueue.getPartitions(); + return event.getNewPartitions().get(queueKey).size() == systemQueue.getPartitions(); }); Mockito.reset(applicationEventPublisher); @@ -336,14 +336,14 @@ public class HashPartitionServiceTest { // expecting event about no partitions for isolated queue key verifyPartitionChangeEvent(event -> { QueueKey queueKey = new QueueKey(ServiceType.TB_RULE_ENGINE, DataConstants.MAIN_QUEUE_NAME, tenantId); - return event.getPartitionsMap().get(queueKey).isEmpty(); + return event.getNewPartitions().get(queueKey).isEmpty(); }); partitionService_dedicated.updateQueues(List.of(queueUpdateMsg)); partitionService_dedicated.recalculatePartitions(dedicatedRuleEngine, List.of(commonRuleEngine)); verifyPartitionChangeEvent(event -> { QueueKey queueKey = new QueueKey(ServiceType.TB_RULE_ENGINE, DataConstants.MAIN_QUEUE_NAME, tenantId); - return event.getPartitionsMap().get(queueKey).size() == isolatedQueue.getPartitions(); + return event.getNewPartitions().get(queueKey).size() == isolatedQueue.getPartitions(); }); @@ -361,7 +361,7 @@ public class HashPartitionServiceTest { partitionService_dedicated.removeQueues(List.of(queueDeleteMsg)); verifyPartitionChangeEvent(event -> { QueueKey queueKey = new QueueKey(ServiceType.TB_RULE_ENGINE, DataConstants.MAIN_QUEUE_NAME, tenantId); - return event.getPartitionsMap().get(queueKey).isEmpty(); + return event.getNewPartitions().get(queueKey).isEmpty(); }); } @@ -381,12 +381,12 @@ public class HashPartitionServiceTest { Stream.concat(Stream.of(TenantId.SYS_TENANT_ID), Stream.generate(UUID::randomUUID).map(TenantId::new).limit(10)).forEach(tenantId -> { List queues = Stream.generate(() -> RandomStringUtils.randomAlphabetic(10)) .map(queueName -> new QueueKey(ServiceType.TB_RULE_ENGINE, queueName, tenantId)) - .limit(100).collect(Collectors.toList()); + .limit(100).toList(); for (int partition = 0; partition < 10; partition++) { - ServiceInfo expectedAssignedRuleEngine = partitionService.resolveByPartitionIdx(ruleEngines, new QueueKey(ServiceType.TB_RULE_ENGINE, tenantId), partition, Collections.emptyMap()); + ServiceInfo expectedAssignedRuleEngine = partitionService.resolveByPartitionIdx(ruleEngines, new QueueKey(ServiceType.TB_RULE_ENGINE, tenantId), partition, Collections.emptyMap()).get(0); for (QueueKey queueKey : queues) { - ServiceInfo assignedRuleEngine = partitionService.resolveByPartitionIdx(ruleEngines, queueKey, partition, Collections.emptyMap()); + ServiceInfo assignedRuleEngine = partitionService.resolveByPartitionIdx(ruleEngines, queueKey, partition, Collections.emptyMap()).get(0); assertThat(assignedRuleEngine).as(queueKey + "[" + partition + "] should be assigned to " + expectedAssignedRuleEngine.getServiceId()) .isEqualTo(expectedAssignedRuleEngine); } diff --git a/application/src/test/java/org/thingsboard/server/service/entitiy/EdqsEntityServiceTest.java b/application/src/test/java/org/thingsboard/server/service/entitiy/EdqsEntityServiceTest.java new file mode 100644 index 0000000000..eea7e0eb7f --- /dev/null +++ b/application/src/test/java/org/thingsboard/server/service/entitiy/EdqsEntityServiceTest.java @@ -0,0 +1,72 @@ +/** + * Copyright © 2016-2024 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.service.entitiy; + +import org.junit.Before; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.mock.mockito.MockBean; +import org.springframework.test.context.TestPropertySource; +import org.thingsboard.server.common.data.id.CustomerId; +import org.thingsboard.server.common.data.page.PageData; +import org.thingsboard.server.common.data.permission.MergedUserPermissions; +import org.thingsboard.server.common.data.query.EntityCountQuery; +import org.thingsboard.server.common.data.query.EntityData; +import org.thingsboard.server.common.data.query.EntityDataQuery; +import org.thingsboard.server.common.msg.edqs.EdqsService; +import org.thingsboard.server.dao.service.DaoSqlTest; +import org.thingsboard.server.edqs.util.EdqsRocksDb; + +import java.util.concurrent.TimeUnit; + +import static org.awaitility.Awaitility.await; + +@DaoSqlTest +@TestPropertySource(properties = { + "queue.edqs.sync_enabled=true", + "queue.edqs.api_enabled=true", + "queue.edqs.mode=local" +}) +public class EdqsEntityServiceTest extends EntityServiceTest { + + @Autowired + private EdqsService edqsService; + + @MockBean + private EdqsRocksDb edqsRocksDb; + + @Before + public void beforeEach() { + await().atMost(TIMEOUT, TimeUnit.SECONDS).until(() -> edqsService.isApiEnabled()); + } + + @Override + protected PageData findByQueryAndCheck(CustomerId customerId, MergedUserPermissions permissions, EntityDataQuery query, long expectedResultSize) { + return await().atMost(15, TimeUnit.SECONDS).until(() -> findByQuery(customerId, permissions, query), + result -> result.getTotalElements() == expectedResultSize); + } + + @Override + protected long countByQueryAndCheck(EntityCountQuery countQuery, int expectedResult) { + return countByQueryAndCheck(new CustomerId(CustomerId.NULL_UUID), mergedUserPermissionsPE, countQuery, expectedResult); + } + + @Override + protected long countByQueryAndCheck(CustomerId customerId, MergedUserPermissions permissions, EntityCountQuery query, int expectedResult) { + return await().atMost(15, TimeUnit.SECONDS).until(() -> countByQuery(customerId, permissions, query), + result -> result == expectedResult); + } + +} diff --git a/dao/src/test/java/org/thingsboard/server/dao/service/EntityServiceTest.java b/application/src/test/java/org/thingsboard/server/service/entitiy/EntityServiceTest.java similarity index 87% rename from dao/src/test/java/org/thingsboard/server/dao/service/EntityServiceTest.java rename to application/src/test/java/org/thingsboard/server/service/entitiy/EntityServiceTest.java index a26f345fb7..dc29cec3cc 100644 --- a/dao/src/test/java/org/thingsboard/server/dao/service/EntityServiceTest.java +++ b/application/src/test/java/org/thingsboard/server/service/entitiy/EntityServiceTest.java @@ -13,22 +13,25 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package org.thingsboard.server.dao.service; +package org.thingsboard.server.service.entitiy; import com.google.common.collect.Lists; import com.google.common.util.concurrent.Futures; import com.google.common.util.concurrent.ListenableFuture; import lombok.extern.slf4j.Slf4j; +import org.apache.commons.lang3.RandomStringUtils; import org.apache.commons.lang3.RandomUtils; import org.junit.Assert; import org.junit.Before; import org.junit.Test; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.jdbc.core.ResultSetExtractor; +import org.thingsboard.common.util.JacksonUtil; import org.thingsboard.server.common.data.AttributeScope; import org.thingsboard.server.common.data.Customer; import org.thingsboard.server.common.data.Device; import org.thingsboard.server.common.data.EntityType; +import org.thingsboard.server.common.data.EntityView; import org.thingsboard.server.common.data.StringUtils; import org.thingsboard.server.common.data.User; import org.thingsboard.server.common.data.asset.Asset; @@ -37,6 +40,7 @@ import org.thingsboard.server.common.data.id.CustomerId; import org.thingsboard.server.common.data.id.DeviceId; import org.thingsboard.server.common.data.id.EdgeId; import org.thingsboard.server.common.data.id.EntityId; +import org.thingsboard.server.common.data.id.EntityViewId; import org.thingsboard.server.common.data.id.IdBased; import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.common.data.kv.AttributeKvEntry; @@ -46,6 +50,7 @@ import org.thingsboard.server.common.data.kv.DoubleDataEntry; import org.thingsboard.server.common.data.kv.KvEntry; import org.thingsboard.server.common.data.kv.LongDataEntry; import org.thingsboard.server.common.data.kv.StringDataEntry; +import org.thingsboard.server.common.data.objects.TelemetryEntityView; import org.thingsboard.server.common.data.page.PageData; import org.thingsboard.server.common.data.query.ApiUsageStateFilter; import org.thingsboard.server.common.data.query.AssetSearchQueryFilter; @@ -63,6 +68,7 @@ import org.thingsboard.server.common.data.query.EntityKey; import org.thingsboard.server.common.data.query.EntityKeyType; import org.thingsboard.server.common.data.query.EntityListFilter; import org.thingsboard.server.common.data.query.EntityNameFilter; +import org.thingsboard.server.common.data.query.EntityViewTypeFilter; import org.thingsboard.server.common.data.query.FilterPredicateValue; import org.thingsboard.server.common.data.query.KeyFilter; import org.thingsboard.server.common.data.query.NumericFilterPredicate; @@ -75,17 +81,22 @@ import org.thingsboard.server.common.data.relation.EntitySearchDirection; import org.thingsboard.server.common.data.relation.RelationEntityTypeFilter; import org.thingsboard.server.common.data.relation.RelationTypeGroup; import org.thingsboard.server.common.data.security.Authority; +import org.thingsboard.server.controller.AbstractControllerTest; import org.thingsboard.server.dao.alarm.AlarmService; +import org.thingsboard.server.dao.asset.AssetProfileService; import org.thingsboard.server.dao.asset.AssetService; import org.thingsboard.server.dao.attributes.AttributesService; import org.thingsboard.server.dao.customer.CustomerService; import org.thingsboard.server.dao.dashboard.DashboardDao; +import org.thingsboard.server.dao.dashboard.DashboardService; import org.thingsboard.server.dao.device.DeviceService; 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; import org.thingsboard.server.dao.timeseries.TimeseriesService; import org.thingsboard.server.dao.usagerecord.ApiUsageStateService; @@ -111,7 +122,7 @@ import static org.thingsboard.server.common.data.query.EntityKeyType.ENTITY_FIEL @Slf4j @DaoSqlTest -public class EntityServiceTest extends AbstractServiceTest { +public class EntityServiceTest extends AbstractControllerTest { static final int ENTITY_COUNT = 5; public static final String TEST_CUSTOMER_NAME = "Test"; @@ -119,6 +130,12 @@ public class EntityServiceTest extends AbstractServiceTest { @Autowired AssetService assetService; @Autowired + AssetProfileService assetProfileService; + @Autowired + DashboardService dashboardService; + @Autowired + EntityViewService entityViewService; + @Autowired UserService userService; @Autowired AttributesService attributesService; @@ -157,7 +174,7 @@ public class EntityServiceTest extends AbstractServiceTest { } @Test - public void testCountEntitiesByQuery() throws InterruptedException { + public void testCountEntitiesByQuery() { List devices = new ArrayList<>(); for (int i = 0; i < 97; i++) { Device device = new Device(); @@ -173,30 +190,24 @@ public class EntityServiceTest extends AbstractServiceTest { filter.setDeviceNameFilter(""); EntityCountQuery countQuery = new EntityCountQuery(filter); - - long count = entityService.countEntitiesByQuery(tenantId, new CustomerId(CustomerId.NULL_UUID), countQuery); - Assert.assertEquals(97, count); + countByQueryAndCheck(countQuery, 97); filter.setDeviceTypes(List.of("unknown")); - count = entityService.countEntitiesByQuery(tenantId, new CustomerId(CustomerId.NULL_UUID), countQuery); - Assert.assertEquals(0, count); + countByQueryAndCheck(countQuery, 0); filter.setDeviceTypes(List.of("default")); filter.setDeviceNameFilter("Device1"); - count = entityService.countEntitiesByQuery(tenantId, new CustomerId(CustomerId.NULL_UUID), countQuery); - Assert.assertEquals(11, count); + countByQueryAndCheck(countQuery, 11); EntityListFilter entityListFilter = new EntityListFilter(); entityListFilter.setEntityType(EntityType.DEVICE); entityListFilter.setEntityList(devices.stream().map(Device::getId).map(DeviceId::toString).collect(Collectors.toList())); countQuery = new EntityCountQuery(entityListFilter); - count = entityService.countEntitiesByQuery(tenantId, new CustomerId(CustomerId.NULL_UUID), countQuery); - Assert.assertEquals(97, count); + countByQueryAndCheck(countQuery, 97); deviceService.deleteDevicesByTenantId(tenantId); - count = entityService.countEntitiesByQuery(tenantId, new CustomerId(CustomerId.NULL_UUID), countQuery); - Assert.assertEquals(0, count); + countByQueryAndCheck(countQuery, 0); } @@ -211,19 +222,15 @@ public class EntityServiceTest extends AbstractServiceTest { filter.setDirection(EntitySearchDirection.FROM); EntityCountQuery countQuery = new EntityCountQuery(filter); - - long count = entityService.countEntitiesByQuery(tenantId, new CustomerId(CustomerId.NULL_UUID), countQuery); - Assert.assertEquals(31, count); //due to the loop relations in hierarchy, the TenantId included in total count (1*Tenant + 5*Asset + 5*5*Devices = 31) + countByQueryAndCheck(countQuery, 31); //due to the loop relations in hierarchy, the TenantId included in total count (1*Tenant + 5*Asset + 5*5*Devices = 31) filter.setFilters(Collections.singletonList(new RelationEntityTypeFilter("Contains", Collections.singletonList(EntityType.DEVICE)))); - count = entityService.countEntitiesByQuery(tenantId, new CustomerId(CustomerId.NULL_UUID), countQuery); - Assert.assertEquals(25, count); + countByQueryAndCheck(countQuery, 25); filter.setRootEntity(devices.get(0).getId()); filter.setDirection(EntitySearchDirection.TO); filter.setFilters(Collections.singletonList(new RelationEntityTypeFilter("Manages", Collections.singletonList(EntityType.TENANT)))); - count = entityService.countEntitiesByQuery(tenantId, new CustomerId(CustomerId.NULL_UUID), countQuery); - Assert.assertEquals(1, count); + countByQueryAndCheck(countQuery, 1); DeviceSearchQueryFilter filter2 = new DeviceSearchQueryFilter(); filter2.setRootEntity(tenantId); @@ -231,18 +238,14 @@ public class EntityServiceTest extends AbstractServiceTest { filter2.setRelationType("Contains"); countQuery = new EntityCountQuery(filter2); - - count = entityService.countEntitiesByQuery(tenantId, new CustomerId(CustomerId.NULL_UUID), countQuery); - Assert.assertEquals(25, count); + countByQueryAndCheck(countQuery, 25); filter2.setDeviceTypes(Arrays.asList("default0", "default1")); - count = entityService.countEntitiesByQuery(tenantId, new CustomerId(CustomerId.NULL_UUID), countQuery); - Assert.assertEquals(10, count); + countByQueryAndCheck(countQuery, 10); filter2.setRootEntity(devices.get(0).getId()); filter2.setDirection(EntitySearchDirection.TO); - count = entityService.countEntitiesByQuery(tenantId, new CustomerId(CustomerId.NULL_UUID), countQuery); - Assert.assertEquals(0, count); + countByQueryAndCheck(countQuery, 0); AssetSearchQueryFilter filter3 = new AssetSearchQueryFilter(); filter3.setRootEntity(tenantId); @@ -250,18 +253,14 @@ public class EntityServiceTest extends AbstractServiceTest { filter3.setRelationType("Manages"); countQuery = new EntityCountQuery(filter3); - - count = entityService.countEntitiesByQuery(tenantId, new CustomerId(CustomerId.NULL_UUID), countQuery); - Assert.assertEquals(5, count); + countByQueryAndCheck(countQuery, 5); filter3.setAssetTypes(Arrays.asList("type0", "type1")); - count = entityService.countEntitiesByQuery(tenantId, new CustomerId(CustomerId.NULL_UUID), countQuery); - Assert.assertEquals(2, count); + countByQueryAndCheck(countQuery, 2); filter3.setRootEntity(devices.get(0).getId()); filter3.setDirection(EntitySearchDirection.TO); - count = entityService.countEntitiesByQuery(tenantId, new CustomerId(CustomerId.NULL_UUID), countQuery); - Assert.assertEquals(0, count); + countByQueryAndCheck(countQuery, 0); } @Test @@ -278,11 +277,12 @@ public class EntityServiceTest extends AbstractServiceTest { EntityDataQuery query = new EntityDataQuery(filter, pageLink, entityFields, null, null); - PageData entityDataByQuery = entityService.findEntityDataByQuery(tenantId, new CustomerId(CustomerId.NULL_UUID), query); + PageData entityDataByQuery = findByQueryAndCheck(query, 5); List data = entityDataByQuery.getData(); Assert.assertEquals(data.size(), 5); data.forEach(entityData -> Assert.assertNotNull(entityData.getLatest().get(EntityKeyType.ENTITY_FIELD).get("phone"))); + countByQueryAndCheck(query, 5); } private void createTestUserRelations(TenantId tenantId, List users) { @@ -312,30 +312,24 @@ public class EntityServiceTest extends AbstractServiceTest { filter.setEdgeNameFilter(""); EntityCountQuery countQuery = new EntityCountQuery(filter); - - long count = entityService.countEntitiesByQuery(tenantId, new CustomerId(CustomerId.NULL_UUID), countQuery); - Assert.assertEquals(97, count); + countByQueryAndCheck(countQuery, 97); filter.setEdgeTypes(List.of("unknown")); - count = entityService.countEntitiesByQuery(tenantId, new CustomerId(CustomerId.NULL_UUID), countQuery); - Assert.assertEquals(0, count); + countByQueryAndCheck(countQuery, 0); filter.setEdgeTypes(List.of("default")); filter.setEdgeNameFilter("Edge1"); - count = entityService.countEntitiesByQuery(tenantId, new CustomerId(CustomerId.NULL_UUID), countQuery); - Assert.assertEquals(11, count); + countByQueryAndCheck(countQuery, 11); EntityListFilter entityListFilter = new EntityListFilter(); entityListFilter.setEntityType(EntityType.EDGE); entityListFilter.setEntityList(edges.stream().map(Edge::getId).map(EdgeId::toString).collect(Collectors.toList())); countQuery = new EntityCountQuery(entityListFilter); - count = entityService.countEntitiesByQuery(tenantId, new CustomerId(CustomerId.NULL_UUID), countQuery); - Assert.assertEquals(97, count); + countByQueryAndCheck(countQuery, 97); edgeService.deleteEdgesByTenantId(tenantId); - count = entityService.countEntitiesByQuery(tenantId, new CustomerId(CustomerId.NULL_UUID), countQuery); - Assert.assertEquals(0, count); + countByQueryAndCheck(countQuery, 0); } @Test @@ -360,13 +354,10 @@ public class EntityServiceTest extends AbstractServiceTest { filter.setRelationType("Manages"); EntityCountQuery countQuery = new EntityCountQuery(filter); - - long count = entityService.countEntitiesByQuery(tenantId, new CustomerId(CustomerId.NULL_UUID), countQuery); - Assert.assertEquals(5, count); + countByQueryAndCheck(countQuery, 5); filter.setEdgeTypes(Arrays.asList("type0", "type1")); - count = entityService.countEntitiesByQuery(tenantId, new CustomerId(CustomerId.NULL_UUID), countQuery); - Assert.assertEquals(2, count); + countByQueryAndCheck(countQuery, 2); } private Edge createEdge(int i, String type) { @@ -424,11 +415,11 @@ public class EntityServiceTest extends AbstractServiceTest { List latestValues = Collections.singletonList(new EntityKey(EntityKeyType.ATTRIBUTE, "temperature")); EntityDataQuery query = new EntityDataQuery(filter, pageLink, entityFields, latestValues, null); - PageData data = entityService.findEntityDataByQuery(tenantId, new CustomerId(CustomerId.NULL_UUID), query); + PageData data = findByQueryAndCheck(query, 25); List loadedEntities = new ArrayList<>(data.getData()); while (data.hasNext()) { query = query.next(); - data = entityService.findEntityDataByQuery(tenantId, new CustomerId(CustomerId.NULL_UUID), query); + data = findByQuery(query); loadedEntities.addAll(data.getData()); } Assert.assertEquals(25, loadedEntities.size()); @@ -437,6 +428,9 @@ public class EntityServiceTest extends AbstractServiceTest { List deviceTemperatures = temperatures.stream().map(aLong -> Long.toString(aLong)).collect(Collectors.toList()); Assert.assertEquals(deviceTemperatures, loadedTemperatures); + //count query + countByQueryAndCheck(query, 25); + pageLink = new EntityDataPageLink(10, 0, null, sortOrder); KeyFilter highTemperatureFilter = new KeyFilter(); highTemperatureFilter.setKey(new EntityKey(EntityKeyType.ATTRIBUTE, "temperature")); @@ -447,13 +441,12 @@ public class EntityServiceTest extends AbstractServiceTest { List keyFilters = Collections.singletonList(highTemperatureFilter); query = new EntityDataQuery(filter, pageLink, entityFields, latestValues, keyFilters); - - data = entityService.findEntityDataByQuery(tenantId, new CustomerId(CustomerId.NULL_UUID), query); + data = findByQueryAndCheck(query, highTemperatures.size()); loadedEntities = new ArrayList<>(data.getData()); while (data.hasNext()) { query = query.next(); - data = entityService.findEntityDataByQuery(tenantId, new CustomerId(CustomerId.NULL_UUID), query); + data = findByQuery(query); loadedEntities.addAll(data.getData()); } Assert.assertEquals(highTemperatures.size(), loadedEntities.size()); @@ -464,6 +457,9 @@ public class EntityServiceTest extends AbstractServiceTest { Assert.assertEquals(deviceHighTemperatures, loadedHighTemperatures); + //count query + countByQueryAndCheck(query, deviceHighTemperatures.size()); + deviceService.deleteDevicesByTenantId(tenantId); } @@ -482,13 +478,10 @@ public class EntityServiceTest extends AbstractServiceTest { filter.setDirection(EntitySearchDirection.FROM); EntityCountQuery countQuery = new EntityCountQuery(filter); - - long count = entityService.countEntitiesByQuery(tenantId, new CustomerId(CustomerId.NULL_UUID), countQuery); - Assert.assertEquals(63, count); + countByQueryAndCheck(countQuery, 63); filter.setFilters(Collections.singletonList(new RelationEntityTypeFilter("AptToHeat", Collections.singletonList(EntityType.DEVICE)))); - count = entityService.countEntitiesByQuery(tenantId, new CustomerId(CustomerId.NULL_UUID), countQuery); - Assert.assertEquals(27, count); + countByQueryAndCheck(countQuery, 27); filter.setMultiRootEntitiesType(EntityType.ASSET); filter.setMultiRootEntityIds(apartments.stream().map(IdBased::getId).map(d -> d.getId().toString()).collect(Collectors.toSet())); @@ -496,13 +489,10 @@ public class EntityServiceTest extends AbstractServiceTest { filter.setFilters(Lists.newArrayList( new RelationEntityTypeFilter("buildingToApt", Collections.singletonList(EntityType.ASSET)), new RelationEntityTypeFilter("AptToEnergy", Collections.singletonList(EntityType.DEVICE)))); - - count = entityService.countEntitiesByQuery(tenantId, new CustomerId(CustomerId.NULL_UUID), countQuery); - Assert.assertEquals(9, count); + countByQueryAndCheck(countQuery, 9); deviceService.deleteDevicesByTenantId(tenantId); assetService.deleteAssetsByTenantId(tenantId); - } @Test @@ -538,15 +528,6 @@ public class EntityServiceTest extends AbstractServiceTest { onlineStatusFilter.setPredicate(predicate); List keyFilters = Collections.singletonList(onlineStatusFilter); - EntityDataQuery query = new EntityDataQuery(filter, pageLink, entityFields, latestValues, keyFilters); - PageData data = entityService.findEntityDataByQuery(tenantId, new CustomerId(CustomerId.NULL_UUID), query); - List loadedEntities = new ArrayList<>(data.getData()); - while (data.hasNext()) { - query = query.next(); - data = entityService.findEntityDataByQuery(tenantId, new CustomerId(CustomerId.NULL_UUID), query); - loadedEntities.addAll(data.getData()); - } - long expectedEntitiesCnt = entityNameByTypeMap.entrySet() .stream() .filter(e -> !e.getKey().equals("building")) @@ -554,6 +535,14 @@ public class EntityServiceTest extends AbstractServiceTest { .map(Map.Entry::getValue) .filter(e -> StringUtils.endsWith(e, "_1")) .count(); + EntityDataQuery query = new EntityDataQuery(filter, pageLink, entityFields, latestValues, keyFilters); + PageData data = findByQueryAndCheck(query, expectedEntitiesCnt); + List loadedEntities = new ArrayList<>(data.getData()); + while (data.hasNext()) { + query = query.next(); + data = findByQuery(query); + loadedEntities.addAll(data.getData()); + } Assert.assertEquals(expectedEntitiesCnt, loadedEntities.size()); Map actualRelations = new HashMap<>(); @@ -603,11 +592,11 @@ public class EntityServiceTest extends AbstractServiceTest { List latestValues = Collections.singletonList(new EntityKey(EntityKeyType.ATTRIBUTE, "temperature")); EntityDataQuery query = new EntityDataQuery(filter, pageLink, entityFields, latestValues, null); - PageData data = entityService.findEntityDataByQuery(tenantId, new CustomerId(CustomerId.NULL_UUID), query); + PageData data = findByQueryAndCheck(query, 25); List loadedEntities = new ArrayList<>(data.getData()); while (data.hasNext()) { query = query.next(); - data = entityService.findEntityDataByQuery(tenantId, new CustomerId(CustomerId.NULL_UUID), query); + data = findByQuery(query); loadedEntities.addAll(data.getData()); } Assert.assertEquals(25, loadedEntities.size()); @@ -628,12 +617,12 @@ public class EntityServiceTest extends AbstractServiceTest { query = new EntityDataQuery(filter, pageLink, entityFields, latestValues, keyFilters); - data = entityService.findEntityDataByQuery(tenantId, new CustomerId(CustomerId.NULL_UUID), query); + data = findByQuery(query); loadedEntities = new ArrayList<>(data.getData()); while (data.hasNext()) { query = query.next(); - data = entityService.findEntityDataByQuery(tenantId, new CustomerId(CustomerId.NULL_UUID), query); + data = findByQuery(query); loadedEntities.addAll(data.getData()); } Assert.assertEquals(highTemperatures.size(), loadedEntities.size()); @@ -676,11 +665,11 @@ public class EntityServiceTest extends AbstractServiceTest { List latestValues = Collections.singletonList(new EntityKey(EntityKeyType.ATTRIBUTE, "consumption")); EntityDataQuery query = new EntityDataQuery(filter, pageLink, entityFields, latestValues, null); - PageData data = entityService.findEntityDataByQuery(tenantId, new CustomerId(CustomerId.NULL_UUID), query); + PageData data = findByQueryAndCheck(query, 5); List loadedEntities = new ArrayList<>(data.getData()); while (data.hasNext()) { query = query.next(); - data = entityService.findEntityDataByQuery(tenantId, new CustomerId(CustomerId.NULL_UUID), query); + data = findByQuery(query); loadedEntities.addAll(data.getData()); } Assert.assertEquals(5, loadedEntities.size()); @@ -700,12 +689,12 @@ public class EntityServiceTest extends AbstractServiceTest { query = new EntityDataQuery(filter, pageLink, entityFields, latestValues, keyFilters); - data = entityService.findEntityDataByQuery(tenantId, new CustomerId(CustomerId.NULL_UUID), query); + data = findByQuery(query); loadedEntities = new ArrayList<>(data.getData()); while (data.hasNext()) { query = query.next(); - data = entityService.findEntityDataByQuery(tenantId, new CustomerId(CustomerId.NULL_UUID), query); + data = findByQuery(query); loadedEntities.addAll(data.getData()); } Assert.assertEquals(highConsumptions.size(), loadedEntities.size()); @@ -896,9 +885,7 @@ public class EntityServiceTest extends AbstractServiceTest { List entityFields = Collections.singletonList(new EntityKey(EntityKeyType.ENTITY_FIELD, "name")); EntityDataQuery query = new EntityDataQuery(filter, pageLink, entityFields, null, null); - PageData data = entityService.findEntityDataByQuery(tenantId, new CustomerId(CustomerId.NULL_UUID), query); - - Assert.assertEquals(97, data.getTotalElements()); + PageData data = findByQueryAndCheck(query, 97); Assert.assertEquals(10, data.getTotalPages()); Assert.assertTrue(data.hasNext()); Assert.assertEquals(10, data.getData().size()); @@ -906,7 +893,7 @@ public class EntityServiceTest extends AbstractServiceTest { List loadedEntities = new ArrayList<>(data.getData()); while (data.hasNext()) { query = query.next(); - data = entityService.findEntityDataByQuery(tenantId, new CustomerId(CustomerId.NULL_UUID), query); + data = findByQuery(query); loadedEntities.addAll(data.getData()); } Assert.assertEquals(97, loadedEntities.size()); @@ -931,7 +918,7 @@ public class EntityServiceTest extends AbstractServiceTest { pageLink = new EntityDataPageLink(10, 0, "device1", sortOrder); query = new EntityDataQuery(filter, pageLink, entityFields, null, null); - data = entityService.findEntityDataByQuery(tenantId, new CustomerId(CustomerId.NULL_UUID), query); + data = findByQuery(query); Assert.assertEquals(11, data.getTotalElements()); Assert.assertEquals("Device19", data.getData().get(0).getLatest().get(EntityKeyType.ENTITY_FIELD).get("name").getValue()); @@ -945,11 +932,12 @@ public class EntityServiceTest extends AbstractServiceTest { devices.get(1).setLabel(null); devices.forEach(deviceService::saveDevice); + // FIXME (for Dasha, plz investigate): + // this and other tests below submit an empty value to a KEY FILTER, this is not "search text". + // why are we supposed to ignore it and return all devices? maybe it's a bug? String searchQuery = ""; EntityDataQuery query = createDeviceSearchQuery("label", StringOperation.EQUAL, searchQuery); - - PageData result = searchEntities(query); - assertEquals(devices.size(), result.getTotalElements()); + findByQueryAndCheck(query, devices.size()); } @Test @@ -961,9 +949,7 @@ public class EntityServiceTest extends AbstractServiceTest { String searchQuery = devices.get(2).getLabel(); EntityDataQuery query = createDeviceSearchQuery("label", StringOperation.NOT_EQUAL, searchQuery); - - PageData result = searchEntities(query); - assertEquals(devices.size() - 1, result.getTotalElements()); + findByQueryAndCheck(query, devices.size() - 1); } @Test @@ -976,8 +962,7 @@ public class EntityServiceTest extends AbstractServiceTest { String searchQuery = ""; EntityDataQuery query = createDeviceSearchQuery("label", StringOperation.NOT_EQUAL, searchQuery); - PageData result = searchEntities(query); - assertEquals(devices.size(), result.getTotalElements()); + findByQueryAndCheck(query, devices.size()); } @Test @@ -989,9 +974,7 @@ public class EntityServiceTest extends AbstractServiceTest { String searchQuery = ""; EntityDataQuery query = createDeviceSearchQuery("label", StringOperation.STARTS_WITH, searchQuery); - - PageData result = searchEntities(query); - assertEquals(devices.size(), result.getTotalElements()); + findByQueryAndCheck(query, devices.size()); } @Test @@ -1003,9 +986,7 @@ public class EntityServiceTest extends AbstractServiceTest { String searchQuery = ""; EntityDataQuery query = createDeviceSearchQuery("label", StringOperation.ENDS_WITH, searchQuery); - - PageData result = searchEntities(query); - assertEquals(devices.size(), result.getTotalElements()); + findByQueryAndCheck(query, devices.size()); } @Test @@ -1017,9 +998,7 @@ public class EntityServiceTest extends AbstractServiceTest { String searchQuery = ""; EntityDataQuery query = createDeviceSearchQuery("label", StringOperation.CONTAINS, searchQuery); - - PageData result = searchEntities(query); - assertEquals(devices.size(), result.getTotalElements()); + findByQueryAndCheck(query, devices.size()); } @Test @@ -1031,9 +1010,7 @@ public class EntityServiceTest extends AbstractServiceTest { String searchQuery = "label-"; EntityDataQuery query = createDeviceSearchQuery("label", StringOperation.NOT_CONTAINS, searchQuery); - - PageData result = searchEntities(query); - assertEquals(2, result.getTotalElements()); + findByQueryAndCheck(query, 2); } @Test @@ -1045,9 +1022,7 @@ public class EntityServiceTest extends AbstractServiceTest { String searchQuery = ""; EntityDataQuery query = createDeviceSearchQuery("label", StringOperation.NOT_CONTAINS, searchQuery); - - PageData result = searchEntities(query); - assertEquals(devices.size(), result.getTotalElements()); + findByQueryAndCheck(query, devices.size()); } @Test @@ -1071,34 +1046,27 @@ public class EntityServiceTest extends AbstractServiceTest { EntityDataPageLink pageLink = new EntityDataPageLink(1000, 0, null, null); EntityDataQuery query = new EntityDataQuery(deviceTypeFilter, pageLink, null, null, null); - - PageData result = searchEntities(query); - assertEquals(devices.size(), result.getTotalElements()); + findByQueryAndCheck(query, devices.size()); deviceTypeFilter.setEntityNameFilter("Device%"); - - result = searchEntities(query); - assertEquals(devices.size(), result.getTotalElements()); + findByQueryAndCheck(query, devices.size()); deviceTypeFilter.setEntityNameFilter("%Device%"); - - result = searchEntities(query); - assertEquals(devices.size(), result.getTotalElements()); + findByQueryAndCheck(query, devices.size()); deviceTypeFilter.setEntityNameFilter("%Device"); - - result = searchEntities(query); - assertEquals(0, result.getTotalElements()); + findByQueryAndCheck(query, 0); } @Test public void testFindEntityDataByQuery_filter_entity_name_ends_with() { List devices = new ArrayList<>(); + String suffixes = RandomStringUtils.randomAlphanumeric(5); for (int i = 0; i < 10; i++) { Device device = new Device(); device.setTenantId(tenantId); - device.setName("Device " + i + " test"); + device.setName("Device " + i + suffixes); device.setType("default"); devices.add(device); } @@ -1107,29 +1075,21 @@ public class EntityServiceTest extends AbstractServiceTest { EntityNameFilter deviceTypeFilter = new EntityNameFilter(); deviceTypeFilter.setEntityType(EntityType.DEVICE); - deviceTypeFilter.setEntityNameFilter("%test"); + deviceTypeFilter.setEntityNameFilter("%" + suffixes); EntityDataPageLink pageLink = new EntityDataPageLink(1000, 0, null, null); EntityDataQuery query = new EntityDataQuery(deviceTypeFilter, pageLink, null, null, null); + findByQueryAndCheck(query, devices.size()); - PageData result = searchEntities(query); - assertEquals(devices.size(), result.getTotalElements()); - - deviceTypeFilter.setEntityNameFilter("%test%"); - - result = searchEntities(query); - assertEquals(devices.size(), result.getTotalElements()); - - deviceTypeFilter.setEntityNameFilter("test%"); - - result = searchEntities(query); - assertEquals(0, result.getTotalElements()); + deviceTypeFilter.setEntityNameFilter("%" + suffixes + "%"); + findByQueryAndCheck(query, devices.size()); - deviceTypeFilter.setEntityNameFilter("test"); + deviceTypeFilter.setEntityNameFilter(suffixes + "%"); + findByQueryAndCheck(query, 0); - result = searchEntities(query); - assertEquals(0, result.getTotalElements()); + deviceTypeFilter.setEntityNameFilter(suffixes); + findByQueryAndCheck(query, 0); } @Test @@ -1153,19 +1113,13 @@ public class EntityServiceTest extends AbstractServiceTest { EntityDataPageLink pageLink = new EntityDataPageLink(1000, 0, null, null); EntityDataQuery query = new EntityDataQuery(deviceTypeFilter, pageLink, null, null, null); - - PageData result = searchEntities(query); - assertEquals(devices.size(), result.getTotalElements()); + findByQueryAndCheck(query, devices.size()); deviceTypeFilter.setEntityNameFilter("test%"); - - result = searchEntities(query); - assertEquals(0, result.getTotalElements()); + findByQueryAndCheck(query, 0); deviceTypeFilter.setEntityNameFilter("%test"); - - result = searchEntities(query); - assertEquals(0, result.getTotalElements()); + findByQueryAndCheck(query, 0); } @Test @@ -1189,24 +1143,16 @@ public class EntityServiceTest extends AbstractServiceTest { EntityDataPageLink pageLink = new EntityDataPageLink(1000, 0, null, null); EntityDataQuery query = new EntityDataQuery(deviceTypeFilter, pageLink, null, null, null); - - PageData result = searchEntities(query); - assertEquals(devices.size(), result.getTotalElements()); + findByQueryAndCheck(query, devices.size()); deviceTypeFilter.setDeviceNameFilter("Device%"); - - result = searchEntities(query); - assertEquals(devices.size(), result.getTotalElements()); + findByQueryAndCheck(query, devices.size()); deviceTypeFilter.setDeviceNameFilter("%Device%"); - - result = searchEntities(query); - assertEquals(devices.size(), result.getTotalElements()); + findByQueryAndCheck(query, devices.size()); deviceTypeFilter.setDeviceNameFilter("%Device"); - - result = searchEntities(query); - assertEquals(0, result.getTotalElements()); + findByQueryAndCheck(query, 0); } @Test @@ -1239,24 +1185,6 @@ public class EntityServiceTest extends AbstractServiceTest { assertThat(deviceName).isEqualTo(devices.get(0).getName()); } - @Test - public void testFindEntitiesByApiUsageStateFilter() { - apiUsageStateService.createDefaultApiUsageState(tenantId, customerId); - ApiUsageStateFilter apiUsageStateFilter = new ApiUsageStateFilter(); - apiUsageStateFilter.setCustomerId(customerId); - - List entityFields = List.of( - new EntityKey(EntityKeyType.ENTITY_FIELD, "name") - ); - - EntityDataPageLink pageLink = new EntityDataPageLink(1000, 0, null, null); - EntityDataQuery query = new EntityDataQuery(apiUsageStateFilter, pageLink, entityFields, null, null); - PageData result = searchEntities(query); - assertEquals(1, result.getTotalElements()); - String name = result.getData().get(0).getLatest().get(EntityKeyType.ENTITY_FIELD).get("name").getValue(); - assertThat(name).isEqualTo(TEST_CUSTOMER_NAME); - } - @Test public void testFindEntitiesByRelationEntityTypeFilter() { Customer customer = new Customer(); @@ -1341,24 +1269,16 @@ public class EntityServiceTest extends AbstractServiceTest { EntityDataPageLink pageLink = new EntityDataPageLink(1000, 0, null, null); EntityDataQuery query = new EntityDataQuery(deviceTypeFilter, pageLink, null, null, null); - - PageData result = searchEntities(query); - assertEquals(devices.size(), result.getTotalElements()); + findByQueryAndCheck(query, devices.size()); deviceTypeFilter.setDeviceNameFilter("%test%"); - - result = searchEntities(query); - assertEquals(devices.size(), result.getTotalElements()); + findByQueryAndCheck(query, devices.size()); deviceTypeFilter.setDeviceNameFilter("test%"); - - result = searchEntities(query); - assertEquals(0, result.getTotalElements()); + findByQueryAndCheck(query, 0); deviceTypeFilter.setDeviceNameFilter("test"); - - result = searchEntities(query); - assertEquals(0, result.getTotalElements()); + findByQueryAndCheck(query, 0); } @Test @@ -1382,19 +1302,13 @@ public class EntityServiceTest extends AbstractServiceTest { EntityDataPageLink pageLink = new EntityDataPageLink(1000, 0, null, null); EntityDataQuery query = new EntityDataQuery(deviceTypeFilter, pageLink, null, null, null); - - PageData result = searchEntities(query); - assertEquals(devices.size(), result.getTotalElements()); + findByQueryAndCheck(query, devices.size()); deviceTypeFilter.setDeviceNameFilter("test%"); - - result = searchEntities(query); - assertEquals(0, result.getTotalElements()); + findByQueryAndCheck(query, 0); deviceTypeFilter.setDeviceNameFilter("%test"); - - result = searchEntities(query); - assertEquals(0, result.getTotalElements()); + findByQueryAndCheck(query, 0); } @Test @@ -1418,24 +1332,16 @@ public class EntityServiceTest extends AbstractServiceTest { EntityDataPageLink pageLink = new EntityDataPageLink(1000, 0, null, null); EntityDataQuery query = new EntityDataQuery(assetTypeFilter, pageLink, null, null, null); - - PageData result = searchEntities(query); - assertEquals(assets.size(), result.getTotalElements()); + findByQueryAndCheck(query, assets.size()); assetTypeFilter.setAssetNameFilter("Asset%"); - - result = searchEntities(query); - assertEquals(assets.size(), result.getTotalElements()); + findByQueryAndCheck(query, assets.size()); assetTypeFilter.setAssetNameFilter("%Asset%"); - - result = searchEntities(query); - assertEquals(assets.size(), result.getTotalElements()); + findByQueryAndCheck(query, assets.size()); assetTypeFilter.setAssetNameFilter("%Asset"); - - result = searchEntities(query); - assertEquals(0, result.getTotalElements()); + findByQueryAndCheck(query, 0); } @Test @@ -1459,24 +1365,16 @@ public class EntityServiceTest extends AbstractServiceTest { EntityDataPageLink pageLink = new EntityDataPageLink(1000, 0, null, null); EntityDataQuery query = new EntityDataQuery(assetTypeFilter, pageLink, null, null, null); - - PageData result = searchEntities(query); - assertEquals(assets.size(), result.getTotalElements()); + findByQueryAndCheck(query, assets.size()); assetTypeFilter.setAssetNameFilter("%test%"); - - result = searchEntities(query); - assertEquals(assets.size(), result.getTotalElements()); + findByQueryAndCheck(query, assets.size()); assetTypeFilter.setAssetNameFilter("test%"); - - result = searchEntities(query); - assertEquals(0, result.getTotalElements()); + findByQueryAndCheck(query, 0); assetTypeFilter.setAssetNameFilter("test"); - - result = searchEntities(query); - assertEquals(0, result.getTotalElements()); + findByQueryAndCheck(query, 0); } @Test @@ -1488,6 +1386,7 @@ public class EntityServiceTest extends AbstractServiceTest { asset.setTenantId(tenantId); asset.setName("Asset test" + i); asset.setType("default"); + asset.setAssetProfileId(assetProfileService.findDefaultAssetProfile(tenantId).getId()); assets.add(asset); } @@ -1500,19 +1399,107 @@ public class EntityServiceTest extends AbstractServiceTest { EntityDataPageLink pageLink = new EntityDataPageLink(1000, 0, null, null); EntityDataQuery query = new EntityDataQuery(assetTypeFilter, pageLink, null, null, null); - - PageData result = searchEntities(query); - assertEquals(assets.size(), result.getTotalElements()); + findByQueryAndCheck(query, assets.size()); assetTypeFilter.setAssetNameFilter("test%"); - - result = searchEntities(query); - assertEquals(0, result.getTotalElements()); + findByQueryAndCheck(query, 0); assetTypeFilter.setAssetNameFilter("%test"); + findByQueryAndCheck(query, 0); + } + + @Test + public void testFindEntitiesBySingleEntityFilter_customer() { + List customerDevices = new ArrayList<>(); + List tenantDevices = new ArrayList<>(); + + for (int i = 0; i < 3; i++) { + Device device = new Device(); + device.setTenantId(tenantId); + device.setCustomerId(customerId); + device.setName("Device test" + i); + device.setType("default"); + Device saved = deviceService.saveDevice(device); + customerDevices.add(saved); + } - result = searchEntities(query); - assertEquals(0, result.getTotalElements()); + for (int i = 0; i < 3; i++) { + Device device = new Device(); + device.setTenantId(tenantId); + device.setName("Tenant test device" + i); + device.setType("default"); + tenantDevices.add(deviceService.saveDevice(device)); + } + + SingleEntityFilter singleEntityFilter = new SingleEntityFilter(); + singleEntityFilter.setSingleEntity(customerDevices.get(0).getId()); + List entityFields = List.of( + new EntityKey(EntityKeyType.ENTITY_FIELD, "name") + ); + EntityDataPageLink pageLink = new EntityDataPageLink(1000, 0, null, null); + EntityDataQuery query = new EntityDataQuery(singleEntityFilter, pageLink, entityFields, null, null); + + PageData result = findByQueryAndCheck(query, 1); + String deviceName = result.getData().get(0).getLatest().get(EntityKeyType.ENTITY_FIELD).get("name").getValue(); + assertThat(deviceName).isEqualTo(customerDevices.get(0).getName()); + + // try to find tenant device by customer user + SingleEntityFilter tenantDeviceFilter = new SingleEntityFilter(); + tenantDeviceFilter.setSingleEntity(tenantDevices.get(0).getId()); + EntityDataQuery customerQuery2 = new EntityDataQuery(tenantDeviceFilter, pageLink, entityFields, null, null); + PageData customerResults2 = entityService.findEntityDataByQuery(tenantId, customerId, customerQuery2); + + assertEquals(0, customerResults2.getTotalElements()); + + // find by tenant user with group permission + PageData results3 = entityService.findEntityDataByQuery(tenantId, new CustomerId(EntityId.NULL_UUID), query); + + assertEquals(1, results3.getTotalElements()); + String deviceName3 = results3.getData().get(0).getLatest().get(EntityKeyType.ENTITY_FIELD).get("name").getValue(); + assertThat(deviceName3).isEqualTo(customerDevices.get(0).getName()); + } + + private List getResultDeviceIds(PageData result) { + return result.getData().stream().map(entityData -> (DeviceId) entityData.getEntityId()).collect(Collectors.toList()); + } + + private Device createDevice(CustomerId customerId) { + Device device = new Device(); + device.setTenantId(tenantId); + device.setCustomerId(customerId); + device.setName("Device test " + RandomStringUtils.randomAlphabetic(5)); + device.setType("default"); + return device; + } + + @Test + public void testFindEntitiesByApiUsageStateFilter() { + ApiUsageStateFilter apiUsageStateFilter = new ApiUsageStateFilter(); + + List entityFields = List.of( + new EntityKey(EntityKeyType.ENTITY_FIELD, "name") + ); + + EntityDataPageLink pageLink = new EntityDataPageLink(1000, 0, null, null); + EntityDataQuery query = new EntityDataQuery(apiUsageStateFilter, pageLink, entityFields, null, null); + PageData result = findByQueryAndCheck(query, 1); + String name = result.getData().get(0).getLatest().get(EntityKeyType.ENTITY_FIELD).get("name").getValue(); + assertThat(name).isEqualTo(TEST_TENANT_NAME); + + // find by customer user with generic permissions + apiUsageStateService.createDefaultApiUsageState(tenantId, customerId); + PageData customerResult = entityService.findEntityDataByQuery(tenantId, customerId, query); + + assertEquals(1, customerResult.getTotalElements()); + String customerResultName = customerResult.getData().get(0).getLatest().get(EntityKeyType.ENTITY_FIELD).get("name").getValue(); + assertThat(customerResultName).isEqualTo(TEST_CUSTOMER_NAME); + + // find by tenant user with customerId filter + apiUsageStateFilter.setCustomerId(customerId); + PageData tenantResult = searchEntities(query); + assertEquals(1, tenantResult.getTotalElements()); + String tenantResultName = tenantResult.getData().get(0).getLatest().get(EntityKeyType.ENTITY_FIELD).get("name").getValue(); + assertThat(tenantResultName).isEqualTo(TEST_CUSTOMER_NAME); } private PageData searchEntities(EntityDataQuery query) { @@ -1597,11 +1584,11 @@ public class EntityServiceTest extends AbstractServiceTest { for (EntityKeyType currentAttributeKeyType : attributesEntityTypes) { List latestValues = Collections.singletonList(new EntityKey(currentAttributeKeyType, "temperature")); EntityDataQuery query = new EntityDataQuery(filter, pageLink, entityFields, latestValues, null); - PageData data = entityService.findEntityDataByQuery(tenantId, new CustomerId(CustomerId.NULL_UUID), query); + PageData data = findByQueryAndCheck(query, 67); List loadedEntities = new ArrayList<>(data.getData()); while (data.hasNext()) { query = query.next(); - data = entityService.findEntityDataByQuery(tenantId, new CustomerId(CustomerId.NULL_UUID), query); + data = findByQuery(query); loadedEntities.addAll(data.getData()); } Assert.assertEquals(67, loadedEntities.size()); @@ -1618,14 +1605,12 @@ public class EntityServiceTest extends AbstractServiceTest { List keyFiltersHighTemperature = Collections.singletonList(highTemperatureFilter); query = new EntityDataQuery(filter, pageLink, entityFields, latestValues, keyFiltersHighTemperature); - - data = entityService.findEntityDataByQuery(tenantId, new CustomerId(CustomerId.NULL_UUID), query); + data = findByQueryAndCheck(query, highTemperatures.size()); loadedEntities = new ArrayList<>(data.getData()); - while (data.hasNext()) { query = query.next(); - data = entityService.findEntityDataByQuery(tenantId, new CustomerId(CustomerId.NULL_UUID), query); + data = findByQuery(query); loadedEntities.addAll(data.getData()); } Assert.assertEquals(highTemperatures.size(), loadedEntities.size()); @@ -1718,7 +1703,7 @@ public class EntityServiceTest extends AbstractServiceTest { EntityDataPageLink pageLink = new EntityDataPageLink(100, 0, null, sortOrder); EntityDataQuery query = new EntityDataQuery(filter, pageLink, entityFields, latestValues, keyFiltersGreaterTemperature); - PageData data = entityService.findEntityDataByQuery(tenantId, new CustomerId(CustomerId.NULL_UUID), query); + PageData data = findByQueryAndCheck(query, greaterTemperatures.size()); List loadedEntities = getLoadedEntities(data, query); Assert.assertEquals(greaterTemperatures.size(), loadedEntities.size()); @@ -1732,7 +1717,7 @@ public class EntityServiceTest extends AbstractServiceTest { pageLink = new EntityDataPageLink(100, 0, null, sortOrder); query = new EntityDataQuery(filter, pageLink, entityFields, latestValues, keyFiltersGreaterOrEqualTemperature); - data = entityService.findEntityDataByQuery(tenantId, new CustomerId(CustomerId.NULL_UUID), query); + data = findByQueryAndCheck(query, greaterOrEqualTemperatures.size()); loadedEntities = getLoadedEntities(data, query); Assert.assertEquals(greaterOrEqualTemperatures.size(), loadedEntities.size()); @@ -1746,7 +1731,7 @@ public class EntityServiceTest extends AbstractServiceTest { pageLink = new EntityDataPageLink(100, 0, null, sortOrder); query = new EntityDataQuery(filter, pageLink, entityFields, latestValues, keyFiltersLessTemperature); - data = entityService.findEntityDataByQuery(tenantId, new CustomerId(CustomerId.NULL_UUID), query); + data = findByQueryAndCheck(query, lessTemperatures.size()); loadedEntities = getLoadedEntities(data, query); Assert.assertEquals(lessTemperatures.size(), loadedEntities.size()); @@ -1760,7 +1745,7 @@ public class EntityServiceTest extends AbstractServiceTest { pageLink = new EntityDataPageLink(100, 0, null, sortOrder); query = new EntityDataQuery(filter, pageLink, entityFields, latestValues, keyFiltersLessOrEqualTemperature); - data = entityService.findEntityDataByQuery(tenantId, new CustomerId(CustomerId.NULL_UUID), query); + data = findByQueryAndCheck(query, lessOrEqualTemperatures.size()); loadedEntities = getLoadedEntities(data, query); Assert.assertEquals(lessOrEqualTemperatures.size(), loadedEntities.size()); @@ -1774,7 +1759,7 @@ public class EntityServiceTest extends AbstractServiceTest { pageLink = new EntityDataPageLink(100, 0, null, sortOrder); query = new EntityDataQuery(filter, pageLink, entityFields, latestValues, keyFiltersEqualTemperature); - data = entityService.findEntityDataByQuery(tenantId, new CustomerId(CustomerId.NULL_UUID), query); + data = findByQueryAndCheck(query, equalTemperatures.size()); loadedEntities = getLoadedEntities(data, query); Assert.assertEquals(equalTemperatures.size(), loadedEntities.size()); @@ -1788,7 +1773,7 @@ public class EntityServiceTest extends AbstractServiceTest { pageLink = new EntityDataPageLink(100, 0, null, sortOrder); query = new EntityDataQuery(filter, pageLink, entityFields, latestValues, keyFiltersNotEqualTemperature); - data = entityService.findEntityDataByQuery(tenantId, new CustomerId(CustomerId.NULL_UUID), query); + data = findByQueryAndCheck(query, notEqualTemperatures.size()); loadedEntities = getLoadedEntities(data, query); Assert.assertEquals(notEqualTemperatures.size(), loadedEntities.size()); @@ -1843,12 +1828,12 @@ public class EntityServiceTest extends AbstractServiceTest { List latestValues = Collections.singletonList(new EntityKey(EntityKeyType.TIME_SERIES, "temperature")); EntityDataQuery query = new EntityDataQuery(filter, pageLink, entityFields, latestValues, null); - PageData data = entityService.findEntityDataByQuery(tenantId, new CustomerId(CustomerId.NULL_UUID), query); + PageData data = findByQueryAndCheck(query, 67); List loadedEntities = new ArrayList<>(data.getData()); while (data.hasNext()) { query = query.next(); - data = entityService.findEntityDataByQuery(tenantId, new CustomerId(CustomerId.NULL_UUID), query); + data = findByQuery(query); loadedEntities.addAll(data.getData()); } Assert.assertEquals(67, loadedEntities.size()); @@ -1871,13 +1856,12 @@ public class EntityServiceTest extends AbstractServiceTest { List keyFilters = Collections.singletonList(highTemperatureFilter); query = new EntityDataQuery(filter, pageLink, entityFields, latestValues, keyFilters); - - data = entityService.findEntityDataByQuery(tenantId, new CustomerId(CustomerId.NULL_UUID), query); + data = findByQueryAndCheck(query, highTemperatures.size()); loadedEntities = new ArrayList<>(data.getData()); while (data.hasNext()) { query = query.next(); - data = entityService.findEntityDataByQuery(tenantId, new CustomerId(CustomerId.NULL_UUID), query); + data = findByQuery(query); loadedEntities.addAll(data.getData()); } Assert.assertEquals(highTemperatures.size(), loadedEntities.size()); @@ -1994,7 +1978,7 @@ public class EntityServiceTest extends AbstractServiceTest { EntityDataPageLink pageLink = new EntityDataPageLink(100, 0, null, sortOrder); EntityDataQuery query = new EntityDataQuery(filter, pageLink, entityFields, latestValues, keyFiltersEqualString); - PageData data = entityService.findEntityDataByQuery(tenantId, new CustomerId(CustomerId.NULL_UUID), query); + PageData data = findByQueryAndCheck(query, equalStrings.size()); List loadedEntities = getLoadedEntities(data, query); Assert.assertEquals(equalStrings.size(), loadedEntities.size()); @@ -2007,7 +1991,7 @@ public class EntityServiceTest extends AbstractServiceTest { pageLink = new EntityDataPageLink(100, 0, null, sortOrder); query = new EntityDataQuery(filter, pageLink, entityFields, latestValues, keyFiltersNotEqualString); - data = entityService.findEntityDataByQuery(tenantId, new CustomerId(CustomerId.NULL_UUID), query); + data = findByQueryAndCheck(query, notEqualStrings.size()); loadedEntities = getLoadedEntities(data, query); Assert.assertEquals(notEqualStrings.size(), loadedEntities.size()); @@ -2020,7 +2004,7 @@ public class EntityServiceTest extends AbstractServiceTest { pageLink = new EntityDataPageLink(100, 0, null, sortOrder); query = new EntityDataQuery(filter, pageLink, entityFields, latestValues, keyFiltersStartsWithString); - data = entityService.findEntityDataByQuery(tenantId, new CustomerId(CustomerId.NULL_UUID), query); + data = findByQueryAndCheck(query, startsWithStrings.size()); loadedEntities = getLoadedEntities(data, query); Assert.assertEquals(startsWithStrings.size(), loadedEntities.size()); @@ -2033,7 +2017,7 @@ public class EntityServiceTest extends AbstractServiceTest { pageLink = new EntityDataPageLink(100, 0, null, sortOrder); query = new EntityDataQuery(filter, pageLink, entityFields, latestValues, keyFiltersEndsWithString); - data = entityService.findEntityDataByQuery(tenantId, new CustomerId(CustomerId.NULL_UUID), query); + data = findByQueryAndCheck(query, endsWithStrings.size()); loadedEntities = getLoadedEntities(data, query); Assert.assertEquals(endsWithStrings.size(), loadedEntities.size()); @@ -2046,7 +2030,7 @@ public class EntityServiceTest extends AbstractServiceTest { pageLink = new EntityDataPageLink(100, 0, null, sortOrder); query = new EntityDataQuery(filter, pageLink, entityFields, latestValues, keyFiltersContainsString); - data = entityService.findEntityDataByQuery(tenantId, new CustomerId(CustomerId.NULL_UUID), query); + data = findByQueryAndCheck(query, containsStrings.size()); loadedEntities = getLoadedEntities(data, query); Assert.assertEquals(containsStrings.size(), loadedEntities.size()); @@ -2059,7 +2043,7 @@ public class EntityServiceTest extends AbstractServiceTest { pageLink = new EntityDataPageLink(100, 0, null, sortOrder); query = new EntityDataQuery(filter, pageLink, entityFields, latestValues, keyFiltersNotContainsString); - data = entityService.findEntityDataByQuery(tenantId, new CustomerId(CustomerId.NULL_UUID), query); + data = findByQueryAndCheck(query, notContainsStrings.size()); loadedEntities = getLoadedEntities(data, query); Assert.assertEquals(notContainsStrings.size(), loadedEntities.size()); @@ -2072,7 +2056,7 @@ public class EntityServiceTest extends AbstractServiceTest { pageLink = new EntityDataPageLink(100, 0, null, sortOrder); query = new EntityDataQuery(filter, pageLink, entityFields, latestValues, deviceTypeFilters); - data = entityService.findEntityDataByQuery(tenantId, new CustomerId(CustomerId.NULL_UUID), query); + data = findByQueryAndCheck(query, devices.size()); loadedEntities = getLoadedEntities(data, query); Assert.assertEquals(devices.size(), loadedEntities.size()); @@ -2117,7 +2101,7 @@ public class EntityServiceTest extends AbstractServiceTest { EntityDataPageLink pageLink = new EntityDataPageLink(100, 0, null, sortOrder); EntityDataQuery query = new EntityDataQuery(filter, pageLink, entityFields, null, keyFiltersEqualString); - PageData data = entityService.findEntityDataByQuery(tenantId, new CustomerId(CustomerId.NULL_UUID), query); + PageData data = findByQueryAndCheck(query, devices.size()); List loadedEntities = getLoadedEntities(data, query); Assert.assertEquals(devices.size(), loadedEntities.size()); @@ -2132,7 +2116,7 @@ public class EntityServiceTest extends AbstractServiceTest { pageLink = new EntityDataPageLink(100, 0, null, sortOrder); query = new EntityDataQuery(filter, pageLink, entityFields, null, keyFiltersNotEqualString); - data = entityService.findEntityDataByQuery(tenantId, new CustomerId(CustomerId.NULL_UUID), query); + data = findByQueryAndCheck(query, devices.size()); loadedEntities = getLoadedEntities(data, query); Assert.assertEquals(devices.size(), loadedEntities.size()); @@ -2145,7 +2129,7 @@ public class EntityServiceTest extends AbstractServiceTest { pageLink = new EntityDataPageLink(100, 0, null, sortOrder); query = new EntityDataQuery(filter, pageLink, entityFields, null, keyFiltersStartsWithString); - data = entityService.findEntityDataByQuery(tenantId, new CustomerId(CustomerId.NULL_UUID), query); + data = findByQueryAndCheck(query, devices.size()); loadedEntities = getLoadedEntities(data, query); Assert.assertEquals(devices.size(), loadedEntities.size()); @@ -2158,7 +2142,7 @@ public class EntityServiceTest extends AbstractServiceTest { pageLink = new EntityDataPageLink(100, 0, null, sortOrder); query = new EntityDataQuery(filter, pageLink, entityFields, null, keyFiltersEndsWithString); - data = entityService.findEntityDataByQuery(tenantId, new CustomerId(CustomerId.NULL_UUID), query); + data = findByQueryAndCheck(query, devices.size()); loadedEntities = getLoadedEntities(data, query); Assert.assertEquals(devices.size(), loadedEntities.size()); @@ -2171,7 +2155,7 @@ public class EntityServiceTest extends AbstractServiceTest { pageLink = new EntityDataPageLink(100, 0, null, sortOrder); query = new EntityDataQuery(filter, pageLink, entityFields, null, keyFiltersContainsString); - data = entityService.findEntityDataByQuery(tenantId, new CustomerId(CustomerId.NULL_UUID), query); + data = findByQueryAndCheck(query, devices.size()); loadedEntities = getLoadedEntities(data, query); Assert.assertEquals(devices.size(), loadedEntities.size()); @@ -2184,7 +2168,7 @@ public class EntityServiceTest extends AbstractServiceTest { pageLink = new EntityDataPageLink(100, 0, null, sortOrder); query = new EntityDataQuery(filter, pageLink, entityFields, null, keyFiltersNotContainsString); - data = entityService.findEntityDataByQuery(tenantId, new CustomerId(CustomerId.NULL_UUID), query); + data = findByQueryAndCheck(query, devices.size()); loadedEntities = getLoadedEntities(data, query); Assert.assertEquals(devices.size(), loadedEntities.size()); @@ -2232,7 +2216,7 @@ public class EntityServiceTest extends AbstractServiceTest { EntityDataPageLink pageLink = new EntityDataPageLink(100, 0, null, sortOrder); EntityDataQuery query = new EntityDataQuery(filter, pageLink, entityFields, null, deviceTypeFilters); - PageData data = entityService.findEntityDataByQuery(tenantId, new CustomerId(CustomerId.NULL_UUID), query); + PageData data = findByQueryAndCheck(query, devices.size()); List loadedEntities = getLoadedEntities(data, query); Assert.assertEquals(devices.size(), loadedEntities.size()); @@ -2240,7 +2224,7 @@ public class EntityServiceTest extends AbstractServiceTest { pageLink = new EntityDataPageLink(100, 0, null, sortOrder); query = new EntityDataQuery(filter, pageLink, entityFields, null, createdTimeFilters); - data = entityService.findEntityDataByQuery(tenantId, new CustomerId(CustomerId.NULL_UUID), query); + data = findByQueryAndCheck(query, devices.size()); loadedEntities = getLoadedEntities(data, query); Assert.assertEquals(devices.size(), loadedEntities.size()); @@ -2248,7 +2232,7 @@ public class EntityServiceTest extends AbstractServiceTest { pageLink = new EntityDataPageLink(100, 0, null, null); query = new EntityDataQuery(filter, pageLink, entityFields, null, nameFilters); - data = entityService.findEntityDataByQuery(tenantId, new CustomerId(CustomerId.NULL_UUID), query); + data = findByQueryAndCheck(query, devices.size()); loadedEntities = getLoadedEntities(data, query); Assert.assertEquals(devices.size(), loadedEntities.size()); @@ -2296,12 +2280,12 @@ public class EntityServiceTest extends AbstractServiceTest { // query with textSearch - optimization is not performing EntityDataPageLink originalPageLink = new EntityDataPageLink(pageSize, 0, "Device", sortOrder); EntityDataQuery originalQuery = new EntityDataQuery(filter, originalPageLink, entityFields, null, deviceTypeFilters); - PageData originalData = entityService.findEntityDataByQuery(tenantId, new CustomerId(CustomerId.NULL_UUID), originalQuery); + PageData originalData = findByQueryAndCheck(originalQuery, expectedDevicesSize); // query without textSearch - optimization is performing EntityDataPageLink optimizedPageLink = new EntityDataPageLink(pageSize, 0, null, sortOrder); EntityDataQuery optimizedQuery = new EntityDataQuery(filter, optimizedPageLink, entityFields, null, deviceTypeFilters); - PageData optimizedData = entityService.findEntityDataByQuery(tenantId, new CustomerId(CustomerId.NULL_UUID), optimizedQuery); + PageData optimizedData = findByQueryAndCheck(optimizedQuery, expectedDevicesSize); List loadedEntities = getLoadedEntities(optimizedData, optimizedQuery); Assert.assertEquals(expectedDevicesSize, loadedEntities.size()); loadedEntities = getLoadedEntities(originalData, originalQuery); @@ -2325,12 +2309,12 @@ public class EntityServiceTest extends AbstractServiceTest { // query with textSearch - optimization is not performing originalPageLink = new EntityDataPageLink(pageSize, 0, "Device", sortOrder); originalQuery = new EntityDataQuery(filter, originalPageLink, entityFields, null, attributeFilters); - originalData = entityService.findEntityDataByQuery(tenantId, new CustomerId(CustomerId.NULL_UUID), originalQuery); + originalData = findByQuery(originalQuery); // query without textSearch - optimization is performing optimizedPageLink = new EntityDataPageLink(pageSize, 0, null, sortOrder); optimizedQuery = new EntityDataQuery(filter, optimizedPageLink, entityFields, null, attributeFilters); - optimizedData = entityService.findEntityDataByQuery(tenantId, new CustomerId(CustomerId.NULL_UUID), optimizedQuery); + optimizedData = findByQuery(optimizedQuery); loadedEntities = getLoadedEntities(optimizedData, optimizedQuery); Assert.assertEquals(expectedDevicesSize, loadedEntities.size()); loadedEntities = getLoadedEntities(originalData, originalQuery); @@ -2354,12 +2338,12 @@ public class EntityServiceTest extends AbstractServiceTest { // query with textSearch - optimization is not performing originalPageLink = new EntityDataPageLink(pageSize, 0, "Device", sortOrder); originalQuery = new EntityDataQuery(filter, originalPageLink, entityFields, null, nameFilters); - originalData = entityService.findEntityDataByQuery(tenantId, new CustomerId(CustomerId.NULL_UUID), originalQuery); + originalData = findByQuery(originalQuery); // query without textSearch - optimization is performing optimizedPageLink = new EntityDataPageLink(pageSize, 0, null, sortOrder); optimizedQuery = new EntityDataQuery(filter, optimizedPageLink, entityFields, null, nameFilters); - optimizedData = entityService.findEntityDataByQuery(tenantId, new CustomerId(CustomerId.NULL_UUID), optimizedQuery); + optimizedData = findByQuery(optimizedQuery); loadedEntities = getLoadedEntities(optimizedData, optimizedQuery); Assert.assertEquals(expectedDevicesSize, loadedEntities.size()); loadedEntities = getLoadedEntities(originalData, originalQuery); @@ -2387,10 +2371,9 @@ public class EntityServiceTest extends AbstractServiceTest { private List getLoadedEntities(PageData data, EntityDataQuery query) { List loadedEntities = new ArrayList<>(data.getData()); - while (data.hasNext()) { query = query.next(); - data = entityService.findEntityDataByQuery(tenantId, new CustomerId(CustomerId.NULL_UUID), query); + data = findByQuery(query); loadedEntities.addAll(data.getData()); } return loadedEntities; @@ -2421,13 +2404,13 @@ public class EntityServiceTest extends AbstractServiceTest { private ListenableFuture> saveLongAttribute(EntityId entityId, String key, long value, AttributeScope scope) { KvEntry attrValue = new LongDataEntry(key, value); AttributeKvEntry attr = new BaseAttributeKvEntry(attrValue, 42L); - return attributesService.save(SYSTEM_TENANT_ID, entityId, scope, Collections.singletonList(attr)); + return attributesService.save(tenantId, entityId, scope, Collections.singletonList(attr)); } private ListenableFuture> saveStringAttribute(EntityId entityId, String key, String value, AttributeScope scope) { KvEntry attrValue = new StringDataEntry(key, value); AttributeKvEntry attr = new BaseAttributeKvEntry(attrValue, 42L); - return attributesService.save(SYSTEM_TENANT_ID, entityId, scope, Collections.singletonList(attr)); + return attributesService.save(tenantId, entityId, scope, Collections.singletonList(attr)); } private ListenableFuture saveLongTimeseries(EntityId entityId, String key, Double value) { @@ -2436,7 +2419,7 @@ public class EntityServiceTest extends AbstractServiceTest { tsKv.setDoubleValue(value); KvEntry telemetryValue = new DoubleDataEntry(key, value); BasicTsKvEntry timeseries = new BasicTsKvEntry(42L, telemetryValue); - return timeseriesService.save(SYSTEM_TENANT_ID, entityId, timeseries); + return timeseriesService.save(tenantId, entityId, timeseries); } private void createMultiRootHierarchy(List buildings, List apartments, @@ -2510,4 +2493,78 @@ public class EntityServiceTest extends AbstractServiceTest { } } } + + @Test + public void testFindEntitiesWithEntityViewFilter() { + EntityView entityView = new EntityView(); + entityView.setTenantId(tenantId); + entityView.setCustomerId(customerId); + entityView.setName("test"); + entityView.setType("default"); + entityView.setEntityId(new DeviceId(UUID.randomUUID())); + entityView.setKeys(new TelemetryEntityView(List.of("test"), null)); + entityView.setStartTimeMs(124); + entityView.setEndTimeMs(256); + entityView.setExternalId(new EntityViewId(UUID.randomUUID())); + entityView.setAdditionalInfo(JacksonUtil.newObjectNode().put("test", "test")); + entityView = entityViewDao.save(tenantId, entityView); + + EntityViewTypeFilter entityViewTypeFilter = new EntityViewTypeFilter(); + entityViewTypeFilter.setEntityViewNameFilter("test"); + entityViewTypeFilter.setEntityViewTypes(List.of("non-existing", "default")); + EntityDataPageLink pageLink = new EntityDataPageLink(10, 0, null, null); + List entityFields = List.of( + new EntityKey(EntityKeyType.ENTITY_FIELD, "name") + ); + EntityDataQuery query = new EntityDataQuery(entityViewTypeFilter, pageLink, entityFields, Collections.emptyList(), null); + + PageData relationsResult = entityService.findEntityDataByQuery(tenantId, new CustomerId(EntityId.NULL_UUID), query); + assertThat(relationsResult.getData()).hasSize(1); + assertThat(relationsResult.getData().get(0).getLatest().get(EntityKeyType.ENTITY_FIELD).get("name").getValue()).isEqualTo(entityView.getName()); + + // find with non existing name + entityViewTypeFilter.setEntityViewNameFilter("non-existing"); + PageData relationsResult2 = entityService.findEntityDataByQuery(tenantId, new CustomerId(EntityId.NULL_UUID), query); + assertThat(relationsResult2.getData()).hasSize(0); + + // find with non existing type + entityViewTypeFilter.setEntityViewNameFilter(null); + entityViewTypeFilter.setEntityViewTypes(Collections.singletonList("non-existing")); + + PageData relationsResult3 = entityService.findEntityDataByQuery(tenantId, new CustomerId(EntityId.NULL_UUID), query); + assertThat(relationsResult3.getData()).hasSize(0); + } + + private PageData findByQuery(EntityDataQuery query) { + return findByQuery(new CustomerId(CustomerId.NULL_UUID), query); + } + + protected PageData findByQuery(CustomerId customerId, EntityDataQuery query) { + return entityService.findEntityDataByQuery(tenantId, customerId, query); + } + + private PageData findByQueryAndCheck(EntityDataQuery query, long expectedResultSize) { + return findByQueryAndCheck(new CustomerId(CustomerId.NULL_UUID), query, expectedResultSize); + } + + protected PageData findByQueryAndCheck(CustomerId customerId, EntityDataQuery query, long expectedResultSize) { + PageData result = entityService.findEntityDataByQuery(tenantId, customerId, query); + assertThat(result.getTotalElements()).isEqualTo(expectedResultSize); + return result; + } + + protected long countByQuery(CustomerId customerId, EntityCountQuery query) { + return entityService.countEntitiesByQuery(tenantId, customerId, query); + } + + protected long countByQueryAndCheck(EntityCountQuery countQuery, int expectedResult) { + return countByQueryAndCheck(new CustomerId(CustomerId.NULL_UUID), countQuery, expectedResult); + } + + protected long countByQueryAndCheck(CustomerId customerId, EntityCountQuery query, int expectedResult) { + long result = countByQuery(customerId, query); + assertThat(result).isEqualTo(expectedResult); + return result; + } + } diff --git a/application/src/test/java/org/thingsboard/server/service/queue/ruleengine/TbRuleEngineQueueConsumerManagerTest.java b/application/src/test/java/org/thingsboard/server/service/queue/ruleengine/TbRuleEngineQueueConsumerManagerTest.java index 0b6e70b84c..449eca3e47 100644 --- a/application/src/test/java/org/thingsboard/server/service/queue/ruleengine/TbRuleEngineQueueConsumerManagerTest.java +++ b/application/src/test/java/org/thingsboard/server/service/queue/ruleengine/TbRuleEngineQueueConsumerManagerTest.java @@ -626,8 +626,7 @@ public class TbRuleEngineQueueConsumerManagerTest { .until(() -> consumer.subscribed && consumer.getPartitions().equals(expectedPartitions) && consumer.pollingStarted); verify(consumer, times(1)).subscribe(any()); verify(consumer).subscribe(eq(expectedPartitions)); - verify(consumer).doSubscribe(argThat(topics -> topics.containsAll(expectedPartitions.stream() - .map(TopicPartitionInfo::getFullTopicName).collect(Collectors.toList())))); + verify(consumer).doSubscribe(argThat(topics -> topics.containsAll(expectedPartitions))); verify(consumer, atLeastOnce()).poll(eq((long) queue.getPollInterval())); verify(consumer, atLeastOnce()).doPoll(eq((long) queue.getPollInterval())); verify(consumer, never()).unsubscribe(); @@ -743,9 +742,11 @@ public class TbRuleEngineQueueConsumerManagerTest { } @Override - protected void doSubscribe(List topicNames) { - log.debug("doSubscribe({})", topicNames); - this.topics = topicNames; + protected void doSubscribe(Set partitions) { + this.topics = partitions.stream() + .map(TopicPartitionInfo::getFullTopicName) + .collect(Collectors.toList()); + log.debug("doSubscribe({})", topics); subscribed = true; } diff --git a/application/src/test/java/org/thingsboard/server/service/state/DefaultDeviceStateServiceTest.java b/application/src/test/java/org/thingsboard/server/service/state/DefaultDeviceStateServiceTest.java index b58d19e0e5..9880ec964b 100644 --- a/application/src/test/java/org/thingsboard/server/service/state/DefaultDeviceStateServiceTest.java +++ b/application/src/test/java/org/thingsboard/server/service/state/DefaultDeviceStateServiceTest.java @@ -556,7 +556,7 @@ public class DefaultDeviceStateServiceTest { .thenReturn(new PageData<>(List.of(deviceIdInfo), 0, 1, false)); PartitionChangeEvent event = new PartitionChangeEvent(this, ServiceType.TB_CORE, Map.of( new QueueKey(ServiceType.TB_CORE), Collections.singleton(tpi) - )); + ), Collections.emptyMap()); service.onApplicationEvent(event); Thread.sleep(100); } diff --git a/application/src/test/resources/application-test.properties b/application/src/test/resources/application-test.properties index 9951caa876..ba6863c705 100644 --- a/application/src/test/resources/application-test.properties +++ b/application/src/test/resources/application-test.properties @@ -53,6 +53,9 @@ sql.ttl.audit_logs.ttl=2592000 sql.edge_events.partition_size=168 sql.ttl.edge_events.edge_event_ttl=2592000 -server.log_controller_error_stack_trace=false +server.log_controller_error_stack_trace=true transport.gateway.dashboard.sync.enabled=false + +queue.edqs.sync_enabled=false +queue.edqs.api_enabled=false diff --git a/application/src/test/resources/logback-test.xml b/application/src/test/resources/logback-test.xml index 13c93da411..a0efcf52c1 100644 --- a/application/src/test/resources/logback-test.xml +++ b/application/src/test/resources/logback-test.xml @@ -9,7 +9,7 @@ - + diff --git a/common/cluster-api/src/main/java/org/thingsboard/server/queue/TbQueueRequestTemplate.java b/common/cluster-api/src/main/java/org/thingsboard/server/queue/TbQueueRequestTemplate.java index 103d7fe9cd..1b14ec4a23 100644 --- a/common/cluster-api/src/main/java/org/thingsboard/server/queue/TbQueueRequestTemplate.java +++ b/common/cluster-api/src/main/java/org/thingsboard/server/queue/TbQueueRequestTemplate.java @@ -26,6 +26,8 @@ public interface TbQueueRequestTemplate send(Request request, long timeoutNs); + ListenableFuture send(Request request, Integer partition); + void stop(); void setMessagesStats(MessagesStats messagesStats); diff --git a/common/cluster-api/src/main/java/org/thingsboard/server/queue/TbQueueResponseTemplate.java b/common/cluster-api/src/main/java/org/thingsboard/server/queue/TbQueueResponseTemplate.java index 641b4d8faf..bf77c8501e 100644 --- a/common/cluster-api/src/main/java/org/thingsboard/server/queue/TbQueueResponseTemplate.java +++ b/common/cluster-api/src/main/java/org/thingsboard/server/queue/TbQueueResponseTemplate.java @@ -15,9 +15,17 @@ */ package org.thingsboard.server.queue; +import org.thingsboard.server.common.msg.queue.TopicPartitionInfo; + +import java.util.Set; + public interface TbQueueResponseTemplate { - void init(TbQueueHandler handler); + void subscribe(); + + void subscribe(Set partitions); + + void launch(TbQueueHandler handler); void stop(); } diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/ObjectType.java b/common/data/src/main/java/org/thingsboard/server/common/data/ObjectType.java new file mode 100644 index 0000000000..c78998cd6a --- /dev/null +++ b/common/data/src/main/java/org/thingsboard/server/common/data/ObjectType.java @@ -0,0 +1,102 @@ +/** + * Copyright © 2016-2024 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.common.data; + +import java.util.Arrays; +import java.util.EnumSet; +import java.util.HashSet; +import java.util.Set; + +public enum ObjectType { + TENANT, + TENANT_PROFILE, + CUSTOMER, + ADMIN_SETTINGS, + QUEUE, + RPC, + RULE_CHAIN, + OTA_PACKAGE, + RESOURCE, + ROLE, + ENTITY_GROUP, + DEVICE_GROUP_OTA_PACKAGE, + GROUP_PERMISSION, + BLOB_ENTITY, + SCHEDULER_EVENT, + EVENT, + RULE_NODE, + CONVERTER, + INTEGRATION, + USER, + USER_CREDENTIALS, + USER_AUTH_SETTINGS, + EDGE, + WIDGETS_BUNDLE, + WIDGET_TYPE, + DASHBOARD, + DEVICE_PROFILE, + DEVICE, + DEVICE_CREDENTIALS, + ASSET_PROFILE, + ASSET, + ENTITY_VIEW, + ALARM, + ENTITY_ALARM, + OAUTH2_CLIENT, + OAUTH2_DOMAIN, + OAUTH2_MOBILE, + USER_SETTINGS, + NOTIFICATION_TARGET, + NOTIFICATION_TEMPLATE, + NOTIFICATION_RULE, + WHITE_LABELING, + CUSTOM_TRANSLATION, + ALARM_COMMENT, + ALARM_TYPE, + API_USAGE_STATE, + QUEUE_STATS, + + AUDIT_LOG, + RELATION, + ATTRIBUTE_KV, + LATEST_TS_KV; + + public static final Set edqsTenantTypes = EnumSet.of( + TENANT_PROFILE, CUSTOMER, DEVICE_PROFILE, DEVICE, ASSET_PROFILE, ASSET, EDGE, ENTITY_VIEW, USER, DASHBOARD, + RULE_CHAIN, WIDGET_TYPE, WIDGETS_BUNDLE, CONVERTER, INTEGRATION, SCHEDULER_EVENT, ROLE, + BLOB_ENTITY, API_USAGE_STATE, QUEUE_STATS + ); + public static final Set edqsTypes = new HashSet<>(edqsTenantTypes); + public static final Set edqsSystemTypes = EnumSet.of(TENANT, TENANT_PROFILE, USER, DASHBOARD, + API_USAGE_STATE, ATTRIBUTE_KV, LATEST_TS_KV); + + static { + edqsTypes.addAll(Arrays.asList(TENANT, ENTITY_GROUP, RELATION, ATTRIBUTE_KV, LATEST_TS_KV)); + } + + public EntityType toEntityType() { + return EntityType.valueOf(name()); + } + + public static ObjectType fromEntityType(EntityType entityType) { + try { + return ObjectType.valueOf(entityType.name()); + } catch (Exception e) { + return null; + } + } + +} diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/alarm/AlarmType.java b/common/data/src/main/java/org/thingsboard/server/common/data/alarm/AlarmType.java new file mode 100644 index 0000000000..0813e72123 --- /dev/null +++ b/common/data/src/main/java/org/thingsboard/server/common/data/alarm/AlarmType.java @@ -0,0 +1,27 @@ +/** + * Copyright © 2016-2024 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.common.data.alarm; + +import lombok.Data; +import org.thingsboard.server.common.data.id.TenantId; + +@Data +public class AlarmType { + + private TenantId tenantId; + private String type; + +} diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/edqs/AttributeKv.java b/common/data/src/main/java/org/thingsboard/server/common/data/edqs/AttributeKv.java new file mode 100644 index 0000000000..f2c35466dc --- /dev/null +++ b/common/data/src/main/java/org/thingsboard/server/common/data/edqs/AttributeKv.java @@ -0,0 +1,67 @@ +/** + * Copyright © 2016-2024 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.common.data.edqs; + +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; +import org.thingsboard.server.common.data.AttributeScope; +import org.thingsboard.server.common.data.id.EntityId; +import org.thingsboard.server.common.data.kv.AttributeKvEntry; +import org.thingsboard.server.common.data.kv.KvEntry; + +@Data +@AllArgsConstructor +@NoArgsConstructor +@Builder +public class AttributeKv implements EdqsObject { + + private EntityId entityId; + private AttributeScope scope; + private String key; + private Long version; + + private Long lastUpdateTs; // optional (on deletion) + private KvEntry value; // optional (on deletion) + + public AttributeKv(EntityId entityId, AttributeScope scope, AttributeKvEntry attributeKvEntry, long version) { + this.entityId = entityId; + this.scope = scope; + this.key = attributeKvEntry.getKey(); + this.version = version; + this.lastUpdateTs = attributeKvEntry.getLastUpdateTs(); + this.value = attributeKvEntry; + } + + public AttributeKv(EntityId entityId, AttributeScope scope, String key, long version) { + this.entityId = entityId; + this.scope = scope; + this.key = key; + this.version = version; + } + + @Override + public String key() { + return "a_" + entityId + "_" + scope + "_" + key; + } + + @Override + public Long version() { + return version; + } + +} diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/edqs/EdqsEvent.java b/common/data/src/main/java/org/thingsboard/server/common/data/edqs/EdqsEvent.java new file mode 100644 index 0000000000..b070121baa --- /dev/null +++ b/common/data/src/main/java/org/thingsboard/server/common/data/edqs/EdqsEvent.java @@ -0,0 +1,34 @@ +/** + * Copyright © 2016-2024 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.common.data.edqs; + +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import org.thingsboard.server.common.data.ObjectType; +import org.thingsboard.server.common.data.id.TenantId; + +@Data +@AllArgsConstructor +@Builder +public class EdqsEvent { + + private final TenantId tenantId; + private final ObjectType objectType; + private final EdqsEventType eventType; + private final EdqsObject object; + +} diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/edqs/EdqsEventType.java b/common/data/src/main/java/org/thingsboard/server/common/data/edqs/EdqsEventType.java new file mode 100644 index 0000000000..2907691f91 --- /dev/null +++ b/common/data/src/main/java/org/thingsboard/server/common/data/edqs/EdqsEventType.java @@ -0,0 +1,21 @@ +/** + * Copyright © 2016-2024 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.common.data.edqs; + +public enum EdqsEventType { + UPDATED, + DELETED +} diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/edqs/EdqsObject.java b/common/data/src/main/java/org/thingsboard/server/common/data/edqs/EdqsObject.java new file mode 100644 index 0000000000..9a2836149a --- /dev/null +++ b/common/data/src/main/java/org/thingsboard/server/common/data/edqs/EdqsObject.java @@ -0,0 +1,28 @@ +/** + * Copyright © 2016-2024 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.common.data.edqs; + +import com.fasterxml.jackson.annotation.JsonIgnore; + +public interface EdqsObject { + + @JsonIgnore + String key(); + + @JsonIgnore + Long version(); + +} diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/edqs/EdqsSyncRequest.java b/common/data/src/main/java/org/thingsboard/server/common/data/edqs/EdqsSyncRequest.java new file mode 100644 index 0000000000..69bb4b8888 --- /dev/null +++ b/common/data/src/main/java/org/thingsboard/server/common/data/edqs/EdqsSyncRequest.java @@ -0,0 +1,24 @@ +/** + * Copyright © 2016-2024 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.common.data.edqs; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import lombok.Data; + +@Data +@JsonIgnoreProperties +public class EdqsSyncRequest { +} diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/edqs/Entity.java b/common/data/src/main/java/org/thingsboard/server/common/data/edqs/Entity.java new file mode 100644 index 0000000000..decfbbd116 --- /dev/null +++ b/common/data/src/main/java/org/thingsboard/server/common/data/edqs/Entity.java @@ -0,0 +1,62 @@ +/** + * Copyright © 2016-2024 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.common.data.edqs; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonTypeInfo; +import lombok.Data; +import lombok.NoArgsConstructor; +import org.thingsboard.server.common.data.EntityType; +import org.thingsboard.server.common.data.edqs.fields.EntityFields; +import org.thingsboard.server.common.data.edqs.fields.EntityIdFields; + +import java.util.UUID; + +@Data +@NoArgsConstructor +public class Entity implements EdqsObject { + + private EntityType type; + + @JsonIgnoreProperties(ignoreUnknown = true) + @JsonTypeInfo(use = JsonTypeInfo.Id.CLASS) + private EntityFields fields; + + public Entity(EntityType type) { + this.type = type; + } + + public Entity(EntityType type, EntityFields fields) { + this.type = type; + this.fields = fields; + } + + public Entity(EntityType entityType, UUID id, long version) { + this.type = entityType; + this.fields = new EntityIdFields(id, version); + } + + @Override + public String key() { + return "e_" + fields.getId().toString(); + } + + @Override + public Long version() { + return fields.getVersion(); + } + +} diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/edqs/LatestTsKv.java b/common/data/src/main/java/org/thingsboard/server/common/data/edqs/LatestTsKv.java new file mode 100644 index 0000000000..8aaabad1a7 --- /dev/null +++ b/common/data/src/main/java/org/thingsboard/server/common/data/edqs/LatestTsKv.java @@ -0,0 +1,62 @@ +/** + * Copyright © 2016-2024 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.common.data.edqs; + +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; +import org.thingsboard.server.common.data.id.EntityId; +import org.thingsboard.server.common.data.kv.KvEntry; +import org.thingsboard.server.common.data.kv.TsKvEntry; + +@Data +@AllArgsConstructor +@NoArgsConstructor +@Builder +public class LatestTsKv implements EdqsObject { + + private EntityId entityId; + private String key; + private Long version; + + private Long ts; // optional (on deletion) + private KvEntry value; // optional (on deletion) + + public LatestTsKv(EntityId entityId, TsKvEntry tsKvEntry, Long version) { + this.entityId = entityId; + this.key = tsKvEntry.getKey(); + this.ts = tsKvEntry.getTs(); + this.version = version != null ? version : 0L; + this.value = tsKvEntry; + } + + public LatestTsKv(EntityId entityId, String key, Long version) { + this.entityId = entityId; + this.key = key; + this.version = version != null ? version : 0L; + } + + public String key() { + return "l_" + entityId + "_" + key; + } + + @Override + public Long version() { + return version; + } + +} diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/edqs/ToCoreEdqsMsg.java b/common/data/src/main/java/org/thingsboard/server/common/data/edqs/ToCoreEdqsMsg.java new file mode 100644 index 0000000000..802a9c23cf --- /dev/null +++ b/common/data/src/main/java/org/thingsboard/server/common/data/edqs/ToCoreEdqsMsg.java @@ -0,0 +1,32 @@ +/** + * Copyright © 2016-2024 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.common.data.edqs; + +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +@Data +@AllArgsConstructor +@NoArgsConstructor +@Builder +public class ToCoreEdqsMsg { + + private EdqsSyncRequest syncRequest; + private Boolean apiEnabled; + +} diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/edqs/ToCoreEdqsRequest.java b/common/data/src/main/java/org/thingsboard/server/common/data/edqs/ToCoreEdqsRequest.java new file mode 100644 index 0000000000..e5382a43c1 --- /dev/null +++ b/common/data/src/main/java/org/thingsboard/server/common/data/edqs/ToCoreEdqsRequest.java @@ -0,0 +1,38 @@ +/** + * Copyright © 2016-2024 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.common.data.edqs; + +import com.fasterxml.jackson.annotation.JsonIgnore; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +@Data +@NoArgsConstructor +@AllArgsConstructor +@Builder +public class ToCoreEdqsRequest { + + private EdqsSyncRequest syncRequest; + private Boolean apiEnabled; + + @JsonIgnore + public ToCoreEdqsMsg toInternalMsg() { + return new ToCoreEdqsMsg(syncRequest, apiEnabled); + } + +} diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/edqs/fields/AbstractEntityFields.java b/common/data/src/main/java/org/thingsboard/server/common/data/edqs/fields/AbstractEntityFields.java new file mode 100644 index 0000000000..f520e60bdc --- /dev/null +++ b/common/data/src/main/java/org/thingsboard/server/common/data/edqs/fields/AbstractEntityFields.java @@ -0,0 +1,65 @@ +/** + * Copyright © 2016-2024 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.common.data.edqs.fields; + +import lombok.Data; +import lombok.experimental.SuperBuilder; +import org.thingsboard.server.common.data.id.CustomerId; + +import java.util.UUID; + +@Data +@SuperBuilder +public class AbstractEntityFields implements EntityFields { + + private UUID id; + private long createdTime; + private UUID tenantId; + private UUID customerId; + private String name; + private Long version; + + public AbstractEntityFields(UUID id, long createdTime, UUID tenantId, UUID customerId, String name, Long version) { + this.id = id; + this.createdTime = createdTime; + this.tenantId = tenantId; + this.customerId = (customerId != null && customerId != CustomerId.NULL_UUID) ? customerId : null; + this.name = name; + this.version = version; + } + + public AbstractEntityFields() { + } + + public AbstractEntityFields(UUID id, long createdTime, UUID tenantId, String name, Long version) { + this(id, createdTime, tenantId, null, name, version); + } + + public AbstractEntityFields(UUID id, long createdTime, UUID tenantId, UUID customerId, Long version) { + this(id, createdTime, tenantId, customerId, null, version); + + } + + public AbstractEntityFields(UUID id, long createdTime, String name, Long version) { + this(id, createdTime, null, name, version); + } + + + public AbstractEntityFields(UUID id, long createdTime, UUID tenantId) { + this(id, createdTime, tenantId, null, null, null); + } + +} diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/edqs/fields/ApiUsageStateFields.java b/common/data/src/main/java/org/thingsboard/server/common/data/edqs/fields/ApiUsageStateFields.java new file mode 100644 index 0000000000..f81604cfbc --- /dev/null +++ b/common/data/src/main/java/org/thingsboard/server/common/data/edqs/fields/ApiUsageStateFields.java @@ -0,0 +1,58 @@ +/** + * Copyright © 2016-2024 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.common.data.edqs.fields; + +import lombok.Data; +import lombok.EqualsAndHashCode; +import lombok.NoArgsConstructor; +import lombok.experimental.SuperBuilder; +import org.thingsboard.server.common.data.ApiUsageStateValue; +import org.thingsboard.server.common.data.id.EntityId; +import org.thingsboard.server.common.data.id.EntityIdFactory; + +import java.util.UUID; + +@Data +@EqualsAndHashCode(callSuper = true) +@NoArgsConstructor +@SuperBuilder +public class ApiUsageStateFields extends AbstractEntityFields { + + private EntityId entityId; + private ApiUsageStateValue transportState; + private ApiUsageStateValue dbStorageState; + private ApiUsageStateValue reExecState; + private ApiUsageStateValue jsExecState; + private ApiUsageStateValue tbelExecState; + private ApiUsageStateValue emailExecState; + private ApiUsageStateValue smsExecState; + private ApiUsageStateValue alarmExecState; + + public ApiUsageStateFields(UUID id, long createdTime, UUID tenantId, UUID entityId, String entityType, ApiUsageStateValue transportState, ApiUsageStateValue dbStorageState, + ApiUsageStateValue reExecState, ApiUsageStateValue jsExecState, ApiUsageStateValue tbelExecState, + ApiUsageStateValue emailExecState, ApiUsageStateValue smsExecState, ApiUsageStateValue alarmExecState) { + super(id, createdTime, tenantId); + this.entityId = (entityType != null && entityId != null) ? EntityIdFactory.getByTypeAndUuid(entityType, entityId) : null; + this.transportState = transportState; + this.dbStorageState = dbStorageState; + this.reExecState = reExecState; + this.jsExecState = jsExecState; + this.tbelExecState = tbelExecState; + this.emailExecState = emailExecState; + this.smsExecState = smsExecState; + this.alarmExecState = alarmExecState; + } +} diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/edqs/fields/AssetFields.java b/common/data/src/main/java/org/thingsboard/server/common/data/edqs/fields/AssetFields.java new file mode 100644 index 0000000000..c5845efb87 --- /dev/null +++ b/common/data/src/main/java/org/thingsboard/server/common/data/edqs/fields/AssetFields.java @@ -0,0 +1,58 @@ +/** + * Copyright © 2016-2024 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.common.data.edqs.fields; + +import com.fasterxml.jackson.annotation.JsonIgnore; +import com.fasterxml.jackson.databind.JsonNode; +import lombok.Data; +import lombok.NoArgsConstructor; +import lombok.experimental.SuperBuilder; + +import java.util.UUID; + +import static org.thingsboard.server.common.data.edqs.fields.FieldsUtil.getText; + +@Data +@NoArgsConstructor +@SuperBuilder +public class AssetFields extends AbstractEntityFields implements ProfileAwareFields { + + private String type; + private UUID assetProfileId; + private String label; + private String additionalInfo; + + @JsonIgnore + @Override + public String getProfileName() { + return type; + } + + @JsonIgnore + @Override + public UUID getProfileId() { + return assetProfileId; + } + + public AssetFields(UUID id, long createdTime, UUID tenantId, UUID customerId, String name, + Long version, String type, String label, UUID assetProfileId, JsonNode additionalInfo) { + super(id, createdTime, tenantId, customerId, name, version); + this.type = type; + this.assetProfileId = assetProfileId; + this.label = label; + this.additionalInfo = getText(additionalInfo); + } +} diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/edqs/fields/AssetProfileFields.java b/common/data/src/main/java/org/thingsboard/server/common/data/edqs/fields/AssetProfileFields.java new file mode 100644 index 0000000000..bbe7efb56a --- /dev/null +++ b/common/data/src/main/java/org/thingsboard/server/common/data/edqs/fields/AssetProfileFields.java @@ -0,0 +1,35 @@ +/** + * Copyright © 2016-2024 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.common.data.edqs.fields; + +import lombok.Data; +import lombok.NoArgsConstructor; +import lombok.experimental.SuperBuilder; + +import java.util.UUID; + +@Data +@NoArgsConstructor +@SuperBuilder +public class AssetProfileFields extends AbstractEntityFields { + + private boolean isDefault; + + public AssetProfileFields(UUID id, long createdTime, UUID tenantId, String name, Long version, boolean isDefault) { + super(id, createdTime, tenantId, null, name, version); + this.isDefault = isDefault; + } +} diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/edqs/fields/CustomerFields.java b/common/data/src/main/java/org/thingsboard/server/common/data/edqs/fields/CustomerFields.java new file mode 100644 index 0000000000..00ab3776f8 --- /dev/null +++ b/common/data/src/main/java/org/thingsboard/server/common/data/edqs/fields/CustomerFields.java @@ -0,0 +1,55 @@ +/** + * Copyright © 2016-2024 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.common.data.edqs.fields; + +import com.fasterxml.jackson.databind.JsonNode; +import lombok.Data; +import lombok.NoArgsConstructor; +import lombok.experimental.SuperBuilder; + +import java.util.UUID; + +import static org.thingsboard.server.common.data.edqs.fields.FieldsUtil.getText; + +@Data +@NoArgsConstructor +@SuperBuilder +public class CustomerFields extends AbstractEntityFields { + + private String additionalInfo; + private String country; + private String state; + private String city; + private String address; + private String address2; + private String zip; + private String phone; + private String email; + + public CustomerFields(UUID id, long createdTime, UUID tenantId, String name, Long version, JsonNode additionalInfo, + String country, String state, String city, String address, String address2, String zip, String phone, String email) { + super(id, createdTime, tenantId, name, version); + this.additionalInfo = getText(additionalInfo); + this.country = country; + this.state = state; + this.city = city; + this.address = address; + this.address2 = address2; + this.zip = zip; + this.phone = phone; + this.email = email; + } +} diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/edqs/fields/DashboardFields.java b/common/data/src/main/java/org/thingsboard/server/common/data/edqs/fields/DashboardFields.java new file mode 100644 index 0000000000..af1640b7be --- /dev/null +++ b/common/data/src/main/java/org/thingsboard/server/common/data/edqs/fields/DashboardFields.java @@ -0,0 +1,33 @@ +/** + * Copyright © 2016-2024 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.common.data.edqs.fields; + +import lombok.Data; +import lombok.NoArgsConstructor; +import lombok.experimental.SuperBuilder; + +import java.util.UUID; + + +@Data +@NoArgsConstructor +@SuperBuilder +public class DashboardFields extends AbstractEntityFields { + + public DashboardFields(UUID id, long createdTime, UUID tenantId, UUID customerId, String name, Long version) { + super(id, createdTime, tenantId, customerId, name, version); + } +} diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/edqs/fields/DeviceFields.java b/common/data/src/main/java/org/thingsboard/server/common/data/edqs/fields/DeviceFields.java new file mode 100644 index 0000000000..f1622b83d9 --- /dev/null +++ b/common/data/src/main/java/org/thingsboard/server/common/data/edqs/fields/DeviceFields.java @@ -0,0 +1,58 @@ +/** + * Copyright © 2016-2024 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.common.data.edqs.fields; + +import com.fasterxml.jackson.annotation.JsonIgnore; +import com.fasterxml.jackson.databind.JsonNode; +import lombok.Data; +import lombok.NoArgsConstructor; +import lombok.experimental.SuperBuilder; + +import java.util.UUID; + +import static org.thingsboard.server.common.data.edqs.fields.FieldsUtil.getText; + +@Data +@NoArgsConstructor +@SuperBuilder +public class DeviceFields extends AbstractEntityFields implements ProfileAwareFields { + + private String label; + private String type; + private UUID deviceProfileId; + private String additionalInfo; + + @JsonIgnore + @Override + public String getProfileName() { + return type; + } + + @JsonIgnore + @Override + public UUID getProfileId() { + return deviceProfileId; + } + + public DeviceFields(UUID id, long createdTime, UUID tenantId, UUID customerId, String name, Long version, String type, + String label, UUID deviceProfileId, JsonNode additionalInfo) { + super(id, createdTime, tenantId, customerId, name, version); + this.label = label; + this.type = type; + this.deviceProfileId = deviceProfileId; + this.additionalInfo = getText(additionalInfo); + } +} diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/edqs/fields/DeviceProfileFields.java b/common/data/src/main/java/org/thingsboard/server/common/data/edqs/fields/DeviceProfileFields.java new file mode 100644 index 0000000000..5015196f69 --- /dev/null +++ b/common/data/src/main/java/org/thingsboard/server/common/data/edqs/fields/DeviceProfileFields.java @@ -0,0 +1,38 @@ +/** + * Copyright © 2016-2024 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.common.data.edqs.fields; + +import lombok.Data; +import lombok.NoArgsConstructor; +import lombok.experimental.SuperBuilder; +import org.thingsboard.server.common.data.DeviceProfileType; + +import java.util.UUID; + +@Data +@NoArgsConstructor +@SuperBuilder +public class DeviceProfileFields extends AbstractEntityFields { + + private String type; + private boolean isDefault; + + public DeviceProfileFields(UUID id, long createdTime, UUID tenantId, String name, Long version, DeviceProfileType type, boolean isDefault) { + super(id, createdTime, tenantId, null, name, version); + this.type = type.name(); + this.isDefault = isDefault; + } +} diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/edqs/fields/EdgeFields.java b/common/data/src/main/java/org/thingsboard/server/common/data/edqs/fields/EdgeFields.java new file mode 100644 index 0000000000..460a43e7c8 --- /dev/null +++ b/common/data/src/main/java/org/thingsboard/server/common/data/edqs/fields/EdgeFields.java @@ -0,0 +1,43 @@ +/** + * Copyright © 2016-2024 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.common.data.edqs.fields; + +import com.fasterxml.jackson.databind.JsonNode; +import lombok.Data; +import lombok.NoArgsConstructor; +import lombok.experimental.SuperBuilder; + +import java.util.UUID; + +import static org.thingsboard.server.common.data.edqs.fields.FieldsUtil.getText; + +@Data +@NoArgsConstructor +@SuperBuilder +public class EdgeFields extends AbstractEntityFields { + + private String type; + private String label; + private String additionalInfo; + + public EdgeFields(UUID id, long createdTime, UUID tenantId, UUID customerId, String name, Long version, + String type, String label, JsonNode additionalInfo) { + super(id, createdTime, tenantId, customerId, name, version); + this.type = type; + this.label = label; + this.additionalInfo = getText(additionalInfo); + } +} diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/edqs/fields/EntityFields.java b/common/data/src/main/java/org/thingsboard/server/common/data/edqs/fields/EntityFields.java new file mode 100644 index 0000000000..7536586f57 --- /dev/null +++ b/common/data/src/main/java/org/thingsboard/server/common/data/edqs/fields/EntityFields.java @@ -0,0 +1,171 @@ +/** + * Copyright © 2016-2024 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.common.data.edqs.fields; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.thingsboard.server.common.data.id.EntityId; + +import java.util.UUID; + +public interface EntityFields { + + Logger log = LoggerFactory.getLogger(EntityFields.class); + + default UUID getId() { + return null; + } + + default UUID getTenantId() { + return null; + } + + default UUID getCustomerId() { + return null; + } + + default long getCreatedTime() { + return 0; + } + + default String getName() { + return ""; + } + + default String getType() { + return ""; + } + + default String getLabel() { + return ""; + } + + default String getAdditionalInfo() { + return ""; + } + + default String getEmail() { + return ""; + } + + default String getCountry() { + return ""; + } + + default String getState() { + return ""; + } + + default String getCity() { + return ""; + } + + default String getAddress() { + return ""; + } + + default String getAddress2() { + return ""; + } + + default String getZip() { + return ""; + } + + default String getPhone() { + return ""; + } + + default String getRegion() { + return ""; + } + + default String getFirstName() { + return ""; + } + + default String getLastName() { + return ""; + } + + default boolean isEdgeTemplate() { + return false; + } + + default String getConfiguration() { + return ""; + } + + default String getSchedule() { + return ""; + } + + default EntityId getOriginatorId() { + return null; + } + + default String getQueueName() { + return ""; + } + + default String getServiceId() { + return ""; + } + + default boolean isDefault() { + return false; + } + + default UUID getOwnerId() { + return null; + } + + default Long getVersion() { + return null; + } + + default String getAsString(String key) { + return switch (key) { + case "createdTime" -> Long.toString(getCreatedTime()); + case "type" -> getType(); + case "label" -> getLabel(); + case "additionalInfo" -> getAdditionalInfo(); + case "email" -> getEmail(); + case "country" -> getCountry(); + case "state" -> getState(); + case "city" -> getCity(); + case "address" -> getAddress(); + case "address2" -> getAddress2(); + case "zip" -> getZip(); + case "phone" -> getPhone(); + case "region" -> getRegion(); + case "firstName" -> getFirstName(); + case "lastName" -> getLastName(); + case "edgeTemplate" -> Boolean.toString(isEdgeTemplate()); + case "configuration" -> getConfiguration(); + case "schedule" -> getSchedule(); + case "originatorId" -> getOriginatorId().getId().toString(); + case "originatorType" -> getOriginatorId().getEntityType().toString(); + case "queueName" -> getQueueName(); + case "serviceId" -> getServiceId(); + default -> { + log.warn("Unknown field '{}'", key); + yield null; + } + }; + } + +} diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/edqs/fields/EntityIdFields.java b/common/data/src/main/java/org/thingsboard/server/common/data/edqs/fields/EntityIdFields.java new file mode 100644 index 0000000000..aabac7865f --- /dev/null +++ b/common/data/src/main/java/org/thingsboard/server/common/data/edqs/fields/EntityIdFields.java @@ -0,0 +1,36 @@ +/** + * Copyright © 2016-2024 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.common.data.edqs.fields; + +import lombok.Data; +import lombok.NoArgsConstructor; +import lombok.experimental.SuperBuilder; + +import java.util.UUID; + +@Data +@NoArgsConstructor +@SuperBuilder +public class EntityIdFields implements EntityFields { + + private UUID id; + private Long version; + + public EntityIdFields(UUID id, Long version) { + this.id = id; + this.version = version; + } +} diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/edqs/fields/EntityViewFields.java b/common/data/src/main/java/org/thingsboard/server/common/data/edqs/fields/EntityViewFields.java new file mode 100644 index 0000000000..5635566cc4 --- /dev/null +++ b/common/data/src/main/java/org/thingsboard/server/common/data/edqs/fields/EntityViewFields.java @@ -0,0 +1,30 @@ +/** + * Copyright © 2016-2024 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.common.data.edqs.fields; + +import lombok.Data; +import lombok.NoArgsConstructor; +import lombok.experimental.SuperBuilder; + +@Data +@NoArgsConstructor +@SuperBuilder +public class EntityViewFields extends AbstractEntityFields { + + private String type; + private String additionalInfo; + +} diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/edqs/fields/FieldsUtil.java b/common/data/src/main/java/org/thingsboard/server/common/data/edqs/fields/FieldsUtil.java new file mode 100644 index 0000000000..5a0db392ef --- /dev/null +++ b/common/data/src/main/java/org/thingsboard/server/common/data/edqs/fields/FieldsUtil.java @@ -0,0 +1,298 @@ +/** + * Copyright © 2016-2024 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.common.data.edqs.fields; + +import com.fasterxml.jackson.databind.JsonNode; +import org.thingsboard.server.common.data.ApiUsageState; +import org.thingsboard.server.common.data.Customer; +import org.thingsboard.server.common.data.Dashboard; +import org.thingsboard.server.common.data.Device; +import org.thingsboard.server.common.data.DeviceProfile; +import org.thingsboard.server.common.data.DeviceProfileType; +import org.thingsboard.server.common.data.EntityView; +import org.thingsboard.server.common.data.Tenant; +import org.thingsboard.server.common.data.TenantProfile; +import org.thingsboard.server.common.data.User; +import org.thingsboard.server.common.data.asset.Asset; +import org.thingsboard.server.common.data.asset.AssetProfile; +import org.thingsboard.server.common.data.edge.Edge; +import org.thingsboard.server.common.data.id.CustomerId; +import org.thingsboard.server.common.data.queue.QueueStats; +import org.thingsboard.server.common.data.rule.RuleChain; +import org.thingsboard.server.common.data.rule.RuleNode; +import org.thingsboard.server.common.data.widget.WidgetType; +import org.thingsboard.server.common.data.widget.WidgetsBundle; + +import java.util.UUID; + +public class FieldsUtil { + + public static EntityFields toFields(Object entity) { + if (entity instanceof Customer customer) { + return toFields(customer); + } else if (entity instanceof Tenant tenant) { + return toFields(tenant); + } else if (entity instanceof TenantProfile tenantProfile) { + return toFields(tenantProfile); + } else if (entity instanceof Device device) { + return toFields(device); + } else if (entity instanceof Asset asset) { + return toFields(asset); + } else if (entity instanceof Edge edge) { + return toFields(edge); + } else if (entity instanceof EntityView entityView) { + return toFields(entityView); + } else if (entity instanceof User user) { + return toFields(user); + } else if (entity instanceof Dashboard dashboard) { + return toFields(dashboard); + } else if (entity instanceof RuleChain ruleChain) { + return toFields(ruleChain); + } else if (entity instanceof RuleNode ruleNode) { + return toFields(ruleNode); + } else if (entity instanceof WidgetType widgetType) { + return toFields(widgetType); + } else if (entity instanceof WidgetsBundle widgetsBundle) { + return toFields(widgetsBundle); + } else if (entity instanceof DeviceProfile deviceProfile) { + return toFields(deviceProfile); + } else if (entity instanceof AssetProfile assetProfile) { + return toFields(assetProfile); + } else if (entity instanceof QueueStats queueStats) { + return toFields(queueStats); + } else if (entity instanceof ApiUsageState apiUsageState) { + return toFields(apiUsageState); + } else { + throw new IllegalArgumentException("Unsupported entity type: " + entity.getClass().getName()); + } + } + + private static CustomerFields toFields(Customer entity) { + return CustomerFields.builder() + .id(entity.getUuidId()) + .createdTime(entity.getCreatedTime()) + .customerId(getCustomerId(entity.getCustomerId())) + .name(entity.getTitle()) + .additionalInfo(getText(entity.getAdditionalInfo())) + .email(entity.getEmail()) + .country(entity.getCountry()) + .state(entity.getState()) + .city(entity.getCity()) + .address(entity.getAddress()) + .address2(entity.getAddress2()) + .zip(entity.getZip()) + .phone(entity.getPhone()) + .version(entity.getVersion()) + .build(); + } + + private static TenantFields toFields(Tenant entity) { + return TenantFields.builder() + .id(entity.getUuidId()) + .createdTime(entity.getCreatedTime()) + .name(entity.getTitle()) + .additionalInfo(getText(entity.getAdditionalInfo())) + .email(entity.getEmail()) + .country(entity.getCountry()) + .state(entity.getState()) + .city(entity.getCity()) + .address(entity.getAddress()) + .address2(entity.getAddress2()) + .zip(entity.getZip()) + .phone(entity.getPhone()) + .region(entity.getRegion()) + .version(entity.getVersion()) + .build(); + } + + private static TenantProfileFields toFields(TenantProfile tenantProfile) { + return TenantProfileFields.builder() + .id(tenantProfile.getUuidId()) + .createdTime(tenantProfile.getCreatedTime()) + .name(tenantProfile.getName()) + .isDefault(tenantProfile.isDefault()) + .build(); + } + + private static DeviceFields toFields(Device entity) { + return DeviceFields.builder() + .id(entity.getUuidId()) + .createdTime(entity.getCreatedTime()) + .customerId(getCustomerId(entity.getCustomerId())) + .name(entity.getName()) + .type(entity.getType()) + .deviceProfileId(entity.getDeviceProfileId().getId()) + .label(entity.getLabel()) + .additionalInfo(getText(entity.getAdditionalInfo())) + .version(entity.getVersion()) + .build(); + } + + private static AssetFields toFields(Asset entity) { + return AssetFields.builder() + .id(entity.getUuidId()) + .createdTime(entity.getCreatedTime()) + .customerId(getCustomerId(entity.getCustomerId())) + .name(entity.getName()) + .type(entity.getType()) + .assetProfileId(entity.getAssetProfileId().getId()) + .label(entity.getLabel()) + .additionalInfo(getText(entity.getAdditionalInfo())) + .version(entity.getVersion()) + .build(); + } + + private static EdgeFields toFields(Edge entity) { + return EdgeFields.builder() + .id(entity.getUuidId()) + .createdTime(entity.getCreatedTime()) + .customerId(getCustomerId(entity.getCustomerId())) + .name(entity.getName()) + .type(entity.getType()) + .label(entity.getLabel()) + .additionalInfo(getText(entity.getAdditionalInfo())) + .version(entity.getVersion()) + .build(); + } + + private static EntityViewFields toFields(EntityView entity) { + return EntityViewFields.builder() + .id(entity.getUuidId()) + .createdTime(entity.getCreatedTime()) + .customerId(getCustomerId(entity.getCustomerId())) + .name(entity.getName()) + .type(entity.getType()) + .additionalInfo(getText(entity.getAdditionalInfo())) + .version(entity.getVersion()) + .build(); + } + + private static UserFields toFields(User entity) { + return UserFields.builder() + .id(entity.getUuidId()) + .createdTime(entity.getCreatedTime()) + .customerId(getCustomerId(entity.getCustomerId())) + .firstName(entity.getFirstName()) + .lastName(entity.getLastName()) + .email(entity.getEmail()) + .phone(entity.getPhone()) + .additionalInfo(getText(entity.getAdditionalInfo())) + .version(entity.getVersion()) + .build(); + } + + private static DashboardFields toFields(Dashboard entity) { + return DashboardFields.builder() + .id(entity.getUuidId()) + .createdTime(entity.getCreatedTime()) + .customerId(getCustomerId(entity.getCustomerId())) + .name(entity.getTitle()) + .version(entity.getVersion()) + .build(); + } + + private static RuleChainFields toFields(RuleChain entity) { + return RuleChainFields.builder() + .id(entity.getUuidId()) + .createdTime(entity.getCreatedTime()) + .name(entity.getName()) + .additionalInfo(getText(entity.getAdditionalInfo())) + .version(entity.getVersion()) + .build(); + } + + private static RuleNodeFields toFields(RuleNode entity) { + return RuleNodeFields.builder() + .id(entity.getUuidId()) + .createdTime(entity.getCreatedTime()) + .name(entity.getName()) + .additionalInfo(getText(entity.getAdditionalInfo())) + .build(); + } + + private static WidgetTypeFields toFields(WidgetType entity) { + return WidgetTypeFields.builder() + .id(entity.getUuidId()) + .createdTime(entity.getCreatedTime()) + .name(entity.getName()) + .version(entity.getVersion()) + .build(); + } + + private static WidgetsBundleFields toFields(WidgetsBundle entity) { + return WidgetsBundleFields.builder() + .id(entity.getUuidId()) + .createdTime(entity.getCreatedTime()) + .name(entity.getName()) + .version(entity.getVersion()) + .build(); + } + + private static AssetProfileFields toFields(DeviceProfile entity) { + return AssetProfileFields.builder() + .id(entity.getUuidId()) + .createdTime(entity.getCreatedTime()) + .name(entity.getName()) + .isDefault(entity.isDefault()) + .version(entity.getVersion()) + .build(); + } + + private static DeviceProfileFields toFields(AssetProfile entity) { + return DeviceProfileFields.builder() + .id(entity.getUuidId()) + .createdTime(entity.getCreatedTime()) + .name(entity.getName()) + .type(DeviceProfileType.DEFAULT.name()) + .isDefault(entity.isDefault()) + .version(entity.getVersion()) + .build(); + } + + private static QueueStatsFields toFields(QueueStats entity) { + return QueueStatsFields.builder() + .id(entity.getUuidId()) + .createdTime(entity.getCreatedTime()) + .queueName(entity.getQueueName()) + .serviceId(entity.getServiceId()) + .build(); + } + + private static ApiUsageStateFields toFields(ApiUsageState entity) { + return ApiUsageStateFields.builder() + .id(entity.getUuidId()) + .createdTime(entity.getCreatedTime()) + .entityId(entity.getEntityId()) + .transportState(entity.getTransportState()) + .dbStorageState(entity.getDbStorageState()) + .reExecState(entity.getReExecState()) + .jsExecState(entity.getJsExecState()) + .tbelExecState(entity.getTbelExecState()) + .emailExecState(entity.getEmailExecState()) + .smsExecState(entity.getSmsExecState()) + .alarmExecState(entity.getAlarmExecState()) + .build(); + } + + public static String getText(JsonNode node) { + return node != null ? node.asText() : ""; + } + + private static UUID getCustomerId(CustomerId customerId) { + return (customerId != null && !customerId.getId().equals(CustomerId.NULL_UUID)) ? customerId.getId() : null; + } + +} diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/edqs/fields/GenericFields.java b/common/data/src/main/java/org/thingsboard/server/common/data/edqs/fields/GenericFields.java new file mode 100644 index 0000000000..ff3af4ee65 --- /dev/null +++ b/common/data/src/main/java/org/thingsboard/server/common/data/edqs/fields/GenericFields.java @@ -0,0 +1,41 @@ +/** + * Copyright © 2016-2024 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.common.data.edqs.fields; + +import com.fasterxml.jackson.databind.JsonNode; +import lombok.Data; +import lombok.NoArgsConstructor; +import lombok.experimental.SuperBuilder; +import java.util.UUID; + +import static org.thingsboard.server.common.data.edqs.fields.FieldsUtil.getText; + +@Data +@NoArgsConstructor +@SuperBuilder +public class GenericFields extends AbstractEntityFields { + + private String additionalInfo; + + public GenericFields(UUID id, long createdTime, UUID tenantId, String name, Long version, JsonNode additionalInfo) { + super(id, createdTime, tenantId, name, version); + this.additionalInfo = getText(additionalInfo); + } + + public GenericFields(UUID id, long createdTime, UUID tenantId, String name, Long version) { + super(id, createdTime, tenantId, name, version); + } +} diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/edqs/fields/ProfileAwareFields.java b/common/data/src/main/java/org/thingsboard/server/common/data/edqs/fields/ProfileAwareFields.java new file mode 100644 index 0000000000..83134af716 --- /dev/null +++ b/common/data/src/main/java/org/thingsboard/server/common/data/edqs/fields/ProfileAwareFields.java @@ -0,0 +1,26 @@ +/** + * Copyright © 2016-2024 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.common.data.edqs.fields; + +import java.util.UUID; + +public interface ProfileAwareFields extends EntityFields { + + String getProfileName(); + + UUID getProfileId(); + +} diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/edqs/fields/QueueStatsFields.java b/common/data/src/main/java/org/thingsboard/server/common/data/edqs/fields/QueueStatsFields.java new file mode 100644 index 0000000000..32d74a4b39 --- /dev/null +++ b/common/data/src/main/java/org/thingsboard/server/common/data/edqs/fields/QueueStatsFields.java @@ -0,0 +1,42 @@ +/** + * Copyright © 2016-2024 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.common.data.edqs.fields; + +import lombok.Data; +import lombok.NoArgsConstructor; +import lombok.experimental.SuperBuilder; + +import java.util.UUID; + +@Data +@NoArgsConstructor +@SuperBuilder +public class QueueStatsFields extends AbstractEntityFields { + + private String queueName; + private String serviceId; + + @Override + public String getName() { + return queueName + '_' + serviceId; + } + + public QueueStatsFields(UUID id, long createdTime, UUID tenantId, String queueName, String serviceId) { + super(id, createdTime, tenantId); + this.queueName = queueName; + this.serviceId = serviceId; + } +} diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/edqs/fields/RuleChainFields.java b/common/data/src/main/java/org/thingsboard/server/common/data/edqs/fields/RuleChainFields.java new file mode 100644 index 0000000000..ced202eeea --- /dev/null +++ b/common/data/src/main/java/org/thingsboard/server/common/data/edqs/fields/RuleChainFields.java @@ -0,0 +1,38 @@ +/** + * Copyright © 2016-2024 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.common.data.edqs.fields; + +import com.fasterxml.jackson.databind.JsonNode; +import lombok.Data; +import lombok.NoArgsConstructor; +import lombok.experimental.SuperBuilder; + +import java.util.UUID; + +import static org.thingsboard.server.common.data.edqs.fields.FieldsUtil.getText; + +@Data +@NoArgsConstructor +@SuperBuilder +public class RuleChainFields extends AbstractEntityFields { + + private String additionalInfo; + + public RuleChainFields(UUID id, long createdTime, UUID tenantId, String name, Long version, JsonNode additionalInfo) { + super(id, createdTime, tenantId, name, version); + this.additionalInfo = getText(additionalInfo); + } +} diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/edqs/fields/RuleNodeFields.java b/common/data/src/main/java/org/thingsboard/server/common/data/edqs/fields/RuleNodeFields.java new file mode 100644 index 0000000000..82495a96de --- /dev/null +++ b/common/data/src/main/java/org/thingsboard/server/common/data/edqs/fields/RuleNodeFields.java @@ -0,0 +1,43 @@ +/** + * Copyright © 2016-2024 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.common.data.edqs.fields; + +import com.fasterxml.jackson.databind.JsonNode; +import lombok.Data; +import lombok.NoArgsConstructor; +import lombok.experimental.SuperBuilder; + +import java.util.UUID; + +import static org.thingsboard.server.common.data.edqs.fields.FieldsUtil.getText; + +@Data +@NoArgsConstructor +@SuperBuilder +public class RuleNodeFields implements EntityFields { + + private UUID id; + private long createdTime; + private String name; + private String additionalInfo; + + public RuleNodeFields(UUID id, long createdTime, String name, JsonNode additionalInfo) { + this.id = id; + this.createdTime = createdTime; + this.name = name; + this.additionalInfo = getText(additionalInfo); + } +} diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/edqs/fields/TenantFields.java b/common/data/src/main/java/org/thingsboard/server/common/data/edqs/fields/TenantFields.java new file mode 100644 index 0000000000..6942d5ea7b --- /dev/null +++ b/common/data/src/main/java/org/thingsboard/server/common/data/edqs/fields/TenantFields.java @@ -0,0 +1,62 @@ +/** + * Copyright © 2016-2024 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.common.data.edqs.fields; + +import com.fasterxml.jackson.databind.JsonNode; +import lombok.Data; +import lombok.NoArgsConstructor; +import lombok.experimental.SuperBuilder; + +import java.util.UUID; + +import static org.thingsboard.server.common.data.edqs.fields.FieldsUtil.getText; + +@Data +@NoArgsConstructor +@SuperBuilder +public class TenantFields extends AbstractEntityFields { + + private String additionalInfo; + private String country; + private String state; + private String city; + private String address; + private String address2; + private String zip; + private String phone; + private String email; + private String region; + + public TenantFields(UUID id, long createdTime, String name, Long version, + JsonNode additionalInfo, String country, String state, String city, String address, + String address2, String zip, String phone, String email, String region) { + super(id, createdTime, name, version); + this.additionalInfo = getText(additionalInfo); + this.country = country; + this.state = state; + this.city = city; + this.address = address; + this.address2 = address2; + this.zip = zip; + this.phone = phone; + this.email = email; + this.region = region; + } + + public TenantFields(UUID id, Long version) { + super(id, 0L, null, version); + } +} diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/edqs/fields/TenantProfileFields.java b/common/data/src/main/java/org/thingsboard/server/common/data/edqs/fields/TenantProfileFields.java new file mode 100644 index 0000000000..766a160fc2 --- /dev/null +++ b/common/data/src/main/java/org/thingsboard/server/common/data/edqs/fields/TenantProfileFields.java @@ -0,0 +1,36 @@ +/** + * Copyright © 2016-2024 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.common.data.edqs.fields; + +import lombok.Data; +import lombok.NoArgsConstructor; +import lombok.experimental.SuperBuilder; +import org.thingsboard.server.common.data.id.TenantId; + +import java.util.UUID; + +@Data +@NoArgsConstructor +@SuperBuilder +public class TenantProfileFields extends AbstractEntityFields { + + private boolean isDefault; + + public TenantProfileFields(UUID id, long createdTime, String name, boolean isDefault) { + super(id, createdTime, TenantId.SYS_TENANT_ID.getId(), null, name, 0L); + this.isDefault = isDefault; + } +} diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/edqs/fields/UserFields.java b/common/data/src/main/java/org/thingsboard/server/common/data/edqs/fields/UserFields.java new file mode 100644 index 0000000000..98e0efecc2 --- /dev/null +++ b/common/data/src/main/java/org/thingsboard/server/common/data/edqs/fields/UserFields.java @@ -0,0 +1,48 @@ +/** + * Copyright © 2016-2024 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.common.data.edqs.fields; + +import com.fasterxml.jackson.databind.JsonNode; +import lombok.Data; +import lombok.NoArgsConstructor; +import lombok.experimental.SuperBuilder; + +import java.util.UUID; + +import static org.thingsboard.server.common.data.edqs.fields.FieldsUtil.getText; + +@Data +@NoArgsConstructor +@SuperBuilder +public class UserFields extends AbstractEntityFields { + + private String firstName; + private String lastName; + private String email; + private String phone; + private String additionalInfo; + + public UserFields(UUID id, long createdTime, UUID tenantId, UUID customerId, + Long version, String firstName, String lastName, String email, + String phone, JsonNode additionalInfo) { + super(id, createdTime, tenantId, customerId, version); + this.firstName = firstName; + this.lastName = lastName; + this.email = email; + this.phone = phone; + this.additionalInfo = getText(additionalInfo); + } +} diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/edqs/fields/WidgetTypeFields.java b/common/data/src/main/java/org/thingsboard/server/common/data/edqs/fields/WidgetTypeFields.java new file mode 100644 index 0000000000..330199d8b2 --- /dev/null +++ b/common/data/src/main/java/org/thingsboard/server/common/data/edqs/fields/WidgetTypeFields.java @@ -0,0 +1,30 @@ +/** + * Copyright © 2016-2024 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.common.data.edqs.fields; + +import lombok.NoArgsConstructor; +import lombok.experimental.SuperBuilder; + +import java.util.UUID; + +@NoArgsConstructor +@SuperBuilder +public class WidgetTypeFields extends AbstractEntityFields { + + public WidgetTypeFields(UUID id, long createdTime, UUID tenantId, String name, Long version) { + super(id, createdTime, tenantId, name, version); + } +} diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/edqs/fields/WidgetsBundleFields.java b/common/data/src/main/java/org/thingsboard/server/common/data/edqs/fields/WidgetsBundleFields.java new file mode 100644 index 0000000000..88d9a1a96c --- /dev/null +++ b/common/data/src/main/java/org/thingsboard/server/common/data/edqs/fields/WidgetsBundleFields.java @@ -0,0 +1,30 @@ +/** + * Copyright © 2016-2024 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.common.data.edqs.fields; + +import lombok.NoArgsConstructor; +import lombok.experimental.SuperBuilder; + +import java.util.UUID; + +@NoArgsConstructor +@SuperBuilder +public class WidgetsBundleFields extends AbstractEntityFields { + + public WidgetsBundleFields(UUID id, long createdTime, UUID tenantId, String name, Long version) { + super(id, createdTime, tenantId, name, version); + } +} diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/edqs/query/EdqsRequest.java b/common/data/src/main/java/org/thingsboard/server/common/data/edqs/query/EdqsRequest.java new file mode 100644 index 0000000000..135061a842 --- /dev/null +++ b/common/data/src/main/java/org/thingsboard/server/common/data/edqs/query/EdqsRequest.java @@ -0,0 +1,38 @@ +/** + * Copyright © 2016-2024 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.common.data.edqs.query; + +import com.fasterxml.jackson.annotation.JsonIncludeProperties; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; +import org.thingsboard.server.common.data.permission.MergedUserPermissions; +import org.thingsboard.server.common.data.query.EntityCountQuery; +import org.thingsboard.server.common.data.query.EntityDataQuery; + +@Data +@AllArgsConstructor +@NoArgsConstructor +@Builder +public class EdqsRequest { + + private EntityDataQuery entityDataQuery; + private EntityCountQuery entityCountQuery; + @JsonIncludeProperties({"genericPermissions", "groupPermissions"}) + private MergedUserPermissions userPermissions; + +} diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/edqs/query/EdqsResponse.java b/common/data/src/main/java/org/thingsboard/server/common/data/edqs/query/EdqsResponse.java new file mode 100644 index 0000000000..bcb242e083 --- /dev/null +++ b/common/data/src/main/java/org/thingsboard/server/common/data/edqs/query/EdqsResponse.java @@ -0,0 +1,35 @@ +/** + * Copyright © 2016-2024 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.common.data.edqs.query; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; +import org.thingsboard.server.common.data.page.PageData; +import org.thingsboard.server.common.data.query.EntityData; + +@Data +@AllArgsConstructor +@NoArgsConstructor +@JsonIgnoreProperties(ignoreUnknown = true) +public class EdqsResponse { + + private PageData entityDataQueryResult; + private Long entityCountQueryResult; + private String error; + +} diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/edqs/query/QueryResult.java b/common/data/src/main/java/org/thingsboard/server/common/data/edqs/query/QueryResult.java new file mode 100644 index 0000000000..0c45812690 --- /dev/null +++ b/common/data/src/main/java/org/thingsboard/server/common/data/edqs/query/QueryResult.java @@ -0,0 +1,41 @@ +/** + * Copyright © 2016-2024 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.common.data.edqs.query; + +import lombok.Data; +import lombok.RequiredArgsConstructor; +import org.thingsboard.server.common.data.id.EntityId; +import org.thingsboard.server.common.data.query.EntityData; +import org.thingsboard.server.common.data.query.EntityKeyType; +import org.thingsboard.server.common.data.query.TsValue; + +import java.util.Collections; +import java.util.Map; + +@Data +@RequiredArgsConstructor +public class QueryResult { + + private final EntityId entityId; + private final boolean readAttrs; + private final boolean readTs; + private final Map> latest; + + public EntityData toOldEntityData() { + return new EntityData(entityId, readAttrs, readTs, latest, Collections.emptyMap(), Collections.emptyMap()); + } + +} diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/id/UserAuthSettingsId.java b/common/data/src/main/java/org/thingsboard/server/common/data/id/UserAuthSettingsId.java index 1b462db4da..cd1fdf9aa6 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/id/UserAuthSettingsId.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/id/UserAuthSettingsId.java @@ -15,11 +15,15 @@ */ package org.thingsboard.server.common.data.id; +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonProperty; + import java.util.UUID; public class UserAuthSettingsId extends UUIDBased { - public UserAuthSettingsId(UUID id) { + @JsonCreator + public UserAuthSettingsId(@JsonProperty("id") UUID id) { super(id); } diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/page/BasePageDataIterable.java b/common/data/src/main/java/org/thingsboard/server/common/data/page/BasePageDataIterable.java index 8d46a46781..625a1d417c 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/page/BasePageDataIterable.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/page/BasePageDataIterable.java @@ -23,6 +23,7 @@ import java.util.NoSuchElementException; public abstract class BasePageDataIterable implements Iterable, Iterator { private final int fetchSize; + private SortOrder sortOrder; private List currentItems; private int currentIdx; @@ -35,6 +36,12 @@ public abstract class BasePageDataIterable implements Iterable, Iterator iterator() { return this; @@ -43,7 +50,7 @@ public abstract class BasePageDataIterable implements Iterable, Iterator extends BasePageDataIterable { this.function = function; } + public PageDataIterable(FetchFunction function, int fetchSize, SortOrder sortOrder) { + super(fetchSize, sortOrder); + this.function = function; + } + @Override PageData fetchPageData(PageLink link) { return function.fetch(link); diff --git a/dao/src/main/java/org/thingsboard/server/dao/sql/query/QuerySecurityContext.java b/common/data/src/main/java/org/thingsboard/server/common/data/permission/QueryContext.java similarity index 77% rename from dao/src/main/java/org/thingsboard/server/dao/sql/query/QuerySecurityContext.java rename to common/data/src/main/java/org/thingsboard/server/common/data/permission/QueryContext.java index 579c5bfa92..b377d63ee4 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/sql/query/QuerySecurityContext.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/permission/QueryContext.java @@ -13,7 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package org.thingsboard.server.dao.sql.query; +package org.thingsboard.server.common.data.permission; import lombok.AllArgsConstructor; import lombok.Getter; @@ -21,8 +21,12 @@ import org.thingsboard.server.common.data.EntityType; import org.thingsboard.server.common.data.id.CustomerId; import org.thingsboard.server.common.data.id.TenantId; +import java.util.HashMap; +import java.util.Map; +import java.util.UUID; + @AllArgsConstructor -public class QuerySecurityContext { +public class QueryContext { @Getter private final TenantId tenantId; @@ -33,7 +37,11 @@ public class QuerySecurityContext { @Getter private final boolean ignorePermissionCheck; - public QuerySecurityContext(TenantId tenantId, CustomerId customerId, EntityType entityType) { + @Getter + private final Map relatedParentIdMap = new HashMap<>(); + + public QueryContext(TenantId tenantId, CustomerId customerId, EntityType entityType) { this(tenantId, customerId, entityType, false); } + } \ No newline at end of file diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/query/DynamicValue.java b/common/data/src/main/java/org/thingsboard/server/common/data/query/DynamicValue.java index 473fc867fa..0b9deebfd9 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/query/DynamicValue.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/query/DynamicValue.java @@ -15,7 +15,6 @@ */ package org.thingsboard.server.common.data.query; -import com.fasterxml.jackson.annotation.JsonIgnore; import lombok.Data; import lombok.RequiredArgsConstructor; import org.thingsboard.server.common.data.validation.NoXss; @@ -26,7 +25,6 @@ import java.io.Serializable; @RequiredArgsConstructor public class DynamicValue implements Serializable { - @JsonIgnore private T resolvedValue; private final DynamicValueSourceType sourceType; diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/query/EntityCountQuery.java b/common/data/src/main/java/org/thingsboard/server/common/data/query/EntityCountQuery.java index 3dd24147e7..8c5d35f03d 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/query/EntityCountQuery.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/query/EntityCountQuery.java @@ -15,6 +15,7 @@ */ package org.thingsboard.server.common.data.query; +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; import io.swagger.v3.oas.annotations.media.Schema; import lombok.Getter; import lombok.ToString; @@ -24,6 +25,7 @@ import java.util.List; @Schema @ToString +@JsonIgnoreProperties(ignoreUnknown = true) public class EntityCountQuery { @Getter diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/query/EntityData.java b/common/data/src/main/java/org/thingsboard/server/common/data/query/EntityData.java index 79730dc23a..fe9f0d3f68 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/query/EntityData.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/query/EntityData.java @@ -16,20 +16,22 @@ package org.thingsboard.server.common.data.query; import com.fasterxml.jackson.annotation.JsonIgnore; +import lombok.AllArgsConstructor; import lombok.Data; -import lombok.RequiredArgsConstructor; +import lombok.NoArgsConstructor; import org.thingsboard.server.common.data.id.EntityId; import java.util.Map; @Data -@RequiredArgsConstructor +@AllArgsConstructor +@NoArgsConstructor public class EntityData { - private final EntityId entityId; - private final Map> latest; - private final Map timeseries; - private final Map aggLatest; + private EntityId entityId; + private Map> latest; + private Map timeseries; + private Map aggLatest; public EntityData(EntityId entityId, Map> latest, Map timeseries) { this(entityId, latest, timeseries, null); @@ -44,4 +46,5 @@ public class EntityData { aggLatest.clear(); } } + } diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/queue/QueueConfig.java b/common/data/src/main/java/org/thingsboard/server/common/data/queue/QueueConfig.java index 383c613a38..17a712d838 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/queue/QueueConfig.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/queue/QueueConfig.java @@ -15,10 +15,22 @@ */ package org.thingsboard.server.common.data.queue; +import lombok.Data; + public interface QueueConfig { boolean isConsumerPerPartition(); int getPollInterval(); + static QueueConfig of(boolean consumerPerPartition, long pollInterval) { + return new BasicQueueConfig(consumerPerPartition, (int) pollInterval); + } + + @Data + class BasicQueueConfig implements QueueConfig { + private final boolean consumerPerPartition; + private final int pollInterval; + } + } diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/queue/QueueStats.java b/common/data/src/main/java/org/thingsboard/server/common/data/queue/QueueStats.java index 6c648daf02..9c3536dea1 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/queue/QueueStats.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/queue/QueueStats.java @@ -18,13 +18,15 @@ package org.thingsboard.server.common.data.queue; import lombok.Data; import lombok.EqualsAndHashCode; import org.thingsboard.server.common.data.BaseData; +import org.thingsboard.server.common.data.EntityType; +import org.thingsboard.server.common.data.HasEntityType; import org.thingsboard.server.common.data.HasTenantId; import org.thingsboard.server.common.data.id.QueueStatsId; import org.thingsboard.server.common.data.id.TenantId; @EqualsAndHashCode(callSuper = true) @Data -public class QueueStats extends BaseData implements HasTenantId { +public class QueueStats extends BaseData implements HasTenantId, HasEntityType { private TenantId tenantId; private String queueName; private String serviceId; @@ -36,4 +38,8 @@ public class QueueStats extends BaseData implements HasTenantId { super(id); } + @Override + public EntityType getEntityType() { + return EntityType.QUEUE_STATS; + } } diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/relation/EntityRelation.java b/common/data/src/main/java/org/thingsboard/server/common/data/relation/EntityRelation.java index 62b5caafd3..927d592cd7 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/relation/EntityRelation.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/relation/EntityRelation.java @@ -25,6 +25,7 @@ import lombok.ToString; import lombok.extern.slf4j.Slf4j; import org.thingsboard.server.common.data.BaseDataWithAdditionalInfo; import org.thingsboard.server.common.data.HasVersion; +import org.thingsboard.server.common.data.edqs.EdqsObject; import org.thingsboard.server.common.data.id.EntityId; import org.thingsboard.server.common.data.validation.Length; @@ -34,7 +35,7 @@ import java.io.Serializable; @Schema @EqualsAndHashCode(exclude = "additionalInfoBytes") @ToString(exclude = {"additionalInfoBytes"}) -public class EntityRelation implements HasVersion, Serializable { +public class EntityRelation implements HasVersion, Serializable, EdqsObject { private static final long serialVersionUID = 2807343040519543363L; @@ -107,7 +108,7 @@ public class EntityRelation implements HasVersion, Serializable { return typeGroup; } - @Schema(description = "Additional parameters of the relation",implementation = com.fasterxml.jackson.databind.JsonNode.class) + @Schema(description = "Additional parameters of the relation", implementation = com.fasterxml.jackson.databind.JsonNode.class) public JsonNode getAdditionalInfo() { return BaseDataWithAdditionalInfo.getJson(() -> additionalInfo, () -> additionalInfoBytes); } @@ -116,4 +117,14 @@ public class EntityRelation implements HasVersion, Serializable { BaseDataWithAdditionalInfo.setJson(addInfo, json -> this.additionalInfo = json, bytes -> this.additionalInfoBytes = bytes); } + @JsonIgnore + public String key() { + return "r_" + from + "_" + to + "_" + typeGroup + "_" + type; + } + + @Override + public Long version() { + return version; + } + } diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/util/CollectionsUtil.java b/common/data/src/main/java/org/thingsboard/server/common/data/util/CollectionsUtil.java index e89f3c1ad8..2fc3f4c040 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/util/CollectionsUtil.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/util/CollectionsUtil.java @@ -17,6 +17,7 @@ package org.thingsboard.server.common.data.util; import java.util.Collection; import java.util.HashMap; +import java.util.HashSet; import java.util.List; import java.util.Map; import java.util.Set; @@ -75,4 +76,11 @@ public class CollectionsUtil { return isEmpty(collection) || collection.contains(element); } + public static HashSet concat(Set set1, Set set2) { + HashSet result = new HashSet<>(); + result.addAll(set1); + result.addAll(set2); + return result; + } + } diff --git a/common/edqs/pom.xml b/common/edqs/pom.xml new file mode 100644 index 0000000000..40e89acee6 --- /dev/null +++ b/common/edqs/pom.xml @@ -0,0 +1,97 @@ + + + 4.0.0 + + org.thingsboard + 4.0.0PE-SNAPSHOT + common + + org.thingsboard.common + edqs + jar + + Thingsboard Server EDQS API + https://thingsboard.io + + + UTF-8 + ${basedir}/../.. + + + + + org.rocksdb + rocksdbjni + + + org.thingsboard.common + proto + + + org.thingsboard.common + data + + + org.thingsboard.common + util + + + org.thingsboard.common + message + + + org.thingsboard.common + stats + + + org.thingsboard.common + cluster-api + + + org.thingsboard.common + queue + + + org.apache.kafka + kafka-clients + + + com.github.ben-manes.caffeine + caffeine + + + org.springframework + spring-context-support + + + org.springframework.boot + spring-boot-autoconfigure + + + + + + thingsboard-repo-deploy + ThingsBoard Repo Deployment + https://repo.thingsboard.io/artifactory/libs-release-public + + + + diff --git a/common/edqs/src/main/java/org/thingsboard/server/edqs/data/ApiUsageStateData.java b/common/edqs/src/main/java/org/thingsboard/server/edqs/data/ApiUsageStateData.java new file mode 100644 index 0000000000..c1ce21507e --- /dev/null +++ b/common/edqs/src/main/java/org/thingsboard/server/edqs/data/ApiUsageStateData.java @@ -0,0 +1,46 @@ +/** + * Copyright © 2016-2024 ThingsBoard, Inc. + * + * 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.edqs.data; + +import lombok.ToString; +import org.thingsboard.server.common.data.EntityType; +import org.thingsboard.server.common.data.edqs.fields.ApiUsageStateFields; + +import java.util.UUID; + +@ToString(callSuper = true) +public class ApiUsageStateData extends BaseEntityData { + + public ApiUsageStateData(UUID entityId) { + super(entityId); + } + + @Override + public EntityType getEntityType() { + return EntityType.API_USAGE_STATE; + } + + @Override + public String getEntityName() { + return getEntityOwnerName(); + } + + @Override + public String getEntityOwnerName() { + return repo.getOwnerName(fields.getEntityId()); + } + +} diff --git a/common/edqs/src/main/java/org/thingsboard/server/edqs/data/AssetData.java b/common/edqs/src/main/java/org/thingsboard/server/edqs/data/AssetData.java new file mode 100644 index 0000000000..ce2a63a2ec --- /dev/null +++ b/common/edqs/src/main/java/org/thingsboard/server/edqs/data/AssetData.java @@ -0,0 +1,36 @@ +/** + * Copyright © 2016-2024 ThingsBoard, Inc. + * + * 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.edqs.data; + +import lombok.ToString; +import org.thingsboard.server.common.data.EntityType; +import org.thingsboard.server.common.data.edqs.fields.AssetFields; + +import java.util.UUID; + +@ToString(callSuper = true) +public class AssetData extends ProfileAwareData { + + public AssetData(UUID id) { + super(id); + } + + @Override + public EntityType getEntityType() { + return EntityType.ASSET; + } + +} diff --git a/common/edqs/src/main/java/org/thingsboard/server/edqs/data/BaseEntityData.java b/common/edqs/src/main/java/org/thingsboard/server/edqs/data/BaseEntityData.java new file mode 100644 index 0000000000..11c8b269b9 --- /dev/null +++ b/common/edqs/src/main/java/org/thingsboard/server/edqs/data/BaseEntityData.java @@ -0,0 +1,180 @@ +/** + * Copyright © 2016-2024 ThingsBoard, Inc. + * + * 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.edqs.data; + +import lombok.Getter; +import lombok.Setter; +import lombok.ToString; +import org.thingsboard.server.common.data.AttributeScope; +import org.thingsboard.server.common.data.EntityType; +import org.thingsboard.server.common.data.edqs.fields.EntityFields; +import org.thingsboard.server.common.data.id.CustomerId; +import org.thingsboard.server.common.data.permission.QueryContext; +import org.thingsboard.server.common.data.query.EntityKeyType; +import org.thingsboard.server.edqs.data.dp.BoolDataPoint; +import org.thingsboard.server.edqs.data.dp.DataPoint; +import org.thingsboard.server.edqs.data.dp.LongDataPoint; +import org.thingsboard.server.edqs.data.dp.StringDataPoint; +import org.thingsboard.server.edqs.query.DataKey; +import org.thingsboard.server.edqs.repo.TenantRepo; + +import java.util.Map; +import java.util.Objects; +import java.util.Optional; +import java.util.UUID; +import java.util.concurrent.ConcurrentHashMap; + +@ToString +public abstract class BaseEntityData implements EntityData { + + @Getter + private final UUID id; + @Getter + protected final Map serverAttrMap; + @Getter + private final Map tMap; + + @Getter + @Setter + private volatile UUID customerId; + + @Setter + protected TenantRepo repo; + + @Getter + @Setter + protected volatile T fields; + + public BaseEntityData(UUID id) { + this.id = id; + this.serverAttrMap = new ConcurrentHashMap<>(); + this.tMap = new ConcurrentHashMap<>(); + } + + @Override + public DataPoint getAttr(Integer keyId, EntityKeyType entityKeyType) { + return switch (entityKeyType) { + case ATTRIBUTE, SERVER_ATTRIBUTE -> serverAttrMap.get(keyId); + default -> null; + }; + } + + @Override + public boolean putAttr(Integer keyId, AttributeScope scope, DataPoint value) { + return serverAttrMap.put(keyId, value) == null; + } + + @Override + public boolean removeAttr(Integer keyId, AttributeScope scope) { + return serverAttrMap.remove(keyId) != null; + } + + @Override + public DataPoint getTs(Integer keyId) { + return tMap.get(keyId); + } + + @Override + public boolean putTs(Integer keyId, DataPoint value) { + return tMap.put(keyId, value) == null; + } + + @Override + public boolean removeTs(Integer keyId) { + return tMap.remove(keyId) != null; + } + + @Override + public EntityType getOwnerType() { + return customerId != null ? EntityType.CUSTOMER : EntityType.TENANT; + } + + @Override + public DataPoint getDataPoint(DataKey key, QueryContext ctx) { + return switch (key.type()) { + case TIME_SERIES -> getTs(key.keyId()); + case ATTRIBUTE, SERVER_ATTRIBUTE, CLIENT_ATTRIBUTE, SHARED_ATTRIBUTE -> getAttr(key.keyId(), key.type()); + case ENTITY_FIELD -> getField(key, ctx); + default -> throw new RuntimeException(key.type() + " not supported"); + }; + } + + private DataPoint getField(DataKey newKey, QueryContext ctx) { + if (fields == null) { + return null; + } + String key = newKey.key(); + return switch (key) { + case "createdTime" -> new LongDataPoint(System.currentTimeMillis(), fields.getCreatedTime()); + case "edgeTemplate" -> new BoolDataPoint(System.currentTimeMillis(), fields.isEdgeTemplate()); + case "parentId" -> new StringDataPoint(System.currentTimeMillis(), getRelatedParentId(ctx)); + default -> new StringDataPoint(System.currentTimeMillis(), getField(key), false); + }; + } + + @Override + public String getField(String name) { + if (fields == null) { + return null; + } + return switch (name) { + case "name" -> getEntityName(); + case "ownerName" -> getEntityOwnerName(); + case "ownerType" -> customerId != null ? EntityType.CUSTOMER.name() : EntityType.TENANT.name(); + case "entityType" -> Optional.ofNullable(getEntityType()).map(EntityType::name).orElse(""); + default -> fields.getAsString(name); + }; + } + + public String getEntityOwnerName() { + return repo.getOwnerName(getCustomerId() == null || CustomerId.NULL_UUID.equals(getCustomerId()) ? null : + new CustomerId(getCustomerId())); + } + + public String getEntityName() { + return getFields().getName(); + } + + private String getRelatedParentId(QueryContext ctx) { + return Optional.ofNullable(ctx.getRelatedParentIdMap().get(getId())) + .map(UUID::toString) + .orElse(""); + } + + @Override + public EntityType getEntityType() { + return null; + } + + @Override + public boolean isEmpty() { + return fields == null; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + BaseEntityData that = (BaseEntityData) o; + return Objects.equals(id, that.id); + } + + @Override + public int hashCode() { + return Objects.hash(id); + } + +} diff --git a/common/edqs/src/main/java/org/thingsboard/server/edqs/data/CustomerData.java b/common/edqs/src/main/java/org/thingsboard/server/edqs/data/CustomerData.java new file mode 100644 index 0000000000..15e2573b81 --- /dev/null +++ b/common/edqs/src/main/java/org/thingsboard/server/edqs/data/CustomerData.java @@ -0,0 +1,62 @@ +/** + * Copyright © 2016-2024 ThingsBoard, Inc. + * + * 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.edqs.data; + +import org.thingsboard.server.common.data.EntityType; +import org.thingsboard.server.common.data.edqs.fields.CustomerFields; + +import java.util.Collection; +import java.util.Collections; +import java.util.UUID; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.ConcurrentMap; + +public class CustomerData extends BaseEntityData { + + private final ConcurrentMap>> entitiesById = new ConcurrentHashMap<>(); + + public CustomerData(UUID entityId) { + super(entityId); + } + + @Override + public EntityType getEntityType() { + return EntityType.CUSTOMER; + } + + public Collection> getEntities(EntityType entityType) { + var map = entitiesById.get(entityType); + if (map == null) { + return Collections.emptyList(); + } else { + return map.values(); + } + } + + public void addOrUpdate(EntityData ed) { + entitiesById.computeIfAbsent(ed.getEntityType(), et -> new ConcurrentHashMap<>()).put(ed.getId(), ed); + } + + public boolean remove(EntityData ed) { + var map = entitiesById.get(ed.getEntityType()); + if (map != null) { + return map.remove(ed.getId()) != null; + } else { + return false; + } + } + +} diff --git a/common/edqs/src/main/java/org/thingsboard/server/edqs/data/DeviceData.java b/common/edqs/src/main/java/org/thingsboard/server/edqs/data/DeviceData.java new file mode 100644 index 0000000000..42042b2a9f --- /dev/null +++ b/common/edqs/src/main/java/org/thingsboard/server/edqs/data/DeviceData.java @@ -0,0 +1,86 @@ +/** + * Copyright © 2016-2024 ThingsBoard, Inc. + * + * 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.edqs.data; + +import lombok.ToString; +import org.thingsboard.server.common.data.AttributeScope; +import org.thingsboard.server.common.data.EntityType; +import org.thingsboard.server.common.data.edqs.fields.DeviceFields; +import org.thingsboard.server.common.data.query.EntityKeyType; +import org.thingsboard.server.edqs.data.dp.DataPoint; + +import java.util.Map; +import java.util.UUID; +import java.util.concurrent.ConcurrentHashMap; + +@ToString(callSuper = true) +public class DeviceData extends ProfileAwareData { + + private final Map clientAttrMap; + private final Map sharedAttrMap; + + public DeviceData(UUID entityId) { + super(entityId); + this.clientAttrMap = new ConcurrentHashMap<>(); + this.sharedAttrMap = new ConcurrentHashMap<>(); + } + + @Override + public EntityType getEntityType() { + return EntityType.DEVICE; + } + + @Override + public DataPoint getAttr(Integer keyId, EntityKeyType entityKeyType) { + return switch (entityKeyType) { + case ATTRIBUTE -> getAttributeDataPoint(keyId); + case SERVER_ATTRIBUTE -> serverAttrMap.get(keyId); + case CLIENT_ATTRIBUTE -> clientAttrMap.get(keyId); + case SHARED_ATTRIBUTE -> sharedAttrMap.get(keyId); + default -> throw new RuntimeException(entityKeyType + " not implemented"); + }; + } + + @Override + public boolean putAttr(Integer keyId, AttributeScope scope, DataPoint value) { + return switch (scope) { + case SERVER_SCOPE -> serverAttrMap.put(keyId, value) == null; + case CLIENT_SCOPE -> clientAttrMap.put(keyId, value) == null; + case SHARED_SCOPE -> sharedAttrMap.put(keyId, value) == null; + }; + } + + @Override + public boolean removeAttr(Integer keyId, AttributeScope scope) { + return switch (scope) { + case SERVER_SCOPE -> serverAttrMap.remove(keyId) != null; + case CLIENT_SCOPE -> clientAttrMap.remove(keyId) != null; + case SHARED_SCOPE -> sharedAttrMap.remove(keyId) != null; + }; + } + + private DataPoint getAttributeDataPoint(Integer keyId) { + DataPoint dp = serverAttrMap.get(keyId); + if (dp == null) { + dp = sharedAttrMap.get(keyId); + if (dp == null) { + dp = clientAttrMap.get(keyId); + } + } + return dp; + } + +} diff --git a/common/edqs/src/main/java/org/thingsboard/server/edqs/data/EntityData.java b/common/edqs/src/main/java/org/thingsboard/server/edqs/data/EntityData.java new file mode 100644 index 0000000000..c373baf305 --- /dev/null +++ b/common/edqs/src/main/java/org/thingsboard/server/edqs/data/EntityData.java @@ -0,0 +1,65 @@ +/** + * Copyright © 2016-2024 ThingsBoard, Inc. + * + * 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.edqs.data; + +import org.thingsboard.server.common.data.AttributeScope; +import org.thingsboard.server.common.data.EntityType; +import org.thingsboard.server.common.data.edqs.fields.EntityFields; +import org.thingsboard.server.common.data.permission.QueryContext; +import org.thingsboard.server.common.data.query.EntityKeyType; +import org.thingsboard.server.edqs.data.dp.DataPoint; +import org.thingsboard.server.edqs.query.DataKey; +import org.thingsboard.server.edqs.repo.TenantRepo; + +import java.util.UUID; + +public interface EntityData { + + UUID getId(); + + EntityType getEntityType(); + + UUID getCustomerId(); + + void setCustomerId(UUID customerId); + + void setRepo(TenantRepo repo); + + T getFields(); + + void setFields(T fields); + + DataPoint getAttr(Integer keyId, EntityKeyType entityKeyType); + + boolean putAttr(Integer keyId, AttributeScope scope, DataPoint value); + + boolean removeAttr(Integer keyId, AttributeScope scope); + + DataPoint getTs(Integer keyId); + + boolean putTs(Integer keyId, DataPoint value); + + boolean removeTs(Integer keyId); + + EntityType getOwnerType(); + + DataPoint getDataPoint(DataKey key, QueryContext queryContext); + + String getField(String name); + + boolean isEmpty(); + +} diff --git a/common/edqs/src/main/java/org/thingsboard/server/edqs/data/EntityGroupData.java b/common/edqs/src/main/java/org/thingsboard/server/edqs/data/EntityGroupData.java new file mode 100644 index 0000000000..d956519cc4 --- /dev/null +++ b/common/edqs/src/main/java/org/thingsboard/server/edqs/data/EntityGroupData.java @@ -0,0 +1,58 @@ +/** + * Copyright © 2016-2024 ThingsBoard, Inc. + * + * 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.edqs.data; + +import org.thingsboard.server.common.data.EntityType; +import org.thingsboard.server.common.data.edqs.fields.EntityGroupFields; + +import java.util.Collection; +import java.util.UUID; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.ConcurrentMap; + +public class EntityGroupData extends BaseEntityData { + + private final ConcurrentMap> entitiesById = new ConcurrentHashMap<>(); + + public EntityGroupData(UUID entityId) { + super(entityId); + } + + @Override + public EntityType getEntityType() { + return EntityType.ENTITY_GROUP; + } + + public Collection> getEntities() { + return entitiesById.values(); + } + + public boolean addOrUpdate(EntityData ed) { + return entitiesById.put(ed.getId(), ed) == null; + } + + public boolean remove(EntityData ed) { + return entitiesById.remove(ed.getId()) != null; + } + + public EntityData getEntity(UUID entityId) { + return entitiesById.get(entityId); + } + + public boolean remove(UUID toId) { + return entitiesById.remove(toId) != null; + } +} diff --git a/common/edqs/src/main/java/org/thingsboard/server/edqs/data/EntityProfileData.java b/common/edqs/src/main/java/org/thingsboard/server/edqs/data/EntityProfileData.java new file mode 100644 index 0000000000..6057ac17e7 --- /dev/null +++ b/common/edqs/src/main/java/org/thingsboard/server/edqs/data/EntityProfileData.java @@ -0,0 +1,39 @@ +/** + * Copyright © 2016-2024 ThingsBoard, Inc. + * + * 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.edqs.data; + +import lombok.ToString; +import org.thingsboard.server.common.data.EntityType; +import org.thingsboard.server.common.data.edqs.fields.EntityFields; + +import java.util.UUID; + +@ToString(callSuper = true) +public class EntityProfileData extends BaseEntityData { + + private final EntityType entityType; + + public EntityProfileData(UUID entityId, EntityType entityType) { + super(entityId); + this.entityType = entityType; + } + + @Override + public EntityType getEntityType() { + return entityType; + } + +} diff --git a/common/edqs/src/main/java/org/thingsboard/server/edqs/data/GenericData.java b/common/edqs/src/main/java/org/thingsboard/server/edqs/data/GenericData.java new file mode 100644 index 0000000000..8fd8924b5c --- /dev/null +++ b/common/edqs/src/main/java/org/thingsboard/server/edqs/data/GenericData.java @@ -0,0 +1,38 @@ +/** + * Copyright © 2016-2024 ThingsBoard, Inc. + * + * 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.edqs.data; + +import lombok.ToString; +import org.thingsboard.server.common.data.EntityType; +import org.thingsboard.server.common.data.edqs.fields.EntityFields; + +import java.util.UUID; + +@ToString(callSuper = true) +public class GenericData extends BaseEntityData { + + private final EntityType entityType; + + public GenericData(EntityType entityType, UUID entityId) { + super(entityId); + this.entityType = entityType; + } + + @Override + public EntityType getEntityType() { + return entityType; + } +} diff --git a/common/edqs/src/main/java/org/thingsboard/server/edqs/data/ProfileAwareData.java b/common/edqs/src/main/java/org/thingsboard/server/edqs/data/ProfileAwareData.java new file mode 100644 index 0000000000..f98c8e6d9b --- /dev/null +++ b/common/edqs/src/main/java/org/thingsboard/server/edqs/data/ProfileAwareData.java @@ -0,0 +1,28 @@ +/** + * Copyright © 2016-2024 ThingsBoard, Inc. + * + * 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.edqs.data; + +import org.thingsboard.server.common.data.edqs.fields.ProfileAwareFields; + +import java.util.UUID; + +public abstract class ProfileAwareData extends BaseEntityData { + + public ProfileAwareData(UUID id) { + super(id); + } + +} diff --git a/common/edqs/src/main/java/org/thingsboard/server/edqs/data/RelationData.java b/common/edqs/src/main/java/org/thingsboard/server/edqs/data/RelationData.java new file mode 100644 index 0000000000..3a660af813 --- /dev/null +++ b/common/edqs/src/main/java/org/thingsboard/server/edqs/data/RelationData.java @@ -0,0 +1,26 @@ +/** + * Copyright © 2016-2024 ThingsBoard, Inc. + * + * 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.edqs.data; + +import org.thingsboard.server.common.data.EntityType; +import org.thingsboard.server.common.data.relation.RelationTypeGroup; + +import java.util.UUID; + +public record RelationData(UUID fromId, EntityType fromType, UUID toId, EntityType toType, String type, + RelationTypeGroup typeGroup) { + +} diff --git a/common/edqs/src/main/java/org/thingsboard/server/edqs/data/RelationInfo.java b/common/edqs/src/main/java/org/thingsboard/server/edqs/data/RelationInfo.java new file mode 100644 index 0000000000..e479c94d00 --- /dev/null +++ b/common/edqs/src/main/java/org/thingsboard/server/edqs/data/RelationInfo.java @@ -0,0 +1,26 @@ +/** + * Copyright © 2016-2024 ThingsBoard, Inc. + * + * 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.edqs.data; + +import lombok.Data; + +@Data +public class RelationInfo { + + private final String type; + private final EntityData target; + +} diff --git a/common/edqs/src/main/java/org/thingsboard/server/edqs/data/RelationsRepo.java b/common/edqs/src/main/java/org/thingsboard/server/edqs/data/RelationsRepo.java new file mode 100644 index 0000000000..92cf10e1d0 --- /dev/null +++ b/common/edqs/src/main/java/org/thingsboard/server/edqs/data/RelationsRepo.java @@ -0,0 +1,62 @@ +/** + * Copyright © 2016-2024 ThingsBoard, Inc. + * + * 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.edqs.data; + +import lombok.NoArgsConstructor; + +import java.util.Collections; +import java.util.Set; +import java.util.UUID; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.ConcurrentMap; + +@NoArgsConstructor +public class RelationsRepo { + + private final ConcurrentMap> fromRelations = new ConcurrentHashMap<>(); + private final ConcurrentMap> toRelations = new ConcurrentHashMap<>(); + + public boolean add(EntityData from, EntityData to, String type) { + boolean addedFromRelation = fromRelations.computeIfAbsent(from.getId(), k -> ConcurrentHashMap.newKeySet()).add(new RelationInfo(type, to)); + boolean addedToRelation = toRelations.computeIfAbsent(to.getId(), k -> ConcurrentHashMap.newKeySet()).add(new RelationInfo(type, from)); + return addedFromRelation || addedToRelation; + } + + public Set getFrom(UUID entityId) { + var result = fromRelations.get(entityId); + return result == null ? Collections.emptySet() : result; + } + + public Set getTo(UUID entityId) { + var result = toRelations.get(entityId); + return result == null ? Collections.emptySet() : result; + } + + public boolean remove(UUID from, UUID to, String type) { + boolean removedFromRelation = false; + boolean removedToRelation = false; + Set fromRelations = this.fromRelations.get(from); + if (fromRelations != null) { + removedFromRelation = fromRelations.removeIf(relationInfo -> relationInfo.getTarget().getId().equals(to) && relationInfo.getType().equals(type)); + } + Set toRelations = this.toRelations.get(to); + if (toRelations != null) { + removedToRelation = toRelations.removeIf(relationInfo -> relationInfo.getTarget().getId().equals(from) && relationInfo.getType().equals(type)); + } + return removedFromRelation || removedToRelation; + } + +} diff --git a/common/edqs/src/main/java/org/thingsboard/server/edqs/data/TenantData.java b/common/edqs/src/main/java/org/thingsboard/server/edqs/data/TenantData.java new file mode 100644 index 0000000000..a9034e251c --- /dev/null +++ b/common/edqs/src/main/java/org/thingsboard/server/edqs/data/TenantData.java @@ -0,0 +1,34 @@ +/** + * Copyright © 2016-2024 ThingsBoard, Inc. + * + * 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.edqs.data; + +import org.thingsboard.server.common.data.EntityType; +import org.thingsboard.server.common.data.edqs.fields.TenantFields; + +import java.util.UUID; + +public class TenantData extends BaseEntityData { + + public TenantData(UUID entityId) { + super(entityId); + } + + @Override + public EntityType getEntityType() { + return EntityType.TENANT; + } + +} diff --git a/common/edqs/src/main/java/org/thingsboard/server/edqs/data/dp/AbstractDataPoint.java b/common/edqs/src/main/java/org/thingsboard/server/edqs/data/dp/AbstractDataPoint.java new file mode 100644 index 0000000000..5eb16f72fa --- /dev/null +++ b/common/edqs/src/main/java/org/thingsboard/server/edqs/data/dp/AbstractDataPoint.java @@ -0,0 +1,56 @@ +/** + * Copyright © 2016-2024 ThingsBoard, Inc. + * + * 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.edqs.data.dp; + +import lombok.Getter; +import lombok.RequiredArgsConstructor; + +@RequiredArgsConstructor +public abstract class AbstractDataPoint implements DataPoint { + + @Getter + private final long ts; + + @Override + public String getStr() { + throw new RuntimeException(NOT_SUPPORTED); + } + + @Override + public long getLong() { + throw new RuntimeException(NOT_SUPPORTED); + } + + @Override + public double getDouble() { + throw new RuntimeException(NOT_SUPPORTED); + } + + @Override + public boolean getBool() { + throw new RuntimeException(NOT_SUPPORTED); + } + + @Override + public String getJson() { + throw new RuntimeException(NOT_SUPPORTED); + } + + public String toString() { + return valueToString(); + } + +} 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 new file mode 100644 index 0000000000..ccc97a2f84 --- /dev/null +++ b/common/edqs/src/main/java/org/thingsboard/server/edqs/data/dp/BoolDataPoint.java @@ -0,0 +1,46 @@ +/** + * Copyright © 2016-2024 ThingsBoard, Inc. + * + * 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.edqs.data.dp; + +import lombok.Getter; +import org.thingsboard.server.common.data.kv.DataType; + +public class BoolDataPoint extends AbstractDataPoint { + + @Getter + private final boolean value; + + public BoolDataPoint(long ts, boolean value) { + super(ts); + this.value = value; + } + + @Override + public DataType getType() { + return DataType.BOOLEAN; + } + + @Override + public boolean getBool() { + return value; + } + + @Override + public String valueToString() { + return Boolean.toString(value); + } + +} diff --git a/common/edqs/src/main/java/org/thingsboard/server/edqs/data/dp/CompressedJsonDataPoint.java b/common/edqs/src/main/java/org/thingsboard/server/edqs/data/dp/CompressedJsonDataPoint.java new file mode 100644 index 0000000000..b10f3badf0 --- /dev/null +++ b/common/edqs/src/main/java/org/thingsboard/server/edqs/data/dp/CompressedJsonDataPoint.java @@ -0,0 +1,31 @@ +/** + * Copyright © 2016-2024 ThingsBoard, Inc. + * + * 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.edqs.data.dp; + +import org.thingsboard.server.common.data.kv.DataType; + +public class CompressedJsonDataPoint extends CompressedStringDataPoint { + + public CompressedJsonDataPoint(long ts, String value) { + super(ts, value); + } + + @Override + public DataType getType() { + return DataType.JSON; + } + +} diff --git a/common/edqs/src/main/java/org/thingsboard/server/edqs/data/dp/CompressedStringDataPoint.java b/common/edqs/src/main/java/org/thingsboard/server/edqs/data/dp/CompressedStringDataPoint.java new file mode 100644 index 0000000000..b679c94f4e --- /dev/null +++ b/common/edqs/src/main/java/org/thingsboard/server/edqs/data/dp/CompressedStringDataPoint.java @@ -0,0 +1,64 @@ +/** + * Copyright © 2016-2024 ThingsBoard, Inc. + * + * 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.edqs.data.dp; + +import lombok.Getter; +import lombok.SneakyThrows; +import org.thingsboard.server.common.data.kv.DataType; +import org.thingsboard.server.edqs.repo.TbBytePool; +import org.xerial.snappy.Snappy; + +import java.nio.charset.StandardCharsets; +import java.util.concurrent.atomic.AtomicInteger; +import java.util.concurrent.atomic.AtomicLong; + +public class CompressedStringDataPoint extends AbstractDataPoint { + + public static final int MIN_STR_SIZE_TO_COMPRESS = 512; + @Getter + private final byte[] value; + + public static final AtomicInteger cnt = new AtomicInteger(); + public static final AtomicLong uncompressedLength = new AtomicLong(); + public static final AtomicLong compressedLength = new AtomicLong(); + + @SneakyThrows + public CompressedStringDataPoint(long ts, String value) { + super(ts); + cnt.incrementAndGet(); + uncompressedLength.addAndGet(value.getBytes(StandardCharsets.UTF_8).length); + this.value = TbBytePool.intern(Snappy.compress(value)); + compressedLength.addAndGet(this.value.length); + } + + @Override + public DataType getType() { + return DataType.STRING; + } + + @SneakyThrows + @Override + public String getStr() { + return Snappy.uncompressString(value); + } + + @SneakyThrows + @Override + public String valueToString() { + return Snappy.uncompressString(value); + } + +} diff --git a/common/edqs/src/main/java/org/thingsboard/server/edqs/data/dp/DataPoint.java b/common/edqs/src/main/java/org/thingsboard/server/edqs/data/dp/DataPoint.java new file mode 100644 index 0000000000..3871bca114 --- /dev/null +++ b/common/edqs/src/main/java/org/thingsboard/server/edqs/data/dp/DataPoint.java @@ -0,0 +1,40 @@ +/** + * Copyright © 2016-2024 ThingsBoard, Inc. + * + * 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.edqs.data.dp; + +import org.thingsboard.server.common.data.kv.DataType; + +public interface DataPoint { + + String NOT_SUPPORTED = "Not supported!"; + + long getTs(); + + DataType getType(); + + String getStr(); + + long getLong(); + + double getDouble(); + + boolean getBool(); + + String getJson(); + + String valueToString(); + +} 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 new file mode 100644 index 0000000000..a7dcdf9e82 --- /dev/null +++ b/common/edqs/src/main/java/org/thingsboard/server/edqs/data/dp/DoubleDataPoint.java @@ -0,0 +1,46 @@ +/** + * Copyright © 2016-2024 ThingsBoard, Inc. + * + * 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.edqs.data.dp; + +import lombok.Getter; +import org.thingsboard.server.common.data.kv.DataType; + +public class DoubleDataPoint extends AbstractDataPoint { + + @Getter + private final double value; + + public DoubleDataPoint(long ts, double value) { + super(ts); + this.value = value; + } + + @Override + public DataType getType() { + return DataType.DOUBLE; + } + + @Override + public double getDouble() { + return value; + } + + @Override + public String valueToString() { + return Double.toString(value); + } + +} diff --git a/common/edqs/src/main/java/org/thingsboard/server/edqs/data/dp/JsonDataPoint.java b/common/edqs/src/main/java/org/thingsboard/server/edqs/data/dp/JsonDataPoint.java new file mode 100644 index 0000000000..df318b6430 --- /dev/null +++ b/common/edqs/src/main/java/org/thingsboard/server/edqs/data/dp/JsonDataPoint.java @@ -0,0 +1,47 @@ +/** + * Copyright © 2016-2024 ThingsBoard, Inc. + * + * 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.edqs.data.dp; + +import lombok.Getter; +import org.thingsboard.server.common.data.kv.DataType; +import org.thingsboard.server.edqs.repo.TbStringPool; + +public class JsonDataPoint extends AbstractDataPoint { + + @Getter + private final String value; + + public JsonDataPoint(long ts, String value) { + super(ts); + this.value = TbStringPool.intern(value); + } + + @Override + public DataType getType() { + return DataType.JSON; + } + + @Override + public String getJson() { + return value; + } + + @Override + public String valueToString() { + return value; + } + +} 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 new file mode 100644 index 0000000000..ac54f19707 --- /dev/null +++ b/common/edqs/src/main/java/org/thingsboard/server/edqs/data/dp/LongDataPoint.java @@ -0,0 +1,50 @@ +/** + * Copyright © 2016-2024 ThingsBoard, Inc. + * + * 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.edqs.data.dp; + +import lombok.Getter; +import org.thingsboard.server.common.data.kv.DataType; + +public class LongDataPoint extends AbstractDataPoint { + + @Getter + private final long value; + + public LongDataPoint(long ts, long value) { + super(ts); + this.value = value; + } + + @Override + public DataType getType() { + return DataType.LONG; + } + + @Override + public long getLong() { + return value; + } + + @Override + public double getDouble() { + return value; + } + + @Override + public String valueToString() { + return Long.toString(value); + } +} diff --git a/common/edqs/src/main/java/org/thingsboard/server/edqs/data/dp/StringDataPoint.java b/common/edqs/src/main/java/org/thingsboard/server/edqs/data/dp/StringDataPoint.java new file mode 100644 index 0000000000..e1e2b5d0ef --- /dev/null +++ b/common/edqs/src/main/java/org/thingsboard/server/edqs/data/dp/StringDataPoint.java @@ -0,0 +1,51 @@ +/** + * Copyright © 2016-2024 ThingsBoard, Inc. + * + * 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.edqs.data.dp; + +import lombok.Getter; +import org.thingsboard.server.common.data.kv.DataType; +import org.thingsboard.server.edqs.repo.TbStringPool; + +public class StringDataPoint extends AbstractDataPoint { + + @Getter + private final String value; + + public StringDataPoint(long ts, String value) { + this(ts, value, true); + } + + public StringDataPoint(long ts, String value, boolean deduplicate) { + super(ts); + this.value = deduplicate ? TbStringPool.intern(value) : value; + } + + @Override + public DataType getType() { + return DataType.STRING; + } + + @Override + public String getStr() { + return value; + } + + @Override + public String valueToString() { + return value; + } + +} diff --git a/common/edqs/src/main/java/org/thingsboard/server/edqs/load/TenantRepoLoader.java b/common/edqs/src/main/java/org/thingsboard/server/edqs/load/TenantRepoLoader.java new file mode 100644 index 0000000000..d59d0cc671 --- /dev/null +++ b/common/edqs/src/main/java/org/thingsboard/server/edqs/load/TenantRepoLoader.java @@ -0,0 +1,144 @@ +/** + * Copyright © 2016-2024 ThingsBoard, Inc. + * + * 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.edqs.load; + +import lombok.Getter; +import lombok.RequiredArgsConstructor; +import org.apache.commons.lang3.RandomStringUtils; +import org.thingsboard.server.common.data.AttributeScope; +import org.thingsboard.server.common.data.Device; +import org.thingsboard.server.common.data.EntityType; +import org.thingsboard.server.common.data.StringUtils; +import org.thingsboard.server.common.data.edqs.AttributeKv; +import org.thingsboard.server.common.data.edqs.LatestTsKv; +import org.thingsboard.server.common.data.id.DeviceId; +import org.thingsboard.server.common.data.kv.BooleanDataEntry; +import org.thingsboard.server.common.data.kv.DataType; +import org.thingsboard.server.common.data.kv.DoubleDataEntry; +import org.thingsboard.server.common.data.kv.JsonDataEntry; +import org.thingsboard.server.common.data.kv.KvEntry; +import org.thingsboard.server.common.data.kv.LongDataEntry; +import org.thingsboard.server.common.data.kv.StringDataEntry; +import org.thingsboard.server.edqs.processor.EdqsConverter; +import org.thingsboard.server.edqs.repo.TenantRepo; + +import java.util.HashMap; +import java.util.Map; +import java.util.Random; +import java.util.UUID; + +@RequiredArgsConstructor +public class TenantRepoLoader { + + private static final int DEVICE_COUNT = 100000; + private static final int ATTRS_PER_DEVICE = 30; + private static final int ATTRS_AVG_STR_LENGTH = 12; + private static final int ATTRS_AVG_JSON_LENGTH = 265; + private static final int TS_PER_DEVICE = 29; + private static final int TS_AVG_STR_LENGTH = 59; + private static final int TS_AVG_JSON_LENGTH = 4005; + + private static final Map ATTR_CHANCES = new HashMap<>(); + private static final Random random = new Random(); + + static { + ATTR_CHANCES.put(DataType.BOOLEAN, 5); + ATTR_CHANCES.put(DataType.STRING, 49); + ATTR_CHANCES.put(DataType.LONG, 34); + ATTR_CHANCES.put(DataType.DOUBLE, 2); + ATTR_CHANCES.put(DataType.JSON, 10); + } + + private static final Map TS_CHANCES = new HashMap<>(); + + static { + TS_CHANCES.put(DataType.BOOLEAN, 6); + TS_CHANCES.put(DataType.STRING, 19); + TS_CHANCES.put(DataType.LONG, 36); + TS_CHANCES.put(DataType.DOUBLE, 32); + TS_CHANCES.put(DataType.JSON, 7); + } + + + @Getter + private final TenantRepo tenantRepo; + + public void load() { + long ts = System.currentTimeMillis() - DEVICE_COUNT; + for (int i = 0; i < DEVICE_COUNT; i++) { + DeviceId deviceId = new DeviceId(UUID.randomUUID()); + Device device = new Device(); + device.setId(deviceId); + device.setCreatedTime(ts + i); + device.setName("Device " + i); + device.setLabel("Device Label" + i); + device.setType("Device Type " + (i % 100)); + tenantRepo.addOrUpdate(EdqsConverter.toEntity(EntityType.DEVICE, device)); + for (int j = 0; j < ATTRS_PER_DEVICE; j++) { + String key = getRandomKey(); + AttributeKv attributeKv = new AttributeKv(); + attributeKv.setEntityId(deviceId); + attributeKv.setScope(AttributeScope.SERVER_SCOPE); + attributeKv.setKey(key); + attributeKv.setLastUpdateTs(ts); + attributeKv.setValue(getRandomKvEntry(key, ATTR_CHANCES, ATTRS_AVG_STR_LENGTH, ATTRS_AVG_JSON_LENGTH)); + tenantRepo.addOrUpdateAttribute(attributeKv); + } + for (int j = 0; j < TS_PER_DEVICE; j++) { + String key = getRandomKey(); + LatestTsKv latestTsKv = new LatestTsKv(); + latestTsKv.setEntityId(deviceId); + latestTsKv.setKey(key); + latestTsKv.setTs(ts); + latestTsKv.setValue(getRandomKvEntry(key, TS_CHANCES, TS_AVG_STR_LENGTH, TS_AVG_JSON_LENGTH)); + tenantRepo.addOrUpdateLatestKv(latestTsKv); + } + } + } + + private KvEntry getRandomKvEntry(String key, Map chances, int strLength, int jsnLength) { + int i = random.nextInt(100); + int s = 0; + for (var pair : chances.entrySet()) { + s += pair.getValue(); + if (i < s) { + switch (pair.getKey()) { + case BOOLEAN -> { + return new BooleanDataEntry(key, random.nextBoolean()); + } + case LONG -> { + return new LongDataEntry(key, random.nextLong()); + } + case DOUBLE -> { + return new DoubleDataEntry(key, random.nextDouble()); + } + case STRING -> { + return new StringDataEntry(key, StringUtils.randomAlphanumeric(strLength)); + } + case JSON -> { + return new JsonDataEntry(key, StringUtils.randomAlphanumeric(jsnLength)); + } + } + } + } + throw new RuntimeException("Something went wrong"); + } + + private String getRandomKey() { + return RandomStringUtils.randomAlphabetic(10); + } + +} diff --git a/common/edqs/src/main/java/org/thingsboard/server/edqs/processor/EdqsConverter.java b/common/edqs/src/main/java/org/thingsboard/server/edqs/processor/EdqsConverter.java new file mode 100644 index 0000000000..125f6c49fd --- /dev/null +++ b/common/edqs/src/main/java/org/thingsboard/server/edqs/processor/EdqsConverter.java @@ -0,0 +1,183 @@ +/** + * Copyright © 2016-2024 ThingsBoard, Inc. + * + * 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.edqs.processor; + +import lombok.RequiredArgsConstructor; +import lombok.SneakyThrows; +import org.springframework.stereotype.Service; +import org.thingsboard.common.util.JacksonUtil; +import org.thingsboard.server.common.data.AttributeScope; +import org.thingsboard.server.common.data.EntityType; +import org.thingsboard.server.common.data.ObjectType; +import org.thingsboard.server.common.data.edqs.AttributeKv; +import org.thingsboard.server.common.data.edqs.EdqsObject; +import org.thingsboard.server.common.data.edqs.Entity; +import org.thingsboard.server.common.data.edqs.LatestTsKv; +import org.thingsboard.server.common.data.edqs.fields.FieldsUtil; +import org.thingsboard.server.common.data.id.EntityId; +import org.thingsboard.server.common.data.id.EntityIdFactory; +import org.thingsboard.server.common.data.kv.KvEntry; +import org.thingsboard.server.common.data.relation.EntityRelation; +import org.thingsboard.server.common.util.KvProtoUtil; +import org.thingsboard.server.common.util.ProtoUtils; +import org.thingsboard.server.gen.transport.TransportProtos; + +import java.util.HashMap; +import java.util.Map; +import java.util.UUID; + +@Service +public class EdqsConverter { + + private final Map> converters = new HashMap<>(); + private final Converter defaultConverter = new JsonConverter<>(Entity.class); + + { + converters.put(ObjectType.RELATION, new JsonConverter<>(EntityRelation.class)); + converters.put(ObjectType.ATTRIBUTE_KV, new Converter() { + @Override + public byte[] serialize(ObjectType type, AttributeKv attributeKv) { + // TODO: some attributes may not fit into kafka + var proto = TransportProtos.AttributeKvProto.newBuilder() + .setEntityIdMSB(attributeKv.getEntityId().getId().getMostSignificantBits()) + .setEntityIdLSB(attributeKv.getEntityId().getId().getLeastSignificantBits()) + .setEntityType(ProtoUtils.toProto(attributeKv.getEntityId().getEntityType())) + .setScope(TransportProtos.AttributeScopeProto.forNumber(attributeKv.getScope().ordinal())) + .setKey(attributeKv.getKey()) + .setVersion(attributeKv.getVersion()); + if (attributeKv.getLastUpdateTs() != null) { + proto.setLastUpdateTs(attributeKv.getLastUpdateTs()); + } + if (attributeKv.getValue() != null) { + proto.setValue(KvProtoUtil.toKeyValueTypeProto(attributeKv.getValue())); + } + return proto.build().toByteArray(); + } + + @Override + public AttributeKv deserialize(ObjectType type, byte[] bytes) throws Exception { + TransportProtos.AttributeKvProto proto = TransportProtos.AttributeKvProto.parseFrom(bytes); + EntityId entityId = EntityIdFactory.getByTypeAndUuid(ProtoUtils.fromProto(proto.getEntityType()), + new UUID(proto.getEntityIdMSB(), proto.getEntityIdLSB())); + AttributeScope scope = AttributeScope.values()[proto.getScope().getNumber()]; + KvEntry value = proto.hasValue() ? KvProtoUtil.fromTsKvProto(proto.getValue()) : null; + return AttributeKv.builder() + .entityId(entityId) + .scope(scope) + .key(proto.getKey()) + .version(proto.getVersion()) + .lastUpdateTs(proto.getLastUpdateTs()) + .value(value) + .build(); + } + }); + converters.put(ObjectType.LATEST_TS_KV, new Converter() { + @Override + public byte[] serialize(ObjectType type, LatestTsKv latestTsKv) { + var proto = TransportProtos.LatestTsKvProto.newBuilder() + .setEntityIdMSB(latestTsKv.getEntityId().getId().getMostSignificantBits()) + .setEntityIdLSB(latestTsKv.getEntityId().getId().getLeastSignificantBits()) + .setEntityType(ProtoUtils.toProto(latestTsKv.getEntityId().getEntityType())) + .setKey(latestTsKv.getKey()) + .setVersion(latestTsKv.getVersion()); + if (latestTsKv.getTs() != null) { + proto.setTs(latestTsKv.getTs()); + } + if (latestTsKv.getValue() != null) { + proto.setValue(KvProtoUtil.toKeyValueTypeProto(latestTsKv.getValue())); + } + return proto.build().toByteArray(); + } + + @Override + public LatestTsKv deserialize(ObjectType type, byte[] bytes) throws Exception { + TransportProtos.LatestTsKvProto proto = TransportProtos.LatestTsKvProto.parseFrom(bytes); + EntityId entityId = EntityIdFactory.getByTypeAndUuid(ProtoUtils.fromProto(proto.getEntityType()), + new UUID(proto.getEntityIdMSB(), proto.getEntityIdLSB())); + KvEntry value = proto.hasValue() ? KvProtoUtil.fromTsKvProto(proto.getValue()) : null; + return LatestTsKv.builder() + .entityId(entityId) + .key(proto.getKey()) + .ts(proto.getTs()) + .version(proto.getVersion()) + .value(value) + .build(); + } + }); + } + + public static Entity toEntity(EntityType entityType, Object entity) { + Entity edqsEntity = new Entity(); + edqsEntity.setType(entityType); + edqsEntity.setFields(FieldsUtil.toFields(entity)); + return edqsEntity; + } + + public EdqsObject check(ObjectType type, Object object) { + if (object instanceof EdqsObject edqsObject) { + return edqsObject; + } else { + return toEntity(type.toEntityType(), object); + } + } + + @SuppressWarnings("unchecked") + @SneakyThrows + public byte[] serialize(ObjectType type, T value) { + Converter converter = (Converter) converters.get(type); + if (converter != null) { + return converter.serialize(type, value); + } else { + return defaultConverter.serialize(type, (Entity) value); + } + } + + @SneakyThrows + public EdqsObject deserialize(ObjectType type, byte[] bytes) { + Converter converter = converters.get(type); + if (converter != null) { + return converter.deserialize(type, bytes); + } else { + return defaultConverter.deserialize(type, bytes); + } + } + + @RequiredArgsConstructor + private static class JsonConverter implements Converter { + + private final Class type; + + @Override + public byte[] serialize(ObjectType objectType, T value) { + return JacksonUtil.writeValueAsBytes(value); + } + + @Override + public T deserialize(ObjectType objectType, byte[] bytes) { + return JacksonUtil.fromBytes(bytes, this.type); + } + + } + + private interface Converter { + + byte[] serialize(ObjectType type, T value) throws Exception; + + T deserialize(ObjectType type, byte[] bytes) throws Exception; + + } + +} diff --git a/common/edqs/src/main/java/org/thingsboard/server/edqs/processor/EdqsProcessor.java b/common/edqs/src/main/java/org/thingsboard/server/edqs/processor/EdqsProcessor.java new file mode 100644 index 0000000000..5383311e1f --- /dev/null +++ b/common/edqs/src/main/java/org/thingsboard/server/edqs/processor/EdqsProcessor.java @@ -0,0 +1,266 @@ +/** + * Copyright © 2016-2024 ThingsBoard, Inc. + * + * 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.edqs.processor; + +import com.google.common.collect.Sets; +import com.google.common.util.concurrent.ListenableFuture; +import com.google.common.util.concurrent.ListeningExecutorService; +import com.google.common.util.concurrent.MoreExecutors; +import jakarta.annotation.PostConstruct; +import jakarta.annotation.PreDestroy; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.apache.commons.lang3.exception.ExceptionUtils; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.context.annotation.Lazy; +import org.springframework.context.event.EventListener; +import org.springframework.stereotype.Service; +import org.thingsboard.common.util.JacksonUtil; +import org.thingsboard.common.util.ThingsBoardExecutors; +import org.thingsboard.common.util.ThingsBoardThreadFactory; +import org.thingsboard.server.common.data.ObjectType; +import org.thingsboard.server.common.data.edqs.EdqsEvent; +import org.thingsboard.server.common.data.edqs.EdqsEventType; +import org.thingsboard.server.common.data.edqs.EdqsObject; +import org.thingsboard.server.common.data.edqs.query.EdqsRequest; +import org.thingsboard.server.common.data.edqs.query.EdqsResponse; +import org.thingsboard.server.common.data.edqs.query.QueryResult; +import org.thingsboard.server.common.data.id.CustomerId; +import org.thingsboard.server.common.data.id.TenantId; +import org.thingsboard.server.common.data.page.PageData; +import org.thingsboard.server.common.data.queue.QueueConfig; +import org.thingsboard.server.common.msg.queue.ServiceType; +import org.thingsboard.server.common.msg.queue.TopicPartitionInfo; +import org.thingsboard.server.edqs.state.EdqsStateService; +import org.thingsboard.server.edqs.repo.EdqRepository; +import org.thingsboard.server.edqs.util.EdqsPartitionService; +import org.thingsboard.server.edqs.util.VersionsStore; +import org.thingsboard.server.gen.transport.TransportProtos; +import org.thingsboard.server.gen.transport.TransportProtos.EdqsEventMsg; +import org.thingsboard.server.gen.transport.TransportProtos.FromEdqsMsg; +import org.thingsboard.server.gen.transport.TransportProtos.ToEdqsMsg; +import org.thingsboard.server.queue.TbQueueHandler; +import org.thingsboard.server.queue.TbQueueResponseTemplate; +import org.thingsboard.server.queue.common.TbProtoQueueMsg; +import org.thingsboard.server.queue.common.consumer.MainQueueConsumerManager; +import org.thingsboard.server.queue.discovery.QueueKey; +import org.thingsboard.server.queue.discovery.event.PartitionChangeEvent; +import org.thingsboard.server.queue.edqs.EdqsComponent; +import org.thingsboard.server.queue.edqs.EdqsConfig; +import org.thingsboard.server.queue.edqs.EdqsConfig.EdqsPartitioningStrategy; +import org.thingsboard.server.queue.edqs.EdqsQueue; +import org.thingsboard.server.queue.edqs.EdqsQueueFactory; +import org.thingsboard.server.queue.util.AfterStartUp; + +import java.util.Set; +import java.util.UUID; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; +import java.util.concurrent.ScheduledExecutorService; +import java.util.stream.Collectors; + +@EdqsComponent +@Service +@RequiredArgsConstructor +@Slf4j +public class EdqsProcessor implements TbQueueHandler, TbProtoQueueMsg> { + + private final EdqsQueueFactory queueFactory; + private final EdqsConverter converter; + private final EdqRepository repository; + private final EdqsConfig config; + private final EdqsPartitionService partitionService; + @Autowired @Lazy + private EdqsStateService stateService; + + private MainQueueConsumerManager, QueueConfig> eventsConsumer; + private TbQueueResponseTemplate, TbProtoQueueMsg> responseTemplate; + + private ExecutorService consumersExecutor; + private ExecutorService mgmtExecutor; + private ScheduledExecutorService scheduler; + private ListeningExecutorService requestExecutor; + + private final VersionsStore versionsStore = new VersionsStore(); + + @PostConstruct + private void init() { + consumersExecutor = Executors.newCachedThreadPool(ThingsBoardThreadFactory.forName("edqs-consumer")); + mgmtExecutor = ThingsBoardExecutors.newWorkStealingPool(4, "edqs-consumer-mgmt"); + scheduler = ThingsBoardExecutors.newSingleThreadScheduledExecutor("edqs-scheduler"); + requestExecutor = MoreExecutors.listeningDecorator(ThingsBoardExecutors.newWorkStealingPool(12, "edqs-requests")); + + eventsConsumer = MainQueueConsumerManager., QueueConfig>builder() + .queueKey(new QueueKey(ServiceType.EDQS, EdqsQueue.EVENTS.getTopic())) + .config(QueueConfig.of(true, config.getPollInterval())) + .msgPackProcessor((msgs, consumer, config) -> { + for (TbProtoQueueMsg queueMsg : msgs) { + try { + ToEdqsMsg msg = queueMsg.getValue(); + log.trace("Processing message: {}", msg); + process(msg, EdqsQueue.EVENTS); + } catch (Throwable t) { + log.error("Failed to process message: {}", queueMsg, t); + } + } + consumer.commit(); + }) + .consumerCreator((config, partitionId) -> queueFactory.createEdqsMsgConsumer(EdqsQueue.EVENTS)) + .consumerExecutor(consumersExecutor) + .taskExecutor(mgmtExecutor) + .scheduler(scheduler) + .build(); + responseTemplate = queueFactory.createEdqsResponseTemplate(); + } + + @AfterStartUp(order = 1) + public void start() { + responseTemplate.launch(this); + } + + @EventListener + public void onPartitionsChange(PartitionChangeEvent event) { + if (event.getServiceType() != ServiceType.EDQS) { + return; + } + consumersExecutor.submit(() -> { + try { + Set newPartitions = event.getNewPartitions().get(new QueueKey(ServiceType.EDQS)); + Set partitions = newPartitions.stream() + .map(tpi -> tpi.withUseInternalPartition(true)) + .collect(Collectors.toSet()); + + try { + stateService.restore(withTopic(partitions, EdqsQueue.STATE.getTopic())); // blocks until restored + } catch (Exception e) { + log.error("Failed to process restore for partitions {}", partitions, e); + } + eventsConsumer.update(withTopic(partitions, EdqsQueue.EVENTS.getTopic())); + responseTemplate.subscribe(withTopic(partitions, config.getRequestsTopic())); + + Set oldPartitions = event.getOldPartitions().get(new QueueKey(ServiceType.EDQS)); + Set removedPartitions = Sets.difference(oldPartitions, newPartitions).stream() + .map(tpi -> tpi.getPartition().orElse(-1)).collect(Collectors.toSet()); + if (config.getPartitioningStrategy() != EdqsPartitioningStrategy.TENANT && !removedPartitions.isEmpty()) { + log.warn("Partitions {} were removed but shouldn't be (due to NONE partitioning strategy)", removedPartitions); + } + repository.clearIf(tenantId -> { + Integer partition = partitionService.resolvePartition(tenantId); + return partition != null && removedPartitions.contains(partition); + }); + } catch (Throwable t) { + log.error("Failed to handle partition change event {}", event, t); + } + }); + } + + @Override + public ListenableFuture> handle(TbProtoQueueMsg queueMsg) { + ToEdqsMsg toEdqsMsg = queueMsg.getValue(); + return requestExecutor.submit(() -> { + EdqsResponse response = new EdqsResponse(); + try { + EdqsRequest request = JacksonUtil.fromString(toEdqsMsg.getRequestMsg().getValue(), EdqsRequest.class); + TenantId tenantId = getTenantId(toEdqsMsg); + CustomerId customerId = getCustomerId(toEdqsMsg); + log.info("[{}] Handling request: {}", tenantId, request); + + if (request.getEntityDataQuery() != null) { + PageData result = repository.findEntityDataByQuery(tenantId, customerId, + request.getUserPermissions(), request.getEntityDataQuery(), false); + response.setEntityDataQueryResult(result.mapData(QueryResult::toOldEntityData)); + } else if (request.getEntityCountQuery() != null) { + long result = repository.countEntitiesByQuery(tenantId, customerId, request.getUserPermissions(), request.getEntityCountQuery(), tenantId.isSysTenantId()); + response.setEntityCountQueryResult(result); + } + + log.info("Answering with response: {}", response); + } catch (Throwable e) { + response.setError(ExceptionUtils.getStackTrace(e)); // TODO: return only the message + log.info("Answering with error", e); + } + return new TbProtoQueueMsg<>(queueMsg.getKey(), FromEdqsMsg.newBuilder() + .setResponseMsg(TransportProtos.EdqsResponseMsg.newBuilder() + .setValue(JacksonUtil.toString(response)) + .build()) + .build(), queueMsg.getHeaders()); + }); + } + + public void process(ToEdqsMsg edqsMsg, EdqsQueue queue) { + if (edqsMsg.hasEventMsg()) { + EdqsEventMsg eventMsg = edqsMsg.getEventMsg(); + TenantId tenantId = getTenantId(edqsMsg); + ObjectType objectType = ObjectType.valueOf(eventMsg.getObjectType()); + EdqsEventType eventType = EdqsEventType.valueOf(eventMsg.getEventType()); + String key = eventMsg.getKey(); + Long version = eventMsg.hasVersion() ? eventMsg.getVersion() : null; + + if (version != null) { + if (!versionsStore.isNew(key, version)) { + return; + } + } else { + log.warn("[{}] {} doesn't have version: {}", tenantId, objectType, edqsMsg); + } + if (queue != EdqsQueue.STATE) { + stateService.save(tenantId, objectType, key, eventType, edqsMsg); + } + + EdqsObject object = converter.deserialize(objectType, eventMsg.getData().toByteArray()); + log.info("[{}] Processing event [{}] [{}] [{}] [{}]", tenantId, objectType, eventType, key, version); + + EdqsEvent event = EdqsEvent.builder() + .tenantId(tenantId) + .objectType(objectType) + .eventType(eventType) + .object(object) + .build(); + repository.processEvent(event); + } + } + + private TenantId getTenantId(ToEdqsMsg edqsMsg) { + return TenantId.fromUUID(new UUID(edqsMsg.getTenantIdMSB(), edqsMsg.getTenantIdLSB())); + } + + private CustomerId getCustomerId(ToEdqsMsg edqsMsg) { + if (edqsMsg.getCustomerIdMSB() != 0 && edqsMsg.getCustomerIdLSB() != 0) { + return new CustomerId(new UUID(edqsMsg.getCustomerIdMSB(), edqsMsg.getCustomerIdLSB())); + } else { + return null; + } + } + + private Set withTopic(Set partitions, String topic) { + return partitions.stream() + .map(tpi -> tpi.withTopic(topic)) + .collect(Collectors.toSet()); + } + + @PreDestroy + public void destroy() throws InterruptedException { + eventsConsumer.stop(); + eventsConsumer.awaitStop(); + responseTemplate.stop(); + + consumersExecutor.shutdownNow(); + mgmtExecutor.shutdownNow(); + scheduler.shutdownNow(); + requestExecutor.shutdownNow(); + } + +} diff --git a/common/edqs/src/main/java/org/thingsboard/server/edqs/processor/EdqsProducer.java b/common/edqs/src/main/java/org/thingsboard/server/edqs/processor/EdqsProducer.java new file mode 100644 index 0000000000..762e5b37ec --- /dev/null +++ b/common/edqs/src/main/java/org/thingsboard/server/edqs/processor/EdqsProducer.java @@ -0,0 +1,83 @@ +/** + * Copyright © 2016-2024 ThingsBoard, Inc. + * + * 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.edqs.processor; + +import lombok.Builder; +import lombok.extern.slf4j.Slf4j; +import org.thingsboard.server.common.data.ObjectType; +import org.thingsboard.server.common.data.id.TenantId; +import org.thingsboard.server.common.msg.queue.TopicPartitionInfo; +import org.thingsboard.server.edqs.util.EdqsPartitionService; +import org.thingsboard.server.gen.transport.TransportProtos.ToEdqsMsg; +import org.thingsboard.server.queue.TbQueueCallback; +import org.thingsboard.server.queue.TbQueueMsgMetadata; +import org.thingsboard.server.queue.TbQueueProducer; +import org.thingsboard.server.queue.common.TbProtoQueueMsg; +import org.thingsboard.server.queue.edqs.EdqsQueue; +import org.thingsboard.server.queue.kafka.TbKafkaProducerTemplate; + +@Slf4j +public class EdqsProducer { + + private final EdqsQueue queue; + private final EdqsPartitionService partitionService; + + private final TbQueueProducer> producer; + + @Builder + public EdqsProducer(EdqsQueue queue, + EdqsPartitionService partitionService, + TbQueueProducer> producer) { + this.queue = queue; + this.partitionService = partitionService; + this.producer = producer; + } + + // TODO: queue prefix! + + public void send(TenantId tenantId, ObjectType type, String key, ToEdqsMsg msg) { + String topic = queue.getTopic(); + TbQueueCallback callback = new TbQueueCallback() { + @Override + public void onSuccess(TbQueueMsgMetadata metadata) { + log.debug("[{}][{}][{}] Published msg to {}: {}", tenantId, type, key, topic, msg); // fixme log levels + } + + @Override + public void onFailure(Throwable t) { + log.warn("[{}][{}][{}] Failed to publish msg to {}: {}", tenantId, type, key, topic, msg, t); + } + }; + if (producer instanceof TbKafkaProducerTemplate> kafkaProducer) { + TopicPartitionInfo tpi = TopicPartitionInfo.builder() + .topic(topic) + .partition(partitionService.resolvePartition(tenantId)) + .useInternalPartition(true) + .build(); + kafkaProducer.send(tpi, key, new TbProtoQueueMsg<>(null, msg), callback); // specifying custom key for compaction + } else { + TopicPartitionInfo tpi = TopicPartitionInfo.builder() + .topic(topic) + .build(); + producer.send(tpi, new TbProtoQueueMsg<>(null, msg), callback); + } + } + + public void stop() { + producer.stop(); + } + +} diff --git a/common/edqs/src/main/java/org/thingsboard/server/edqs/query/DataKey.java b/common/edqs/src/main/java/org/thingsboard/server/edqs/query/DataKey.java new file mode 100644 index 0000000000..0b0805639a --- /dev/null +++ b/common/edqs/src/main/java/org/thingsboard/server/edqs/query/DataKey.java @@ -0,0 +1,22 @@ +/** + * Copyright © 2016-2024 ThingsBoard, Inc. + * + * 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.edqs.query; + +import org.thingsboard.server.common.data.query.EntityKeyType; + +public record DataKey(EntityKeyType type, String key, Integer keyId) { + +} diff --git a/common/edqs/src/main/java/org/thingsboard/server/edqs/query/EdqsCountQuery.java b/common/edqs/src/main/java/org/thingsboard/server/edqs/query/EdqsCountQuery.java new file mode 100644 index 0000000000..6a7c1e0ce0 --- /dev/null +++ b/common/edqs/src/main/java/org/thingsboard/server/edqs/query/EdqsCountQuery.java @@ -0,0 +1,30 @@ +/** + * Copyright © 2016-2024 ThingsBoard, Inc. + * + * 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.edqs.query; + +import lombok.Builder; +import org.thingsboard.server.common.data.query.EntityFilter; + +import java.util.List; + +public class EdqsCountQuery extends EdqsQuery { + + @Builder + EdqsCountQuery(EntityFilter entityFilter, boolean hasKeyFilters, List keyFilters) { + super(entityFilter, hasKeyFilters, keyFilters); + } + +} diff --git a/common/edqs/src/main/java/org/thingsboard/server/edqs/query/EdqsDataQuery.java b/common/edqs/src/main/java/org/thingsboard/server/edqs/query/EdqsDataQuery.java new file mode 100644 index 0000000000..7f70d2ac6c --- /dev/null +++ b/common/edqs/src/main/java/org/thingsboard/server/edqs/query/EdqsDataQuery.java @@ -0,0 +1,59 @@ +/** + * Copyright © 2016-2024 ThingsBoard, Inc. + * + * 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.edqs.query; + +import lombok.Builder; +import lombok.EqualsAndHashCode; +import lombok.Getter; +import org.thingsboard.server.common.data.StringUtils; +import org.thingsboard.server.common.data.query.EntityDataSortOrder; +import org.thingsboard.server.common.data.query.EntityFilter; +import org.thingsboard.server.common.data.query.EntityKeyType; +import org.thingsboard.server.common.data.util.CollectionsUtil; + +import java.util.List; + +@EqualsAndHashCode(callSuper = true) +@Getter +public class EdqsDataQuery extends EdqsQuery { + + private final int pageSize; + private final int page; + private final boolean hasTextSearch; + private final String textSearch; + private final boolean defaultSort; + private final DataKey sortKey; + private final EntityDataSortOrder.Direction sortDirection; + private final List entityFields; + private final List latestValues; + + @Builder + public EdqsDataQuery(EntityFilter entityFilter, List keyFilters, + int pageSize, int page, String textSearch, DataKey sortKey, EntityDataSortOrder.Direction sortDirection, + List entityFields, List latestValues) { + super(entityFilter, CollectionsUtil.isNotEmpty(keyFilters), keyFilters); + this.pageSize = pageSize; + this.page = page; + this.hasTextSearch = StringUtils.isNotBlank(textSearch); + this.textSearch = textSearch; + this.defaultSort = EntityKeyType.ENTITY_FIELD.equals(sortKey.type()) && "createdTime".equals(sortKey.key()) && EntityDataSortOrder.Direction.DESC.equals(sortDirection); + this.sortKey = sortKey; + this.sortDirection = sortDirection; + this.entityFields = entityFields; + this.latestValues = latestValues; + } + +} diff --git a/common/edqs/src/main/java/org/thingsboard/server/edqs/query/EdqsFilter.java b/common/edqs/src/main/java/org/thingsboard/server/edqs/query/EdqsFilter.java new file mode 100644 index 0000000000..a728ea567e --- /dev/null +++ b/common/edqs/src/main/java/org/thingsboard/server/edqs/query/EdqsFilter.java @@ -0,0 +1,23 @@ +/** + * Copyright © 2016-2024 ThingsBoard, Inc. + * + * 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.edqs.query; + +import org.thingsboard.server.common.data.query.EntityKeyValueType; +import org.thingsboard.server.common.data.query.KeyFilterPredicate; + +public record EdqsFilter(DataKey key, EntityKeyValueType valueType, KeyFilterPredicate predicate) { + +} diff --git a/common/edqs/src/main/java/org/thingsboard/server/edqs/query/EdqsQuery.java b/common/edqs/src/main/java/org/thingsboard/server/edqs/query/EdqsQuery.java new file mode 100644 index 0000000000..31e2c482b1 --- /dev/null +++ b/common/edqs/src/main/java/org/thingsboard/server/edqs/query/EdqsQuery.java @@ -0,0 +1,30 @@ +/** + * Copyright © 2016-2024 ThingsBoard, Inc. + * + * 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.edqs.query; + +import lombok.Data; +import org.thingsboard.server.common.data.query.EntityFilter; + +import java.util.List; + +@Data +public abstract class EdqsQuery { + + private final EntityFilter entityFilter; + private final boolean hasKeyFilters; + private final List keyFilters; + +} diff --git a/common/edqs/src/main/java/org/thingsboard/server/edqs/query/SortableEntityData.java b/common/edqs/src/main/java/org/thingsboard/server/edqs/query/SortableEntityData.java new file mode 100644 index 0000000000..7185cfc63f --- /dev/null +++ b/common/edqs/src/main/java/org/thingsboard/server/edqs/query/SortableEntityData.java @@ -0,0 +1,40 @@ +/** + * Copyright © 2016-2024 ThingsBoard, Inc. + * + * 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.edqs.query; + +import lombok.Data; +import org.thingsboard.server.common.data.id.EntityId; +import org.thingsboard.server.common.data.id.EntityIdFactory; +import org.thingsboard.server.edqs.data.EntityData; + +import java.util.UUID; + +@Data +public class SortableEntityData { + + private final EntityData entityData; + private String sortValue; + private boolean readAttrs; + private boolean readTs; + + public UUID getId(){ + return entityData.getId(); + } + + public EntityId getEntityId() { + return EntityIdFactory.getByTypeAndUuid(entityData.getEntityType(), entityData.getId()); + } +} diff --git a/common/edqs/src/main/java/org/thingsboard/server/edqs/query/processor/AbstractEntityGroupQueryProcessor.java b/common/edqs/src/main/java/org/thingsboard/server/edqs/query/processor/AbstractEntityGroupQueryProcessor.java new file mode 100644 index 0000000000..87df5586c1 --- /dev/null +++ b/common/edqs/src/main/java/org/thingsboard/server/edqs/query/processor/AbstractEntityGroupQueryProcessor.java @@ -0,0 +1,75 @@ +/** + * Copyright © 2016-2024 ThingsBoard, Inc. + * + * 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.edqs.query.processor; + +import org.thingsboard.server.common.data.permission.QueryContext; +import org.thingsboard.server.common.data.query.EntityFilter; +import org.thingsboard.server.edqs.data.EntityGroupData; +import org.thingsboard.server.edqs.query.EdqsQuery; +import org.thingsboard.server.edqs.query.SortableEntityData; +import org.thingsboard.server.edqs.repo.TenantRepo; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.UUID; + +public abstract class AbstractEntityGroupQueryProcessor extends AbstractSingleEntityTypeQueryProcessor { + + public AbstractEntityGroupQueryProcessor(TenantRepo repo, QueryContext ctx, EdqsQuery query, T filter) { + super(repo, ctx, query, filter); + } + + @Override + protected List processCustomerGenericReadWithGroups(UUID customerId, boolean readAttrPermissions, boolean readTsPermissions, List groupPermissions) { + var genericReadResults = processCustomerGenericRead(customerId, readAttrPermissions, readTsPermissions); + Map mergedResult = new HashMap<>(genericReadResults.size()); + for (SortableEntityData sd : genericReadResults) { + mergedResult.put(sd.getId(), sd); + } + for (GroupPermissions permissions : groupPermissions) { + SortableEntityData alreadyAdded = mergedResult.get(permissions.groupId); + if (alreadyAdded != null) { + alreadyAdded.setReadAttrs(alreadyAdded.isReadAttrs() || permissions.readAttrs); + alreadyAdded.setReadTs(alreadyAdded.isReadTs() || permissions.readTs); + } else { + EntityGroupData egData = repository.getEntityGroup(permissions.groupId); + if (matches(egData)) { + SortableEntityData sortData = toSortData(egData, permissions); + mergedResult.put(egData.getId(), sortData); + } + } + } + return new ArrayList<>(mergedResult.values()); + } + + @Override + protected CombinedPermissions getCombinedPermissionsInternal(UUID id, boolean read, boolean readAttrs, boolean readTs, List groupPermissions) { + for (GroupPermissions eg : groupPermissions) { + if (read && readAttrs && readTs) { + break; + } + if (eg.groupId.equals(id)) { + read = true; + readAttrs = readAttrs || eg.readAttrs; + readTs = readTs || eg.readTs; + } + } + return new CombinedPermissions(read, readAttrs, readTs); + } + +} diff --git a/common/edqs/src/main/java/org/thingsboard/server/edqs/query/processor/AbstractEntityProfileNameQueryProcessor.java b/common/edqs/src/main/java/org/thingsboard/server/edqs/query/processor/AbstractEntityProfileNameQueryProcessor.java new file mode 100644 index 0000000000..e5e9dd6364 --- /dev/null +++ b/common/edqs/src/main/java/org/thingsboard/server/edqs/query/processor/AbstractEntityProfileNameQueryProcessor.java @@ -0,0 +1,52 @@ +/** + * Copyright © 2016-2024 ThingsBoard, Inc. + * + * 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.edqs.query.processor; + +import org.thingsboard.server.common.data.EntityType; +import org.thingsboard.server.common.data.permission.QueryContext; +import org.thingsboard.server.common.data.query.EntityFilter; +import org.thingsboard.server.edqs.data.EntityData; +import org.thingsboard.server.edqs.query.EdqsQuery; +import org.thingsboard.server.edqs.repo.TenantRepo; +import org.thingsboard.server.edqs.util.RepositoryUtils; + +import java.util.HashSet; +import java.util.List; +import java.util.Set; +import java.util.regex.Pattern; + +public abstract class AbstractEntityProfileNameQueryProcessor extends AbstractSimpleQueryProcessor { + + private final Set entityProfileNames; + private final Pattern pattern; + + public AbstractEntityProfileNameQueryProcessor(TenantRepo repo, QueryContext ctx, EdqsQuery query, T filter, EntityType entityType) { + super(repo, ctx, query, filter, entityType); + entityProfileNames = new HashSet<>(getProfileNames(this.filter)); + pattern = RepositoryUtils.toSqlLikePattern(getEntityNameFilter(filter)); + } + + protected abstract String getEntityNameFilter(T filter); + + protected abstract List getProfileNames(T filter); + + @Override + protected boolean matches(EntityData ed) { + return super.matches(ed) && entityProfileNames.contains(ed.getFields().getType()) + && (pattern == null || pattern.matcher(ed.getFields().getName()).matches()); + } + +} diff --git a/common/edqs/src/main/java/org/thingsboard/server/edqs/query/processor/AbstractEntityProfileQueryProcessor.java b/common/edqs/src/main/java/org/thingsboard/server/edqs/query/processor/AbstractEntityProfileQueryProcessor.java new file mode 100644 index 0000000000..96c3186358 --- /dev/null +++ b/common/edqs/src/main/java/org/thingsboard/server/edqs/query/processor/AbstractEntityProfileQueryProcessor.java @@ -0,0 +1,62 @@ +/** + * Copyright © 2016-2024 ThingsBoard, Inc. + * + * 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.edqs.query.processor; + +import org.thingsboard.server.common.data.EntityType; +import org.thingsboard.server.common.data.permission.QueryContext; +import org.thingsboard.server.common.data.query.EntityFilter; +import org.thingsboard.server.edqs.data.EntityData; +import org.thingsboard.server.edqs.data.ProfileAwareData; +import org.thingsboard.server.edqs.query.EdqsQuery; +import org.thingsboard.server.edqs.repo.TenantRepo; +import org.thingsboard.server.edqs.util.RepositoryUtils; + +import java.util.HashSet; +import java.util.List; +import java.util.Set; +import java.util.UUID; +import java.util.regex.Pattern; + +public abstract class AbstractEntityProfileQueryProcessor extends AbstractSimpleQueryProcessor { + + private final Set entityProfileIds = new HashSet<>(); + private final Pattern pattern; + + public AbstractEntityProfileQueryProcessor(TenantRepo repo, QueryContext ctx, EdqsQuery query, T filter, EntityType entityType) { + super(repo, ctx, query, filter, entityType); + var profileNamesSet = new HashSet<>(getProfileNames(this.filter)); + for (EntityData dp : repo.getEntitySet(getProfileEntityType())) { + if (profileNamesSet.contains(dp.getFields().getName())) { + entityProfileIds.add(dp.getId()); + } + } + pattern = RepositoryUtils.toSqlLikePattern(getEntityNameFilter(filter)); + } + + protected abstract String getEntityNameFilter(T filter); + + protected abstract List getProfileNames(T filter); + + protected abstract EntityType getProfileEntityType(); + + @Override + protected boolean matches(EntityData ed) { + ProfileAwareData profileAwareData = (ProfileAwareData) ed; + return super.matches(ed) && entityProfileIds.contains(profileAwareData.getFields().getProfileId()) + && (pattern == null || pattern.matcher(profileAwareData.getFields().getName()).matches()); + } + +} diff --git a/common/edqs/src/main/java/org/thingsboard/server/edqs/query/processor/AbstractEntitySearchQueryProcessor.java b/common/edqs/src/main/java/org/thingsboard/server/edqs/query/processor/AbstractEntitySearchQueryProcessor.java new file mode 100644 index 0000000000..a5f7621d43 --- /dev/null +++ b/common/edqs/src/main/java/org/thingsboard/server/edqs/query/processor/AbstractEntitySearchQueryProcessor.java @@ -0,0 +1,66 @@ +/** + * Copyright © 2016-2024 ThingsBoard, Inc. + * + * 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.edqs.query.processor; + +import org.thingsboard.server.common.data.EntityType; +import org.thingsboard.server.common.data.permission.QueryContext; +import org.thingsboard.server.common.data.query.EntitySearchQueryFilter; +import org.thingsboard.server.common.data.relation.EntitySearchDirection; +import org.thingsboard.server.edqs.data.EntityData; +import org.thingsboard.server.edqs.data.RelationInfo; +import org.thingsboard.server.edqs.query.EdqsQuery; +import org.thingsboard.server.edqs.repo.TenantRepo; + +import java.util.Set; +import java.util.UUID; + +public abstract class AbstractEntitySearchQueryProcessor extends AbstractRelationQueryProcessor { + + + public AbstractEntitySearchQueryProcessor(TenantRepo repo, QueryContext ctx, EdqsQuery query, T filter) { + super(repo, ctx, query, filter); + } + + @Override + public Set getRootEntities() { + return Set.of(filter.getRootEntity().getId()); + } + + @Override + public EntitySearchDirection getDirection() { + return filter.getDirection(); + } + + @Override + public int getMaxLevel() { + return filter.getMaxLevel(); + } + + @Override + public boolean isFetchLastLevelOnly() { + return filter.isFetchLastLevelOnly(); + } + + public abstract EntityType getEntityType(); + + @Override + protected boolean check(RelationInfo relationInfo) { + EntityData target = relationInfo.getTarget(); + return (filter.getRelationType() == null || relationInfo.getType().equals(filter.getRelationType())) && + getEntityType().equals(target.getEntityType()) && super.matches(target); + } + +} diff --git a/common/edqs/src/main/java/org/thingsboard/server/edqs/query/processor/AbstractQueryProcessor.java b/common/edqs/src/main/java/org/thingsboard/server/edqs/query/processor/AbstractQueryProcessor.java new file mode 100644 index 0000000000..9efc005df2 --- /dev/null +++ b/common/edqs/src/main/java/org/thingsboard/server/edqs/query/processor/AbstractQueryProcessor.java @@ -0,0 +1,129 @@ +/** + * Copyright © 2016-2024 ThingsBoard, Inc. + * + * 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.edqs.query.processor; + +import org.thingsboard.server.common.data.id.EntityGroupId; +import org.thingsboard.server.common.data.permission.MergedGroupTypePermissionInfo; +import org.thingsboard.server.common.data.permission.QueryContext; +import org.thingsboard.server.common.data.query.EntityFilter; +import org.thingsboard.server.edqs.data.EntityData; +import org.thingsboard.server.edqs.query.DataKey; +import org.thingsboard.server.edqs.query.EdqsDataQuery; +import org.thingsboard.server.edqs.query.EdqsQuery; +import org.thingsboard.server.edqs.query.SortableEntityData; +import org.thingsboard.server.edqs.repo.TenantRepo; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.List; +import java.util.Set; +import java.util.UUID; +import java.util.function.Consumer; + +import static org.thingsboard.server.edqs.util.RepositoryUtils.checkFilters; +import static org.thingsboard.server.edqs.util.RepositoryUtils.getSortValue; + +public abstract class AbstractQueryProcessor implements EntityQueryProcessor { + + protected final TenantRepo repository; + protected final QueryContext ctx; + protected final EdqsQuery query; + protected final DataKey sortKey; + protected final T filter; + + public AbstractQueryProcessor(TenantRepo repository, QueryContext ctx, EdqsQuery query, T filter) { + this.repository = repository; + this.ctx = ctx; + this.query = query; + this.sortKey = query instanceof EdqsDataQuery dataQuery ? dataQuery.getSortKey() : null; + this.filter = filter; + } + + protected CombinedPermissions getCombinedPermissions(UUID id, boolean genericRead, boolean genericAttrs, boolean genericTs, List groupPermissions) { + return getCombinedPermissionsInternal(id, genericRead, genericRead && genericAttrs, genericRead && genericTs, groupPermissions); + } + + protected CombinedPermissions getCombinedPermissions(UUID id, List groupPermissions) { + return getCombinedPermissionsInternal(id, false, false, false, groupPermissions); + } + + protected CombinedPermissions getCombinedPermissionsInternal(UUID id, boolean read, boolean readAttrs, boolean readTs, List groupPermissions) { + for (GroupPermissions eg : groupPermissions) { + if (read && readAttrs && readTs) { + break; + } + boolean hasMorePermissions = !read || (!readAttrs && eg.readAttrs) || (!readTs && eg.readTs); + if (hasMorePermissions && repository.contains(eg.groupId, id)) { + read = true; + readAttrs = readAttrs || eg.readAttrs; + readTs = readTs || eg.readTs; + } + } + return new CombinedPermissions(read, readAttrs, readTs); + } + + protected SortableEntityData toSortDataGroupsOnly(EntityData ed, List groupPermissions) { + SortableEntityData sortData; + CombinedPermissions permissions = getCombinedPermissions(ed.getId(), groupPermissions); + if (permissions.isRead()) { + sortData = toSortData(ed, permissions); + } else { + sortData = null; + } + return sortData; + } + + protected SortableEntityData toSortData(EntityData ed, boolean readAttrs, boolean readTs) { + SortableEntityData sortData = new SortableEntityData(ed); + sortData.setSortValue(getSortValue(ed, sortKey)); + sortData.setReadAttrs(readAttrs); + sortData.setReadTs(readTs); + return sortData; + } + + protected SortableEntityData toSortData(EntityData ed, Permissions permissions) { + return toSortData(ed, permissions.isReadAttrs(), permissions.isReadTs()); + } + + protected static List toGroupPermissions(MergedGroupTypePermissionInfo readPermissions, + MergedGroupTypePermissionInfo readAttrPermissions, + MergedGroupTypePermissionInfo readTsPermissions) { + List permissions = new ArrayList<>(); + for (EntityGroupId egId : readPermissions.getEntityGroupIds()) { + permissions.add(new GroupPermissions(egId.getId(), + readAttrPermissions.getEntityGroupIds() != null && readAttrPermissions.getEntityGroupIds().contains(egId), + readTsPermissions.getEntityGroupIds() != null && readTsPermissions.getEntityGroupIds().contains(egId))); + } + return permissions; + } + + protected static boolean checkCustomerHierarchy(Set customers, EntityData ed) { + return ed.getCustomerId() != null && customers.contains(ed.getCustomerId()); + } + + protected void process(Collection> entities, Consumer> processor) { + for (EntityData ed : entities) { + if (matches(ed)) { + processor.accept(ed); + } + } + } + + protected boolean matches(EntityData ed) { + return checkFilters(query, ed); + } + +} diff --git a/common/edqs/src/main/java/org/thingsboard/server/edqs/query/processor/AbstractRelationQueryProcessor.java b/common/edqs/src/main/java/org/thingsboard/server/edqs/query/processor/AbstractRelationQueryProcessor.java new file mode 100644 index 0000000000..755c79cea6 --- /dev/null +++ b/common/edqs/src/main/java/org/thingsboard/server/edqs/query/processor/AbstractRelationQueryProcessor.java @@ -0,0 +1,260 @@ +/** + * Copyright © 2016-2024 ThingsBoard, Inc. + * + * 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.edqs.query.processor; + +import lombok.RequiredArgsConstructor; +import org.thingsboard.server.common.data.EntityType; +import org.thingsboard.server.common.data.permission.QueryContext; +import org.thingsboard.server.common.data.permission.Resource; +import org.thingsboard.server.common.data.query.EntityFilter; +import org.thingsboard.server.common.data.relation.EntitySearchDirection; +import org.thingsboard.server.common.data.relation.RelationTypeGroup; +import org.thingsboard.server.edqs.data.EntityData; +import org.thingsboard.server.edqs.data.RelationInfo; +import org.thingsboard.server.edqs.data.RelationsRepo; +import org.thingsboard.server.edqs.query.EdqsQuery; +import org.thingsboard.server.edqs.query.SortableEntityData; +import org.thingsboard.server.edqs.repo.TenantRepo; + +import java.util.ArrayList; +import java.util.HashSet; +import java.util.LinkedList; +import java.util.List; +import java.util.Queue; +import java.util.Set; +import java.util.UUID; + +import static org.thingsboard.server.edqs.util.RepositoryUtils.getSortValue; + +public abstract class AbstractRelationQueryProcessor extends AbstractQueryProcessor { + + public static final int MAXIMUM_QUERY_LEVEL = 100; + + public AbstractRelationQueryProcessor(TenantRepo repo, QueryContext ctx, EdqsQuery query, T filter) { + super(repo, ctx, query, filter); + } + + protected abstract Set getRootEntities(); + + protected abstract EntitySearchDirection getDirection(); + + protected abstract int getMaxLevel(); + + protected abstract boolean isFetchLastLevelOnly(); + + protected boolean isMultiRoot() { + return false; + } + + @Override + public List processQuery() { + var relations = repository.getRelations(RelationTypeGroup.COMMON); + var entities = getEntitiesSet(relations); + if (ctx.isTenantUser()) { + return processTenantQuery(entities); + } else { + return processCustomerQuery(entities); + } + } + + @Override + public long count() { + var relations = repository.getRelations(RelationTypeGroup.COMMON); + var entities = getEntitiesSet(relations); + long result = 0; + + RelationQueryPermissions[] permissionsArray = buildPermissionsArray(); + if (ctx.isTenantUser()) { + for (EntityData ed : entities) { + var permissions = permissionsArray[ed.getEntityType().ordinal()]; + if (permissions != null) { + if (permissions.isHasGroups()) { + CombinedPermissions combinedPermissions = getCombinedPermissions(ed.getId(), + permissions.isReadEntity(), permissions.isReadAttrs(), permissions.isReadTs(), permissions.getGroupPermissions()); + if (combinedPermissions.isRead()) { + result++; + } + } else if (permissions.isReadEntity()) { + result++; + } + } + } + } else { + var customerIds = repository.getAllCustomers(ctx.getCustomerId().getId()); + for (EntityData ed : entities) { + var permissions = permissionsArray[ed.getEntityType().ordinal()]; + if (permissions != null) { + boolean isReadEntity = permissions.isReadEntity() && ed.getCustomerId() != null && customerIds.contains(ed.getCustomerId()); + if (permissions.isHasGroups()) { + CombinedPermissions combinedPermissions = getCombinedPermissions(ed.getId(), + isReadEntity, + permissions.isReadAttrs(), permissions.isReadTs(), permissions.getGroupPermissions()); + if (combinedPermissions.isRead()) { + result++; + } + } else if (isReadEntity) { + result++; + } + } + } + return result; + } + return result; + } + + private List processTenantQuery(Set> entities) { + List result = new ArrayList<>(); + RelationQueryPermissions[] permissionsArray = buildPermissionsArray(); + for (EntityData ed : entities) { + var permissions = permissionsArray[ed.getEntityType().ordinal()]; + if (permissions != null) { + if (permissions.isHasGroups()) { + CombinedPermissions combinedPermissions = getCombinedPermissions(ed.getId(), + permissions.isReadEntity(), permissions.isReadAttrs(), permissions.isReadTs(), permissions.getGroupPermissions()); + if (combinedPermissions.isRead()) { + SortableEntityData sortData = new SortableEntityData(ed); + sortData.setSortValue(getSortValue(ed, sortKey)); + sortData.setReadAttrs(combinedPermissions.isReadAttrs()); + sortData.setReadTs(combinedPermissions.isReadTs()); + result.add(sortData); + } + } else if (permissions.isReadEntity()) { + result.add(toSortData(ed, permissions)); + } + } + } + return result; + } + + private List processCustomerQuery(Set> entities) { + var customerIds = repository.getAllCustomers(ctx.getCustomerId().getId()); + RelationQueryPermissions[] permissionsArray = buildPermissionsArray(); + List result = new ArrayList<>(); + for (EntityData ed : entities) { + var permissions = permissionsArray[ed.getEntityType().ordinal()]; + if (permissions != null) { + boolean isReadEntity = permissions.isReadEntity() && ed.getCustomerId() != null && customerIds.contains(ed.getCustomerId()); + if (permissions.isHasGroups()) { + SortableEntityData sortData = new SortableEntityData(ed); + sortData.setSortValue(getSortValue(ed, sortKey)); + CombinedPermissions combinedPermissions = getCombinedPermissions(ed.getId(), + isReadEntity, + permissions.isReadAttrs(), permissions.isReadTs(), permissions.getGroupPermissions()); + if (combinedPermissions.isRead()) { + sortData.setReadAttrs(combinedPermissions.isReadAttrs()); + sortData.setReadTs(combinedPermissions.isReadTs()); + result.add(sortData); + } + } else if (isReadEntity) { + result.add(toSortData(ed, permissions)); + } + } + } + return result; + } + + private RelationQueryPermissions[] buildPermissionsArray() { + RelationQueryPermissions[] permissionsArray = new RelationQueryPermissions[EntityType.values().length]; + var readEntityPermissionsMap = ctx.getMergedReadEntityPermissionsMap(); + var readAttrPermissionsMap = ctx.getMergedReadAttrPermissionsMap(); + var readTsPermissionsMap = ctx.getMergedReadTsPermissionsMap(); + for (EntityType et : EntityType.values()) { + var resource = Resource.resourceFromEntityType(et); + if (resource == null) { + continue; + } + var readEntityPermissions = readEntityPermissionsMap.get(resource); + var readAttrPermissions = readAttrPermissionsMap.get(resource); + var readTsPermissions = readTsPermissionsMap.get(resource); + var groupPermissions = toGroupPermissions(readEntityPermissions, readAttrPermissions, readTsPermissions); + RelationQueryPermissions entityPermissions = RelationQueryPermissions + .builder() + .readEntity(readEntityPermissions.isHasGenericRead()) + .readAttrs(readAttrPermissions.isHasGenericRead()) + .readTs(readTsPermissions.isHasGenericRead()) + .hasGroups(!groupPermissions.isEmpty()) + .groupPermissions(groupPermissions) + .build(); + permissionsArray[et.ordinal()] = entityPermissions; + } + return permissionsArray; + } + + private Set> getEntitiesSet(RelationsRepo relations) { + Set> result = new HashSet<>(); + Set processed = new HashSet<>(); + Queue tasks = new LinkedList<>(); + int maxLvl = getMaxLevel() == 0 ? MAXIMUM_QUERY_LEVEL : Math.max(1, getMaxLevel()); + for (UUID uuid : getRootEntities()) { + tasks.add(new RelationSearchTask(uuid, 0)); + } + while (!tasks.isEmpty()) { + RelationSearchTask task = tasks.poll(); + if (processed.add(task.entityId)) { + var entityLvl = task.lvl + 1; + Set entities = EntitySearchDirection.FROM.equals(getDirection()) ? relations.getFrom(task.entityId) : relations.getTo(task.entityId); + if (isFetchLastLevelOnly() && entities.isEmpty() && task.previous != null && check(task.previous)) { + result.add(task.previous.getTarget()); + } + for (RelationInfo relationInfo : entities) { + var entity = relationInfo.getTarget(); + if (entity.isEmpty()) { + continue; + } + var entityId = entity.getId(); + if (isFetchLastLevelOnly()) { + if (entityLvl < maxLvl) { + tasks.add(new RelationSearchTask(entityId, entityLvl, relationInfo)); + } else if (entityLvl == maxLvl) { + if (check(relationInfo)) { + if (isMultiRoot()) { + ctx.getRelatedParentIdMap().put(entity.getId(), task.entityId); + } + result.add(entity); + } + } + } else { + if (check(relationInfo)) { + if (isMultiRoot()) { + ctx.getRelatedParentIdMap().put(entity.getId(), task.entityId); + } + result.add(entity); + } + if (entityLvl < maxLvl) { + tasks.add(new RelationSearchTask(entityId, entityLvl)); + } + } + } + } + } + return result; + } + + protected abstract boolean check(RelationInfo relationInfo); + + @RequiredArgsConstructor + private static class RelationSearchTask { + private final UUID entityId; + private final int lvl; + private final RelationInfo previous; + + public RelationSearchTask(UUID entityId, int lvl) { + this(entityId, lvl, null); + } + + } + +} diff --git a/common/edqs/src/main/java/org/thingsboard/server/edqs/query/processor/AbstractSimpleQueryProcessor.java b/common/edqs/src/main/java/org/thingsboard/server/edqs/query/processor/AbstractSimpleQueryProcessor.java new file mode 100644 index 0000000000..2a9ef090e7 --- /dev/null +++ b/common/edqs/src/main/java/org/thingsboard/server/edqs/query/processor/AbstractSimpleQueryProcessor.java @@ -0,0 +1,99 @@ +/** + * Copyright © 2016-2024 ThingsBoard, Inc. + * + * 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.edqs.query.processor; + +import org.thingsboard.server.common.data.EntityType; +import org.thingsboard.server.common.data.permission.QueryContext; +import org.thingsboard.server.common.data.query.EntityFilter; +import org.thingsboard.server.edqs.data.CustomerData; +import org.thingsboard.server.edqs.data.EntityData; +import org.thingsboard.server.edqs.data.EntityGroupData; +import org.thingsboard.server.edqs.query.EdqsQuery; +import org.thingsboard.server.edqs.query.SortableEntityData; +import org.thingsboard.server.edqs.repo.TenantRepo; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.UUID; +import java.util.function.Consumer; + +public abstract class AbstractSimpleQueryProcessor extends AbstractSingleEntityTypeQueryProcessor { + + private final EntityType entityType; + + public AbstractSimpleQueryProcessor(TenantRepo repo, QueryContext ctx, EdqsQuery query, T filter, EntityType entityType) { + super(repo, ctx, query, filter); + this.entityType = entityType; + } + + @Override + protected void processCustomerGenericRead(UUID customerId, Consumer> processor) { + var customers = repository.getEntityMap(EntityType.CUSTOMER); + for (UUID cId : repository.getAllCustomers(customerId)) { + var customerData = (CustomerData) customers.get(cId); + if (customerData != null) { + process(customerData.getEntities(entityType), processor); + } + } + } + + @Override + protected List processCustomerGenericReadWithGroups(UUID customerId, boolean readAttrPermissions, boolean readTsPermissions, List groupPermissions) { + var genericReadResults = processCustomerGenericRead(customerId, readAttrPermissions, readTsPermissions); + Map mergedResult = new HashMap<>(genericReadResults.size()); + for (SortableEntityData sd : genericReadResults) { + mergedResult.put(sd.getId(), sd); + } + + for (GroupPermissions permissions : groupPermissions) { + EntityGroupData egData = repository.getEntityGroup(permissions.groupId); + for (EntityData ed : egData.getEntities()) { + SortableEntityData alreadyAdded = mergedResult.get(ed.getId()); + if (alreadyAdded != null) { + alreadyAdded.setReadAttrs(alreadyAdded.isReadAttrs() || permissions.readAttrs); + alreadyAdded.setReadTs(alreadyAdded.isReadTs() || permissions.readTs); + } else { + if (matches(ed)) { + SortableEntityData sortData = toSortData(ed, permissions); + mergedResult.put(ed.getId(), sortData); + } + } + } + } + return new ArrayList<>(mergedResult.values()); + } + + @Override + protected void processGroupsOnly(List groupPermissions, Consumer> processor) { + for (GroupPermissions groupPermission : groupPermissions) { + EntityGroupData egData = repository.getEntityGroup(groupPermission.groupId); + process(egData.getEntities(), processor); + } + } + + @Override + protected void processAll(Consumer> processor) { + process(repository.getEntitySet(entityType), processor); + } + + @Override + protected int getProbableResultSize() { + return 1024; + } + +} diff --git a/common/edqs/src/main/java/org/thingsboard/server/edqs/query/processor/AbstractSingleEntityTypeQueryProcessor.java b/common/edqs/src/main/java/org/thingsboard/server/edqs/query/processor/AbstractSingleEntityTypeQueryProcessor.java new file mode 100644 index 0000000000..9411d75935 --- /dev/null +++ b/common/edqs/src/main/java/org/thingsboard/server/edqs/query/processor/AbstractSingleEntityTypeQueryProcessor.java @@ -0,0 +1,172 @@ +/** + * Copyright © 2016-2024 ThingsBoard, Inc. + * + * 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.edqs.query.processor; + +import org.thingsboard.server.common.data.permission.QueryContext; +import org.thingsboard.server.common.data.query.EntityFilter; +import org.thingsboard.server.edqs.data.EntityData; +import org.thingsboard.server.edqs.query.EdqsQuery; +import org.thingsboard.server.edqs.query.SortableEntityData; +import org.thingsboard.server.edqs.repo.TenantRepo; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.UUID; +import java.util.concurrent.atomic.AtomicLong; +import java.util.function.Consumer; + +public abstract class AbstractSingleEntityTypeQueryProcessor extends AbstractQueryProcessor { + + public AbstractSingleEntityTypeQueryProcessor(TenantRepo repository, QueryContext ctx, EdqsQuery query, T filter) { + super(repository, ctx, query, filter); + } + + @Override + public List processQuery() { + var readPermissions = ctx.getMergedReadPermissionsByEntityType(); + if (readPermissions == null) { + return Collections.emptyList(); + } + var readAttrPermissions = ctx.getMergedReadAttrPermissionsByEntityType(); + var readTsPermissions = ctx.getMergedReadTsPermissionsByEntityType(); + + boolean hasGenericRead = readPermissions.isHasGenericRead(); + boolean hasGroups = readPermissions.getEntityGroupIds() != null && !readPermissions.getEntityGroupIds().isEmpty(); + if (!hasGenericRead && !hasGroups) { + return Collections.emptyList(); + } + boolean hasGenericAttrRead = readAttrPermissions.isHasGenericRead(); + boolean hasGenericTsRead = readTsPermissions.isHasGenericRead(); + + if (hasGenericRead) { + if (ctx.isTenantUser()) { + if (hasGroups && (!hasGenericAttrRead || !hasGenericTsRead)) { + return processTenantGenericReadWithGroups(hasGenericAttrRead, hasGenericTsRead, + toGroupPermissions(readPermissions, readAttrPermissions, readTsPermissions)); + } else { + return processTenantGenericRead(hasGenericAttrRead, hasGenericTsRead); + } + } else { + if (hasGroups) { + return processCustomerGenericReadWithGroups(ctx.getCustomerId().getId(), hasGenericAttrRead, hasGenericTsRead, + toGroupPermissions(readPermissions, readAttrPermissions, readTsPermissions)); + } else { + return processCustomerGenericRead(ctx.getCustomerId().getId(), hasGenericAttrRead, hasGenericTsRead); + } + } + } else { + return processGroupsOnly(toGroupPermissions(readPermissions, readAttrPermissions, readTsPermissions)); + } + } + + @Override + public long count() { // TODO: get rid of the duplicates + var readPermissions = ctx.getMergedReadPermissionsByEntityType(); + if (readPermissions == null) { + return 0; + } + var readAttrPermissions = ctx.getMergedReadAttrPermissionsByEntityType(); + var readTsPermissions = ctx.getMergedReadTsPermissionsByEntityType(); + boolean hasGenericRead = readPermissions.isHasGenericRead(); + boolean hasGroups = readPermissions.getEntityGroupIds() != null && !readPermissions.getEntityGroupIds().isEmpty(); + + if (!hasGenericRead && !hasGroups && !ctx.isIgnorePermissionCheck()) { + return 0; + } + + AtomicLong result = new AtomicLong(); + Consumer> counter = ed -> result.incrementAndGet(); + + if (ctx.isIgnorePermissionCheck()) { + processAll(counter); + } else if (ctx.isTenantUser()) { + if (hasGenericRead) { + processAll(counter); + } else { + processGroupsOnly(toGroupPermissions(readPermissions, readAttrPermissions, readTsPermissions), counter); + } + } else { + if (hasGenericRead) { + if (hasGroups) { + result.addAndGet(processCustomerGenericReadWithGroups(ctx.getCustomerId().getId(), readAttrPermissions.isHasGenericRead(), readTsPermissions.isHasGenericRead(), + toGroupPermissions(readPermissions, readAttrPermissions, readTsPermissions)).size()); // FIXME: not efficient + } else { + processCustomerGenericRead(ctx.getCustomerId().getId(), counter); + } + } else { + processGroupsOnly(toGroupPermissions(readPermissions, readAttrPermissions, readTsPermissions), counter); + } + } + return result.get(); + } + + protected List processTenantGenericRead(boolean readAttrPermissions, + boolean readTsPermissions) { + List result = new ArrayList<>(getProbableResultSize()); + processAll(ed -> { + result.add(toSortData(ed, readAttrPermissions, readTsPermissions)); + }); + return result; + } + + protected List processCustomerGenericRead(UUID customerId, + boolean readAttrPermissions, + boolean readTsPermissions) { + List result = new ArrayList<>(getProbableResultSize()); + processCustomerGenericRead(customerId, ed -> { + result.add(toSortData(ed, readAttrPermissions, readTsPermissions)); + }); + return result; + } + + protected abstract void processCustomerGenericRead(UUID customerId, Consumer> processor); + + protected List processTenantGenericReadWithGroups(boolean readAttrPermissions, + boolean readTsPermissions, + List groupPermissions) { + List result = new ArrayList<>(getProbableResultSize()); + processAll(ed -> { + CombinedPermissions permissions = getCombinedPermissions(ed.getId(), true, readAttrPermissions, readTsPermissions, groupPermissions); + SortableEntityData sortData = toSortData(ed, permissions); + result.add(sortData); + }); + return result; + } + + protected abstract List processCustomerGenericReadWithGroups(UUID customerId, + boolean readAttrPermissions, + boolean readTsPermissions, + List groupPermissions); + + protected List processGroupsOnly(List groupPermissions) { + List result = new ArrayList<>(getProbableResultSize()); + processGroupsOnly(groupPermissions, ed -> { + SortableEntityData sortData = toSortDataGroupsOnly(ed, groupPermissions); + if (sortData != null) { + result.add(sortData); + } + }); + return result; + } + + protected abstract void processGroupsOnly(List groupPermissions, Consumer> processor); + + protected abstract void processAll(Consumer> processor); + + protected abstract int getProbableResultSize(); + +} diff --git a/common/edqs/src/main/java/org/thingsboard/server/edqs/query/processor/ApiUsageStateQueryProcessor.java b/common/edqs/src/main/java/org/thingsboard/server/edqs/query/processor/ApiUsageStateQueryProcessor.java new file mode 100644 index 0000000000..c2821161c7 --- /dev/null +++ b/common/edqs/src/main/java/org/thingsboard/server/edqs/query/processor/ApiUsageStateQueryProcessor.java @@ -0,0 +1,88 @@ +/** + * Copyright © 2016-2024 ThingsBoard, Inc. + * + * 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.edqs.query.processor; + +import org.thingsboard.server.common.data.EntityType; +import org.thingsboard.server.common.data.edqs.fields.ApiUsageStateFields; +import org.thingsboard.server.common.data.permission.QueryContext; +import org.thingsboard.server.common.data.query.ApiUsageStateFilter; +import org.thingsboard.server.edqs.data.CustomerData; +import org.thingsboard.server.edqs.data.EntityData; +import org.thingsboard.server.edqs.query.EdqsQuery; +import org.thingsboard.server.edqs.query.SortableEntityData; +import org.thingsboard.server.edqs.repo.TenantRepo; + +import java.util.Collection; +import java.util.Collections; +import java.util.List; +import java.util.UUID; +import java.util.function.Consumer; + +public class ApiUsageStateQueryProcessor extends AbstractSingleEntityTypeQueryProcessor { + + public ApiUsageStateQueryProcessor(TenantRepo repo, QueryContext ctx, EdqsQuery query) { + super(repo, ctx, query, (ApiUsageStateFilter) query.getEntityFilter()); + } + + @Override + protected void processCustomerGenericRead(UUID customerId, Consumer> processor) { + CustomerData customerData = (CustomerData) repository.getEntityMap(EntityType.CUSTOMER).get(customerId); + process(customerData.getEntities(EntityType.API_USAGE_STATE), processor); + } + + + @Override + protected List processCustomerGenericReadWithGroups(UUID customerId, boolean readAttrPermissions, boolean readTsPermissions, List groupPermissions) { + CustomerData customerData = (CustomerData) repository.getEntityMap(EntityType.CUSTOMER).get(customerId); + Collection> entities = customerData.getEntities(EntityType.API_USAGE_STATE); + EntityData ed = entities.iterator().next(); + if (entities.isEmpty() || !matches(ed)) { + return Collections.emptyList(); + } else { + boolean genericRead = customerId.equals(ed.getCustomerId()); + CombinedPermissions permissions = getCombinedPermissions(ed.getId(), genericRead, readAttrPermissions, readTsPermissions, groupPermissions); + if (permissions.isRead()) { + SortableEntityData sortData = toSortData(customerData, permissions); + return Collections.singletonList(sortData); + } else { + return Collections.emptyList(); + } + } + } + + @Override + protected void processGroupsOnly(List groupPermissions, Consumer> processor) { + processAll(processor); + } + + @Override + protected void processAll(Consumer> processor) { + process(repository.getEntitySet(EntityType.API_USAGE_STATE), processor); + } + + @Override + protected boolean matches(EntityData ed) { + ApiUsageStateFields entityFields = (ApiUsageStateFields) ed.getFields(); + return super.matches(ed) && (filter.getCustomerId() != null ? entityFields.getEntityId().equals(filter.getCustomerId()) : + entityFields.getEntityId().equals(repository.getTenantId())); + } + + @Override + protected int getProbableResultSize() { + return 1; + } + +} diff --git a/common/edqs/src/main/java/org/thingsboard/server/edqs/query/processor/AssetSearchQueryProcessor.java b/common/edqs/src/main/java/org/thingsboard/server/edqs/query/processor/AssetSearchQueryProcessor.java new file mode 100644 index 0000000000..89826fe1be --- /dev/null +++ b/common/edqs/src/main/java/org/thingsboard/server/edqs/query/processor/AssetSearchQueryProcessor.java @@ -0,0 +1,59 @@ +/** + * Copyright © 2016-2024 ThingsBoard, Inc. + * + * 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.edqs.query.processor; + +import org.thingsboard.server.common.data.EntityType; +import org.thingsboard.server.common.data.permission.QueryContext; +import org.thingsboard.server.common.data.query.AssetSearchQueryFilter; +import org.thingsboard.server.common.data.util.CollectionsUtil; +import org.thingsboard.server.edqs.data.EntityData; +import org.thingsboard.server.edqs.data.ProfileAwareData; +import org.thingsboard.server.edqs.data.RelationInfo; +import org.thingsboard.server.edqs.query.EdqsQuery; +import org.thingsboard.server.edqs.repo.TenantRepo; + +import java.util.HashSet; +import java.util.Set; +import java.util.UUID; + +public class AssetSearchQueryProcessor extends AbstractEntitySearchQueryProcessor { + + private final Set entityProfileIds = new HashSet<>(); + + public AssetSearchQueryProcessor(TenantRepo repo, QueryContext ctx, EdqsQuery query) { + super(repo, ctx, query, (AssetSearchQueryFilter) query.getEntityFilter()); + if (CollectionsUtil.isNotEmpty(filter.getAssetTypes())) { + var profileNamesSet = new HashSet<>(this.filter.getAssetTypes()); + for (EntityData dp : repo.getEntitySet(EntityType.ASSET_PROFILE)) { + if (profileNamesSet.contains(dp.getFields().getName())) { + entityProfileIds.add(dp.getId()); + } + } + } + } + + @Override + public EntityType getEntityType() { + return EntityType.ASSET; + } + + @Override + protected boolean check(RelationInfo relationInfo) { + return super.check(relationInfo) && + (entityProfileIds.isEmpty() || entityProfileIds.contains(((ProfileAwareData) relationInfo.getTarget()).getFields().getProfileId())); + } + +} diff --git a/common/edqs/src/main/java/org/thingsboard/server/edqs/query/processor/AssetTypeQueryProcessor.java b/common/edqs/src/main/java/org/thingsboard/server/edqs/query/processor/AssetTypeQueryProcessor.java new file mode 100644 index 0000000000..d012e12e46 --- /dev/null +++ b/common/edqs/src/main/java/org/thingsboard/server/edqs/query/processor/AssetTypeQueryProcessor.java @@ -0,0 +1,47 @@ +/** + * Copyright © 2016-2024 ThingsBoard, Inc. + * + * 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.edqs.query.processor; + +import org.thingsboard.server.common.data.EntityType; +import org.thingsboard.server.common.data.permission.QueryContext; +import org.thingsboard.server.common.data.query.AssetTypeFilter; +import org.thingsboard.server.edqs.query.EdqsQuery; +import org.thingsboard.server.edqs.repo.TenantRepo; + +import java.util.List; + +public class AssetTypeQueryProcessor extends AbstractEntityProfileQueryProcessor { + + public AssetTypeQueryProcessor(TenantRepo repo, QueryContext ctx, EdqsQuery query) { + super(repo, ctx, query, (AssetTypeFilter) query.getEntityFilter(), EntityType.ASSET); + } + + @Override + protected String getEntityNameFilter(AssetTypeFilter filter) { + return filter.getAssetNameFilter(); + } + + @Override + protected List getProfileNames(AssetTypeFilter filter) { + return filter.getAssetTypes(); + } + + @Override + protected EntityType getProfileEntityType() { + return EntityType.ASSET_PROFILE; + } + +} diff --git a/common/edqs/src/main/java/org/thingsboard/server/edqs/query/processor/CombinedPermissions.java b/common/edqs/src/main/java/org/thingsboard/server/edqs/query/processor/CombinedPermissions.java new file mode 100644 index 0000000000..3ff6879d46 --- /dev/null +++ b/common/edqs/src/main/java/org/thingsboard/server/edqs/query/processor/CombinedPermissions.java @@ -0,0 +1,25 @@ +/** + * Copyright © 2016-2024 ThingsBoard, Inc. + * + * 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.edqs.query.processor; + +import lombok.Data; + +@Data +public class CombinedPermissions implements Permissions { + private final boolean read; + private final boolean readAttrs; + private final boolean readTs; +} diff --git a/common/edqs/src/main/java/org/thingsboard/server/edqs/query/processor/DeviceSearchQueryProcessor.java b/common/edqs/src/main/java/org/thingsboard/server/edqs/query/processor/DeviceSearchQueryProcessor.java new file mode 100644 index 0000000000..e76f0159bf --- /dev/null +++ b/common/edqs/src/main/java/org/thingsboard/server/edqs/query/processor/DeviceSearchQueryProcessor.java @@ -0,0 +1,59 @@ +/** + * Copyright © 2016-2024 ThingsBoard, Inc. + * + * 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.edqs.query.processor; + +import org.thingsboard.server.common.data.EntityType; +import org.thingsboard.server.common.data.permission.QueryContext; +import org.thingsboard.server.common.data.query.DeviceSearchQueryFilter; +import org.thingsboard.server.common.data.util.CollectionsUtil; +import org.thingsboard.server.edqs.data.EntityData; +import org.thingsboard.server.edqs.data.ProfileAwareData; +import org.thingsboard.server.edqs.data.RelationInfo; +import org.thingsboard.server.edqs.query.EdqsQuery; +import org.thingsboard.server.edqs.repo.TenantRepo; + +import java.util.HashSet; +import java.util.Set; +import java.util.UUID; + +public class DeviceSearchQueryProcessor extends AbstractEntitySearchQueryProcessor { + + private final Set entityProfileIds = new HashSet<>(); + + public DeviceSearchQueryProcessor(TenantRepo repo, QueryContext ctx, EdqsQuery query) { + super(repo, ctx, query, (DeviceSearchQueryFilter) query.getEntityFilter()); + if (CollectionsUtil.isNotEmpty(filter.getDeviceTypes())) { + var profileNamesSet = new HashSet<>(this.filter.getDeviceTypes()); + for (EntityData dp : repo.getEntitySet(EntityType.DEVICE_PROFILE)) { + if (profileNamesSet.contains(dp.getFields().getName())) { + entityProfileIds.add(dp.getId()); + } + } + } + } + + @Override + public EntityType getEntityType() { + return EntityType.DEVICE; + } + + @Override + protected boolean check(RelationInfo relationInfo) { + return super.check(relationInfo) && + (entityProfileIds.isEmpty() || entityProfileIds.contains(((ProfileAwareData) relationInfo.getTarget()).getFields().getProfileId())); + } + +} diff --git a/common/edqs/src/main/java/org/thingsboard/server/edqs/query/processor/DeviceTypeQueryProcessor.java b/common/edqs/src/main/java/org/thingsboard/server/edqs/query/processor/DeviceTypeQueryProcessor.java new file mode 100644 index 0000000000..19068eb160 --- /dev/null +++ b/common/edqs/src/main/java/org/thingsboard/server/edqs/query/processor/DeviceTypeQueryProcessor.java @@ -0,0 +1,47 @@ +/** + * Copyright © 2016-2024 ThingsBoard, Inc. + * + * 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.edqs.query.processor; + +import org.thingsboard.server.common.data.EntityType; +import org.thingsboard.server.common.data.permission.QueryContext; +import org.thingsboard.server.common.data.query.DeviceTypeFilter; +import org.thingsboard.server.edqs.query.EdqsQuery; +import org.thingsboard.server.edqs.repo.TenantRepo; + +import java.util.List; + +public class DeviceTypeQueryProcessor extends AbstractEntityProfileQueryProcessor { + + public DeviceTypeQueryProcessor(TenantRepo repo, QueryContext ctx, EdqsQuery query) { + super(repo, ctx, query, (DeviceTypeFilter) query.getEntityFilter(), EntityType.DEVICE); + } + + @Override + protected String getEntityNameFilter(DeviceTypeFilter filter) { + return filter.getDeviceNameFilter(); + } + + @Override + protected List getProfileNames(DeviceTypeFilter filter) { + return filter.getDeviceTypes(); + } + + @Override + protected EntityType getProfileEntityType() { + return EntityType.DEVICE_PROFILE; + } + +} diff --git a/common/edqs/src/main/java/org/thingsboard/server/edqs/query/processor/EdgeTypeQueryProcessor.java b/common/edqs/src/main/java/org/thingsboard/server/edqs/query/processor/EdgeTypeQueryProcessor.java new file mode 100644 index 0000000000..2c5c7b1dd2 --- /dev/null +++ b/common/edqs/src/main/java/org/thingsboard/server/edqs/query/processor/EdgeTypeQueryProcessor.java @@ -0,0 +1,42 @@ +/** + * Copyright © 2016-2024 ThingsBoard, Inc. + * + * 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.edqs.query.processor; + +import org.thingsboard.server.common.data.EntityType; +import org.thingsboard.server.common.data.permission.QueryContext; +import org.thingsboard.server.common.data.query.EdgeTypeFilter; +import org.thingsboard.server.edqs.query.EdqsQuery; +import org.thingsboard.server.edqs.repo.TenantRepo; + +import java.util.List; + +public class EdgeTypeQueryProcessor extends AbstractEntityProfileNameQueryProcessor { + + public EdgeTypeQueryProcessor(TenantRepo repo, QueryContext ctx, EdqsQuery query) { + super(repo, ctx, query, (EdgeTypeFilter) query.getEntityFilter(), EntityType.EDGE); + } + + @Override + protected String getEntityNameFilter(EdgeTypeFilter filter) { + return filter.getEdgeNameFilter(); + } + + @Override + protected List getProfileNames(EdgeTypeFilter filter) { + return filter.getEdgeTypes(); + } + +} diff --git a/common/edqs/src/main/java/org/thingsboard/server/edqs/query/processor/EdgeTypeSearchQueryProcessor.java b/common/edqs/src/main/java/org/thingsboard/server/edqs/query/processor/EdgeTypeSearchQueryProcessor.java new file mode 100644 index 0000000000..3ea4c247a9 --- /dev/null +++ b/common/edqs/src/main/java/org/thingsboard/server/edqs/query/processor/EdgeTypeSearchQueryProcessor.java @@ -0,0 +1,44 @@ +/** + * Copyright © 2016-2024 ThingsBoard, Inc. + * + * 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.edqs.query.processor; + +import org.thingsboard.server.common.data.EntityType; +import org.thingsboard.server.common.data.permission.QueryContext; +import org.thingsboard.server.common.data.query.EdgeSearchQueryFilter; +import org.thingsboard.server.edqs.data.EntityData; +import org.thingsboard.server.edqs.data.RelationInfo; +import org.thingsboard.server.edqs.query.EdqsQuery; +import org.thingsboard.server.edqs.repo.TenantRepo; + +public class EdgeTypeSearchQueryProcessor extends AbstractEntitySearchQueryProcessor { + + public EdgeTypeSearchQueryProcessor(TenantRepo repo, QueryContext ctx, EdqsQuery query) { + super(repo, ctx, query, (EdgeSearchQueryFilter) query.getEntityFilter()); + } + + @Override + public EntityType getEntityType() { + return EntityType.EDGE; + } + + @Override + protected boolean check(RelationInfo relationInfo) { + EntityData ed = relationInfo.getTarget(); + return super.check(relationInfo) && + (filter.getEdgeTypes() == null || filter.getEdgeTypes().contains(ed.getFields().getType())); + } + +} diff --git a/common/edqs/src/main/java/org/thingsboard/server/edqs/query/processor/EntitiesByGroupNameQueryProcessor.java b/common/edqs/src/main/java/org/thingsboard/server/edqs/query/processor/EntitiesByGroupNameQueryProcessor.java new file mode 100644 index 0000000000..cc3c26dc18 --- /dev/null +++ b/common/edqs/src/main/java/org/thingsboard/server/edqs/query/processor/EntitiesByGroupNameQueryProcessor.java @@ -0,0 +1,121 @@ +/** + * Copyright © 2016-2024 ThingsBoard, Inc. + * + * 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.edqs.query.processor; + +import org.thingsboard.server.common.data.EntityType; +import org.thingsboard.server.common.data.edqs.fields.EntityGroupFields; +import org.thingsboard.server.common.data.permission.QueryContext; +import org.thingsboard.server.common.data.query.EntitiesByGroupNameFilter; +import org.thingsboard.server.edqs.data.CustomerData; +import org.thingsboard.server.edqs.data.EntityData; +import org.thingsboard.server.edqs.query.EdqsQuery; +import org.thingsboard.server.edqs.query.SortableEntityData; +import org.thingsboard.server.edqs.repo.TenantRepo; +import org.thingsboard.server.edqs.util.RepositoryUtils; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.HashSet; +import java.util.List; +import java.util.UUID; +import java.util.function.Consumer; +import java.util.regex.Pattern; + +import static org.thingsboard.server.common.data.EntityType.ENTITY_GROUP; +import static org.thingsboard.server.edqs.util.RepositoryUtils.getSortValue; + +public class EntitiesByGroupNameQueryProcessor extends AbstractSingleEntityTypeQueryProcessor { + + private final String groupType; + private final UUID ownerId; + private final Pattern pattern; + + public EntitiesByGroupNameQueryProcessor(TenantRepo repo, QueryContext ctx, EdqsQuery query) { + super(repo, ctx, query, (EntitiesByGroupNameFilter) query.getEntityFilter()); + this.groupType = filter.getGroupType().name(); + this.ownerId = filter.getOwnerId() != null ? filter.getOwnerId().getId() : null; + this.pattern = RepositoryUtils.toSqlLikePattern(filter.getEntityGroupNameFilter()); + } + + @Override + protected void processCustomerGenericRead(UUID customerId, Consumer> processor) { + var customers = repository.getEntityMap(EntityType.CUSTOMER); + for (UUID cId : repository.getAllCustomers(customerId)) { + var customerData = (CustomerData) customers.get(cId); + if (customerData != null) { + process(customerData.getEntities(ENTITY_GROUP), processor); + } + } + } + + @Override + protected List processCustomerGenericReadWithGroups(UUID customerId, boolean readAttrPermissions, boolean readTsPermissions, List groupPermissions) { + List result = new ArrayList<>(getProbableResultSize()); + var customers = repository.getAllCustomers(customerId); + processAll(ed -> { + CombinedPermissions permissions = getCombinedPermissions(ed.getId(), checkCustomerHierarchy(customers, ed), readAttrPermissions, readTsPermissions, groupPermissions); + if (permissions.isRead()) { + SortableEntityData sortData = new SortableEntityData(ed); + sortData.setSortValue(getSortValue(ed, sortKey)); + sortData.setReadAttrs(permissions.isReadAttrs()); + sortData.setReadTs(permissions.isReadTs()); + result.add(sortData); + } + }); + return result; + } + + @Override + protected void processGroupsOnly(List groupPermissions, Consumer> processor) { + Collection> entities = new HashSet<>(getProbableResultSize()); + for (GroupPermissions groupPermission : groupPermissions) { + entities.add(repository.getEntityGroup(groupPermission.groupId)); + } + process(entities, processor); + } + + @Override + protected void processAll(Consumer> processor) { + process(repository.getEntitySet(ENTITY_GROUP), processor); + } + + @Override + protected void process(Collection> entities, Consumer> processor) { + for (EntityData ed : entities) { + if (matches(ed)) { + Collection> groupEntities = repository.getEntityGroup(ed.getId()).getEntities(); + for (EntityData groupEntity : groupEntities) { + processor.accept(groupEntity); + } + return; + } + } + } + + @Override + protected boolean matches(EntityData ed) { + EntityGroupFields fields = (EntityGroupFields)ed.getFields(); + return super.matches(ed) && groupType.equals(fields.getType()) + && (pattern == null || pattern.matcher(fields.getName()).matches()) + && (ownerId == null || ownerId.equals(fields.getOwnerId())); + } + + @Override + protected int getProbableResultSize() { + return 1024; + } + +} diff --git a/common/edqs/src/main/java/org/thingsboard/server/edqs/query/processor/EntitiesByGroupQueryProcessor.java b/common/edqs/src/main/java/org/thingsboard/server/edqs/query/processor/EntitiesByGroupQueryProcessor.java new file mode 100644 index 0000000000..feb51e5f46 --- /dev/null +++ b/common/edqs/src/main/java/org/thingsboard/server/edqs/query/processor/EntitiesByGroupQueryProcessor.java @@ -0,0 +1,96 @@ +/** + * Copyright © 2016-2024 ThingsBoard, Inc. + * + * 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.edqs.query.processor; + +import org.thingsboard.server.common.data.permission.QueryContext; +import org.thingsboard.server.common.data.query.EntityGroupFilter; +import org.thingsboard.server.edqs.data.EntityData; +import org.thingsboard.server.edqs.data.EntityGroupData; +import org.thingsboard.server.edqs.query.EdqsQuery; +import org.thingsboard.server.edqs.query.SortableEntityData; +import org.thingsboard.server.edqs.repo.TenantRepo; + +import java.util.ArrayList; +import java.util.List; +import java.util.UUID; +import java.util.function.Consumer; + +import static org.thingsboard.server.edqs.util.RepositoryUtils.getSortValue; + +public class EntitiesByGroupQueryProcessor extends AbstractSingleEntityTypeQueryProcessor { + + private final String groupType; + private final UUID groupId; + + public EntitiesByGroupQueryProcessor(TenantRepo repo, QueryContext ctx, EdqsQuery query) { + super(repo, ctx, query, (EntityGroupFilter) query.getEntityFilter()); + groupId = UUID.fromString(filter.getEntityGroup()); + groupType = filter.getGroupType().name(); + } + + @Override + protected void processCustomerGenericRead(UUID customerId, Consumer> processor) { + var customers = repository.getAllCustomers(customerId); + processAll(ed -> { + if (checkCustomerHierarchy(customers, ed)) { + processor.accept(ed); + } + }); + } + + @Override + protected List processCustomerGenericReadWithGroups(UUID customerId, boolean readAttrPermissions, boolean readTsPermissions, List groupPermissions) { + List result = new ArrayList<>(getProbableResultSize()); + var customers = repository.getAllCustomers(customerId); + processAll(ed -> { + CombinedPermissions permissions = getCombinedPermissions(ed.getId(), checkCustomerHierarchy(customers, ed), readAttrPermissions, readTsPermissions, groupPermissions); + if (permissions.isRead()) { + SortableEntityData sortData = new SortableEntityData(ed); + sortData.setSortValue(getSortValue(ed, sortKey)); + sortData.setReadAttrs(permissions.isReadAttrs()); + sortData.setReadTs(permissions.isReadTs()); + result.add(sortData); + } + }); + return result; + } + + @Override + protected void processGroupsOnly(List groupPermissions, Consumer> processor) { + processAll(processor); + } + + @Override + protected void processAll(Consumer> processor) { + EntityGroupData entityGroup = repository.getEntityGroup(groupId); + if (matches(entityGroup)) { + for (EntityData ed : entityGroup.getEntities()) { + processor.accept(ed); + } + } + } + + @Override + protected boolean matches(EntityData ed) { + return super.matches(ed) && groupType.equals(ed.getFields().getType()); + } + + @Override + protected int getProbableResultSize() { + return 1024; + } + +} diff --git a/common/edqs/src/main/java/org/thingsboard/server/edqs/query/processor/EntityGroupListQueryProcessor.java b/common/edqs/src/main/java/org/thingsboard/server/edqs/query/processor/EntityGroupListQueryProcessor.java new file mode 100644 index 0000000000..7022ca5cb6 --- /dev/null +++ b/common/edqs/src/main/java/org/thingsboard/server/edqs/query/processor/EntityGroupListQueryProcessor.java @@ -0,0 +1,84 @@ +/** + * Copyright © 2016-2024 ThingsBoard, Inc. + * + * 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.edqs.query.processor; + +import org.thingsboard.server.common.data.permission.QueryContext; +import org.thingsboard.server.common.data.query.EntityGroupListFilter; +import org.thingsboard.server.edqs.data.EntityData; +import org.thingsboard.server.edqs.data.EntityGroupData; +import org.thingsboard.server.edqs.query.EdqsQuery; +import org.thingsboard.server.edqs.repo.TenantRepo; + +import java.util.List; +import java.util.Set; +import java.util.UUID; +import java.util.function.Consumer; +import java.util.stream.Collectors; + +public class EntityGroupListQueryProcessor extends AbstractEntityGroupQueryProcessor { + + private final String groupType; + private final Set groupIds; + + public EntityGroupListQueryProcessor(TenantRepo repo, QueryContext ctx, EdqsQuery query) { + super(repo, ctx, query, (EntityGroupListFilter) query.getEntityFilter()); + this.groupType = filter.getGroupType().name(); + this.groupIds = filter.getEntityGroupList().stream().map(UUID::fromString).collect(Collectors.toSet()); + } + + @Override + protected void processCustomerGenericRead(UUID customerId, Consumer> processor) { + var customers = repository.getAllCustomers(customerId); + processAll(ed -> { + if (checkCustomerHierarchy(customers, ed)) { + processor.accept(ed); + } + }); + } + + @Override + protected void processGroupsOnly(List groupPermissions, Consumer> processor) { + Set allowedGroupIds = groupPermissions.stream().map(GroupPermissions::getGroupId) + .filter(this.groupIds::contains).collect(Collectors.toSet()); + + checkGroupIds(allowedGroupIds, processor); + } + + @Override + protected void processAll(Consumer> processor) { + checkGroupIds(groupIds, processor); + } + + @Override + protected int getProbableResultSize() { + return groupIds.size(); + } + + @Override + protected boolean matches(EntityData ed) { + return super.matches(ed) && groupType.equals(ed.getFields().getType()); + } + + private void checkGroupIds(Set allowedGroupIds, Consumer> processor) { + for (UUID groupId : allowedGroupIds) { + EntityGroupData entityGroup = repository.getEntityGroup(groupId); + if (matches(entityGroup)) { + processor.accept(entityGroup); + } + } + } + +} diff --git a/common/edqs/src/main/java/org/thingsboard/server/edqs/query/processor/EntityGroupNameQueryProcessor.java b/common/edqs/src/main/java/org/thingsboard/server/edqs/query/processor/EntityGroupNameQueryProcessor.java new file mode 100644 index 0000000000..9491142619 --- /dev/null +++ b/common/edqs/src/main/java/org/thingsboard/server/edqs/query/processor/EntityGroupNameQueryProcessor.java @@ -0,0 +1,83 @@ +/** + * Copyright © 2016-2024 ThingsBoard, Inc. + * + * 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.edqs.query.processor; + +import org.thingsboard.server.common.data.EntityType; +import org.thingsboard.server.common.data.permission.QueryContext; +import org.thingsboard.server.common.data.query.EntityGroupNameFilter; +import org.thingsboard.server.edqs.data.CustomerData; +import org.thingsboard.server.edqs.data.EntityData; +import org.thingsboard.server.edqs.data.EntityGroupData; +import org.thingsboard.server.edqs.query.EdqsQuery; +import org.thingsboard.server.edqs.repo.TenantRepo; +import org.thingsboard.server.edqs.util.RepositoryUtils; + +import java.util.List; +import java.util.UUID; +import java.util.function.Consumer; +import java.util.regex.Pattern; + +import static org.thingsboard.server.common.data.EntityType.ENTITY_GROUP; + +public class EntityGroupNameQueryProcessor extends AbstractEntityGroupQueryProcessor { + + private final String groupType; + private final Pattern groupNamePattern; + + public EntityGroupNameQueryProcessor(TenantRepo repo, QueryContext ctx, EdqsQuery query) { + super(repo, ctx, query, (EntityGroupNameFilter) query.getEntityFilter()); + this.groupType = filter.getGroupType().name(); + this.groupNamePattern = RepositoryUtils.toSqlLikePattern(filter.getEntityGroupNameFilter()); + } + + @Override + protected void processCustomerGenericRead(UUID customerId, Consumer> processor) { + var customers = repository.getEntityMap(EntityType.CUSTOMER); + for (UUID cId : repository.getAllCustomers(customerId)) { + var customerData = (CustomerData) customers.get(cId); + if (customerData != null) { + process(customerData.getEntities(ENTITY_GROUP), processor); + } + } + } + + @Override + protected void processGroupsOnly(List groupPermissions, Consumer> processor) { + for (GroupPermissions groupPermission : groupPermissions) { + EntityGroupData entityGroup = repository.getEntityGroup(groupPermission.groupId); + if (matches(entityGroup)) { + processor.accept(entityGroup); + } + } + } + + @Override + protected void processAll(Consumer> processor) { + process(repository.getEntitySet(ENTITY_GROUP), processor); + } + + @Override + protected boolean matches(EntityData ed) { + return super.matches(ed) && (groupNamePattern == null || groupNamePattern.matcher(ed.getFields().getName()).matches()) + && groupType.equals(ed.getFields().getType()); + } + + @Override + protected int getProbableResultSize() { + return 1024; + } + +} diff --git a/common/edqs/src/main/java/org/thingsboard/server/edqs/query/processor/EntityListQueryProcessor.java b/common/edqs/src/main/java/org/thingsboard/server/edqs/query/processor/EntityListQueryProcessor.java new file mode 100644 index 0000000000..495f925bd0 --- /dev/null +++ b/common/edqs/src/main/java/org/thingsboard/server/edqs/query/processor/EntityListQueryProcessor.java @@ -0,0 +1,94 @@ +/** + * Copyright © 2016-2024 ThingsBoard, Inc. + * + * 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.edqs.query.processor; + +import org.thingsboard.server.common.data.EntityType; +import org.thingsboard.server.common.data.permission.QueryContext; +import org.thingsboard.server.common.data.query.EntityListFilter; +import org.thingsboard.server.edqs.data.EntityData; +import org.thingsboard.server.edqs.query.EdqsQuery; +import org.thingsboard.server.edqs.query.SortableEntityData; +import org.thingsboard.server.edqs.repo.TenantRepo; + +import java.util.ArrayList; +import java.util.List; +import java.util.Set; +import java.util.UUID; +import java.util.function.Consumer; +import java.util.stream.Collectors; + +import static org.thingsboard.server.edqs.util.RepositoryUtils.getSortValue; + +public class EntityListQueryProcessor extends AbstractSingleEntityTypeQueryProcessor { + + private final EntityType entityType; + private final Set entityIds; + + public EntityListQueryProcessor(TenantRepo repo, QueryContext ctx, EdqsQuery query) { + super(repo, ctx, query, (EntityListFilter) query.getEntityFilter()); + this.entityType = filter.getEntityType(); + this.entityIds = filter.getEntityList().stream().map(UUID::fromString).collect(Collectors.toSet()); + } + + @Override + protected void processCustomerGenericRead(UUID customerId, Consumer> processor) { + var customers = repository.getAllCustomers(customerId); + processAll(ed -> { + if (checkCustomerHierarchy(customers, ed)) { + processor.accept(ed); + } + }); + } + + @Override + protected List processCustomerGenericReadWithGroups(UUID customerId, boolean readAttrPermissions, boolean readTsPermissions, List groupPermissions) { + List result = new ArrayList<>(getProbableResultSize()); + var customers = repository.getAllCustomers(customerId); + processAll(ed -> { + CombinedPermissions permissions = getCombinedPermissions(ed.getId(), checkCustomerHierarchy(customers, ed), readAttrPermissions, readTsPermissions, groupPermissions); + if (permissions.isRead()) { + SortableEntityData sortData = new SortableEntityData(ed); + sortData.setSortValue(getSortValue(ed, sortKey)); + sortData.setReadAttrs(permissions.isReadAttrs()); + sortData.setReadTs(permissions.isReadTs()); + result.add(sortData); + } + }); + return result; + } + + @Override + protected void processGroupsOnly(List groupPermissions, Consumer> processor) { + processAll(processor); + } + + @Override + protected void processAll(Consumer> processor) { + var map = repository.getEntityMap(entityType); + for (UUID entityId : entityIds) { + EntityData ed = map.get(entityId); + if (matches(ed)) { + processor.accept(ed); + } + } + } + + @Override + protected int getProbableResultSize() { + return entityIds.size(); + } + +} diff --git a/common/edqs/src/main/java/org/thingsboard/server/edqs/query/processor/EntityNameQueryProcessor.java b/common/edqs/src/main/java/org/thingsboard/server/edqs/query/processor/EntityNameQueryProcessor.java new file mode 100644 index 0000000000..3b64706bfe --- /dev/null +++ b/common/edqs/src/main/java/org/thingsboard/server/edqs/query/processor/EntityNameQueryProcessor.java @@ -0,0 +1,41 @@ +/** + * Copyright © 2016-2024 ThingsBoard, Inc. + * + * 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.edqs.query.processor; + +import org.thingsboard.server.common.data.permission.QueryContext; +import org.thingsboard.server.common.data.query.EntityNameFilter; +import org.thingsboard.server.edqs.data.EntityData; +import org.thingsboard.server.edqs.query.EdqsQuery; +import org.thingsboard.server.edqs.repo.TenantRepo; +import org.thingsboard.server.edqs.util.RepositoryUtils; + +import java.util.regex.Pattern; + +public class EntityNameQueryProcessor extends AbstractSimpleQueryProcessor { + + private final Pattern pattern; + + public EntityNameQueryProcessor(TenantRepo repo, QueryContext ctx, EdqsQuery query) { + super(repo, ctx, query, (EntityNameFilter) query.getEntityFilter(), ((EntityNameFilter) query.getEntityFilter()).getEntityType()); + pattern = RepositoryUtils.toSqlLikePattern(filter.getEntityNameFilter()); + } + + @Override + protected boolean matches(EntityData ed) { + return ed.getFields() != null && (pattern == null || pattern.matcher(ed.getFields().getName()).matches()); + } + +} diff --git a/common/edqs/src/main/java/org/thingsboard/server/edqs/query/processor/EntityQueryProcessor.java b/common/edqs/src/main/java/org/thingsboard/server/edqs/query/processor/EntityQueryProcessor.java new file mode 100644 index 0000000000..a42c79b61f --- /dev/null +++ b/common/edqs/src/main/java/org/thingsboard/server/edqs/query/processor/EntityQueryProcessor.java @@ -0,0 +1,28 @@ +/** + * Copyright © 2016-2024 ThingsBoard, Inc. + * + * 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.edqs.query.processor; + +import org.thingsboard.server.edqs.query.SortableEntityData; + +import java.util.List; + +public interface EntityQueryProcessor { + + List processQuery(); + + long count(); + +} diff --git a/common/edqs/src/main/java/org/thingsboard/server/edqs/query/processor/EntityQueryProcessorFactory.java b/common/edqs/src/main/java/org/thingsboard/server/edqs/query/processor/EntityQueryProcessorFactory.java new file mode 100644 index 0000000000..271ff01425 --- /dev/null +++ b/common/edqs/src/main/java/org/thingsboard/server/edqs/query/processor/EntityQueryProcessorFactory.java @@ -0,0 +1,50 @@ +/** + * Copyright © 2016-2024 ThingsBoard, Inc. + * + * 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.edqs.query.processor; + +import org.thingsboard.server.common.data.permission.QueryContext; +import org.thingsboard.server.edqs.query.EdqsQuery; +import org.thingsboard.server.edqs.repo.TenantRepo; + +public class EntityQueryProcessorFactory { + + public static EntityQueryProcessor create(TenantRepo repo, QueryContext ctx, EdqsQuery query) { + return switch (query.getEntityFilter().getType()) { + case SINGLE_ENTITY -> new SingleEntityQueryProcessor(repo, ctx, query); + case ENTITY_LIST -> new EntityListQueryProcessor(repo, ctx, query); + case ENTITY_NAME -> new EntityNameQueryProcessor(repo, ctx, query); + case ENTITY_TYPE -> new EntityTypeQueryProcessor(repo, ctx, query); + case DEVICE_TYPE -> new DeviceTypeQueryProcessor(repo, ctx, query); + case ASSET_TYPE -> new AssetTypeQueryProcessor(repo, ctx, query); + case ENTITY_VIEW_TYPE -> new EntityViewTypeQueryProcessor(repo, ctx, query); + case EDGE_TYPE -> new EdgeTypeQueryProcessor(repo, ctx, query); + case RELATIONS_QUERY -> new RelationQueryProcessor(repo, ctx, query); + case ENTITY_GROUP -> new EntitiesByGroupQueryProcessor(repo, ctx, query); + case ENTITY_GROUP_LIST -> new EntityGroupListQueryProcessor(repo, ctx, query); + case ENTITY_GROUP_NAME -> new EntityGroupNameQueryProcessor(repo, ctx, query); + case ENTITIES_BY_GROUP_NAME -> new EntitiesByGroupNameQueryProcessor(repo, ctx, query); + case STATE_ENTITY_OWNER -> new StateEntityOwnerQueryProcessor(repo, ctx, query); + case API_USAGE_STATE -> new ApiUsageStateQueryProcessor(repo, ctx, query); + case ASSET_SEARCH_QUERY -> new AssetSearchQueryProcessor(repo, ctx, query); + case DEVICE_SEARCH_QUERY -> new DeviceSearchQueryProcessor(repo, ctx, query); + case ENTITY_VIEW_SEARCH_QUERY -> new EntityViewSearchQueryProcessor(repo, ctx, query); + case EDGE_SEARCH_QUERY -> new EdgeTypeSearchQueryProcessor(repo, ctx, query); + case SCHEDULER_EVENT -> new SchedulerEventQueryProcessor(repo, ctx, query); + default -> throw new RuntimeException("Not Implemented!"); + }; + } + +} diff --git a/common/edqs/src/main/java/org/thingsboard/server/edqs/query/processor/EntityTypeQueryProcessor.java b/common/edqs/src/main/java/org/thingsboard/server/edqs/query/processor/EntityTypeQueryProcessor.java new file mode 100644 index 0000000000..b390a3c80c --- /dev/null +++ b/common/edqs/src/main/java/org/thingsboard/server/edqs/query/processor/EntityTypeQueryProcessor.java @@ -0,0 +1,35 @@ +/** + * Copyright © 2016-2024 ThingsBoard, Inc. + * + * 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.edqs.query.processor; + +import org.thingsboard.server.common.data.permission.QueryContext; +import org.thingsboard.server.common.data.query.EntityTypeFilter; +import org.thingsboard.server.edqs.data.EntityData; +import org.thingsboard.server.edqs.query.EdqsQuery; +import org.thingsboard.server.edqs.repo.TenantRepo; + +public class EntityTypeQueryProcessor extends AbstractSimpleQueryProcessor { + + public EntityTypeQueryProcessor(TenantRepo repo, QueryContext ctx, EdqsQuery query) { + super(repo, ctx, query, (EntityTypeFilter) query.getEntityFilter(), ((EntityTypeFilter) query.getEntityFilter()).getEntityType()); + } + + @Override + protected boolean matches(EntityData ed) { + return super.matches(ed); + } + +} diff --git a/common/edqs/src/main/java/org/thingsboard/server/edqs/query/processor/EntityViewSearchQueryProcessor.java b/common/edqs/src/main/java/org/thingsboard/server/edqs/query/processor/EntityViewSearchQueryProcessor.java new file mode 100644 index 0000000000..f16de48b6a --- /dev/null +++ b/common/edqs/src/main/java/org/thingsboard/server/edqs/query/processor/EntityViewSearchQueryProcessor.java @@ -0,0 +1,44 @@ +/** + * Copyright © 2016-2024 ThingsBoard, Inc. + * + * 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.edqs.query.processor; + +import org.thingsboard.server.common.data.EntityType; +import org.thingsboard.server.common.data.permission.QueryContext; +import org.thingsboard.server.common.data.query.EntityViewSearchQueryFilter; +import org.thingsboard.server.edqs.data.EntityData; +import org.thingsboard.server.edqs.data.RelationInfo; +import org.thingsboard.server.edqs.query.EdqsQuery; +import org.thingsboard.server.edqs.repo.TenantRepo; + +public class EntityViewSearchQueryProcessor extends AbstractEntitySearchQueryProcessor { + + public EntityViewSearchQueryProcessor(TenantRepo repo, QueryContext ctx, EdqsQuery query) { + super(repo, ctx, query, (EntityViewSearchQueryFilter) query.getEntityFilter()); + } + + @Override + public EntityType getEntityType() { + return EntityType.ENTITY_VIEW; + } + + @Override + protected boolean check(RelationInfo relationInfo) { + EntityData ed = relationInfo.getTarget(); + return super.check(relationInfo) && + (filter.getEntityViewTypes() == null || filter.getEntityViewTypes().contains(ed.getFields().getType())); + } + +} diff --git a/common/edqs/src/main/java/org/thingsboard/server/edqs/query/processor/EntityViewTypeQueryProcessor.java b/common/edqs/src/main/java/org/thingsboard/server/edqs/query/processor/EntityViewTypeQueryProcessor.java new file mode 100644 index 0000000000..3932cd15fd --- /dev/null +++ b/common/edqs/src/main/java/org/thingsboard/server/edqs/query/processor/EntityViewTypeQueryProcessor.java @@ -0,0 +1,42 @@ +/** + * Copyright © 2016-2024 ThingsBoard, Inc. + * + * 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.edqs.query.processor; + +import org.thingsboard.server.common.data.EntityType; +import org.thingsboard.server.common.data.permission.QueryContext; +import org.thingsboard.server.common.data.query.EntityViewTypeFilter; +import org.thingsboard.server.edqs.query.EdqsQuery; +import org.thingsboard.server.edqs.repo.TenantRepo; + +import java.util.List; + +public class EntityViewTypeQueryProcessor extends AbstractEntityProfileNameQueryProcessor { + + public EntityViewTypeQueryProcessor(TenantRepo repo, QueryContext ctx, EdqsQuery query) { + super(repo, ctx, query, (EntityViewTypeFilter) query.getEntityFilter(), EntityType.ENTITY_VIEW); + } + + @Override + protected String getEntityNameFilter(EntityViewTypeFilter filter) { + return filter.getEntityViewNameFilter(); + } + + @Override + protected List getProfileNames(EntityViewTypeFilter filter) { + return filter.getEntityViewTypes(); + } + +} diff --git a/common/edqs/src/main/java/org/thingsboard/server/edqs/query/processor/GroupPermissions.java b/common/edqs/src/main/java/org/thingsboard/server/edqs/query/processor/GroupPermissions.java new file mode 100644 index 0000000000..65a5a7d9b0 --- /dev/null +++ b/common/edqs/src/main/java/org/thingsboard/server/edqs/query/processor/GroupPermissions.java @@ -0,0 +1,29 @@ +/** + * Copyright © 2016-2024 ThingsBoard, Inc. + * + * 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.edqs.query.processor; + +import lombok.Data; + +import java.util.UUID; + +@Data +public class GroupPermissions implements Permissions { + + protected final UUID groupId; + protected final boolean readAttrs; + protected final boolean readTs; + +} diff --git a/common/edqs/src/main/java/org/thingsboard/server/edqs/query/processor/Permissions.java b/common/edqs/src/main/java/org/thingsboard/server/edqs/query/processor/Permissions.java new file mode 100644 index 0000000000..e2231a8df2 --- /dev/null +++ b/common/edqs/src/main/java/org/thingsboard/server/edqs/query/processor/Permissions.java @@ -0,0 +1,24 @@ +/** + * Copyright © 2016-2024 ThingsBoard, Inc. + * + * 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.edqs.query.processor; + +public interface Permissions { + + boolean isReadAttrs(); + + boolean isReadTs(); + +} diff --git a/common/edqs/src/main/java/org/thingsboard/server/edqs/query/processor/RelationQueryPermissions.java b/common/edqs/src/main/java/org/thingsboard/server/edqs/query/processor/RelationQueryPermissions.java new file mode 100644 index 0000000000..fb151d85af --- /dev/null +++ b/common/edqs/src/main/java/org/thingsboard/server/edqs/query/processor/RelationQueryPermissions.java @@ -0,0 +1,33 @@ +/** + * Copyright © 2016-2024 ThingsBoard, Inc. + * + * 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.edqs.query.processor; + +import lombok.Builder; +import lombok.Getter; + +import java.util.List; + +@Getter +@Builder +public class RelationQueryPermissions implements Permissions { + + private final boolean readEntity; + private final boolean readAttrs; + private final boolean readTs; + private final boolean hasGroups; + private final List groupPermissions; + +} diff --git a/common/edqs/src/main/java/org/thingsboard/server/edqs/query/processor/RelationQueryProcessor.java b/common/edqs/src/main/java/org/thingsboard/server/edqs/query/processor/RelationQueryProcessor.java new file mode 100644 index 0000000000..8f70344239 --- /dev/null +++ b/common/edqs/src/main/java/org/thingsboard/server/edqs/query/processor/RelationQueryProcessor.java @@ -0,0 +1,84 @@ +/** + * Copyright © 2016-2024 ThingsBoard, Inc. + * + * 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.edqs.query.processor; + +import org.thingsboard.server.common.data.permission.QueryContext; +import org.thingsboard.server.common.data.query.RelationsQueryFilter; +import org.thingsboard.server.common.data.relation.EntitySearchDirection; +import org.thingsboard.server.edqs.data.RelationInfo; +import org.thingsboard.server.edqs.query.EdqsQuery; +import org.thingsboard.server.edqs.repo.TenantRepo; + +import java.util.Set; +import java.util.UUID; +import java.util.stream.Collectors; + +public class RelationQueryProcessor extends AbstractRelationQueryProcessor { + + private final boolean hasFilters; + + public RelationQueryProcessor(TenantRepo repo, QueryContext ctx, EdqsQuery query) { + super(repo, ctx, query, (RelationsQueryFilter) query.getEntityFilter()); + this.hasFilters = filter.getFilters() != null && !filter.getFilters().isEmpty(); + } + + @Override + public Set getRootEntities() { + if (filter.isMultiRoot()) { + return filter.getMultiRootEntityIds().stream().map(UUID::fromString).collect(Collectors.toSet()); + } else { + return Set.of(filter.getRootEntity().getId()); + } + } + + @Override + public EntitySearchDirection getDirection() { + return filter.getDirection(); + } + + @Override + public int getMaxLevel() { + return filter.getMaxLevel(); + } + + @Override + public boolean isMultiRoot() { + return filter.isMultiRoot(); + } + + @Override + public boolean isFetchLastLevelOnly() { + return filter.isFetchLastLevelOnly(); + } + + @Override + protected boolean check(RelationInfo relationInfo) { + if (hasFilters) { + for (var f : filter.getFilters()) { + if (((!filter.isNegate() && !f.isNegate()) || (filter.isNegate() && f.isNegate())) == f.getRelationType().equals(relationInfo.getType())) { + if (f.getEntityTypes() == null || f.getEntityTypes().isEmpty() + || f.getEntityTypes().contains(relationInfo.getTarget().getEntityType())) { + return super.matches(relationInfo.getTarget()); + } + } + } + return false; + } else { + return super.matches(relationInfo.getTarget()); + } + } + +} diff --git a/common/edqs/src/main/java/org/thingsboard/server/edqs/query/processor/SchedulerEventQueryProcessor.java b/common/edqs/src/main/java/org/thingsboard/server/edqs/query/processor/SchedulerEventQueryProcessor.java new file mode 100644 index 0000000000..3c48b89cb5 --- /dev/null +++ b/common/edqs/src/main/java/org/thingsboard/server/edqs/query/processor/SchedulerEventQueryProcessor.java @@ -0,0 +1,37 @@ +/** + * Copyright © 2016-2024 ThingsBoard, Inc. + * + * 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.edqs.query.processor; + +import org.thingsboard.server.common.data.EntityType; +import org.thingsboard.server.common.data.permission.QueryContext; +import org.thingsboard.server.common.data.query.SchedulerEventFilter; +import org.thingsboard.server.edqs.data.EntityData; +import org.thingsboard.server.edqs.query.EdqsQuery; +import org.thingsboard.server.edqs.repo.TenantRepo; + +public class SchedulerEventQueryProcessor extends AbstractSimpleQueryProcessor { + + public SchedulerEventQueryProcessor(TenantRepo repo, QueryContext ctx, EdqsQuery query) { + super(repo, ctx, query, (SchedulerEventFilter) query.getEntityFilter(), EntityType.SCHEDULER_EVENT); + } + + @Override + protected boolean matches(EntityData ed) { + return super.matches(ed) && (filter.getEventType() == null || filter.getEventType().equals(ed.getFields().getType())) + && (filter.getOriginator() == null || filter.getOriginator().equals(ed.getFields().getOriginatorId())); + } + +} diff --git a/common/edqs/src/main/java/org/thingsboard/server/edqs/query/processor/SingleEntityQueryProcessor.java b/common/edqs/src/main/java/org/thingsboard/server/edqs/query/processor/SingleEntityQueryProcessor.java new file mode 100644 index 0000000000..ee3810ca6b --- /dev/null +++ b/common/edqs/src/main/java/org/thingsboard/server/edqs/query/processor/SingleEntityQueryProcessor.java @@ -0,0 +1,87 @@ +/** + * Copyright © 2016-2024 ThingsBoard, Inc. + * + * 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.edqs.query.processor; + +import org.thingsboard.server.common.data.EntityType; +import org.thingsboard.server.common.data.permission.QueryContext; +import org.thingsboard.server.common.data.query.SingleEntityFilter; +import org.thingsboard.server.edqs.data.EntityData; +import org.thingsboard.server.edqs.query.EdqsQuery; +import org.thingsboard.server.edqs.query.SortableEntityData; +import org.thingsboard.server.edqs.repo.TenantRepo; + +import java.util.Collections; +import java.util.List; +import java.util.UUID; +import java.util.function.Consumer; + +public class SingleEntityQueryProcessor extends AbstractSingleEntityTypeQueryProcessor { + + private final EntityType entityType; + private final UUID entityId; + + public SingleEntityQueryProcessor(TenantRepo repo, QueryContext ctx, EdqsQuery query) { + super(repo, ctx, query, (SingleEntityFilter) query.getEntityFilter()); + this.entityType = filter.getSingleEntity().getEntityType(); + this.entityId = filter.getSingleEntity().getId(); + } + + @Override + protected void processCustomerGenericRead(UUID customerId, Consumer> processor) { + EntityData ed = repository.getEntityMap(entityType).get(entityId); + if (ed != null && ed.getCustomerId() != null && matches(ed)) { + if (customerId.equals(ed.getCustomerId()) || repository.getAllCustomers(customerId).contains(ed.getCustomerId())) { + processor.accept(ed); + } + } + } + + @Override + protected List processCustomerGenericReadWithGroups(UUID customerId, boolean readAttrPermissions, boolean readTsPermissions, List groupPermissions) { + EntityData ed = repository.getEntityMap(entityType).get(entityId); + if (!matches(ed)) { + return Collections.emptyList(); + } else { + boolean genericRead = customerId.equals(ed.getCustomerId()) || repository.getAllCustomers(customerId).contains(ed.getCustomerId()); + CombinedPermissions permissions = getCombinedPermissions(ed.getId(), genericRead, readAttrPermissions, readTsPermissions, groupPermissions); + if (permissions.isRead()) { + SortableEntityData sortData = toSortData(ed, permissions); + return Collections.singletonList(sortData); + } else { + return Collections.emptyList(); + } + } + } + + @Override + protected void processGroupsOnly(List groupPermissions, Consumer> processor) { + processAll(processor); + } + + @Override + protected void processAll(Consumer> processor) { + EntityData ed = repository.getEntityMap(entityType).get(entityId); + if (matches(ed)) { + processor.accept(ed); + } + } + + @Override + protected int getProbableResultSize() { + return 1; + } + +} diff --git a/common/edqs/src/main/java/org/thingsboard/server/edqs/query/processor/StateEntityOwnerQueryProcessor.java b/common/edqs/src/main/java/org/thingsboard/server/edqs/query/processor/StateEntityOwnerQueryProcessor.java new file mode 100644 index 0000000000..0e79f21747 --- /dev/null +++ b/common/edqs/src/main/java/org/thingsboard/server/edqs/query/processor/StateEntityOwnerQueryProcessor.java @@ -0,0 +1,91 @@ +/** + * Copyright © 2016-2024 ThingsBoard, Inc. + * + * 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.edqs.query.processor; + +import org.thingsboard.server.common.data.EntityType; +import org.thingsboard.server.common.data.id.EntityId; +import org.thingsboard.server.common.data.permission.QueryContext; +import org.thingsboard.server.common.data.query.StateEntityOwnerFilter; +import org.thingsboard.server.edqs.data.EntityData; +import org.thingsboard.server.edqs.query.EdqsQuery; +import org.thingsboard.server.edqs.query.SortableEntityData; +import org.thingsboard.server.edqs.repo.TenantRepo; + +import java.util.Collections; +import java.util.List; +import java.util.UUID; +import java.util.function.Consumer; + +public class StateEntityOwnerQueryProcessor extends AbstractSingleEntityTypeQueryProcessor { + + private final EntityId entityId; + + public StateEntityOwnerQueryProcessor(TenantRepo repo, QueryContext ctx, EdqsQuery query) { + super(repo, ctx, query, (StateEntityOwnerFilter) query.getEntityFilter()); + this.entityId = filter.getSingleEntity(); + } + + @Override + protected void processCustomerGenericRead(UUID customerId, Consumer> processor) { + EntityData ed = repository.getEntityMap(entityId.getEntityType()).get(entityId.getId()); + if (ed != null && ed.getCustomerId() != null && matches(ed)) { + if (customerId.equals(ed.getCustomerId()) || repository.getAllCustomers(customerId).contains(ed.getCustomerId())) { + processor.accept(ed); + } + } + } + + + @Override + protected List processCustomerGenericReadWithGroups(UUID customerId, boolean readAttrPermissions, boolean readTsPermissions, List groupPermissions) { + EntityData ed = repository.getEntityMap(entityId.getEntityType()).get(entityId.getId()); + if (!matches(ed)) { + return Collections.emptyList(); + } else { + boolean genericRead = customerId.equals(ed.getCustomerId()) || repository.getAllCustomers(customerId).contains(ed.getCustomerId()); + CombinedPermissions permissions = getCombinedPermissions(ed.getId(), genericRead, readAttrPermissions, readTsPermissions, groupPermissions); + if (permissions.isRead()) { + SortableEntityData sortData = toSortData(ed, permissions); + return Collections.singletonList(sortData); + } else { + return Collections.emptyList(); + } + } + } + + @Override + protected void processGroupsOnly(List groupPermissions, Consumer> processor) { + processAll(processor); + } + + @Override + protected void processAll(Consumer> processor) { + EntityData ed = repository.getEntityMap(entityId.getEntityType()).get(entityId.getId()); + if (ed != null) { + if (ed.getCustomerId() != null) { + processor.accept(repository.getEntityMap(EntityType.CUSTOMER).get(ed.getCustomerId())); + } else { + processor.accept(repository.getEntityMap(EntityType.TENANT).get(repository.getTenantId().getId())); + } + } + } + + @Override + protected int getProbableResultSize() { + return 1; + } + +} diff --git a/common/edqs/src/main/java/org/thingsboard/server/edqs/repo/EdqRepository.java b/common/edqs/src/main/java/org/thingsboard/server/edqs/repo/EdqRepository.java new file mode 100644 index 0000000000..9e829dfac1 --- /dev/null +++ b/common/edqs/src/main/java/org/thingsboard/server/edqs/repo/EdqRepository.java @@ -0,0 +1,44 @@ +/** + * Copyright © 2016-2024 ThingsBoard, Inc. + * + * 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.edqs.repo; + +import org.thingsboard.server.common.data.edqs.EdqsEvent; +import org.thingsboard.server.common.data.edqs.query.QueryResult; +import org.thingsboard.server.common.data.id.CustomerId; +import org.thingsboard.server.common.data.id.TenantId; +import org.thingsboard.server.common.data.page.PageData; +import org.thingsboard.server.common.data.permission.MergedUserPermissions; +import org.thingsboard.server.common.data.query.EntityCountQuery; +import org.thingsboard.server.common.data.query.EntityDataQuery; + +import java.util.function.Predicate; + +public interface EdqRepository { + + void processEvent(EdqsEvent event); + + @Deprecated + default void addOrUpdate(TenantId tenantId, Object object) {} + + long countEntitiesByQuery(TenantId tenantId, CustomerId customerId, MergedUserPermissions userPermissions, EntityCountQuery query, boolean ignorePermissionCheck); + + PageData findEntityDataByQuery(TenantId tenantId, CustomerId customerId, MergedUserPermissions userPermissions, EntityDataQuery query, boolean ignorePermissionCheck); + + void clearIf(Predicate predicate); + + void clear(); + +} diff --git a/common/edqs/src/main/java/org/thingsboard/server/edqs/repo/InMemoryEdqRepository.java b/common/edqs/src/main/java/org/thingsboard/server/edqs/repo/InMemoryEdqRepository.java new file mode 100644 index 0000000000..2934fdbfba --- /dev/null +++ b/common/edqs/src/main/java/org/thingsboard/server/edqs/repo/InMemoryEdqRepository.java @@ -0,0 +1,91 @@ +/** + * Copyright © 2016-2024 ThingsBoard, Inc. + * + * 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.edqs.repo; + +import lombok.AllArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Service; +import org.thingsboard.server.common.data.edqs.EdqsEvent; +import org.thingsboard.server.common.data.edqs.query.QueryResult; +import org.thingsboard.server.common.data.id.CustomerId; +import org.thingsboard.server.common.data.id.TenantId; +import org.thingsboard.server.common.data.page.PageData; +import org.thingsboard.server.common.data.permission.MergedUserPermissions; +import org.thingsboard.server.common.data.query.EntityCountQuery; +import org.thingsboard.server.common.data.query.EntityDataQuery; +import org.thingsboard.server.edqs.stats.EdqsStatsService; +import org.thingsboard.server.queue.edqs.EdqsComponent; + +import java.util.Optional; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.ConcurrentMap; +import java.util.function.Predicate; + +@EdqsComponent +@AllArgsConstructor +@Service +@Slf4j +public class InMemoryEdqRepository implements EdqRepository { + + private final static ConcurrentMap repos = new ConcurrentHashMap<>(); + private final Optional statsService; + + public TenantRepo get(TenantId tenantId) { + return repos.computeIfAbsent(tenantId, id -> new TenantRepo(id, statsService)); + } + + @Override + public void processEvent(EdqsEvent event) { + get(event.getTenantId()).processEvent(event); + } + + @Override + public long countEntitiesByQuery(TenantId tenantId, CustomerId customerId, MergedUserPermissions userPermissions, EntityCountQuery query, boolean ignorePermissionCheck) { + long startNs = System.nanoTime(); + long result = 0; + if (TenantId.SYS_TENANT_ID.equals(tenantId)) { + for (TenantRepo repo : repos.values()) { + result += repo.countEntitiesByQuery(customerId, userPermissions, query, ignorePermissionCheck); + } + } else { + result = get(tenantId).countEntitiesByQuery(customerId, userPermissions, query, ignorePermissionCheck); + } + double timingMs = (double) (System.nanoTime() - startNs) / 1000_000; + log.info("countEntitiesByQuery: {} ms", timingMs); + return result; + } + + @Override + public PageData findEntityDataByQuery(TenantId tenantId, CustomerId customerId, + MergedUserPermissions userPermissions, EntityDataQuery query, boolean ignorePermissionCheck) { + long startNs = System.nanoTime(); + var result = get(tenantId).findEntityDataByQuery(customerId, userPermissions, query, ignorePermissionCheck); + double timingMs = (double) (System.nanoTime() - startNs) / 1000_000; + log.info("findEntityDataByQuery: {} ms", timingMs); + return result; + } + + @Override + public void clearIf(Predicate predicate) { + repos.keySet().removeIf(predicate); + } + + @Override + public void clear() { + repos.clear(); + } + +} diff --git a/common/edqs/src/main/java/org/thingsboard/server/edqs/repo/KeyDictionary.java b/common/edqs/src/main/java/org/thingsboard/server/edqs/repo/KeyDictionary.java new file mode 100644 index 0000000000..f2dcd41e7c --- /dev/null +++ b/common/edqs/src/main/java/org/thingsboard/server/edqs/repo/KeyDictionary.java @@ -0,0 +1,40 @@ +/** + * Copyright © 2016-2024 ThingsBoard, Inc. + * + * 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.edqs.repo; + +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.ConcurrentMap; +import java.util.concurrent.atomic.AtomicInteger; + +public class KeyDictionary { + + private static final ConcurrentMap keyToIdDict = new ConcurrentHashMap<>(); + private static final ConcurrentMap idToKeyDict = new ConcurrentHashMap<>(); + private static final AtomicInteger keySeq = new AtomicInteger(); + + public static Integer get(String key) { + return keyToIdDict.computeIfAbsent(key, __ -> { + int keyId = keySeq.incrementAndGet(); + idToKeyDict.put(keyId, key); + return keyId; + }); + } + + public static String get(Integer keyId) { + return idToKeyDict.get(keyId); + } + +} diff --git a/common/edqs/src/main/java/org/thingsboard/server/edqs/repo/TbBytePool.java b/common/edqs/src/main/java/org/thingsboard/server/edqs/repo/TbBytePool.java new file mode 100644 index 0000000000..876d92ee82 --- /dev/null +++ b/common/edqs/src/main/java/org/thingsboard/server/edqs/repo/TbBytePool.java @@ -0,0 +1,39 @@ +/** + * Copyright © 2016-2024 ThingsBoard, Inc. + * + * 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.edqs.repo; + +import com.google.common.hash.Hashing; +import org.springframework.util.ConcurrentReferenceHashMap; + +import java.util.concurrent.ConcurrentMap; + +public class TbBytePool { + + private static final ConcurrentMap pool = new ConcurrentReferenceHashMap<>(); + + public static byte[] intern(byte[] data) { + if (data == null) { + return null; + } + var checksum = Hashing.sha512().hashBytes(data).toString(); + return pool.computeIfAbsent(checksum, c -> data); + } + + public static int size(){ + return pool.size(); + } + +} diff --git a/common/edqs/src/main/java/org/thingsboard/server/edqs/repo/TbStringPool.java b/common/edqs/src/main/java/org/thingsboard/server/edqs/repo/TbStringPool.java new file mode 100644 index 0000000000..177651248b --- /dev/null +++ b/common/edqs/src/main/java/org/thingsboard/server/edqs/repo/TbStringPool.java @@ -0,0 +1,37 @@ +/** + * Copyright © 2016-2024 ThingsBoard, Inc. + * + * 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.edqs.repo; + +import org.springframework.util.ConcurrentReferenceHashMap; + +import java.util.concurrent.ConcurrentMap; + +public class TbStringPool { + + private static final ConcurrentMap pool = new ConcurrentReferenceHashMap<>(); + + public static String intern(String data) { + if (data == null) { + return null; + } + return pool.computeIfAbsent(data, str -> str); + } + + public static int size(){ + return pool.size(); + } + +} diff --git a/common/edqs/src/main/java/org/thingsboard/server/edqs/repo/TenantRepo.java b/common/edqs/src/main/java/org/thingsboard/server/edqs/repo/TenantRepo.java new file mode 100644 index 0000000000..93422eb5fd --- /dev/null +++ b/common/edqs/src/main/java/org/thingsboard/server/edqs/repo/TenantRepo.java @@ -0,0 +1,584 @@ +/** + * Copyright © 2016-2024 ThingsBoard, Inc. + * + * 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.edqs.repo; + +import lombok.extern.slf4j.Slf4j; +import org.thingsboard.server.common.data.EntityType; +import org.thingsboard.server.common.data.ObjectType; +import org.thingsboard.server.common.data.edqs.AttributeKv; +import org.thingsboard.server.common.data.edqs.EdqsEvent; +import org.thingsboard.server.common.data.edqs.EdqsEventType; +import org.thingsboard.server.common.data.edqs.EdqsObject; +import org.thingsboard.server.common.data.edqs.Entity; +import org.thingsboard.server.common.data.edqs.LatestTsKv; +import org.thingsboard.server.common.data.edqs.fields.AssetFields; +import org.thingsboard.server.common.data.edqs.fields.CustomerFields; +import org.thingsboard.server.common.data.edqs.fields.EntityFields; +import org.thingsboard.server.common.data.edqs.fields.EntityGroupFields; +import org.thingsboard.server.common.data.edqs.query.QueryResult; +import org.thingsboard.server.common.data.id.CustomerId; +import org.thingsboard.server.common.data.id.EntityId; +import org.thingsboard.server.common.data.id.TenantId; +import org.thingsboard.server.common.data.kv.KvEntry; +import org.thingsboard.server.common.data.page.PageData; +import org.thingsboard.server.common.data.permission.MergedUserPermissions; +import org.thingsboard.server.common.data.permission.QueryContext; +import org.thingsboard.server.common.data.query.EntityCountQuery; +import org.thingsboard.server.common.data.query.EntityDataQuery; +import org.thingsboard.server.common.data.query.EntityDataSortOrder; +import org.thingsboard.server.common.data.query.EntityFilter; +import org.thingsboard.server.common.data.query.EntityKeyType; +import org.thingsboard.server.common.data.query.SingleEntityFilter; +import org.thingsboard.server.common.data.query.StateEntityOwnerFilter; +import org.thingsboard.server.common.data.query.TsValue; +import org.thingsboard.server.common.data.relation.EntityRelation; +import org.thingsboard.server.common.data.relation.RelationTypeGroup; +import org.thingsboard.server.edqs.data.ApiUsageStateData; +import org.thingsboard.server.edqs.data.AssetData; +import org.thingsboard.server.edqs.data.CustomerData; +import org.thingsboard.server.edqs.data.DeviceData; +import org.thingsboard.server.edqs.data.EntityData; +import org.thingsboard.server.edqs.data.EntityGroupData; +import org.thingsboard.server.edqs.data.EntityProfileData; +import org.thingsboard.server.edqs.data.GenericData; +import org.thingsboard.server.edqs.data.RelationsRepo; +import org.thingsboard.server.edqs.data.TenantData; +import org.thingsboard.server.edqs.data.dp.BoolDataPoint; +import org.thingsboard.server.edqs.data.dp.CompressedJsonDataPoint; +import org.thingsboard.server.edqs.data.dp.CompressedStringDataPoint; +import org.thingsboard.server.edqs.data.dp.DataPoint; +import org.thingsboard.server.edqs.data.dp.DoubleDataPoint; +import org.thingsboard.server.edqs.data.dp.JsonDataPoint; +import org.thingsboard.server.edqs.data.dp.LongDataPoint; +import org.thingsboard.server.edqs.data.dp.StringDataPoint; +import org.thingsboard.server.edqs.query.EdqsDataQuery; +import org.thingsboard.server.edqs.query.EdqsQuery; +import org.thingsboard.server.edqs.query.SortableEntityData; +import org.thingsboard.server.edqs.query.processor.EntityQueryProcessor; +import org.thingsboard.server.edqs.query.processor.EntityQueryProcessorFactory; +import org.thingsboard.server.edqs.stats.EdqsStatsService; +import org.thingsboard.server.edqs.util.RepositoryUtils; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.Comparator; +import java.util.HashMap; +import java.util.HashSet; +import java.util.LinkedList; +import java.util.List; +import java.util.Map; +import java.util.Optional; +import java.util.Queue; +import java.util.Set; +import java.util.TreeSet; +import java.util.UUID; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.ConcurrentMap; +import java.util.concurrent.ConcurrentSkipListSet; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.locks.Lock; +import java.util.concurrent.locks.ReentrantLock; +import java.util.stream.Collectors; + +import static org.thingsboard.server.edqs.util.RepositoryUtils.SORT_ASC; +import static org.thingsboard.server.edqs.util.RepositoryUtils.SORT_DESC; +import static org.thingsboard.server.edqs.util.RepositoryUtils.SYS_ADMIN_PERMISSIONS; +import static org.thingsboard.server.edqs.util.RepositoryUtils.resolveEntityType; + +@Slf4j +public class TenantRepo { + + public static final Comparator> CREATED_TIME_COMPARATOR = Comparator.comparingLong(o -> o.getFields() != null ? o.getFields().getCreatedTime() : 0); // FIXME: fields may be null at first + public static final Comparator> CREATED_TIME_AND_ID_COMPARATOR = CREATED_TIME_COMPARATOR + .thenComparing(EntityData::getId); + public static final Comparator> CREATED_TIME_AND_ID_DESC_COMPARATOR = CREATED_TIME_AND_ID_COMPARATOR.reversed(); + + private final ConcurrentMap>> entitySetByType = new ConcurrentHashMap<>(); + private final ConcurrentMap>> entityMapByType = new ConcurrentHashMap<>(); + private final ConcurrentMap> customersHierarchy = new ConcurrentHashMap<>(); + private final ConcurrentMap entityGroups = new ConcurrentHashMap<>(); + private final ConcurrentMap relations = new ConcurrentHashMap<>(); + + private final Lock entityUpdateLock = new ReentrantLock(); + + private final TenantId tenantId; + private final Optional edqsStatsService; + + public TenantRepo(TenantId tenantId, Optional edqsStatsService) { + this.tenantId = tenantId; + this.edqsStatsService = edqsStatsService; + } + + public void processEvent(EdqsEvent event) { + EdqsObject edqsObject = event.getObject(); + log.debug("[{}] Processing event: {}", tenantId, event); + if (event.getEventType() == EdqsEventType.UPDATED) { + addOrUpdate(edqsObject); + } else if (event.getEventType() == EdqsEventType.DELETED) { + remove(edqsObject); + } + } + + public void addOrUpdate(EdqsObject object) { + if (object instanceof EntityRelation relation) { + addOrUpdateRelation(relation); + } else if (object instanceof AttributeKv attributeKv) { + addOrUpdateAttribute(attributeKv); + } else if (object instanceof LatestTsKv latestTsKv) { + addOrUpdateLatestKv(latestTsKv); + } else if (object instanceof Entity entity) { + addOrUpdateEntity(entity); + } + } + + public void remove(EdqsObject object) { + if (object instanceof EntityRelation relation) { + removeRelation(relation); + } else if (object instanceof AttributeKv attributeKv) { + removeAttribute(attributeKv); + } else if (object instanceof LatestTsKv latestTsKv) { + removeLatestKv(latestTsKv); + } else if (object instanceof Entity entity) { + removeEntity(entity); + } + } + + private void addOrUpdateRelation(EntityRelation entity) { + entityUpdateLock.lock(); + try { + if (RelationTypeGroup.COMMON.equals(entity.getTypeGroup())) { + RelationsRepo repo = relations.computeIfAbsent(entity.getTypeGroup(), tg -> new RelationsRepo()); + EntityData from = getOrCreate(entity.getFrom()); + EntityData to = getOrCreate(entity.getTo()); + boolean added = repo.add(from, to, TbStringPool.intern(entity.getType())); + if (added) { + edqsStatsService.ifPresent(statService -> statService.reportTenantEdqsObject(tenantId, ObjectType.RELATION, EdqsEventType.UPDATED)); + } + } else if (RelationTypeGroup.FROM_ENTITY_GROUP.equals(entity.getTypeGroup())) { + var eg = getEntityGroup(entity.getFrom().getId()); + if (eg != null) { + eg.addOrUpdate(getOrCreate(entity.getTo())); + } + } + } finally { + entityUpdateLock.unlock(); + } + } + + private void removeRelation(EntityRelation entityRelation) { + if (RelationTypeGroup.COMMON.equals(entityRelation.getTypeGroup())) { + RelationsRepo relationsRepo = relations.get(entityRelation.getTypeGroup()); + if (relationsRepo != null) { + boolean removed = relationsRepo.remove(entityRelation.getFrom().getId(), entityRelation.getTo().getId(), entityRelation.getType()); + if (removed) { + edqsStatsService.ifPresent(statService -> statService.reportTenantEdqsObject(tenantId, ObjectType.RELATION, EdqsEventType.DELETED)); + } + } + } else if (RelationTypeGroup.FROM_ENTITY_GROUP.equals(entityRelation.getTypeGroup())) { + var eg = getEntityGroup(entityRelation.getFrom().getId()); + if (eg != null) { + eg.remove(entityRelation.getTo().getId()); + } + } + } + + private void addOrUpdateEntity(Entity entity) { + entityUpdateLock.lock(); + try { + log.trace("[{}] addOrUpdateEntity: {}", tenantId, entity); + EntityFields fields = entity.getFields(); + UUID entityId = fields.getId(); + EntityType entityType = entity.getType(); + + EntityData entityData = getOrCreate(entityType, entityId); + processFields(fields); + entityData.setFields(entity.getFields()); + + switch (entity.getType()) { + case ENTITY_GROUP -> { + EntityGroupFields entityGroupFields = (EntityGroupFields) fields; + UUID ownerId = entityGroupFields.getOwnerId(); + if (EntityType.CUSTOMER.equals(entityGroupFields.getOwnerType())) { + entityData.setCustomerId(ownerId); + ((CustomerData) getEntityMap(EntityType.CUSTOMER).computeIfAbsent(ownerId, CustomerData::new)).addOrUpdate(entityData); + } + entityGroups.put(fields.getId(), (EntityGroupData) entityData); + } + case CUSTOMER -> { + CustomerFields customerFields = (CustomerFields) fields; + UUID newParentId = customerFields.getCustomerId(); // for customer, customerId is parentCustomerId + UUID oldParentId = entityData.getCustomerId(); + entityData.setCustomerId(newParentId); + if (entityIdMismatch(oldParentId, newParentId)) { + if (oldParentId != null) { + customersHierarchy.computeIfAbsent(oldParentId, id -> new HashSet<>()).remove(entityData.getId()); + } + if (newParentId != null) { + customersHierarchy.computeIfAbsent(newParentId, id -> new HashSet<>()).add(entityData.getId()); + } + } + } + default -> { + UUID newCustomerId = fields.getCustomerId(); + UUID oldCustomerId = entityData.getCustomerId(); + entityData.setCustomerId(newCustomerId); + if (entityIdMismatch(oldCustomerId, newCustomerId)) { + if (oldCustomerId != null) { + CustomerData old = (CustomerData) getEntityMap(EntityType.CUSTOMER).get(oldCustomerId); + if (old != null) { + old.remove(entityData); + } + } + if (newCustomerId != null) { + CustomerData newData = (CustomerData) getEntityMap(EntityType.CUSTOMER).computeIfAbsent(newCustomerId, CustomerData::new); + newData.addOrUpdate(entityData); + } + } + } + } + } finally { + entityUpdateLock.unlock(); + } + } + + public void removeEntity(Entity entity) { + entityUpdateLock.lock(); + try { + UUID entityId = entity.getFields().getId(); + EntityType entityType = entity.getType(); + EntityData removed = getEntityMap(entityType).remove(entityId); + if (removed != null) { + getEntitySet(entityType).remove(removed); + edqsStatsService.ifPresent(statService -> statService.reportTenantEdqsObject(tenantId, ObjectType.fromEntityType(entityType), EdqsEventType.DELETED)); + } + switch (entityType) { + case ENTITY_GROUP -> { + entityGroups.remove(entityId); + } + case CUSTOMER -> { + customersHierarchy.remove(entityId); + } + } + } finally { + entityUpdateLock.unlock(); + } + } + + public void addOrUpdateAttribute(AttributeKv attributeKv) { + var entityData = getOrCreate(attributeKv.getEntityId()); + if (entityData != null) { + KvEntry value = attributeKv.getValue(); + Integer keyId = KeyDictionary.get(attributeKv.getKey()); + boolean added = entityData.putAttr(keyId, attributeKv.getScope(), toDataPoint(attributeKv.getLastUpdateTs(), value)); + if (added) { + edqsStatsService.ifPresent(statService -> statService.reportTenantEdqsObject(tenantId, ObjectType.ATTRIBUTE_KV, EdqsEventType.UPDATED)); + } + } + } + + private void removeAttribute(AttributeKv attributeKv) { + var entityData = get(attributeKv.getEntityId()); + if (entityData != null) { + boolean removed = entityData.removeAttr(KeyDictionary.get(attributeKv.getKey()), attributeKv.getScope()); + if (removed) { + edqsStatsService.ifPresent(statService -> statService.reportTenantEdqsObject(tenantId, ObjectType.ATTRIBUTE_KV, EdqsEventType.DELETED)); + } + } + } + + public void addOrUpdateLatestKv(LatestTsKv latestTsKv) { + var entityData = getOrCreate(latestTsKv.getEntityId()); + if (entityData != null) { + KvEntry value = latestTsKv.getValue(); + Integer keyId = KeyDictionary.get(latestTsKv.getKey()); + boolean added = entityData.putTs(keyId, toDataPoint(latestTsKv.getTs(), value)); + if (added) { + edqsStatsService.ifPresent(statService -> statService.reportTenantEdqsObject(tenantId, ObjectType.LATEST_TS_KV, EdqsEventType.UPDATED)); + } + } + } + + private void removeLatestKv(LatestTsKv latestTsKv) { + var entityData = get(latestTsKv.getEntityId()); + if (entityData != null) { + boolean removed = entityData.removeTs(KeyDictionary.get(latestTsKv.getKey())); + if (removed) { + edqsStatsService.ifPresent(statService -> statService.reportTenantEdqsObject(tenantId, ObjectType.LATEST_TS_KV, EdqsEventType.DELETED)); + } + } + } + + private DataPoint toDataPoint(long ts, KvEntry kvEntry) { + return switch (kvEntry.getDataType()) { + case BOOLEAN -> new BoolDataPoint(ts, kvEntry.getBooleanValue().get()); + case STRING -> getStrDataPoint(ts, kvEntry.getStrValue().get()); + case LONG -> new LongDataPoint(ts, kvEntry.getLongValue().get()); + case DOUBLE -> new DoubleDataPoint(ts, kvEntry.getDoubleValue().get()); + case JSON -> getJsonDataPoint(ts, kvEntry.getJsonValue().get()); + }; + } + + public void processFields(EntityFields fields) { + if (fields instanceof AssetFields assetFields) { + assetFields.setType(TbStringPool.intern(assetFields.getType())); + } + } + + private static DataPoint getStrDataPoint(long ts, String strV) { + DataPoint dp; + if (strV.length() < CompressedStringDataPoint.MIN_STR_SIZE_TO_COMPRESS) { + dp = new StringDataPoint(ts, strV); + } else { + dp = new CompressedStringDataPoint(ts, strV); + } + return dp; + } + + private static DataPoint getJsonDataPoint(long ts, String strV) { + DataPoint dp; + if (strV.length() < CompressedStringDataPoint.MIN_STR_SIZE_TO_COMPRESS) { + dp = new JsonDataPoint(ts, strV); + } else { + dp = new CompressedJsonDataPoint(ts, strV); + } + return dp; + } + + public ConcurrentMap> getEntityMap(EntityType entityType) { + return entityMapByType.computeIfAbsent(entityType, et -> new ConcurrentHashMap<>()); + } + + //TODO: automatically remove entities that has nothing except the ID. + private EntityData getOrCreate(EntityId entityId) { + return getOrCreate(entityId.getEntityType(), entityId.getId()); + } + + private EntityData getOrCreate(EntityType entityType, UUID entityId) { + return getEntityMap(entityType).computeIfAbsent(entityId, id -> { + log.debug("[{}] Adding {} {}", tenantId, entityType, id); + EntityData entityData = constructEntityData(entityType, entityId); + getEntitySet(entityType).add(entityData); + edqsStatsService.ifPresent(statService -> statService.reportTenantEdqsObject(tenantId, ObjectType.fromEntityType(entityType), EdqsEventType.UPDATED)); + return entityData; + }); + } + + private EntityData get(EntityId entityId) { + return getEntityMap(entityId.getEntityType()).get(entityId.getId()); + } + + private EntityData constructEntityData(EntityType entityType, UUID id) { + EntityData entityData = switch (entityType) { + case DEVICE -> new DeviceData(id); + case ASSET -> new AssetData(id); + case DEVICE_PROFILE, ASSET_PROFILE -> new EntityProfileData(id, entityType); + case CUSTOMER -> new CustomerData(id); + case TENANT -> new TenantData(id); + case ENTITY_GROUP -> new EntityGroupData(id); + case API_USAGE_STATE -> new ApiUsageStateData(id); + default -> new GenericData(entityType, id); + }; + entityData.setRepo(this); + return entityData; + } + + private static boolean entityIdMismatch(UUID oldOrNull, UUID newOrNull) { + if (oldOrNull == null) { + return newOrNull != null; + } else { + return !oldOrNull.equals(newOrNull); + } + } + + public Set> getEntitySet(EntityType entityType) { + return entitySetByType.computeIfAbsent(entityType, et -> new ConcurrentSkipListSet<>(CREATED_TIME_AND_ID_DESC_COMPARATOR)); + } + + public PageData findEntityDataByQuery(CustomerId customerId, MergedUserPermissions userPermissions, + EntityDataQuery oldQuery, boolean ignorePermissionCheck) { + EdqsDataQuery query = RepositoryUtils.toNewQuery(oldQuery); + log.info("[{}][{}] findEntityDataByQuery: {}", tenantId, customerId, query); + QueryContext ctx = buildContext(customerId, userPermissions, query.getEntityFilter(), ignorePermissionCheck); + if (ctx == null) { + return PageData.emptyPageData(); + } + EntityQueryProcessor queryProcessor = EntityQueryProcessorFactory.create(this, ctx, query); + return sortAndConvert(query, queryProcessor.processQuery(), ctx); + } + + public long countEntitiesByQuery(CustomerId customerId, MergedUserPermissions userPermissions, EntityCountQuery oldQuery, boolean ignorePermissionCheck) { + EdqsQuery query = RepositoryUtils.toNewQuery(oldQuery); + log.info("[{}][{}] countEntitiesByQuery: {}", tenantId, customerId, query); + QueryContext ctx = buildContext(customerId, userPermissions, query.getEntityFilter(), ignorePermissionCheck); + if (ctx == null) { + return 0; + } + EntityQueryProcessor queryProcessor = EntityQueryProcessorFactory.create(this, ctx, query); + return queryProcessor.count(); + } + + private PageData sortAndConvert(EdqsDataQuery query, List data, QueryContext ctx) { + int totalSize = data.size(); + int totalPages = (int) Math.ceil((float) totalSize / query.getPageSize()); + int offset = query.getPage() * query.getPageSize(); + if (offset > totalSize) { + return new PageData<>(Collections.emptyList(), totalPages, totalSize, false); + } else { + Comparator comparator = EntityDataSortOrder.Direction.ASC.equals(query.getSortDirection()) ? SORT_ASC : SORT_DESC; + long startTs = System.nanoTime(); +// IMPLEMENTATION THAT IS BASED ON PRIORITY_QUEUE +// var requiredSize = Math.min(offset + query.getPageSize(), totalSize); +// PriorityQueue topN = new PriorityQueue<>(requiredSize, comparator.reversed()); +// for (SortableEntityData item : data) { +// topN.add(item); +// if (topN.size() > requiredSize) { +// topN.poll(); +// } +// } +// List result = new ArrayList<>(topN); +// Collections.reverse(result); +// result = result.subList(offset, requiredSize); +// IMPLEMENTATION THAT IS BASED ON TREE SET (For offset + query.getPageSize() << totalSize) + var requiredSize = Math.min(offset + query.getPageSize(), totalSize); + TreeSet topNSet = new TreeSet<>(comparator); + for (SortableEntityData sp : data) { + topNSet.add(sp); + if (topNSet.size() > requiredSize) { + topNSet.pollLast(); + } + } + var result = topNSet.stream().skip(offset).limit(query.getPageSize()).collect(Collectors.toList()); +// IMPLEMENTATION THAT IS BASED ON TIM SORT (For offset + query.getPageSize() > totalSize / 2) +// data.sort(comparator); +// var result = data.subList(offset, endIndex); + log.debug("EDQ Sorted in {}", TimeUnit.NANOSECONDS.toMillis(System.nanoTime() - startTs)); + return new PageData<>(toQueryResult(result, query, ctx), totalPages, totalSize, totalSize > requiredSize); + } + } + + private List toQueryResult(List data, EdqsDataQuery query, QueryContext ctx) { + long ts = System.currentTimeMillis(); + List results = new ArrayList<>(data.size()); + for (SortableEntityData entityData : data) { + Map> latest = new HashMap<>(); + for (var key : query.getEntityFields()) { + DataPoint dp = entityData.getEntityData().getDataPoint(key, ctx); + TsValue v = RepositoryUtils.toTsValue(ts, dp); + latest.computeIfAbsent(EntityKeyType.ENTITY_FIELD, t -> new HashMap<>()).put(key.key(), v); + } + for (var key : query.getLatestValues()) { + DataPoint dp = entityData.getEntityData().getDataPoint(key, ctx); + TsValue v = RepositoryUtils.toTsValue(ts, dp); + latest.computeIfAbsent(key.type(), t -> new HashMap<>()).put(KeyDictionary.get(key.keyId()), v); + } + + results.add(new QueryResult(entityData.getEntityId(), entityData.isReadAttrs(), entityData.isReadTs(), latest)); + } + return results; + } + + private QueryContext buildContext(CustomerId customerId, MergedUserPermissions userPermissions, EntityFilter filter, boolean ignorePermissionCheck) { + QueryContext queryContext; + if (TenantId.SYS_TENANT_ID.equals(tenantId)) { + queryContext = new QueryContext(tenantId, customerId, resolveEntityType(filter), SYS_ADMIN_PERMISSIONS, filter, ignorePermissionCheck); + } else { + switch (filter.getType()) { + case STATE_ENTITY_OWNER: + var singleEntity = ((StateEntityOwnerFilter) filter).getSingleEntity(); + EntityData ed = getEntityMap(singleEntity.getEntityType()).get(singleEntity.getId()); + if (ed != null) { + EntityId owner = ed.getCustomerId() != null ? new CustomerId(ed.getCustomerId()) : tenantId; + queryContext = new QueryContext(tenantId, customerId, owner.getEntityType(), userPermissions, filter, owner, ignorePermissionCheck); + } else { + return null; + } + break; + case SINGLE_ENTITY: + SingleEntityFilter seFilter = (SingleEntityFilter) filter; + EntityId entityId = seFilter.getSingleEntity(); + if (entityId != null && entityId.getEntityType().equals(EntityType.ENTITY_GROUP)) { + EntityGroupData entityGroupData = entityGroups.get(entityId.getId()); + if (entityGroupData != null) { + queryContext = new QueryContext(tenantId, customerId, EntityType.ENTITY_GROUP, userPermissions, filter, entityGroupData.getEntityType(), ignorePermissionCheck); + } else { + return null; + } + } else { + queryContext = new QueryContext(tenantId, customerId, resolveEntityType(filter), userPermissions, filter, ignorePermissionCheck); + } + break; + default: + queryContext = new QueryContext(tenantId, customerId, resolveEntityType(filter), userPermissions, filter, ignorePermissionCheck); + } + } + return queryContext; + } + + public TenantId getTenantId() { + return tenantId; + } + + public Set getAllCustomers(UUID customerId) { + Set result = new HashSet<>(); + Queue queue = new LinkedList<>(); + + if (customerId != null) { + queue.add(customerId); + } + + while (!queue.isEmpty()) { + UUID current = queue.poll(); + if (!result.contains(current)) { + result.add(current); + Set children = customersHierarchy.get(current); + if (children != null) { + queue.addAll(children); + } + } + } + + return result; + } + + public boolean contains(UUID entityGroupID, UUID entityId) { + var groupData = entityGroups.get(entityGroupID); + return groupData != null && groupData.getEntity(entityId) != null; + } + + public EntityGroupData getEntityGroup(UUID groupId) { + return entityGroups.get(groupId); + } + + public RelationsRepo getRelations(RelationTypeGroup relationTypeGroup) { + return relations.get(relationTypeGroup); + } + + public String getOwnerName(EntityId ownerId) { + if (ownerId == null || (EntityType.CUSTOMER.equals(ownerId.getEntityType()) && CustomerId.NULL_UUID.equals(ownerId.getId()))) { + ownerId = tenantId; + } + return getEntityName(ownerId); + } + + private String getEntityName(EntityId entityId) { + EntityType entityType = entityId.getEntityType(); + return switch (entityType) { + case CUSTOMER, TENANT -> getEntityMap(entityType).get(entityId.getId()).getFields().getName(); + default -> throw new RuntimeException("Unsupported entity type: " + entityType); + }; + } + +} diff --git a/common/edqs/src/main/java/org/thingsboard/server/edqs/state/EdqsStateService.java b/common/edqs/src/main/java/org/thingsboard/server/edqs/state/EdqsStateService.java new file mode 100644 index 0000000000..e36e5ae5ab --- /dev/null +++ b/common/edqs/src/main/java/org/thingsboard/server/edqs/state/EdqsStateService.java @@ -0,0 +1,32 @@ +/** + * Copyright © 2016-2024 ThingsBoard, Inc. + * + * 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.edqs.state; + +import org.thingsboard.server.common.data.ObjectType; +import org.thingsboard.server.common.data.edqs.EdqsEventType; +import org.thingsboard.server.common.data.id.TenantId; +import org.thingsboard.server.common.msg.queue.TopicPartitionInfo; +import org.thingsboard.server.gen.transport.TransportProtos.ToEdqsMsg; + +import java.util.Set; + +public interface EdqsStateService { + + void restore(Set partitions); + + void save(TenantId tenantId, ObjectType type, String key, EdqsEventType eventType, ToEdqsMsg msg); + +} diff --git a/common/edqs/src/main/java/org/thingsboard/server/edqs/state/KafkaEdqsStateService.java b/common/edqs/src/main/java/org/thingsboard/server/edqs/state/KafkaEdqsStateService.java new file mode 100644 index 0000000000..e8e8ee610e --- /dev/null +++ b/common/edqs/src/main/java/org/thingsboard/server/edqs/state/KafkaEdqsStateService.java @@ -0,0 +1,187 @@ +/** + * Copyright © 2016-2024 ThingsBoard, Inc. + * + * 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.edqs.state; + +import jakarta.annotation.PostConstruct; +import jakarta.annotation.PreDestroy; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Service; +import org.thingsboard.common.util.ThingsBoardExecutors; +import org.thingsboard.common.util.ThingsBoardThreadFactory; +import org.thingsboard.server.common.data.ObjectType; +import org.thingsboard.server.common.data.edqs.EdqsEventType; +import org.thingsboard.server.common.data.id.TenantId; +import org.thingsboard.server.common.data.queue.QueueConfig; +import org.thingsboard.server.common.msg.queue.ServiceType; +import org.thingsboard.server.common.msg.queue.TopicPartitionInfo; +import org.thingsboard.server.edqs.processor.EdqsProcessor; +import org.thingsboard.server.edqs.processor.EdqsProducer; +import org.thingsboard.server.edqs.util.EdqsPartitionService; +import org.thingsboard.server.edqs.util.VersionsStore; +import org.thingsboard.server.gen.transport.TransportProtos.EdqsEventMsg; +import org.thingsboard.server.gen.transport.TransportProtos.ToEdqsMsg; +import org.thingsboard.server.queue.common.TbProtoQueueMsg; +import org.thingsboard.server.queue.common.consumer.MainQueueConsumerManager; +import org.thingsboard.server.queue.common.consumer.QueueConsumerManager; +import org.thingsboard.server.queue.discovery.QueueKey; +import org.thingsboard.server.queue.edqs.EdqsConfig; +import org.thingsboard.server.queue.edqs.EdqsQueue; +import org.thingsboard.server.queue.edqs.EdqsQueueFactory; +import org.thingsboard.server.queue.edqs.KafkaEdqsComponent; +import org.thingsboard.server.queue.util.AfterStartUp; + +import java.util.Set; +import java.util.UUID; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; +import java.util.concurrent.ScheduledExecutorService; +import java.util.concurrent.atomic.AtomicInteger; + +@Service +@RequiredArgsConstructor +@KafkaEdqsComponent +@Slf4j +public class KafkaEdqsStateService implements EdqsStateService { + + private final EdqsConfig config; + private final EdqsPartitionService partitionService; + private final EdqsQueueFactory queueFactory; + private final EdqsProcessor edqsProcessor; + + private MainQueueConsumerManager, QueueConfig> stateConsumer; + private QueueConsumerManager> eventsConsumer; + private EdqsProducer stateProducer; + + private ExecutorService consumersExecutor; + private ExecutorService mgmtExecutor; + private ScheduledExecutorService scheduler; + + private final VersionsStore versionsStore = new VersionsStore(); + private final AtomicInteger restoredCount = new AtomicInteger(); + + @PostConstruct + private void init() { + consumersExecutor = Executors.newCachedThreadPool(ThingsBoardThreadFactory.forName("edqs-backup-consumer")); + mgmtExecutor = ThingsBoardExecutors.newWorkStealingPool(4, "edqs-backup-consumer-mgmt"); + scheduler = ThingsBoardExecutors.newSingleThreadScheduledExecutor("edqs-backup-scheduler"); + + stateConsumer = MainQueueConsumerManager., QueueConfig>builder() + .queueKey(new QueueKey(ServiceType.EDQS, EdqsQueue.STATE.getTopic())) + .config(QueueConfig.of(true, config.getPollInterval())) + .msgPackProcessor((msgs, consumer, config) -> { + for (TbProtoQueueMsg queueMsg : msgs) { + try { + ToEdqsMsg msg = queueMsg.getValue(); + log.trace("Processing message: {}", msg); + edqsProcessor.process(msg, EdqsQueue.STATE); + if (restoredCount.incrementAndGet() % 1000 == 0) { + log.info("Processed {} msgs", restoredCount.get()); + } + } catch (Throwable t) { + log.error("Failed to process message: {}", queueMsg, t); + } + } + consumer.commit(); + }) + .consumerCreator((config, partitionId) -> queueFactory.createEdqsMsgConsumer(EdqsQueue.STATE)) + .consumerExecutor(consumersExecutor) + .taskExecutor(mgmtExecutor) + .scheduler(scheduler) + .build(); + + eventsConsumer = QueueConsumerManager.>builder() + .name("edqs-events-to-backup-consumer") + .pollInterval(config.getPollInterval()) + .msgPackProcessor((msgs, consumer) -> { + for (TbProtoQueueMsg queueMsg : msgs) { + try { + ToEdqsMsg msg = queueMsg.getValue(); + log.trace("Processing message: {}", msg); + + if (msg.hasEventMsg()) { + EdqsEventMsg eventMsg = msg.getEventMsg(); + String key = eventMsg.getKey(); + if (eventMsg.hasVersion()) { + if (!versionsStore.isNew(key, eventMsg.getVersion())) { + return; + } + } + + TenantId tenantId = getTenantId(msg); + ObjectType objectType = ObjectType.valueOf(eventMsg.getObjectType()); + EdqsEventType eventType = EdqsEventType.valueOf(eventMsg.getEventType()); + log.debug("[{}] Saving to backup [{}] [{}] [{}]", tenantId, objectType, eventType, key); + stateProducer.send(tenantId, objectType, key, msg); + } + } catch (Throwable t) { + log.error("Failed to process message: {}", queueMsg, t); + } + } + consumer.commit(); + }) + .consumerCreator(() -> queueFactory.createEdqsMsgConsumer(EdqsQueue.EVENTS, "events-to-backup-consumer-group")) // shared by all instances consumer group + .consumerExecutor(consumersExecutor) + .threadPrefix("edqs-events-to-backup") + .build(); + + stateProducer = EdqsProducer.builder() + .queue(EdqsQueue.STATE) + .partitionService(partitionService) + .producer(queueFactory.createEdqsMsgProducer(EdqsQueue.STATE)) + .build(); + } + + @AfterStartUp(order = AfterStartUp.REGULAR_SERVICE) + public void afterStartUp() { + eventsConsumer.subscribe(); + eventsConsumer.launch(); + } + + @Override + public void restore(Set partitions) { + restoredCount.set(0); + long startTs = System.currentTimeMillis(); + log.info("Restore started for partitions {}", partitions.stream().map(tpi -> tpi.getPartition().orElse(-1)).sorted().toList()); + + stateConsumer.doUpdate(partitions); // calling blocking doUpdate instead of update + stateConsumer.awaitStop(0); // consumers should stop on their own because EdqsQueue.STATE.stopWhenRead is true, we just need to wait + + log.info("Restore finished in {} ms. Processed {} msgs", (System.currentTimeMillis() - startTs), restoredCount.get()); + } + + @Override + public void save(TenantId tenantId, ObjectType type, String key, EdqsEventType eventType, ToEdqsMsg msg) { + // do nothing here, backup is done by events consumer + } + + private TenantId getTenantId(ToEdqsMsg edqsMsg) { + return TenantId.fromUUID(new UUID(edqsMsg.getTenantIdMSB(), edqsMsg.getTenantIdLSB())); + } + + @PreDestroy + private void preDestroy() { + stateConsumer.stop(); + stateConsumer.awaitStop(); + eventsConsumer.stop(); + stateProducer.stop(); + + consumersExecutor.shutdownNow(); + mgmtExecutor.shutdownNow(); + scheduler.shutdownNow(); + } + +} diff --git a/common/edqs/src/main/java/org/thingsboard/server/edqs/state/LocalEdqsStateService.java b/common/edqs/src/main/java/org/thingsboard/server/edqs/state/LocalEdqsStateService.java new file mode 100644 index 0000000000..8786f1866c --- /dev/null +++ b/common/edqs/src/main/java/org/thingsboard/server/edqs/state/LocalEdqsStateService.java @@ -0,0 +1,81 @@ +/** + * Copyright © 2016-2024 ThingsBoard, Inc. + * + * 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.edqs.state; + +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.context.annotation.Lazy; +import org.springframework.stereotype.Service; +import org.thingsboard.server.common.data.ObjectType; +import org.thingsboard.server.common.data.edqs.EdqsEventType; +import org.thingsboard.server.common.data.id.TenantId; +import org.thingsboard.server.common.msg.queue.TopicPartitionInfo; +import org.thingsboard.server.edqs.processor.EdqsProcessor; +import org.thingsboard.server.edqs.util.EdqsRocksDb; +import org.thingsboard.server.gen.transport.TransportProtos.ToEdqsMsg; +import org.thingsboard.server.queue.edqs.EdqsQueue; +import org.thingsboard.server.queue.edqs.InMemoryEdqsComponent; + +import java.util.Set; + +@Service +@RequiredArgsConstructor +@InMemoryEdqsComponent +@Slf4j +public class LocalEdqsStateService implements EdqsStateService { + + @Autowired @Lazy + private EdqsProcessor processor; + @Autowired + private EdqsRocksDb db; + + private Set partitions; + + @Override + public void restore(Set partitions) { + if (this.partitions == null) { + this.partitions = partitions; + } else { + return; + } + + db.forEach((key, value) -> { + try { + ToEdqsMsg edqsMsg = ToEdqsMsg.parseFrom(value); + log.trace("[{}] Restored msg from RocksDB: {}", key, edqsMsg); + processor.process(edqsMsg, EdqsQueue.STATE); + } catch (Exception e) { + log.error("[{}] Failed to restore value", key, e); + } + }); + } + + @Override + public void save(TenantId tenantId, ObjectType type, String key, EdqsEventType eventType, ToEdqsMsg msg) { + log.trace("Save to RocksDB: {} {} {} {}", tenantId, type, key, msg); + try { + if (eventType == EdqsEventType.DELETED) { + db.delete(key); + } else { + db.put(key, msg.toByteArray()); + } + } catch (Exception e) { + log.error("[{}] Failed to save event {}", key, msg, e); + } + } + +} diff --git a/common/edqs/src/main/java/org/thingsboard/server/edqs/stats/EdqsStatsService.java b/common/edqs/src/main/java/org/thingsboard/server/edqs/stats/EdqsStatsService.java new file mode 100644 index 0000000000..cfca43b08c --- /dev/null +++ b/common/edqs/src/main/java/org/thingsboard/server/edqs/stats/EdqsStatsService.java @@ -0,0 +1,91 @@ +/** + * Copyright © 2016-2024 ThingsBoard, Inc. + * + * 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.edqs.stats; + +import lombok.AllArgsConstructor; +import lombok.Getter; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; +import org.springframework.scheduling.annotation.Scheduled; +import org.springframework.stereotype.Service; +import org.thingsboard.server.common.data.ObjectType; +import org.thingsboard.server.common.data.edqs.EdqsEventType; +import org.thingsboard.server.common.data.id.TenantId; +import org.thingsboard.server.common.stats.StatsFactory; +import org.thingsboard.server.common.stats.StatsType; +import org.thingsboard.server.queue.edqs.EdqsComponent; + +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.atomic.AtomicInteger; +import java.util.stream.Collectors; + +@EdqsComponent +@Service +@Slf4j +@RequiredArgsConstructor +@ConditionalOnProperty(name = "queue.edqs.stats.enabled", havingValue = "true", matchIfMissing = true) +public class EdqsStatsService { + + private final ConcurrentHashMap statsMap = new ConcurrentHashMap<>(); + private final StatsFactory statsFactory; + + @Scheduled(initialDelayString = "${queue.edqs.stats.print-interval-ms:60000}", + fixedDelayString = "${queue.edqs.stats.print-interval-ms:60000}") + private void reportStats() { + String values = statsMap.entrySet().stream() + .map(kv -> "TenantId [" + kv.getKey() + "] stats [" + kv.getValue() + "]") + .collect(Collectors.joining(System.lineSeparator())); + log.info("EDQS Stats: {}", values); + } + + public void reportTenantEdqsObject(TenantId tenantId, ObjectType objectType, EdqsEventType eventType) { + statsMap.computeIfAbsent(tenantId, id -> new EdqsStats(tenantId, statsFactory)) + .reportEdqsObject(objectType, eventType); + } + + @Getter + @AllArgsConstructor + static class EdqsStats { + + private final TenantId tenantId; + private final ConcurrentHashMap entityCounters = new ConcurrentHashMap<>(); + private final StatsFactory statsFactory; + + private AtomicInteger getOrCreateObjectCounter(ObjectType objectType) { + return entityCounters.computeIfAbsent(objectType, + type -> statsFactory.createGauge(StatsType.EDQS.getName() + "_object_count", new AtomicInteger(), + "tenantId", tenantId.toString(), "objectType", type.name())); + } + + @Override + public String toString() { + return entityCounters.entrySet().stream() + .map(counters -> counters.getKey().name()+ " total = [" + counters.getValue() + "]") + .collect(Collectors.joining(", ")); + } + + public void reportEdqsObject(ObjectType objectType, EdqsEventType eventType) { + AtomicInteger objectCounter = getOrCreateObjectCounter(objectType); + if (eventType == EdqsEventType.UPDATED){ + objectCounter.incrementAndGet(); + } else if (eventType == EdqsEventType.DELETED) { + objectCounter.decrementAndGet(); + } + } + } + +} diff --git a/common/edqs/src/main/java/org/thingsboard/server/edqs/util/EdqsPartitionService.java b/common/edqs/src/main/java/org/thingsboard/server/edqs/util/EdqsPartitionService.java new file mode 100644 index 0000000000..e8c260a996 --- /dev/null +++ b/common/edqs/src/main/java/org/thingsboard/server/edqs/util/EdqsPartitionService.java @@ -0,0 +1,40 @@ +/** + * Copyright © 2016-2024 ThingsBoard, Inc. + * + * 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.edqs.util; + +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; +import org.thingsboard.server.common.data.id.TenantId; +import org.thingsboard.server.queue.discovery.HashPartitionService; +import org.thingsboard.server.queue.edqs.EdqsConfig; +import org.thingsboard.server.queue.edqs.EdqsConfig.EdqsPartitioningStrategy; + +@Service +@RequiredArgsConstructor +public class EdqsPartitionService { + + private final HashPartitionService hashPartitionService; + private final EdqsConfig edqsConfig; + + public Integer resolvePartition(TenantId tenantId) { + if (edqsConfig.getPartitioningStrategy() == EdqsPartitioningStrategy.TENANT) { + return hashPartitionService.resolvePartitionIndex(tenantId.getId(), edqsConfig.getPartitions()); + } else { + return null; + } + } + +} diff --git a/common/edqs/src/main/java/org/thingsboard/server/edqs/util/EdqsRocksDb.java b/common/edqs/src/main/java/org/thingsboard/server/edqs/util/EdqsRocksDb.java new file mode 100644 index 0000000000..933d293c79 --- /dev/null +++ b/common/edqs/src/main/java/org/thingsboard/server/edqs/util/EdqsRocksDb.java @@ -0,0 +1,51 @@ +/** + * Copyright © 2016-2024 ThingsBoard, Inc. + * + * 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.edqs.util; + +import jakarta.annotation.PostConstruct; +import jakarta.annotation.PreDestroy; +import lombok.Getter; +import org.rocksdb.Options; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.stereotype.Component; +import org.thingsboard.server.queue.edqs.InMemoryEdqsComponent; + +import java.nio.file.Files; +import java.nio.file.Path; + +@Component +@InMemoryEdqsComponent +public class EdqsRocksDb extends TbRocksDb { + + @Getter + private boolean isNew; + + public EdqsRocksDb(@Value("${queue.edqs.local.rocksdb_path:${java.io.tmpdir}/edqs-backup}") String path) { + super(path, new Options().setCreateIfMissing(true)); + } + + @PostConstruct + public void init() { + isNew = !Files.exists(Path.of(path)); + super.init(); + } + + @PreDestroy + public void close() { + super.close(); + } + +} 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 new file mode 100644 index 0000000000..1ad2634c1b --- /dev/null +++ b/common/edqs/src/main/java/org/thingsboard/server/edqs/util/RepositoryUtils.java @@ -0,0 +1,424 @@ +/** + * Copyright © 2016-2024 ThingsBoard, Inc. + * + * 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.edqs.util; + +import lombok.extern.slf4j.Slf4j; +import org.thingsboard.server.common.data.EntityType; +import org.thingsboard.server.common.data.StringUtils; +import org.thingsboard.server.common.data.group.EntityGroup; +import org.thingsboard.server.common.data.permission.MergedGroupTypePermissionInfo; +import org.thingsboard.server.common.data.permission.MergedUserPermissions; +import org.thingsboard.server.common.data.permission.Operation; +import org.thingsboard.server.common.data.permission.QueryContext; +import org.thingsboard.server.common.data.permission.Resource; +import org.thingsboard.server.common.data.query.BooleanFilterPredicate; +import org.thingsboard.server.common.data.query.ComplexFilterPredicate; +import org.thingsboard.server.common.data.query.EntitiesByGroupNameFilter; +import org.thingsboard.server.common.data.query.EntityCountQuery; +import org.thingsboard.server.common.data.query.EntityDataQuery; +import org.thingsboard.server.common.data.query.EntityDataSortOrder; +import org.thingsboard.server.common.data.query.EntityFilter; +import org.thingsboard.server.common.data.query.EntityGroupFilter; +import org.thingsboard.server.common.data.query.EntityKey; +import org.thingsboard.server.common.data.query.EntityKeyType; +import org.thingsboard.server.common.data.query.EntityKeyValueType; +import org.thingsboard.server.common.data.query.EntityListFilter; +import org.thingsboard.server.common.data.query.EntityNameFilter; +import org.thingsboard.server.common.data.query.EntityTypeFilter; +import org.thingsboard.server.common.data.query.FilterPredicateType; +import org.thingsboard.server.common.data.query.KeyFilter; +import org.thingsboard.server.common.data.query.KeyFilterPredicate; +import org.thingsboard.server.common.data.query.NumericFilterPredicate; +import org.thingsboard.server.common.data.query.RelationsQueryFilter; +import org.thingsboard.server.common.data.query.SingleEntityFilter; +import org.thingsboard.server.common.data.query.StringFilterPredicate; +import org.thingsboard.server.common.data.query.TsValue; +import org.thingsboard.server.common.data.util.CollectionsUtil; +import org.thingsboard.server.edqs.data.EntityData; +import org.thingsboard.server.edqs.data.dp.DataPoint; +import org.thingsboard.server.edqs.query.DataKey; +import org.thingsboard.server.edqs.query.EdqsCountQuery; +import org.thingsboard.server.edqs.query.EdqsDataQuery; +import org.thingsboard.server.edqs.query.EdqsFilter; +import org.thingsboard.server.edqs.query.EdqsQuery; +import org.thingsboard.server.edqs.query.SortableEntityData; +import org.thingsboard.server.edqs.repo.KeyDictionary; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.Comparator; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.regex.Pattern; +import java.util.stream.Stream; + +import static org.apache.commons.lang3.StringUtils.containsIgnoreCase; +import static org.thingsboard.server.common.data.StringUtils.equalsAny; +import static org.thingsboard.server.common.data.StringUtils.splitByCommaWithoutQuotes; +import static org.thingsboard.server.common.data.query.ComplexFilterPredicate.ComplexOperation.AND; +import static org.thingsboard.server.common.data.query.ComplexFilterPredicate.ComplexOperation.OR; + +@Slf4j +public class RepositoryUtils { + + public static final Comparator SORT_ASC = Comparator.comparing(SortableEntityData::getSortValue) + .thenComparing(sp -> sp.getId().toString()); + + public static final Comparator SORT_DESC = Comparator.comparing(SortableEntityData::getSortValue) + .thenComparing(sp -> sp.getId().toString()).reversed(); + + public static final MergedUserPermissions SYS_ADMIN_PERMISSIONS = new MergedUserPermissions(Collections.singletonMap(Resource.ALL, Set.of(Operation.READ, Operation.READ_ATTRIBUTES, Operation.READ_TELEMETRY)), Collections.emptyMap()); + public static final MergedUserPermissions ALL_READ_PERMISSIONS = new MergedUserPermissions( + Collections.singletonMap(Resource.ALL, Set.of(Operation.READ, Operation.READ_ATTRIBUTES, Operation.READ_TELEMETRY)), Collections.emptyMap()); + + public static EntityType resolveEntityType(EntityFilter entityFilter) { + return switch (entityFilter.getType()) { + case SINGLE_ENTITY -> ((SingleEntityFilter) entityFilter).getSingleEntity().getEntityType(); + case ENTITY_GROUP -> ((EntityGroupFilter) entityFilter).getGroupType(); + case ENTITY_LIST -> ((EntityListFilter) entityFilter).getEntityType(); + case ENTITY_NAME -> ((EntityNameFilter) entityFilter).getEntityType(); + case ENTITY_TYPE -> ((EntityTypeFilter) entityFilter).getEntityType(); + case ENTITY_GROUP_LIST, ENTITY_GROUP_NAME -> EntityType.ENTITY_GROUP; + case ENTITIES_BY_GROUP_NAME -> ((EntitiesByGroupNameFilter) entityFilter).getGroupType(); + case STATE_ENTITY_OWNER -> throw new RuntimeException("Not implemented!"); // TODO: implement + case ASSET_TYPE, ASSET_SEARCH_QUERY -> EntityType.ASSET; + case DEVICE_TYPE, DEVICE_SEARCH_QUERY -> EntityType.DEVICE; + case ENTITY_VIEW_TYPE, ENTITY_VIEW_SEARCH_QUERY -> EntityType.ENTITY_VIEW; + case EDGE_TYPE, EDGE_SEARCH_QUERY -> EntityType.EDGE; + case RELATIONS_QUERY -> { + RelationsQueryFilter rgf = (RelationsQueryFilter) entityFilter; + yield rgf.isMultiRoot() ? rgf.getMultiRootEntitiesType() : rgf.getRootEntity().getEntityType(); + } + case API_USAGE_STATE -> EntityType.API_USAGE_STATE; + case SCHEDULER_EVENT -> EntityType.SCHEDULER_EVENT; + }; + } + + public static boolean hasNoPermissionsForAllRelationQueryResources(Map permissionsMap) { + if (permissionsMap.get(Resource.resourceFromEntityType(EntityType.TENANT)).isHasGenericRead()) { + return false; + } + for (EntityType entityType : EntityGroup.groupTypes) { + if (permissionsMap.get(Resource.resourceFromEntityType(entityType)).isHasGenericRead() || + !permissionsMap.get(Resource.resourceFromEntityType(entityType)).getEntityGroupIds().isEmpty()) { + return false; + } + if (permissionsMap.get(Resource.groupResourceFromGroupType(entityType)).isHasGenericRead() || + !permissionsMap.get(Resource.groupResourceFromGroupType(entityType)).getEntityGroupIds().isEmpty()) { + return false; + } + } + return true; + } + + public static boolean customerUserIsTryingToAccessTenantEntity(QueryContext ctx, EntityFilter entityFilter) { + if (ctx.isTenantUser()) { + return false; + } else { + return switch (entityFilter.getType()) { + case SINGLE_ENTITY -> { + SingleEntityFilter seFilter = (SingleEntityFilter) entityFilter; + yield isSystemOrTenantEntity(seFilter.getSingleEntity().getEntityType()); + } + case ENTITY_LIST -> { + EntityListFilter elFilter = (EntityListFilter) entityFilter; + yield isSystemOrTenantEntity(elFilter.getEntityType()); + } + case ENTITY_NAME -> { + EntityNameFilter enFilter = (EntityNameFilter) entityFilter; + yield isSystemOrTenantEntity(enFilter.getEntityType()); + } + case ENTITY_TYPE -> { + EntityTypeFilter etFilter = (EntityTypeFilter) entityFilter; + yield isSystemOrTenantEntity(etFilter.getEntityType()); + } + default -> false; + }; + } + } + + private static boolean isSystemOrTenantEntity(EntityType entityType) { + return switch (entityType) { + case INTEGRATION, CONVERTER, DEVICE_PROFILE, ASSET_PROFILE, RULE_CHAIN, SCHEDULER_EVENT, TENANT, + TENANT_PROFILE, WIDGET_TYPE, WIDGETS_BUNDLE -> true; + default -> false; + }; + } + + public static EdqsDataQuery toNewQuery(EntityDataQuery oldQuery) { + var query = EdqsDataQuery.builder(); + query.page(oldQuery.getPageLink().getPage()); + query.pageSize(oldQuery.getPageLink().getPageSize()); + query.textSearch(oldQuery.getPageLink().getTextSearch()); + var sortOrder = oldQuery.getPageLink().getSortOrder(); + if (sortOrder != null && toNewKey(sortOrder.getKey()) != null) { + query.sortKey(toNewKey(sortOrder.getKey())); + query.sortDirection(sortOrder.getDirection()); + } else { + query.sortKey(new DataKey(EntityKeyType.ENTITY_FIELD, "createdTime", null)); + query.sortDirection(EntityDataSortOrder.Direction.DESC); + } + query.entityFilter(oldQuery.getEntityFilter()); + query.keyFilters(toKeyFilters(oldQuery.getKeyFilters())); + query.entityFields(toNewKeys(oldQuery.getEntityFields())); + query.latestValues(toNewKeys(oldQuery.getLatestValues())); + return query.build(); + } + + public static EdqsCountQuery toNewQuery(EntityCountQuery oldQuery) { + return EdqsCountQuery.builder() + .entityFilter(oldQuery.getEntityFilter()) + .hasKeyFilters(CollectionsUtil.isNotEmpty(oldQuery.getKeyFilters())) + .keyFilters(toKeyFilters(oldQuery.getKeyFilters())) + .build(); + } + + private static List toKeyFilters(List keyFilters) { + if (keyFilters == null || keyFilters.isEmpty()) { + return Collections.emptyList(); + } else { + List result = new ArrayList<>(); + for (KeyFilter entityFilter : keyFilters) { + var newKey = toNewKey(entityFilter.getKey()); + if (newKey != null) { + result.add(new EdqsFilter(newKey, entityFilter.getValueType(), entityFilter.getPredicate())); + } + } + return result; + } + } + + private static DataKey toNewKey(EntityKey entityKey) { + if (EntityKeyType.ENTITY_FIELD.equals(entityKey.getType())) { + return new DataKey(entityKey.getType(), entityKey.getKey(), null); + } + Integer keyId = KeyDictionary.get(entityKey.getKey()); + if (keyId != null) { + return new DataKey(entityKey.getType(), entityKey.getKey(), keyId); + } else { + log.warn("Missing dictionary key for {}", entityKey.getKey()); + return null; + } + } + + private static List toNewKeys(List entityKeys) { + if (entityKeys == null || entityKeys.isEmpty()) { + return Collections.emptyList(); + } else { + var result = new ArrayList(entityKeys.size()); + for (EntityKey entityKey : entityKeys) { + var newKey = toNewKey(entityKey); + if (newKey != null) { + result.add(newKey); + } + } + return result; + } + } + + public static boolean checkKeyFilters(EntityData entity, List keyFilters) { + for (EdqsFilter keyFilter : keyFilters) { + EntityKeyValueType valueType = keyFilter.valueType(); + if (valueType == null) { + valueType = switch (keyFilter.predicate().getType()) { + case STRING -> EntityKeyValueType.STRING; + case NUMERIC -> EntityKeyValueType.NUMERIC; + case BOOLEAN -> EntityKeyValueType.BOOLEAN; + default -> throw new IllegalStateException(); + }; + } + DataPoint dp = entity.getDataPoint(keyFilter.key(), null); + boolean checkResult = switch (valueType) { + case STRING -> { + String str = dp != null ? dp.valueToString() : null; + yield StringUtils.isEmpty(str) || checkKeyFilter(str, keyFilter.predicate()); + } + case BOOLEAN -> { + Boolean booleanValue = dp != null ? dp.getBool() : null; + yield booleanValue != null && checkKeyFilter(booleanValue, keyFilter.predicate()); + } + case DATE_TIME, NUMERIC -> { + Double doubleValue = dp != null ? dp.getDouble() : null; + yield doubleValue != null && checkKeyFilter(doubleValue, keyFilter.predicate()); + } + }; + if (!checkResult) { + return false; + } + } + return true; + } + + public static boolean checkKeyFilter(String value, KeyFilterPredicate keyFilterPredicate) { + if (keyFilterPredicate.getType() == FilterPredicateType.COMPLEX) { + return checkComplexKeyFilter(value, (ComplexFilterPredicate) keyFilterPredicate, RepositoryUtils::checkKeyFilter); + } + if (keyFilterPredicate.getType() != FilterPredicateType.STRING) { + throw new IllegalStateException("Not implemented"); + } + StringFilterPredicate predicate = (StringFilterPredicate) keyFilterPredicate; + String predicateValue = predicate.getValue().getValue(); + if (StringUtils.isEmpty(predicateValue)) { + return true; + } + if (predicate.isIgnoreCase()) { + predicateValue = predicateValue.toLowerCase(); + value = value.toLowerCase(); + } + return switch (predicate.getOperation()) { + case EQUAL -> value.equals(predicateValue); + case STARTS_WITH -> value.startsWith(predicateValue); + case ENDS_WITH -> value.endsWith(predicateValue); + case NOT_EQUAL -> !value.equals(predicateValue); + case CONTAINS -> value.contains(predicateValue); + case NOT_CONTAINS -> !value.contains(predicateValue); + case IN -> equalsAny(value, splitByCommaWithoutQuotes(predicateValue)); + case NOT_IN -> !equalsAny(value, splitByCommaWithoutQuotes(predicateValue)); + }; + } + + public static boolean checkKeyFilter(Double value, KeyFilterPredicate keyFilterPredicate) { + if (keyFilterPredicate.getType() == FilterPredicateType.COMPLEX) { + return checkComplexKeyFilter(value, (ComplexFilterPredicate) keyFilterPredicate, RepositoryUtils::checkKeyFilter); + } + if (keyFilterPredicate.getType() != FilterPredicateType.NUMERIC) { + throw new IllegalStateException("Not implemented"); + } + NumericFilterPredicate predicate = (NumericFilterPredicate) keyFilterPredicate; + Double predicateValue = predicate.getValue().getValue(); + return switch (predicate.getOperation()) { + case EQUAL -> value.equals(predicateValue); + case NOT_EQUAL -> !value.equals(predicateValue); + case GREATER -> value.compareTo(predicateValue) > 0; + case LESS -> value.compareTo(predicateValue) < 0; + case GREATER_OR_EQUAL -> value.compareTo(predicateValue) >= 0; + case LESS_OR_EQUAL -> value.compareTo(predicateValue) <= 0; + }; + } + + public static boolean checkKeyFilter(Boolean value, KeyFilterPredicate keyFilterPredicate) { + if (keyFilterPredicate.getType() == FilterPredicateType.COMPLEX) { + return checkComplexKeyFilter(value, (ComplexFilterPredicate) keyFilterPredicate, RepositoryUtils::checkKeyFilter); + } + if (keyFilterPredicate.getType() != FilterPredicateType.BOOLEAN) { + throw new IllegalStateException("Not implemented"); + } + BooleanFilterPredicate predicate = (BooleanFilterPredicate) keyFilterPredicate; + Boolean predicateValue = predicate.getValue().getValue(); + return switch (predicate.getOperation()) { + case EQUAL -> value.equals(predicateValue); + case NOT_EQUAL -> !value.equals(predicateValue); + }; + } + + public static boolean checkComplexKeyFilter(T value, ComplexFilterPredicate filterPredicates, + SimpleKeyFilter simpleKeyFilter) { + if (filterPredicates.getOperation() == AND) { + for (KeyFilterPredicate filterPredicate : filterPredicates.getPredicates()) { + if (!simpleKeyFilter.check(value, filterPredicate)) { + return false; + } + } + return true; + } else if (filterPredicates.getOperation() == OR) { + for (KeyFilterPredicate filterPredicate : filterPredicates.getPredicates()) { + if (simpleKeyFilter.check(value, filterPredicate)) { + return true; + } + } + return false; + } else { + return false; + } + } + + public static Pattern toSqlLikePattern(String nameFilter) { + if (StringUtils.isNotBlank(nameFilter)) { + boolean percentSymbolOnStart = nameFilter.startsWith("%"); + boolean percentSymbolOnEnd = nameFilter.endsWith("%"); + if (percentSymbolOnStart) { + nameFilter = nameFilter.substring(1); + } + if (percentSymbolOnEnd) { + nameFilter = nameFilter.substring(0, nameFilter.length() - 1); + } + if (percentSymbolOnStart || percentSymbolOnEnd) { + return Pattern.compile((percentSymbolOnStart ? ".*" : "") + Pattern.quote(nameFilter) + (percentSymbolOnEnd ? ".*" : ""), Pattern.CASE_INSENSITIVE); + } else { + return Pattern.compile(Pattern.quote(nameFilter) + ".*", Pattern.CASE_INSENSITIVE); + } + } + return null; + } + + @FunctionalInterface + public interface SimpleKeyFilter { + + boolean check(T value, KeyFilterPredicate predicate); + + } + + public static TsValue toTsValue(long ts, DataPoint dp) { + if (dp != null) { + return new TsValue(dp.getTs() > 0 ? dp.getTs() : ts, dp.valueToString()); + } else { + return new TsValue(ts, ""); + } + } + + public static String getSortValue(EntityData entity, DataKey sortKey) { + if (sortKey == null) { + return null; + } + switch (sortKey.type()) { + case ENTITY_FIELD -> { + return entity.getField(sortKey.key()); + } + case ATTRIBUTE, CLIENT_ATTRIBUTE, SHARED_ATTRIBUTE, SERVER_ATTRIBUTE -> { + var dp = entity.getAttr(sortKey.keyId(), sortKey.type()); + return dp != null ? dp.valueToString() : ""; + } + case TIME_SERIES -> { + var dp = entity.getTs(sortKey.keyId()); + return dp != null ? dp.valueToString() : ""; + } + default -> throw new IllegalStateException("toSortKey is not implemented for type: " + sortKey.type()); + } + } + + public static boolean checkFilters(EdqsQuery query, EntityData entity) { + if (entity == null || entity.getFields() == null) { + return false; // Entity was already removed or not arrived yet; + } + if (query.isHasKeyFilters() && !checkKeyFilters(entity, query.getKeyFilters())) { + return false; + } + if (query instanceof EdqsDataQuery dataQuery) { + return !dataQuery.isHasTextSearch() || checkTextSearch(entity, dataQuery); + } + return true; + } + + private static boolean checkTextSearch(EntityData entityData, EdqsDataQuery query) { + return Stream.concat(query.getEntityFields().stream(), query.getLatestValues().stream()) + .anyMatch(key -> { + DataPoint value = entityData.getDataPoint(key, null); + return value != null && containsIgnoreCase(value.valueToString(), query.getTextSearch()); + }); + } + +} diff --git a/common/edqs/src/main/java/org/thingsboard/server/edqs/util/TbRocksDb.java b/common/edqs/src/main/java/org/thingsboard/server/edqs/util/TbRocksDb.java new file mode 100644 index 0000000000..9b06a79a1f --- /dev/null +++ b/common/edqs/src/main/java/org/thingsboard/server/edqs/util/TbRocksDb.java @@ -0,0 +1,68 @@ +/** + * Copyright © 2016-2024 ThingsBoard, Inc. + * + * 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.edqs.util; + +import lombok.RequiredArgsConstructor; +import lombok.SneakyThrows; +import org.rocksdb.Options; +import org.rocksdb.RocksDB; +import org.rocksdb.RocksDBException; +import org.rocksdb.RocksIterator; + +import java.nio.charset.StandardCharsets; +import java.util.function.BiConsumer; + +@RequiredArgsConstructor +public class TbRocksDb { + + protected final String path; + private final Options options; + + private RocksDB db; + + static { + RocksDB.loadLibrary(); + } + + @SneakyThrows + public void init() { + db = RocksDB.open(options, path); + } + + public void put(String key, byte[] value) throws RocksDBException { + db.put(key.getBytes(StandardCharsets.UTF_8), value); + } + + public void forEach(BiConsumer processor) { + try (RocksIterator iterator = db.newIterator()) { + for (iterator.seekToFirst(); iterator.isValid(); iterator.next()) { + String key = new String(iterator.key(), StandardCharsets.UTF_8); + processor.accept(key, iterator.value()); + } + } + } + + public void delete(String key) throws RocksDBException { + db.delete(key.getBytes(StandardCharsets.UTF_8)); + } + + public void close() { + if (db != null) { + db.close(); + } + } + +} diff --git a/common/edqs/src/main/java/org/thingsboard/server/edqs/util/VersionsStore.java b/common/edqs/src/main/java/org/thingsboard/server/edqs/util/VersionsStore.java new file mode 100644 index 0000000000..de2c5a1955 --- /dev/null +++ b/common/edqs/src/main/java/org/thingsboard/server/edqs/util/VersionsStore.java @@ -0,0 +1,48 @@ +/** + * Copyright © 2016-2024 ThingsBoard, Inc. + * + * 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.edqs.util; + +import com.github.benmanes.caffeine.cache.Cache; +import com.github.benmanes.caffeine.cache.Caffeine; +import lombok.extern.slf4j.Slf4j; + +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicBoolean; + +@Slf4j +public class VersionsStore { + + private final Cache versions = Caffeine.newBuilder() + .expireAfterWrite(1, TimeUnit.HOURS) + .build(); + + public boolean isNew(String key, Long version) { + AtomicBoolean isNew = new AtomicBoolean(false); + versions.asMap().compute(key, (k, prevVersion) -> { + if (prevVersion == null || prevVersion < version) { + isNew.set(true); + return version; + } else { + if (version < prevVersion) { + log.info("[{}] Version {} is outdated, the latest is {}", key, version, prevVersion); + } + return prevVersion; + } + }); + return isNew.get(); + } + +} diff --git a/common/message/src/main/java/org/thingsboard/server/common/msg/edqs/EdqsService.java b/common/message/src/main/java/org/thingsboard/server/common/msg/edqs/EdqsService.java new file mode 100644 index 0000000000..9ca7d86e0a --- /dev/null +++ b/common/message/src/main/java/org/thingsboard/server/common/msg/edqs/EdqsService.java @@ -0,0 +1,47 @@ +/** + * Copyright © 2016-2024 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.common.msg.edqs; + +import com.google.common.util.concurrent.ListenableFuture; +import org.thingsboard.server.common.data.ObjectType; +import org.thingsboard.server.common.data.edqs.EdqsObject; +import org.thingsboard.server.common.data.edqs.ToCoreEdqsMsg; +import org.thingsboard.server.common.data.edqs.ToCoreEdqsRequest; +import org.thingsboard.server.common.data.edqs.query.EdqsRequest; +import org.thingsboard.server.common.data.edqs.query.EdqsResponse; +import org.thingsboard.server.common.data.id.CustomerId; +import org.thingsboard.server.common.data.id.EntityId; +import org.thingsboard.server.common.data.id.TenantId; + +public interface EdqsService { + + ListenableFuture processRequest(TenantId tenantId, CustomerId customerId, EdqsRequest request); + + boolean isApiEnabled(); + + void onUpdate(TenantId tenantId, EntityId entityId, Object entity); + + void onUpdate(TenantId tenantId, ObjectType objectType, EdqsObject object); + + void onDelete(TenantId tenantId, EntityId entityId); + + void onDelete(TenantId tenantId, ObjectType objectType, EdqsObject object); + + void processSystemRequest(ToCoreEdqsRequest request); + + void processSystemMsg(ToCoreEdqsMsg request); + +} diff --git a/common/message/src/main/java/org/thingsboard/server/common/msg/queue/ServiceType.java b/common/message/src/main/java/org/thingsboard/server/common/msg/queue/ServiceType.java index f3a0e47d09..ce2d31a745 100644 --- a/common/message/src/main/java/org/thingsboard/server/common/msg/queue/ServiceType.java +++ b/common/message/src/main/java/org/thingsboard/server/common/msg/queue/ServiceType.java @@ -26,7 +26,8 @@ public enum ServiceType { TB_RULE_ENGINE("TB Rule Engine"), TB_TRANSPORT("TB Transport"), JS_EXECUTOR("JS Executor"), - TB_VC_EXECUTOR("TB VC Executor"); + TB_VC_EXECUTOR("TB VC Executor"), + EDQS("TB Entity Data Query Service"); private final String label; diff --git a/common/message/src/main/java/org/thingsboard/server/common/msg/queue/TopicPartitionInfo.java b/common/message/src/main/java/org/thingsboard/server/common/msg/queue/TopicPartitionInfo.java index cbddc83b2b..552bdf50d6 100644 --- a/common/message/src/main/java/org/thingsboard/server/common/msg/queue/TopicPartitionInfo.java +++ b/common/message/src/main/java/org/thingsboard/server/common/msg/queue/TopicPartitionInfo.java @@ -30,26 +30,33 @@ public class TopicPartitionInfo { private final TenantId tenantId; private final Integer partition; @Getter + private final boolean useInternalPartition; + @Getter private final String fullTopicName; @Getter private final boolean myPartition; @Builder - public TopicPartitionInfo(String topic, TenantId tenantId, Integer partition, boolean myPartition) { + public TopicPartitionInfo(String topic, TenantId tenantId, Integer partition, boolean useInternalPartition, boolean myPartition) { this.topic = topic; this.tenantId = tenantId; this.partition = partition; + this.useInternalPartition = useInternalPartition; this.myPartition = myPartition; String tmp = topic; if (tenantId != null && !tenantId.isNullUid()) { tmp += ".isolated." + tenantId.getId().toString(); } - if (partition != null) { + if (partition != null && !useInternalPartition) { tmp += "." + partition; } this.fullTopicName = tmp; } + public TopicPartitionInfo(String topic, TenantId tenantId, Integer partition, boolean myPartition) { + this(topic, tenantId, partition, false, myPartition); + } + public TopicPartitionInfo newByTopic(String topic) { return new TopicPartitionInfo(topic, this.tenantId, this.partition, this.myPartition); } @@ -66,6 +73,14 @@ public class TopicPartitionInfo { return Optional.ofNullable(partition); } + public TopicPartitionInfo withTopic(String topic) { + return new TopicPartitionInfo(topic, this.tenantId, this.partition, this.useInternalPartition, this.myPartition); + } + + public TopicPartitionInfo withUseInternalPartition(boolean useInternalPartition) { + return new TopicPartitionInfo(this.topic, this.tenantId, this.partition, useInternalPartition, this.myPartition); + } + @Override public boolean equals(Object o) { if (this == o) return true; @@ -79,6 +94,7 @@ public class TopicPartitionInfo { @Override public int hashCode() { - return Objects.hash(fullTopicName); + return Objects.hash(fullTopicName, partition); } + } diff --git a/common/pom.xml b/common/pom.xml index 4c8f56cbce..9cb67ec989 100644 --- a/common/pom.xml +++ b/common/pom.xml @@ -49,6 +49,7 @@ edge-api version-control script + edqs diff --git a/common/proto/src/main/proto/queue.proto b/common/proto/src/main/proto/queue.proto index 228a4039d2..d21862d3e1 100644 --- a/common/proto/src/main/proto/queue.proto +++ b/common/proto/src/main/proto/queue.proto @@ -70,6 +70,7 @@ message ServiceInfo { repeated string transports = 6; SystemInfoProto systemInfo = 10; repeated string assignedTenantProfiles = 11; + string label = 12; } message SystemInfoProto { @@ -170,12 +171,33 @@ message AttributeValueProto { optional int64 version = 10; } +message AttributeKvProto { + int64 entityIdMSB = 1; + int64 entityIdLSB = 2; + EntityTypeProto entityType = 3; + AttributeScopeProto scope = 4; + string key = 5; + int64 version = 6; + int64 lastUpdateTs = 7; + KeyValueProto value = 8; +} + message TsKvProto { int64 ts = 1; KeyValueProto kv = 2; optional int64 version = 3; } +message LatestTsKvProto { + int64 entityIdMSB = 1; + int64 entityIdLSB = 2; + EntityTypeProto entityType = 3; + string key = 4; + int64 ts = 5; + int64 version = 6; + KeyValueProto value = 7; +} + message TsKvListProto { int64 ts = 1; repeated KeyValueProto kv = 2; @@ -482,6 +504,10 @@ message ImageCacheKeyProto { optional string publicResourceKey = 2; } +message ToEdqsCoreServiceMsg { + bytes value = 1; +} + message LwM2MRegistrationRequestMsg { string tenantId = 1; string endpoint = 2; @@ -1532,6 +1558,7 @@ message ToCoreNotificationMsg { ToEdgeSyncRequestMsgProto toEdgeSyncRequest = 11 [deprecated = true]; FromEdgeSyncResponseMsgProto fromEdgeSyncResponse = 12 [deprecated = true]; ResourceCacheInvalidateMsg resourceCacheInvalidateMsg = 13; + ToEdqsCoreServiceMsg toEdqsCoreServiceMsg = 17; RestApiCallResponseMsgProto restApiCallResponseMsg = 50; } @@ -1660,3 +1687,33 @@ message HousekeeperTaskProto { int32 attempt = 50; repeated string errors = 51; } + +message ToEdqsMsg { + int64 tenantIdMSB = 1; + int64 tenantIdLSB = 2; + int64 customerIdMSB = 3; + int64 customerIdLSB = 4; + int64 ts = 5; + EdqsEventMsg eventMsg = 6; + EdqsRequestMsg requestMsg = 7; +} + +message FromEdqsMsg { + EdqsResponseMsg responseMsg = 1; +} + +message EdqsEventMsg { + string key = 1; + string objectType = 2; + bytes data = 3; + string eventType = 4; + optional int64 version = 5; +} + +message EdqsRequestMsg { + string value = 1; +} + +message EdqsResponseMsg { + string value = 1; +} diff --git a/common/queue/pom.xml b/common/queue/pom.xml index 44e2239e05..99ad24575e 100644 --- a/common/queue/pom.xml +++ b/common/queue/pom.xml @@ -116,6 +116,10 @@ org.apache.curator curator-recipes + + org.xerial.snappy + snappy-java + org.springframework.boot spring-boot-starter-test diff --git a/common/queue/src/main/java/org/thingsboard/server/queue/common/AbstractTbQueueConsumerTemplate.java b/common/queue/src/main/java/org/thingsboard/server/queue/common/AbstractTbQueueConsumerTemplate.java index 9513565ca1..88aa2a233f 100644 --- a/common/queue/src/main/java/org/thingsboard/server/queue/common/AbstractTbQueueConsumerTemplate.java +++ b/common/queue/src/main/java/org/thingsboard/server/queue/common/AbstractTbQueueConsumerTemplate.java @@ -95,9 +95,8 @@ public abstract class AbstractTbQueueConsumerTemplate i partitions = subscribeQueue.poll(); } if (!subscribed) { - List topicNames = getFullTopicNames(); - log.info("Subscribing to topics {}", topicNames); - doSubscribe(topicNames); + log.info("Subscribing to topics {}", getFullTopicNames()); + doSubscribe(partitions); subscribed = true; } records = partitions.isEmpty() ? emptyList() : doPoll(durationInMillis); @@ -187,7 +186,7 @@ public abstract class AbstractTbQueueConsumerTemplate i abstract protected T decode(R record) throws IOException; - abstract protected void doSubscribe(List topicNames); + abstract protected void doSubscribe(Set partitions); abstract protected void doCommit(); @@ -198,7 +197,10 @@ public abstract class AbstractTbQueueConsumerTemplate i if (partitions == null) { return Collections.emptyList(); } - return partitions.stream().map(TopicPartitionInfo::getFullTopicName).collect(Collectors.toList()); + return partitions.stream() + .map(tpi -> tpi.getFullTopicName() + (tpi.isUseInternalPartition() ? + "[" + tpi.getPartition().orElse(-1) + "]" : "")) + .collect(Collectors.toList()); } protected boolean isLongPollingSupported() { diff --git a/common/queue/src/main/java/org/thingsboard/server/queue/common/DefaultTbQueueRequestTemplate.java b/common/queue/src/main/java/org/thingsboard/server/queue/common/DefaultTbQueueRequestTemplate.java index 950519b098..a2bdde6d66 100644 --- a/common/queue/src/main/java/org/thingsboard/server/queue/common/DefaultTbQueueRequestTemplate.java +++ b/common/queue/src/main/java/org/thingsboard/server/queue/common/DefaultTbQueueRequestTemplate.java @@ -211,6 +211,15 @@ public class DefaultTbQueueRequestTemplate send(Request request, long requestTimeoutNs) { + return send(request, requestTimeoutNs, null); + } + + @Override + public ListenableFuture send(Request request, Integer partition) { + return send(request, this.maxRequestTimeoutNs, partition); + } + + private ListenableFuture send(Request request, long requestTimeoutNs, Integer partition) { if (pendingRequests.mappingCount() >= maxPendingRequests) { log.warn("Pending request map is full [{}]! Consider to increase maxPendingRequests or increase processing performance. Request is {}", maxPendingRequests, request); return Futures.immediateFailedFuture(new RuntimeException("Pending request map is full!")); @@ -227,7 +236,7 @@ public class DefaultTbQueueRequestTemplate future, ResponseMetaData responseMetaData) { + void sendToRequestTemplate(Request request, UUID requestId, Integer partition, SettableFuture future, ResponseMetaData responseMetaData) { log.trace("[{}] Sending request, key [{}], expTime [{}], request {}", requestId, request.getKey(), responseMetaData.expTime, request); if (messagesStats != null) { messagesStats.incrementTotal(); } - requestTemplate.send(TopicPartitionInfo.builder().topic(requestTemplate.getDefaultTopic()).build(), request, new TbQueueCallback() { + TopicPartitionInfo tpi = TopicPartitionInfo.builder() + .topic(requestTemplate.getDefaultTopic()) + .partition(partition) + .useInternalPartition(partition != null) + .build(); + requestTemplate.send(tpi, request, new TbQueueCallback() { @Override public void onSuccess(TbQueueMsgMetadata metadata) { if (messagesStats != null) { diff --git a/common/queue/src/main/java/org/thingsboard/server/queue/common/DefaultTbQueueResponseTemplate.java b/common/queue/src/main/java/org/thingsboard/server/queue/common/DefaultTbQueueResponseTemplate.java index 1042eeb280..78f2085397 100644 --- a/common/queue/src/main/java/org/thingsboard/server/queue/common/DefaultTbQueueResponseTemplate.java +++ b/common/queue/src/main/java/org/thingsboard/server/queue/common/DefaultTbQueueResponseTemplate.java @@ -28,6 +28,7 @@ import org.thingsboard.server.queue.TbQueueProducer; import org.thingsboard.server.queue.TbQueueResponseTemplate; import java.util.List; +import java.util.Set; import java.util.UUID; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.ConcurrentMap; @@ -77,9 +78,18 @@ public class DefaultTbQueueResponseTemplate handler) { - this.responseTemplate.init(); + public void subscribe() { requestTemplate.subscribe(); + } + + @Override + public void subscribe(Set partitions) { + requestTemplate.subscribe(partitions); + } + + @Override + public void launch(TbQueueHandler handler) { + this.responseTemplate.init(); loopExecutor.submit(() -> { while (!stopped) { try { diff --git a/application/src/main/java/org/thingsboard/server/service/queue/consumer/MainQueueConsumerManager.java b/common/queue/src/main/java/org/thingsboard/server/queue/common/consumer/MainQueueConsumerManager.java similarity index 95% rename from application/src/main/java/org/thingsboard/server/service/queue/consumer/MainQueueConsumerManager.java rename to common/queue/src/main/java/org/thingsboard/server/queue/common/consumer/MainQueueConsumerManager.java index 6eb5c94c9b..2f44a9c4ae 100644 --- a/application/src/main/java/org/thingsboard/server/service/queue/consumer/MainQueueConsumerManager.java +++ b/common/queue/src/main/java/org/thingsboard/server/queue/common/consumer/MainQueueConsumerManager.java @@ -13,7 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package org.thingsboard.server.service.queue.consumer; +package org.thingsboard.server.queue.common.consumer; import lombok.Builder; import lombok.Getter; @@ -24,9 +24,6 @@ import org.thingsboard.server.common.msg.queue.TopicPartitionInfo; import org.thingsboard.server.queue.TbQueueConsumer; import org.thingsboard.server.queue.TbQueueMsg; import org.thingsboard.server.queue.discovery.QueueKey; -import org.thingsboard.server.service.queue.ruleengine.QueueEvent; -import org.thingsboard.server.service.queue.ruleengine.TbQueueConsumerManagerTask; -import org.thingsboard.server.service.queue.ruleengine.TbQueueConsumerTask; import java.util.Collection; import java.util.Collections; @@ -47,6 +44,7 @@ import java.util.stream.Collectors; @Slf4j public class MainQueueConsumerManager { + @Getter protected final QueueKey queueKey; @Getter protected C config; @@ -182,7 +180,7 @@ public class MainQueueConsumerManager partitions) { + public void doUpdate(Set partitions) { this.partitions = partitions; consumerWrapper.updatePartitions(partitions); } @@ -236,13 +234,19 @@ public class MainQueueConsumerManager consumerTask.awaitCompletion(timeoutSec)); log.debug("[{}] Unsubscribed and stopped consumers", queueKey); } private static String partitionsToString(Collection partitions) { - return partitions.stream().map(TopicPartitionInfo::getFullTopicName).collect(Collectors.joining(", ", "[", "]")); + return partitions.stream().map(tpi -> tpi.getFullTopicName() + (tpi.isUseInternalPartition() ? + "[" + tpi.getPartition().orElse(-1) + "]" : "")) + .collect(Collectors.joining(", ", "[", "]")); } public interface MsgPackProcessor { diff --git a/application/src/main/java/org/thingsboard/server/service/queue/ruleengine/QueueEvent.java b/common/queue/src/main/java/org/thingsboard/server/queue/common/consumer/QueueEvent.java similarity index 92% rename from application/src/main/java/org/thingsboard/server/service/queue/ruleengine/QueueEvent.java rename to common/queue/src/main/java/org/thingsboard/server/queue/common/consumer/QueueEvent.java index 9e5766b374..1a78cfc2ba 100644 --- a/application/src/main/java/org/thingsboard/server/service/queue/ruleengine/QueueEvent.java +++ b/common/queue/src/main/java/org/thingsboard/server/queue/common/consumer/QueueEvent.java @@ -13,7 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package org.thingsboard.server.service.queue.ruleengine; +package org.thingsboard.server.queue.common.consumer; import java.io.Serializable; diff --git a/application/src/main/java/org/thingsboard/server/service/queue/ruleengine/TbQueueConsumerManagerTask.java b/common/queue/src/main/java/org/thingsboard/server/queue/common/consumer/TbQueueConsumerManagerTask.java similarity index 96% rename from application/src/main/java/org/thingsboard/server/service/queue/ruleengine/TbQueueConsumerManagerTask.java rename to common/queue/src/main/java/org/thingsboard/server/queue/common/consumer/TbQueueConsumerManagerTask.java index e5821df68d..67bf370db0 100644 --- a/application/src/main/java/org/thingsboard/server/service/queue/ruleengine/TbQueueConsumerManagerTask.java +++ b/common/queue/src/main/java/org/thingsboard/server/queue/common/consumer/TbQueueConsumerManagerTask.java @@ -13,7 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package org.thingsboard.server.service.queue.ruleengine; +package org.thingsboard.server.queue.common.consumer; import lombok.AllArgsConstructor; import lombok.Getter; diff --git a/application/src/main/java/org/thingsboard/server/service/queue/ruleengine/TbQueueConsumerTask.java b/common/queue/src/main/java/org/thingsboard/server/queue/common/consumer/TbQueueConsumerTask.java similarity index 89% rename from application/src/main/java/org/thingsboard/server/service/queue/ruleengine/TbQueueConsumerTask.java rename to common/queue/src/main/java/org/thingsboard/server/queue/common/consumer/TbQueueConsumerTask.java index 5e672eb5c6..708e24fdac 100644 --- a/application/src/main/java/org/thingsboard/server/service/queue/ruleengine/TbQueueConsumerTask.java +++ b/common/queue/src/main/java/org/thingsboard/server/queue/common/consumer/TbQueueConsumerTask.java @@ -13,7 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package org.thingsboard.server.service.queue.ruleengine; +package org.thingsboard.server.queue.common.consumer; import lombok.Getter; import lombok.Setter; @@ -70,10 +70,18 @@ public class TbQueueConsumerTask { } public void awaitCompletion() { + awaitCompletion(30); + } + + public void awaitCompletion(int timeoutSec) { log.trace("[{}] Awaiting finish", key); if (isRunning()) { try { - task.get(30, TimeUnit.SECONDS); + if (timeoutSec > 0) { + task.get(timeoutSec, TimeUnit.SECONDS); + } else { + task.get(); + } log.trace("[{}] Awaited finish", key); } catch (Exception e) { log.warn("[{}] Failed to await for consumer to stop", key, e); diff --git a/common/queue/src/main/java/org/thingsboard/server/queue/discovery/DefaultTbServiceInfoProvider.java b/common/queue/src/main/java/org/thingsboard/server/queue/discovery/DefaultTbServiceInfoProvider.java index 1ceb52b3d6..64677009ff 100644 --- a/common/queue/src/main/java/org/thingsboard/server/queue/discovery/DefaultTbServiceInfoProvider.java +++ b/common/queue/src/main/java/org/thingsboard/server/queue/discovery/DefaultTbServiceInfoProvider.java @@ -28,6 +28,7 @@ import org.thingsboard.server.common.data.util.CollectionsUtil; import org.thingsboard.server.common.msg.queue.ServiceType; import org.thingsboard.server.gen.transport.TransportProtos; import org.thingsboard.server.gen.transport.TransportProtos.ServiceInfo; +import org.thingsboard.server.queue.edqs.EdqsConfig; import org.thingsboard.server.queue.util.AfterContextReady; import java.net.InetAddress; @@ -63,6 +64,9 @@ public class DefaultTbServiceInfoProvider implements TbServiceInfoProvider { @Value("${service.rule_engine.assigned_tenant_profiles:}") private Set assignedTenantProfiles; + @Autowired + private EdqsConfig edqsConfig; + @Autowired private ApplicationContext applicationContext; @@ -87,6 +91,11 @@ public class DefaultTbServiceInfoProvider implements TbServiceInfoProvider { if (!serviceTypes.contains(ServiceType.TB_RULE_ENGINE) || assignedTenantProfiles == null) { assignedTenantProfiles = Collections.emptySet(); } + if (serviceTypes.contains(ServiceType.EDQS)) { + if (StringUtils.isBlank(edqsConfig.getLabel())) { + edqsConfig.setLabel(serviceId); + } + } generateNewServiceInfoWithCurrentSystemInfo(); } @@ -123,6 +132,7 @@ public class DefaultTbServiceInfoProvider implements TbServiceInfoProvider { if (CollectionsUtil.isNotEmpty(assignedTenantProfiles)) { builder.addAllAssignedTenantProfiles(assignedTenantProfiles.stream().map(UUID::toString).collect(Collectors.toList())); } + builder.setLabel(edqsConfig.getLabel()); return serviceInfo = builder.build(); } diff --git a/common/queue/src/main/java/org/thingsboard/server/queue/discovery/HashPartitionService.java b/common/queue/src/main/java/org/thingsboard/server/queue/discovery/HashPartitionService.java index 37e519e3f2..165a54b300 100644 --- a/common/queue/src/main/java/org/thingsboard/server/queue/discovery/HashPartitionService.java +++ b/common/queue/src/main/java/org/thingsboard/server/queue/discovery/HashPartitionService.java @@ -20,6 +20,7 @@ import com.google.common.hash.Hashing; import jakarta.annotation.PostConstruct; import lombok.Data; import lombok.extern.slf4j.Slf4j; +import org.jetbrains.annotations.NotNull; import org.springframework.beans.factory.annotation.Value; import org.springframework.context.ApplicationEventPublisher; import org.springframework.stereotype.Service; @@ -70,6 +71,8 @@ public class HashPartitionService implements PartitionService { private String edgeTopic; @Value("${queue.edge.partitions:10}") private Integer edgePartitions; + @Value("${queue.edqs.partitions:12}") + private Integer edqsPartitions; @Value("${queue.partitions.hash_function_name:murmur3_128}") private String hashFunctionName; @@ -123,6 +126,10 @@ public class HashPartitionService implements PartitionService { QueueKey edgeKey = coreKey.withQueueName(EDGE_QUEUE_NAME); partitionSizesMap.put(edgeKey, edgePartitions); partitionTopicsMap.put(edgeKey, edgeTopic); + + QueueKey edqsKey = new QueueKey(ServiceType.EDQS); + partitionSizesMap.put(edqsKey, edqsPartitions); + partitionTopicsMap.put(edqsKey, "edqs"); // placeholder, not used } @AfterStartUp(order = AfterStartUp.QUEUE_INFO_INITIALIZATION) @@ -211,7 +218,7 @@ public class HashPartitionService implements PartitionService { }); if (serviceInfoProvider.isService(ServiceType.TB_RULE_ENGINE)) { publishPartitionChangeEvent(ServiceType.TB_RULE_ENGINE, queueKeys.stream() - .collect(Collectors.toMap(k -> k, k -> Collections.emptySet()))); + .collect(Collectors.toMap(k -> k, k -> Collections.emptySet())), Collections.emptyMap()); } } @@ -354,6 +361,11 @@ public class HashPartitionService implements PartitionService { } } + @Override + public boolean isSystemPartitionMine(ServiceType serviceType) { + return isMyPartition(serviceType, TenantId.SYS_TENANT_ID, TenantId.SYS_TENANT_ID); + } + @Override public synchronized void recalculatePartitions(ServiceInfo currentService, List otherServices) { log.info("Recalculating partitions"); @@ -374,9 +386,9 @@ public class HashPartitionService implements PartitionService { partitionSizesMap.forEach((queueKey, size) -> { for (int i = 0; i < size; i++) { try { - ServiceInfo serviceInfo = resolveByPartitionIdx(queueServicesMap.get(queueKey), queueKey, i, responsibleServices); - log.trace("Server responsible for {}[{}] - {}", queueKey, i, serviceInfo != null ? serviceInfo.getServiceId() : "none"); - if (currentService.equals(serviceInfo)) { + List services = resolveByPartitionIdx(queueServicesMap.get(queueKey), queueKey, i, responsibleServices); + log.trace("Server responsible for {}[{}] - {}", queueKey, i, services); + if (services.contains(currentService)) { newPartitions.computeIfAbsent(queueKey, key -> new ArrayList<>()).add(i); } } catch (Exception e) { @@ -390,6 +402,7 @@ public class HashPartitionService implements PartitionService { myPartitions = newPartitions; Map> changedPartitionsMap = new HashMap<>(); + Map> oldPartitionsMap = new HashMap<>(); Set removed = new HashSet<>(); oldPartitions.forEach((queueKey, partitions) -> { @@ -410,19 +423,16 @@ public class HashPartitionService implements PartitionService { myPartitions.forEach((queueKey, partitions) -> { if (!partitions.equals(oldPartitions.get(queueKey))) { - Set tpiList = partitions.stream() - .map(partition -> buildTopicPartitionInfo(queueKey, partition)) - .collect(Collectors.toSet()); - changedPartitionsMap.put(queueKey, tpiList); + changedPartitionsMap.put(queueKey, toTpiList(queueKey, partitions)); + oldPartitionsMap.put(queueKey, toTpiList(queueKey, oldPartitions.get(queueKey))); } }); if (!changedPartitionsMap.isEmpty()) { - Map>> partitionsByServiceType = new HashMap<>(); - changedPartitionsMap.forEach((queueKey, partitions) -> { - partitionsByServiceType.computeIfAbsent(queueKey.getType(), serviceType -> new HashMap<>()) - .put(queueKey, partitions); - }); - partitionsByServiceType.forEach(this::publishPartitionChangeEvent); + changedPartitionsMap.entrySet().stream() + .collect(Collectors.groupingBy(entry -> entry.getKey().getType(), Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue))) + .forEach((serviceType, partitionsMap) -> { + publishPartitionChangeEvent(serviceType, partitionsMap, oldPartitionsMap); + }); } if (currentOtherServices == null) { @@ -454,13 +464,15 @@ public class HashPartitionService implements PartitionService { applicationEventPublisher.publishEvent(new ServiceListChangedEvent(otherServices, currentService)); } - private void publishPartitionChangeEvent(ServiceType serviceType, Map> partitionsMap) { - log.info("Partitions changed: {}", System.lineSeparator() + partitionsMap.entrySet().stream() + private void publishPartitionChangeEvent(ServiceType serviceType, + Map> newPartitions, + Map> oldPartitions) { + log.info("Partitions changed: {}", System.lineSeparator() + newPartitions.entrySet().stream() .map(entry -> "[" + entry.getKey() + "] - [" + entry.getValue().stream() .map(tpi -> tpi.getPartition().orElse(-1).toString()).sorted() .collect(Collectors.joining(", ")) + "]") .collect(Collectors.joining(System.lineSeparator()))); - PartitionChangeEvent event = new PartitionChangeEvent(this, serviceType, partitionsMap); + PartitionChangeEvent event = new PartitionChangeEvent(this, serviceType, newPartitions, oldPartitions); try { applicationEventPublisher.publishEvent(event); } catch (Exception e) { @@ -468,6 +480,15 @@ public class HashPartitionService implements PartitionService { } } + private Set toTpiList(QueueKey queueKey, List partitions) { + if (partitions == null) { + return null; + } + return partitions.stream() + .map(partition -> buildTopicPartitionInfo(queueKey, partition)) + .collect(Collectors.toSet()); + } + @Override public Set getAllServiceIds(ServiceType serviceType) { return getAllServices(serviceType).stream().map(ServiceInfo::getServiceId).collect(Collectors.toSet()); @@ -598,6 +619,8 @@ public class HashPartitionService implements PartitionService { queueServiceList.computeIfAbsent(new QueueKey(serviceType).withQueueName(EDGE_QUEUE_NAME), key -> new ArrayList<>()).add(instance); } else if (ServiceType.TB_VC_EXECUTOR.equals(serviceType)) { queueServiceList.computeIfAbsent(new QueueKey(serviceType), key -> new ArrayList<>()).add(instance); + } else if (ServiceType.EDQS.equals(serviceType)) { + queueServiceList.computeIfAbsent(new QueueKey(serviceType), key -> new ArrayList<>()).add(instance); } } @@ -606,10 +629,11 @@ public class HashPartitionService implements PartitionService { } } - protected ServiceInfo resolveByPartitionIdx(List servers, QueueKey queueKey, int partition, - Map> responsibleServices) { + @NotNull + protected List resolveByPartitionIdx(List servers, QueueKey queueKey, int partition, + Map> responsibleServices) { if (servers == null || servers.isEmpty()) { - return null; + return Collections.emptyList(); } TenantId tenantId = queueKey.getTenantId(); @@ -637,15 +661,21 @@ public class HashPartitionService implements PartitionService { responsibleServices.put(profileId, responsible); } if (responsible.isEmpty()) { - return null; + return Collections.emptyList(); } servers = responsible; } int hash = hash(tenantId.getId()); - return servers.get(Math.abs((hash + partition) % servers.size())); + ServiceInfo server = servers.get(Math.abs((hash + partition) % servers.size())); + return server != null ? List.of(server) : Collections.emptyList(); + } else if (queueKey.getType() == ServiceType.EDQS) { + List> sets = servers.stream().collect(Collectors.groupingBy(ServiceInfo::getLabel)) + .entrySet().stream().sorted(Map.Entry.comparingByKey()).map(Map.Entry::getValue).toList(); + return sets.get(partition % sets.size()); } else { - return servers.get(partition % servers.size()); + ServiceInfo server = servers.get(partition % servers.size()); + return server != null ? List.of(server) : Collections.emptyList(); } } diff --git a/common/queue/src/main/java/org/thingsboard/server/queue/discovery/PartitionService.java b/common/queue/src/main/java/org/thingsboard/server/queue/discovery/PartitionService.java index b5744981bd..a95456b5e5 100644 --- a/common/queue/src/main/java/org/thingsboard/server/queue/discovery/PartitionService.java +++ b/common/queue/src/main/java/org/thingsboard/server/queue/discovery/PartitionService.java @@ -41,6 +41,8 @@ public interface PartitionService { boolean isMyPartition(ServiceType serviceType, TenantId tenantId, EntityId entityId); + boolean isSystemPartitionMine(ServiceType serviceType); + List getMyPartitions(QueueKey queueKey); /** @@ -61,8 +63,6 @@ public interface PartitionService { Set getOtherServices(ServiceType serviceType); - int resolvePartitionIndex(UUID entityId, int partitions); - void evictTenantInfo(TenantId tenantId); int countTransportsByType(String type); @@ -75,4 +75,6 @@ public interface PartitionService { boolean isManagedByCurrentService(TenantId tenantId); + int resolvePartitionIndex(UUID entityId, int partitions); + } diff --git a/common/queue/src/main/java/org/thingsboard/server/queue/discovery/ZkDiscoveryService.java b/common/queue/src/main/java/org/thingsboard/server/queue/discovery/ZkDiscoveryService.java index 3a8d365433..8107b7c3eb 100644 --- a/common/queue/src/main/java/org/thingsboard/server/queue/discovery/ZkDiscoveryService.java +++ b/common/queue/src/main/java/org/thingsboard/server/queue/discovery/ZkDiscoveryService.java @@ -19,6 +19,7 @@ import com.google.protobuf.InvalidProtocolBufferException; import com.google.protobuf.ProtocolStringList; import jakarta.annotation.PostConstruct; import jakarta.annotation.PreDestroy; +import lombok.Getter; import lombok.SneakyThrows; import lombok.extern.slf4j.Slf4j; import org.apache.curator.framework.CuratorFramework; @@ -68,6 +69,7 @@ public class ZkDiscoveryService implements DiscoveryService, PathChildrenCacheLi private Integer zkConnectionTimeout; @Value("${zk.session_timeout_ms}") private Integer zkSessionTimeout; + @Getter @Value("${zk.zk_dir}") private String zkDir; @Value("${zk.recalculate_delay:0}") @@ -80,6 +82,7 @@ public class ZkDiscoveryService implements DiscoveryService, PathChildrenCacheLi private final PartitionService partitionService; private ScheduledExecutorService zkExecutorService; + @Getter private CuratorFramework client; private PathChildrenCache cache; private String nodePath; diff --git a/common/queue/src/main/java/org/thingsboard/server/queue/discovery/event/PartitionChangeEvent.java b/common/queue/src/main/java/org/thingsboard/server/queue/discovery/event/PartitionChangeEvent.java index 88ceb4aa08..e0e7db80a7 100644 --- a/common/queue/src/main/java/org/thingsboard/server/queue/discovery/event/PartitionChangeEvent.java +++ b/common/queue/src/main/java/org/thingsboard/server/queue/discovery/event/PartitionChangeEvent.java @@ -23,6 +23,7 @@ import org.thingsboard.server.common.msg.queue.TopicPartitionInfo; import org.thingsboard.server.queue.discovery.QueueKey; import java.io.Serial; +import java.util.Collection; import java.util.Map; import java.util.Set; import java.util.stream.Collectors; @@ -36,12 +37,17 @@ public class PartitionChangeEvent extends TbApplicationEvent { @Getter private final ServiceType serviceType; @Getter - private final Map> partitionsMap; + private final Map> newPartitions; + @Getter + private final Map> oldPartitions; - public PartitionChangeEvent(Object source, ServiceType serviceType, Map> partitionsMap) { + public PartitionChangeEvent(Object source, ServiceType serviceType, + Map> newPartitions, + Map> oldPartitions) { super(source); this.serviceType = serviceType; - this.partitionsMap = partitionsMap; + this.newPartitions = newPartitions; + this.oldPartitions = oldPartitions; } public Set getCorePartitions() { @@ -52,11 +58,16 @@ public class PartitionChangeEvent extends TbApplicationEvent { return getPartitionsByServiceTypeAndQueueName(ServiceType.TB_CORE, DataConstants.EDGE_QUEUE_NAME); } + public Set getPartitions() { + return newPartitions.values().stream().flatMap(Collection::stream).collect(Collectors.toSet()); + } + private Set getPartitionsByServiceTypeAndQueueName(ServiceType serviceType, String queueName) { - return partitionsMap.entrySet() + return newPartitions.entrySet() .stream() .filter(entry -> serviceType.equals(entry.getKey().getType()) && queueName.equals(entry.getKey().getQueueName())) .flatMap(entry -> entry.getValue().stream()) .collect(Collectors.toSet()); } + } diff --git a/common/queue/src/main/java/org/thingsboard/server/queue/edqs/EdqsComponent.java b/common/queue/src/main/java/org/thingsboard/server/queue/edqs/EdqsComponent.java new file mode 100644 index 0000000000..838f9b4aa2 --- /dev/null +++ b/common/queue/src/main/java/org/thingsboard/server/queue/edqs/EdqsComponent.java @@ -0,0 +1,29 @@ +/** + * Copyright © 2016-2024 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.queue.edqs; + +import org.springframework.boot.autoconfigure.condition.ConditionalOnExpression; + +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; + +@Retention(RetentionPolicy.RUNTIME) +// TODO: tb-core ? +@ConditionalOnExpression("'${queue.edqs.sync_enabled:true}'=='true' && ('${service.type:null}'=='edqs' || " + + "(('${service.type:null}'=='monolith' || '${service.type:null}'=='tb-core') && " + + "'${queue.edqs.mode:null}'=='local'))") +public @interface EdqsComponent { +} diff --git a/common/queue/src/main/java/org/thingsboard/server/queue/edqs/EdqsConfig.java b/common/queue/src/main/java/org/thingsboard/server/queue/edqs/EdqsConfig.java new file mode 100644 index 0000000000..f35827a8ce --- /dev/null +++ b/common/queue/src/main/java/org/thingsboard/server/queue/edqs/EdqsConfig.java @@ -0,0 +1,55 @@ +/** + * Copyright © 2016-2024 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.queue.edqs; + +import lombok.Data; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.stereotype.Component; + +@Component +@Data +public class EdqsConfig { + + @Value("${queue.edqs.partitions:12}") + private int partitions; + @Value("${service.edqs.label:}") + private String label; + @Value("#{'${queue.edqs.partitioning_strategy:tenant}'.toUpperCase()}") + private EdqsPartitioningStrategy partitioningStrategy; + + @Value("${queue.edqs.requests_topic:edqs.requests}") + private String requestsTopic; + @Value("${queue.edqs.responses_topic:edqs.responses}") + private String responsesTopic; + @Value("${queue.edqs.poll_interval:125}") + private long pollInterval; + @Value("${queue.edqs.max_pending_requests:10000}") + private int maxPendingRequests; + @Value("${queue.edqs.max_request_timeout:10000}") + private int maxRequestTimeout; + + public String getLabel() { + if (partitioningStrategy == EdqsPartitioningStrategy.NONE) { + label = "all"; // single set for all instances, so that each instance has all partitions + } + return label; + } + + public enum EdqsPartitioningStrategy { + TENANT, NONE + } + +} diff --git a/common/queue/src/main/java/org/thingsboard/server/queue/edqs/EdqsQueue.java b/common/queue/src/main/java/org/thingsboard/server/queue/edqs/EdqsQueue.java new file mode 100644 index 0000000000..c773ea4e93 --- /dev/null +++ b/common/queue/src/main/java/org/thingsboard/server/queue/edqs/EdqsQueue.java @@ -0,0 +1,36 @@ +/** + * Copyright © 2016-2024 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.queue.edqs; + +import lombok.Getter; + +@Getter +public enum EdqsQueue { + + EVENTS("edqs.events", false, false), + STATE("edqs.state", true, true); + + private final String topic; + private final boolean readFromBeginning; // read from the beginning of the topic, instead of the latest committed offset + private final boolean stopWhenRead; // stop consuming when reached an empty msg pack + + EdqsQueue(String topic, boolean readFromBeginning, boolean stopWhenRead) { + this.topic = topic; + this.readFromBeginning = readFromBeginning; + this.stopWhenRead = stopWhenRead; + } + +} diff --git a/common/queue/src/main/java/org/thingsboard/server/queue/edqs/EdqsQueueFactory.java b/common/queue/src/main/java/org/thingsboard/server/queue/edqs/EdqsQueueFactory.java new file mode 100644 index 0000000000..dc4a9d645c --- /dev/null +++ b/common/queue/src/main/java/org/thingsboard/server/queue/edqs/EdqsQueueFactory.java @@ -0,0 +1,35 @@ +/** + * Copyright © 2016-2024 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.queue.edqs; + +import org.thingsboard.server.gen.transport.TransportProtos.FromEdqsMsg; +import org.thingsboard.server.gen.transport.TransportProtos.ToEdqsMsg; +import org.thingsboard.server.queue.TbQueueConsumer; +import org.thingsboard.server.queue.TbQueueProducer; +import org.thingsboard.server.queue.TbQueueResponseTemplate; +import org.thingsboard.server.queue.common.TbProtoQueueMsg; + +public interface EdqsQueueFactory { + + TbQueueConsumer> createEdqsMsgConsumer(EdqsQueue queue); + + TbQueueConsumer> createEdqsMsgConsumer(EdqsQueue queue, String group); + + TbQueueProducer> createEdqsMsgProducer(EdqsQueue queue); + + TbQueueResponseTemplate, TbProtoQueueMsg> createEdqsResponseTemplate(); + +} diff --git a/common/queue/src/main/java/org/thingsboard/server/queue/edqs/InMemoryEdqsComponent.java b/common/queue/src/main/java/org/thingsboard/server/queue/edqs/InMemoryEdqsComponent.java new file mode 100644 index 0000000000..5055787fde --- /dev/null +++ b/common/queue/src/main/java/org/thingsboard/server/queue/edqs/InMemoryEdqsComponent.java @@ -0,0 +1,26 @@ +/** + * Copyright © 2016-2024 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.queue.edqs; + +import org.springframework.boot.autoconfigure.condition.ConditionalOnExpression; + +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; + +@Retention(RetentionPolicy.RUNTIME) +@ConditionalOnExpression("'${queue.edqs.sync_enabled:true}'=='true' && '${service.type:null}'=='monolith' && '${queue.edqs.mode:null}'=='local' && '${queue.type:null}'=='in-memory'") +public @interface InMemoryEdqsComponent { +} diff --git a/common/queue/src/main/java/org/thingsboard/server/queue/edqs/InMemoryEdqsQueueFactory.java b/common/queue/src/main/java/org/thingsboard/server/queue/edqs/InMemoryEdqsQueueFactory.java new file mode 100644 index 0000000000..20b4beadcf --- /dev/null +++ b/common/queue/src/main/java/org/thingsboard/server/queue/edqs/InMemoryEdqsQueueFactory.java @@ -0,0 +1,77 @@ +/** + * Copyright © 2016-2024 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.queue.edqs; + +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Component; +import org.thingsboard.common.util.ThingsBoardExecutors; +import org.thingsboard.server.common.stats.DummyMessagesStats; +import org.thingsboard.server.gen.transport.TransportProtos.FromEdqsMsg; +import org.thingsboard.server.gen.transport.TransportProtos.ToEdqsMsg; +import org.thingsboard.server.queue.TbQueueConsumer; +import org.thingsboard.server.queue.TbQueueProducer; +import org.thingsboard.server.queue.TbQueueResponseTemplate; +import org.thingsboard.server.queue.common.DefaultTbQueueResponseTemplate; +import org.thingsboard.server.queue.common.TbProtoQueueMsg; +import org.thingsboard.server.queue.memory.InMemoryStorage; +import org.thingsboard.server.queue.memory.InMemoryTbQueueConsumer; +import org.thingsboard.server.queue.memory.InMemoryTbQueueProducer; + +@Component +@InMemoryEdqsComponent +@RequiredArgsConstructor +public class InMemoryEdqsQueueFactory implements EdqsQueueFactory { + + private final InMemoryStorage storage; + private final EdqsConfig edqsConfig; + + @Override + public TbQueueConsumer> createEdqsMsgConsumer(EdqsQueue queue) { + if (queue == EdqsQueue.STATE) { + throw new UnsupportedOperationException(); + } + return new InMemoryTbQueueConsumer<>(storage, queue.getTopic()); + } + + @Override + public TbQueueConsumer> createEdqsMsgConsumer(EdqsQueue queue, String group) { + return createEdqsMsgConsumer(queue); + } + + @Override + public TbQueueProducer> createEdqsMsgProducer(EdqsQueue queue) { + if (queue == EdqsQueue.STATE) { + throw new UnsupportedOperationException(); + } + return new InMemoryTbQueueProducer<>(storage, queue.getTopic()); + } + + @Override + public TbQueueResponseTemplate, TbProtoQueueMsg> createEdqsResponseTemplate() { + TbQueueConsumer> requestConsumer = new InMemoryTbQueueConsumer<>(storage, edqsConfig.getRequestsTopic()); + TbQueueProducer> responseProducer = new InMemoryTbQueueProducer<>(storage, edqsConfig.getResponsesTopic()); + return DefaultTbQueueResponseTemplate., TbProtoQueueMsg>builder() + .requestTemplate(requestConsumer) + .responseTemplate(responseProducer) + .maxPendingRequests(edqsConfig.getMaxPendingRequests()) + .requestTimeout(edqsConfig.getMaxRequestTimeout()) + .pollInterval(edqsConfig.getPollInterval()) + .stats(new DummyMessagesStats()) // FIXME + .executor(ThingsBoardExecutors.newWorkStealingPool(5, "edqs")) + .build(); + } + +} diff --git a/common/queue/src/main/java/org/thingsboard/server/queue/edqs/KafkaEdqsComponent.java b/common/queue/src/main/java/org/thingsboard/server/queue/edqs/KafkaEdqsComponent.java new file mode 100644 index 0000000000..9f112e7f59 --- /dev/null +++ b/common/queue/src/main/java/org/thingsboard/server/queue/edqs/KafkaEdqsComponent.java @@ -0,0 +1,28 @@ +/** + * Copyright © 2016-2024 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.queue.edqs; + +import org.springframework.boot.autoconfigure.condition.ConditionalOnExpression; + +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; + +@Retention(RetentionPolicy.RUNTIME) +@ConditionalOnExpression("'${queue.edqs.sync_enabled:true}'=='true' && ('${service.type:null}'=='edqs' || " + + "(('${service.type:null}'=='monolith' || '${service.type:null}'=='tb-core') && " + + "'${queue.edqs.mode:null}'=='local' && '${queue.type:null}'=='kafka'))") +public @interface KafkaEdqsComponent { +} diff --git a/common/queue/src/main/java/org/thingsboard/server/queue/edqs/KafkaEdqsQueueFactory.java b/common/queue/src/main/java/org/thingsboard/server/queue/edqs/KafkaEdqsQueueFactory.java new file mode 100644 index 0000000000..4b599670a1 --- /dev/null +++ b/common/queue/src/main/java/org/thingsboard/server/queue/edqs/KafkaEdqsQueueFactory.java @@ -0,0 +1,122 @@ +/** + * Copyright © 2016-2024 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.queue.edqs; + +import org.springframework.stereotype.Component; +import org.thingsboard.common.util.ThingsBoardExecutors; +import org.thingsboard.server.common.stats.DummyMessagesStats; +import org.thingsboard.server.gen.transport.TransportProtos; +import org.thingsboard.server.gen.transport.TransportProtos.FromEdqsMsg; +import org.thingsboard.server.gen.transport.TransportProtos.ToEdqsMsg; +import org.thingsboard.server.queue.TbQueueConsumer; +import org.thingsboard.server.queue.TbQueueProducer; +import org.thingsboard.server.queue.TbQueueResponseTemplate; +import org.thingsboard.server.queue.common.DefaultTbQueueResponseTemplate; +import org.thingsboard.server.queue.common.TbProtoQueueMsg; +import org.thingsboard.server.queue.discovery.TbServiceInfoProvider; +import org.thingsboard.server.queue.kafka.TbKafkaAdmin; +import org.thingsboard.server.queue.kafka.TbKafkaConsumerStatsService; +import org.thingsboard.server.queue.kafka.TbKafkaConsumerTemplate; +import org.thingsboard.server.queue.kafka.TbKafkaProducerTemplate; +import org.thingsboard.server.queue.kafka.TbKafkaSettings; +import org.thingsboard.server.queue.kafka.TbKafkaTopicConfigs; + +import java.util.concurrent.atomic.AtomicInteger; + +@Component +@KafkaEdqsComponent +public class KafkaEdqsQueueFactory implements EdqsQueueFactory { + + private final TbKafkaSettings kafkaSettings; + private final TbKafkaAdmin edqsEventsAdmin; + private final TbKafkaAdmin edqsRequestsAdmin; + private final TbKafkaAdmin edqsStateAdmin; + private final EdqsConfig edqsConfig; + private final TbServiceInfoProvider serviceInfoProvider; + private final TbKafkaConsumerStatsService consumerStatsService; + + private final AtomicInteger consumerCounter = new AtomicInteger(); + + public KafkaEdqsQueueFactory(TbKafkaSettings kafkaSettings, TbKafkaTopicConfigs topicConfigs, + EdqsConfig edqsConfig, TbServiceInfoProvider serviceInfoProvider, + TbKafkaConsumerStatsService consumerStatsService) { + this.edqsEventsAdmin = new TbKafkaAdmin(kafkaSettings, topicConfigs.getEdqsEventsConfigs()); + this.edqsRequestsAdmin = new TbKafkaAdmin(kafkaSettings, topicConfigs.getEdqsRequestsConfigs()); + this.edqsStateAdmin = new TbKafkaAdmin(kafkaSettings, topicConfigs.getEdqsStateConfigs()); + this.kafkaSettings = kafkaSettings; + this.edqsConfig = edqsConfig; + this.serviceInfoProvider = serviceInfoProvider; + this.consumerStatsService = consumerStatsService; + } + + @Override + public TbQueueConsumer> createEdqsMsgConsumer(EdqsQueue queue) { + String consumerGroup = "edqs-" + queue.name().toLowerCase() + "-consumer-group-" + serviceInfoProvider.getServiceId(); + return createEdqsMsgConsumer(queue, consumerGroup); + } + + @Override + public TbQueueConsumer> createEdqsMsgConsumer(EdqsQueue queue, String group) { + return TbKafkaConsumerTemplate.>builder() + .settings(kafkaSettings) + .topic(queue.getTopic()) + .readFromBeginning(queue.isReadFromBeginning()) + .stopWhenRead(queue.isStopWhenRead()) + .clientId("edqs-" + queue.name().toLowerCase() + "-" + consumerCounter.getAndIncrement() + "-consumer-" + serviceInfoProvider.getServiceId()) + .groupId(group) + .decoder(msg -> new TbProtoQueueMsg<>(msg.getKey(), ToEdqsMsg.parseFrom(msg.getData()), msg.getHeaders())) + .admin(queue == EdqsQueue.STATE ? edqsStateAdmin : edqsEventsAdmin) + .statsService(consumerStatsService) + .build(); + } + + @Override + public TbQueueProducer> createEdqsMsgProducer(EdqsQueue queue) { + return TbKafkaProducerTemplate.>builder() + .clientId("edqs-" + queue.name().toLowerCase() + "-producer-" + serviceInfoProvider.getServiceId()) + .settings(kafkaSettings) + .admin(queue == EdqsQueue.STATE ? edqsStateAdmin : edqsEventsAdmin) + .build(); + } + + @Override + public TbQueueResponseTemplate, TbProtoQueueMsg> createEdqsResponseTemplate() { + String requestsConsumerGroup = "edqs-requests-consumer-group-" + edqsConfig.getLabel(); + var requestConsumer = TbKafkaConsumerTemplate.>builder() + .settings(kafkaSettings) + .topic(edqsConfig.getRequestsTopic()) + .clientId("edqs-requests-consumer-" + serviceInfoProvider.getServiceId()) + .groupId(requestsConsumerGroup) + .decoder(msg -> new TbProtoQueueMsg<>(msg.getKey(), TransportProtos.ToEdqsMsg.parseFrom(msg.getData()), msg.getHeaders())) + .admin(edqsRequestsAdmin) + .statsService(consumerStatsService); + var responseProducer = TbKafkaProducerTemplate.>builder() + .settings(kafkaSettings) + .clientId("edqs-response-producer-" + serviceInfoProvider.getServiceId()) + .defaultTopic(edqsConfig.getResponsesTopic()) + .admin(edqsRequestsAdmin); + return DefaultTbQueueResponseTemplate., TbProtoQueueMsg>builder() + .requestTemplate(requestConsumer.build()) + .responseTemplate(responseProducer.build()) + .maxPendingRequests(edqsConfig.getMaxPendingRequests()) + .requestTimeout(edqsConfig.getMaxRequestTimeout()) + .pollInterval(edqsConfig.getPollInterval()) + .stats(new DummyMessagesStats()) // FIXME + .executor(ThingsBoardExecutors.newWorkStealingPool(5, "edqs")) + .build(); + } + +} diff --git a/common/queue/src/main/java/org/thingsboard/server/queue/environment/DistributedLock.java b/common/queue/src/main/java/org/thingsboard/server/queue/environment/DistributedLock.java new file mode 100644 index 0000000000..b69b98dd4f --- /dev/null +++ b/common/queue/src/main/java/org/thingsboard/server/queue/environment/DistributedLock.java @@ -0,0 +1,24 @@ +/** + * Copyright © 2016-2024 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.queue.environment; + +public interface DistributedLock { + + void lock(); + + void unlock(); + +} diff --git a/common/queue/src/main/java/org/thingsboard/server/queue/environment/DistributedLockService.java b/common/queue/src/main/java/org/thingsboard/server/queue/environment/DistributedLockService.java new file mode 100644 index 0000000000..61a21cca7c --- /dev/null +++ b/common/queue/src/main/java/org/thingsboard/server/queue/environment/DistributedLockService.java @@ -0,0 +1,22 @@ +/** + * Copyright © 2016-2024 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.queue.environment; + +public interface DistributedLockService { + + DistributedLock getLock(String key); + +} diff --git a/common/queue/src/main/java/org/thingsboard/server/queue/environment/DummyDistributedLockService.java b/common/queue/src/main/java/org/thingsboard/server/queue/environment/DummyDistributedLockService.java new file mode 100644 index 0000000000..03b6d0f2d9 --- /dev/null +++ b/common/queue/src/main/java/org/thingsboard/server/queue/environment/DummyDistributedLockService.java @@ -0,0 +1,53 @@ +/** + * Copyright © 2016-2024 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.queue.environment; + +import lombok.RequiredArgsConstructor; +import lombok.SneakyThrows; +import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; +import org.springframework.stereotype.Service; + +import java.util.concurrent.locks.ReentrantLock; + +@Service +@ConditionalOnProperty(prefix = "zk", value = "enabled", havingValue = "false", matchIfMissing = true) +public class DummyDistributedLockService implements DistributedLockService { + + @Override + public DistributedLock getLock(String key) { + return new DummyDistributedLock<>(); + } + + @RequiredArgsConstructor + private static class DummyDistributedLock implements DistributedLock { + + private final ReentrantLock lock = new ReentrantLock(); + + @SneakyThrows + @Override + public void lock() { + lock.lock(); + } + + @SneakyThrows + @Override + public void unlock() { + lock.unlock(); + } + + } + +} diff --git a/common/queue/src/main/java/org/thingsboard/server/queue/environment/ZkDistributedLockService.java b/common/queue/src/main/java/org/thingsboard/server/queue/environment/ZkDistributedLockService.java new file mode 100644 index 0000000000..07b8b503f9 --- /dev/null +++ b/common/queue/src/main/java/org/thingsboard/server/queue/environment/ZkDistributedLockService.java @@ -0,0 +1,62 @@ +/** + * Copyright © 2016-2024 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.queue.environment; + +import lombok.RequiredArgsConstructor; +import lombok.SneakyThrows; +import lombok.extern.slf4j.Slf4j; +import org.apache.curator.framework.recipes.locks.InterProcessLock; +import org.apache.curator.framework.recipes.locks.InterProcessMutex; +import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; +import org.springframework.stereotype.Service; +import org.thingsboard.server.queue.discovery.ZkDiscoveryService; + +@Service +@RequiredArgsConstructor +@ConditionalOnProperty(prefix = "zk", value = "enabled", havingValue = "true") +@Slf4j +public class ZkDistributedLockService implements DistributedLockService { + + private final ZkDiscoveryService zkDiscoveryService; + + @Override + public DistributedLock getLock(String key) { + return new ZkDistributedLock<>(key); + } + + @RequiredArgsConstructor + private class ZkDistributedLock implements DistributedLock { + + private final InterProcessLock interProcessLock; + + public ZkDistributedLock(String key) { + this.interProcessLock = new InterProcessMutex(zkDiscoveryService.getClient(), zkDiscoveryService.getZkDir() + "/locks/" + key); + } + + @SneakyThrows + @Override + public void lock() { + interProcessLock.acquire(); + } + + @SneakyThrows + @Override + public void unlock() { + interProcessLock.release(); + } + } + +} diff --git a/common/queue/src/main/java/org/thingsboard/server/queue/kafka/KafkaTbQueueMsg.java b/common/queue/src/main/java/org/thingsboard/server/queue/kafka/KafkaTbQueueMsg.java index a2edc35d94..2469e720bd 100644 --- a/common/queue/src/main/java/org/thingsboard/server/queue/kafka/KafkaTbQueueMsg.java +++ b/common/queue/src/main/java/org/thingsboard/server/queue/kafka/KafkaTbQueueMsg.java @@ -28,7 +28,13 @@ public class KafkaTbQueueMsg implements TbQueueMsg { private final byte[] data; public KafkaTbQueueMsg(ConsumerRecord record) { - this.key = UUID.fromString(record.key()); + UUID key; + try { + key = UUID.fromString(record.key()); + } catch (IllegalArgumentException e) { + key = null; // FIXME + } + this.key = key; TbQueueMsgHeaders headers = new DefaultTbQueueMsgHeaders(); record.headers().forEach(header -> { headers.put(header.key(), header.value()); diff --git a/common/queue/src/main/java/org/thingsboard/server/queue/kafka/TbKafkaAdmin.java b/common/queue/src/main/java/org/thingsboard/server/queue/kafka/TbKafkaAdmin.java index 2ea11c7afa..461defa58f 100644 --- a/common/queue/src/main/java/org/thingsboard/server/queue/kafka/TbKafkaAdmin.java +++ b/common/queue/src/main/java/org/thingsboard/server/queue/kafka/TbKafkaAdmin.java @@ -57,7 +57,6 @@ public class TbKafkaAdmin implements TbQueueAdmin { String numPartitionsStr = topicConfigs.get(TbKafkaTopicConfigs.NUM_PARTITIONS_SETTING); if (numPartitionsStr != null) { numPartitions = Integer.parseInt(numPartitionsStr); - topicConfigs.remove("partitions"); } else { numPartitions = 1; } @@ -71,7 +70,9 @@ public class TbKafkaAdmin implements TbQueueAdmin { return; } try { - NewTopic newTopic = new NewTopic(topic, numPartitions, replicationFactor).configs(PropertyUtils.getProps(topicConfigs, properties)); + Map configs = PropertyUtils.getProps(topicConfigs, properties); + configs.remove(TbKafkaTopicConfigs.NUM_PARTITIONS_SETTING); + NewTopic newTopic = new NewTopic(topic, numPartitions, replicationFactor).configs(configs); createTopic(newTopic).values().get(topic).get(); topics.add(topic); } catch (ExecutionException ee) { @@ -188,6 +189,9 @@ public class TbKafkaAdmin implements TbQueueAdmin { public boolean isTopicEmpty(String topic) { try { + if (!getTopics().contains(topic)) { + return true; + } TopicDescription topicDescription = settings.getAdminClient().describeTopics(Collections.singletonList(topic)).topicNameValues().get(topic).get(); List partitions = topicDescription.partitions().stream().map(partitionInfo -> new TopicPartition(topic, partitionInfo.partition())).toList(); diff --git a/common/queue/src/main/java/org/thingsboard/server/queue/kafka/TbKafkaConsumerTemplate.java b/common/queue/src/main/java/org/thingsboard/server/queue/kafka/TbKafkaConsumerTemplate.java index ef79834735..167fbb1751 100644 --- a/common/queue/src/main/java/org/thingsboard/server/queue/kafka/TbKafkaConsumerTemplate.java +++ b/common/queue/src/main/java/org/thingsboard/server/queue/kafka/TbKafkaConsumerTemplate.java @@ -21,7 +21,9 @@ import org.apache.kafka.clients.consumer.ConsumerConfig; import org.apache.kafka.clients.consumer.ConsumerRecord; import org.apache.kafka.clients.consumer.ConsumerRecords; import org.apache.kafka.clients.consumer.KafkaConsumer; +import org.apache.kafka.common.TopicPartition; import org.springframework.util.StopWatch; +import org.thingsboard.server.common.msg.queue.TopicPartitionInfo; import org.thingsboard.server.queue.TbQueueAdmin; import org.thingsboard.server.queue.TbQueueMsg; import org.thingsboard.server.queue.common.AbstractTbQueueConsumerTemplate; @@ -30,8 +32,12 @@ import java.io.IOException; import java.time.Duration; import java.util.ArrayList; import java.util.Collections; +import java.util.HashMap; import java.util.List; +import java.util.Map; import java.util.Properties; +import java.util.Set; +import java.util.stream.Collectors; /** * Created by ashvayka on 24.09.18. @@ -46,10 +52,17 @@ public class TbKafkaConsumerTemplate extends AbstractTbQue private final TbKafkaConsumerStatsService statsService; private final String groupId; + private final boolean readFromBeginning; // reset offset to beginning + private final boolean stopWhenRead; // stop consuming when reached an empty msg pack + private Map endOffsets; // needed if stopWhenRead is true + + private boolean partitionsAssigned = false; + @Builder private TbKafkaConsumerTemplate(TbKafkaSettings settings, TbKafkaDecoder decoder, String clientId, String groupId, String topic, - TbQueueAdmin admin, TbKafkaConsumerStatsService statsService) { + TbQueueAdmin admin, TbKafkaConsumerStatsService statsService, + boolean readFromBeginning, boolean stopWhenRead) { super(topic); Properties props = settings.toConsumerProps(topic); props.put(ConsumerConfig.CLIENT_ID_CONFIG, clientId); @@ -67,13 +80,45 @@ public class TbKafkaConsumerTemplate extends AbstractTbQue this.admin = admin; this.consumer = new KafkaConsumer<>(props); this.decoder = decoder; + this.readFromBeginning = readFromBeginning; + this.stopWhenRead = stopWhenRead; } @Override - protected void doSubscribe(List topicNames) { - if (!topicNames.isEmpty()) { - topicNames.forEach(admin::createTopicIfNotExists); - consumer.subscribe(topicNames); + protected void doSubscribe(Set partitions) { + Map> topics; + if (partitions == null) { + topics = Collections.emptyMap(); + } else { + topics = new HashMap<>(); + partitions.forEach(tpi -> { + if (tpi.isUseInternalPartition()) { + topics.computeIfAbsent(tpi.getFullTopicName(), t -> new ArrayList<>()).add(tpi.getPartition().get()); + } else { + topics.put(tpi.getFullTopicName(), null); + } + }); + } + if (!topics.isEmpty()) { + topics.keySet().forEach(admin::createTopicIfNotExists); + List toSubscribe = new ArrayList<>(); + topics.forEach((topic, kafkaPartitions) -> { + if (kafkaPartitions == null) { + toSubscribe.add(topic); + } else { + consumer.assign(kafkaPartitions.stream() + .map(partition -> new TopicPartition(topic, partition)) + .toList()); + partitionsAssigned = true; + onPartitionsAssigned(); + } + }); + if (!toSubscribe.isEmpty()) { + consumer.subscribe(toSubscribe); + } + if (readFromBeginning) { + consumer.seekToBeginning(Collections.emptySet()); // for all assigned partitions + } } else { log.info("unsubscribe due to empty topic list"); consumer.unsubscribe(); @@ -88,6 +133,13 @@ public class TbKafkaConsumerTemplate extends AbstractTbQue log.trace("poll topic {} maxDuration {}", getTopic(), durationInMillis); ConsumerRecords records = consumer.poll(Duration.ofMillis(durationInMillis)); + if (!partitionsAssigned) { + if (readFromBeginning) { + consumer.seekToBeginning(Collections.emptySet()); + } + partitionsAssigned = true; + onPartitionsAssigned(); + } stopWatch.stop(); log.trace("poll topic {} took {}ms", getTopic(), stopWatch.getTotalTimeMillis()); @@ -96,11 +148,36 @@ public class TbKafkaConsumerTemplate extends AbstractTbQue return Collections.emptyList(); } else { List> recordList = new ArrayList<>(256); - records.forEach(recordList::add); + records.forEach(record -> { + recordList.add(record); + if (stopWhenRead) { + int partition = record.partition(); + Long endOffset = endOffsets.get(partition); + if (endOffset == null) { + log.warn("End offset not found for {} [{}]", record.topic(), partition); + return; + } + log.trace("[{}-{}] Got record offset {}, expected end offset: {}", record.topic(), partition, record.offset(), endOffset - 1); + if (record.offset() >= endOffset - 1) { + endOffsets.remove(partition); + } + } + }); + if (endOffsets != null && endOffsets.isEmpty()) { + log.info("Reached end offsets for {}, stopping consumer", consumer.assignment()); + stop(); + } return recordList; } } + private void onPartitionsAssigned() { + if (stopWhenRead) { + endOffsets = consumer.endOffsets(consumer.assignment()).entrySet().stream() + .collect(Collectors.toMap(entry -> entry.getKey().partition(), Map.Entry::getValue)); + } + } + @Override public T decode(ConsumerRecord record) throws IOException { return decoder.decode(new KafkaTbQueueMsg(record)); diff --git a/common/queue/src/main/java/org/thingsboard/server/queue/kafka/TbKafkaProducerTemplate.java b/common/queue/src/main/java/org/thingsboard/server/queue/kafka/TbKafkaProducerTemplate.java index 3c9b85e925..6acf24b3f2 100644 --- a/common/queue/src/main/java/org/thingsboard/server/queue/kafka/TbKafkaProducerTemplate.java +++ b/common/queue/src/main/java/org/thingsboard/server/queue/kafka/TbKafkaProducerTemplate.java @@ -54,7 +54,7 @@ public class TbKafkaProducerTemplate implements TbQueuePro private final TbQueueAdmin admin; - private final Set topics; + private final Set topics; @Getter private final String clientId; @@ -97,16 +97,21 @@ public class TbKafkaProducerTemplate implements TbQueuePro @Override public void send(TopicPartitionInfo tpi, T msg, TbQueueCallback callback) { + send(tpi, msg.getKey().toString(), msg, callback); + } + + public void send(TopicPartitionInfo tpi, String key, T msg, TbQueueCallback callback) { try { - createTopicIfNotExist(tpi); - String key = msg.getKey().toString(); + String topic = tpi.getFullTopicName(); + createTopicIfNotExist(topic); byte[] data = msg.getData(); ProducerRecord record; List
headers = msg.getHeaders().getData().entrySet().stream().map(e -> new RecordHeader(e.getKey(), e.getValue())).collect(Collectors.toList()); if (log.isDebugEnabled()) { addAnalyticHeaders(headers); } - record = new ProducerRecord<>(tpi.getFullTopicName(), null, key, data, headers); + Integer partition = tpi.isUseInternalPartition() ? tpi.getPartition().orElse(null) : null; + record = new ProducerRecord<>(topic, partition, key, data, headers); producer.send(record, (metadata, exception) -> { if (exception == null) { if (callback != null) { @@ -130,12 +135,12 @@ public class TbKafkaProducerTemplate implements TbQueuePro } } - private void createTopicIfNotExist(TopicPartitionInfo tpi) { - if (topics.contains(tpi)) { + private void createTopicIfNotExist(String topic) { + if (topics.contains(topic)) { return; } - admin.createTopicIfNotExists(tpi.getFullTopicName()); - topics.add(tpi); + admin.createTopicIfNotExists(topic); + topics.add(topic); } @Override @@ -144,4 +149,5 @@ public class TbKafkaProducerTemplate implements TbQueuePro producer.close(); } } + } diff --git a/common/queue/src/main/java/org/thingsboard/server/queue/kafka/TbKafkaTopicConfigs.java b/common/queue/src/main/java/org/thingsboard/server/queue/kafka/TbKafkaTopicConfigs.java index ee529e8a68..2c4ccae003 100644 --- a/common/queue/src/main/java/org/thingsboard/server/queue/kafka/TbKafkaTopicConfigs.java +++ b/common/queue/src/main/java/org/thingsboard/server/queue/kafka/TbKafkaTopicConfigs.java @@ -52,6 +52,12 @@ public class TbKafkaTopicConfigs { private String housekeeperProperties; @Value("${queue.kafka.topic-properties.housekeeper-reprocessing:}") private String housekeeperReprocessingProperties; + @Value("${queue.kafka.topic-properties.edqs-events:}") + private String edqsEventsProperties; + @Value("${queue.kafka.topic-properties.edqs-requests:}") + private String edqsRequestsProperties; + @Value("${queue.kafka.topic-properties.edqs-state:}") + private String edqsStateProperties; @Getter private Map coreConfigs; @@ -79,6 +85,12 @@ public class TbKafkaTopicConfigs { private Map edgeConfigs; @Getter private Map edgeEventConfigs; + @Getter + private Map edqsEventsConfigs; + @Getter + private Map edqsRequestsConfigs; + @Getter + private Map edqsStateConfigs; @PostConstruct private void init() { @@ -97,6 +109,9 @@ public class TbKafkaTopicConfigs { housekeeperReprocessingConfigs = PropertyUtils.getProps(housekeeperReprocessingProperties); edgeConfigs = PropertyUtils.getProps(edgeProperties); edgeEventConfigs = PropertyUtils.getProps(edgeEventProperties); + edqsEventsConfigs = PropertyUtils.getProps(edqsEventsProperties); + edqsRequestsConfigs = PropertyUtils.getProps(edqsRequestsProperties); + edqsStateConfigs = PropertyUtils.getProps(edqsStateProperties); } } diff --git a/common/queue/src/main/java/org/thingsboard/server/queue/provider/EdqsClientQueueFactory.java b/common/queue/src/main/java/org/thingsboard/server/queue/provider/EdqsClientQueueFactory.java new file mode 100644 index 0000000000..094f22c423 --- /dev/null +++ b/common/queue/src/main/java/org/thingsboard/server/queue/provider/EdqsClientQueueFactory.java @@ -0,0 +1,31 @@ +/** + * Copyright © 2016-2024 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.queue.provider; + +import org.thingsboard.server.gen.transport.TransportProtos.FromEdqsMsg; +import org.thingsboard.server.gen.transport.TransportProtos.ToEdqsMsg; +import org.thingsboard.server.queue.TbQueueProducer; +import org.thingsboard.server.queue.TbQueueRequestTemplate; +import org.thingsboard.server.queue.common.TbProtoQueueMsg; +import org.thingsboard.server.queue.edqs.EdqsQueue; + +public interface EdqsClientQueueFactory { + + TbQueueProducer> createEdqsMsgProducer(EdqsQueue queue); + + TbQueueRequestTemplate, TbProtoQueueMsg> createEdqsRequestTemplate(); + +} diff --git a/common/queue/src/main/java/org/thingsboard/server/queue/provider/InMemoryMonolithQueueFactory.java b/common/queue/src/main/java/org/thingsboard/server/queue/provider/InMemoryMonolithQueueFactory.java index d70cad159b..a7c351a3c9 100644 --- a/common/queue/src/main/java/org/thingsboard/server/queue/provider/InMemoryMonolithQueueFactory.java +++ b/common/queue/src/main/java/org/thingsboard/server/queue/provider/InMemoryMonolithQueueFactory.java @@ -15,6 +15,7 @@ */ package org.thingsboard.server.queue.provider; +import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.springframework.boot.autoconfigure.condition.ConditionalOnExpression; import org.springframework.scheduling.annotation.Scheduled; @@ -23,13 +24,19 @@ import org.thingsboard.server.common.data.queue.Queue; import org.thingsboard.server.common.msg.queue.ServiceType; import org.thingsboard.server.gen.js.JsInvokeProtos; import org.thingsboard.server.gen.transport.TransportProtos; +import org.thingsboard.server.gen.transport.TransportProtos.FromEdqsMsg; +import org.thingsboard.server.gen.transport.TransportProtos.ToEdqsMsg; +import org.thingsboard.server.queue.TbQueueAdmin; import org.thingsboard.server.queue.TbQueueConsumer; import org.thingsboard.server.queue.TbQueueProducer; import org.thingsboard.server.queue.TbQueueRequestTemplate; +import org.thingsboard.server.queue.common.DefaultTbQueueRequestTemplate; import org.thingsboard.server.queue.common.TbProtoJsQueueMsg; import org.thingsboard.server.queue.common.TbProtoQueueMsg; import org.thingsboard.server.queue.discovery.TbServiceInfoProvider; import org.thingsboard.server.queue.discovery.TopicService; +import org.thingsboard.server.queue.edqs.EdqsConfig; +import org.thingsboard.server.queue.edqs.EdqsQueue; import org.thingsboard.server.queue.memory.InMemoryStorage; import org.thingsboard.server.queue.memory.InMemoryTbQueueConsumer; import org.thingsboard.server.queue.memory.InMemoryTbQueueProducer; @@ -43,37 +50,21 @@ import org.thingsboard.server.queue.settings.TbQueueVersionControlSettings; @Slf4j @Component @ConditionalOnExpression("'${queue.type:null}'=='in-memory' && '${service.type:null}'=='monolith'") +@RequiredArgsConstructor public class InMemoryMonolithQueueFactory implements TbCoreQueueFactory, TbRuleEngineQueueFactory, TbVersionControlQueueFactory { private final TopicService topicService; private final TbQueueCoreSettings coreSettings; private final TbServiceInfoProvider serviceInfoProvider; + private final TbQueueAdmin queueAdmin; private final TbQueueRuleEngineSettings ruleEngineSettings; private final TbQueueVersionControlSettings vcSettings; private final TbQueueTransportApiSettings transportApiSettings; private final TbQueueTransportNotificationSettings transportNotificationSettings; private final TbQueueEdgeSettings edgeSettings; + private final EdqsConfig edqsConfig; private final InMemoryStorage storage; - public InMemoryMonolithQueueFactory(TopicService topicService, TbQueueCoreSettings coreSettings, - TbQueueRuleEngineSettings ruleEngineSettings, - TbQueueVersionControlSettings vcSettings, - TbServiceInfoProvider serviceInfoProvider, - TbQueueTransportApiSettings transportApiSettings, - TbQueueTransportNotificationSettings transportNotificationSettings, - TbQueueEdgeSettings edgeSettings, - InMemoryStorage storage) { - this.topicService = topicService; - this.coreSettings = coreSettings; - this.vcSettings = vcSettings; - this.serviceInfoProvider = serviceInfoProvider; - this.ruleEngineSettings = ruleEngineSettings; - this.transportApiSettings = transportApiSettings; - this.transportNotificationSettings = transportNotificationSettings; - this.edgeSettings = edgeSettings; - this.storage = storage; - } - @Override public TbQueueProducer> createTransportNotificationsMsgProducer() { return new InMemoryTbQueueProducer<>(storage, topicService.buildTopicName(transportNotificationSettings.getNotificationsTopic())); @@ -209,6 +200,26 @@ public class InMemoryMonolithQueueFactory implements TbCoreQueueFactory, TbRuleE return null; } + @Override + public TbQueueProducer> createEdqsMsgProducer(EdqsQueue queue) { + return new InMemoryTbQueueProducer<>(storage, queue.getTopic()); + } + + @Override + public TbQueueRequestTemplate, TbProtoQueueMsg> createEdqsRequestTemplate() { + TbQueueProducer> requestProducer = new InMemoryTbQueueProducer<>(storage, edqsConfig.getRequestsTopic()); + TbQueueConsumer> responseConsumer = new InMemoryTbQueueConsumer<>(storage, edqsConfig.getResponsesTopic()); + + return DefaultTbQueueRequestTemplate., TbProtoQueueMsg>builder() + .queueAdmin(queueAdmin) + .requestTemplate(requestProducer) + .responseTemplate(responseConsumer) + .maxPendingRequests(edqsConfig.getMaxPendingRequests()) + .maxRequestTimeout(edqsConfig.getMaxRequestTimeout()) + .pollInterval(edqsConfig.getPollInterval()) + .build(); + } + @Scheduled(fixedRateString = "${queue.in_memory.stats.print-interval-ms:60000}") private void printInMemoryStats() { storage.printStats(); diff --git a/common/queue/src/main/java/org/thingsboard/server/queue/provider/KafkaMonolithQueueFactory.java b/common/queue/src/main/java/org/thingsboard/server/queue/provider/KafkaMonolithQueueFactory.java index dd5d61e834..7a83264dfc 100644 --- a/common/queue/src/main/java/org/thingsboard/server/queue/provider/KafkaMonolithQueueFactory.java +++ b/common/queue/src/main/java/org/thingsboard/server/queue/provider/KafkaMonolithQueueFactory.java @@ -25,11 +25,13 @@ import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.common.data.queue.Queue; import org.thingsboard.server.common.msg.queue.ServiceType; import org.thingsboard.server.gen.js.JsInvokeProtos; +import org.thingsboard.server.gen.transport.TransportProtos.FromEdqsMsg; import org.thingsboard.server.gen.transport.TransportProtos.ToCoreMsg; import org.thingsboard.server.gen.transport.TransportProtos.ToCoreNotificationMsg; import org.thingsboard.server.gen.transport.TransportProtos.ToEdgeEventNotificationMsg; import org.thingsboard.server.gen.transport.TransportProtos.ToEdgeMsg; import org.thingsboard.server.gen.transport.TransportProtos.ToEdgeNotificationMsg; +import org.thingsboard.server.gen.transport.TransportProtos.ToEdqsMsg; import org.thingsboard.server.gen.transport.TransportProtos.ToHousekeeperServiceMsg; import org.thingsboard.server.gen.transport.TransportProtos.ToOtaPackageStateServiceMsg; import org.thingsboard.server.gen.transport.TransportProtos.ToRuleEngineMsg; @@ -48,6 +50,8 @@ import org.thingsboard.server.queue.common.TbProtoJsQueueMsg; import org.thingsboard.server.queue.common.TbProtoQueueMsg; import org.thingsboard.server.queue.discovery.TbServiceInfoProvider; import org.thingsboard.server.queue.discovery.TopicService; +import org.thingsboard.server.queue.edqs.EdqsConfig; +import org.thingsboard.server.queue.edqs.EdqsQueue; import org.thingsboard.server.queue.kafka.TbKafkaAdmin; import org.thingsboard.server.queue.kafka.TbKafkaConsumerStatsService; import org.thingsboard.server.queue.kafka.TbKafkaConsumerTemplate; @@ -80,6 +84,7 @@ public class KafkaMonolithQueueFactory implements TbCoreQueueFactory, TbRuleEngi private final TbQueueVersionControlSettings vcSettings; private final TbQueueEdgeSettings edgeSettings; private final TbKafkaConsumerStatsService consumerStatsService; + private final EdqsConfig edqsConfig; private final TbQueueAdmin coreAdmin; private final TbKafkaAdmin ruleEngineAdmin; @@ -94,6 +99,8 @@ public class KafkaMonolithQueueFactory implements TbCoreQueueFactory, TbRuleEngi private final TbQueueAdmin housekeeperReprocessingAdmin; private final TbQueueAdmin edgeAdmin; private final TbQueueAdmin edgeEventAdmin; + private final TbQueueAdmin edqsEventsAdmin; + private final TbKafkaAdmin edqsRequestsAdmin; private final AtomicLong consumerCount = new AtomicLong(); @@ -107,7 +114,8 @@ public class KafkaMonolithQueueFactory implements TbCoreQueueFactory, TbRuleEngi TbQueueVersionControlSettings vcSettings, TbQueueEdgeSettings edgeSettings, TbKafkaConsumerStatsService consumerStatsService, - TbKafkaTopicConfigs kafkaTopicConfigs) { + TbKafkaTopicConfigs kafkaTopicConfigs, + EdqsConfig edqsConfig) { this.topicService = topicService; this.kafkaSettings = kafkaSettings; this.serviceInfoProvider = serviceInfoProvider; @@ -119,6 +127,7 @@ public class KafkaMonolithQueueFactory implements TbCoreQueueFactory, TbRuleEngi this.vcSettings = vcSettings; this.consumerStatsService = consumerStatsService; this.edgeSettings = edgeSettings; + this.edqsConfig = edqsConfig; this.coreAdmin = new TbKafkaAdmin(kafkaSettings, kafkaTopicConfigs.getCoreConfigs()); this.ruleEngineAdmin = new TbKafkaAdmin(kafkaSettings, kafkaTopicConfigs.getRuleEngineConfigs()); @@ -133,6 +142,8 @@ public class KafkaMonolithQueueFactory implements TbCoreQueueFactory, TbRuleEngi this.housekeeperReprocessingAdmin = new TbKafkaAdmin(kafkaSettings, kafkaTopicConfigs.getHousekeeperReprocessingConfigs()); this.edgeAdmin = new TbKafkaAdmin(kafkaSettings, kafkaTopicConfigs.getEdgeConfigs()); this.edgeEventAdmin = new TbKafkaAdmin(kafkaSettings, kafkaTopicConfigs.getEdgeEventConfigs()); + this.edqsEventsAdmin = new TbKafkaAdmin(kafkaSettings, kafkaTopicConfigs.getEdqsEventsConfigs()); + this.edqsRequestsAdmin = new TbKafkaAdmin(kafkaSettings, kafkaTopicConfigs.getEdqsRequestsConfigs()); } @Override @@ -490,6 +501,42 @@ public class KafkaMonolithQueueFactory implements TbCoreQueueFactory, TbRuleEngi return requestBuilder.build(); } + @Override + public TbQueueProducer> createEdqsMsgProducer(EdqsQueue queue) { + return TbKafkaProducerTemplate.>builder() + .clientId("edqs-producer-" + queue.name().toLowerCase() + "-" + serviceInfoProvider.getServiceId()) + .settings(kafkaSettings) + .admin(edqsEventsAdmin) + .build(); + } + + @Override + public TbQueueRequestTemplate, TbProtoQueueMsg> createEdqsRequestTemplate() { + var requestProducer = TbKafkaProducerTemplate.>builder() + .settings(kafkaSettings) + .clientId("edqs-request-" + serviceInfoProvider.getServiceId()) + .defaultTopic(topicService.buildTopicName(edqsConfig.getRequestsTopic())) + .admin(edqsRequestsAdmin); + + var responseConsumer = TbKafkaConsumerTemplate.>builder() + .settings(kafkaSettings) + .topic(topicService.buildTopicName(edqsConfig.getResponsesTopic() + "." + serviceInfoProvider.getServiceId())) + .clientId(topicService.buildTopicName("monolith-edqs-response-consumer-" + serviceInfoProvider.getServiceId())) + .groupId(topicService.buildTopicName("monolith-edqs-response-consumer-" + serviceInfoProvider.getServiceId())) + .decoder(msg -> new TbProtoQueueMsg<>(msg.getKey(), FromEdqsMsg.parseFrom(msg.getData()), msg.getHeaders())) + .admin(edqsRequestsAdmin) + .statsService(consumerStatsService); + + return DefaultTbQueueRequestTemplate., TbProtoQueueMsg>builder() + .queueAdmin(edqsRequestsAdmin) + .requestTemplate(requestProducer.build()) + .responseTemplate(responseConsumer.build()) + .maxPendingRequests(edqsConfig.getMaxPendingRequests()) + .maxRequestTimeout(edqsConfig.getMaxRequestTimeout()) + .pollInterval(edqsConfig.getPollInterval()) + .build(); + } + @PreDestroy private void destroy() { if (coreAdmin != null) { @@ -523,4 +570,5 @@ public class KafkaMonolithQueueFactory implements TbCoreQueueFactory, TbRuleEngi edgeAdmin.destroy(); } } + } diff --git a/common/queue/src/main/java/org/thingsboard/server/queue/provider/KafkaTbCoreQueueFactory.java b/common/queue/src/main/java/org/thingsboard/server/queue/provider/KafkaTbCoreQueueFactory.java index cc0e044917..2ac607a346 100644 --- a/common/queue/src/main/java/org/thingsboard/server/queue/provider/KafkaTbCoreQueueFactory.java +++ b/common/queue/src/main/java/org/thingsboard/server/queue/provider/KafkaTbCoreQueueFactory.java @@ -24,11 +24,13 @@ import org.thingsboard.server.common.data.id.EdgeId; import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.common.msg.queue.ServiceType; import org.thingsboard.server.gen.js.JsInvokeProtos; +import org.thingsboard.server.gen.transport.TransportProtos.FromEdqsMsg; import org.thingsboard.server.gen.transport.TransportProtos.ToCoreMsg; import org.thingsboard.server.gen.transport.TransportProtos.ToCoreNotificationMsg; import org.thingsboard.server.gen.transport.TransportProtos.ToEdgeEventNotificationMsg; import org.thingsboard.server.gen.transport.TransportProtos.ToEdgeMsg; import org.thingsboard.server.gen.transport.TransportProtos.ToEdgeNotificationMsg; +import org.thingsboard.server.gen.transport.TransportProtos.ToEdqsMsg; import org.thingsboard.server.gen.transport.TransportProtos.ToHousekeeperServiceMsg; import org.thingsboard.server.gen.transport.TransportProtos.ToOtaPackageStateServiceMsg; import org.thingsboard.server.gen.transport.TransportProtos.ToRuleEngineMsg; @@ -47,6 +49,8 @@ import org.thingsboard.server.queue.common.TbProtoJsQueueMsg; import org.thingsboard.server.queue.common.TbProtoQueueMsg; import org.thingsboard.server.queue.discovery.TbServiceInfoProvider; import org.thingsboard.server.queue.discovery.TopicService; +import org.thingsboard.server.queue.edqs.EdqsConfig; +import org.thingsboard.server.queue.edqs.EdqsQueue; import org.thingsboard.server.queue.kafka.TbKafkaAdmin; import org.thingsboard.server.queue.kafka.TbKafkaConsumerStatsService; import org.thingsboard.server.queue.kafka.TbKafkaConsumerTemplate; @@ -79,6 +83,7 @@ public class KafkaTbCoreQueueFactory implements TbCoreQueueFactory { private final TbKafkaConsumerStatsService consumerStatsService; private final TbQueueTransportNotificationSettings transportNotificationSettings; private final TbQueueEdgeSettings edgeSettings; + private final EdqsConfig edqsConfig; private final TbQueueAdmin coreAdmin; private final TbQueueAdmin ruleEngineAdmin; @@ -93,6 +98,8 @@ public class KafkaTbCoreQueueFactory implements TbCoreQueueFactory { private final TbQueueAdmin housekeeperReprocessingAdmin; private final TbQueueAdmin edgeAdmin; private final TbQueueAdmin edgeEventAdmin; + private final TbQueueAdmin edqsEventsAdmin; + private final TbKafkaAdmin edqsRequestsAdmin; private final AtomicLong consumerCount = new AtomicLong(); @@ -107,6 +114,7 @@ public class KafkaTbCoreQueueFactory implements TbCoreQueueFactory { TbQueueEdgeSettings edgeSettings, TbKafkaConsumerStatsService consumerStatsService, TbQueueTransportNotificationSettings transportNotificationSettings, + EdqsConfig edqsConfig, TbKafkaTopicConfigs kafkaTopicConfigs) { this.topicService = topicService; this.kafkaSettings = kafkaSettings; @@ -119,6 +127,7 @@ public class KafkaTbCoreQueueFactory implements TbCoreQueueFactory { this.consumerStatsService = consumerStatsService; this.transportNotificationSettings = transportNotificationSettings; this.edgeSettings = edgeSettings; + this.edqsConfig = edqsConfig; this.coreAdmin = new TbKafkaAdmin(kafkaSettings, kafkaTopicConfigs.getCoreConfigs()); this.ruleEngineAdmin = new TbKafkaAdmin(kafkaSettings, kafkaTopicConfigs.getRuleEngineConfigs()); @@ -133,6 +142,8 @@ public class KafkaTbCoreQueueFactory implements TbCoreQueueFactory { this.housekeeperReprocessingAdmin = new TbKafkaAdmin(kafkaSettings, kafkaTopicConfigs.getHousekeeperReprocessingConfigs()); this.edgeAdmin = new TbKafkaAdmin(kafkaSettings, kafkaTopicConfigs.getEdgeConfigs()); this.edgeEventAdmin = new TbKafkaAdmin(kafkaSettings, kafkaTopicConfigs.getEdgeEventConfigs()); + this.edqsEventsAdmin = new TbKafkaAdmin(kafkaSettings, kafkaTopicConfigs.getEdqsEventsConfigs()); + this.edqsRequestsAdmin = new TbKafkaAdmin(kafkaSettings, kafkaTopicConfigs.getEdqsRequestsConfigs()); } @Override @@ -439,6 +450,42 @@ public class KafkaTbCoreQueueFactory implements TbCoreQueueFactory { return requestBuilder.build(); } + @Override + public TbQueueProducer> createEdqsMsgProducer(EdqsQueue queue) { + return TbKafkaProducerTemplate.>builder() + .clientId("edqs-producer-" + queue.name().toLowerCase() + "-" + serviceInfoProvider.getServiceId()) + .settings(kafkaSettings) + .admin(edqsEventsAdmin) + .build(); + } + + @Override + public TbQueueRequestTemplate, TbProtoQueueMsg> createEdqsRequestTemplate() { + var requestProducer = TbKafkaProducerTemplate.>builder() + .settings(kafkaSettings) + .clientId("edqs-request-" + serviceInfoProvider.getServiceId()) + .defaultTopic(topicService.buildTopicName(edqsConfig.getRequestsTopic())) + .admin(edqsRequestsAdmin); + + var responseConsumer = TbKafkaConsumerTemplate.>builder() + .settings(kafkaSettings) + .topic(topicService.buildTopicName(edqsConfig.getResponsesTopic() + "." + serviceInfoProvider.getServiceId())) + .clientId(topicService.buildTopicName("tb-core-edqs-response-consumer-" + serviceInfoProvider.getServiceId())) + .groupId(topicService.buildTopicName("tb-core-edqs-response-consumer-" + serviceInfoProvider.getServiceId())) + .decoder(msg -> new TbProtoQueueMsg<>(msg.getKey(), FromEdqsMsg.parseFrom(msg.getData()), msg.getHeaders())) + .admin(edqsRequestsAdmin) + .statsService(consumerStatsService); + + return DefaultTbQueueRequestTemplate., TbProtoQueueMsg>builder() + .queueAdmin(edqsRequestsAdmin) + .requestTemplate(requestProducer.build()) + .responseTemplate(responseConsumer.build()) + .maxPendingRequests(edqsConfig.getMaxPendingRequests()) + .maxRequestTimeout(edqsConfig.getMaxRequestTimeout()) + .pollInterval(edqsConfig.getPollInterval()) + .build(); + } + @PreDestroy private void destroy() { if (coreAdmin != null) { @@ -469,4 +516,5 @@ public class KafkaTbCoreQueueFactory implements TbCoreQueueFactory { vcAdmin.destroy(); } } + } diff --git a/common/queue/src/main/java/org/thingsboard/server/queue/provider/KafkaTbRuleEngineQueueFactory.java b/common/queue/src/main/java/org/thingsboard/server/queue/provider/KafkaTbRuleEngineQueueFactory.java index 87a1a69c2e..b681f1fc2d 100644 --- a/common/queue/src/main/java/org/thingsboard/server/queue/provider/KafkaTbRuleEngineQueueFactory.java +++ b/common/queue/src/main/java/org/thingsboard/server/queue/provider/KafkaTbRuleEngineQueueFactory.java @@ -23,11 +23,13 @@ import org.springframework.stereotype.Component; import org.thingsboard.server.common.data.queue.Queue; import org.thingsboard.server.common.msg.queue.ServiceType; import org.thingsboard.server.gen.js.JsInvokeProtos; +import org.thingsboard.server.gen.transport.TransportProtos.FromEdqsMsg; import org.thingsboard.server.gen.transport.TransportProtos.ToCoreMsg; import org.thingsboard.server.gen.transport.TransportProtos.ToCoreNotificationMsg; import org.thingsboard.server.gen.transport.TransportProtos.ToEdgeEventNotificationMsg; import org.thingsboard.server.gen.transport.TransportProtos.ToEdgeMsg; import org.thingsboard.server.gen.transport.TransportProtos.ToEdgeNotificationMsg; +import org.thingsboard.server.gen.transport.TransportProtos.ToEdqsMsg; import org.thingsboard.server.gen.transport.TransportProtos.ToHousekeeperServiceMsg; import org.thingsboard.server.gen.transport.TransportProtos.ToOtaPackageStateServiceMsg; import org.thingsboard.server.gen.transport.TransportProtos.ToRuleEngineMsg; @@ -43,6 +45,7 @@ import org.thingsboard.server.queue.common.TbProtoJsQueueMsg; import org.thingsboard.server.queue.common.TbProtoQueueMsg; import org.thingsboard.server.queue.discovery.TbServiceInfoProvider; import org.thingsboard.server.queue.discovery.TopicService; +import org.thingsboard.server.queue.edqs.EdqsQueue; import org.thingsboard.server.queue.kafka.TbKafkaAdmin; import org.thingsboard.server.queue.kafka.TbKafkaConsumerStatsService; import org.thingsboard.server.queue.kafka.TbKafkaConsumerTemplate; @@ -81,6 +84,7 @@ public class KafkaTbRuleEngineQueueFactory implements TbRuleEngineQueueFactory { private final TbQueueAdmin housekeeperAdmin; private final TbQueueAdmin edgeAdmin; private final TbQueueAdmin edgeEventAdmin; + private final TbQueueAdmin edqsEventsAdmin; private final AtomicLong consumerCount = new AtomicLong(); public KafkaTbRuleEngineQueueFactory(TopicService topicService, TbKafkaSettings kafkaSettings, @@ -111,6 +115,7 @@ public class KafkaTbRuleEngineQueueFactory implements TbRuleEngineQueueFactory { this.housekeeperAdmin = new TbKafkaAdmin(kafkaSettings, kafkaTopicConfigs.getHousekeeperConfigs()); this.edgeAdmin = new TbKafkaAdmin(kafkaSettings, kafkaTopicConfigs.getEdgeConfigs()); this.edgeEventAdmin = new TbKafkaAdmin(kafkaSettings, kafkaTopicConfigs.getEdgeEventConfigs()); + this.edqsEventsAdmin = new TbKafkaAdmin(kafkaSettings, kafkaTopicConfigs.getEdqsEventsConfigs()); } @Override @@ -293,6 +298,20 @@ public class KafkaTbRuleEngineQueueFactory implements TbRuleEngineQueueFactory { .build(); } + @Override + public TbQueueProducer> createEdqsMsgProducer(EdqsQueue queue) { + return TbKafkaProducerTemplate.>builder() + .clientId("edqs-producer-" + queue.name().toLowerCase() + "-" + serviceInfoProvider.getServiceId()) + .settings(kafkaSettings) + .admin(edqsEventsAdmin) + .build(); + } + + @Override + public TbQueueRequestTemplate, TbProtoQueueMsg> createEdqsRequestTemplate() { + throw new UnsupportedOperationException(); + } + @PreDestroy private void destroy() { if (coreAdmin != null) { diff --git a/common/queue/src/main/java/org/thingsboard/server/queue/provider/TbCoreQueueFactory.java b/common/queue/src/main/java/org/thingsboard/server/queue/provider/TbCoreQueueFactory.java index c4002f4d3e..ec47177f28 100644 --- a/common/queue/src/main/java/org/thingsboard/server/queue/provider/TbCoreQueueFactory.java +++ b/common/queue/src/main/java/org/thingsboard/server/queue/provider/TbCoreQueueFactory.java @@ -42,7 +42,7 @@ import org.thingsboard.server.queue.common.TbProtoQueueMsg; * Responsible for initialization of various Producers and Consumers used by TB Core Node. * Implementation Depends on the queue queue.type from yml or TB_QUEUE_TYPE environment variable */ -public interface TbCoreQueueFactory extends TbUsageStatsClientQueueFactory, HousekeeperClientQueueFactory { +public interface TbCoreQueueFactory extends TbUsageStatsClientQueueFactory, HousekeeperClientQueueFactory, EdqsClientQueueFactory { /** * Used to push messages to instances of TB Transport Service diff --git a/common/queue/src/main/java/org/thingsboard/server/queue/provider/TbQueueProducerProvider.java b/common/queue/src/main/java/org/thingsboard/server/queue/provider/TbQueueProducerProvider.java index ec31763baa..913bd99ea7 100644 --- a/common/queue/src/main/java/org/thingsboard/server/queue/provider/TbQueueProducerProvider.java +++ b/common/queue/src/main/java/org/thingsboard/server/queue/provider/TbQueueProducerProvider.java @@ -76,7 +76,7 @@ public interface TbQueueProducerProvider { */ TbQueueProducer> getTbUsageStatsMsgProducer(); - /** + /** * Used to push messages to other instances of TB Core Service * * @return diff --git a/common/queue/src/main/java/org/thingsboard/server/queue/provider/TbRuleEngineQueueFactory.java b/common/queue/src/main/java/org/thingsboard/server/queue/provider/TbRuleEngineQueueFactory.java index c406aeb311..d58441c0ad 100644 --- a/common/queue/src/main/java/org/thingsboard/server/queue/provider/TbRuleEngineQueueFactory.java +++ b/common/queue/src/main/java/org/thingsboard/server/queue/provider/TbRuleEngineQueueFactory.java @@ -36,7 +36,7 @@ import org.thingsboard.server.queue.common.TbProtoQueueMsg; * Responsible for initialization of various Producers and Consumers used by TB Core Node. * Implementation Depends on the queue queue.type from yml or TB_QUEUE_TYPE environment variable */ -public interface TbRuleEngineQueueFactory extends TbUsageStatsClientQueueFactory, HousekeeperClientQueueFactory { +public interface TbRuleEngineQueueFactory extends TbUsageStatsClientQueueFactory, HousekeeperClientQueueFactory, EdqsClientQueueFactory { /** * Used to push messages to instances of TB Transport Service diff --git a/common/queue/src/main/java/org/thingsboard/server/queue/util/PropertyUtils.java b/common/queue/src/main/java/org/thingsboard/server/queue/util/PropertyUtils.java index f00cc2c103..5b120e42e2 100644 --- a/common/queue/src/main/java/org/thingsboard/server/queue/util/PropertyUtils.java +++ b/common/queue/src/main/java/org/thingsboard/server/queue/util/PropertyUtils.java @@ -43,9 +43,8 @@ public class PropertyUtils { } public static Map getProps(Map defaultProperties, String propertiesStr, Function> parser) { - Map properties = defaultProperties; + Map properties = new HashMap<>(defaultProperties); if (StringUtils.isNotBlank(propertiesStr)) { - properties = new HashMap<>(properties); properties.putAll(parser.apply(propertiesStr)); } return properties; diff --git a/common/queue/src/test/java/org/thingsboard/server/queue/common/DefaultTbQueueRequestTemplateTest.java b/common/queue/src/test/java/org/thingsboard/server/queue/common/DefaultTbQueueRequestTemplateTest.java index b7a3ae20d3..6e8c9640e3 100644 --- a/common/queue/src/test/java/org/thingsboard/server/queue/common/DefaultTbQueueRequestTemplateTest.java +++ b/common/queue/src/test/java/org/thingsboard/server/queue/common/DefaultTbQueueRequestTemplateTest.java @@ -145,19 +145,19 @@ public class DefaultTbQueueRequestTemplateTest { @Test public void givenMessages_whenSend_thenOK() { - willDoNothing().given(inst).sendToRequestTemplate(any(), any(), any(), any()); + willDoNothing().given(inst).sendToRequestTemplate(any(), any(), any(), any(), any()); inst.init(); final int msgCount = 10; for (int i = 0; i < msgCount; i++) { inst.send(getRequestMsgMock()); } assertThat(inst.pendingRequests.mappingCount(), equalTo((long) msgCount)); - verify(inst, times(msgCount)).sendToRequestTemplate(any(), any(), any(), any()); + verify(inst, times(msgCount)).sendToRequestTemplate(any(), any(), any(), any(), any()); } @Test public void givenMessagesOverMaxPendingRequests_whenSend_thenImmediateFailedFutureForTheOfRequests() { - willDoNothing().given(inst).sendToRequestTemplate(any(), any(), any(), any()); + willDoNothing().given(inst).sendToRequestTemplate(any(), any(), any(), any(), any()); inst.init(); int msgOverflowCount = 10; for (int i = 0; i < inst.maxPendingRequests; i++) { @@ -167,7 +167,7 @@ public class DefaultTbQueueRequestTemplateTest { assertThat("max pending requests overflow", inst.send(getRequestMsgMock()).isDone(), is(true)); //overflow, immediate failed future } assertThat(inst.pendingRequests.mappingCount(), equalTo(inst.maxPendingRequests)); - verify(inst, times((int) inst.maxPendingRequests)).sendToRequestTemplate(any(), any(), any(), any()); + verify(inst, times((int) inst.maxPendingRequests)).sendToRequestTemplate(any(), any(), any(), any(), any()); } @SuppressWarnings("unchecked") diff --git a/common/stats/src/main/java/org/thingsboard/server/common/stats/DummyMessagesStats.java b/common/stats/src/main/java/org/thingsboard/server/common/stats/DummyMessagesStats.java new file mode 100644 index 0000000000..7847860dcd --- /dev/null +++ b/common/stats/src/main/java/org/thingsboard/server/common/stats/DummyMessagesStats.java @@ -0,0 +1,54 @@ +/** + * Copyright © 2016-2024 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.common.stats; + +public class DummyMessagesStats implements MessagesStats { + @Override + public void incrementTotal(int amount) { + + } + + @Override + public void incrementSuccessful(int amount) { + + } + + @Override + public void incrementFailed(int amount) { + + } + + @Override + public int getTotal() { + return 0; + } + + @Override + public int getSuccessful() { + return 0; + } + + @Override + public int getFailed() { + return 0; + } + + @Override + public void reset() { + + } + +} diff --git a/common/stats/src/main/java/org/thingsboard/server/common/stats/StatsType.java b/common/stats/src/main/java/org/thingsboard/server/common/stats/StatsType.java index 3833155c05..2217be8d23 100644 --- a/common/stats/src/main/java/org/thingsboard/server/common/stats/StatsType.java +++ b/common/stats/src/main/java/org/thingsboard/server/common/stats/StatsType.java @@ -22,7 +22,8 @@ public enum StatsType { JS_INVOKE("jsInvoke"), RATE_EXECUTOR("rateExecutor"), HOUSEKEEPER("housekeeper"), - EDGE("edge"); + EDGE("edge"), + EDQS("edqs"); private final String name; diff --git a/common/util/src/main/java/org/thingsboard/common/util/JacksonUtil.java b/common/util/src/main/java/org/thingsboard/common/util/JacksonUtil.java index fe0e019537..8dc98377d3 100644 --- a/common/util/src/main/java/org/thingsboard/common/util/JacksonUtil.java +++ b/common/util/src/main/java/org/thingsboard/common/util/JacksonUtil.java @@ -559,6 +559,10 @@ public class JacksonUtil { } } + public static JsonNode getValueByPath(ObjectNode node, String path) { + return node.at("/" + path.replace(".", "/")); + } + @Data public static class JsonNodeProcessingTask { private final String path; diff --git a/common/version-control/src/main/java/org/thingsboard/server/service/sync/vc/DefaultClusterVersionControlService.java b/common/version-control/src/main/java/org/thingsboard/server/service/sync/vc/DefaultClusterVersionControlService.java index ca0636761d..bde497ed94 100644 --- a/common/version-control/src/main/java/org/thingsboard/server/service/sync/vc/DefaultClusterVersionControlService.java +++ b/common/version-control/src/main/java/org/thingsboard/server/service/sync/vc/DefaultClusterVersionControlService.java @@ -175,7 +175,7 @@ public class DefaultClusterVersionControlService extends TbApplicationEventListe } } } - consumer.subscribe(event.getPartitionsMap().values().stream().findAny().orElse(Collections.emptySet())); + consumer.subscribe(event.getNewPartitions().values().stream().findAny().orElse(Collections.emptySet())); } @Override diff --git a/dao/src/main/java/org/thingsboard/server/dao/Dao.java b/dao/src/main/java/org/thingsboard/server/dao/Dao.java index e912872496..79a4ced3b7 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/Dao.java +++ b/dao/src/main/java/org/thingsboard/server/dao/Dao.java @@ -17,7 +17,11 @@ package org.thingsboard.server.dao; import com.google.common.util.concurrent.ListenableFuture; import org.thingsboard.server.common.data.EntityType; +import org.thingsboard.server.common.data.edqs.fields.EntityFields; import org.thingsboard.server.common.data.id.TenantId; +import org.thingsboard.server.common.data.page.PageData; +import org.thingsboard.server.common.data.page.PageLink; +import org.thingsboard.server.common.data.util.TbPair; import java.util.Collection; import java.util.List; @@ -45,6 +49,12 @@ public interface Dao { List findIdsByTenantIdAndIdOffset(TenantId tenantId, UUID idOffset, int limit); - default EntityType getEntityType() { return null; } + default PageData findAllFields(PageLink pageLink) { + throw new UnsupportedOperationException(); + } + + default EntityType getEntityType() { + return null; + } } diff --git a/dao/src/main/java/org/thingsboard/server/dao/DaoUtil.java b/dao/src/main/java/org/thingsboard/server/dao/DaoUtil.java index 01a72ea58c..a4bf25cd4d 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/DaoUtil.java +++ b/dao/src/main/java/org/thingsboard/server/dao/DaoUtil.java @@ -31,6 +31,7 @@ import org.thingsboard.server.common.data.page.SortOrder; import org.thingsboard.server.dao.model.ToData; import java.util.ArrayList; +import java.util.Arrays; import java.util.Collection; import java.util.Collections; import java.util.Comparator; @@ -85,6 +86,10 @@ public abstract class DaoUtil { return toPageable(pageLink, Collections.emptyMap(), sortOrders); } + public static Pageable toPageable(PageLink pageLink, String... sortColumns) { + return toPageable(pageLink, Collections.emptyMap(), Arrays.stream(sortColumns).map(column -> new SortOrder(column, SortOrder.Direction.ASC)).toList(), false); + } + public static Pageable toPageable(PageLink pageLink, Map columnMap, List sortOrders) { return toPageable(pageLink, columnMap, sortOrders, true); } diff --git a/dao/src/main/java/org/thingsboard/server/dao/TenantEntityDao.java b/dao/src/main/java/org/thingsboard/server/dao/TenantEntityDao.java index d0c54550ba..19fe45ed88 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/TenantEntityDao.java +++ b/dao/src/main/java/org/thingsboard/server/dao/TenantEntityDao.java @@ -15,9 +15,23 @@ */ package org.thingsboard.server.dao; +import org.thingsboard.server.common.data.ObjectType; import org.thingsboard.server.common.data.id.TenantId; +import org.thingsboard.server.common.data.page.PageData; +import org.thingsboard.server.common.data.page.PageLink; -public interface TenantEntityDao { +public interface TenantEntityDao { + + default Long countByTenantId(TenantId tenantId) { + throw new UnsupportedOperationException(); + } + + default PageData findAllByTenantId(TenantId tenantId, PageLink pageLink) { + throw new UnsupportedOperationException(); + } + + default ObjectType getType() { + throw new UnsupportedOperationException(); + } - Long countByTenantId(TenantId tenantId); } diff --git a/dao/src/main/java/org/thingsboard/server/dao/asset/AssetDao.java b/dao/src/main/java/org/thingsboard/server/dao/asset/AssetDao.java index 42ab2e2545..ffc2ebe796 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/asset/AssetDao.java +++ b/dao/src/main/java/org/thingsboard/server/dao/asset/AssetDao.java @@ -36,7 +36,7 @@ import java.util.UUID; * The Interface AssetDao. * */ -public interface AssetDao extends Dao, TenantEntityDao, ExportableEntityDao { +public interface AssetDao extends Dao, TenantEntityDao, ExportableEntityDao { /** * Find asset info by id. @@ -226,4 +226,5 @@ public interface AssetDao extends Dao, TenantEntityDao, ExportableEntityD PageData findAssetsByTenantIdAndEdgeIdAndType(UUID tenantId, UUID edgeId, String type, PageLink pageLink); PageData> getAllAssetTypes(PageLink pageLink); + } diff --git a/dao/src/main/java/org/thingsboard/server/dao/asset/AssetProfileServiceImpl.java b/dao/src/main/java/org/thingsboard/server/dao/asset/AssetProfileServiceImpl.java index 7d4d5300f9..305bba4004 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/asset/AssetProfileServiceImpl.java +++ b/dao/src/main/java/org/thingsboard/server/dao/asset/AssetProfileServiceImpl.java @@ -148,12 +148,11 @@ public class AssetProfileServiceImpl extends CachedVersionedEntityService findAll(TenantId tenantId, EntityId entityId, AttributeScope attributeScope); + PageData findAll(PageLink pageLink); + ListenableFuture save(TenantId tenantId, EntityId entityId, AttributeScope attributeScope, AttributeKvEntry attribute); List> removeAll(TenantId tenantId, EntityId entityId, AttributeScope attributeScope, List keys); diff --git a/dao/src/main/java/org/thingsboard/server/dao/attributes/BaseAttributesService.java b/dao/src/main/java/org/thingsboard/server/dao/attributes/BaseAttributesService.java index 0694178540..512ac5913d 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/attributes/BaseAttributesService.java +++ b/dao/src/main/java/org/thingsboard/server/dao/attributes/BaseAttributesService.java @@ -17,6 +17,8 @@ package org.thingsboard.server.dao.attributes; import com.google.common.util.concurrent.Futures; import com.google.common.util.concurrent.ListenableFuture; +import com.google.common.util.concurrent.MoreExecutors; +import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.apache.commons.lang3.tuple.Pair; import org.springframework.beans.factory.annotation.Value; @@ -24,13 +26,18 @@ import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; import org.springframework.context.annotation.Primary; import org.springframework.stereotype.Service; import org.thingsboard.server.common.data.AttributeScope; +import org.thingsboard.server.common.data.ObjectType; import org.thingsboard.server.common.data.StringUtils; +import org.thingsboard.server.common.data.edqs.AttributeKv; import org.thingsboard.server.common.data.id.DeviceProfileId; import org.thingsboard.server.common.data.id.EntityId; import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.common.data.kv.AttributeKvEntry; +import org.thingsboard.server.common.data.util.TbPair; +import org.thingsboard.server.common.msg.edqs.EdqsService; import org.thingsboard.server.dao.service.Validator; +import java.util.ArrayList; import java.util.Collection; import java.util.List; import java.util.Optional; @@ -45,16 +52,15 @@ import static org.thingsboard.server.dao.attributes.AttributeUtils.validate; @ConditionalOnProperty(prefix = "cache.attributes", value = "enabled", havingValue = "false", matchIfMissing = true) @Primary @Slf4j +@RequiredArgsConstructor public class BaseAttributesService implements AttributesService { + private final AttributesDao attributesDao; + private final EdqsService edqsService; @Value("${sql.attributes.value_no_xss_validation:false}") private boolean valueNoXssValidation; - public BaseAttributesService(AttributesDao attributesDao) { - this.attributesDao = attributesDao; - } - @Override public ListenableFuture> find(TenantId tenantId, EntityId entityId, AttributeScope scope, String attributeKey) { validate(entityId, scope); @@ -98,26 +104,51 @@ public class BaseAttributesService implements AttributesService { public ListenableFuture save(TenantId tenantId, EntityId entityId, AttributeScope scope, AttributeKvEntry attribute) { validate(entityId, scope); AttributeUtils.validate(attribute, valueNoXssValidation); - return attributesDao.save(tenantId, entityId, scope, attribute); + return doSave(tenantId, entityId, scope, attribute); } @Override public ListenableFuture> save(TenantId tenantId, EntityId entityId, AttributeScope scope, List attributes) { validate(entityId, scope); AttributeUtils.validate(attributes, valueNoXssValidation); - List> saveFutures = attributes.stream().map(attribute -> attributesDao.save(tenantId, entityId, scope, attribute)).collect(Collectors.toList()); + List> saveFutures = attributes.stream().map(attribute -> doSave(tenantId, entityId, scope, attribute)).collect(Collectors.toList()); return Futures.allAsList(saveFutures); } + private ListenableFuture doSave(TenantId tenantId, EntityId entityId, AttributeScope scope, AttributeKvEntry attribute) { + ListenableFuture future = attributesDao.save(tenantId, entityId, scope, attribute); + return Futures.transform(future, version -> { + edqsService.onUpdate(tenantId, ObjectType.ATTRIBUTE_KV, new AttributeKv(entityId, scope, attribute, version)); + return version; + }, MoreExecutors.directExecutor()); + } + @Override public ListenableFuture> removeAll(TenantId tenantId, EntityId entityId, AttributeScope scope, List attributeKeys) { validate(entityId, scope); - return Futures.allAsList(attributesDao.removeAll(tenantId, entityId, scope, attributeKeys)); + List>> futures = attributesDao.removeAllWithVersions(tenantId, entityId, scope, attributeKeys); + return Futures.transform(Futures.allAsList(futures), result -> { + List keys = new ArrayList<>(); + for (TbPair keyVersionPair : result) { + String key = keyVersionPair.getFirst(); + Long version = keyVersionPair.getSecond(); + edqsService.onDelete(tenantId, ObjectType.ATTRIBUTE_KV, new AttributeKv(entityId, scope, key, version)); + keys.add(key); + } + return keys; + }, MoreExecutors.directExecutor()); } @Override public int removeAllByEntityId(TenantId tenantId, EntityId entityId) { List> deleted = attributesDao.removeAllByEntityId(tenantId, entityId); + deleted.forEach(attribute -> { + AttributeScope scope = attribute.getKey(); + String key = attribute.getValue(); + if (scope != null && key != null) { + edqsService.onDelete(tenantId, ObjectType.ATTRIBUTE_KV, new AttributeKv(entityId, scope, key, Long.MAX_VALUE)); + } + }); return deleted.size(); } diff --git a/dao/src/main/java/org/thingsboard/server/dao/attributes/CachedAttributesService.java b/dao/src/main/java/org/thingsboard/server/dao/attributes/CachedAttributesService.java index 1ebd5c0ba4..c5ac50c9f1 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/attributes/CachedAttributesService.java +++ b/dao/src/main/java/org/thingsboard/server/dao/attributes/CachedAttributesService.java @@ -24,18 +24,22 @@ import lombok.extern.slf4j.Slf4j; import org.apache.commons.lang3.tuple.Pair; import org.springframework.beans.factory.annotation.Value; import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; +import org.springframework.context.annotation.Lazy; import org.springframework.context.annotation.Primary; import org.springframework.stereotype.Service; import org.thingsboard.server.cache.TbCacheValueWrapper; import org.thingsboard.server.cache.VersionedTbCache; import org.thingsboard.server.common.data.AttributeScope; +import org.thingsboard.server.common.data.ObjectType; import org.thingsboard.server.common.data.StringUtils; +import org.thingsboard.server.common.data.edqs.AttributeKv; import org.thingsboard.server.common.data.id.DeviceProfileId; import org.thingsboard.server.common.data.id.EntityId; import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.common.data.kv.AttributeKvEntry; import org.thingsboard.server.common.data.kv.BaseAttributeKvEntry; import org.thingsboard.server.common.data.util.TbPair; +import org.thingsboard.server.common.msg.edqs.EdqsService; import org.thingsboard.server.common.stats.DefaultCounter; import org.thingsboard.server.common.stats.StatsFactory; import org.thingsboard.server.dao.cache.CacheExecutorService; @@ -67,6 +71,7 @@ public class CachedAttributesService implements AttributesService { private final AttributesDao attributesDao; private final JpaExecutorService jpaExecutorService; private final CacheExecutorService cacheExecutorService; + private final EdqsService edqsService; private final DefaultCounter hitCounter; private final DefaultCounter missCounter; private final VersionedTbCache cache; @@ -79,11 +84,12 @@ public class CachedAttributesService implements AttributesService { public CachedAttributesService(AttributesDao attributesDao, JpaExecutorService jpaExecutorService, - StatsFactory statsFactory, + @Lazy EdqsService edqsService, StatsFactory statsFactory, CacheExecutorService cacheExecutorService, VersionedTbCache cache) { this.attributesDao = attributesDao; this.jpaExecutorService = jpaExecutorService; + this.edqsService = edqsService; this.cacheExecutorService = cacheExecutorService; this.cache = cache; @@ -237,8 +243,10 @@ public class CachedAttributesService implements AttributesService { private ListenableFuture doSave(TenantId tenantId, EntityId entityId, AttributeScope scope, AttributeKvEntry attribute) { ListenableFuture future = attributesDao.save(tenantId, entityId, scope, attribute); - return Futures.transform(future, version -> { - put(entityId, scope, new BaseAttributeKvEntry(((BaseAttributeKvEntry)attribute).getKv(), attribute.getLastUpdateTs(), version)); + return Futures.transform(future, version -> { + BaseAttributeKvEntry attributeKvEntry = new BaseAttributeKvEntry(((BaseAttributeKvEntry) attribute).getKv(), attribute.getLastUpdateTs(), version); + put(entityId, scope, attributeKvEntry); + edqsService.onUpdate(tenantId, ObjectType.ATTRIBUTE_KV, new AttributeKv(entityId, scope, attributeKvEntry, version)); return version; }, cacheExecutor); } @@ -256,7 +264,9 @@ public class CachedAttributesService implements AttributesService { List>> futures = attributesDao.removeAllWithVersions(tenantId, entityId, scope, attributeKeys); return Futures.allAsList(futures.stream().map(future -> Futures.transform(future, keyVersionPair -> { String key = keyVersionPair.getFirst(); - cache.evict(new AttributeCacheKey(scope, entityId, key), keyVersionPair.getSecond()); + Long version = keyVersionPair.getSecond(); + cache.evict(new AttributeCacheKey(scope, entityId, key), version); + edqsService.onDelete(tenantId, ObjectType.ATTRIBUTE_KV, new AttributeKv(entityId, scope, key, version)); return key; }, cacheExecutor)).collect(Collectors.toList())); } @@ -269,6 +279,8 @@ public class CachedAttributesService implements AttributesService { String key = deleted.getValue(); if (scope != null && key != null) { cache.evict(new AttributeCacheKey(scope, entityId, key)); + // FIXME: version is Long.MAX_VALUE because we expect that the entity is deleted and there won't be any attributes after this + edqsService.onDelete(tenantId, ObjectType.ATTRIBUTE_KV, new AttributeKv(entityId, scope, key, Long.MAX_VALUE)); } }); return result.size(); diff --git a/dao/src/main/java/org/thingsboard/server/dao/customer/CustomerDao.java b/dao/src/main/java/org/thingsboard/server/dao/customer/CustomerDao.java index 7888161cd0..da050bdbc6 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/customer/CustomerDao.java +++ b/dao/src/main/java/org/thingsboard/server/dao/customer/CustomerDao.java @@ -30,7 +30,7 @@ import java.util.UUID; /** * The Interface CustomerDao. */ -public interface CustomerDao extends Dao, TenantEntityDao, ExportableEntityDao { +public interface CustomerDao extends Dao, TenantEntityDao, ExportableEntityDao { /** * Save or update customer object diff --git a/dao/src/main/java/org/thingsboard/server/dao/dashboard/DashboardDao.java b/dao/src/main/java/org/thingsboard/server/dao/dashboard/DashboardDao.java index 2878fbd8e3..bc6adbe8b6 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/dashboard/DashboardDao.java +++ b/dao/src/main/java/org/thingsboard/server/dao/dashboard/DashboardDao.java @@ -30,7 +30,7 @@ import java.util.UUID; /** * The Interface DashboardDao. */ -public interface DashboardDao extends Dao, TenantEntityDao, ExportableEntityDao { +public interface DashboardDao extends Dao, TenantEntityDao, ExportableEntityDao { /** * Save or update dashboard object diff --git a/dao/src/main/java/org/thingsboard/server/dao/dashboard/DashboardServiceImpl.java b/dao/src/main/java/org/thingsboard/server/dao/dashboard/DashboardServiceImpl.java index 2e9e37577c..127abb5c3f 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/dashboard/DashboardServiceImpl.java +++ b/dao/src/main/java/org/thingsboard/server/dao/dashboard/DashboardServiceImpl.java @@ -172,7 +172,7 @@ public class DashboardServiceImpl extends AbstractEntityService implements Dashb var saved = dashboardDao.save(tenantId, dashboard); publishEvictEvent(new DashboardTitleEvictEvent(saved.getId())); eventPublisher.publishEvent(SaveEntityEvent.builder().tenantId(tenantId) - .entityId(saved.getId()).created(dashboard.getId() == null).build()); + .entityId(saved.getId()).entity(saved).created(dashboard.getId() == null).build()); if (dashboard.getId() == null) { countService.publishCountEntityEvictEvent(tenantId, EntityType.DASHBOARD); } diff --git a/dao/src/main/java/org/thingsboard/server/dao/device/DeviceDao.java b/dao/src/main/java/org/thingsboard/server/dao/device/DeviceDao.java index 2305e4419c..895eba06a5 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/device/DeviceDao.java +++ b/dao/src/main/java/org/thingsboard/server/dao/device/DeviceDao.java @@ -39,7 +39,7 @@ import java.util.UUID; * The Interface DeviceDao. * */ -public interface DeviceDao extends Dao, TenantEntityDao, ExportableEntityDao { +public interface DeviceDao extends Dao, TenantEntityDao, ExportableEntityDao { /** * Find device info by id. diff --git a/dao/src/main/java/org/thingsboard/server/dao/device/DeviceProfileServiceImpl.java b/dao/src/main/java/org/thingsboard/server/dao/device/DeviceProfileServiceImpl.java index 09e2be01e0..b444ef16ed 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/device/DeviceProfileServiceImpl.java +++ b/dao/src/main/java/org/thingsboard/server/dao/device/DeviceProfileServiceImpl.java @@ -180,13 +180,12 @@ public class DeviceProfileServiceImpl extends CachedVersionedEntityService findAll(PageLink pageLink); } diff --git a/dao/src/main/java/org/thingsboard/server/dao/edge/EdgeDao.java b/dao/src/main/java/org/thingsboard/server/dao/edge/EdgeDao.java index d813d2e3c9..af79d0469a 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/edge/EdgeDao.java +++ b/dao/src/main/java/org/thingsboard/server/dao/edge/EdgeDao.java @@ -35,7 +35,7 @@ import java.util.UUID; * The Interface EdgeDao. * */ -public interface EdgeDao extends Dao, TenantEntityDao { +public interface EdgeDao extends Dao, TenantEntityDao { Edge save(TenantId tenantId, Edge edge); diff --git a/dao/src/main/java/org/thingsboard/server/dao/entity/BaseEntityService.java b/dao/src/main/java/org/thingsboard/server/dao/entity/BaseEntityService.java index 3cdcf5e874..9fcb6c92ae 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/entity/BaseEntityService.java +++ b/dao/src/main/java/org/thingsboard/server/dao/entity/BaseEntityService.java @@ -27,6 +27,8 @@ import org.thingsboard.server.common.data.HasLabel; import org.thingsboard.server.common.data.HasName; import org.thingsboard.server.common.data.HasTitle; import org.thingsboard.server.common.data.StringUtils; +import org.thingsboard.server.common.data.edqs.query.EdqsRequest; +import org.thingsboard.server.common.data.edqs.query.EdqsResponse; import org.thingsboard.server.common.data.id.CustomerId; import org.thingsboard.server.common.data.id.EntityId; import org.thingsboard.server.common.data.id.HasId; @@ -40,9 +42,21 @@ import org.thingsboard.server.common.data.query.EntityDataQuery; import org.thingsboard.server.common.data.query.EntityFilterType; import org.thingsboard.server.common.data.query.EntityKey; import org.thingsboard.server.common.data.query.EntityListFilter; +import org.thingsboard.server.common.data.query.EntityTypeFilter; import org.thingsboard.server.common.data.query.KeyFilter; import org.thingsboard.server.common.data.query.RelationsQueryFilter; +import org.thingsboard.server.common.data.query.StateEntityOwnerFilter; +import org.thingsboard.server.common.msg.edqs.EdqsService; +import org.thingsboard.server.dao.asset.AssetService; +import org.thingsboard.server.dao.customer.CustomerService; +import org.thingsboard.server.dao.dashboard.DashboardService; +import org.thingsboard.server.dao.device.DeviceService; +import org.thingsboard.server.dao.edge.EdgeService; +import org.thingsboard.server.dao.entityview.EntityViewService; import org.thingsboard.server.dao.exception.IncorrectParameterException; +import org.thingsboard.server.dao.sql.alarm.AlarmRepository; +import org.thingsboard.server.dao.sql.query.EntityMapping; +import org.thingsboard.server.dao.user.UserService; import java.util.ArrayList; import java.util.Collections; @@ -50,9 +64,12 @@ import java.util.HashSet; import java.util.List; import java.util.Optional; import java.util.Set; +import java.util.concurrent.ExecutionException; import java.util.function.Function; import java.util.stream.Collectors; +import static org.thingsboard.server.common.data.query.EntityFilterType.ENTITY_GROUP_NAME; +import static org.thingsboard.server.common.data.query.EntityFilterType.ENTITY_TYPE; import static org.thingsboard.server.common.data.id.EntityId.NULL_UUID; import static org.thingsboard.server.dao.service.Validator.validateEntityDataPageLink; import static org.thingsboard.server.dao.service.Validator.validateId; @@ -79,12 +96,24 @@ public class BaseEntityService extends AbstractEntityService implements EntitySe @Lazy EntityServiceRegistry entityServiceRegistry; + @Autowired @Lazy + private EdqsService edqsService; + @Override public long countEntitiesByQuery(TenantId tenantId, CustomerId customerId, EntityCountQuery query) { log.trace("Executing countEntitiesByQuery, tenantId [{}], customerId [{}], query [{}]", tenantId, customerId, query); validateId(tenantId, id -> INCORRECT_TENANT_ID + id); validateId(customerId, id -> INCORRECT_CUSTOMER_ID + id); validateEntityCountQuery(query); + + if (edqsService.isApiEnabled() && validForEdqs(query)) { // TODO: separate boolean param whether to use in dashboards; but sync to edqs - always + EdqsRequest request = EdqsRequest.builder() + .entityCountQuery(query) + .userPermissions(userPermissions) + .build(); + EdqsResponse response = processEdqsRequest(tenantId, customerId, request); + return response.getEntityCountQueryResult(); + } return this.entityQueryDao.countEntitiesByQuery(tenantId, customerId, query); } @@ -95,6 +124,15 @@ public class BaseEntityService extends AbstractEntityService implements EntitySe validateId(customerId, id -> INCORRECT_CUSTOMER_ID + id); validateEntityDataQuery(query); + if (edqsService.isApiEnabled() && validForEdqs(query)) { // TODO: separate boolean param whether to use in dashboards; but sync to edqs - always + EdqsRequest request = EdqsRequest.builder() + .entityDataQuery(query) + .userPermissions(userPermissions) + .build(); + EdqsResponse response = processEdqsRequest(tenantId, customerId, request); + return response.getEntityDataQueryResult(); + } + if (!isValidForOptimization(query)) { return this.entityQueryDao.findEntityDataByQuery(tenantId, customerId, query); } @@ -110,6 +148,25 @@ public class BaseEntityService extends AbstractEntityService implements EntitySe return new PageData<>(result, entityDataByQuery.getTotalPages(), entityDataByQuery.getTotalElements(), entityDataByQuery.hasNext()); } + private boolean validForEdqs(EntityCountQuery query) { + return !(query.getEntityFilter() instanceof StateEntityOwnerFilter filter) || !EntityType.ALARM.equals(filter.getSingleEntity().getEntityType()); + } + + private EdqsResponse processEdqsRequest(TenantId tenantId, CustomerId customerId, EdqsRequest request) { + EdqsResponse response; + try { + log.info("Sending request to EDQS: {}", request); + response = edqsService.processRequest(tenantId, customerId, request).get(); + } catch (InterruptedException | ExecutionException e) { + throw new RuntimeException(e); + } + log.info("Received response from EDQS: {}", response); + if (response.getError() != null) { + throw new RuntimeException(response.getError()); + } + return response; + } + @Override public Optional fetchEntityName(TenantId tenantId, EntityId entityId) { log.trace("Executing fetchEntityName [{}]", entityId); @@ -184,6 +241,10 @@ public class BaseEntityService extends AbstractEntityService implements EntitySe throw new IncorrectParameterException("Query entity filter type must be specified."); } else if (query.getEntityFilter().getType().equals(EntityFilterType.RELATIONS_QUERY)) { validateRelationQuery((RelationsQueryFilter) query.getEntityFilter()); + } else if (query.getEntityFilter().getType().equals(ENTITY_TYPE)) { + validateEntityTypeQuery((EntityTypeFilter) query.getEntityFilter()); + } else if (query.getEntityFilter().getType().equals(ENTITY_GROUP_NAME)) { + validateGroupNameQuery((EntityGroupNameFilter) query.getEntityFilter()); } } @@ -192,6 +253,18 @@ public class BaseEntityService extends AbstractEntityService implements EntitySe validateEntityDataPageLink(query.getPageLink()); } + private static void validateEntityTypeQuery(EntityTypeFilter filter) { + if (filter.getEntityType() == null) { + throw new IncorrectParameterException("Entity type is required"); + } + } + + private static void validateGroupNameQuery(EntityGroupNameFilter filter) { + if (filter.getGroupType() == null) { + throw new IncorrectParameterException("Group type is required"); + } + } + private static void validateRelationQuery(RelationsQueryFilter queryFilter) { if (queryFilter.isMultiRoot() && queryFilter.getMultiRootEntitiesType() == null) { throw new IncorrectParameterException("Multi-root relation query filter should contain 'multiRootEntitiesType'"); diff --git a/dao/src/main/java/org/thingsboard/server/dao/entity/EntityDaoRegistry.java b/dao/src/main/java/org/thingsboard/server/dao/entity/EntityDaoRegistry.java index 7b2f2d42c1..24f8aa4d77 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/entity/EntityDaoRegistry.java +++ b/dao/src/main/java/org/thingsboard/server/dao/entity/EntityDaoRegistry.java @@ -18,7 +18,9 @@ package org.thingsboard.server.dao.entity; import lombok.extern.slf4j.Slf4j; import org.springframework.stereotype.Service; import org.thingsboard.server.common.data.EntityType; +import org.thingsboard.server.common.data.ObjectType; import org.thingsboard.server.dao.Dao; +import org.thingsboard.server.dao.TenantEntityDao; import java.util.EnumMap; import java.util.List; @@ -26,26 +28,39 @@ import java.util.Map; @Service @Slf4j +@SuppressWarnings({"unchecked"}) public class EntityDaoRegistry { - private final Map> daos = new EnumMap<>(EntityType.class); + private final Map> tenantEntityDaos = new EnumMap<>(ObjectType.class); + private final Map> entityDaos = new EnumMap<>(EntityType.class); - private EntityDaoRegistry(List> daos) { - daos.forEach(dao -> { - EntityType entityType = dao.getEntityType(); - if (entityType != null) { - this.daos.put(entityType, dao); + private EntityDaoRegistry(List> entityDaos, List> tenantEntityDaos) { + entityDaos.forEach(dao -> { + if (dao.getEntityType() != null) { + this.entityDaos.put(dao.getEntityType(), dao); + } + }); + tenantEntityDaos.forEach(dao -> { + if (dao.getType() != null) { + this.tenantEntityDaos.put(dao.getType(), dao); } }); } - @SuppressWarnings("unchecked") public Dao getDao(EntityType entityType) { - Dao dao = (Dao) daos.get(entityType); + Dao dao = (Dao) entityDaos.get(entityType); if (dao == null) { throw new IllegalArgumentException("Missing dao for entity type " + entityType); } return dao; } + public TenantEntityDao getTenantEntityDao(ObjectType objectType) { + TenantEntityDao dao = tenantEntityDaos.get(objectType); + if (dao == null) { + throw new IllegalArgumentException("Missing tenant entity dao for entity type " + objectType); + } + return (TenantEntityDao) dao; + } + } diff --git a/dao/src/main/java/org/thingsboard/server/dao/entityview/EntityViewServiceImpl.java b/dao/src/main/java/org/thingsboard/server/dao/entityview/EntityViewServiceImpl.java index 43b5e5cdfb..2dc3dab627 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/entityview/EntityViewServiceImpl.java +++ b/dao/src/main/java/org/thingsboard/server/dao/entityview/EntityViewServiceImpl.java @@ -123,7 +123,7 @@ public class EntityViewServiceImpl extends CachedVersionedEntityService { private final T oldEntity; private final EntityId entityId; private final Boolean created; + private final Boolean broadcastEvent; } diff --git a/dao/src/main/java/org/thingsboard/server/dao/model/ModelConstants.java b/dao/src/main/java/org/thingsboard/server/dao/model/ModelConstants.java index d404c3128c..f762be0429 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/model/ModelConstants.java +++ b/dao/src/main/java/org/thingsboard/server/dao/model/ModelConstants.java @@ -281,6 +281,8 @@ public class ModelConstants { public static final String ALARM_COMMENT_TYPE = "type"; public static final String ALARM_COMMENT_COMMENT = "comment"; + public static final String ALARM_TYPES_TABLE_NAME = "alarm_types"; + /** * Entity relation constants. */ diff --git a/dao/src/main/java/org/thingsboard/server/dao/model/sql/AlarmTypeCompositeKey.java b/dao/src/main/java/org/thingsboard/server/dao/model/sql/AlarmTypeCompositeKey.java new file mode 100644 index 0000000000..60f5232142 --- /dev/null +++ b/dao/src/main/java/org/thingsboard/server/dao/model/sql/AlarmTypeCompositeKey.java @@ -0,0 +1,33 @@ +/** + * Copyright © 2016-2024 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.dao.model.sql; + +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; + +import java.io.Serializable; +import java.util.UUID; + +@Data +@NoArgsConstructor +@AllArgsConstructor +public class AlarmTypeCompositeKey implements Serializable { + + private UUID tenantId; + private String type; + +} diff --git a/dao/src/main/java/org/thingsboard/server/dao/model/sql/AlarmTypeEntity.java b/dao/src/main/java/org/thingsboard/server/dao/model/sql/AlarmTypeEntity.java new file mode 100644 index 0000000000..729d7a44f0 --- /dev/null +++ b/dao/src/main/java/org/thingsboard/server/dao/model/sql/AlarmTypeEntity.java @@ -0,0 +1,65 @@ +/** + * Copyright © 2016-2024 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.dao.model.sql; + +import jakarta.persistence.Column; +import jakarta.persistence.Entity; +import jakarta.persistence.Id; +import jakarta.persistence.IdClass; +import jakarta.persistence.Table; +import lombok.Data; +import org.thingsboard.server.common.data.alarm.AlarmType; +import org.thingsboard.server.common.data.id.TenantId; +import org.thingsboard.server.dao.model.ModelConstants; +import org.thingsboard.server.dao.model.ToData; + +import java.util.UUID; + +@Data +@Entity +@Table(name = ModelConstants.ALARM_TYPES_TABLE_NAME) +@IdClass(AlarmTypeCompositeKey.class) +public class AlarmTypeEntity implements ToData { + + @Id + @Column(name = ModelConstants.TENANT_ID_PROPERTY, nullable = false) + private UUID tenantId; + + @Id + @Column(name = ModelConstants.ALARM_TYPE_PROPERTY, nullable = false) + private String type; + + public AlarmTypeEntity() {} + + public AlarmTypeEntity(AlarmType alarmType) { + setTenantId(alarmType.getTenantId().getId()); + setType(alarmType.getType()); + } + + public AlarmTypeEntity(UUID tenantId, String type) { + this.tenantId = tenantId; + this.type = type; + } + + @Override + public AlarmType toData() { + AlarmType alarmType = new AlarmType(); + alarmType.setTenantId(TenantId.fromUUID(tenantId)); + alarmType.setType(type); + return alarmType; + } + +} diff --git a/dao/src/main/java/org/thingsboard/server/dao/model/sqlts/latest/TsKvLatestEntity.java b/dao/src/main/java/org/thingsboard/server/dao/model/sqlts/latest/TsKvLatestEntity.java index 23fe580fa3..05930fbe00 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/model/sqlts/latest/TsKvLatestEntity.java +++ b/dao/src/main/java/org/thingsboard/server/dao/model/sqlts/latest/TsKvLatestEntity.java @@ -26,6 +26,7 @@ import jakarta.persistence.SqlResultSetMapping; import jakarta.persistence.SqlResultSetMappings; import jakarta.persistence.Table; import lombok.Data; +import org.thingsboard.server.common.data.kv.TsKvEntry; import org.thingsboard.server.dao.model.sql.AbstractTsKvEntity; import org.thingsboard.server.dao.sqlts.latest.SearchTsKvLatestRepository; @@ -91,4 +92,12 @@ public final class TsKvLatestEntity extends AbstractTsKvEntity { this.strKey = strKey; this.version = version; } + + @Override + public TsKvEntry toData() { + TsKvEntry tsKvEntry = super.toData(); + tsKvEntry.setVersion(version); + return tsKvEntry; + } + } diff --git a/dao/src/main/java/org/thingsboard/server/dao/notification/NotificationTargetDao.java b/dao/src/main/java/org/thingsboard/server/dao/notification/NotificationTargetDao.java index 94126ebd1b..d92d7cfd0f 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/notification/NotificationTargetDao.java +++ b/dao/src/main/java/org/thingsboard/server/dao/notification/NotificationTargetDao.java @@ -28,7 +28,7 @@ import org.thingsboard.server.dao.TenantEntityDao; import java.util.List; -public interface NotificationTargetDao extends Dao, TenantEntityDao, ExportableEntityDao { +public interface NotificationTargetDao extends Dao, TenantEntityDao, ExportableEntityDao { PageData findByTenantIdAndPageLink(TenantId tenantId, PageLink pageLink); diff --git a/dao/src/main/java/org/thingsboard/server/dao/ota/OtaPackageDao.java b/dao/src/main/java/org/thingsboard/server/dao/ota/OtaPackageDao.java index e83650b3b5..742ad45ffa 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/ota/OtaPackageDao.java +++ b/dao/src/main/java/org/thingsboard/server/dao/ota/OtaPackageDao.java @@ -21,5 +21,7 @@ import org.thingsboard.server.dao.Dao; import org.thingsboard.server.dao.TenantEntityWithDataDao; public interface OtaPackageDao extends Dao, TenantEntityWithDataDao { + Long sumDataSizeByTenantId(TenantId tenantId); + } diff --git a/dao/src/main/java/org/thingsboard/server/dao/queue/BaseQueueStatsService.java b/dao/src/main/java/org/thingsboard/server/dao/queue/BaseQueueStatsService.java index 69c6a7c418..efc9dab41b 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/queue/BaseQueueStatsService.java +++ b/dao/src/main/java/org/thingsboard/server/dao/queue/BaseQueueStatsService.java @@ -27,6 +27,8 @@ import org.thingsboard.server.common.data.page.PageData; import org.thingsboard.server.common.data.page.PageLink; import org.thingsboard.server.common.data.queue.QueueStats; import org.thingsboard.server.dao.entity.AbstractEntityService; +import org.thingsboard.server.dao.eventsourcing.DeleteEntityEvent; +import org.thingsboard.server.dao.eventsourcing.SaveEntityEvent; import org.thingsboard.server.dao.service.DataValidator; import org.thingsboard.server.dao.service.Validator; @@ -51,7 +53,10 @@ public class BaseQueueStatsService extends AbstractEntityService implements Queu public QueueStats save(TenantId tenantId, QueueStats queueStats) { log.trace("Executing save [{}]", queueStats); queueStatsValidator.validate(queueStats, QueueStats::getTenantId); - return queueStatsDao.save(tenantId, queueStats); + QueueStats savedQueueStats = queueStatsDao.save(tenantId, queueStats); + eventPublisher.publishEvent(SaveEntityEvent.builder().tenantId(savedQueueStats.getTenantId()).entityId(savedQueueStats.getId()) + .entity(savedQueueStats).created(queueStats.getId() == null).build()); + return savedQueueStats; } @Override @@ -93,6 +98,7 @@ public class BaseQueueStatsService extends AbstractEntityService implements Queu @Override public void deleteEntity(TenantId tenantId, EntityId id, boolean force) { queueStatsDao.removeById(tenantId, id.getId()); + eventPublisher.publishEvent(DeleteEntityEvent.builder().tenantId(tenantId).entityId(id).build()); } @Override diff --git a/dao/src/main/java/org/thingsboard/server/dao/relation/RelationDao.java b/dao/src/main/java/org/thingsboard/server/dao/relation/RelationDao.java index d668f51dbd..d29b57c5c3 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/relation/RelationDao.java +++ b/dao/src/main/java/org/thingsboard/server/dao/relation/RelationDao.java @@ -18,6 +18,8 @@ package org.thingsboard.server.dao.relation; import com.google.common.util.concurrent.ListenableFuture; import org.thingsboard.server.common.data.id.EntityId; import org.thingsboard.server.common.data.id.TenantId; +import org.thingsboard.server.common.data.page.PageData; +import org.thingsboard.server.common.data.page.PageLink; import org.thingsboard.server.common.data.relation.EntityRelation; import org.thingsboard.server.common.data.relation.RelationTypeGroup; import org.thingsboard.server.common.data.rule.RuleChainType; @@ -41,6 +43,8 @@ public interface RelationDao { List findAllByToAndType(TenantId tenantId, EntityId to, String relationType, RelationTypeGroup typeGroup); + PageData findAll(PageLink pageLink); + ListenableFuture checkRelationAsync(TenantId tenantId, EntityId from, EntityId to, String relationType, RelationTypeGroup typeGroup); boolean checkRelation(TenantId tenantId, EntityId from, EntityId to, String relationType, RelationTypeGroup typeGroup); diff --git a/dao/src/main/java/org/thingsboard/server/dao/rule/BaseRuleChainService.java b/dao/src/main/java/org/thingsboard/server/dao/rule/BaseRuleChainService.java index 0e51cf256a..58cfdfe02a 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/rule/BaseRuleChainService.java +++ b/dao/src/main/java/org/thingsboard/server/dao/rule/BaseRuleChainService.java @@ -124,10 +124,9 @@ public class BaseRuleChainService extends AbstractEntityService implements RuleC if (ruleChain.getId() == null) { entityCountService.publishCountEntityEvictEvent(ruleChain.getTenantId(), EntityType.RULE_CHAIN); } - if (publishSaveEvent) { - eventPublisher.publishEvent(SaveEntityEvent.builder().tenantId(savedRuleChain.getTenantId()) - .entity(savedRuleChain).entityId(savedRuleChain.getId()).created(ruleChain.getId() == null).build()); - } + eventPublisher.publishEvent(SaveEntityEvent.builder().tenantId(savedRuleChain.getTenantId()) + .entity(savedRuleChain).entityId(savedRuleChain.getId()).created(ruleChain.getId() == null) + .broadcastEvent(publishSaveEvent).build()); return savedRuleChain; } catch (Exception e) { checkConstraintViolation(e, "rule_chain_external_id_unq_key", "Rule Chain with such external id already exists!"); @@ -289,9 +288,8 @@ public class BaseRuleChainService extends AbstractEntityService implements RuleC relationService.saveRelations(tenantId, relations); } ruleChain = ruleChainDao.save(tenantId, ruleChain); - if (publishSaveEvent) { - eventPublisher.publishEvent(SaveEntityEvent.builder().tenantId(tenantId).entity(ruleChain).entityId(ruleChain.getId()).build()); - } + eventPublisher.publishEvent(SaveEntityEvent.builder().tenantId(tenantId).entity(ruleChain) + .entityId(ruleChain.getId()).broadcastEvent(publishSaveEvent).build()); return RuleChainUpdateResult.successful(updatedRuleNodes); } diff --git a/dao/src/main/java/org/thingsboard/server/dao/rule/RuleChainDao.java b/dao/src/main/java/org/thingsboard/server/dao/rule/RuleChainDao.java index a394255b2e..0a8d449afe 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/rule/RuleChainDao.java +++ b/dao/src/main/java/org/thingsboard/server/dao/rule/RuleChainDao.java @@ -31,7 +31,7 @@ import java.util.UUID; /** * Created by igor on 3/12/18. */ -public interface RuleChainDao extends Dao, TenantEntityDao, ExportableEntityDao { +public interface RuleChainDao extends Dao, TenantEntityDao, ExportableEntityDao { /** * Find rule chains by tenantId and page link. diff --git a/dao/src/main/java/org/thingsboard/server/dao/sql/alarm/AlarmCommentRepository.java b/dao/src/main/java/org/thingsboard/server/dao/sql/alarm/AlarmCommentRepository.java index d0faee3c9c..991147cedb 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/sql/alarm/AlarmCommentRepository.java +++ b/dao/src/main/java/org/thingsboard/server/dao/sql/alarm/AlarmCommentRepository.java @@ -35,5 +35,9 @@ public interface AlarmCommentRepository extends JpaRepository findAllByAlarmId(@Param("alarmId") UUID alarmId, - Pageable pageable); + Pageable pageable); + + @Query("SELECT c FROM AlarmCommentEntity c WHERE c.userId IN (SELECT u.id FROM UserEntity u WHERE u.tenantId = :tenantId)") + Page findByTenantId(@Param("tenantId") UUID tenantId, Pageable pageable); + } diff --git a/dao/src/main/java/org/thingsboard/server/dao/sql/alarm/AlarmRepository.java b/dao/src/main/java/org/thingsboard/server/dao/sql/alarm/AlarmRepository.java index 6083dac6cf..9f57ab6e52 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/sql/alarm/AlarmRepository.java +++ b/dao/src/main/java/org/thingsboard/server/dao/sql/alarm/AlarmRepository.java @@ -414,4 +414,6 @@ public interface AlarmRepository extends JpaRepository { @Param("alarmSeverities") List alarmSeverities, int limit); + Page findByTenantId(UUID tenantId, Pageable pageable); + } diff --git a/dao/src/main/java/org/thingsboard/server/dao/sql/alarm/AlarmTypeRepository.java b/dao/src/main/java/org/thingsboard/server/dao/sql/alarm/AlarmTypeRepository.java new file mode 100644 index 0000000000..b84542e3c3 --- /dev/null +++ b/dao/src/main/java/org/thingsboard/server/dao/sql/alarm/AlarmTypeRepository.java @@ -0,0 +1,30 @@ +/** + * Copyright © 2016-2024 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.dao.sql.alarm; + +import org.springframework.data.domain.Page; +import org.springframework.data.domain.Pageable; +import org.springframework.data.jpa.repository.JpaRepository; +import org.thingsboard.server.dao.model.sql.AlarmTypeCompositeKey; +import org.thingsboard.server.dao.model.sql.AlarmTypeEntity; + +import java.util.UUID; + +public interface AlarmTypeRepository extends JpaRepository { + + Page findByTenantId(UUID tenantId, Pageable pageable); + +} diff --git a/dao/src/main/java/org/thingsboard/server/dao/sql/alarm/EntityAlarmRepository.java b/dao/src/main/java/org/thingsboard/server/dao/sql/alarm/EntityAlarmRepository.java index ccf82e1b5b..41e6f3dcc5 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/sql/alarm/EntityAlarmRepository.java +++ b/dao/src/main/java/org/thingsboard/server/dao/sql/alarm/EntityAlarmRepository.java @@ -15,6 +15,8 @@ */ package org.thingsboard.server.dao.sql.alarm; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.Pageable; import org.springframework.data.jpa.repository.JpaRepository; import org.springframework.data.jpa.repository.Modifying; import org.springframework.data.jpa.repository.Query; @@ -42,4 +44,6 @@ public interface EntityAlarmRepository extends JpaRepository findAllByEntityId(UUID entityId); + Page findByTenantId(UUID tenantId, Pageable pageable); + } diff --git a/dao/src/main/java/org/thingsboard/server/dao/sql/alarm/JpaAlarmCommentDao.java b/dao/src/main/java/org/thingsboard/server/dao/sql/alarm/JpaAlarmCommentDao.java index 3d366a5a56..9d74b9bf8e 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/sql/alarm/JpaAlarmCommentDao.java +++ b/dao/src/main/java/org/thingsboard/server/dao/sql/alarm/JpaAlarmCommentDao.java @@ -22,6 +22,7 @@ import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Value; import org.springframework.data.jpa.repository.JpaRepository; import org.springframework.stereotype.Component; +import org.thingsboard.server.common.data.ObjectType; import org.thingsboard.server.common.data.alarm.AlarmComment; import org.thingsboard.server.common.data.alarm.AlarmCommentInfo; import org.thingsboard.server.common.data.id.AlarmId; @@ -29,6 +30,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.dao.DaoUtil; +import org.thingsboard.server.dao.TenantEntityDao; import org.thingsboard.server.dao.alarm.AlarmCommentDao; import org.thingsboard.server.dao.model.sql.AlarmCommentEntity; import org.thingsboard.server.dao.sql.JpaPartitionedAbstractDao; @@ -44,7 +46,7 @@ import static org.thingsboard.server.dao.model.ModelConstants.ALARM_COMMENT_TABL @Component @SqlDao @RequiredArgsConstructor -public class JpaAlarmCommentDao extends JpaPartitionedAbstractDao implements AlarmCommentDao { +public class JpaAlarmCommentDao extends JpaPartitionedAbstractDao implements AlarmCommentDao, TenantEntityDao { private final SqlPartitioningRepository partitioningRepository; @Value("${sql.alarm_comments.partition_size:168}") private int partitionSizeInHours; @@ -76,6 +78,11 @@ public class JpaAlarmCommentDao extends JpaPartitionedAbstractDao findAllByTenantId(TenantId tenantId, PageLink pageLink) { + return DaoUtil.toPageData(alarmCommentRepository.findByTenantId(tenantId.getId(), DaoUtil.toPageable(pageLink))); + } + @Override protected Class getEntityClass() { return AlarmCommentEntity.class; @@ -85,4 +92,10 @@ public class JpaAlarmCommentDao extends JpaPartitionedAbstractDao getRepository() { return alarmCommentRepository; } + + @Override + public ObjectType getType() { + return ObjectType.ALARM_COMMENT; + } + } diff --git a/dao/src/main/java/org/thingsboard/server/dao/sql/alarm/JpaAlarmDao.java b/dao/src/main/java/org/thingsboard/server/dao/sql/alarm/JpaAlarmDao.java index e433dfbd86..7aea2adbec 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/sql/alarm/JpaAlarmDao.java +++ b/dao/src/main/java/org/thingsboard/server/dao/sql/alarm/JpaAlarmDao.java @@ -29,6 +29,7 @@ import org.springframework.util.CollectionUtils; import org.thingsboard.common.util.JacksonUtil; import org.thingsboard.server.common.data.EntitySubtype; import org.thingsboard.server.common.data.EntityType; +import org.thingsboard.server.common.data.ObjectType; import org.thingsboard.server.common.data.StringUtils; import org.thingsboard.server.common.data.alarm.Alarm; import org.thingsboard.server.common.data.alarm.AlarmApiCallResult; @@ -57,6 +58,7 @@ import org.thingsboard.server.common.data.query.AlarmDataQuery; import org.thingsboard.server.common.data.query.OriginatorAlarmFilter; import org.thingsboard.server.common.data.util.TbPair; import org.thingsboard.server.dao.DaoUtil; +import org.thingsboard.server.dao.TenantEntityDao; import org.thingsboard.server.dao.alarm.AlarmDao; import org.thingsboard.server.dao.model.ModelConstants; import org.thingsboard.server.dao.model.sql.AlarmEntity; @@ -84,7 +86,7 @@ import static org.thingsboard.server.dao.DaoUtil.toPageable; @Slf4j @Component @SqlDao -public class JpaAlarmDao extends JpaAbstractDao implements AlarmDao { +public class JpaAlarmDao extends JpaAbstractDao implements AlarmDao, TenantEntityDao { @Autowired private AlarmRepository alarmRepository; @@ -551,9 +553,19 @@ public class JpaAlarmDao extends JpaAbstractDao implements A } } + @Override + public PageData findAllByTenantId(TenantId tenantId, PageLink pageLink) { + return DaoUtil.toPageData(alarmRepository.findByTenantId(tenantId.getId(), DaoUtil.toPageable(pageLink))); + } + @Override public EntityType getEntityType() { return EntityType.ALARM; } + @Override + public ObjectType getType() { + return ObjectType.ALARM; + } + } diff --git a/dao/src/main/java/org/thingsboard/server/dao/sql/alarm/JpaAlarmTypeDao.java b/dao/src/main/java/org/thingsboard/server/dao/sql/alarm/JpaAlarmTypeDao.java new file mode 100644 index 0000000000..532f06eb03 --- /dev/null +++ b/dao/src/main/java/org/thingsboard/server/dao/sql/alarm/JpaAlarmTypeDao.java @@ -0,0 +1,46 @@ +/** + * Copyright © 2016-2024 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.dao.sql.alarm; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Component; +import org.thingsboard.server.common.data.ObjectType; +import org.thingsboard.server.common.data.alarm.AlarmType; +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.dao.DaoUtil; +import org.thingsboard.server.dao.TenantEntityDao; +import org.thingsboard.server.dao.util.SqlDao; + +@Component +@SqlDao +public class JpaAlarmTypeDao implements TenantEntityDao { + + @Autowired + private AlarmTypeRepository alarmTypeRepository; + + @Override + public PageData findAllByTenantId(TenantId tenantId, PageLink pageLink) { + return DaoUtil.toPageData(alarmTypeRepository.findByTenantId(tenantId.getId(), DaoUtil.toPageable(pageLink, "tenantId", "type"))); + } + + @Override + public ObjectType getType() { + return ObjectType.ALARM_TYPE; + } + +} diff --git a/dao/src/main/java/org/thingsboard/server/dao/sql/alarm/JpaEntityAlarmDao.java b/dao/src/main/java/org/thingsboard/server/dao/sql/alarm/JpaEntityAlarmDao.java new file mode 100644 index 0000000000..6bbc59559a --- /dev/null +++ b/dao/src/main/java/org/thingsboard/server/dao/sql/alarm/JpaEntityAlarmDao.java @@ -0,0 +1,46 @@ +/** + * Copyright © 2016-2024 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.dao.sql.alarm; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Component; +import org.thingsboard.server.common.data.ObjectType; +import org.thingsboard.server.common.data.alarm.EntityAlarm; +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.dao.DaoUtil; +import org.thingsboard.server.dao.TenantEntityDao; +import org.thingsboard.server.dao.util.SqlDao; + +@Component +@SqlDao +public class JpaEntityAlarmDao implements TenantEntityDao { + + @Autowired + private EntityAlarmRepository entityAlarmRepository; + + @Override + public PageData findAllByTenantId(TenantId tenantId, PageLink pageLink) { + return DaoUtil.toPageData(entityAlarmRepository.findByTenantId(tenantId.getId(), DaoUtil.toPageable(pageLink, "entityId", "alarmId"))); + } + + @Override + public ObjectType getType() { + return ObjectType.ENTITY_ALARM; + } + +} diff --git a/dao/src/main/java/org/thingsboard/server/dao/sql/asset/AssetProfileRepository.java b/dao/src/main/java/org/thingsboard/server/dao/sql/asset/AssetProfileRepository.java index 372201bc47..bd3f35cf76 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/sql/asset/AssetProfileRepository.java +++ b/dao/src/main/java/org/thingsboard/server/dao/sql/asset/AssetProfileRepository.java @@ -22,6 +22,7 @@ import org.springframework.data.jpa.repository.Query; import org.springframework.data.repository.query.Param; import org.thingsboard.server.common.data.EntityInfo; import org.thingsboard.server.common.data.asset.AssetProfileInfo; +import org.thingsboard.server.common.data.edqs.fields.AssetProfileFields; import org.thingsboard.server.dao.ExportableEntityRepository; import org.thingsboard.server.dao.model.sql.AssetProfileEntity; @@ -81,4 +82,8 @@ public interface AssetProfileRepository extends JpaRepository findAllTenantAssetProfileNames(@Param("tenantId") UUID tenantId); + @Query("SELECT new org.thingsboard.server.common.data.edqs.fields.AssetProfileFields(a.id, a.createdTime, a.tenantId," + + "a.name, a.version, a.isDefault) FROM AssetProfileEntity a") + Page findAllFields(Pageable pageable); + } diff --git a/dao/src/main/java/org/thingsboard/server/dao/sql/asset/AssetRepository.java b/dao/src/main/java/org/thingsboard/server/dao/sql/asset/AssetRepository.java index bf4798fa4d..1be4fc6075 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/sql/asset/AssetRepository.java +++ b/dao/src/main/java/org/thingsboard/server/dao/sql/asset/AssetRepository.java @@ -20,6 +20,7 @@ import org.springframework.data.domain.Pageable; import org.springframework.data.jpa.repository.JpaRepository; import org.springframework.data.jpa.repository.Query; import org.springframework.data.repository.query.Param; +import org.thingsboard.server.common.data.edqs.fields.AssetFields; import org.thingsboard.server.common.data.util.TbPair; import org.thingsboard.server.dao.ExportableEntityRepository; import org.thingsboard.server.dao.model.sql.AssetEntity; @@ -216,4 +217,9 @@ public interface AssetRepository extends JpaRepository, Expor @Query(value = "SELECT DISTINCT new org.thingsboard.server.common.data.util.TbPair(a.tenantId , a.type) FROM AssetEntity a") Page> getAllAssetTypes(Pageable pageable); + + @Query("SELECT new org.thingsboard.server.common.data.edqs.fields.AssetFields(a.id, a.createdTime, a.tenantId, a.customerId," + + "a.name, a.version, a.type, a.label, a.assetProfileId, a.additionalInfo) FROM AssetEntity a") + Page findAllFields(Pageable pageable); + } diff --git a/dao/src/main/java/org/thingsboard/server/dao/sql/asset/JpaAssetDao.java b/dao/src/main/java/org/thingsboard/server/dao/sql/asset/JpaAssetDao.java index 0ef4370f30..569c13f376 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/sql/asset/JpaAssetDao.java +++ b/dao/src/main/java/org/thingsboard/server/dao/sql/asset/JpaAssetDao.java @@ -22,8 +22,10 @@ import org.springframework.data.jpa.repository.JpaRepository; import org.springframework.stereotype.Component; import org.thingsboard.server.common.data.EntitySubtype; import org.thingsboard.server.common.data.EntityType; +import org.thingsboard.server.common.data.ObjectType; import org.thingsboard.server.common.data.asset.Asset; import org.thingsboard.server.common.data.asset.AssetInfo; +import org.thingsboard.server.common.data.edqs.fields.AssetFields; import org.thingsboard.server.common.data.id.AssetId; import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.common.data.page.PageData; @@ -267,9 +269,24 @@ public class JpaAssetDao extends JpaAbstractDao implements A .map(AssetId::new).orElse(null); } + @Override + public PageData findAllByTenantId(TenantId tenantId, PageLink pageLink) { + return findByTenantId(tenantId.getId(), pageLink); + } + + @Override + public PageData findAllFields(PageLink pageLink) { + return DaoUtil.pageToPageData(assetRepository.findAllFields(DaoUtil.toPageable(pageLink))); + } + @Override public EntityType getEntityType() { return EntityType.ASSET; } + @Override + public ObjectType getType() { + return ObjectType.ASSET; + } + } diff --git a/dao/src/main/java/org/thingsboard/server/dao/sql/asset/JpaAssetProfileDao.java b/dao/src/main/java/org/thingsboard/server/dao/sql/asset/JpaAssetProfileDao.java index 6ed74a5aa9..eb2fb6a9ed 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/sql/asset/JpaAssetProfileDao.java +++ b/dao/src/main/java/org/thingsboard/server/dao/sql/asset/JpaAssetProfileDao.java @@ -21,13 +21,16 @@ import org.springframework.data.jpa.repository.JpaRepository; import org.springframework.stereotype.Component; import org.thingsboard.server.common.data.EntityInfo; import org.thingsboard.server.common.data.EntityType; +import org.thingsboard.server.common.data.ObjectType; import org.thingsboard.server.common.data.asset.AssetProfile; import org.thingsboard.server.common.data.asset.AssetProfileInfo; +import org.thingsboard.server.common.data.edqs.fields.AssetProfileFields; import org.thingsboard.server.common.data.id.AssetProfileId; 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.dao.DaoUtil; +import org.thingsboard.server.dao.TenantEntityDao; import org.thingsboard.server.dao.asset.AssetProfileDao; import org.thingsboard.server.dao.model.sql.AssetProfileEntity; import org.thingsboard.server.dao.sql.JpaAbstractDao; @@ -37,7 +40,7 @@ import java.util.Optional; import java.util.UUID; @Component -public class JpaAssetProfileDao extends JpaAbstractDao implements AssetProfileDao { +public class JpaAssetProfileDao extends JpaAbstractDao implements AssetProfileDao, TenantEntityDao { @Autowired private AssetProfileRepository assetProfileRepository; @@ -138,9 +141,24 @@ public class JpaAssetProfileDao extends JpaAbstractDao findAllByTenantId(TenantId tenantId, PageLink pageLink) { + return findByTenantId(tenantId.getId(), pageLink); + } + + @Override + public PageData findAllFields(PageLink pageLink) { + return DaoUtil.pageToPageData(assetProfileRepository.findAllFields(DaoUtil.toPageable(pageLink))); + } + @Override public EntityType getEntityType() { return EntityType.ASSET_PROFILE; } + @Override + public ObjectType getType() { + return ObjectType.ASSET_PROFILE; + } + } diff --git a/dao/src/main/java/org/thingsboard/server/dao/sql/attributes/JpaAttributeDao.java b/dao/src/main/java/org/thingsboard/server/dao/sql/attributes/JpaAttributeDao.java index a0e1f1447d..b344a0ca41 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/sql/attributes/JpaAttributeDao.java +++ b/dao/src/main/java/org/thingsboard/server/dao/sql/attributes/JpaAttributeDao.java @@ -23,6 +23,7 @@ import lombok.extern.slf4j.Slf4j; import org.apache.commons.lang3.tuple.Pair; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Value; +import org.springframework.data.domain.Page; import org.springframework.stereotype.Component; import org.springframework.transaction.annotation.Transactional; import org.thingsboard.server.common.data.AttributeScope; @@ -30,6 +31,8 @@ import org.thingsboard.server.common.data.id.DeviceProfileId; import org.thingsboard.server.common.data.id.EntityId; import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.common.data.kv.AttributeKvEntry; +import org.thingsboard.server.common.data.page.PageData; +import org.thingsboard.server.common.data.page.PageLink; import org.thingsboard.server.common.data.util.TbPair; import org.thingsboard.server.common.stats.StatsFactory; import org.thingsboard.server.dao.DaoUtil; @@ -48,6 +51,7 @@ import java.util.ArrayList; import java.util.Collection; import java.util.Comparator; import java.util.List; +import java.util.Map; import java.util.Optional; import java.util.function.Function; import java.util.stream.Collectors; @@ -152,6 +156,12 @@ public class JpaAttributeDao extends JpaAbstractDaoListeningExecutorService impl return DaoUtil.convertDataList(Lists.newArrayList(attributes)); } + @Override + public PageData findAll(PageLink pageLink) { + Page attributes = attributeKvRepository.findAll(DaoUtil.toPageable(pageLink)); + return DaoUtil.pageToPageData(attributes); + } + @Override public List findAllKeysByDeviceProfileId(TenantId tenantId, DeviceProfileId deviceProfileId) { if (deviceProfileId != null) { diff --git a/dao/src/main/java/org/thingsboard/server/dao/sql/customer/CustomerRepository.java b/dao/src/main/java/org/thingsboard/server/dao/sql/customer/CustomerRepository.java index 448b850ba7..19b3ec8fe8 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/sql/customer/CustomerRepository.java +++ b/dao/src/main/java/org/thingsboard/server/dao/sql/customer/CustomerRepository.java @@ -20,6 +20,8 @@ import org.springframework.data.domain.Pageable; import org.springframework.data.jpa.repository.JpaRepository; import org.springframework.data.jpa.repository.Query; import org.springframework.data.repository.query.Param; +import org.springframework.transaction.annotation.Transactional; +import org.thingsboard.server.common.data.edqs.fields.CustomerFields; import org.thingsboard.server.dao.ExportableEntityRepository; import org.thingsboard.server.dao.model.sql.CustomerEntity; @@ -55,4 +57,8 @@ public interface CustomerRepository extends JpaRepository, nativeQuery = true) Page findCustomersWithTheSameTitle(Pageable pageable); + @Query("SELECT new org.thingsboard.server.common.data.edqs.fields.CustomerFields(c.id, c.createdTime, c.tenantId, " + + "c.title, c.version, c.additionalInfo, c.country, c.state, c.city, c.address, c.address2, c.zip, c.phone, c.email) FROM CustomerEntity c") + Page findAllFields(Pageable pageable); + } diff --git a/dao/src/main/java/org/thingsboard/server/dao/sql/customer/JpaCustomerDao.java b/dao/src/main/java/org/thingsboard/server/dao/sql/customer/JpaCustomerDao.java index 497a2917d8..a371803763 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/sql/customer/JpaCustomerDao.java +++ b/dao/src/main/java/org/thingsboard/server/dao/sql/customer/JpaCustomerDao.java @@ -20,6 +20,8 @@ import org.springframework.data.jpa.repository.JpaRepository; import org.springframework.stereotype.Component; import org.thingsboard.server.common.data.Customer; import org.thingsboard.server.common.data.EntityType; +import org.thingsboard.server.common.data.ObjectType; +import org.thingsboard.server.common.data.edqs.fields.CustomerFields; import org.thingsboard.server.common.data.id.CustomerId; import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.common.data.page.PageData; @@ -104,9 +106,24 @@ public class JpaCustomerDao extends JpaAbstractDao imp ); } + @Override + public PageData findAllByTenantId(TenantId tenantId, PageLink pageLink) { + return findByTenantId(tenantId.getId(), pageLink); + } + + @Override + public PageData findAllFields(PageLink pageLink) { + return DaoUtil.pageToPageData(customerRepository.findAllFields(DaoUtil.toPageable(pageLink))); + } + @Override public EntityType getEntityType() { return EntityType.CUSTOMER; } + @Override + public ObjectType getType() { + return ObjectType.CUSTOMER; + } + } diff --git a/dao/src/main/java/org/thingsboard/server/dao/sql/dashboard/DashboardRepository.java b/dao/src/main/java/org/thingsboard/server/dao/sql/dashboard/DashboardRepository.java index 6f182351c1..9932819ce0 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/sql/dashboard/DashboardRepository.java +++ b/dao/src/main/java/org/thingsboard/server/dao/sql/dashboard/DashboardRepository.java @@ -20,6 +20,7 @@ import org.springframework.data.domain.Pageable; import org.springframework.data.jpa.repository.JpaRepository; import org.springframework.data.jpa.repository.Query; import org.springframework.data.repository.query.Param; +import org.thingsboard.server.common.data.edqs.fields.DashboardFields; import org.thingsboard.server.dao.ExportableEntityRepository; import org.thingsboard.server.dao.model.sql.DashboardEntity; @@ -46,4 +47,8 @@ public interface DashboardRepository extends JpaRepository findAllIds(Pageable pageable); + @Query("SELECT new org.thingsboard.server.common.data.edqs.fields.DashboardFields(d.id, d.createdTime, d.tenantId, " + + "d.customerId, d.title, d.version) FROM DashboardEntity d") + Page findAllFields(Pageable pageable); + } diff --git a/dao/src/main/java/org/thingsboard/server/dao/sql/dashboard/JpaDashboardDao.java b/dao/src/main/java/org/thingsboard/server/dao/sql/dashboard/JpaDashboardDao.java index 640107add0..436c32d32b 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/sql/dashboard/JpaDashboardDao.java +++ b/dao/src/main/java/org/thingsboard/server/dao/sql/dashboard/JpaDashboardDao.java @@ -20,6 +20,8 @@ import org.springframework.data.jpa.repository.JpaRepository; import org.springframework.stereotype.Component; import org.thingsboard.server.common.data.Dashboard; import org.thingsboard.server.common.data.EntityType; +import org.thingsboard.server.common.data.ObjectType; +import org.thingsboard.server.common.data.edqs.fields.DashboardFields; import org.thingsboard.server.common.data.id.DashboardId; import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.common.data.page.PageData; @@ -90,9 +92,24 @@ public class JpaDashboardDao extends JpaAbstractDao return DaoUtil.pageToPageData(dashboardRepository.findAllIds(DaoUtil.toPageable(pageLink)).map(DashboardId::new)); } + @Override + public PageData findAllByTenantId(TenantId tenantId, PageLink pageLink) { + return findByTenantId(tenantId.getId(), pageLink); + } + + @Override + public PageData findAllFields(PageLink pageLink) { + return DaoUtil.pageToPageData(dashboardRepository.findAllFields(DaoUtil.toPageable(pageLink))); + } + @Override public EntityType getEntityType() { return EntityType.DASHBOARD; } + @Override + public ObjectType getType() { + return ObjectType.DASHBOARD; + } + } diff --git a/dao/src/main/java/org/thingsboard/server/dao/sql/device/DeviceCredentialsRepository.java b/dao/src/main/java/org/thingsboard/server/dao/sql/device/DeviceCredentialsRepository.java index c136d361e2..f2b541cb45 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/sql/device/DeviceCredentialsRepository.java +++ b/dao/src/main/java/org/thingsboard/server/dao/sql/device/DeviceCredentialsRepository.java @@ -15,6 +15,8 @@ */ package org.thingsboard.server.dao.sql.device; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.Pageable; import org.springframework.data.jpa.repository.JpaRepository; import org.springframework.data.jpa.repository.Query; import org.springframework.data.repository.query.Param; @@ -36,4 +38,8 @@ public interface DeviceCredentialsRepository extends JpaRepository findByTenantId(@Param("tenantId") UUID tenantId, Pageable pageable); + } diff --git a/dao/src/main/java/org/thingsboard/server/dao/sql/device/DeviceProfileRepository.java b/dao/src/main/java/org/thingsboard/server/dao/sql/device/DeviceProfileRepository.java index b51324f11a..e41796381d 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/sql/device/DeviceProfileRepository.java +++ b/dao/src/main/java/org/thingsboard/server/dao/sql/device/DeviceProfileRepository.java @@ -23,6 +23,8 @@ import org.springframework.data.repository.query.Param; import org.thingsboard.server.common.data.DeviceProfileInfo; import org.thingsboard.server.common.data.DeviceTransportType; import org.thingsboard.server.common.data.EntityInfo; +import org.thingsboard.server.common.data.edqs.fields.DeviceProfileFields; +import org.thingsboard.server.common.data.edqs.fields.GenericFields; import org.thingsboard.server.dao.ExportableEntityRepository; import org.thingsboard.server.dao.model.sql.DeviceProfileEntity; @@ -92,4 +94,8 @@ public interface DeviceProfileRepository extends JpaRepository findAllTenantDeviceProfileNames(@Param("tenantId") UUID tenantId); + @Query("SELECT new org.thingsboard.server.common.data.edqs.fields.DeviceProfileFields(d.id, d.createdTime, d.tenantId," + + "d.name, d.version, d.type, d.isDefault) FROM DeviceProfileEntity d") + Page findAllFields(Pageable pageable); + } diff --git a/dao/src/main/java/org/thingsboard/server/dao/sql/device/DeviceRepository.java b/dao/src/main/java/org/thingsboard/server/dao/sql/device/DeviceRepository.java index c55210b606..161c201404 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/sql/device/DeviceRepository.java +++ b/dao/src/main/java/org/thingsboard/server/dao/sql/device/DeviceRepository.java @@ -21,6 +21,7 @@ import org.springframework.data.jpa.repository.JpaRepository; import org.springframework.data.jpa.repository.Query; import org.springframework.data.repository.query.Param; import org.thingsboard.server.common.data.DeviceTransportType; +import org.thingsboard.server.common.data.edqs.fields.DeviceFields; import org.thingsboard.server.dao.ExportableEntityRepository; import org.thingsboard.server.dao.model.sql.DeviceEntity; import org.thingsboard.server.dao.model.sql.DeviceInfoEntity; @@ -194,4 +195,9 @@ public interface DeviceRepository extends JpaRepository, Exp @Query("SELECT externalId FROM DeviceEntity WHERE id = :id") UUID getExternalIdById(@Param("id") UUID id); + + @Query("SELECT new org.thingsboard.server.common.data.edqs.fields.DeviceFields(d.id, d.createdTime, d.tenantId, d.customerId," + + "d.name, d.version, d.type, d.label, d.deviceProfileId, d.additionalInfo) FROM DeviceEntity d") + Page findAllFields(Pageable pageable); + } diff --git a/dao/src/main/java/org/thingsboard/server/dao/sql/device/JpaDeviceCredentialsDao.java b/dao/src/main/java/org/thingsboard/server/dao/sql/device/JpaDeviceCredentialsDao.java index 7571c00b6a..31d2aa6cb9 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/sql/device/JpaDeviceCredentialsDao.java +++ b/dao/src/main/java/org/thingsboard/server/dao/sql/device/JpaDeviceCredentialsDao.java @@ -19,10 +19,14 @@ import lombok.extern.slf4j.Slf4j; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.data.jpa.repository.JpaRepository; import org.springframework.stereotype.Component; +import org.thingsboard.server.common.data.ObjectType; import org.thingsboard.server.common.data.id.DeviceId; import org.thingsboard.server.common.data.id.TenantId; +import org.thingsboard.server.common.data.page.PageData; +import org.thingsboard.server.common.data.page.PageLink; import org.thingsboard.server.common.data.security.DeviceCredentials; import org.thingsboard.server.dao.DaoUtil; +import org.thingsboard.server.dao.TenantEntityDao; import org.thingsboard.server.dao.device.DeviceCredentialsDao; import org.thingsboard.server.dao.model.sql.DeviceCredentialsEntity; import org.thingsboard.server.dao.sql.JpaAbstractDao; @@ -36,7 +40,7 @@ import java.util.UUID; @Slf4j @Component @SqlDao -public class JpaDeviceCredentialsDao extends JpaAbstractDao implements DeviceCredentialsDao { +public class JpaDeviceCredentialsDao extends JpaAbstractDao implements DeviceCredentialsDao, TenantEntityDao { @Autowired private DeviceCredentialsRepository deviceCredentialsRepository; @@ -67,4 +71,14 @@ public class JpaDeviceCredentialsDao extends JpaAbstractDao findAllByTenantId(TenantId tenantId, PageLink pageLink) { + return DaoUtil.toPageData(deviceCredentialsRepository.findByTenantId(tenantId.getId(), DaoUtil.toPageable(pageLink))); + } + + @Override + public ObjectType getType() { + return ObjectType.DEVICE_CREDENTIALS; + } + } diff --git a/dao/src/main/java/org/thingsboard/server/dao/sql/device/JpaDeviceDao.java b/dao/src/main/java/org/thingsboard/server/dao/sql/device/JpaDeviceDao.java index 48bb998016..2763e5dca7 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/sql/device/JpaDeviceDao.java +++ b/dao/src/main/java/org/thingsboard/server/dao/sql/device/JpaDeviceDao.java @@ -29,7 +29,9 @@ import org.thingsboard.server.common.data.DeviceInfoFilter; import org.thingsboard.server.common.data.DeviceTransportType; import org.thingsboard.server.common.data.EntitySubtype; import org.thingsboard.server.common.data.EntityType; +import org.thingsboard.server.common.data.ObjectType; import org.thingsboard.server.common.data.StringUtils; +import org.thingsboard.server.common.data.edqs.fields.DeviceFields; import org.thingsboard.server.common.data.id.DeviceId; import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.common.data.ota.OtaPackageType; @@ -282,9 +284,24 @@ public class JpaDeviceDao extends JpaAbstractDao implement .map(DeviceId::new).orElse(null); } + @Override + public PageData findAllByTenantId(TenantId tenantId, PageLink pageLink) { + return findByTenantId(tenantId.getId(), pageLink); + } + + @Override + public PageData findAllFields(PageLink pageLink) { + return DaoUtil.pageToPageData(deviceRepository.findAllFields(DaoUtil.toPageable(pageLink))); + } + @Override public EntityType getEntityType() { return EntityType.DEVICE; } + @Override + public ObjectType getType() { + return ObjectType.DEVICE; + } + } diff --git a/dao/src/main/java/org/thingsboard/server/dao/sql/device/JpaDeviceProfileDao.java b/dao/src/main/java/org/thingsboard/server/dao/sql/device/JpaDeviceProfileDao.java index 4e1594eeff..ff6e61c051 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/sql/device/JpaDeviceProfileDao.java +++ b/dao/src/main/java/org/thingsboard/server/dao/sql/device/JpaDeviceProfileDao.java @@ -24,12 +24,15 @@ import org.thingsboard.server.common.data.DeviceProfileInfo; import org.thingsboard.server.common.data.DeviceTransportType; import org.thingsboard.server.common.data.EntityInfo; import org.thingsboard.server.common.data.EntityType; +import org.thingsboard.server.common.data.ObjectType; import org.thingsboard.server.common.data.StringUtils; +import org.thingsboard.server.common.data.edqs.fields.DeviceProfileFields; import org.thingsboard.server.common.data.id.DeviceProfileId; 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.dao.DaoUtil; +import org.thingsboard.server.dao.TenantEntityDao; import org.thingsboard.server.dao.device.DeviceProfileDao; import org.thingsboard.server.dao.model.sql.DeviceProfileEntity; import org.thingsboard.server.dao.sql.JpaAbstractDao; @@ -41,7 +44,7 @@ import java.util.UUID; @Component @SqlDao -public class JpaDeviceProfileDao extends JpaAbstractDao implements DeviceProfileDao { +public class JpaDeviceProfileDao extends JpaAbstractDao implements DeviceProfileDao, TenantEntityDao { @Autowired private DeviceProfileRepository deviceProfileRepository; @@ -156,9 +159,24 @@ public class JpaDeviceProfileDao extends JpaAbstractDao findAllByTenantId(TenantId tenantId, PageLink pageLink) { + return findDeviceProfiles(tenantId, pageLink); + } + + @Override + public PageData findAllFields(PageLink pageLink) { + return DaoUtil.pageToPageData(deviceProfileRepository.findAllFields(DaoUtil.toPageable(pageLink))); + } + @Override public EntityType getEntityType() { return EntityType.DEVICE_PROFILE; } + @Override + public ObjectType getType() { + return ObjectType.DEVICE_PROFILE; + } + } diff --git a/dao/src/main/java/org/thingsboard/server/dao/sql/edge/EdgeRepository.java b/dao/src/main/java/org/thingsboard/server/dao/sql/edge/EdgeRepository.java index 1bcfab7ab3..e75ae5984a 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/sql/edge/EdgeRepository.java +++ b/dao/src/main/java/org/thingsboard/server/dao/sql/edge/EdgeRepository.java @@ -20,6 +20,7 @@ import org.springframework.data.domain.Pageable; import org.springframework.data.jpa.repository.JpaRepository; import org.springframework.data.jpa.repository.Query; import org.springframework.data.repository.query.Param; +import org.thingsboard.server.common.data.edqs.fields.EdgeFields; import org.thingsboard.server.dao.model.sql.EdgeEntity; import org.thingsboard.server.dao.model.sql.EdgeInfoEntity; @@ -154,4 +155,8 @@ public interface EdgeRepository extends JpaRepository { EdgeEntity findByRoutingKey(String routingKey); + @Query("SELECT new org.thingsboard.server.common.data.edqs.fields.EdgeFields(e.id, e.createdTime, e.tenantId, e.customerId," + + "e.name, e.version, e.type, e.label, e.additionalInfo) FROM EdgeEntity e") + Page findAllFields(Pageable pageable); + } diff --git a/dao/src/main/java/org/thingsboard/server/dao/sql/edge/JpaEdgeDao.java b/dao/src/main/java/org/thingsboard/server/dao/sql/edge/JpaEdgeDao.java index 2249ced97c..eb46d5bec7 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/sql/edge/JpaEdgeDao.java +++ b/dao/src/main/java/org/thingsboard/server/dao/sql/edge/JpaEdgeDao.java @@ -22,8 +22,10 @@ import org.springframework.data.jpa.repository.JpaRepository; import org.springframework.stereotype.Component; import org.thingsboard.server.common.data.EntitySubtype; import org.thingsboard.server.common.data.EntityType; +import org.thingsboard.server.common.data.ObjectType; import org.thingsboard.server.common.data.edge.Edge; import org.thingsboard.server.common.data.edge.EdgeInfo; +import org.thingsboard.server.common.data.edqs.fields.EdgeFields; import org.thingsboard.server.common.data.id.EdgeId; import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.common.data.page.PageData; @@ -219,9 +221,24 @@ public class JpaEdgeDao extends JpaAbstractDao implements Edge return edgeRepository.countByTenantId(tenantId.getId()); } + @Override + public PageData findAllByTenantId(TenantId tenantId, PageLink pageLink) { + return findEdgesByTenantId(tenantId.getId(), pageLink); + } + + @Override + public PageData findAllFields(PageLink pageLink) { + return DaoUtil.pageToPageData(edgeRepository.findAllFields(DaoUtil.toPageable(pageLink))); + } + @Override public EntityType getEntityType() { return EntityType.EDGE; } + @Override + public ObjectType getType() { + return ObjectType.EDGE; + } + } diff --git a/dao/src/main/java/org/thingsboard/server/dao/sql/entityview/EntityViewRepository.java b/dao/src/main/java/org/thingsboard/server/dao/sql/entityview/EntityViewRepository.java index 8f2e61d787..b4ad0478e9 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/sql/entityview/EntityViewRepository.java +++ b/dao/src/main/java/org/thingsboard/server/dao/sql/entityview/EntityViewRepository.java @@ -20,6 +20,7 @@ import org.springframework.data.domain.Pageable; import org.springframework.data.jpa.repository.JpaRepository; import org.springframework.data.jpa.repository.Query; import org.springframework.data.repository.query.Param; +import org.thingsboard.server.common.data.edqs.fields.GenericFields; import org.thingsboard.server.dao.ExportableEntityRepository; import org.thingsboard.server.dao.model.sql.EntityViewEntity; import org.thingsboard.server.dao.model.sql.EntityViewInfoEntity; @@ -145,4 +146,8 @@ public interface EntityViewRepository extends JpaRepository findAllFields(Pageable pageable); + } diff --git a/dao/src/main/java/org/thingsboard/server/dao/sql/entityview/JpaEntityViewDao.java b/dao/src/main/java/org/thingsboard/server/dao/sql/entityview/JpaEntityViewDao.java index 98c5ad7036..82e7eab609 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/sql/entityview/JpaEntityViewDao.java +++ b/dao/src/main/java/org/thingsboard/server/dao/sql/entityview/JpaEntityViewDao.java @@ -24,11 +24,14 @@ import org.thingsboard.server.common.data.EntitySubtype; import org.thingsboard.server.common.data.EntityType; import org.thingsboard.server.common.data.EntityView; import org.thingsboard.server.common.data.EntityViewInfo; +import org.thingsboard.server.common.data.ObjectType; +import org.thingsboard.server.common.data.edqs.fields.GenericFields; import org.thingsboard.server.common.data.id.EntityViewId; 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.dao.DaoUtil; +import org.thingsboard.server.dao.TenantEntityDao; import org.thingsboard.server.dao.entityview.EntityViewDao; import org.thingsboard.server.dao.model.sql.EntityViewEntity; import org.thingsboard.server.dao.model.sql.EntityViewInfoEntity; @@ -47,8 +50,7 @@ import static org.thingsboard.server.dao.DaoUtil.convertTenantEntityTypesToDto; @Component @Slf4j @SqlDao -public class JpaEntityViewDao extends JpaAbstractDao - implements EntityViewDao { +public class JpaEntityViewDao extends JpaAbstractDao implements EntityViewDao, TenantEntityDao { @Autowired private EntityViewRepository entityViewRepository; @@ -218,8 +220,24 @@ public class JpaEntityViewDao extends JpaAbstractDao findAllByTenantId(TenantId tenantId, PageLink pageLink) { + return findByTenantId(tenantId.getId(), pageLink); + } + + @Override + public PageData findAllFields(PageLink pageLink) { + return DaoUtil.pageToPageData(entityViewRepository.findAllFields(DaoUtil.toPageable(pageLink))); + } + @Override public EntityType getEntityType() { return EntityType.ENTITY_VIEW; } + + @Override + public ObjectType getType() { + return ObjectType.ENTITY_VIEW; + } + } diff --git a/dao/src/main/java/org/thingsboard/server/dao/sql/notification/JpaNotificationRuleDao.java b/dao/src/main/java/org/thingsboard/server/dao/sql/notification/JpaNotificationRuleDao.java index 48a7df0f3b..e6df0ff235 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/sql/notification/JpaNotificationRuleDao.java +++ b/dao/src/main/java/org/thingsboard/server/dao/sql/notification/JpaNotificationRuleDao.java @@ -19,6 +19,7 @@ import lombok.RequiredArgsConstructor; import org.springframework.data.jpa.repository.JpaRepository; import org.springframework.stereotype.Component; import org.thingsboard.server.common.data.EntityType; +import org.thingsboard.server.common.data.ObjectType; import org.thingsboard.server.common.data.id.NotificationRuleId; import org.thingsboard.server.common.data.id.NotificationTargetId; import org.thingsboard.server.common.data.id.TenantId; @@ -28,6 +29,7 @@ import org.thingsboard.server.common.data.notification.rule.trigger.config.Notif import org.thingsboard.server.common.data.page.PageData; import org.thingsboard.server.common.data.page.PageLink; import org.thingsboard.server.dao.DaoUtil; +import org.thingsboard.server.dao.TenantEntityDao; import org.thingsboard.server.dao.model.sql.NotificationRuleEntity; import org.thingsboard.server.dao.model.sql.NotificationRuleInfoEntity; import org.thingsboard.server.dao.notification.NotificationRuleDao; @@ -41,7 +43,7 @@ import java.util.UUID; @Component @SqlDao @RequiredArgsConstructor -public class JpaNotificationRuleDao extends JpaAbstractDao implements NotificationRuleDao { +public class JpaNotificationRuleDao extends JpaAbstractDao implements NotificationRuleDao, TenantEntityDao { private final NotificationRuleRepository notificationRuleRepository; @@ -101,6 +103,11 @@ public class JpaNotificationRuleDao extends JpaAbstractDao findAllByTenantId(TenantId tenantId, PageLink pageLink) { + return findByTenantId(tenantId.getId(), pageLink); + } + @Override protected Class getEntityClass() { return NotificationRuleEntity.class; @@ -116,4 +123,9 @@ public class JpaNotificationRuleDao extends JpaAbstractDao findAllByTenantId(TenantId tenantId, PageLink pageLink) { + return findByTenantId(tenantId.getId(), pageLink); + } + @Override protected Class getEntityClass() { return NotificationTargetEntity.class; @@ -116,4 +122,9 @@ public class JpaNotificationTargetDao extends JpaAbstractDao implements NotificationTemplateDao { +public class JpaNotificationTemplateDao extends JpaAbstractDao implements NotificationTemplateDao, TenantEntityDao { private final NotificationTemplateRepository notificationTemplateRepository; @@ -83,6 +85,11 @@ public class JpaNotificationTemplateDao extends JpaAbstractDao findAllByTenantId(TenantId tenantId, PageLink pageLink) { + return findByTenantId(tenantId.getId(), pageLink); + } + @Override protected JpaRepository getRepository() { return notificationTemplateRepository; @@ -93,4 +100,9 @@ public class JpaNotificationTemplateDao extends JpaAbstractDao implements OtaPackageDao { +public class JpaOtaPackageDao extends JpaAbstractDao implements OtaPackageDao, TenantEntityDao { @Autowired private OtaPackageRepository otaPackageRepository; @@ -52,9 +58,20 @@ public class JpaOtaPackageDao extends JpaAbstractDao findAllByTenantId(TenantId tenantId, PageLink pageLink) { + return DaoUtil.toPageData(otaPackageRepository.findByTenantId(tenantId.getId(), DaoUtil.toPageable(pageLink))); + } + @Override public EntityType getEntityType() { return EntityType.OTA_PACKAGE; } + @Override + public ObjectType getType() { + return ObjectType.OTA_PACKAGE; + } + } diff --git a/dao/src/main/java/org/thingsboard/server/dao/sql/ota/JpaOtaPackageInfoDao.java b/dao/src/main/java/org/thingsboard/server/dao/sql/ota/JpaOtaPackageInfoDao.java index 707a6e7062..74f14a9697 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/sql/ota/JpaOtaPackageInfoDao.java +++ b/dao/src/main/java/org/thingsboard/server/dao/sql/ota/JpaOtaPackageInfoDao.java @@ -93,4 +93,5 @@ public class JpaOtaPackageInfoDao extends JpaAbstractDao { + @Query(value = "SELECT COALESCE(SUM(ota.data_size), 0) FROM ota_package ota WHERE ota.tenant_id = :tenantId AND ota.data IS NOT NULL", nativeQuery = true) Long sumDataSizeByTenantId(@Param("tenantId") UUID tenantId); + + Page findByTenantId(UUID tenantId, Pageable pageable); + } diff --git a/dao/src/main/java/org/thingsboard/server/dao/sql/query/DefaultAlarmQueryRepository.java b/dao/src/main/java/org/thingsboard/server/dao/sql/query/DefaultAlarmQueryRepository.java index 05da22f9a5..6c8f48446e 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/sql/query/DefaultAlarmQueryRepository.java +++ b/dao/src/main/java/org/thingsboard/server/dao/sql/query/DefaultAlarmQueryRepository.java @@ -28,6 +28,10 @@ import org.thingsboard.server.common.data.id.CustomerId; import org.thingsboard.server.common.data.id.EntityId; import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.common.data.page.PageData; +import org.thingsboard.server.common.data.permission.MergedUserPermissions; +import org.thingsboard.server.common.data.permission.Operation; +import org.thingsboard.server.common.data.permission.QueryContext; +import org.thingsboard.server.common.data.permission.Resource; import org.thingsboard.server.common.data.query.AlarmCountQuery; import org.thingsboard.server.common.data.query.AlarmData; import org.thingsboard.server.common.data.query.AlarmDataPageLink; @@ -128,7 +132,7 @@ public class DefaultAlarmQueryRepository implements AlarmQueryRepository { public PageData findAlarmDataByQueryForEntities(TenantId tenantId, AlarmDataQuery query, Collection orderedEntityIds) { return transactionTemplate.execute(trStatus -> { AlarmDataPageLink pageLink = query.getPageLink(); - QueryContext ctx = new QueryContext(new QuerySecurityContext(tenantId, null, EntityType.ALARM)); + SqlQueryContext ctx = new SqlQueryContext(new QueryContext(tenantId, null, EntityType.ALARM)); ctx.addUuidListParameter("entity_ids", orderedEntityIds.stream().map(EntityId::getId).collect(Collectors.toList())); StringBuilder selectPart = new StringBuilder(FIELDS_SELECTION); StringBuilder fromPart = new StringBuilder(" from alarm_info a "); @@ -315,7 +319,7 @@ public class DefaultAlarmQueryRepository implements AlarmQueryRepository { @Override public long countAlarmsByQuery(TenantId tenantId, CustomerId customerId, AlarmCountQuery query) { - QueryContext ctx = new QueryContext(new QuerySecurityContext(tenantId, null, EntityType.ALARM)); + SqlQueryContext ctx = new SqlQueryContext(new QueryContext(tenantId, null, EntityType.ALARM)); if (query.isSearchPropagatedAlarms()) { ctx.append("select count(distinct(a.id)) from alarm_info a "); @@ -402,7 +406,7 @@ public class DefaultAlarmQueryRepository implements AlarmQueryRepository { }); } - private String buildTextSearchQuery(QueryContext ctx, List selectionMapping, String searchText) { + private String buildTextSearchQuery(SqlQueryContext ctx, List selectionMapping, String searchText) { if (!StringUtils.isEmpty(searchText) && selectionMapping != null && !selectionMapping.isEmpty()) { String lowerSearchText = searchText.toLowerCase() + "%"; List searchPredicates = selectionMapping.stream() @@ -420,7 +424,7 @@ public class DefaultAlarmQueryRepository implements AlarmQueryRepository { } } - private String buildPermissionsQuery(TenantId tenantId, QueryContext ctx) { + private String buildPermissionsQuery(TenantId tenantId, SqlQueryContext ctx) { StringBuilder permissionsQuery = new StringBuilder(); ctx.addUuidParameter("permissions_tenant_id", tenantId.getId()); permissionsQuery.append(" a.tenant_id = :permissions_tenant_id and ea.tenant_id = :permissions_tenant_id "); diff --git a/dao/src/main/java/org/thingsboard/server/dao/sql/query/DefaultEntityQueryRepository.java b/dao/src/main/java/org/thingsboard/server/dao/sql/query/DefaultEntityQueryRepository.java index 36ae779edd..31443f7f47 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/sql/query/DefaultEntityQueryRepository.java +++ b/dao/src/main/java/org/thingsboard/server/dao/sql/query/DefaultEntityQueryRepository.java @@ -27,6 +27,7 @@ import org.thingsboard.server.common.data.id.CustomerId; import org.thingsboard.server.common.data.id.EntityId; import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.common.data.page.PageData; +import org.thingsboard.server.common.data.permission.QueryContext; import org.thingsboard.server.common.data.query.ApiUsageStateFilter; import org.thingsboard.server.common.data.query.AssetSearchQueryFilter; import org.thingsboard.server.common.data.query.AssetTypeFilter; @@ -334,7 +335,7 @@ public class DefaultEntityQueryRepository implements EntityQueryRepository { @Override public long countEntitiesByQuery(TenantId tenantId, CustomerId customerId, EntityCountQuery query) { EntityType entityType = resolveEntityType(query.getEntityFilter()); - QueryContext ctx = new QueryContext(new QuerySecurityContext(tenantId, customerId, entityType, TenantId.SYS_TENANT_ID.equals(tenantId))); + SqlQueryContext ctx = new SqlQueryContext(new QueryContext(tenantId, customerId, entityType, TenantId.SYS_TENANT_ID.equals(tenantId))); if (query.getKeyFilters() == null || query.getKeyFilters().isEmpty()) { ctx.append("select count(e.id) from "); ctx.append(addEntityTableQuery(ctx, query.getEntityFilter())); @@ -416,7 +417,7 @@ public class DefaultEntityQueryRepository implements EntityQueryRepository { public PageData findEntityDataByQuery(TenantId tenantId, CustomerId customerId, EntityDataQuery query, boolean ignorePermissionCheck) { return transactionTemplate.execute(status -> { EntityType entityType = resolveEntityType(query.getEntityFilter()); - QueryContext ctx = new QueryContext(new QuerySecurityContext(tenantId, customerId, entityType, ignorePermissionCheck)); + SqlQueryContext ctx = new SqlQueryContext(new QueryContext(tenantId, customerId, entityType, ignorePermissionCheck)); EntityDataPageLink pageLink = query.getPageLink(); List mappings = EntityKeyMapping.prepareKeyMapping(entityType, query); @@ -524,7 +525,7 @@ public class DefaultEntityQueryRepository implements EntityQueryRepository { }); } - private String buildEntityWhere(QueryContext ctx, EntityFilter entityFilter, List entityFieldsFilters) { + private String buildEntityWhere(SqlQueryContext ctx, EntityFilter entityFilter, List entityFieldsFilters) { String permissionQuery = this.buildPermissionQuery(ctx, entityFilter); String entityFilterQuery = this.buildEntityFilterQuery(ctx, entityFilter); String entityFieldsQuery = EntityKeyMapping.buildQuery(ctx, entityFieldsFilters, entityFilter.getType()); @@ -538,7 +539,7 @@ public class DefaultEntityQueryRepository implements EntityQueryRepository { return result; } - private String buildPermissionQuery(QueryContext ctx, EntityFilter entityFilter) { + private String buildPermissionQuery(SqlQueryContext ctx, EntityFilter entityFilter) { if (ctx.isIgnorePermissionCheck()) { return "1=1"; } @@ -575,7 +576,7 @@ public class DefaultEntityQueryRepository implements EntityQueryRepository { } } - private String defaultPermissionQuery(QueryContext ctx) { + private String defaultPermissionQuery(SqlQueryContext ctx) { ctx.addUuidParameter("permissions_tenant_id", ctx.getTenantId().getId()); if (ctx.getCustomerId() != null && !ctx.getCustomerId().isNullUid()) { ctx.addUuidParameter("permissions_customer_id", ctx.getCustomerId().getId()); @@ -593,7 +594,7 @@ public class DefaultEntityQueryRepository implements EntityQueryRepository { } } - private String buildEntityFilterQuery(QueryContext ctx, EntityFilter entityFilter) { + private String buildEntityFilterQuery(SqlQueryContext ctx, EntityFilter entityFilter) { switch (entityFilter.getType()) { case SINGLE_ENTITY: return this.singleEntityQuery(ctx, (SingleEntityFilter) entityFilter); @@ -619,7 +620,7 @@ public class DefaultEntityQueryRepository implements EntityQueryRepository { } } - private String addEntityTableQuery(QueryContext ctx, EntityFilter entityFilter) { + private String addEntityTableQuery(SqlQueryContext ctx, EntityFilter entityFilter) { switch (entityFilter.getType()) { case RELATIONS_QUERY: return relationQuery(ctx, (RelationsQueryFilter) entityFilter); @@ -640,7 +641,7 @@ public class DefaultEntityQueryRepository implements EntityQueryRepository { } } - private String entitySearchQuery(QueryContext ctx, EntitySearchQueryFilter entityFilter, EntityType entityType, List types) { + private String entitySearchQuery(SqlQueryContext ctx, EntitySearchQueryFilter entityFilter, EntityType entityType, List types) { EntityId rootId = entityFilter.getRootEntity(); String lvlFilter = getLvlFilter(entityFilter.getMaxLevel()); String selectFields = "SELECT tenant_id, customer_id, id, created_time, type, name, additional_info " @@ -680,7 +681,7 @@ public class DefaultEntityQueryRepository implements EntityQueryRepository { return query; } - private String relationQuery(QueryContext ctx, RelationsQueryFilter entityFilter) { + private String relationQuery(SqlQueryContext ctx, RelationsQueryFilter entityFilter) { EntityId rootId = entityFilter.getRootEntity(); String lvlFilter = getLvlFilter(entityFilter.getMaxLevel()); String selectFields = SELECT_TENANT_ID + ", " + SELECT_CUSTOMER_ID @@ -763,7 +764,7 @@ public class DefaultEntityQueryRepository implements EntityQueryRepository { return "( " + selectFields + from + ")"; } - private String buildEtfCondition(QueryContext ctx, RelationEntityTypeFilter etf, EntitySearchDirection direction, int entityTypeFilterIdx) { + private String buildEtfCondition(SqlQueryContext ctx, RelationEntityTypeFilter etf, EntitySearchDirection direction, int entityTypeFilterIdx) { StringBuilder whereFilter = new StringBuilder(); String relationType = etf.getRelationType(); List entityTypes = etf.getEntityTypes(); @@ -812,7 +813,7 @@ public class DefaultEntityQueryRepository implements EntityQueryRepository { return from; } - private String buildAliasWhereQuery(QueryContext ctx, EntityFilter entityFilter, List selectionMapping, String searchText) { + private String buildAliasWhereQuery(SqlQueryContext ctx, EntityFilter entityFilter, List selectionMapping, String searchText) { List aliasFiltersMapping = selectionMapping.stream().filter(mapping -> !mapping.isLatest() && mapping.getEntityKeyColumn() == null) .collect(Collectors.toList()); String entityFieldsQuery = EntityKeyMapping.buildQuery(ctx, aliasFiltersMapping, entityFilter.getType()); @@ -822,12 +823,12 @@ public class DefaultEntityQueryRepository implements EntityQueryRepository { result += " where (" + entityFieldsQuery + ")"; } if (!searchTextQuery.isEmpty()) { - result += (result.isEmpty() ? " where ": " and ") + "(" + searchTextQuery + ") "; + result += (result.isEmpty() ? " where " : " and ") + "(" + searchTextQuery + ") "; } return result; } - private String buildTextSearchQuery(QueryContext ctx, List selectionMapping, String searchText) { + private String buildTextSearchQuery(SqlQueryContext ctx, List selectionMapping, String searchText) { if (!StringUtils.isEmpty(searchText) && !selectionMapping.isEmpty()) { String sqlSearchText = "%" + searchText + "%"; ctx.addStringParameter("lowerSearchTextParam", sqlSearchText); @@ -844,17 +845,17 @@ public class DefaultEntityQueryRepository implements EntityQueryRepository { } } - private String singleEntityQuery(QueryContext ctx, SingleEntityFilter filter) { + private String singleEntityQuery(SqlQueryContext ctx, SingleEntityFilter filter) { ctx.addUuidParameter("entity_filter_single_entity_id", filter.getSingleEntity().getId()); return "e.id=:entity_filter_single_entity_id"; } - private String entityListQuery(QueryContext ctx, EntityListFilter filter) { + private String entityListQuery(SqlQueryContext ctx, EntityListFilter filter) { ctx.addUuidListParameter("entity_filter_entity_ids", filter.getEntityList().stream().map(UUID::fromString).collect(Collectors.toList())); return "e.id in (:entity_filter_entity_ids)"; } - private String entityNameQuery(QueryContext ctx, EntityNameFilter filter) { + private String entityNameQuery(SqlQueryContext ctx, EntityNameFilter filter) { ctx.addStringParameter("entity_filter_name_filter", filter.getEntityNameFilter()); String nameColumn = getNameColumn(filter.getEntityType()); if (filter.getEntityNameFilter().startsWith("%") || filter.getEntityNameFilter().endsWith("%")) { @@ -864,7 +865,7 @@ public class DefaultEntityQueryRepository implements EntityQueryRepository { return String.format("e.%s ilike concat(:entity_filter_name_filter, '%%')", nameColumn); } - private String typeQuery(QueryContext ctx, EntityFilter filter) { + private String typeQuery(SqlQueryContext ctx, EntityFilter filter) { List types; String name; String nameColumn; diff --git a/dao/src/main/java/org/thingsboard/server/dao/sql/query/DefaultQueryLogComponent.java b/dao/src/main/java/org/thingsboard/server/dao/sql/query/DefaultQueryLogComponent.java index 643b4edeec..a6ab1bee7f 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/sql/query/DefaultQueryLogComponent.java +++ b/dao/src/main/java/org/thingsboard/server/dao/sql/query/DefaultQueryLogComponent.java @@ -37,7 +37,7 @@ public class DefaultQueryLogComponent implements QueryLogComponent { private long logQueriesThreshold; @Override - public void logQuery(QueryContext ctx, String query, long duration) { + public void logQuery(SqlQueryContext ctx, String query, long duration) { if (logSqlQueries && duration > logQueriesThreshold) { String sqlToUse = substituteParametersInSqlString(query, ctx); diff --git a/dao/src/main/java/org/thingsboard/server/dao/sql/query/DummyEdqsService.java b/dao/src/main/java/org/thingsboard/server/dao/sql/query/DummyEdqsService.java new file mode 100644 index 0000000000..91934293a6 --- /dev/null +++ b/dao/src/main/java/org/thingsboard/server/dao/sql/query/DummyEdqsService.java @@ -0,0 +1,64 @@ +/** + * Copyright © 2016-2024 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.dao.sql.query; + +import com.google.common.util.concurrent.ListenableFuture; +import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; +import org.springframework.stereotype.Service; +import org.thingsboard.server.common.data.ObjectType; +import org.thingsboard.server.common.data.edqs.EdqsObject; +import org.thingsboard.server.common.data.edqs.ToCoreEdqsMsg; +import org.thingsboard.server.common.data.edqs.ToCoreEdqsRequest; +import org.thingsboard.server.common.data.edqs.query.EdqsRequest; +import org.thingsboard.server.common.data.edqs.query.EdqsResponse; +import org.thingsboard.server.common.data.id.CustomerId; +import org.thingsboard.server.common.data.id.EntityId; +import org.thingsboard.server.common.data.id.TenantId; +import org.thingsboard.server.common.msg.edqs.EdqsService; + +@Service +@ConditionalOnProperty(value = "queue.edqs.sync_enabled", havingValue = "false", matchIfMissing = true) +public class DummyEdqsService implements EdqsService { + + @Override + public ListenableFuture processRequest(TenantId tenantId, CustomerId customerId, EdqsRequest request) { + throw new UnsupportedOperationException(); + } + + @Override + public boolean isApiEnabled() { + return false; + } + + @Override + public void onUpdate(TenantId tenantId, EntityId entityId, Object entity) {} + + @Override + public void onUpdate(TenantId tenantId, ObjectType objectType, EdqsObject object) {} + + @Override + public void onDelete(TenantId tenantId, EntityId entityId) {} + + @Override + public void onDelete(TenantId tenantId, ObjectType objectType, EdqsObject object) {} + + @Override + public void processSystemRequest(ToCoreEdqsRequest request) {} + + @Override + public void processSystemMsg(ToCoreEdqsMsg request) {} + +} diff --git a/dao/src/main/java/org/thingsboard/server/dao/sql/query/EntityKeyMapping.java b/dao/src/main/java/org/thingsboard/server/dao/sql/query/EntityKeyMapping.java index 599ac3d918..b02c208dae 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/sql/query/EntityKeyMapping.java +++ b/dao/src/main/java/org/thingsboard/server/dao/sql/query/EntityKeyMapping.java @@ -103,7 +103,7 @@ public class EntityKeyMapping { public static final List labeledEntityFields = Arrays.asList(CREATED_TIME, ENTITY_TYPE, NAME, TYPE, LABEL, ADDITIONAL_INFO); public static final List contactBasedEntityFields = Arrays.asList(CREATED_TIME, ENTITY_TYPE, EMAIL, TITLE, COUNTRY, STATE, CITY, ADDRESS, ADDRESS_2, ZIP, PHONE, ADDITIONAL_INFO); - public static final Set apiUsageStateEntityFields = new HashSet<>(Arrays.asList(CREATED_TIME, ENTITY_TYPE, NAME)); + public static final Set apiUsageStateEntityFields = new HashSet<>(Arrays.asList(CREATED_TIME, ENTITY_TYPE, NAME)); public static final Set commonEntityFieldsSet = new HashSet<>(commonEntityFields); public static final Set relationQueryEntityFieldsSet = new HashSet<>(Arrays.asList(CREATED_TIME, ENTITY_TYPE, NAME, TYPE, LABEL, FIRST_NAME, LAST_NAME, EMAIL, REGION, TITLE, COUNTRY, STATE, CITY, ADDRESS, ADDRESS_2, ZIP, PHONE, ADDITIONAL_INFO, RELATED_PARENT_ID)); @@ -265,7 +265,7 @@ public class EntityKeyMapping { return alias; } - public Stream toQueries(QueryContext ctx, EntityFilterType filterType) { + public Stream toQueries(SqlQueryContext ctx, EntityFilterType filterType) { if (hasFilter()) { String keyAlias = (entityKey.getType().equals(EntityKeyType.ENTITY_FIELD) && getEntityKeyColumn() != null) ? "e" : alias; return keyFilters.stream().map(keyFilter -> @@ -275,7 +275,7 @@ public class EntityKeyMapping { } } - public String toLatestJoin(QueryContext ctx, EntityFilter entityFilter, EntityType entityType) { + public String toLatestJoin(SqlQueryContext ctx, EntityFilter entityFilter, EntityType entityType) { String entityTypeStr; if (entityFilter.getType().equals(EntityFilterType.RELATIONS_QUERY)) { entityTypeStr = "entities.entity_type"; @@ -303,9 +303,9 @@ public class EntityKeyMapping { if (entityKey.getType().equals(EntityKeyType.CLIENT_ATTRIBUTE)) { scope = AttributeScope.CLIENT_SCOPE.getId(); } else if (entityKey.getType().equals(EntityKeyType.SHARED_ATTRIBUTE)) { - scope = AttributeScope.SHARED_SCOPE.getId();; + scope = AttributeScope.SHARED_SCOPE.getId(); ; } else { - scope = AttributeScope.SERVER_SCOPE.getId();; + scope = AttributeScope.SERVER_SCOPE.getId(); ; } query = String.format("%s AND %s.attribute_type=%s %s", query, alias, scope, filterQuery); } else { @@ -318,7 +318,7 @@ public class EntityKeyMapping { } } - private boolean hasFilterValues(QueryContext ctx) { + private boolean hasFilterValues(SqlQueryContext ctx) { return Arrays.stream(ctx.getParameterNames()).anyMatch(parameterName -> { return !parameterName.equals(getKeyId()) && parameterName.startsWith(alias); }); @@ -333,14 +333,14 @@ public class EntityKeyMapping { Collectors.joining(", ")); } - public static String buildLatestJoins(QueryContext ctx, EntityFilter entityFilter, EntityType entityType, List latestMappings, boolean countQuery) { + public static String buildLatestJoins(SqlQueryContext ctx, EntityFilter entityFilter, EntityType entityType, List latestMappings, boolean countQuery) { return latestMappings.stream() .filter(mapping -> !countQuery || mapping.hasFilter()) .map(mapping -> mapping.toLatestJoin(ctx, entityFilter, entityType)) .collect(Collectors.joining(" ")); } - public static String buildQuery(QueryContext ctx, List mappings, EntityFilterType filterType) { + public static String buildQuery(SqlQueryContext ctx, List mappings, EntityFilterType filterType) { return mappings.stream() .flatMap(mapping -> mapping.toQueries(ctx, filterType)) .filter(StringUtils::isNotEmpty) @@ -510,12 +510,12 @@ public class EntityKeyMapping { return getValueAlias() + "_so_num"; } - private String buildKeyQuery(QueryContext ctx, String alias, KeyFilter keyFilter, + private String buildKeyQuery(SqlQueryContext ctx, String alias, KeyFilter keyFilter, EntityFilterType filterType) { return this.buildPredicateQuery(ctx, alias, keyFilter.getKey(), keyFilter.getPredicate(), filterType); } - private String buildPredicateQuery(QueryContext ctx, String alias, EntityKey key, + private String buildPredicateQuery(SqlQueryContext ctx, String alias, EntityKey key, KeyFilterPredicate predicate, EntityFilterType filterType) { if (predicate.getType().equals(FilterPredicateType.COMPLEX)) { return this.buildComplexPredicateQuery(ctx, alias, key, (ComplexFilterPredicate) predicate, filterType); @@ -524,7 +524,7 @@ public class EntityKeyMapping { } } - private String buildComplexPredicateQuery(QueryContext ctx, String alias, EntityKey key, + private String buildComplexPredicateQuery(SqlQueryContext ctx, String alias, EntityKey key, ComplexFilterPredicate predicate, EntityFilterType filterType) { String result = predicate.getPredicates().stream() .map(keyFilterPredicate -> this.buildPredicateQuery(ctx, alias, key, keyFilterPredicate, filterType)) @@ -536,7 +536,7 @@ public class EntityKeyMapping { return result; } - private String buildSimplePredicateQuery(QueryContext ctx, String alias, EntityKey key, + private String buildSimplePredicateQuery(SqlQueryContext ctx, String alias, EntityKey key, KeyFilterPredicate predicate, EntityFilterType filterType) { if (key.getType().equals(EntityKeyType.ENTITY_FIELD)) { String field = (getEntityKeyColumn() != null) ? alias + "." + getEntityKeyColumn() : alias; @@ -571,7 +571,7 @@ public class EntityKeyMapping { } } - private String buildStringPredicateQuery(QueryContext ctx, String field, StringFilterPredicate stringFilterPredicate) { + private String buildStringPredicateQuery(SqlQueryContext ctx, String field, StringFilterPredicate stringFilterPredicate) { String operationField = field; String paramName = getNextParameterName(field); String value = stringFilterPredicate.getValue().getValue(); @@ -624,7 +624,7 @@ public class EntityKeyMapping { return String.format("((%s is not null and %s)", field, stringOperationQuery); } - private String buildNumericPredicateQuery(QueryContext ctx, String field, NumericFilterPredicate numericFilterPredicate) { + private String buildNumericPredicateQuery(SqlQueryContext ctx, String field, NumericFilterPredicate numericFilterPredicate) { String paramName = getNextParameterName(field); ctx.addDoubleParameter(paramName, numericFilterPredicate.getValue().getValue()); String numericOperationQuery = ""; @@ -651,7 +651,7 @@ public class EntityKeyMapping { return String.format("(%s is not null and %s)", field, numericOperationQuery); } - private String buildBooleanPredicateQuery(QueryContext ctx, String field, + private String buildBooleanPredicateQuery(SqlQueryContext ctx, String field, BooleanFilterPredicate booleanFilterPredicate) { String paramName = getNextParameterName(field); ctx.addBooleanParameter(paramName, booleanFilterPredicate.getValue().getValue()); diff --git a/dao/src/main/java/org/thingsboard/server/dao/sql/query/QueryLogComponent.java b/dao/src/main/java/org/thingsboard/server/dao/sql/query/QueryLogComponent.java index f455e2d591..cbb3d4141d 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/sql/query/QueryLogComponent.java +++ b/dao/src/main/java/org/thingsboard/server/dao/sql/query/QueryLogComponent.java @@ -17,5 +17,5 @@ package org.thingsboard.server.dao.sql.query; public interface QueryLogComponent { - void logQuery(QueryContext ctx, String query, long duration); + void logQuery(SqlQueryContext ctx, String query, long duration); } diff --git a/dao/src/main/java/org/thingsboard/server/dao/sql/query/QueryContext.java b/dao/src/main/java/org/thingsboard/server/dao/sql/query/SqlQueryContext.java similarity index 94% rename from dao/src/main/java/org/thingsboard/server/dao/sql/query/QueryContext.java rename to dao/src/main/java/org/thingsboard/server/dao/sql/query/SqlQueryContext.java index 6869703bce..42ac8280ed 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/sql/query/QueryContext.java +++ b/dao/src/main/java/org/thingsboard/server/dao/sql/query/SqlQueryContext.java @@ -21,6 +21,7 @@ import org.springframework.jdbc.core.namedparam.SqlParameterSource; import org.thingsboard.server.common.data.EntityType; import org.thingsboard.server.common.data.id.CustomerId; import org.thingsboard.server.common.data.id.TenantId; +import org.thingsboard.server.common.data.permission.QueryContext; import java.sql.Types; import java.util.HashMap; @@ -29,14 +30,14 @@ import java.util.Map; import java.util.UUID; @Slf4j -public class QueryContext implements SqlParameterSource { +public class SqlQueryContext implements SqlParameterSource { private static final UUIDJdbcType UUID_TYPE = UUIDJdbcType.INSTANCE; - private final QuerySecurityContext securityCtx; + private final QueryContext securityCtx; private final StringBuilder query; private final Map params; - public QueryContext(QuerySecurityContext securityCtx) { + public SqlQueryContext(QueryContext securityCtx) { this.securityCtx = securityCtx; query = new StringBuilder(); params = new HashMap<>(); @@ -48,7 +49,7 @@ public class QueryContext implements SqlParameterSource { if (oldParam != null && oldParam.value != null && !oldParam.value.equals(newParam.value)) { throw new RuntimeException("Parameter with name: " + name + " was already registered!"); } - if(value == null){ + if (value == null) { log.warn("[{}][{}][{}] Trying to set null value", getTenantId(), getCustomerId(), name); } } diff --git a/dao/src/main/java/org/thingsboard/server/dao/sql/queue/JpaQueueDao.java b/dao/src/main/java/org/thingsboard/server/dao/sql/queue/JpaQueueDao.java index e4563b7d35..c84392842a 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/sql/queue/JpaQueueDao.java +++ b/dao/src/main/java/org/thingsboard/server/dao/sql/queue/JpaQueueDao.java @@ -22,11 +22,13 @@ import org.springframework.data.jpa.repository.JpaRepository; import org.springframework.stereotype.Component; import org.thingsboard.server.common.data.DataConstants; import org.thingsboard.server.common.data.EntityType; +import org.thingsboard.server.common.data.ObjectType; import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.common.data.page.PageData; import org.thingsboard.server.common.data.page.PageLink; import org.thingsboard.server.common.data.queue.Queue; import org.thingsboard.server.dao.DaoUtil; +import org.thingsboard.server.dao.TenantEntityDao; import org.thingsboard.server.dao.model.sql.QueueEntity; import org.thingsboard.server.dao.queue.QueueDao; import org.thingsboard.server.dao.sql.JpaAbstractDao; @@ -38,7 +40,7 @@ import java.util.UUID; @Slf4j @Component @SqlDao -public class JpaQueueDao extends JpaAbstractDao implements QueueDao { +public class JpaQueueDao extends JpaAbstractDao implements QueueDao, TenantEntityDao { @Autowired private QueueRepository queueRepository; @@ -87,9 +89,19 @@ public class JpaQueueDao extends JpaAbstractDao implements Q .findByTenantId(tenantId.getId(), pageLink.getTextSearch(), DaoUtil.toPageable(pageLink))); } + @Override + public PageData findAllByTenantId(TenantId tenantId, PageLink pageLink) { + return findQueuesByTenantId(tenantId, pageLink); + } + @Override public EntityType getEntityType() { return EntityType.QUEUE; } + @Override + public ObjectType getType() { + return ObjectType.QUEUE; + } + } diff --git a/dao/src/main/java/org/thingsboard/server/dao/sql/queue/JpaQueueStatsDao.java b/dao/src/main/java/org/thingsboard/server/dao/sql/queue/JpaQueueStatsDao.java index f680a42dc1..b78bbb8f37 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/sql/queue/JpaQueueStatsDao.java +++ b/dao/src/main/java/org/thingsboard/server/dao/sql/queue/JpaQueueStatsDao.java @@ -20,6 +20,7 @@ import org.springframework.beans.factory.annotation.Autowired; import org.springframework.data.jpa.repository.JpaRepository; import org.springframework.stereotype.Component; import org.thingsboard.server.common.data.EntityType; +import org.thingsboard.server.common.data.edqs.fields.QueueStatsFields; import org.thingsboard.server.common.data.id.QueueStatsId; import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.common.data.page.PageData; @@ -74,6 +75,11 @@ public class JpaQueueStatsDao extends JpaAbstractDao findAllFields(PageLink pageLink) { + return DaoUtil.pageToPageData(queueStatsRepository.findAllFields(DaoUtil.toPageable(pageLink))); + } + @Override public EntityType getEntityType() { return EntityType.QUEUE_STATS; diff --git a/dao/src/main/java/org/thingsboard/server/dao/sql/queue/QueueStatsRepository.java b/dao/src/main/java/org/thingsboard/server/dao/sql/queue/QueueStatsRepository.java index 585f010469..594e80e42a 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/sql/queue/QueueStatsRepository.java +++ b/dao/src/main/java/org/thingsboard/server/dao/sql/queue/QueueStatsRepository.java @@ -22,6 +22,7 @@ import org.springframework.data.jpa.repository.Modifying; import org.springframework.data.jpa.repository.Query; import org.springframework.data.repository.query.Param; import org.springframework.transaction.annotation.Transactional; +import org.thingsboard.server.common.data.edqs.fields.QueueStatsFields; import org.thingsboard.server.dao.model.sql.QueueStatsEntity; import java.util.List; @@ -45,4 +46,8 @@ public interface QueueStatsRepository extends JpaRepository findByTenantIdAndIdIn(UUID tenantId, List queueStatsIds); + @Query("SELECT new org.thingsboard.server.common.data.edqs.fields.QueueStatsFields(q.id, q.createdTime," + + "q.tenantId, q.queueName, q.serviceId) FROM QueueStatsEntity q") + Page findAllFields(Pageable pageable); + } \ No newline at end of file diff --git a/dao/src/main/java/org/thingsboard/server/dao/sql/relation/JpaRelationDao.java b/dao/src/main/java/org/thingsboard/server/dao/sql/relation/JpaRelationDao.java index 0e726f8917..1df7115518 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/sql/relation/JpaRelationDao.java +++ b/dao/src/main/java/org/thingsboard/server/dao/sql/relation/JpaRelationDao.java @@ -24,6 +24,8 @@ import org.springframework.util.CollectionUtils; import org.thingsboard.server.common.data.id.EntityId; import org.thingsboard.server.common.data.id.EntityIdFactory; import org.thingsboard.server.common.data.id.TenantId; +import org.thingsboard.server.common.data.page.PageData; +import org.thingsboard.server.common.data.page.PageLink; import org.thingsboard.server.common.data.relation.EntityRelation; import org.thingsboard.server.common.data.relation.RelationTypeGroup; import org.thingsboard.server.common.data.rule.RuleChainType; @@ -127,6 +129,11 @@ public class JpaRelationDao extends JpaAbstractDaoListeningExecutorService imple typeGroup.name())); } + @Override + public PageData findAll(PageLink pageLink) { + return DaoUtil.toPageData(relationRepository.findAll(DaoUtil.toPageable(pageLink))); + } + @Override public ListenableFuture checkRelationAsync(TenantId tenantId, EntityId from, EntityId to, String relationType, RelationTypeGroup typeGroup) { return service.submit(() -> checkRelation(tenantId, from, to, relationType, typeGroup)); diff --git a/dao/src/main/java/org/thingsboard/server/dao/sql/relation/RelationRepository.java b/dao/src/main/java/org/thingsboard/server/dao/sql/relation/RelationRepository.java index 0f56a413df..d945f1f640 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/sql/relation/RelationRepository.java +++ b/dao/src/main/java/org/thingsboard/server/dao/sql/relation/RelationRepository.java @@ -15,6 +15,7 @@ */ package org.thingsboard.server.dao.sql.relation; +import org.springframework.data.domain.Page; import org.springframework.data.domain.Pageable; import org.springframework.data.jpa.repository.JpaRepository; import org.springframework.data.jpa.repository.JpaSpecificationExecutor; @@ -84,4 +85,6 @@ public interface RelationRepository @Query("DELETE FROM RelationEntity r where r.fromId = :fromId and r.fromType = :fromType and r.relationTypeGroup in :relationTypeGroups") void deleteByFromIdAndFromTypeAndRelationTypeGroupIn(@Param("fromId") UUID fromId, @Param("fromType") String fromType, @Param("relationTypeGroups") List relationTypeGroups); + @Query("SELECT e FROM RelationEntity e ORDER BY e.fromId, e.fromType, e.toId, e.toType, e.relationType, e.relationTypeGroup") + Page findAll(Pageable pageable); } diff --git a/dao/src/main/java/org/thingsboard/server/dao/sql/resource/JpaTbResourceDao.java b/dao/src/main/java/org/thingsboard/server/dao/sql/resource/JpaTbResourceDao.java index f8927aaa81..ca0045b153 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/sql/resource/JpaTbResourceDao.java +++ b/dao/src/main/java/org/thingsboard/server/dao/sql/resource/JpaTbResourceDao.java @@ -19,6 +19,7 @@ import lombok.extern.slf4j.Slf4j; import org.springframework.data.jpa.repository.JpaRepository; import org.springframework.stereotype.Component; import org.thingsboard.server.common.data.EntityType; +import org.thingsboard.server.common.data.ObjectType; import org.thingsboard.server.common.data.ResourceSubType; import org.thingsboard.server.common.data.ResourceType; import org.thingsboard.server.common.data.TbResource; @@ -27,6 +28,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.dao.DaoUtil; +import org.thingsboard.server.dao.TenantEntityDao; import org.thingsboard.server.dao.model.sql.TbResourceEntity; import org.thingsboard.server.dao.resource.TbResourceDao; import org.thingsboard.server.dao.sql.JpaAbstractDao; @@ -38,7 +40,7 @@ import java.util.UUID; @Slf4j @Component @SqlDao -public class JpaTbResourceDao extends JpaAbstractDao implements TbResourceDao { +public class JpaTbResourceDao extends JpaAbstractDao implements TbResourceDao, TenantEntityDao { private final TbResourceRepository resourceRepository; @@ -145,4 +147,9 @@ public class JpaTbResourceDao extends JpaAbstractDao implements RpcDao { +public class JpaRpcDao extends JpaAbstractDao implements RpcDao, TenantEntityDao { private final RpcRepository rpcRepository; @@ -74,9 +76,19 @@ public class JpaRpcDao extends JpaAbstractDao implements RpcDao return rpcRepository.deleteOutdatedRpcByTenantId(tenantId.getId(), expirationTime); } + @Override + public PageData findAllByTenantId(TenantId tenantId, PageLink pageLink) { + return findAllRpcByTenantId(tenantId, pageLink); + } + @Override public EntityType getEntityType() { return EntityType.RPC; } + @Override + public ObjectType getType() { + return ObjectType.RPC; + } + } diff --git a/dao/src/main/java/org/thingsboard/server/dao/sql/rule/JpaRuleChainDao.java b/dao/src/main/java/org/thingsboard/server/dao/sql/rule/JpaRuleChainDao.java index eaf151f256..588be25ba9 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/sql/rule/JpaRuleChainDao.java +++ b/dao/src/main/java/org/thingsboard/server/dao/sql/rule/JpaRuleChainDao.java @@ -20,6 +20,8 @@ import org.springframework.beans.factory.annotation.Autowired; import org.springframework.data.jpa.repository.JpaRepository; import org.springframework.stereotype.Component; import org.thingsboard.server.common.data.EntityType; +import org.thingsboard.server.common.data.ObjectType; +import org.thingsboard.server.common.data.edqs.fields.RuleChainFields; import org.thingsboard.server.common.data.id.RuleChainId; import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.common.data.page.PageData; @@ -133,9 +135,24 @@ public class JpaRuleChainDao extends JpaAbstractDao return findRootRuleChainByTenantIdAndType(tenantId, RuleChainType.CORE); } + @Override + public PageData findAllByTenantId(TenantId tenantId, PageLink pageLink) { + return findRuleChainsByTenantId(tenantId.getId(), pageLink); + } + + @Override + public PageData findAllFields(PageLink pageLink) { + return DaoUtil.pageToPageData(ruleChainRepository.findAllFields(DaoUtil.toPageable(pageLink))); + } + @Override public EntityType getEntityType() { return EntityType.RULE_CHAIN; } + @Override + public ObjectType getType() { + return ObjectType.RULE_CHAIN; + } + } diff --git a/dao/src/main/java/org/thingsboard/server/dao/sql/rule/JpaRuleNodeDao.java b/dao/src/main/java/org/thingsboard/server/dao/sql/rule/JpaRuleNodeDao.java index c267eab4a8..307f15c7f4 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/sql/rule/JpaRuleNodeDao.java +++ b/dao/src/main/java/org/thingsboard/server/dao/sql/rule/JpaRuleNodeDao.java @@ -20,6 +20,7 @@ import org.springframework.beans.factory.annotation.Autowired; import org.springframework.data.jpa.repository.JpaRepository; import org.springframework.stereotype.Component; import org.thingsboard.server.common.data.EntityType; +import org.thingsboard.server.common.data.ObjectType; import org.thingsboard.server.common.data.id.RuleChainId; import org.thingsboard.server.common.data.id.RuleNodeId; import org.thingsboard.server.common.data.id.TenantId; @@ -27,6 +28,7 @@ import org.thingsboard.server.common.data.page.PageData; import org.thingsboard.server.common.data.page.PageLink; import org.thingsboard.server.common.data.rule.RuleNode; import org.thingsboard.server.dao.DaoUtil; +import org.thingsboard.server.dao.TenantEntityDao; import org.thingsboard.server.dao.model.sql.RuleNodeEntity; import org.thingsboard.server.dao.rule.RuleNodeDao; import org.thingsboard.server.dao.sql.JpaAbstractDao; @@ -40,7 +42,7 @@ import java.util.stream.Collectors; @Slf4j @Component @SqlDao -public class JpaRuleNodeDao extends JpaAbstractDao implements RuleNodeDao { +public class JpaRuleNodeDao extends JpaAbstractDao implements RuleNodeDao, TenantEntityDao { @Autowired private RuleNodeRepository ruleNodeRepository; @@ -106,9 +108,19 @@ public class JpaRuleNodeDao extends JpaAbstractDao imp ruleNodeRepository.deleteAllById(ruleNodeIds.stream().map(RuleNodeId::getId).collect(Collectors.toList())); } + @Override + public PageData findAllByTenantId(TenantId tenantId, PageLink pageLink) { + return DaoUtil.toPageData(ruleNodeRepository.findByTenantId(tenantId.getId(), DaoUtil.toPageable(pageLink))); + } + @Override public EntityType getEntityType() { return EntityType.RULE_NODE; } + @Override + public ObjectType getType() { + return ObjectType.RULE_NODE; + } + } diff --git a/dao/src/main/java/org/thingsboard/server/dao/sql/rule/RuleChainRepository.java b/dao/src/main/java/org/thingsboard/server/dao/sql/rule/RuleChainRepository.java index 305d5993d4..6713e4b645 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/sql/rule/RuleChainRepository.java +++ b/dao/src/main/java/org/thingsboard/server/dao/sql/rule/RuleChainRepository.java @@ -20,6 +20,7 @@ import org.springframework.data.domain.Pageable; import org.springframework.data.jpa.repository.JpaRepository; import org.springframework.data.jpa.repository.Query; import org.springframework.data.repository.query.Param; +import org.thingsboard.server.common.data.edqs.fields.RuleChainFields; import org.thingsboard.server.common.data.rule.RuleChainType; import org.thingsboard.server.dao.ExportableEntityRepository; import org.thingsboard.server.dao.model.sql.RuleChainEntity; @@ -70,4 +71,7 @@ public interface RuleChainRepository extends JpaRepository findAllFields(Pageable pageable); } diff --git a/dao/src/main/java/org/thingsboard/server/dao/sql/rule/RuleNodeRepository.java b/dao/src/main/java/org/thingsboard/server/dao/sql/rule/RuleNodeRepository.java index 264ea88326..bf19d61a9d 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/sql/rule/RuleNodeRepository.java +++ b/dao/src/main/java/org/thingsboard/server/dao/sql/rule/RuleNodeRepository.java @@ -63,4 +63,7 @@ public interface RuleNodeRepository extends JpaRepository @Query("DELETE FROM RuleNodeEntity e where e.id in :ids") void deleteByIdIn(@Param("ids") List ids); + @Query("SELECT n FROM RuleNodeEntity n WHERE n.ruleChainId IN (SELECT rc.id FROM RuleChainEntity rc WHERE rc.tenantId = :tenantId)") + Page findByTenantId(@Param("tenantId") UUID tenantId, Pageable pageable); + } diff --git a/dao/src/main/java/org/thingsboard/server/dao/sql/settings/AdminSettingsRepository.java b/dao/src/main/java/org/thingsboard/server/dao/sql/settings/AdminSettingsRepository.java index b390bfffd8..78cf891870 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/sql/settings/AdminSettingsRepository.java +++ b/dao/src/main/java/org/thingsboard/server/dao/sql/settings/AdminSettingsRepository.java @@ -15,6 +15,8 @@ */ package org.thingsboard.server.dao.sql.settings; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.Pageable; import org.springframework.data.jpa.repository.JpaRepository; import org.thingsboard.server.dao.model.sql.AdminSettingsEntity; @@ -33,4 +35,6 @@ public interface AdminSettingsRepository extends JpaRepository findByTenantId(UUID tenantId, Pageable pageable); + } diff --git a/dao/src/main/java/org/thingsboard/server/dao/sql/settings/JpaAdminSettingsDao.java b/dao/src/main/java/org/thingsboard/server/dao/sql/settings/JpaAdminSettingsDao.java index 9aa348876e..9e0f6a70cb 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/sql/settings/JpaAdminSettingsDao.java +++ b/dao/src/main/java/org/thingsboard/server/dao/sql/settings/JpaAdminSettingsDao.java @@ -21,7 +21,12 @@ import org.springframework.data.jpa.repository.JpaRepository; import org.springframework.stereotype.Component; import org.springframework.transaction.annotation.Transactional; import org.thingsboard.server.common.data.AdminSettings; +import org.thingsboard.server.common.data.ObjectType; +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.dao.DaoUtil; +import org.thingsboard.server.dao.TenantEntityDao; import org.thingsboard.server.dao.model.sql.AdminSettingsEntity; import org.thingsboard.server.dao.settings.AdminSettingsDao; import org.thingsboard.server.dao.sql.JpaAbstractDao; @@ -32,7 +37,7 @@ import java.util.UUID; @Component @SqlDao @Slf4j -public class JpaAdminSettingsDao extends JpaAbstractDao implements AdminSettingsDao { +public class JpaAdminSettingsDao extends JpaAbstractDao implements AdminSettingsDao, TenantEntityDao { @Autowired private AdminSettingsRepository adminSettingsRepository; @@ -68,4 +73,14 @@ public class JpaAdminSettingsDao extends JpaAbstractDao findAllByTenantId(TenantId tenantId, PageLink pageLink) { + return DaoUtil.toPageData(adminSettingsRepository.findByTenantId(tenantId.getId(), DaoUtil.toPageable(pageLink))); + } + + @Override + public ObjectType getType() { + return ObjectType.ADMIN_SETTINGS; + } + } diff --git a/dao/src/main/java/org/thingsboard/server/dao/sql/tenant/JpaTenantDao.java b/dao/src/main/java/org/thingsboard/server/dao/sql/tenant/JpaTenantDao.java index cea414a42f..ff014d012f 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/sql/tenant/JpaTenantDao.java +++ b/dao/src/main/java/org/thingsboard/server/dao/sql/tenant/JpaTenantDao.java @@ -21,6 +21,7 @@ import org.springframework.stereotype.Component; import org.thingsboard.server.common.data.EntityType; import org.thingsboard.server.common.data.Tenant; import org.thingsboard.server.common.data.TenantInfo; +import org.thingsboard.server.common.data.edqs.fields.TenantFields; import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.common.data.id.TenantProfileId; import org.thingsboard.server.common.data.page.PageData; @@ -94,4 +95,15 @@ public class JpaTenantDao extends JpaAbstractDao implement .map(TenantId::fromUUID) .collect(Collectors.toList()); } + + @Override + public PageData findAllFields(PageLink pageLink) { + return DaoUtil.pageToPageData(tenantRepository.findAllFields(DaoUtil.toPageable(pageLink))); + } + + @Override + public EntityType getEntityType() { + return EntityType.TENANT; + } + } diff --git a/dao/src/main/java/org/thingsboard/server/dao/sql/tenant/JpaTenantProfileDao.java b/dao/src/main/java/org/thingsboard/server/dao/sql/tenant/JpaTenantProfileDao.java index 372c5bc9b8..4668c44b61 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/sql/tenant/JpaTenantProfileDao.java +++ b/dao/src/main/java/org/thingsboard/server/dao/sql/tenant/JpaTenantProfileDao.java @@ -21,6 +21,7 @@ import org.springframework.stereotype.Component; import org.thingsboard.server.common.data.EntityInfo; import org.thingsboard.server.common.data.EntityType; import org.thingsboard.server.common.data.TenantProfile; +import org.thingsboard.server.common.data.edqs.fields.TenantProfileFields; import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.common.data.page.PageData; import org.thingsboard.server.common.data.page.PageLink; @@ -87,6 +88,11 @@ public class JpaTenantProfileDao extends JpaAbstractDao findAllFields(PageLink pageLink) { + return DaoUtil.pageToPageData(tenantProfileRepository.findAllFields(DaoUtil.toPageable(pageLink))); + } + @Override public EntityType getEntityType() { return EntityType.TENANT_PROFILE; diff --git a/dao/src/main/java/org/thingsboard/server/dao/sql/tenant/TenantProfileRepository.java b/dao/src/main/java/org/thingsboard/server/dao/sql/tenant/TenantProfileRepository.java index a9cbf3d591..b7a57d0942 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/sql/tenant/TenantProfileRepository.java +++ b/dao/src/main/java/org/thingsboard/server/dao/sql/tenant/TenantProfileRepository.java @@ -21,6 +21,7 @@ import org.springframework.data.jpa.repository.JpaRepository; import org.springframework.data.jpa.repository.Query; import org.springframework.data.repository.query.Param; import org.thingsboard.server.common.data.EntityInfo; +import org.thingsboard.server.common.data.edqs.fields.TenantProfileFields; import org.thingsboard.server.dao.model.sql.TenantProfileEntity; import java.util.List; @@ -55,4 +56,8 @@ public interface TenantProfileRepository extends JpaRepository findByIdIn(List ids); + @Query("SELECT new org.thingsboard.server.common.data.edqs.fields.TenantProfileFields(t.id, t.createdTime, t.name," + + "t.isDefault) FROM TenantProfileEntity t") + Page findAllFields(Pageable pageable); + } diff --git a/dao/src/main/java/org/thingsboard/server/dao/sql/tenant/TenantRepository.java b/dao/src/main/java/org/thingsboard/server/dao/sql/tenant/TenantRepository.java index 795404399e..86dbeaff80 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/sql/tenant/TenantRepository.java +++ b/dao/src/main/java/org/thingsboard/server/dao/sql/tenant/TenantRepository.java @@ -20,6 +20,7 @@ import org.springframework.data.domain.Pageable; import org.springframework.data.jpa.repository.JpaRepository; import org.springframework.data.jpa.repository.Query; import org.springframework.data.repository.query.Param; +import org.thingsboard.server.common.data.edqs.fields.TenantFields; import org.thingsboard.server.dao.model.sql.TenantEntity; import org.thingsboard.server.dao.model.sql.TenantInfoEntity; @@ -53,4 +54,8 @@ public interface TenantRepository extends JpaRepository { @Query("SELECT t.id FROM TenantEntity t where t.tenantProfileId = :tenantProfileId") List findTenantIdsByTenantProfileId(@Param("tenantProfileId") UUID tenantProfileId); + + @Query("SELECT new org.thingsboard.server.common.data.edqs.fields.TenantFields(t.id, t.createdTime, t.title, t.version," + + "t.additionalInfo, t.country, t.state, t.city, t.address, t.address2, t.zip, t.phone, t.email, t.region) FROM TenantEntity t") + Page findAllFields(Pageable pageable); } diff --git a/dao/src/main/java/org/thingsboard/server/dao/sql/usagerecord/ApiUsageStateRepository.java b/dao/src/main/java/org/thingsboard/server/dao/sql/usagerecord/ApiUsageStateRepository.java index 88873e8671..509796879c 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/sql/usagerecord/ApiUsageStateRepository.java +++ b/dao/src/main/java/org/thingsboard/server/dao/sql/usagerecord/ApiUsageStateRepository.java @@ -15,11 +15,14 @@ */ package org.thingsboard.server.dao.sql.usagerecord; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.Pageable; import org.springframework.data.jpa.repository.JpaRepository; import org.springframework.data.jpa.repository.Modifying; import org.springframework.data.jpa.repository.Query; import org.springframework.data.repository.query.Param; import org.springframework.transaction.annotation.Transactional; +import org.thingsboard.server.common.data.edqs.fields.ApiUsageStateFields; import org.thingsboard.server.dao.model.sql.ApiUsageStateEntity; import java.util.UUID; @@ -35,6 +38,8 @@ public interface ApiUsageStateRepository extends JpaRepository findAllByTenantId(UUID tenantId, Pageable pageable); + @Transactional @Modifying @Query("DELETE FROM ApiUsageStateEntity ur WHERE ur.tenantId = :tenantId") @@ -44,4 +49,9 @@ public interface ApiUsageStateRepository extends JpaRepository findAllFields(Pageable pageable); + } diff --git a/dao/src/main/java/org/thingsboard/server/dao/sql/usagerecord/JpaApiUsageStateDao.java b/dao/src/main/java/org/thingsboard/server/dao/sql/usagerecord/JpaApiUsageStateDao.java index 63c0d9d3d1..85d9d18839 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/sql/usagerecord/JpaApiUsageStateDao.java +++ b/dao/src/main/java/org/thingsboard/server/dao/sql/usagerecord/JpaApiUsageStateDao.java @@ -19,8 +19,12 @@ import org.springframework.data.jpa.repository.JpaRepository; import org.springframework.stereotype.Component; import org.thingsboard.server.common.data.ApiUsageState; import org.thingsboard.server.common.data.EntityType; +import org.thingsboard.server.common.data.ObjectType; +import org.thingsboard.server.common.data.edqs.fields.ApiUsageStateFields; import org.thingsboard.server.common.data.id.EntityId; 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.dao.DaoUtil; import org.thingsboard.server.dao.model.sql.ApiUsageStateEntity; import org.thingsboard.server.dao.sql.JpaAbstractDao; @@ -72,9 +76,24 @@ public class JpaApiUsageStateDao extends JpaAbstractDao findAllByTenantId(TenantId tenantId, PageLink pageLink) { + return DaoUtil.toPageData(apiUsageStateRepository.findAllByTenantId(tenantId.getId(), DaoUtil.toPageable(pageLink))); + } + + @Override + public PageData findAllFields(PageLink pageLink) { + return DaoUtil.pageToPageData(apiUsageStateRepository.findAllFields(DaoUtil.toPageable(pageLink))); + } + @Override public EntityType getEntityType() { return EntityType.API_USAGE_STATE; } + @Override + public ObjectType getType() { + return ObjectType.API_USAGE_STATE; + } + } diff --git a/dao/src/main/java/org/thingsboard/server/dao/sql/user/JpaUserAuthSettingsDao.java b/dao/src/main/java/org/thingsboard/server/dao/sql/user/JpaUserAuthSettingsDao.java index 316e34de87..171d10b0c2 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/sql/user/JpaUserAuthSettingsDao.java +++ b/dao/src/main/java/org/thingsboard/server/dao/sql/user/JpaUserAuthSettingsDao.java @@ -18,9 +18,15 @@ package org.thingsboard.server.dao.sql.user; import lombok.RequiredArgsConstructor; import org.springframework.data.jpa.repository.JpaRepository; import org.springframework.stereotype.Component; +import org.thingsboard.server.common.data.ObjectType; +import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.common.data.id.UserId; +import org.thingsboard.server.common.data.page.PageData; +import org.thingsboard.server.common.data.page.PageLink; import org.thingsboard.server.common.data.security.UserAuthSettings; +import org.thingsboard.server.common.data.security.model.mfa.account.AccountTwoFaSettings; import org.thingsboard.server.dao.DaoUtil; +import org.thingsboard.server.dao.TenantEntityDao; import org.thingsboard.server.dao.model.sql.UserAuthSettingsEntity; import org.thingsboard.server.dao.sql.JpaAbstractDao; import org.thingsboard.server.dao.user.UserAuthSettingsDao; @@ -31,7 +37,7 @@ import java.util.UUID; @Component @RequiredArgsConstructor @SqlDao -public class JpaUserAuthSettingsDao extends JpaAbstractDao implements UserAuthSettingsDao { +public class JpaUserAuthSettingsDao extends JpaAbstractDao implements UserAuthSettingsDao, TenantEntityDao { private final UserAuthSettingsRepository repository; @@ -45,6 +51,18 @@ public class JpaUserAuthSettingsDao extends JpaAbstractDao findAllByTenantId(TenantId tenantId, PageLink pageLink) { + PageData data = DaoUtil.toPageData(repository.findByTenantId(tenantId.getId(), DaoUtil.toPageable(pageLink))); + data.getData().forEach(settings -> { + AccountTwoFaSettings twoFaSettings = settings.getTwoFaSettings(); + if (twoFaSettings != null && twoFaSettings.getConfigs() != null) { + twoFaSettings.getConfigs().values().forEach(config -> config.setSerializeHiddenFields(true)); + } + }); + return data; + } + @Override protected Class getEntityClass() { return UserAuthSettingsEntity.class; @@ -55,4 +73,9 @@ public class JpaUserAuthSettingsDao extends JpaAbstractDao implements UserCredentialsDao { +public class JpaUserCredentialsDao extends JpaAbstractDao implements UserCredentialsDao, TenantEntityDao { @Autowired private UserCredentialsRepository userCredentialsRepository; @@ -84,4 +88,14 @@ public class JpaUserCredentialsDao extends JpaAbstractDao findAllByTenantId(TenantId tenantId, PageLink pageLink) { + return DaoUtil.toPageData(userCredentialsRepository.findByTenantId(tenantId.getId(), DaoUtil.toPageable(pageLink))); + } + + @Override + public ObjectType getType() { + return ObjectType.USER_CREDENTIALS; + } + } diff --git a/dao/src/main/java/org/thingsboard/server/dao/sql/user/JpaUserDao.java b/dao/src/main/java/org/thingsboard/server/dao/sql/user/JpaUserDao.java index 7749dbff29..55d74634d6 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/sql/user/JpaUserDao.java +++ b/dao/src/main/java/org/thingsboard/server/dao/sql/user/JpaUserDao.java @@ -19,7 +19,9 @@ import org.springframework.beans.factory.annotation.Autowired; import org.springframework.data.jpa.repository.JpaRepository; import org.springframework.stereotype.Component; import org.thingsboard.server.common.data.EntityType; +import org.thingsboard.server.common.data.ObjectType; import org.thingsboard.server.common.data.User; +import org.thingsboard.server.common.data.edqs.fields.UserFields; import org.thingsboard.server.common.data.id.CustomerId; import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.common.data.id.TenantProfileId; @@ -139,9 +141,24 @@ public class JpaUserDao extends JpaAbstractDao implements User return userRepository.countByTenantId(tenantId.getId()); } + @Override + public PageData findAllByTenantId(TenantId tenantId, PageLink pageLink) { + return findByTenantId(tenantId.getId(), pageLink); + } + + @Override + public PageData findAllFields(PageLink pageLink) { + return DaoUtil.pageToPageData(userRepository.findAllFields(DaoUtil.toPageable(pageLink))); + } + @Override public EntityType getEntityType() { return EntityType.USER; } + @Override + public ObjectType getType() { + return ObjectType.USER; + } + } diff --git a/dao/src/main/java/org/thingsboard/server/dao/sql/user/JpaUserSettingsDao.java b/dao/src/main/java/org/thingsboard/server/dao/sql/user/JpaUserSettingsDao.java index 671304256e..b41bfa7728 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/sql/user/JpaUserSettingsDao.java +++ b/dao/src/main/java/org/thingsboard/server/dao/sql/user/JpaUserSettingsDao.java @@ -18,14 +18,17 @@ package org.thingsboard.server.dao.sql.user; import lombok.extern.slf4j.Slf4j; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Component; +import org.thingsboard.server.common.data.ObjectType; import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.common.data.id.UserId; +import org.thingsboard.server.common.data.page.PageData; +import org.thingsboard.server.common.data.page.PageLink; import org.thingsboard.server.common.data.settings.UserSettings; import org.thingsboard.server.common.data.settings.UserSettingsCompositeKey; import org.thingsboard.server.common.data.settings.UserSettingsType; import org.thingsboard.server.dao.DaoUtil; +import org.thingsboard.server.dao.TenantEntityDao; import org.thingsboard.server.dao.model.sql.UserSettingsEntity; -import org.thingsboard.server.dao.sql.JpaAbstractDaoListeningExecutorService; import org.thingsboard.server.dao.user.UserSettingsDao; import org.thingsboard.server.dao.util.SqlDao; @@ -34,7 +37,7 @@ import java.util.List; @Slf4j @Component @SqlDao -public class JpaUserSettingsDao extends JpaAbstractDaoListeningExecutorService implements UserSettingsDao { +public class JpaUserSettingsDao implements UserSettingsDao, TenantEntityDao { @Autowired private UserSettingsRepository userSettingsRepository; @@ -66,4 +69,14 @@ public class JpaUserSettingsDao extends JpaAbstractDaoListeningExecutorService i return DaoUtil.convertDataList(userSettingsRepository.findByTypeAndPathExisting(type.name(), path)); } + @Override + public PageData findAllByTenantId(TenantId tenantId, PageLink pageLink) { + return DaoUtil.toPageData(userSettingsRepository.findByTenantId(tenantId.getId(), DaoUtil.toPageable(pageLink))); + } + + @Override + public ObjectType getType() { + return ObjectType.USER_SETTINGS; + } + } diff --git a/dao/src/main/java/org/thingsboard/server/dao/sql/user/UserAuthSettingsRepository.java b/dao/src/main/java/org/thingsboard/server/dao/sql/user/UserAuthSettingsRepository.java index cea5d428dd..3c9d69f085 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/sql/user/UserAuthSettingsRepository.java +++ b/dao/src/main/java/org/thingsboard/server/dao/sql/user/UserAuthSettingsRepository.java @@ -15,6 +15,8 @@ */ package org.thingsboard.server.dao.sql.user; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.Pageable; import org.springframework.data.jpa.repository.JpaRepository; import org.springframework.data.jpa.repository.Modifying; import org.springframework.data.jpa.repository.Query; @@ -35,4 +37,7 @@ public interface UserAuthSettingsRepository extends JpaRepository findByTenantId(@Param("tenantId") UUID tenantId, Pageable pageable); + } diff --git a/dao/src/main/java/org/thingsboard/server/dao/sql/user/UserCredentialsRepository.java b/dao/src/main/java/org/thingsboard/server/dao/sql/user/UserCredentialsRepository.java index 4aca40647c..682219ed3a 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/sql/user/UserCredentialsRepository.java +++ b/dao/src/main/java/org/thingsboard/server/dao/sql/user/UserCredentialsRepository.java @@ -15,9 +15,12 @@ */ package org.thingsboard.server.dao.sql.user; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.Pageable; import org.springframework.data.jpa.repository.JpaRepository; import org.springframework.data.jpa.repository.Modifying; import org.springframework.data.jpa.repository.Query; +import org.springframework.data.repository.query.Param; import org.springframework.transaction.annotation.Transactional; import org.thingsboard.server.dao.model.sql.UserCredentialsEntity; @@ -52,4 +55,7 @@ public interface UserCredentialsRepository extends JpaRepository findByTenantId(@Param("tenantId") UUID tenantId, Pageable pageable); + } diff --git a/dao/src/main/java/org/thingsboard/server/dao/sql/user/UserRepository.java b/dao/src/main/java/org/thingsboard/server/dao/sql/user/UserRepository.java index 79b0a00308..e67330a1a4 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/sql/user/UserRepository.java +++ b/dao/src/main/java/org/thingsboard/server/dao/sql/user/UserRepository.java @@ -20,6 +20,7 @@ import org.springframework.data.domain.Pageable; import org.springframework.data.jpa.repository.JpaRepository; import org.springframework.data.jpa.repository.Query; import org.springframework.data.repository.query.Param; +import org.thingsboard.server.common.data.edqs.fields.UserFields; import org.thingsboard.server.common.data.security.Authority; import org.thingsboard.server.dao.model.sql.UserEntity; @@ -71,4 +72,9 @@ public interface UserRepository extends JpaRepository { Long countByTenantId(UUID tenantId); + @Query("SELECT new org.thingsboard.server.common.data.edqs.fields.UserFields(u.id, u.createdTime, u.tenantId," + + "u.customerId, u.version, u.firstName, u.lastName, u.email, u.phone, u.additionalInfo) " + + "FROM UserEntity u") + Page findAllFields(Pageable pageable); + } diff --git a/dao/src/main/java/org/thingsboard/server/dao/sql/user/UserSettingsRepository.java b/dao/src/main/java/org/thingsboard/server/dao/sql/user/UserSettingsRepository.java index 436062ef4c..9a0add31ec 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/sql/user/UserSettingsRepository.java +++ b/dao/src/main/java/org/thingsboard/server/dao/sql/user/UserSettingsRepository.java @@ -15,6 +15,8 @@ */ package org.thingsboard.server.dao.sql.user; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.Pageable; import org.springframework.data.jpa.repository.JpaRepository; import org.springframework.data.jpa.repository.Modifying; import org.springframework.data.jpa.repository.Query; @@ -36,4 +38,7 @@ public interface UserSettingsRepository extends JpaRepository findByTypeAndPathExisting(@Param("type") String type, @Param("path") String[] path); + @Query("SELECT s FROM UserSettingsEntity s WHERE s.userId IN (SELECT u.id FROM UserEntity u WHERE u.tenantId = :tenantId)") + Page findByTenantId(@Param("tenantId") UUID tenantId, Pageable pageable); + } diff --git a/dao/src/main/java/org/thingsboard/server/dao/sql/widget/JpaWidgetTypeDao.java b/dao/src/main/java/org/thingsboard/server/dao/sql/widget/JpaWidgetTypeDao.java index 6220d9b717..ed0c5155b9 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/sql/widget/JpaWidgetTypeDao.java +++ b/dao/src/main/java/org/thingsboard/server/dao/sql/widget/JpaWidgetTypeDao.java @@ -19,6 +19,9 @@ import org.springframework.beans.factory.annotation.Autowired; import org.springframework.data.jpa.repository.JpaRepository; import org.springframework.stereotype.Component; import org.thingsboard.server.common.data.EntityType; +import org.thingsboard.server.common.data.ObjectType; +import org.thingsboard.server.common.data.edqs.fields.UserFields; +import org.thingsboard.server.common.data.edqs.fields.WidgetTypeFields; import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.common.data.id.WidgetTypeId; import org.thingsboard.server.common.data.page.PageData; @@ -30,6 +33,7 @@ import org.thingsboard.server.common.data.widget.WidgetTypeFilter; import org.thingsboard.server.common.data.widget.WidgetTypeInfo; import org.thingsboard.server.common.data.widget.WidgetsBundleWidget; import org.thingsboard.server.dao.DaoUtil; +import org.thingsboard.server.dao.TenantEntityDao; import org.thingsboard.server.dao.model.sql.WidgetTypeDetailsEntity; import org.thingsboard.server.dao.model.sql.WidgetTypeInfoEntity; import org.thingsboard.server.dao.model.sql.WidgetsBundleWidgetCompositeKey; @@ -53,7 +57,7 @@ import static org.thingsboard.server.dao.model.ModelConstants.NULL_UUID; */ @Component @SqlDao -public class JpaWidgetTypeDao extends JpaAbstractDao implements WidgetTypeDao { +public class JpaWidgetTypeDao extends JpaAbstractDao implements WidgetTypeDao, TenantEntityDao { @Autowired private WidgetTypeRepository widgetTypeRepository; @@ -255,10 +259,24 @@ public class JpaWidgetTypeDao extends JpaAbstractDao findAllByTenantId(TenantId tenantId, PageLink pageLink) { + return findByTenantId(tenantId.getId(), pageLink); + } + + @Override + public PageData findAllFields(PageLink pageLink) { + return DaoUtil.pageToPageData(widgetTypeRepository.findAllFields(DaoUtil.toPageable(pageLink))); + } + @Override public EntityType getEntityType() { return EntityType.WIDGET_TYPE; } + @Override + public ObjectType getType() { + return ObjectType.WIDGET_TYPE; + } } diff --git a/dao/src/main/java/org/thingsboard/server/dao/sql/widget/JpaWidgetsBundleDao.java b/dao/src/main/java/org/thingsboard/server/dao/sql/widget/JpaWidgetsBundleDao.java index 290e949c38..5c9b4127f1 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/sql/widget/JpaWidgetsBundleDao.java +++ b/dao/src/main/java/org/thingsboard/server/dao/sql/widget/JpaWidgetsBundleDao.java @@ -19,6 +19,8 @@ import org.springframework.beans.factory.annotation.Autowired; import org.springframework.data.jpa.repository.JpaRepository; import org.springframework.stereotype.Component; import org.thingsboard.server.common.data.EntityType; +import org.thingsboard.server.common.data.ObjectType; +import org.thingsboard.server.common.data.edqs.fields.WidgetsBundleFields; import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.common.data.id.WidgetsBundleId; import org.thingsboard.server.common.data.page.PageData; @@ -26,6 +28,7 @@ import org.thingsboard.server.common.data.page.PageLink; import org.thingsboard.server.common.data.widget.WidgetsBundle; import org.thingsboard.server.common.data.widget.WidgetsBundleFilter; import org.thingsboard.server.dao.DaoUtil; +import org.thingsboard.server.dao.TenantEntityDao; import org.thingsboard.server.dao.model.sql.WidgetsBundleEntity; import org.thingsboard.server.dao.sql.JpaAbstractDao; import org.thingsboard.server.dao.util.SqlDao; @@ -44,7 +47,7 @@ import static org.thingsboard.server.dao.model.ModelConstants.NULL_UUID; */ @Component @SqlDao -public class JpaWidgetsBundleDao extends JpaAbstractDao implements WidgetsBundleDao { +public class JpaWidgetsBundleDao extends JpaAbstractDao implements WidgetsBundleDao, TenantEntityDao { @Autowired private WidgetsBundleRepository widgetsBundleRepository; @@ -155,7 +158,17 @@ public class JpaWidgetsBundleDao extends JpaAbstractDao findByImageLink(String imageUrl, int limit) { - return DaoUtil.convertDataList(widgetsBundleRepository.findByImageUrl(imageUrl, limit)); + return DaoUtil.convertDataList(widgetsBundleRepository.findByImageUrl(imageUrl, limit)); + } + + @Override + public PageData findAllByTenantId(TenantId tenantId, PageLink pageLink) { + return findByTenantId(tenantId.getId(), pageLink); + } + + @Override + public PageData findAllFields(PageLink pageLink) { + return DaoUtil.pageToPageData(widgetsBundleRepository.findAllFields(DaoUtil.toPageable(pageLink))); } @Override @@ -163,4 +176,9 @@ public class JpaWidgetsBundleDao extends JpaAbstractDao findIdsByTenantId(@Param("tenantId") UUID tenantId, Pageable pageable); + @Query("SELECT new org.thingsboard.server.common.data.edqs.fields.WidgetTypeFields(w.id, w.createdTime, w.tenantId," + + "w.name, w.version) FROM WidgetTypeEntity w") + Page findAllFields(Pageable pageable); + } diff --git a/dao/src/main/java/org/thingsboard/server/dao/sql/widget/WidgetsBundleRepository.java b/dao/src/main/java/org/thingsboard/server/dao/sql/widget/WidgetsBundleRepository.java index 8de78986ae..97b2f52d63 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/sql/widget/WidgetsBundleRepository.java +++ b/dao/src/main/java/org/thingsboard/server/dao/sql/widget/WidgetsBundleRepository.java @@ -20,6 +20,7 @@ import org.springframework.data.domain.Pageable; import org.springframework.data.jpa.repository.JpaRepository; import org.springframework.data.jpa.repository.Query; import org.springframework.data.repository.query.Param; +import org.thingsboard.server.common.data.edqs.fields.WidgetsBundleFields; import org.thingsboard.server.dao.ExportableEntityRepository; import org.thingsboard.server.dao.model.sql.WidgetsBundleEntity; @@ -139,4 +140,8 @@ public interface WidgetsBundleRepository extends JpaRepository findByImageUrl(@Param("imageLink") String imageLink, @Param("lmt") int lmt); + + @Query("SELECT new org.thingsboard.server.common.data.edqs.fields.WidgetsBundleFields(w.id, w.createdTime, w.tenantId," + + "w.alias, w.version) FROM WidgetsBundleEntity w") + Page findAllFields(Pageable pageable); } diff --git a/dao/src/main/java/org/thingsboard/server/dao/sql/widget/WidgetsBundleWidgetRepository.java b/dao/src/main/java/org/thingsboard/server/dao/sql/widget/WidgetsBundleWidgetRepository.java index 89533106f3..3a024e1687 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/sql/widget/WidgetsBundleWidgetRepository.java +++ b/dao/src/main/java/org/thingsboard/server/dao/sql/widget/WidgetsBundleWidgetRepository.java @@ -15,7 +15,11 @@ */ package org.thingsboard.server.dao.sql.widget; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.Pageable; import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.Query; +import org.springframework.data.repository.query.Param; import org.thingsboard.server.dao.model.sql.WidgetsBundleWidgetCompositeKey; import org.thingsboard.server.dao.model.sql.WidgetsBundleWidgetEntity; @@ -26,4 +30,7 @@ public interface WidgetsBundleWidgetRepository extends JpaRepository findAllByWidgetsBundleId(UUID widgetsBundleId); + @Query("SELECT w FROM WidgetsBundleWidgetEntity w WHERE w.widgetsBundleId IN (SELECT b.id FROM WidgetsBundleEntity b WHERE b.tenantId = :tenantId)") + Page findByTenantId(@Param("tenantId") UUID tenantId, Pageable pageable); + } diff --git a/dao/src/main/java/org/thingsboard/server/dao/sqlts/AbstractSqlTimeseriesDao.java b/dao/src/main/java/org/thingsboard/server/dao/sqlts/AbstractSqlTimeseriesDao.java index 18364c6b1b..b134b6308d 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/sqlts/AbstractSqlTimeseriesDao.java +++ b/dao/src/main/java/org/thingsboard/server/dao/sqlts/AbstractSqlTimeseriesDao.java @@ -39,7 +39,6 @@ import java.util.Objects; import java.util.concurrent.TimeUnit; import java.util.stream.Collectors; -@SuppressWarnings("UnstableApiUsage") @Slf4j public abstract class AbstractSqlTimeseriesDao extends BaseAbstractSqlTimeseriesDao implements AggregationTimeseriesDao { @@ -119,4 +118,5 @@ public abstract class AbstractSqlTimeseriesDao extends BaseAbstractSqlTimeseries protected int getDataPointDays(TsKvEntry tsKvEntry, long ttl) { return tsKvEntry.getDataPoints() * Math.max(1, (int) (ttl / SECONDS_IN_DAY)); } + } diff --git a/dao/src/main/java/org/thingsboard/server/dao/sqlts/CachedRedisSqlTimeseriesLatestDao.java b/dao/src/main/java/org/thingsboard/server/dao/sqlts/CachedRedisSqlTimeseriesLatestDao.java index 0be078a89f..0e9cebd150 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/sqlts/CachedRedisSqlTimeseriesLatestDao.java +++ b/dao/src/main/java/org/thingsboard/server/dao/sqlts/CachedRedisSqlTimeseriesLatestDao.java @@ -33,14 +33,18 @@ import org.thingsboard.server.common.data.kv.BasicTsKvEntry; import org.thingsboard.server.common.data.kv.DeleteTsKvQuery; import org.thingsboard.server.common.data.kv.TsKvEntry; import org.thingsboard.server.common.data.kv.TsKvLatestRemovingResult; +import org.thingsboard.server.common.data.page.PageData; +import org.thingsboard.server.common.data.page.PageLink; import org.thingsboard.server.common.stats.DefaultCounter; import org.thingsboard.server.common.stats.StatsFactory; import org.thingsboard.server.dao.cache.CacheExecutorService; +import org.thingsboard.server.dao.model.sqlts.latest.TsKvLatestEntity; import org.thingsboard.server.dao.timeseries.TimeseriesLatestDao; import org.thingsboard.server.dao.timeseries.TsLatestCacheKey; import org.thingsboard.server.dao.util.SqlTsLatestAnyDaoCachedRedis; import java.util.List; +import java.util.Map; import java.util.Optional; @Slf4j @@ -167,4 +171,9 @@ public class CachedRedisSqlTimeseriesLatestDao extends BaseAbstractSqlTimeseries return sqlDao.findAllKeysByEntityIds(tenantId, entityIds); } + @Override + public PageData findAllLatest(PageLink pageLink) { + return null; + } + } diff --git a/dao/src/main/java/org/thingsboard/server/dao/sqlts/SqlTimeseriesLatestDao.java b/dao/src/main/java/org/thingsboard/server/dao/sqlts/SqlTimeseriesLatestDao.java index 44859c26e7..fd1b180eef 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/sqlts/SqlTimeseriesLatestDao.java +++ b/dao/src/main/java/org/thingsboard/server/dao/sqlts/SqlTimeseriesLatestDao.java @@ -24,6 +24,7 @@ import jakarta.annotation.PreDestroy; import lombok.extern.slf4j.Slf4j; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Value; +import org.springframework.data.domain.Page; import org.springframework.stereotype.Component; import org.thingsboard.server.common.data.id.DeviceProfileId; import org.thingsboard.server.common.data.id.EntityId; @@ -37,6 +38,8 @@ import org.thingsboard.server.common.data.kv.ReadTsKvQueryResult; import org.thingsboard.server.common.data.kv.StringDataEntry; import org.thingsboard.server.common.data.kv.TsKvEntry; import org.thingsboard.server.common.data.kv.TsKvLatestRemovingResult; +import org.thingsboard.server.common.data.page.PageData; +import org.thingsboard.server.common.data.page.PageLink; import org.thingsboard.server.common.stats.StatsFactory; import org.thingsboard.server.dao.DaoUtil; import org.thingsboard.server.dao.dictionary.KeyDictionaryDao; @@ -185,6 +188,11 @@ public class SqlTimeseriesLatestDao extends BaseAbstractSqlTimeseriesDao impleme return tsKvLatestRepository.findAllKeysByEntityIds(entityIds.stream().map(EntityId::getId).collect(Collectors.toList())); } + @Override + public PageData findAllLatest(PageLink pageLink) { + return DaoUtil.pageToPageData(tsKvLatestRepository.findAll(DaoUtil.toPageable(pageLink, "entityId", "key"))); + } + private ListenableFuture getNewLatestEntryFuture(TenantId tenantId, EntityId entityId, DeleteTsKvQuery query) { ListenableFuture> future = findNewLatestEntryFuture(tenantId, entityId, query); return Futures.transformAsync(future, entryList -> { diff --git a/dao/src/main/java/org/thingsboard/server/dao/sqlts/dictionary/JpaKeyDictionaryDao.java b/dao/src/main/java/org/thingsboard/server/dao/sqlts/dictionary/JpaKeyDictionaryDao.java index b01d1c4ea0..7194ae957c 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/sqlts/dictionary/JpaKeyDictionaryDao.java +++ b/dao/src/main/java/org/thingsboard/server/dao/sqlts/dictionary/JpaKeyDictionaryDao.java @@ -22,6 +22,9 @@ import org.springframework.dao.DataIntegrityViolationException; import org.springframework.stereotype.Component; import org.springframework.transaction.annotation.Propagation; import org.springframework.transaction.annotation.Transactional; +import org.thingsboard.server.common.data.page.PageData; +import org.thingsboard.server.common.data.page.PageLink; +import org.thingsboard.server.dao.DaoUtil; import org.thingsboard.server.dao.dictionary.KeyDictionaryDao; import org.thingsboard.server.dao.model.sqlts.dictionary.KeyDictionaryCompositeKey; import org.thingsboard.server.dao.model.sqlts.dictionary.KeyDictionaryEntry; @@ -92,4 +95,9 @@ public class JpaKeyDictionaryDao extends JpaAbstractDaoListeningExecutorService return byKeyId.map(KeyDictionaryEntry::getKey).orElse(null); } + @Override + public PageData findAll(PageLink pageLink) { + return DaoUtil.pageToPageData(keyDictionaryRepository.findAll(DaoUtil.toPageable(pageLink))); + } + } diff --git a/dao/src/main/java/org/thingsboard/server/dao/sqlts/dictionary/KeyDictionaryRepository.java b/dao/src/main/java/org/thingsboard/server/dao/sqlts/dictionary/KeyDictionaryRepository.java index 17e24ea5e5..0667f4315e 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/sqlts/dictionary/KeyDictionaryRepository.java +++ b/dao/src/main/java/org/thingsboard/server/dao/sqlts/dictionary/KeyDictionaryRepository.java @@ -15,7 +15,10 @@ */ package org.thingsboard.server.dao.sqlts.dictionary; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.Pageable; import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.Query; import org.thingsboard.server.dao.model.sqlts.dictionary.KeyDictionaryCompositeKey; import org.thingsboard.server.dao.model.sqlts.dictionary.KeyDictionaryEntry; @@ -25,5 +28,7 @@ public interface KeyDictionaryRepository extends JpaRepository findByKeyId(int keyId); + @Query("SELECT e FROM KeyDictionaryEntry e ORDER BY e.keyId ASC") + Page findAll(Pageable pageable); } \ No newline at end of file diff --git a/dao/src/main/java/org/thingsboard/server/dao/sqlts/latest/TsKvLatestRepository.java b/dao/src/main/java/org/thingsboard/server/dao/sqlts/latest/TsKvLatestRepository.java index 38bfde0acf..833ffd185e 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/sqlts/latest/TsKvLatestRepository.java +++ b/dao/src/main/java/org/thingsboard/server/dao/sqlts/latest/TsKvLatestRepository.java @@ -15,6 +15,8 @@ */ package org.thingsboard.server.dao.sqlts.latest; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.Pageable; import org.springframework.data.jpa.repository.JpaRepository; import org.springframework.data.jpa.repository.Query; import org.springframework.data.repository.query.Param; @@ -41,4 +43,7 @@ public interface TsKvLatestRepository extends JpaRepository findAllKeysByEntityIds(@Param("entityIds") List entityIds); + @Query("SELECT e FROM TsKvLatestEntity e ORDER BY e.entityId ASC, e.key ASC") + Page findAll(Pageable pageable); + } diff --git a/dao/src/main/java/org/thingsboard/server/dao/tenant/TenantDao.java b/dao/src/main/java/org/thingsboard/server/dao/tenant/TenantDao.java index 5fb5ff28c4..40ec208f08 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/tenant/TenantDao.java +++ b/dao/src/main/java/org/thingsboard/server/dao/tenant/TenantDao.java @@ -37,10 +37,10 @@ public interface TenantDao extends Dao { * @return saved tenant object */ Tenant save(TenantId tenantId, Tenant tenant); - + /** * Find tenants by page link. - * + * * @param pageLink the page link * @return the list of tenant objects */ @@ -51,4 +51,5 @@ public interface TenantDao extends Dao { PageData findTenantsIds(PageLink pageLink); List findTenantIdsByTenantProfileId(TenantProfileId tenantProfileId); + } diff --git a/dao/src/main/java/org/thingsboard/server/dao/timeseries/BaseTimeseriesService.java b/dao/src/main/java/org/thingsboard/server/dao/timeseries/BaseTimeseriesService.java index 756b73d88b..cb731a782f 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/timeseries/BaseTimeseriesService.java +++ b/dao/src/main/java/org/thingsboard/server/dao/timeseries/BaseTimeseriesService.java @@ -26,6 +26,8 @@ import org.springframework.beans.factory.annotation.Value; import org.springframework.stereotype.Service; import org.thingsboard.server.common.data.EntityType; import org.thingsboard.server.common.data.EntityView; +import org.thingsboard.server.common.data.ObjectType; +import org.thingsboard.server.common.data.edqs.LatestTsKv; import org.thingsboard.server.common.data.id.DeviceProfileId; import org.thingsboard.server.common.data.id.EntityId; import org.thingsboard.server.common.data.id.EntityViewId; @@ -38,6 +40,7 @@ import org.thingsboard.server.common.data.kv.ReadTsKvQuery; import org.thingsboard.server.common.data.kv.ReadTsKvQueryResult; import org.thingsboard.server.common.data.kv.TsKvEntry; import org.thingsboard.server.common.data.kv.TsKvLatestRemovingResult; +import org.thingsboard.server.common.msg.edqs.EdqsService; import org.thingsboard.server.dao.entityview.EntityViewService; import org.thingsboard.server.dao.exception.IncorrectParameterException; import org.thingsboard.server.dao.service.Validator; @@ -54,7 +57,6 @@ import static org.thingsboard.server.common.data.StringUtils.isBlank; /** * @author Andrew Shvayka */ -@SuppressWarnings("UnstableApiUsage") @Service @Slf4j public class BaseTimeseriesService implements TimeseriesService { @@ -89,6 +91,9 @@ public class BaseTimeseriesService implements TimeseriesService { @Autowired private EntityViewService entityViewService; + @Autowired + private EdqsService edqsService; + @Override public ListenableFuture> findAllByQueries(TenantId tenantId, EntityId entityId, List queries) { validate(entityId); @@ -190,14 +195,21 @@ public class BaseTimeseriesService implements TimeseriesService { public ListenableFuture> saveLatest(TenantId tenantId, EntityId entityId, List tsKvEntries) { List> futures = new ArrayList<>(tsKvEntries.size()); for (TsKvEntry tsKvEntry : tsKvEntries) { - futures.add(timeseriesLatestDao.saveLatest(tenantId, entityId, tsKvEntry)); + futures.add(doSaveLatest(tenantId, entityId, tsKvEntry)); } return Futures.allAsList(futures); } private void saveAndRegisterFutures(TenantId tenantId, List> futures, EntityId entityId, TsKvEntry tsKvEntry, long ttl) { doSaveAndRegisterFuturesFor(tenantId, futures, entityId, tsKvEntry, ttl); - futures.add(Futures.transform(timeseriesLatestDao.saveLatest(tenantId, entityId, tsKvEntry), v -> 0, MoreExecutors.directExecutor())); + futures.add(Futures.transform(doSaveLatest(tenantId, entityId, tsKvEntry), v -> 0, MoreExecutors.directExecutor())); + } + + private ListenableFuture doSaveLatest(TenantId tenantId, EntityId entityId, TsKvEntry tsKvEntry) { + return Futures.transform(timeseriesLatestDao.saveLatest(tenantId, entityId, tsKvEntry), version -> { + edqsService.onUpdate(tenantId, ObjectType.LATEST_TS_KV, new LatestTsKv(entityId, tsKvEntry, version)); + return version; + }, MoreExecutors.directExecutor()); } private void saveWithoutLatestAndRegisterFutures(TenantId tenantId, List> futures, EntityId entityId, TsKvEntry tsKvEntry, long ttl) { @@ -248,7 +260,7 @@ public class BaseTimeseriesService implements TimeseriesService { List> futures = new ArrayList<>(keys.size()); for (String key : keys) { DeleteTsKvQuery query = new BaseDeleteTsKvQuery(key, 0, System.currentTimeMillis(), false); - futures.add(timeseriesLatestDao.removeLatest(tenantId, entityId, query)); + futures.add(doRemove(tenantId, entityId, query)); } return Futures.allAsList(futures); } @@ -269,10 +281,20 @@ public class BaseTimeseriesService implements TimeseriesService { private void deleteAndRegisterFutures(TenantId tenantId, List> futures, EntityId entityId, DeleteTsKvQuery query) { futures.add(Futures.transform(timeseriesDao.remove(tenantId, entityId, query), v -> null, MoreExecutors.directExecutor())); if (query.getDeleteLatest()) { - futures.add(timeseriesLatestDao.removeLatest(tenantId, entityId, query)); + futures.add(doRemove(tenantId, entityId, query)); } } + private ListenableFuture doRemove(TenantId tenantId, EntityId entityId, DeleteTsKvQuery query) { + return Futures.transform(timeseriesLatestDao.removeLatest(tenantId, entityId, query), result -> { + if (result.isRemoved()) { + Long version = result.getVersion(); + edqsService.onDelete(tenantId, ObjectType.LATEST_TS_KV, new LatestTsKv(entityId, query.getKey(), version)); + } + return result; + }, MoreExecutors.directExecutor()); + } + private static void validate(EntityId entityId) { Validator.validateEntityId(entityId, id -> "Incorrect entityId " + id); } @@ -302,4 +324,5 @@ public class BaseTimeseriesService implements TimeseriesService { throw new IncorrectParameterException("Incorrect DeleteTsKvQuery. Key can't be empty"); } } + } diff --git a/dao/src/main/java/org/thingsboard/server/dao/timeseries/CassandraBaseTimeseriesLatestDao.java b/dao/src/main/java/org/thingsboard/server/dao/timeseries/CassandraBaseTimeseriesLatestDao.java index 01c91d4801..99bf57a1db 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/timeseries/CassandraBaseTimeseriesLatestDao.java +++ b/dao/src/main/java/org/thingsboard/server/dao/timeseries/CassandraBaseTimeseriesLatestDao.java @@ -36,13 +36,17 @@ import org.thingsboard.server.common.data.kv.ReadTsKvQuery; import org.thingsboard.server.common.data.kv.ReadTsKvQueryResult; import org.thingsboard.server.common.data.kv.TsKvEntry; import org.thingsboard.server.common.data.kv.TsKvLatestRemovingResult; +import org.thingsboard.server.common.data.page.PageData; +import org.thingsboard.server.common.data.page.PageLink; import org.thingsboard.server.dao.model.ModelConstants; +import org.thingsboard.server.dao.model.sqlts.latest.TsKvLatestEntity; import org.thingsboard.server.dao.nosql.TbResultSet; import org.thingsboard.server.dao.sqlts.AggregationTimeseriesDao; import org.thingsboard.server.dao.util.NoSqlTsLatestDao; import java.util.Collections; import java.util.List; +import java.util.Map; import java.util.Optional; import static com.datastax.oss.driver.api.querybuilder.QueryBuilder.literal; @@ -99,6 +103,11 @@ public class CassandraBaseTimeseriesLatestDao extends AbstractCassandraBaseTimes return Collections.emptyList(); } + @Override + public PageData findAllLatest(PageLink pageLink) { + return null; + } + @Override public ListenableFuture saveLatest(TenantId tenantId, EntityId entityId, TsKvEntry tsKvEntry) { BoundStatementBuilder stmtBuilder = new BoundStatementBuilder(getLatestStmt().bind()); diff --git a/dao/src/main/java/org/thingsboard/server/dao/timeseries/TimeseriesLatestDao.java b/dao/src/main/java/org/thingsboard/server/dao/timeseries/TimeseriesLatestDao.java index 9f62fd033a..1372f2f5d8 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/timeseries/TimeseriesLatestDao.java +++ b/dao/src/main/java/org/thingsboard/server/dao/timeseries/TimeseriesLatestDao.java @@ -22,8 +22,12 @@ import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.common.data.kv.DeleteTsKvQuery; import org.thingsboard.server.common.data.kv.TsKvEntry; import org.thingsboard.server.common.data.kv.TsKvLatestRemovingResult; +import org.thingsboard.server.common.data.page.PageData; +import org.thingsboard.server.common.data.page.PageLink; +import org.thingsboard.server.dao.model.sqlts.latest.TsKvLatestEntity; import java.util.List; +import java.util.Map; import java.util.Optional; public interface TimeseriesLatestDao { @@ -49,4 +53,6 @@ public interface TimeseriesLatestDao { List findAllKeysByDeviceProfileId(TenantId tenantId, DeviceProfileId deviceProfileId); List findAllKeysByEntityIds(TenantId tenantId, List entityIds); + + PageData findAllLatest(PageLink pageLink); } diff --git a/dao/src/main/java/org/thingsboard/server/dao/usagerecord/ApiUsageStateDao.java b/dao/src/main/java/org/thingsboard/server/dao/usagerecord/ApiUsageStateDao.java index ce71b733e5..d0da2a3943 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/usagerecord/ApiUsageStateDao.java +++ b/dao/src/main/java/org/thingsboard/server/dao/usagerecord/ApiUsageStateDao.java @@ -19,10 +19,11 @@ import org.thingsboard.server.common.data.ApiUsageState; import org.thingsboard.server.common.data.id.EntityId; import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.dao.Dao; +import org.thingsboard.server.dao.TenantEntityDao; import java.util.UUID; -public interface ApiUsageStateDao extends Dao { +public interface ApiUsageStateDao extends Dao, TenantEntityDao { /** * Save or update usage record object @@ -50,4 +51,5 @@ public interface ApiUsageStateDao extends Dao { void deleteApiUsageStateByTenantId(TenantId tenantId); void deleteApiUsageStateByEntityId(EntityId entityId); + } diff --git a/dao/src/main/java/org/thingsboard/server/dao/usagerecord/ApiUsageStateServiceImpl.java b/dao/src/main/java/org/thingsboard/server/dao/usagerecord/ApiUsageStateServiceImpl.java index d901ae950e..9512120a3b 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/usagerecord/ApiUsageStateServiceImpl.java +++ b/dao/src/main/java/org/thingsboard/server/dao/usagerecord/ApiUsageStateServiceImpl.java @@ -113,6 +113,14 @@ public class ApiUsageStateServiceImpl extends AbstractEntityService implements A ApiUsageState saved = apiUsageStateDao.save(apiUsageState.getTenantId(), apiUsageState); + eventPublisher.publishEvent(SaveEntityEvent.builder() + .tenantId(saved.getTenantId()) + .entityId(saved.getId()) + .entity(saved) + .created(true) + .broadcastEvent(false) + .build()); + List apiUsageStates = new ArrayList<>(); apiUsageStates.add(new BasicTsKvEntry(saved.getCreatedTime(), new StringDataEntry(ApiFeature.TRANSPORT.getApiStateKey(), ApiUsageStateValue.ENABLED.name()))); diff --git a/dao/src/main/java/org/thingsboard/server/dao/user/UserDao.java b/dao/src/main/java/org/thingsboard/server/dao/user/UserDao.java index 42add1bbe2..78b9455586 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/user/UserDao.java +++ b/dao/src/main/java/org/thingsboard/server/dao/user/UserDao.java @@ -28,7 +28,7 @@ import org.thingsboard.server.dao.TenantEntityDao; import java.util.List; import java.util.UUID; -public interface UserDao extends Dao, TenantEntityDao { +public interface UserDao extends Dao, TenantEntityDao { /** * Save or update user object diff --git a/dao/src/test/java/org/thingsboard/server/dao/service/AbstractServiceTest.java b/dao/src/test/java/org/thingsboard/server/dao/service/AbstractServiceTest.java index 7d8018cdd8..f8a0b43741 100644 --- a/dao/src/test/java/org/thingsboard/server/dao/service/AbstractServiceTest.java +++ b/dao/src/test/java/org/thingsboard/server/dao/service/AbstractServiceTest.java @@ -77,7 +77,6 @@ import java.util.UUID; import static org.junit.Assert.assertNotNull; - @RunWith(SpringRunner.class) @ContextConfiguration(classes = AbstractServiceTest.class, loader = AnnotationConfigContextLoader.class) @DirtiesContext(classMode = DirtiesContext.ClassMode.AFTER_CLASS) @@ -131,7 +130,7 @@ public abstract class AbstractServiceTest { } public JsonNode readFromResource(String resourceName) throws IOException { - try (InputStream is = this.getClass().getClassLoader().getResourceAsStream(resourceName)){ + try (InputStream is = this.getClass().getClassLoader().getResourceAsStream(resourceName)) { return JacksonUtil.fromBytes(Objects.requireNonNull(is).readAllBytes()); } } diff --git a/dao/src/test/java/org/thingsboard/server/dao/service/EntityDaoRegistryTest.java b/dao/src/test/java/org/thingsboard/server/dao/service/EntityDaoRegistryTest.java index 30110a2741..e06d83682a 100644 --- a/dao/src/test/java/org/thingsboard/server/dao/service/EntityDaoRegistryTest.java +++ b/dao/src/test/java/org/thingsboard/server/dao/service/EntityDaoRegistryTest.java @@ -21,16 +21,31 @@ import org.junit.Test; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.data.jpa.repository.JpaRepository; import org.thingsboard.server.common.data.EntityType; +import org.thingsboard.server.common.data.ObjectType; import org.thingsboard.server.common.data.id.TenantId; +import org.thingsboard.server.common.data.page.PageLink; import org.thingsboard.server.dao.Dao; +import org.thingsboard.server.dao.TenantEntityDao; import org.thingsboard.server.dao.entity.EntityDaoRegistry; +import java.util.EnumSet; import java.util.List; +import java.util.Set; import java.util.UUID; import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.fail; import static org.junit.jupiter.api.Assertions.assertDoesNotThrow; +import static org.thingsboard.server.common.data.ObjectType.ATTRIBUTE_KV; +import static org.thingsboard.server.common.data.ObjectType.AUDIT_LOG; +import static org.thingsboard.server.common.data.ObjectType.EVENT; +import static org.thingsboard.server.common.data.ObjectType.LATEST_TS_KV; +import static org.thingsboard.server.common.data.ObjectType.OAUTH2_CLIENT; +import static org.thingsboard.server.common.data.ObjectType.OAUTH2_DOMAIN; +import static org.thingsboard.server.common.data.ObjectType.OAUTH2_MOBILE; +import static org.thingsboard.server.common.data.ObjectType.RELATION; +import static org.thingsboard.server.common.data.ObjectType.TENANT; +import static org.thingsboard.server.common.data.ObjectType.TENANT_PROFILE; @Slf4j @DaoSqlTest @@ -88,4 +103,20 @@ public class EntityDaoRegistryTest extends AbstractServiceTest { } } + @Test + public void givenAllTenantEntityDaos_whenFindAllByTenantId_thenOk() { + Set ignored = EnumSet.of(TENANT, TENANT_PROFILE, RELATION, EVENT, ATTRIBUTE_KV, LATEST_TS_KV, AUDIT_LOG, + OAUTH2_CLIENT, OAUTH2_DOMAIN, OAUTH2_MOBILE); + for (ObjectType type : ObjectType.values()) { + if (ignored.contains(type)) { + continue; + } + + TenantEntityDao dao = assertDoesNotThrow(() -> entityDaoRegistry.getTenantEntityDao(type)); + assertDoesNotThrow(() -> { + dao.findAllByTenantId(tenantId, new PageLink(100)); + }); + } + } + } diff --git a/dao/src/test/java/org/thingsboard/server/dao/sql/query/DefaultQueryLogComponentTest.java b/dao/src/test/java/org/thingsboard/server/dao/sql/query/DefaultQueryLogComponentTest.java index 2a0210f8c8..b880871bd6 100644 --- a/dao/src/test/java/org/thingsboard/server/dao/sql/query/DefaultQueryLogComponentTest.java +++ b/dao/src/test/java/org/thingsboard/server/dao/sql/query/DefaultQueryLogComponentTest.java @@ -29,6 +29,8 @@ import org.springframework.test.context.junit.jupiter.SpringExtension; import org.springframework.test.context.junit4.SpringRunner; import org.thingsboard.server.common.data.EntityType; import org.thingsboard.server.common.data.id.TenantId; +import org.thingsboard.server.common.data.permission.QueryContext; +import org.thingsboard.server.common.data.permission.QueryContext; import java.util.List; import java.util.UUID; @@ -48,7 +50,7 @@ import static org.mockito.Mockito.times; public class DefaultQueryLogComponentTest { private TenantId tenantId; - private QueryContext ctx; + private SqlQueryContext ctx; @SpyBean private DefaultQueryLogComponent queryLog; @@ -56,7 +58,7 @@ public class DefaultQueryLogComponentTest { @Before public void setUp() { tenantId = new TenantId(UUID.fromString("97275c1c-9cf2-4d25-a68d-933031158f84")); - ctx = new QueryContext(new QuerySecurityContext(tenantId, null, EntityType.ALARM)); + ctx = new SqlQueryContext(new QueryContext(tenantId, null, EntityType.ALARM)); } @Test diff --git a/edqs/pom.xml b/edqs/pom.xml new file mode 100644 index 0000000000..bf83c75b32 --- /dev/null +++ b/edqs/pom.xml @@ -0,0 +1,260 @@ + + + 4.0.0 + + org.thingsboard + 4.0.0PE-SNAPSHOT + thingsboard + + edqs + jar + + ThingsBoard Entity Data Query Service Application + https://thingsboard.io + ThingsBoard Professional Edition: IoT Platform - Device management, data collection, processing and visualization + + + + UTF-8 + ${basedir}/.. + java + false + process-resources + package + edqs + ${project.build.directory}/windows + true + ThingsBoard Entity Data Query Service + org.thingsboard.server.edqs.ThingsboardEdqsApplication + + + + + org.thingsboard.common + edqs + + + org.slf4j + slf4j-api + + + org.slf4j + log4j-over-slf4j + + + ch.qos.logback + logback-core + + + ch.qos.logback + logback-classic + + + org.apache.curator + curator-recipes + + + com.google.protobuf + protobuf-java + + + io.grpc + grpc-protobuf + + + io.grpc + grpc-stub + + + + com.sun.winsw + winsw + bin + exe + provided + + + org.thingsboard + tools + test + + + org.springframework.security + spring-security-test + test + + + org.springframework.boot + spring-boot-starter-test + test + + + org.junit.vintage + junit-vintage-engine + test + + + org.awaitility + awaitility + test + + + org.dbunit + dbunit + test + + + com.github.springtestdbunit + spring-test-dbunit + test + + + org.testcontainers + cassandra + test + + + org.testcontainers + postgresql + test + + + org.testcontainers + jdbc + test + + + org.java-websocket + Java-WebSocket + test + + + org.eclipse.milo + sdk-server + test + + + org.assertj + assertj-core + test + + + + + ${pkg.name}-${project.version} + + + ${project.basedir}/src/main/resources + true + + edqs.yml + + + + ${project.basedir}/src/main/resources + false + + edqs.yml + + + + + + org.apache.maven.plugins + maven-compiler-plugin + + + org.apache.maven.plugins + maven-surefire-plugin + + + thingsboard + + + **/nosql/*Test.java + + + **/*Test.java + **/*TestSuite.java + + + + + org.apache.maven.plugins + maven-resources-plugin + + + org.apache.maven.plugins + maven-dependency-plugin + + + copy-winsw-service + package + + + + + org.apache.maven.plugins + maven-jar-plugin + + + org.springframework.boot + spring-boot-maven-plugin + + + org.thingsboard + gradle-maven-plugin + + + org.apache.maven.plugins + maven-assembly-plugin + + + org.apache.maven.plugins + maven-install-plugin + + + org.xolstice.maven.plugins + protobuf-maven-plugin + + + org.codehaus.mojo + build-helper-maven-plugin + + + org.apache.maven.plugins + maven-deploy-plugin + + true + + + + + + + jenkins + Jenkins Repository + https://repo.jenkins-ci.org/releases + + false + + + + diff --git a/edqs/src/main/java/org/thingsboard/server/edqs/DummyQueueRoutingInfoService.java b/edqs/src/main/java/org/thingsboard/server/edqs/DummyQueueRoutingInfoService.java new file mode 100644 index 0000000000..ea8af83310 --- /dev/null +++ b/edqs/src/main/java/org/thingsboard/server/edqs/DummyQueueRoutingInfoService.java @@ -0,0 +1,33 @@ +/** + * Copyright © 2016-2024 ThingsBoard, Inc. + * + * 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.edqs; + +import org.springframework.stereotype.Service; +import org.thingsboard.server.queue.discovery.QueueRoutingInfo; +import org.thingsboard.server.queue.discovery.QueueRoutingInfoService; + +import java.util.Collections; +import java.util.List; + +@Service +public class DummyQueueRoutingInfoService implements QueueRoutingInfoService { + + @Override + public List getAllQueuesRoutingInfo() { + return Collections.emptyList(); + } + +} diff --git a/edqs/src/main/java/org/thingsboard/server/edqs/DummyTenantRoutingInfoService.java b/edqs/src/main/java/org/thingsboard/server/edqs/DummyTenantRoutingInfoService.java new file mode 100644 index 0000000000..5037652e5c --- /dev/null +++ b/edqs/src/main/java/org/thingsboard/server/edqs/DummyTenantRoutingInfoService.java @@ -0,0 +1,30 @@ +/** + * Copyright © 2016-2024 ThingsBoard, Inc. + * + * 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.edqs; + +import org.springframework.stereotype.Service; +import org.thingsboard.server.common.data.id.TenantId; +import org.thingsboard.server.queue.discovery.TenantRoutingInfo; +import org.thingsboard.server.queue.discovery.TenantRoutingInfoService; + +@Service +public class DummyTenantRoutingInfoService implements TenantRoutingInfoService { + @Override + public TenantRoutingInfo getRoutingInfo(TenantId tenantId) { + return null; + } + +} diff --git a/edqs/src/main/java/org/thingsboard/server/edqs/ThingsboardEdqsApplication.java b/edqs/src/main/java/org/thingsboard/server/edqs/ThingsboardEdqsApplication.java new file mode 100644 index 0000000000..1157f74de1 --- /dev/null +++ b/edqs/src/main/java/org/thingsboard/server/edqs/ThingsboardEdqsApplication.java @@ -0,0 +1,119 @@ +/** + * Copyright © 2016-2024 ThingsBoard, Inc. + * + * 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.edqs; + +import lombok.extern.slf4j.Slf4j; +import org.springframework.boot.ApplicationRunner; +import org.springframework.boot.SpringApplication; +import org.springframework.boot.SpringBootConfiguration; +import org.springframework.context.annotation.ComponentScan; +import org.springframework.scheduling.annotation.EnableAsync; +import org.springframework.scheduling.annotation.EnableScheduling; +import org.thingsboard.server.edqs.repo.EdqRepository; + +import java.util.Arrays; + +@SpringBootConfiguration +@EnableAsync +@EnableScheduling +@ComponentScan({"org.thingsboard.server.edqs", "org.thingsboard.server.queue.edqs", "org.thingsboard.server.queue.discovery", "org.thingsboard.server.queue.kafka", + "org.thingsboard.server.queue.settings", "org.thingsboard.server.queue.environment"}) +@Slf4j +public class ThingsboardEdqsApplication { + + private static final String SPRING_CONFIG_NAME_KEY = "--spring.config.name"; + private static final String DEFAULT_SPRING_CONFIG_PARAM = SPRING_CONFIG_NAME_KEY + "=" + "edqs"; + + public static void main(String[] args) { + SpringApplication.run(ThingsboardEdqsApplication.class, updateArguments(args)); + } + + // @Bean + public ApplicationRunner runner(CSVLoader loader, EdqRepository edqRepository) { + return args -> { +// long startTs = System.currentTimeMillis(); +// var loader = new TenantRepoLoader(new TenantRepo(TenantId.fromUUID(UUID.fromString("2a209df0-c7ff-11ea-a3e0-f321b0429d60")))); +// loader.load(); +// log.info("Loaded all in {} ms", System.currentTimeMillis() - startTs); + + + +// log.info("Compressed {} strings/json, Before: {}, After: {}", +// CompressedStringDataPoint.cnt.get(), +// CompressedStringDataPoint.uncompressedLength.get(), +// CompressedStringDataPoint.compressedLength.get()); +// +// log.info("Deduplicated {} short and {} long strings", +// TbStringPool.size(), TbBytePool.size()); +// +// var tenantId = TenantId.fromUUID(UUID.fromString("2a209df0-c7ff-11ea-a3e0-f321b0429d60")); +// var customerId = new CustomerId(UUID.fromString("fcbf2f50-d0d9-11ea-bea3-177755191a6e")); +// System.gc(); +// +// while (true) { +// EntityTypeFilter filter = new EntityTypeFilter(); +// filter.setEntityType(EntityType.DEVICE); +// var pageLink = new EntityDataPageLink(20, 0, null, new EntityDataSortOrder(new EntityKey(EntityKeyType.TIME_SERIES, "state"), EntityDataSortOrder.Direction.DESC), false); +// +// var entityFields = Arrays.asList(new EntityKey(EntityKeyType.ENTITY_FIELD, "name"), new EntityKey(EntityKeyType.ENTITY_FIELD, "createdTime")); +// var latestValues = Arrays.asList(new EntityKey(EntityKeyType.TIME_SERIES, "state")); +// KeyFilter nameFilter = new KeyFilter(); +// nameFilter.setKey(new EntityKey(EntityKeyType.ENTITY_FIELD, "name")); +// var predicate = new StringFilterPredicate(); +// predicate.setIgnoreCase(false); +// predicate.setOperation(StringFilterPredicate.StringOperation.CONTAINS); +// predicate.setValue(new FilterPredicateValue<>("LoRa-")); +// nameFilter.setPredicate(predicate); +// nameFilter.setValueType(EntityKeyValueType.STRING); +// +// EntityDataQuery edq = new EntityDataQuery(filter, pageLink, entityFields, latestValues, Arrays.asList(nameFilter)); +// var result = edqRepository.findEntityDataByQuery(tenantId, customerId, RepositoryUtils.ALL_READ_PERMISSIONS, edq, false); +// log.info("Device count: {}", result.getTotalElements()); +// log.info("First: {}", result.getData().get(0).getEntityId()); +// log.info("Last: {}", result.getData().get(19).getEntityId()); +// +// pageLink.setSortOrder(new EntityDataSortOrder(new EntityKey(EntityKeyType.TIME_SERIES, "state"), EntityDataSortOrder.Direction.ASC)); +// result = edqRepository.findEntityDataByQuery(tenantId, customerId, RepositoryUtils.ALL_READ_PERMISSIONS, edq, false); +// log.info("Device count: {}", result.getTotalElements()); +// log.info("First: {}", result.getData().get(0).getEntityId()); +// log.info("Last: {}", result.getData().get(19).getEntityId()); +// +// result.getData().forEach(data -> { +// System.err.println(data.getEntityId() + ":"); +// data.getLatest().forEach((type, values) -> { +// System.err.println(type); +// values.forEach((key, tsValue) -> { +// System.err.println(key + " = " + tsValue.getValue()); +// }); +// }); +// System.err.println(); +// }); +// Thread.sleep(5000); +// } + }; + } + + private static String[] updateArguments(String[] args) { + if (Arrays.stream(args).noneMatch(arg -> arg.startsWith(SPRING_CONFIG_NAME_KEY))) { + String[] modifiedArgs = new String[args.length + 1]; + System.arraycopy(args, 0, modifiedArgs, 0, args.length); + modifiedArgs[args.length] = DEFAULT_SPRING_CONFIG_PARAM; + return modifiedArgs; + } + return args; + } + +} diff --git a/edqs/src/main/resources/edqs.yml b/edqs/src/main/resources/edqs.yml new file mode 100644 index 0000000000..f5c83178c3 --- /dev/null +++ b/edqs/src/main/resources/edqs.yml @@ -0,0 +1,364 @@ +# +# Copyright © 2016-2024 ThingsBoard, Inc. +# +# 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. +# + +# Application info parameters +app: + # Application version + version: "@project.version@" + +# Zookeeper connection parameters +zk: + # Enable/disable zookeeper discovery service. + enabled: "${ZOOKEEPER_ENABLED:true}" + # Zookeeper connect string + url: "${ZOOKEEPER_URL:localhost:2181}" + # Zookeeper retry interval in milliseconds + retry_interval_ms: "${ZOOKEEPER_RETRY_INTERVAL_MS:3000}" + # Zookeeper connection timeout in milliseconds + connection_timeout_ms: "${ZOOKEEPER_CONNECTION_TIMEOUT_MS:3000}" + # Zookeeper session timeout in milliseconds + session_timeout_ms: "${ZOOKEEPER_SESSION_TIMEOUT_MS:3000}" + # Name of the directory in zookeeper 'filesystem' + zk_dir: "${ZOOKEEPER_NODES_DIR:/thingsboard}" + # The recalculate_delay property is recommended in a microservices architecture setup for rule-engine services. + # This property provides a pause to ensure that when a rule-engine service is restarted, other nodes don't immediately attempt to recalculate their partitions. + # The delay is recommended because the initialization of rule chain actors is time-consuming. Avoiding unnecessary recalculations during a restart can enhance system performance and stability. + recalculate_delay: "${ZOOKEEPER_RECALCULATE_DELAY_MS:0}" + +# SQL DAO Configuration parameters +spring: + main: + web-application-type: "none" + +# Queue configuration parameters +queue: + type: "${TB_QUEUE_TYPE:kafka}" # kafka (Apache Kafka) + prefix: "${TB_QUEUE_PREFIX:}" # Global queue prefix. If specified, prefix is added before default topic name: 'prefix.default_topic_name'. Prefix is applied to all topics (and consumer groups for kafka). + in_memory: + stats: + # For debug level + print-interval-ms: "${TB_QUEUE_IN_MEMORY_STATS_PRINT_INTERVAL_MS:60000}" + edqs: + enabled: "${TB_EDQS_ENABLED:true}" + mode: "${TB_EDQS_MODE:local}" + partitions: "${TB_EDQS_PARTITIONS:12}" + requests_topic: "${TB_EDQS_REQUESTS_TOPIC:edqs.requests}" + responses_topic: "${TB_EDQS_RESPONSES_TOPIC:edqs.responses}" + poll_interval: "${TB_EDQS_POLL_INTERVAL_MS:125}" + max_pending_requests: "${TB_EDQS_MAX_PENDING_REQUESTS:10000}" + max_request_timeout: "${TB_EDQS_MAX_REQUEST_TIMEOUT:10000}" + partitioning_strategy: "${TB_EDQS_PARTITIONING_STRATEGY:tenant}" # tenant or none. For 'none', each instance handles all partitions and duplicates all the data + kafka: + # Kafka Bootstrap nodes in "host:port" format + bootstrap.servers: "${TB_KAFKA_SERVERS:localhost:9092}" + ssl: + # Enable/Disable SSL Kafka communication + enabled: "${TB_KAFKA_SSL_ENABLED:false}" + # The location of the trust store file + truststore.location: "${TB_KAFKA_SSL_TRUSTSTORE_LOCATION:}" + # The password of trust store file if specified + truststore.password: "${TB_KAFKA_SSL_TRUSTSTORE_PASSWORD:}" + # The location of the key store file. This is optional for the client and can be used for two-way authentication for the client + keystore.location: "${TB_KAFKA_SSL_KEYSTORE_LOCATION:}" + # The store password for the key store file. This is optional for the client and only needed if ‘ssl.keystore.location’ is configured. Key store password is not supported for PEM format + keystore.password: "${TB_KAFKA_SSL_KEYSTORE_PASSWORD:}" + # The password of the private key in the key store file or the PEM key specified in ‘keystore.key’ + key.password: "${TB_KAFKA_SSL_KEY_PASSWORD:}" + # The number of acknowledgments the producer requires the leader to have received before considering a request complete. This controls the durability of records that are sent. The following settings are allowed:0, 1 and all + acks: "${TB_KAFKA_ACKS:all}" + # Number of retries. Resend any record whose send fails with a potentially transient error + retries: "${TB_KAFKA_RETRIES:1}" + # The compression type for all data generated by the producer. The default is none (i.e. no compression). Valid values none or gzip + compression.type: "${TB_KAFKA_COMPRESSION_TYPE:none}" # none or gzip + # Default batch size. This setting gives the upper bound of the batch size to be sent + batch.size: "${TB_KAFKA_BATCH_SIZE:16384}" + # This variable creates a small amount of artificial delay—that is, rather than immediately sending out a record + linger.ms: "${TB_KAFKA_LINGER_MS:1}" + # The maximum size of a request in bytes. This setting will limit the number of record batches the producer will send in a single request to avoid sending huge requests + max.request.size: "${TB_KAFKA_MAX_REQUEST_SIZE:1048576}" + # The maximum number of unacknowledged requests the client will send on a single connection before blocking + max.in.flight.requests.per.connection: "${TB_KAFKA_MAX_IN_FLIGHT_REQUESTS_PER_CONNECTION:5}" + # The total bytes of memory the producer can use to buffer records waiting to be sent to the server + buffer.memory: "${TB_BUFFER_MEMORY:33554432}" + # The multiple copies of data over the multiple brokers of Kafka + replication_factor: "${TB_QUEUE_KAFKA_REPLICATION_FACTOR:1}" + # The maximum delay between invocations of poll() method when using consumer group management. This places an upper bound on the amount of time that the consumer can be idle before fetching more records + max_poll_interval_ms: "${TB_QUEUE_KAFKA_MAX_POLL_INTERVAL_MS:300000}" + # The maximum number of records returned in a single call of poll() method + max_poll_records: "${TB_QUEUE_KAFKA_MAX_POLL_RECORDS:8192}" + # The maximum amount of data per-partition the server will return. Records are fetched in batches by the consumer + max_partition_fetch_bytes: "${TB_QUEUE_KAFKA_MAX_PARTITION_FETCH_BYTES:16777216}" + # The maximum amount of data the server will return. Records are fetched in batches by the consumer + fetch_max_bytes: "${TB_QUEUE_KAFKA_FETCH_MAX_BYTES:134217728}" + request.timeout.ms: "${TB_QUEUE_KAFKA_REQUEST_TIMEOUT_MS:30000}" # (30 seconds) # refer to https://docs.confluent.io/platform/current/installation/configuration/producer-configs.html#producerconfigs_request.timeout.ms + session.timeout.ms: "${TB_QUEUE_KAFKA_SESSION_TIMEOUT_MS:10000}" # (10 seconds) # refer to https://docs.confluent.io/platform/current/installation/configuration/consumer-configs.html#consumerconfigs_session.timeout.ms + auto_offset_reset: "${TB_QUEUE_KAFKA_AUTO_OFFSET_RESET:earliest}" # earliest, latest or none + # Enable/Disable using of Confluent Cloud + use_confluent_cloud: "${TB_QUEUE_KAFKA_USE_CONFLUENT_CLOUD:false}" + confluent: + # The endpoint identification algorithm used by clients to validate server hostname. The default value is https + ssl.algorithm: "${TB_QUEUE_KAFKA_CONFLUENT_SSL_ALGORITHM:https}" + # The mechanism used to authenticate Schema Registry requests. SASL/PLAIN should only be used with TLS/SSL as a transport layer to ensure that clear passwords are not transmitted on the wire without encryption + sasl.mechanism: "${TB_QUEUE_KAFKA_CONFLUENT_SASL_MECHANISM:PLAIN}" + # Using JAAS Configuration for specifying multiple SASL mechanisms on a broker + sasl.config: "${TB_QUEUE_KAFKA_CONFLUENT_SASL_JAAS_CONFIG:org.apache.kafka.common.security.plain.PlainLoginModule required username=\"CLUSTER_API_KEY\" password=\"CLUSTER_API_SECRET\";}" + # Protocol used to communicate with brokers. Valid values are: PLAINTEXT, SSL, SASL_PLAINTEXT, SASL_SSL + security.protocol: "${TB_QUEUE_KAFKA_CONFLUENT_SECURITY_PROTOCOL:SASL_SSL}" + # Key-value properties for Kafka consumer per specific topic, e.g. tb_ota_package is a topic name for ota, tb_rule_engine.sq is a topic name for default SequentialByOriginator queue. + # Check TB_QUEUE_CORE_OTA_TOPIC and TB_QUEUE_RE_SQ_TOPIC params + consumer-properties-per-topic: + tb_ota_package: + # Key-value properties for Kafka consumer per specific topic, e.g. tb_ota_package is a topic name for ota, tb_rule_engine.sq is a topic name for default SequentialByOriginator queue. Check TB_QUEUE_CORE_OTA_TOPIC and TB_QUEUE_RE_SQ_TOPIC params + - key: max.poll.records + # Example of specific consumer properties value per topic + value: "${TB_QUEUE_KAFKA_OTA_MAX_POLL_RECORDS:10}" + tb_version_control: + # Example of specific consumer properties value per topic for VC + - key: max.poll.interval.ms + # Example of specific consumer properties value per topic for VC + value: "${TB_QUEUE_KAFKA_VC_MAX_POLL_INTERVAL_MS:600000}" + # tb_rule_engine.sq: + # - key: max.poll.records + # value: "${TB_QUEUE_KAFKA_SQ_MAX_POLL_RECORDS:1024}" + tb_housekeeper: + # Consumer properties for Housekeeper tasks topic + - key: max.poll.records + # Amount of records to be returned in a single poll. For Housekeeper tasks topic, we should consume messages (tasks) one by one + value: "${TB_QUEUE_KAFKA_HOUSEKEEPER_MAX_POLL_RECORDS:1}" + tb_housekeeper.reprocessing: + # Consumer properties for Housekeeper reprocessing topic + - key: max.poll.records + # Amount of records to be returned in a single poll. For Housekeeper reprocessing topic, we should consume messages (tasks) one by one + value: "${TB_QUEUE_KAFKA_HOUSEKEEPER_REPROCESSING_MAX_POLL_RECORDS:1}" + other-inline: "${TB_QUEUE_KAFKA_OTHER_PROPERTIES:}" # In this section you can specify custom parameters (semicolon separated) for Kafka consumer/producer/admin # Example "metrics.recording.level:INFO;metrics.sample.window.ms:30000" + other: # DEPRECATED. In this section, you can specify custom parameters for Kafka consumer/producer and expose the env variables to configure outside + # - key: "request.timeout.ms" # refer to https://docs.confluent.io/platform/current/installation/configuration/producer-configs.html#producerconfigs_request.timeout.ms + # value: "${TB_QUEUE_KAFKA_REQUEST_TIMEOUT_MS:30000}" # (30 seconds) + # - key: "session.timeout.ms" # refer to https://docs.confluent.io/platform/current/installation/configuration/consumer-configs.html#consumerconfigs_session.timeout.ms + # value: "${TB_QUEUE_KAFKA_SESSION_TIMEOUT_MS:10000}" # (10 seconds) + topic-properties: + # Kafka properties for Rule Engine + rule-engine: "${TB_QUEUE_KAFKA_RE_TOPIC_PROPERTIES:retention.ms:604800000;segment.bytes:52428800;retention.bytes:1048576000;partitions:1;min.insync.replicas:1}" + # Kafka properties for Core topics + core: "${TB_QUEUE_KAFKA_CORE_TOPIC_PROPERTIES:retention.ms:604800000;segment.bytes:52428800;retention.bytes:1048576000;partitions:1;min.insync.replicas:1}" + # Kafka properties for Transport Api topics + transport-api: "${TB_QUEUE_KAFKA_TA_TOPIC_PROPERTIES:retention.ms:604800000;segment.bytes:52428800;retention.bytes:1048576000;partitions:10;min.insync.replicas:1}" + # Kafka properties for Notifications topics + notifications: "${TB_QUEUE_KAFKA_NOTIFICATIONS_TOPIC_PROPERTIES:retention.ms:604800000;segment.bytes:52428800;retention.bytes:1048576000;partitions:1;min.insync.replicas:1}" + # Kafka properties for JS Executor topics + js-executor: "${TB_QUEUE_KAFKA_JE_TOPIC_PROPERTIES:retention.ms:604800000;segment.bytes:52428800;retention.bytes:104857600;partitions:100;min.insync.replicas:1}" + # Kafka properties for OTA updates topic + ota-updates: "${TB_QUEUE_KAFKA_OTA_TOPIC_PROPERTIES:retention.ms:604800000;segment.bytes:52428800;retention.bytes:1048576000;partitions:10;min.insync.replicas:1}" + # Kafka properties for Version Control topic + version-control: "${TB_QUEUE_KAFKA_VC_TOPIC_PROPERTIES:retention.ms:604800000;segment.bytes:52428800;retention.bytes:1048576000;partitions:1;min.insync.replicas:1}" + # Kafka properties for Integration Api topics + integration-api: "${TB_QUEUE_KAFKA_INTEGRATION_TOPIC_PROPERTIES:retention.ms:604800000;segment.bytes:52428800;retention.bytes:1048576000;partitions:10;min.insync.replicas:1}" + # Kafka properties for Housekeeper tasks topic + housekeeper: "${TB_QUEUE_KAFKA_HOUSEKEEPER_TOPIC_PROPERTIES:retention.ms:604800000;segment.bytes:52428800;retention.bytes:1048576000;partitions:10;min.insync.replicas:1}" + # Kafka properties for Housekeeper reprocessing topic; retention.ms is set to 90 days; partitions is set to 1 since only one reprocessing service is running at a time + housekeeper-reprocessing: "${TB_QUEUE_KAFKA_HOUSEKEEPER_REPROCESSING_TOPIC_PROPERTIES:retention.ms:7776000000;segment.bytes:52428800;retention.bytes:1048576000;partitions:1;min.insync.replicas:1}" + # Kafka properties for EDQS events topics. Partitions number must be the same as queue.edqs.partitions + edqs-events: "${TB_QUEUE_KAFKA_EDQS_EVENTS_TOPIC_PROPERTIES:retention.ms:604800000;segment.bytes:52428800;retention.bytes:1048576000;partitions:12;min.insync.replicas:1}" + # Kafka properties for EDQS requests topic (default: 3 minutes retention). Partitions number must be the same as queue.edqs.partitions + edqs-requests: "${TB_QUEUE_KAFKA_EDQS_REQUESTS_TOPIC_PROPERTIES:retention.ms:180000;segment.bytes:52428800;retention.bytes:1048576000;partitions:12;min.insync.replicas:1}" + # Kafka properties for EDQS state topic (infinite retention, compaction). Partitions number must be the same as queue.edqs.partitions + edqs-state: "${TB_QUEUE_KAFKA_EDQS_LATEST_EVENTS_TOPIC_PROPERTIES:retention.ms:-1;segment.bytes:52428800;retention.bytes:1048576000;partitions:12;min.insync.replicas:1;cleanup.policy:compact}" + consumer-stats: + # Prints lag between consumer group offset and last messages offset in Kafka topics + enabled: "${TB_QUEUE_KAFKA_CONSUMER_STATS_ENABLED:true}" + # Statistics printing interval for Kafka's consumer-groups stats + print-interval-ms: "${TB_QUEUE_KAFKA_CONSUMER_STATS_MIN_PRINT_INTERVAL_MS:60000}" + # Time to wait for the stats-loading requests to Kafka to finish + kafka-response-timeout-ms: "${TB_QUEUE_KAFKA_CONSUMER_STATS_RESPONSE_TIMEOUT_MS:1000}" + partitions: + hash_function_name: "${TB_QUEUE_PARTITIONS_HASH_FUNCTION_NAME:murmur3_128}" # murmur3_32, murmur3_128 or sha256 + transport_api: + # Topic used to consume api requests from transport microservices + requests_topic: "${TB_QUEUE_TRANSPORT_API_REQUEST_TOPIC:tb_transport.api.requests}" + # Topic used to produce api responses to transport microservices + responses_topic: "${TB_QUEUE_TRANSPORT_API_RESPONSE_TOPIC:tb_transport.api.responses}" + # Maximum pending api requests from transport microservices to be handled by server + max_pending_requests: "${TB_QUEUE_TRANSPORT_MAX_PENDING_REQUESTS:10000}" + # Maximum timeout in milliseconds to handle api request from transport microservice by server + max_requests_timeout: "${TB_QUEUE_TRANSPORT_MAX_REQUEST_TIMEOUT:10000}" + # Amount of threads used to invoke callbacks + max_callback_threads: "${TB_QUEUE_TRANSPORT_MAX_CALLBACK_THREADS:100}" + # Amount of threads used for transport API requests + max_core_handler_threads: "${TB_QUEUE_TRANSPORT_MAX_CORE_HANDLER_THREADS:16}" + # Interval in milliseconds to poll api requests from transport microservices + request_poll_interval: "${TB_QUEUE_TRANSPORT_REQUEST_POLL_INTERVAL_MS:25}" + # Interval in milliseconds to poll api response from transport microservices + response_poll_interval: "${TB_QUEUE_TRANSPORT_RESPONSE_POLL_INTERVAL_MS:25}" + core: + # Default topic name of Kafka, RabbitMQ, etc. queue + topic: "${TB_QUEUE_CORE_TOPIC:tb_core}" + # Interval in milliseconds to poll messages by Core microservices + poll-interval: "${TB_QUEUE_CORE_POLL_INTERVAL_MS:25}" + # Amount of partitions used by Core microservices + partitions: "${TB_QUEUE_CORE_PARTITIONS:10}" + # Timeout for processing a message pack by Core microservices + pack-processing-timeout: "${TB_QUEUE_CORE_PACK_PROCESSING_TIMEOUT_MS:2000}" + # Enable/disable a separate consumer per partition for Core queue + consumer-per-partition: "${TB_QUEUE_CORE_CONSUMER_PER_PARTITION:true}" + ota: + # Default topic name for OTA updates + topic: "${TB_QUEUE_CORE_OTA_TOPIC:tb_ota_package}" + # The interval of processing the OTA updates for devices. Used to avoid any harm to the network due to many parallel OTA updates + pack-interval-ms: "${TB_QUEUE_CORE_OTA_PACK_INTERVAL_MS:60000}" + # The size of OTA updates notifications fetched from the queue. The queue stores pairs of firmware and device ids + pack-size: "${TB_QUEUE_CORE_OTA_PACK_SIZE:100}" + # Stats topic name for queue Kafka, RabbitMQ, etc. + usage-stats-topic: "${TB_QUEUE_US_TOPIC:tb_usage_stats}" + stats: + # Enable/disable statistics for Core microservices + enabled: "${TB_QUEUE_CORE_STATS_ENABLED:true}" + # Statistics printing interval for Core microservices + print-interval-ms: "${TB_QUEUE_CORE_STATS_PRINT_INTERVAL_MS:60000}" + housekeeper: + # Topic name for Housekeeper tasks + topic: "${TB_HOUSEKEEPER_TOPIC:tb_housekeeper}" + # Topic name for Housekeeper tasks to be reprocessed + reprocessing-topic: "${TB_HOUSEKEEPER_REPROCESSING_TOPIC:tb_housekeeper.reprocessing}" + # Poll interval for topics related to Housekeeper + poll-interval-ms: "${TB_HOUSEKEEPER_POLL_INTERVAL_MS:500}" + # Timeout in milliseconds for task processing. Tasks that fail to finish on time will be submitted for reprocessing + task-processing-timeout-ms: "${TB_HOUSEKEEPER_TASK_PROCESSING_TIMEOUT_MS:120000}" + # Comma-separated list of task types that shouldn't be processed. Available task types: + # DELETE_ATTRIBUTES, DELETE_TELEMETRY (both DELETE_LATEST_TS and DELETE_TS_HISTORY will be disabled), + # DELETE_LATEST_TS, DELETE_TS_HISTORY, DELETE_EVENTS, DELETE_ALARMS, UNASSIGN_ALARMS + disabled-task-types: "${TB_HOUSEKEEPER_DISABLED_TASK_TYPES:}" + # Delay in milliseconds between tasks reprocessing + task-reprocessing-delay-ms: "${TB_HOUSEKEEPER_TASK_REPROCESSING_DELAY_MS:3000}" + # Maximum amount of task reprocessing attempts. After exceeding, the task will be dropped + max-reprocessing-attempts: "${TB_HOUSEKEEPER_MAX_REPROCESSING_ATTEMPTS:10}" + stats: + # Enable/disable statistics for Housekeeper + enabled: "${TB_HOUSEKEEPER_STATS_ENABLED:true}" + # Statistics printing interval for Housekeeper + print-interval-ms: "${TB_HOUSEKEEPER_STATS_PRINT_INTERVAL_MS:60000}" + + vc: + # Default topic name for Kafka, RabbitMQ, etc. + topic: "${TB_QUEUE_VC_TOPIC:tb_version_control}" + # Number of partitions to associate with this queue. Used for scaling the number of messages that can be processed in parallel + partitions: "${TB_QUEUE_VC_PARTITIONS:10}" + # Interval in milliseconds between polling of the messages if no new messages arrive + poll-interval: "${TB_QUEUE_VC_INTERVAL_MS:25}" + # Timeout before retrying all failed and timed-out messages from the processing pack + pack-processing-timeout: "${TB_QUEUE_VC_PACK_PROCESSING_TIMEOUT_MS:180000}" + # Timeout for a request to VC-executor (for a request for the version of the entity, for a commit charge, etc.) + request-timeout: "${TB_QUEUE_VC_REQUEST_TIMEOUT:180000}" + # Queue settings for Kafka, RabbitMQ, etc. Limit for single message size + msg-chunk-size: "${TB_QUEUE_VC_MSG_CHUNK_SIZE:250000}" + js: + # JS Eval request topic + request_topic: "${REMOTE_JS_EVAL_REQUEST_TOPIC:js_eval.requests}" + # JS Eval responses topic prefix that is combined with node id + response_topic_prefix: "${REMOTE_JS_EVAL_RESPONSE_TOPIC:js_eval.responses}" + # JS Eval max pending requests + max_pending_requests: "${REMOTE_JS_MAX_PENDING_REQUESTS:10000}" + # JS Eval max request timeout + max_eval_requests_timeout: "${REMOTE_JS_MAX_EVAL_REQUEST_TIMEOUT:60000}" + # JS max request timeout + max_requests_timeout: "${REMOTE_JS_MAX_REQUEST_TIMEOUT:10000}" + # JS execution max request timeout + max_exec_requests_timeout: "${REMOTE_JS_MAX_EXEC_REQUEST_TIMEOUT:2000}" + # JS response poll interval + response_poll_interval: "${REMOTE_JS_RESPONSE_POLL_INTERVAL_MS:25}" + rule-engine: + # Deprecated. It will be removed in the nearest releases + topic: "${TB_QUEUE_RULE_ENGINE_TOPIC:tb_rule_engine}" + # Interval in milliseconds to poll messages by Rule Engine + poll-interval: "${TB_QUEUE_RULE_ENGINE_POLL_INTERVAL_MS:25}" + # Timeout for processing a message pack of Rule Engine + pack-processing-timeout: "${TB_QUEUE_RULE_ENGINE_PACK_PROCESSING_TIMEOUT_MS:2000}" + stats: + # Enable/disable statistics for Rule Engine + enabled: "${TB_QUEUE_RULE_ENGINE_STATS_ENABLED:true}" + # Statistics printing interval for Rule Engine + print-interval-ms: "${TB_QUEUE_RULE_ENGINE_STATS_PRINT_INTERVAL_MS:60000}" + # Max length of the error message that is printed by statistics + max-error-message-length: "${TB_QUEUE_RULE_ENGINE_MAX_ERROR_MESSAGE_LENGTH:4096}" + # After a queue is deleted (or the profile's isolation option was disabled), Rule Engine will continue reading related topics during this period before deleting the actual topics + topic-deletion-delay: "${TB_QUEUE_RULE_ENGINE_TOPIC_DELETION_DELAY_SEC:15}" + # Size of the thread pool that handles such operations as partition changes, config updates, queue deletion + management-thread-pool-size: "${TB_QUEUE_RULE_ENGINE_MGMT_THREAD_POOL_SIZE:12}" + transport: + # For high-priority notifications that require minimum latency and processing time + notifications_topic: "${TB_QUEUE_TRANSPORT_NOTIFICATIONS_TOPIC:tb_transport.notifications}" + # Interval in milliseconds to poll messages + poll_interval: "${TB_QUEUE_TRANSPORT_NOTIFICATIONS_POLL_INTERVAL_MS:25}" + integration: + # Name of hash function used for consistent hash ring in Cluster Mode. See architecture docs for more details. Valid values - murmur3_32, murmur3_128 or sha256 + partitions: "${TB_QUEUE_INTEGRATION_PARTITIONS:3}" + # Default notification topic name used by queue + notifications_topic: "${TB_QUEUE_INTEGRATION_NOTIFICATIONS_TOPIC:tb_ie.notifications}" + # Default downlink topic name used by queue + downlink_topic: "${TB_QUEUE_INTEGRATION_DOWNLINK_TOPIC:tb_ie.downlink}" + # Default uplink topic name used by queue + uplink_topic: "${TB_QUEUE_INTEGRATION_UPLINK_TOPIC:tb_ie.uplink}" + # Interval in milliseconds to poll messages by integrations + poll_interval: "${TB_QUEUE_INTEGRATION_POLL_INTERVAL_MS:25}" + # Timeout for processing a message pack by integrations + pack-processing-timeout: "${TB_QUEUE_INTEGRATION_PACK_PROCESSING_TIMEOUT_MS:10000}" + integration_api: + # Default Integration Api request topic name used by queue + requests_topic: "${TB_QUEUE_INTEGRATION_EXECUTOR_API_REQUEST_TOPIC:tb_ie.api.requests}" + # Default Integration Api response topic name used by queue + responses_topic: "${TB_QUEUE_INTEGRATION_EXECUTOR_API_RESPONSE_TOPIC:tb_ie.api.responses}" + # Maximum pending api requests from integration executor to be handled by server< + max_pending_requests: "${TB_QUEUE_INTEGRATION_EXECUTOR_MAX_PENDING_REQUESTS:10000}" + # Maximum timeout in milliseconds to handle api request from integration executor microservice by server + max_requests_timeout: "${TB_QUEUE_INTEGRATION_EXECUTOR_MAX_REQUEST_TIMEOUT:10000}" + # Amount of threads used to invoke callbacks + max_callback_threads: "${TB_QUEUE_INTEGRATION_EXECUTOR_MAX_CALLBACK_THREADS:10}" + # Interval in milliseconds to poll api requests from integration executor microservices + request_poll_interval: "${TB_QUEUE_INTEGRATION_EXECUTOR_REQUEST_POLL_INTERVAL_MS:25}" + # Interval in milliseconds to poll api response from integration executor microservices + response_poll_interval: "${TB_QUEUE_INTEGRATION_EXECUTOR_RESPONSE_POLL_INTERVAL_MS:25}" + +# General service parameters +service: + type: "${TB_SERVICE_TYPE:edqs}" + # Unique id for this service (autogenerated if empty) + id: "${TB_SERVICE_ID:}" + edqs: + label: "${TB_EDQS_LABEL:}" # services with the same label will share the list of partitions + +# Metrics parameters +metrics: + # Enable/disable actuator metrics. + enabled: "${METRICS_ENABLED:false}" + timer: + # Metrics percentiles returned by actuator for timer metrics. List of double values (divided by ,). + percentiles: "${METRICS_TIMER_PERCENTILES:0.5}" + system_info: + # Persist frequency of system info (CPU, memory usage, etc.) in seconds + persist_frequency: "${METRICS_SYSTEM_INFO_PERSIST_FREQUENCY_SECONDS:60}" + # TTL in days for system info timeseries + ttl: "${METRICS_SYSTEM_INFO_TTL_DAYS:7}" + +# General management parameters +management: + endpoints: + web: + exposure: + # Expose metrics endpoint (use value 'prometheus' to enable prometheus metrics). + include: '${METRICS_ENDPOINTS_EXPOSE:info}' + health: + elasticsearch: + # Enable the org.springframework.boot.actuate.elasticsearch.ElasticsearchRestClientHealthIndicator.doHealthCheck + enabled: "false" diff --git a/edqs/src/main/resources/logback.xml b/edqs/src/main/resources/logback.xml new file mode 100644 index 0000000000..09aa2b1ecb --- /dev/null +++ b/edqs/src/main/resources/logback.xml @@ -0,0 +1,38 @@ + + + + + + + + %d{ISO8601} [%thread] %-5level %logger{36} - %msg%n + + + + + + + + + + + + + + diff --git a/edqs/src/test/java/org/thingsboard/server/edqs/repo/AbstractEDQTest.java b/edqs/src/test/java/org/thingsboard/server/edqs/repo/AbstractEDQTest.java new file mode 100644 index 0000000000..f6f3fcc60e --- /dev/null +++ b/edqs/src/test/java/org/thingsboard/server/edqs/repo/AbstractEDQTest.java @@ -0,0 +1,296 @@ +/** + * Copyright © 2016-2024 ThingsBoard, Inc. + * + * 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.edqs.repo; + +import org.junit.After; +import org.junit.Before; +import org.junit.runner.RunWith; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.autoconfigure.domain.EntityScan; +import org.springframework.context.annotation.ComponentScan; +import org.springframework.context.annotation.Configuration; +import org.springframework.test.context.TestExecutionListeners; +import org.springframework.test.context.TestPropertySource; +import org.springframework.test.context.junit4.SpringRunner; +import org.springframework.test.context.support.DependencyInjectionTestExecutionListener; +import org.springframework.test.context.support.DirtiesContextTestExecutionListener; +import org.thingsboard.common.util.JacksonUtil; +import org.thingsboard.server.common.data.Customer; +import org.thingsboard.server.common.data.Dashboard; +import org.thingsboard.server.common.data.Device; +import org.thingsboard.server.common.data.DeviceProfile; +import org.thingsboard.server.common.data.DeviceProfileType; +import org.thingsboard.server.common.data.EntityType; +import org.thingsboard.server.common.data.EntityView; +import org.thingsboard.server.common.data.asset.Asset; +import org.thingsboard.server.common.data.asset.AssetProfile; +import org.thingsboard.server.common.data.edge.Edge; +import org.thingsboard.server.common.data.edqs.EdqsObject; +import org.thingsboard.server.common.data.edqs.query.QueryResult; +import org.thingsboard.server.common.data.group.EntityGroup; +import org.thingsboard.server.common.data.id.AssetId; +import org.thingsboard.server.common.data.id.AssetProfileId; +import org.thingsboard.server.common.data.id.CustomerId; +import org.thingsboard.server.common.data.id.DashboardId; +import org.thingsboard.server.common.data.id.DeviceId; +import org.thingsboard.server.common.data.id.DeviceProfileId; +import org.thingsboard.server.common.data.id.EdgeId; +import org.thingsboard.server.common.data.id.EntityGroupId; +import org.thingsboard.server.common.data.id.EntityId; +import org.thingsboard.server.common.data.id.EntityIdFactory; +import org.thingsboard.server.common.data.id.EntityViewId; +import org.thingsboard.server.common.data.id.SchedulerEventId; +import org.thingsboard.server.common.data.id.TenantId; +import org.thingsboard.server.common.data.page.PageData; +import org.thingsboard.server.common.data.query.EntityKey; +import org.thingsboard.server.common.data.query.EntityKeyType; +import org.thingsboard.server.common.data.query.EntityKeyValueType; +import org.thingsboard.server.common.data.query.FilterPredicateValue; +import org.thingsboard.server.common.data.query.KeyFilter; +import org.thingsboard.server.common.data.query.StringFilterPredicate; +import org.thingsboard.server.common.data.relation.EntityRelation; +import org.thingsboard.server.common.data.relation.RelationTypeGroup; +import org.thingsboard.server.common.data.scheduler.SchedulerEvent; +import org.thingsboard.server.edqs.processor.EdqsConverter; + +import java.util.Collections; +import java.util.List; +import java.util.UUID; + +@RunWith(SpringRunner.class) +@Configuration +@ComponentScan({"org.thingsboard.server.edqs.repo"}) +@EntityScan("org.thingsboard.server.edqs") +@TestPropertySource(locations = {"classpath:edq-test.properties"}) +@TestExecutionListeners({ + DependencyInjectionTestExecutionListener.class, + DirtiesContextTestExecutionListener.class}) +public abstract class AbstractEDQTest { + + @Autowired + protected InMemoryEdqRepository repository; + + protected final TenantId tenantId = TenantId.fromUUID(UUID.randomUUID()); + protected final CustomerId customerId = new CustomerId(UUID.randomUUID()); + + protected final UUID defaultAssetProfileId = UUID.randomUUID(); + protected final UUID defaultDeviceProfileId = UUID.randomUUID(); + + @Before + public final void before() { + AssetProfile ap = new AssetProfile(new AssetProfileId(defaultAssetProfileId)); + ap.setName("default"); + ap.setDefault(true); + addOrUpdate(EntityType.ASSET_PROFILE, ap); + + DeviceProfile dp = new DeviceProfile(new DeviceProfileId(defaultDeviceProfileId)); + dp.setName("default"); + dp.setDefault(true); + dp.setType(DeviceProfileType.DEFAULT); + addOrUpdate(EntityType.DEVICE_PROFILE, dp); + + createCustomer(customerId.getId(), null, "Customer A"); + } + + @After + public final void after() { + repository.clear(); + } + + protected void createCustomer(UUID id, UUID parentCustomerId, String title) { + Customer entity = new Customer(); + entity.setId(new CustomerId(id)); + entity.setTitle(title); + entity.setOwnerId(parentCustomerId != null ? new CustomerId(parentCustomerId) : tenantId); + addOrUpdate(EntityType.CUSTOMER, entity); + } + + protected UUID createGroup(EntityType entityType, String groupName) { + return createGroup(null, entityType, groupName); + } + + protected UUID createGroup(UUID customerId, EntityType entityType, String groupName) { + EntityGroup eg = new EntityGroup(); + eg.setId(new EntityGroupId(UUID.randomUUID())); + eg.setType(entityType); + eg.setName(groupName); + eg.setOwnerId(customerId != null ? new CustomerId(customerId) : tenantId); + addOrUpdate(EntityType.ENTITY_GROUP, eg); + return eg.getId().getId(); + } + + protected UUID createDevice(String name) { + return createDevice(null, defaultDeviceProfileId, name); + } + + protected UUID createDevice(CustomerId customerId, String name) { + return createDevice(customerId.getId(), defaultDeviceProfileId, name); + } + + protected UUID createDevice(UUID customerId, UUID profileId, String name) { + UUID entityId = UUID.randomUUID(); + Device entity = new Device(); + entity.setId(new DeviceId(entityId)); + if (profileId != null) { + entity.setDeviceProfileId(new DeviceProfileId(profileId)); + } + if (customerId != null) { + entity.setCustomerId(new CustomerId(customerId)); + } + entity.setName(name); + addOrUpdate(EntityType.DEVICE, entity); + return entityId; + } + + protected UUID createDashboard(String name) { + return createDashboard(null, name); + } + + protected UUID createDashboard(UUID customerId, String name) { + UUID entityId = UUID.randomUUID(); + Dashboard entity = new Dashboard(); + entity.setId(new DashboardId(entityId)); + if (customerId != null) { + entity.setCustomerId(new CustomerId(customerId)); + } + entity.setTitle(name); + addOrUpdate(EntityType.DEVICE, entity); + return entityId; + } + + protected UUID createView(String name) { + return createView(null, "default", name); + } + + protected UUID createView(CustomerId customerId, String name) { + return createView(customerId.getId(), "default", name); + } + + protected UUID createView(UUID customerId, String type, String name) { + UUID entityId = UUID.randomUUID(); + EntityView entity = new EntityView(); + entity.setId(new EntityViewId(entityId)); + entity.setType(type); + if (customerId != null) { + entity.setCustomerId(new CustomerId(customerId)); + } + entity.setName(name); + addOrUpdate(EntityType.ENTITY_VIEW, entity); + return entityId; + } + + protected UUID createEdge(String name) { + return createEdge(null, "default", name); + } + + protected UUID createEdge(CustomerId customerId, String name) { + return createEdge(customerId.getId(), "default", name); + } + + protected UUID createEdge(UUID customerId, String type, String name) { + UUID id = UUID.randomUUID(); + Edge edge = new Edge(); + edge.setId(new EdgeId(id)); + edge.setTenantId(tenantId); + if (customerId != null) { + edge.setCustomerId(new CustomerId(customerId)); + } + edge.setType(type); + edge.setName(name); + edge.setCreatedTime(42L); + addOrUpdate(EntityType.EDGE, edge); + return id; + } + + protected UUID createSchedulerEvent(String type, EntityId originatorId, String name) { + return createSchedulerEvent(null, type, originatorId, name); + } + + protected UUID createSchedulerEvent(UUID customerId, String type, EntityId originatorId, String name) { + UUID id = UUID.randomUUID(); + SchedulerEvent schedulerEvent = new SchedulerEvent(); + schedulerEvent.setId(new SchedulerEventId(id)); + schedulerEvent.setTenantId(tenantId); + if (customerId != null) { + schedulerEvent.setCustomerId(new CustomerId(customerId)); + } + schedulerEvent.setType(type); + schedulerEvent.setName(name); + schedulerEvent.setConfiguration(JacksonUtil.newObjectNode()); + schedulerEvent.setSchedule(JacksonUtil.newObjectNode()); + schedulerEvent.setOriginatorId(originatorId); + schedulerEvent.setCreatedTime(42L); + addOrUpdate(EntityType.SCHEDULER_EVENT, schedulerEvent); + return id; + } + + protected UUID createAsset(String name) { + return createAsset(null, defaultAssetProfileId, name); + } + + protected UUID createAsset(UUID customerId, String name) { + return createAsset(customerId, defaultAssetProfileId, name); + } + + protected UUID createAsset(UUID customerId, UUID profileId, String name) { + UUID entityId = UUID.randomUUID(); + Asset entity = new Asset(); + entity.setId(new AssetId(entityId)); + if (profileId != null) { + entity.setAssetProfileId(new AssetProfileId(profileId)); + } + if (customerId != null) { + entity.setCustomerId(new CustomerId(customerId)); + } + entity.setName(name); + addOrUpdate(EntityType.ASSET, entity); + return entityId; + } + + protected void createRelation(EntityType fromType, UUID fromId, EntityType toType, UUID toId, String type) { + createRelation(fromType, fromId, toType, toId, RelationTypeGroup.COMMON, type); + } + + protected void createRelation(EntityType fromType, UUID fromId, EntityType toType, UUID toId, RelationTypeGroup group, String type) { + repository.get(tenantId).addOrUpdate(new EntityRelation(EntityIdFactory.getByTypeAndUuid(fromType, fromId), EntityIdFactory.getByTypeAndUuid(toType, toId), type, group)); + } + + + protected boolean checkContains(PageData data, UUID entityId) { + return data.getData().stream().anyMatch(r -> r.getEntityId().getId().equals(entityId)); + } + + protected List createStringKeyFilters(String key, EntityKeyType keyType, StringFilterPredicate.StringOperation operation, String value) { + KeyFilter filter = new KeyFilter(); + filter.setKey(new EntityKey(keyType, key)); + filter.setValueType(EntityKeyValueType.STRING); + StringFilterPredicate predicate = new StringFilterPredicate(); + predicate.setValue(FilterPredicateValue.fromString(value)); + predicate.setOperation(operation); + predicate.setIgnoreCase(true); + filter.setPredicate(predicate); + return Collections.singletonList(filter); + } + + protected void addOrUpdate(EntityType entityType, Object entity) { + addOrUpdate(EdqsConverter.toEntity(entityType, entity)); + } + + protected void addOrUpdate(EdqsObject edqsObject) { + repository.get(tenantId).addOrUpdate(edqsObject); + } + +} diff --git a/edqs/src/test/java/org/thingsboard/server/edqs/repo/ApiUsageStateFilterTest.java b/edqs/src/test/java/org/thingsboard/server/edqs/repo/ApiUsageStateFilterTest.java new file mode 100644 index 0000000000..de409ca293 --- /dev/null +++ b/edqs/src/test/java/org/thingsboard/server/edqs/repo/ApiUsageStateFilterTest.java @@ -0,0 +1,106 @@ +/** + * Copyright © 2016-2024 ThingsBoard, Inc. + * + * 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.edqs.repo; + +import org.junit.After; +import org.junit.Assert; +import org.junit.Before; +import org.junit.Test; +import org.thingsboard.server.common.data.ApiUsageState; +import org.thingsboard.server.common.data.ApiUsageStateValue; +import org.thingsboard.server.common.data.EntityType; +import org.thingsboard.server.common.data.Tenant; +import org.thingsboard.server.common.data.id.ApiUsageStateId; +import org.thingsboard.server.common.data.id.CustomerId; +import org.thingsboard.server.common.data.query.ApiUsageStateFilter; +import org.thingsboard.server.common.data.query.EntityDataPageLink; +import org.thingsboard.server.common.data.query.EntityDataQuery; +import org.thingsboard.server.common.data.query.EntityDataSortOrder; +import org.thingsboard.server.common.data.query.EntityKey; +import org.thingsboard.server.common.data.query.EntityKeyType; +import org.thingsboard.server.common.data.query.EntityKeyValueType; +import org.thingsboard.server.common.data.query.FilterPredicateValue; +import org.thingsboard.server.common.data.query.KeyFilter; +import org.thingsboard.server.common.data.query.StringFilterPredicate; +import org.thingsboard.server.edqs.util.RepositoryUtils; + +import java.util.Arrays; +import java.util.UUID; + +public class ApiUsageStateFilterTest extends AbstractEDQTest { + + @Before + public void setUp() { + Tenant entity = new Tenant(); + entity.setId(tenantId); + entity.setTitle("test tenant"); + addOrUpdate(EntityType.TENANT, entity); + } + + @After + public void tearDown() { + } + + @Test + public void testFindCustomerApiUsageState() { + UUID customerId = UUID.randomUUID(); + createCustomer(customerId, null, "Customer A"); + + ApiUsageState apiUsageState = buildApiUsageState(customerId); + addOrUpdate(EntityType.API_USAGE_STATE, apiUsageState); + + var result = repository.findEntityDataByQuery(tenantId, null, RepositoryUtils.ALL_READ_PERMISSIONS, getEntityDataQuery(new CustomerId(customerId)), false); + + Assert.assertEquals(1, result.getTotalElements()); + var customer = result.getData().get(0); + Assert.assertEquals("Customer A", customer.getLatest().get(EntityKeyType.ENTITY_FIELD).get("name").getValue()); + } + + private ApiUsageState buildApiUsageState(UUID customerId) { + ApiUsageState apiUsageState = new ApiUsageState(); + apiUsageState.setId(new ApiUsageStateId(UUID.randomUUID())); + apiUsageState.setTenantId(tenantId); + apiUsageState.setEntityId(new CustomerId(customerId)); + apiUsageState.setTransportState(ApiUsageStateValue.ENABLED); + apiUsageState.setReExecState(ApiUsageStateValue.ENABLED); + apiUsageState.setJsExecState(ApiUsageStateValue.ENABLED); + apiUsageState.setTbelExecState(ApiUsageStateValue.ENABLED); + apiUsageState.setDbStorageState(ApiUsageStateValue.ENABLED); + apiUsageState.setSmsExecState(ApiUsageStateValue.ENABLED); + apiUsageState.setEmailExecState(ApiUsageStateValue.ENABLED); + apiUsageState.setAlarmExecState(ApiUsageStateValue.ENABLED); + return apiUsageState; + } + + private static EntityDataQuery getEntityDataQuery(CustomerId customerId) { + ApiUsageStateFilter filter = new ApiUsageStateFilter(); + filter.setCustomerId(customerId); + var pageLink = new EntityDataPageLink(20, 0, null, new EntityDataSortOrder(new EntityKey(EntityKeyType.TIME_SERIES, "name"), EntityDataSortOrder.Direction.DESC), false); + + var entityFields = Arrays.asList(new EntityKey(EntityKeyType.ENTITY_FIELD, "name"), new EntityKey(EntityKeyType.ENTITY_FIELD, "createdTime")); + KeyFilter nameFilter = new KeyFilter(); + nameFilter.setKey(new EntityKey(EntityKeyType.ENTITY_FIELD, "name")); + var predicate = new StringFilterPredicate(); + predicate.setIgnoreCase(false); + predicate.setOperation(StringFilterPredicate.StringOperation.CONTAINS); + predicate.setValue(new FilterPredicateValue<>("Customer A")); + nameFilter.setPredicate(predicate); + nameFilter.setValueType(EntityKeyValueType.STRING); + + return new EntityDataQuery(filter, pageLink, entityFields, null, Arrays.asList(nameFilter)); + } + +} diff --git a/edqs/src/test/java/org/thingsboard/server/edqs/repo/AssetSearchQueryFilterTest.java b/edqs/src/test/java/org/thingsboard/server/edqs/repo/AssetSearchQueryFilterTest.java new file mode 100644 index 0000000000..2983ce57c3 --- /dev/null +++ b/edqs/src/test/java/org/thingsboard/server/edqs/repo/AssetSearchQueryFilterTest.java @@ -0,0 +1,223 @@ +/** + * Copyright © 2016-2024 ThingsBoard, Inc. + * + * 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.edqs.repo; + +import org.junit.Assert; +import org.junit.Before; +import org.junit.Test; +import org.thingsboard.server.common.data.EntityType; +import org.thingsboard.server.common.data.asset.AssetProfile; +import org.thingsboard.server.common.data.edqs.query.QueryResult; +import org.thingsboard.server.common.data.id.AssetId; +import org.thingsboard.server.common.data.id.AssetProfileId; +import org.thingsboard.server.common.data.id.CustomerId; +import org.thingsboard.server.common.data.id.EntityGroupId; +import org.thingsboard.server.common.data.id.EntityId; +import org.thingsboard.server.common.data.page.PageData; +import org.thingsboard.server.common.data.permission.MergedGroupPermissionInfo; +import org.thingsboard.server.common.data.permission.MergedUserPermissions; +import org.thingsboard.server.common.data.permission.Operation; +import org.thingsboard.server.common.data.permission.Resource; +import org.thingsboard.server.common.data.query.AssetSearchQueryFilter; +import org.thingsboard.server.common.data.query.EntityDataPageLink; +import org.thingsboard.server.common.data.query.EntityDataQuery; +import org.thingsboard.server.common.data.query.EntityKeyType; +import org.thingsboard.server.common.data.query.KeyFilter; +import org.thingsboard.server.common.data.query.StringFilterPredicate; +import org.thingsboard.server.common.data.relation.EntitySearchDirection; +import org.thingsboard.server.common.data.relation.RelationTypeGroup; +import org.thingsboard.server.edqs.util.RepositoryUtils; + +import java.util.Arrays; +import java.util.Collections; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.UUID; + +public class AssetSearchQueryFilterTest extends AbstractEDQTest { + private final AssetProfileId assetProfileId = new AssetProfileId(UUID.randomUUID()); + + @Before + public void setUp() { + } + + @Test + public void testFindTenantAssets() { + AssetProfile assetProfile = new AssetProfile(assetProfileId); + assetProfile.setName("Office"); + assetProfile.setDefault(false); + addOrUpdate(EntityType.ASSET_PROFILE, assetProfile); + + UUID root = createAsset(null, assetProfileId.getId(), "root"); + UUID asset1 = createAsset(null, assetProfileId.getId(), "A1"); + UUID asset2 = createAsset(null, assetProfileId.getId(), "A2"); + + createRelation(EntityType.ASSET, root, EntityType.ASSET, asset1, "Contains"); + createRelation(EntityType.ASSET, asset1, EntityType.ASSET, asset2, "Contains"); + + // find all assets of root asset + PageData relationsResult = findData(RepositoryUtils.ALL_READ_PERMISSIONS, null, new AssetId(root), + EntitySearchDirection.FROM, "Contains", 2, false, Arrays.asList("Office")); + Assert.assertEquals(2, relationsResult.getData().size()); + Assert.assertTrue(checkContains(relationsResult, asset1)); + Assert.assertTrue(checkContains(relationsResult, asset2)); + + // find all assets with max level = 1 + relationsResult = findData(RepositoryUtils.ALL_READ_PERMISSIONS, null, new AssetId(root), + EntitySearchDirection.FROM, "Contains", 1, false, Arrays.asList("Office")); + Assert.assertEquals(1, relationsResult.getData().size()); + Assert.assertTrue(checkContains(relationsResult, asset1)); + + // find all assets with asset type = default + relationsResult = findData(RepositoryUtils.ALL_READ_PERMISSIONS, null, new AssetId(root), + EntitySearchDirection.FROM, "Contains", 1, false, Arrays.asList("default")); + Assert.assertEquals(0, relationsResult.getData().size()); + + // find all assets last level only, level = 2 + relationsResult = findData(RepositoryUtils.ALL_READ_PERMISSIONS, null, new AssetId(root), + EntitySearchDirection.FROM, "Contains", 2, true, Arrays.asList("Office")); + Assert.assertEquals(1, relationsResult.getData().size()); + Assert.assertTrue(checkContains(relationsResult, asset2)); + + // find all assets last level only, level = 1 + relationsResult = findData(RepositoryUtils.ALL_READ_PERMISSIONS, null, new AssetId(root), + EntitySearchDirection.FROM, "Contains", 1, true, Arrays.asList("Office")); + Assert.assertEquals(1, relationsResult.getData().size()); + Assert.assertTrue(checkContains(relationsResult, asset1)); + } + + @Test + public void testFindTenantAssetsWithGroupPermissionsOnly() { + UUID eg1 = createGroup(EntityType.ASSET, "Group A"); + + UUID root = createAsset("root"); + UUID asset1 = createAsset("A1"); + createRelation(EntityType.ENTITY_GROUP, eg1, EntityType.ASSET, asset1, RelationTypeGroup.FROM_ENTITY_GROUP, "Contains"); + UUID asset2 = createAsset("A2"); + + createRelation(EntityType.ASSET, root, EntityType.ASSET, asset1, "Contains"); + createRelation(EntityType.ASSET, asset1, EntityType.ASSET, asset2, "Contains"); + + // find all assets group permission only + MergedUserPermissions readGroupPermissions = new MergedUserPermissions(Collections.emptyMap(), Collections.singletonMap(new EntityGroupId(eg1), + new MergedGroupPermissionInfo(EntityType.ASSET, new HashSet<>(Arrays.asList(Operation.READ, Operation.READ_ATTRIBUTES, Operation.READ_TELEMETRY))))); + PageData relationsResult = findData(readGroupPermissions, null, new AssetId(root), + EntitySearchDirection.FROM, "Contains", 2, false, Arrays.asList("default")); + Assert.assertEquals(1, relationsResult.getData().size()); + Assert.assertTrue(checkContains(relationsResult, asset1)); + } + + @Test + public void testFindCustomerAssets() { + AssetProfile assetProfile = new AssetProfile(assetProfileId); + assetProfile.setName("Office"); + assetProfile.setDefault(false); + addOrUpdate(EntityType.ASSET_PROFILE, assetProfile); + + UUID root = createAsset(customerId.getId(), assetProfileId.getId(), "root"); + UUID asset1 = createAsset(customerId.getId(), assetProfileId.getId(), "A1"); + UUID asset2 = createAsset(customerId.getId(), assetProfileId.getId(), "A2"); + UUID asset3 = createAsset(customerId.getId(), defaultAssetProfileId, "A3"); + + createRelation(EntityType.ASSET, root, EntityType.ASSET, asset1, "Contains"); + createRelation(EntityType.ASSET, root, EntityType.ASSET, asset3, "Contains"); + createRelation(EntityType.ASSET, asset1, EntityType.ASSET, asset2, "Contains"); + + // find all assets of root asset with profile "Office" + PageData relationsResult = findData(RepositoryUtils.ALL_READ_PERMISSIONS, customerId, new AssetId(root), + EntitySearchDirection.FROM, "Contains", 2, false, Arrays.asList("Office")); + Assert.assertEquals(2, relationsResult.getData().size()); + Assert.assertTrue(checkContains(relationsResult, asset1)); + Assert.assertTrue(checkContains(relationsResult, asset2)); + + // find all assets of root asset with profile "Office" and "default" + relationsResult = findData(RepositoryUtils.ALL_READ_PERMISSIONS, customerId, new AssetId(root), + EntitySearchDirection.FROM, "Contains", 2, false, Arrays.asList("Office", "default")); + Assert.assertEquals(3, relationsResult.getData().size()); + Assert.assertTrue(checkContains(relationsResult, asset1)); + Assert.assertTrue(checkContains(relationsResult, asset2)); + Assert.assertTrue(checkContains(relationsResult, asset3)); + + // find all assets with other customer + relationsResult = findData(RepositoryUtils.ALL_READ_PERMISSIONS, new CustomerId(UUID.randomUUID()), new AssetId(root), + EntitySearchDirection.FROM, "Contains", 1, false, Arrays.asList("Office")); + Assert.assertEquals(0, relationsResult.getData().size()); + } + + @Test + public void testFindCustomerAssetsWithGroupPermission() { + UUID eg1 = createGroup(EntityType.ASSET, "Group A"); + + UUID root = createAsset(customerId.getId(), defaultAssetProfileId, "root"); + UUID asset1 = createAsset(customerId.getId(), defaultAssetProfileId,"A1"); + createRelation(EntityType.ENTITY_GROUP, eg1, EntityType.ASSET, asset1, RelationTypeGroup.FROM_ENTITY_GROUP, "Contains"); + UUID asset2 = createAsset(customerId.getId(), defaultAssetProfileId,"A2"); + + createRelation(EntityType.ASSET, root, EntityType.ASSET, asset1, "Contains"); + createRelation(EntityType.ASSET, asset1, EntityType.ASSET, asset2, "Contains"); + + // find all assets group permission only + MergedUserPermissions readGroupPermissions = new MergedUserPermissions(Collections.emptyMap(), Collections.singletonMap(new EntityGroupId(eg1), + new MergedGroupPermissionInfo(EntityType.ASSET, new HashSet<>(Arrays.asList(Operation.READ, Operation.READ_ATTRIBUTES, Operation.READ_TELEMETRY))))); + PageData relationsResult = findData(readGroupPermissions, customerId, new AssetId(root), + EntitySearchDirection.FROM, "Contains", 2, false, Arrays.asList("default")); + Assert.assertEquals(1, relationsResult.getData().size()); + Assert.assertTrue(checkContains(relationsResult, asset1)); + } + + @Test + public void testFindCustomerAssetsWithGenericAndGroupPermission() { + CustomerId subCustomer = new CustomerId(UUID.randomUUID()); + createCustomer(subCustomer.getId(), customerId.getId(), "Sub Customer A"); + UUID asset1 = createAsset(subCustomer.getId(), defaultAssetProfileId,"A1"); + UUID eg1 = createGroup(subCustomer.getId(), EntityType.ASSET, "Group A"); + createRelation(EntityType.ENTITY_GROUP, eg1, EntityType.ASSET, asset1, RelationTypeGroup.FROM_ENTITY_GROUP, "Contains"); + + UUID root = createAsset(customerId.getId(), defaultAssetProfileId, "root"); + UUID asset2 = createAsset(customerId.getId(), defaultAssetProfileId,"A2"); + + createRelation(EntityType.ASSET, root, EntityType.ASSET, asset1, "Contains"); + createRelation(EntityType.ASSET, root, EntityType.ASSET, asset2, "Contains"); + + // find all assets group permission only + MergedUserPermissions readGroupPermissions = new MergedUserPermissions(Map.of(Resource.ALL, Set.of(Operation.ALL)), Collections.singletonMap(new EntityGroupId(eg1), + new MergedGroupPermissionInfo(EntityType.ASSET, new HashSet<>(Arrays.asList(Operation.READ, Operation.READ_ATTRIBUTES, Operation.READ_TELEMETRY))))); + PageData relationsResult = findData(readGroupPermissions, customerId, new AssetId(root), + EntitySearchDirection.FROM, "Contains", 2, false, Arrays.asList("default")); + Assert.assertEquals(2, relationsResult.getData().size()); + Assert.assertTrue(checkContains(relationsResult, asset1)); + Assert.assertTrue(checkContains(relationsResult, asset2)); + } + + + private PageData findData(MergedUserPermissions permissions, CustomerId customerId, EntityId rootId, + EntitySearchDirection direction, String relationType, int maxLevel, boolean lastLevelOnly, List assetTypes) { + AssetSearchQueryFilter filter = new AssetSearchQueryFilter(); + filter.setRootEntity(rootId); + filter.setDirection(direction); + filter.setRelationType(relationType); + filter.setAssetTypes(assetTypes); + filter.setFetchLastLevelOnly(lastLevelOnly); + filter.setMaxLevel(maxLevel); + EntityDataPageLink pageLink = new EntityDataPageLink(10, 0, null, null); + List keyFiltersEqualString = createStringKeyFilters("name", EntityKeyType.ENTITY_FIELD, StringFilterPredicate.StringOperation.STARTS_WITH, "A"); + EntityDataQuery query = new EntityDataQuery(filter, pageLink, Collections.emptyList(), Collections.emptyList(), keyFiltersEqualString); + return repository.findEntityDataByQuery(tenantId, customerId, permissions, query, false); + } + +} diff --git a/edqs/src/test/java/org/thingsboard/server/edqs/repo/AssetTypeFilterTest.java b/edqs/src/test/java/org/thingsboard/server/edqs/repo/AssetTypeFilterTest.java new file mode 100644 index 0000000000..6048669d66 --- /dev/null +++ b/edqs/src/test/java/org/thingsboard/server/edqs/repo/AssetTypeFilterTest.java @@ -0,0 +1,187 @@ +/** + * Copyright © 2016-2024 ThingsBoard, Inc. + * + * 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.edqs.repo; + +import org.jetbrains.annotations.NotNull; +import org.junit.After; +import org.junit.Assert; +import org.junit.Before; +import org.junit.Test; +import org.thingsboard.server.common.data.EntityType; +import org.thingsboard.server.common.data.asset.Asset; +import org.thingsboard.server.common.data.asset.AssetProfile; +import org.thingsboard.server.common.data.edqs.LatestTsKv; +import org.thingsboard.server.common.data.id.AssetId; +import org.thingsboard.server.common.data.id.AssetProfileId; +import org.thingsboard.server.common.data.kv.BasicTsKvEntry; +import org.thingsboard.server.common.data.kv.StringDataEntry; +import org.thingsboard.server.common.data.query.AssetTypeFilter; +import org.thingsboard.server.common.data.query.EntityDataPageLink; +import org.thingsboard.server.common.data.query.EntityDataQuery; +import org.thingsboard.server.common.data.query.EntityDataSortOrder; +import org.thingsboard.server.common.data.query.EntityKey; +import org.thingsboard.server.common.data.query.EntityKeyType; +import org.thingsboard.server.common.data.query.EntityKeyValueType; +import org.thingsboard.server.common.data.query.FilterPredicateValue; +import org.thingsboard.server.common.data.query.KeyFilter; +import org.thingsboard.server.common.data.query.StringFilterPredicate; +import org.thingsboard.server.edqs.util.RepositoryUtils; + +import java.util.Arrays; +import java.util.Collections; +import java.util.List; +import java.util.UUID; + +import static org.assertj.core.api.Assertions.assertThat; + +public class AssetTypeFilterTest extends AbstractEDQTest { + + private final AssetProfileId assetProfileId = new AssetProfileId(UUID.randomUUID()); + private final AssetProfileId assetProfileId2 = new AssetProfileId(UUID.randomUUID()); + private Asset asset; + private Asset asset2; + private Asset asset3; + + @Before + public void setUp() { + AssetProfile assetProfile = new AssetProfile(assetProfileId); + assetProfile.setName("Office"); + assetProfile.setDefault(false); + addOrUpdate(EntityType.ASSET_PROFILE, assetProfile); + + AssetProfile assetProfile2 = new AssetProfile(assetProfileId2); + assetProfile2.setName("Street"); + assetProfile2.setDefault(false); + addOrUpdate(EntityType.ASSET_PROFILE, assetProfile2); + + asset = buildAsset(assetProfileId, "Office 1"); + asset2 = buildAsset(assetProfileId, "Office 2"); + asset3 = buildAsset(assetProfileId2, "Abbey Road"); + + addOrUpdate(EntityType.ASSET, asset); + addOrUpdate(EntityType.ASSET, asset2); + addOrUpdate(EntityType.ASSET, asset3); + } + + @After + public void tearDown() { + } + + @Test + public void testFindTenantAsset() { + // find asset with type "Office" + var result = repository.findEntityDataByQuery(tenantId, null, RepositoryUtils.ALL_READ_PERMISSIONS, getAssetTypeQuery(Collections.singletonList("Office"), null, null), false); + + Assert.assertEquals(2, result.getTotalElements()); + var first = result.getData().stream().filter(queryResult -> queryResult.getLatest().get(EntityKeyType.ENTITY_FIELD).get("name").getValue().equals("Office 1")).findAny(); + assertThat(first).isPresent(); + assertThat(first.get().getEntityId()).isEqualTo(asset.getId()); + assertThat(first.get().getLatest().get(EntityKeyType.ENTITY_FIELD).get("createdTime").getValue()).isEqualTo(String.valueOf(asset.getCreatedTime())); + + // find asset with type "Office" and "Street" + result = repository.findEntityDataByQuery(tenantId, null, RepositoryUtils.ALL_READ_PERMISSIONS, getAssetTypeQuery(List.of("Office", "Street"), null, null), false); + + Assert.assertEquals(3, result.getTotalElements()); + var third = result.getData().stream().filter(queryResult -> queryResult.getLatest().get(EntityKeyType.ENTITY_FIELD).get("name").getValue().equals("Abbey Road")).findAny(); + assertThat(third).isPresent(); + assertThat(third.get().getEntityId()).isEqualTo(asset3.getId()); + assertThat(third.get().getLatest().get(EntityKeyType.ENTITY_FIELD).get("createdTime").getValue()).isEqualTo(String.valueOf(asset.getCreatedTime())); + + // find asset with type "Supermarket" + result = repository.findEntityDataByQuery(tenantId, null, RepositoryUtils.ALL_READ_PERMISSIONS, getAssetTypeQuery(List.of("Supermarket"), null, null), false); + Assert.assertEquals(0, result.getTotalElements()); + + // find asset with name "%Office%" + result = repository.findEntityDataByQuery(tenantId, null, RepositoryUtils.ALL_READ_PERMISSIONS, getAssetTypeQuery(List.of("Office"), "%Office%", null), false); + Assert.assertEquals(2, result.getTotalElements()); + + // find asset with name "Office 1" + result = repository.findEntityDataByQuery(tenantId, null, RepositoryUtils.ALL_READ_PERMISSIONS, getAssetTypeQuery(List.of("Office"), "Office 1", null), false); + Assert.assertEquals(1, result.getTotalElements()); + + // find asset with name "%Super%" + result = repository.findEntityDataByQuery(tenantId, null, RepositoryUtils.ALL_READ_PERMISSIONS, getAssetTypeQuery(List.of("Office"), "%Super%", null), false); + Assert.assertEquals(0, result.getTotalElements()); + + // find asset with key filter: name contains "Office" + KeyFilter containsNameFilter = getAssetNameKeyFilter(StringFilterPredicate.StringOperation.CONTAINS, "office", true); + result = repository.findEntityDataByQuery(tenantId, null, RepositoryUtils.ALL_READ_PERMISSIONS, getAssetTypeQuery(List.of("Office"), null, Arrays.asList(containsNameFilter)), false); + Assert.assertEquals(2, result.getTotalElements()); + + // find asset with key filter: name starts with "office" and matches case + KeyFilter startsWithNameFilter = getAssetNameKeyFilter(StringFilterPredicate.StringOperation.STARTS_WITH, "office", false); + result = repository.findEntityDataByQuery(tenantId, null, RepositoryUtils.ALL_READ_PERMISSIONS, getAssetTypeQuery(List.of("Office"), null, Arrays.asList(startsWithNameFilter)), false); + Assert.assertEquals(0, result.getTotalElements()); + } + + @Test + public void testFindCustomerAsset() { + addOrUpdate(EntityType.ASSET, asset); + addOrUpdate(new LatestTsKv(asset.getId(), new BasicTsKvEntry(43, new StringDataEntry("state", "TEST")), 0L)); + + var result = repository.findEntityDataByQuery(tenantId, customerId, RepositoryUtils.ALL_READ_PERMISSIONS, getAssetTypeQuery(List.of("Office"), null, null), false); + Assert.assertEquals(0, result.getTotalElements()); + + asset.setCustomerId(customerId); + addOrUpdate(EntityType.ASSET, asset); + + result = repository.findEntityDataByQuery(tenantId, customerId, RepositoryUtils.ALL_READ_PERMISSIONS, getAssetTypeQuery(List.of("Office"), null, null), false); + + Assert.assertEquals(1, result.getTotalElements()); + var first = result.getData().get(0); + Assert.assertEquals(asset.getId(), first.getEntityId()); + Assert.assertEquals("Office 1", first.getLatest().get(EntityKeyType.ENTITY_FIELD).get("name").getValue()); + Assert.assertEquals("42", first.getLatest().get(EntityKeyType.ENTITY_FIELD).get("createdTime").getValue()); + + result = repository.findEntityDataByQuery(tenantId, customerId, RepositoryUtils.ALL_READ_PERMISSIONS, getAssetTypeQuery(List.of("Supermarket"), null, null), false); + Assert.assertEquals(0, result.getTotalElements()); + } + + private Asset buildAsset(AssetProfileId assetProfileId, String assetName) { + Asset asset = new Asset(); + asset.setId(new AssetId(UUID.randomUUID())); + asset.setTenantId(tenantId); + asset.setAssetProfileId(assetProfileId); + asset.setName(assetName); + asset.setCreatedTime(42L); + return asset; + } + + private static EntityDataQuery getAssetTypeQuery(List assetTypes, String assetNameRegex, List keyFilters) { + AssetTypeFilter filter = new AssetTypeFilter(); + filter.setAssetTypes(assetTypes); + filter.setAssetNameFilter(assetNameRegex); + var pageLink = new EntityDataPageLink(20, 0, null, new EntityDataSortOrder(new EntityKey(EntityKeyType.ENTITY_FIELD, "createdTime"), EntityDataSortOrder.Direction.DESC), false); + + var entityFields = Arrays.asList(new EntityKey(EntityKeyType.ENTITY_FIELD, "name"), new EntityKey(EntityKeyType.ENTITY_FIELD, "createdTime")); + var latestValues = Arrays.asList(new EntityKey(EntityKeyType.TIME_SERIES, "state")); + + return new EntityDataQuery(filter, pageLink, entityFields, latestValues, keyFilters); + } + + private static KeyFilter getAssetNameKeyFilter(StringFilterPredicate.StringOperation operation, String predicateValue, boolean ignoreCase) { + KeyFilter nameFilter = new KeyFilter(); + nameFilter.setKey(new EntityKey(EntityKeyType.ENTITY_FIELD, "name")); + var predicate = new StringFilterPredicate(); + predicate.setIgnoreCase(ignoreCase); + predicate.setOperation(operation); + predicate.setValue(new FilterPredicateValue<>(predicateValue)); + nameFilter.setPredicate(predicate); + nameFilter.setValueType(EntityKeyValueType.STRING); + return nameFilter; + } + +} diff --git a/edqs/src/test/java/org/thingsboard/server/edqs/repo/DeviceSearchQueryFilterTest.java b/edqs/src/test/java/org/thingsboard/server/edqs/repo/DeviceSearchQueryFilterTest.java new file mode 100644 index 0000000000..bea0f57246 --- /dev/null +++ b/edqs/src/test/java/org/thingsboard/server/edqs/repo/DeviceSearchQueryFilterTest.java @@ -0,0 +1,241 @@ +/** + * Copyright © 2016-2024 ThingsBoard, Inc. + * + * 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.edqs.repo; + +import org.junit.Assert; +import org.junit.Before; +import org.junit.Test; +import org.thingsboard.server.common.data.DeviceProfile; +import org.thingsboard.server.common.data.DeviceProfileType; +import org.thingsboard.server.common.data.EntityType; +import org.thingsboard.server.common.data.asset.AssetProfile; +import org.thingsboard.server.common.data.edqs.query.QueryResult; +import org.thingsboard.server.common.data.id.AssetId; +import org.thingsboard.server.common.data.id.CustomerId; +import org.thingsboard.server.common.data.id.DeviceProfileId; +import org.thingsboard.server.common.data.id.EntityGroupId; +import org.thingsboard.server.common.data.id.EntityId; +import org.thingsboard.server.common.data.page.PageData; +import org.thingsboard.server.common.data.permission.MergedGroupPermissionInfo; +import org.thingsboard.server.common.data.permission.MergedUserPermissions; +import org.thingsboard.server.common.data.permission.Operation; +import org.thingsboard.server.common.data.permission.Resource; +import org.thingsboard.server.common.data.query.AssetSearchQueryFilter; +import org.thingsboard.server.common.data.query.DeviceSearchQueryFilter; +import org.thingsboard.server.common.data.query.EntityDataPageLink; +import org.thingsboard.server.common.data.query.EntityDataQuery; +import org.thingsboard.server.common.data.query.EntityKeyType; +import org.thingsboard.server.common.data.query.KeyFilter; +import org.thingsboard.server.common.data.query.StringFilterPredicate; +import org.thingsboard.server.common.data.relation.EntitySearchDirection; +import org.thingsboard.server.common.data.relation.RelationTypeGroup; +import org.thingsboard.server.edqs.util.RepositoryUtils; + +import java.util.Arrays; +import java.util.Collections; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.UUID; + +public class DeviceSearchQueryFilterTest extends AbstractEDQTest { + private final DeviceProfileId deviceProfileId = new DeviceProfileId(UUID.randomUUID()); + + @Before + public void setUp() { + } + + @Test + public void testFindTenantDevices() { + DeviceProfile deviceProfile = new DeviceProfile(deviceProfileId); + deviceProfile.setName("thermostat"); + deviceProfile.setDefault(false); + deviceProfile.setType(DeviceProfileType.DEFAULT); + addOrUpdate(EntityType.DEVICE_PROFILE, deviceProfile); + + UUID asset1 = createAsset( "A1"); + UUID asset2 = createAsset( "A2"); + UUID device1 = createDevice(null, deviceProfileId.getId(), "D1"); + UUID device2 = createDevice(null, deviceProfileId.getId(), "D2"); + + createRelation(EntityType.ASSET, asset1, EntityType.DEVICE, device1, "Contains"); + createRelation(EntityType.ASSET, asset1, EntityType.ASSET, asset2, "Contains"); + createRelation(EntityType.ASSET, asset2, EntityType.DEVICE, device2, "Contains"); + + // find all devices of asset A1 + PageData relationsResult = findData(RepositoryUtils.ALL_READ_PERMISSIONS, null, new AssetId(asset1), + EntitySearchDirection.FROM, "Contains", 2, false, Arrays.asList("thermostat")); + Assert.assertEquals(2, relationsResult.getData().size()); + Assert.assertTrue(checkContains(relationsResult, device1)); + Assert.assertTrue(checkContains(relationsResult, device2)); + + // find all devices with max level = 1 + relationsResult = findData(RepositoryUtils.ALL_READ_PERMISSIONS, null, new AssetId(asset1), + EntitySearchDirection.FROM, "Contains", 1, false, Arrays.asList("thermostat")); + Assert.assertEquals(1, relationsResult.getData().size()); + Assert.assertTrue(checkContains(relationsResult, device1)); + + // find all devices with asset type = default + relationsResult = findData(RepositoryUtils.ALL_READ_PERMISSIONS, null, new AssetId(asset1), + EntitySearchDirection.FROM, "Contains", 1, false, Arrays.asList("default")); + Assert.assertEquals(0, relationsResult.getData().size()); + + // find all devices last level only, level = 2 + relationsResult = findData(RepositoryUtils.ALL_READ_PERMISSIONS, null, new AssetId(asset1), + EntitySearchDirection.FROM, "Contains", 2, true, Arrays.asList("thermostat")); + Assert.assertEquals(2, relationsResult.getData().size()); + Assert.assertTrue(checkContains(relationsResult, device2)); + Assert.assertTrue(checkContains(relationsResult, device1)); + + // find all devices last level only, level = 1 + relationsResult = findData(RepositoryUtils.ALL_READ_PERMISSIONS, null, new AssetId(asset1), + EntitySearchDirection.FROM, "Contains", 1, true, Arrays.asList("thermostat")); + Assert.assertEquals(1, relationsResult.getData().size()); + Assert.assertTrue(checkContains(relationsResult, device1)); + } + + @Test + public void testFindTenantDevicesWithGroupPermissionsOnly() { + UUID eg1 = createGroup(EntityType.DEVICE, "Group A"); + + UUID asset1 = createAsset( "A1"); + UUID asset2 = createAsset( "A2"); + UUID device1 = createDevice(null, defaultDeviceProfileId, "D1"); + UUID device2 = createDevice(null, defaultDeviceProfileId, "D2"); + createRelation(EntityType.ENTITY_GROUP, eg1, EntityType.DEVICE, device1, RelationTypeGroup.FROM_ENTITY_GROUP, "Contains"); + + createRelation(EntityType.ASSET, asset1, EntityType.ASSET, asset2, "Contains"); + createRelation(EntityType.ASSET, asset2, EntityType.DEVICE, device1, "Contains"); + createRelation(EntityType.ASSET, asset2, EntityType.DEVICE, device2, "Contains"); + + // find all devices with group permission only + MergedUserPermissions readGroupPermissions = new MergedUserPermissions(Collections.emptyMap(), Collections.singletonMap(new EntityGroupId(eg1), + new MergedGroupPermissionInfo(EntityType.DEVICE, new HashSet<>(Arrays.asList(Operation.READ, Operation.READ_ATTRIBUTES, Operation.READ_TELEMETRY))))); + PageData relationsResult = findData(readGroupPermissions, null, new AssetId(asset1), + EntitySearchDirection.FROM, "Contains", 2, false, Arrays.asList("default")); + Assert.assertEquals(1, relationsResult.getData().size()); + Assert.assertTrue(checkContains(relationsResult, device1)); + } + + @Test + public void testFindCustomerDevices() { + DeviceProfile deviceProfile = new DeviceProfile(deviceProfileId); + deviceProfile.setName("thermostat"); + deviceProfile.setDefault(false); + deviceProfile.setType(DeviceProfileType.DEFAULT); + addOrUpdate(EntityType.DEVICE_PROFILE, deviceProfile); + + UUID asset1 = createAsset(customerId.getId(), defaultAssetProfileId, "A1"); + UUID asset2 = createAsset(customerId.getId(), defaultAssetProfileId, "A2"); + UUID device1 = createDevice(customerId.getId(), deviceProfileId.getId(), "D1"); + UUID device2 = createDevice(customerId.getId(), defaultDeviceProfileId, "D2"); + + createRelation(EntityType.ASSET, asset1, EntityType.ASSET, asset2, "Contains"); + createRelation(EntityType.ASSET, asset1, EntityType.DEVICE, device1, "Contains"); + createRelation(EntityType.ASSET, asset2, EntityType.DEVICE, device2, "Contains"); + + // find all devices of type "thermostat" + PageData relationsResult = findData(RepositoryUtils.ALL_READ_PERMISSIONS, customerId, new AssetId(asset1), + EntitySearchDirection.FROM, "Contains", 2, false, Arrays.asList("thermostat")); + Assert.assertEquals(1, relationsResult.getData().size()); + Assert.assertTrue(checkContains(relationsResult, device1)); + + // find all assets of root asset with profile "Office" and "default" + relationsResult = findData(RepositoryUtils.ALL_READ_PERMISSIONS, customerId, new AssetId(asset1), + EntitySearchDirection.FROM, "Contains", 2, false, Arrays.asList("thermostat", "default")); + Assert.assertEquals(2, relationsResult.getData().size()); + Assert.assertTrue(checkContains(relationsResult, device1)); + Assert.assertTrue(checkContains(relationsResult, device2)); + + // find all assets with other customer + relationsResult = findData(RepositoryUtils.ALL_READ_PERMISSIONS, new CustomerId(UUID.randomUUID()), new AssetId(asset1), + EntitySearchDirection.FROM, "Contains", 2, false, Arrays.asList("thermostat")); + Assert.assertEquals(0, relationsResult.getData().size()); + } + + @Test + public void testFindCustomerAssetsWithGroupPermission() { + UUID eg1 = createGroup(EntityType.DEVICE, "Group A"); + + DeviceProfile deviceProfile = new DeviceProfile(deviceProfileId); + deviceProfile.setName("thermostat"); + deviceProfile.setDefault(false); + deviceProfile.setType(DeviceProfileType.DEFAULT); + addOrUpdate(EntityType.DEVICE_PROFILE, deviceProfile); + + UUID asset1 = createAsset(customerId.getId(), defaultAssetProfileId, "A1"); + UUID asset2 = createAsset(customerId.getId(), defaultAssetProfileId, "A2"); + UUID device1 = createDevice(customerId.getId(), deviceProfileId.getId(), "D1"); + createRelation(EntityType.ENTITY_GROUP, eg1, EntityType.DEVICE, device1, RelationTypeGroup.FROM_ENTITY_GROUP, "Contains"); + UUID device2 = createDevice(customerId.getId(), defaultDeviceProfileId, "D2"); + + createRelation(EntityType.ASSET, asset1, EntityType.ASSET, asset2, "Contains"); + createRelation(EntityType.ASSET, asset1, EntityType.DEVICE, device1, "Contains"); + createRelation(EntityType.ASSET, asset2, EntityType.DEVICE, device2, "Contains"); + + // find all devices with group permission only + MergedUserPermissions readGroupPermissions = new MergedUserPermissions(Collections.emptyMap(), Collections.singletonMap(new EntityGroupId(eg1), + new MergedGroupPermissionInfo(EntityType.DEVICE, new HashSet<>(Arrays.asList(Operation.READ, Operation.READ_ATTRIBUTES, Operation.READ_TELEMETRY))))); + PageData relationsResult = findData(readGroupPermissions, customerId, new AssetId(asset1), + EntitySearchDirection.FROM, "Contains", 2, false, Arrays.asList("thermostat")); + Assert.assertEquals(1, relationsResult.getData().size()); + Assert.assertTrue(checkContains(relationsResult, device1)); + } + + @Test + public void testFindCustomerEdgesWithGenericAndGroupPermission() { + CustomerId subCustomer = new CustomerId(UUID.randomUUID()); + createCustomer(subCustomer.getId(), customerId.getId(), "Sub Customer A"); + + UUID eg1 = createGroup(subCustomer.getId(), EntityType.DEVICE, "Group A"); + + UUID asset1 = createAsset(customerId.getId(), defaultAssetProfileId, "A1"); + UUID asset2 = createAsset(subCustomer.getId(), defaultAssetProfileId, "A2"); + UUID device1 = createDevice(customerId.getId(), defaultDeviceProfileId, "D1"); + createRelation(EntityType.ENTITY_GROUP, eg1, EntityType.DEVICE, device1, RelationTypeGroup.FROM_ENTITY_GROUP, "Contains"); + UUID device2 = createDevice(subCustomer.getId(), defaultDeviceProfileId, "D2"); + + createRelation(EntityType.ASSET, asset1, EntityType.ASSET, asset2, "Contains"); + createRelation(EntityType.ASSET, asset1, EntityType.DEVICE, device1, "Contains"); + createRelation(EntityType.ASSET, asset2, EntityType.DEVICE, device2, "Contains"); + + // find all devices with generic and group permission + MergedUserPermissions readGroupPermissions = new MergedUserPermissions(Map.of(Resource.ALL, Set.of(Operation.ALL)), Collections.singletonMap(new EntityGroupId(eg1), + new MergedGroupPermissionInfo(EntityType.DEVICE, new HashSet<>(Arrays.asList(Operation.READ, Operation.READ_ATTRIBUTES, Operation.READ_TELEMETRY))))); + PageData relationsResult = findData(readGroupPermissions, subCustomer, new AssetId(asset1), + EntitySearchDirection.FROM, "Contains", 2, false, Arrays.asList("default")); + Assert.assertEquals(2, relationsResult.getData().size()); + Assert.assertTrue(checkContains(relationsResult, device1)); + Assert.assertTrue(checkContains(relationsResult, device2)); + } + + private PageData findData(MergedUserPermissions permissions, CustomerId customerId, EntityId rootId, + EntitySearchDirection direction, String relationType, int maxLevel, boolean lastLevelOnly, List deviceTypes) { + DeviceSearchQueryFilter filter = new DeviceSearchQueryFilter(); + filter.setRootEntity(rootId); + filter.setDirection(direction); + filter.setRelationType(relationType); + filter.setDeviceTypes(deviceTypes); + filter.setFetchLastLevelOnly(lastLevelOnly); + filter.setMaxLevel(maxLevel); + EntityDataPageLink pageLink = new EntityDataPageLink(10, 0, null, null); + List keyFiltersEqualString = createStringKeyFilters("name", EntityKeyType.ENTITY_FIELD, StringFilterPredicate.StringOperation.STARTS_WITH, "D"); + EntityDataQuery query = new EntityDataQuery(filter, pageLink, Collections.emptyList(), Collections.emptyList(), keyFiltersEqualString); + return repository.findEntityDataByQuery(tenantId, customerId, permissions, query, false); + } + +} diff --git a/edqs/src/test/java/org/thingsboard/server/edqs/repo/DeviceTypeFilterTest.java b/edqs/src/test/java/org/thingsboard/server/edqs/repo/DeviceTypeFilterTest.java new file mode 100644 index 0000000000..e4c3a0ffad --- /dev/null +++ b/edqs/src/test/java/org/thingsboard/server/edqs/repo/DeviceTypeFilterTest.java @@ -0,0 +1,142 @@ +/** + * Copyright © 2016-2024 ThingsBoard, Inc. + * + * 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.edqs.repo; + +import org.junit.After; +import org.junit.Assert; +import org.junit.Before; +import org.junit.Test; +import org.thingsboard.server.common.data.Device; +import org.thingsboard.server.common.data.DeviceProfile; +import org.thingsboard.server.common.data.DeviceProfileType; +import org.thingsboard.server.common.data.EntityType; +import org.thingsboard.server.common.data.edqs.LatestTsKv; +import org.thingsboard.server.common.data.id.DeviceId; +import org.thingsboard.server.common.data.id.DeviceProfileId; +import org.thingsboard.server.common.data.kv.BasicTsKvEntry; +import org.thingsboard.server.common.data.kv.StringDataEntry; +import org.thingsboard.server.common.data.query.DeviceTypeFilter; +import org.thingsboard.server.common.data.query.EntityDataPageLink; +import org.thingsboard.server.common.data.query.EntityDataQuery; +import org.thingsboard.server.common.data.query.EntityDataSortOrder; +import org.thingsboard.server.common.data.query.EntityKey; +import org.thingsboard.server.common.data.query.EntityKeyType; +import org.thingsboard.server.common.data.query.EntityKeyValueType; +import org.thingsboard.server.common.data.query.FilterPredicateValue; +import org.thingsboard.server.common.data.query.KeyFilter; +import org.thingsboard.server.common.data.query.StringFilterPredicate; +import org.thingsboard.server.edqs.util.RepositoryUtils; + +import java.util.Arrays; +import java.util.Collections; +import java.util.UUID; + +public class DeviceTypeFilterTest extends AbstractEDQTest { + + private final DeviceProfileId loraProfileId = new DeviceProfileId(UUID.randomUUID()); + + @Before + public void setUp() { + DeviceProfile deviceProfile = new DeviceProfile(loraProfileId); + deviceProfile.setName("LoRa"); + deviceProfile.setDefault(false); + deviceProfile.setType(DeviceProfileType.DEFAULT); + addOrUpdate(EntityType.DEVICE_PROFILE, deviceProfile); + } + + @After + public void tearDown() { + } + + @Test + public void testFindTenantDevice() { + DeviceId deviceId = new DeviceId(UUID.randomUUID()); + Device device = new Device(); + device.setId(deviceId); + device.setTenantId(tenantId); + device.setDeviceProfileId(loraProfileId); + device.setName("LoRa-1"); + device.setCreatedTime(42L); + addOrUpdate(EntityType.DEVICE, device); + + var result = repository.findEntityDataByQuery(tenantId, null, RepositoryUtils.ALL_READ_PERMISSIONS, getDeviceTypeQuery("LoRa"), false); + + Assert.assertEquals(1, result.getTotalElements()); + var first = result.getData().get(0); + Assert.assertEquals(deviceId, first.getEntityId()); + Assert.assertEquals("LoRa-1", first.getLatest().get(EntityKeyType.ENTITY_FIELD).get("name").getValue()); + Assert.assertEquals("42", first.getLatest().get(EntityKeyType.ENTITY_FIELD).get("createdTime").getValue()); + + result = repository.findEntityDataByQuery(tenantId, null, RepositoryUtils.ALL_READ_PERMISSIONS, getDeviceTypeQuery("Not LoRa"), false); + Assert.assertEquals(0, result.getTotalElements()); + + device.setCustomerId(customerId); + addOrUpdate(EntityType.DEVICE, device); + + result = repository.findEntityDataByQuery(tenantId, customerId, RepositoryUtils.ALL_READ_PERMISSIONS, getDeviceTypeQuery("LoRa"), false); + Assert.assertEquals(1, result.getTotalElements()); + result = repository.findEntityDataByQuery(tenantId, customerId, RepositoryUtils.ALL_READ_PERMISSIONS, getDeviceTypeQuery("default"), false); + Assert.assertEquals(0, result.getTotalElements()); + } + + @Test + public void testFindCustomerDevice() { + DeviceId deviceId = new DeviceId(UUID.randomUUID()); + Device device = new Device(); + device.setId(deviceId); + device.setTenantId(tenantId); + device.setName("LoRa-1"); + device.setCreatedTime(42L); + device.setDeviceProfileId(loraProfileId); + + addOrUpdate(EntityType.DEVICE, device); + addOrUpdate(new LatestTsKv(deviceId, new BasicTsKvEntry(43, new StringDataEntry("state", "TEST")), 0L)); + + var result = repository.findEntityDataByQuery(tenantId, customerId, RepositoryUtils.ALL_READ_PERMISSIONS, getDeviceTypeQuery("LoRa"), false); + Assert.assertEquals(0, result.getTotalElements()); + + device.setCustomerId(customerId); + addOrUpdate(EntityType.DEVICE, device); + + result = repository.findEntityDataByQuery(tenantId, customerId, RepositoryUtils.ALL_READ_PERMISSIONS, getDeviceTypeQuery("LoRa"), false); + + Assert.assertEquals(1, result.getTotalElements()); + var first = result.getData().get(0); + Assert.assertEquals(deviceId, first.getEntityId()); + Assert.assertEquals("LoRa-1", first.getLatest().get(EntityKeyType.ENTITY_FIELD).get("name").getValue()); + Assert.assertEquals("42", first.getLatest().get(EntityKeyType.ENTITY_FIELD).get("createdTime").getValue()); + } + + private static EntityDataQuery getDeviceTypeQuery(String deviceType) { + DeviceTypeFilter filter = new DeviceTypeFilter(); + filter.setDeviceTypes(Collections.singletonList(deviceType)); + var pageLink = new EntityDataPageLink(20, 0, null, new EntityDataSortOrder(new EntityKey(EntityKeyType.TIME_SERIES, "state"), EntityDataSortOrder.Direction.DESC), false); + + var entityFields = Arrays.asList(new EntityKey(EntityKeyType.ENTITY_FIELD, "name"), new EntityKey(EntityKeyType.ENTITY_FIELD, "createdTime")); + var latestValues = Arrays.asList(new EntityKey(EntityKeyType.TIME_SERIES, "state")); + KeyFilter nameFilter = new KeyFilter(); + nameFilter.setKey(new EntityKey(EntityKeyType.ENTITY_FIELD, "name")); + var predicate = new StringFilterPredicate(); + predicate.setIgnoreCase(false); + predicate.setOperation(StringFilterPredicate.StringOperation.CONTAINS); + predicate.setValue(new FilterPredicateValue<>("LoRa-")); + nameFilter.setPredicate(predicate); + nameFilter.setValueType(EntityKeyValueType.STRING); + + return new EntityDataQuery(filter, pageLink, entityFields, latestValues, Arrays.asList(nameFilter)); + } + +} diff --git a/edqs/src/test/java/org/thingsboard/server/edqs/repo/EdgeSearchQueryFilterTest.java b/edqs/src/test/java/org/thingsboard/server/edqs/repo/EdgeSearchQueryFilterTest.java new file mode 100644 index 0000000000..15a0b8b4e4 --- /dev/null +++ b/edqs/src/test/java/org/thingsboard/server/edqs/repo/EdgeSearchQueryFilterTest.java @@ -0,0 +1,167 @@ +/** + * Copyright © 2016-2024 ThingsBoard, Inc. + * + * 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.edqs.repo; + +import org.junit.Assert; +import org.junit.Before; +import org.junit.Test; +import org.thingsboard.server.common.data.EntityType; +import org.thingsboard.server.common.data.edqs.query.QueryResult; +import org.thingsboard.server.common.data.id.CustomerId; +import org.thingsboard.server.common.data.id.DeviceId; +import org.thingsboard.server.common.data.id.EntityGroupId; +import org.thingsboard.server.common.data.id.EntityId; +import org.thingsboard.server.common.data.page.PageData; +import org.thingsboard.server.common.data.permission.MergedGroupPermissionInfo; +import org.thingsboard.server.common.data.permission.MergedUserPermissions; +import org.thingsboard.server.common.data.permission.Operation; +import org.thingsboard.server.common.data.query.EdgeSearchQueryFilter; +import org.thingsboard.server.common.data.query.EntityDataPageLink; +import org.thingsboard.server.common.data.query.EntityDataQuery; +import org.thingsboard.server.common.data.query.EntityKeyType; +import org.thingsboard.server.common.data.query.KeyFilter; +import org.thingsboard.server.common.data.query.StringFilterPredicate; +import org.thingsboard.server.common.data.relation.EntitySearchDirection; +import org.thingsboard.server.common.data.relation.RelationTypeGroup; +import org.thingsboard.server.edqs.util.RepositoryUtils; + +import java.util.Arrays; +import java.util.Collections; +import java.util.HashSet; +import java.util.List; +import java.util.UUID; + +public class EdgeSearchQueryFilterTest extends AbstractEDQTest { + + @Before + public void setUp() { + } + + @Test + public void testFindDevicesManagesByTenant() { + UUID edge1 = createEdge("E1"); + UUID edge2 = createEdge("E2"); + UUID device1 = createDevice("D1"); + UUID device2 = createDevice("D2"); + UUID device3 = createDevice("D3"); + + createRelation(EntityType.EDGE, edge1, EntityType.DEVICE, device1, "Manages"); + createRelation(EntityType.EDGE, edge2, EntityType.DEVICE, device2, "Manages"); + createRelation(EntityType.EDGE, edge2, EntityType.DEVICE, device3, "Manages"); + + // find devices managed by edge + PageData relationsResult = findData(RepositoryUtils.ALL_READ_PERMISSIONS, null, new DeviceId(device1), + EntitySearchDirection.TO, "Manages", 2, false, Arrays.asList("default")); + Assert.assertEquals(1, relationsResult.getData().size()); + Assert.assertTrue(checkContains(relationsResult, edge1)); + + // find devices managed by edge with non-existing type + relationsResult = findData(RepositoryUtils.ALL_READ_PERMISSIONS, null, new DeviceId(device1), + EntitySearchDirection.TO, "Manages", 1, false, Arrays.asList("non-existing type")); + Assert.assertEquals(0, relationsResult.getData().size()); + + // find all entity views last level only, level = 2 + relationsResult = findData(RepositoryUtils.ALL_READ_PERMISSIONS, null, new DeviceId(device1), + EntitySearchDirection.TO, "Manages", 2, true, Arrays.asList("default")); + Assert.assertEquals(1, relationsResult.getData().size()); + Assert.assertTrue(checkContains(relationsResult, edge1)); + } + + @Test + public void testFindTenantEdgesWithGroupPermissionOnly() { + UUID eg1 = createGroup(EntityType.EDGE, "Group A"); + + UUID edge1 = createEdge("E1"); + UUID edge2 = createEdge("E2"); + createRelation(EntityType.TENANT, tenantId.getId(), EntityType.EDGE, edge1, "Manages"); + createRelation(EntityType.TENANT, tenantId.getId(), EntityType.EDGE, edge2, "Manages"); + createRelation(EntityType.ENTITY_GROUP, eg1, EntityType.ENTITY_VIEW, edge1, RelationTypeGroup.FROM_ENTITY_GROUP, "Contains"); + + // find all devices with group permission only + MergedUserPermissions readGroupPermissions = new MergedUserPermissions(Collections.emptyMap(), Collections.singletonMap(new EntityGroupId(eg1), + new MergedGroupPermissionInfo(EntityType.EDGE, new HashSet<>(Arrays.asList(Operation.READ, Operation.READ_ATTRIBUTES, Operation.READ_TELEMETRY))))); + PageData relationsResult = findData(readGroupPermissions, null, tenantId, + EntitySearchDirection.FROM, "Manages", 2, false, Arrays.asList("default")); + Assert.assertEquals(1, relationsResult.getData().size()); + Assert.assertTrue(checkContains(relationsResult, edge1)); + } + + @Test + public void testFindCustomerEdges() { + UUID edge1 = createEdge(customerId, "E1"); + UUID edge2 = createEdge(customerId, "E2"); + createRelation(EntityType.CUSTOMER, customerId.getId(), EntityType.EDGE, edge1, "Manages"); + createRelation(EntityType.CUSTOMER, customerId.getId(), EntityType.EDGE, edge2, "Manages"); + + // find all edges managed by customer + PageData relationsResult = findData(RepositoryUtils.ALL_READ_PERMISSIONS, customerId, customerId, + EntitySearchDirection.FROM, "Manages", 2, false, Arrays.asList("default")); + Assert.assertEquals(2, relationsResult.getData().size()); + Assert.assertTrue(checkContains(relationsResult, edge1)); + Assert.assertTrue(checkContains(relationsResult, edge2)); + + // find all edges managed by customer with non-existing type + relationsResult = findData(RepositoryUtils.ALL_READ_PERMISSIONS, customerId, customerId, + EntitySearchDirection.FROM, "Manages", 2, false, Arrays.asList("non existing")); + Assert.assertEquals(0, relationsResult.getData().size()); + + // find all entity views with other customer + relationsResult = findData(RepositoryUtils.ALL_READ_PERMISSIONS, new CustomerId(UUID.randomUUID()), customerId, + EntitySearchDirection.FROM, "Manages", 2, false, Arrays.asList("default")); + Assert.assertEquals(0, relationsResult.getData().size()); + } + + @Test + public void testFindCustomerEdgesWithGroupPermission() { + UUID eg1 = createGroup(EntityType.EDGE, "Group A"); + + UUID edge1 = createEdge(customerId, "E1"); + UUID edge2 = createEdge(customerId, "E2"); + UUID edge3 = createEdge(customerId, "E3"); + + createRelation(EntityType.ENTITY_GROUP, eg1, EntityType.ENTITY_VIEW, edge1, RelationTypeGroup.FROM_ENTITY_GROUP, "Contains"); + createRelation(EntityType.ENTITY_GROUP, eg1, EntityType.ENTITY_VIEW, edge2, RelationTypeGroup.FROM_ENTITY_GROUP, "Contains"); + + createRelation(EntityType.CUSTOMER, customerId.getId(), EntityType.EDGE, edge1, "Manages"); + createRelation(EntityType.CUSTOMER, customerId.getId(), EntityType.EDGE, edge2, "Manages"); + createRelation(EntityType.CUSTOMER, customerId.getId(), EntityType.EDGE, edge3, "Manages"); + + // find all entity views with group permission only + MergedUserPermissions readGroupPermissions = new MergedUserPermissions(Collections.emptyMap(), Collections.singletonMap(new EntityGroupId(eg1), + new MergedGroupPermissionInfo(EntityType.EDGE, new HashSet<>(Arrays.asList(Operation.READ, Operation.READ_ATTRIBUTES, Operation.READ_TELEMETRY))))); + PageData relationsResult = findData(readGroupPermissions, customerId, customerId, + EntitySearchDirection.FROM, "Manages", 2, false, Arrays.asList("default")); + Assert.assertEquals(2, relationsResult.getData().size()); + Assert.assertTrue(checkContains(relationsResult, edge1)); + Assert.assertTrue(checkContains(relationsResult, edge2)); + } + + private PageData findData(MergedUserPermissions permissions, CustomerId customerId, EntityId rootId, + EntitySearchDirection direction, String relationType, int maxLevel, boolean lastLevelOnly, List edgeTypes) { + EdgeSearchQueryFilter filter = new EdgeSearchQueryFilter(); + filter.setRootEntity(rootId); + filter.setDirection(direction); + filter.setRelationType(relationType); + filter.setEdgeTypes(edgeTypes); + filter.setFetchLastLevelOnly(lastLevelOnly); + filter.setMaxLevel(maxLevel); + EntityDataPageLink pageLink = new EntityDataPageLink(10, 0, null, null); + List keyFiltersEqualString = createStringKeyFilters("name", EntityKeyType.ENTITY_FIELD, StringFilterPredicate.StringOperation.STARTS_WITH, "E"); + EntityDataQuery query = new EntityDataQuery(filter, pageLink, Collections.emptyList(), Collections.emptyList(), keyFiltersEqualString); + return repository.findEntityDataByQuery(tenantId, customerId, permissions, query, false); + } + +} diff --git a/edqs/src/test/java/org/thingsboard/server/edqs/repo/EdgeTypeFilterTest.java b/edqs/src/test/java/org/thingsboard/server/edqs/repo/EdgeTypeFilterTest.java new file mode 100644 index 0000000000..8c48e0f1ea --- /dev/null +++ b/edqs/src/test/java/org/thingsboard/server/edqs/repo/EdgeTypeFilterTest.java @@ -0,0 +1,177 @@ +/** + * Copyright © 2016-2024 ThingsBoard, Inc. + * + * 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.edqs.repo; + +import org.junit.After; +import org.junit.Assert; +import org.junit.Before; +import org.junit.Test; +import org.thingsboard.server.common.data.EntityType; +import org.thingsboard.server.common.data.edge.Edge; +import org.thingsboard.server.common.data.edqs.LatestTsKv; +import org.thingsboard.server.common.data.edqs.query.QueryResult; +import org.thingsboard.server.common.data.id.EdgeId; +import org.thingsboard.server.common.data.kv.BasicTsKvEntry; +import org.thingsboard.server.common.data.kv.StringDataEntry; +import org.thingsboard.server.common.data.query.EdgeTypeFilter; +import org.thingsboard.server.common.data.query.EntityDataPageLink; +import org.thingsboard.server.common.data.query.EntityDataQuery; +import org.thingsboard.server.common.data.query.EntityDataSortOrder; +import org.thingsboard.server.common.data.query.EntityKey; +import org.thingsboard.server.common.data.query.EntityKeyType; +import org.thingsboard.server.common.data.query.EntityKeyValueType; +import org.thingsboard.server.common.data.query.FilterPredicateValue; +import org.thingsboard.server.common.data.query.KeyFilter; +import org.thingsboard.server.common.data.query.StringFilterPredicate; +import org.thingsboard.server.edqs.util.RepositoryUtils; + +import java.util.Arrays; +import java.util.Collections; +import java.util.List; +import java.util.Optional; +import java.util.UUID; + +import static org.assertj.core.api.AssertionsForClassTypes.assertThat; + +public class EdgeTypeFilterTest extends AbstractEDQTest { + + private Edge edge; + private Edge edge2; + private Edge edge3; + + + @Before + public void setUp() { + edge = buildEdge("default", "Edge 1"); + edge2 = buildEdge("default", "Edge 2"); + edge3 = buildEdge("edge v2", "Edge 3"); + addOrUpdate(EntityType.EDGE, edge); + addOrUpdate(EntityType.EDGE, edge2); + addOrUpdate(EntityType.EDGE, edge3); + } + + @After + public void tearDown() { + } + + @Test + public void testFindTenantEdges() { + // find edges with type "default" + var result = repository.findEntityDataByQuery(tenantId, null, RepositoryUtils.ALL_READ_PERMISSIONS, getEdgeTypeQuery(Collections.singletonList("default"), null, null), false); + + Assert.assertEquals(2, result.getTotalElements()); + Optional firstView = result.getData().stream().filter(queryResult -> queryResult.getLatest().get(EntityKeyType.ENTITY_FIELD).get("name").getValue().equals("Edge 1")).findFirst(); + assertThat(firstView).isPresent(); + assertThat(firstView.get().getEntityId()).isEqualTo(edge.getId()); + assertThat(firstView.get().getLatest().get(EntityKeyType.ENTITY_FIELD).get("createdTime").getValue()).isEqualTo(String.valueOf(edge.getCreatedTime())); + + // find edges with types "default" and "edge v2" + result = repository.findEntityDataByQuery(tenantId, null, RepositoryUtils.ALL_READ_PERMISSIONS, getEdgeTypeQuery(Arrays.asList("default", "edge v2"), null, null), false); + + Assert.assertEquals(3, result.getTotalElements()); + Optional thirdView = result.getData().stream().filter(queryResult -> queryResult.getLatest().get(EntityKeyType.ENTITY_FIELD).get("name").getValue().equals("Edge 3")).findFirst(); + assertThat(thirdView).isPresent(); + assertThat(thirdView.get().getEntityId()).isEqualTo(edge3.getId()); + assertThat(thirdView.get().getLatest().get(EntityKeyType.ENTITY_FIELD).get("createdTime").getValue()).isEqualTo(String.valueOf(edge.getCreatedTime())); + + // find entity view with type "day 3" + result = repository.findEntityDataByQuery(tenantId, null, RepositoryUtils.ALL_READ_PERMISSIONS, getEdgeTypeQuery(List.of("edge v3"), null, null), false); + Assert.assertEquals(0, result.getTotalElements()); + + // find entity view with name "%Edge%" + result = repository.findEntityDataByQuery(tenantId, null, RepositoryUtils.ALL_READ_PERMISSIONS, getEdgeTypeQuery(List.of("default"), "%Edge%", null), false); + Assert.assertEquals(2, result.getTotalElements()); + + // find entity view with name "Edge 1" + result = repository.findEntityDataByQuery(tenantId, null, RepositoryUtils.ALL_READ_PERMISSIONS, getEdgeTypeQuery(List.of("default"), "Edge 1", null), false); + Assert.assertEquals(1, result.getTotalElements()); + + // find entity view with name "%Edge 4%" + result = repository.findEntityDataByQuery(tenantId, null, RepositoryUtils.ALL_READ_PERMISSIONS, getEdgeTypeQuery(List.of("default"), "%Edge 4%", null), false); + Assert.assertEquals(0, result.getTotalElements()); + + // find entity view with key filter: name contains "Edge" + KeyFilter containsNameFilter = getEntityViewNameKeyFilter(StringFilterPredicate.StringOperation.CONTAINS, "Edge", true); + result = repository.findEntityDataByQuery(tenantId, null, RepositoryUtils.ALL_READ_PERMISSIONS, getEdgeTypeQuery(List.of("default"), null, List.of(containsNameFilter)), false); + Assert.assertEquals(2, result.getTotalElements()); + + // find entity view with key filter: name starts with "edge" and matches case + KeyFilter startsWithNameFilter = getEntityViewNameKeyFilter(StringFilterPredicate.StringOperation.STARTS_WITH, "edge", false); + result = repository.findEntityDataByQuery(tenantId, null, RepositoryUtils.ALL_READ_PERMISSIONS, getEdgeTypeQuery(List.of("default"), null, List.of(startsWithNameFilter)), false); + Assert.assertEquals(0, result.getTotalElements()); + } + + @Test + public void testFindCustomerEdges() { + addOrUpdate(new LatestTsKv(edge.getId(), new BasicTsKvEntry(43, new StringDataEntry("state", "TEST")), 0L)); + + var result = repository.findEntityDataByQuery(tenantId, customerId, RepositoryUtils.ALL_READ_PERMISSIONS, getEdgeTypeQuery(List.of("default"), null, null), false); + Assert.assertEquals(0, result.getTotalElements()); + + edge.setCustomerId(customerId); + edge2.setCustomerId(customerId); + edge3.setCustomerId(customerId); + addOrUpdate(EntityType.EDGE, edge); + addOrUpdate(EntityType.EDGE, edge2); + addOrUpdate(EntityType.EDGE, edge3); + + result = repository.findEntityDataByQuery(tenantId, customerId, RepositoryUtils.ALL_READ_PERMISSIONS, getEdgeTypeQuery(List.of("default"), null, null), false); + + Assert.assertEquals(2, result.getTotalElements()); + Optional firstView = result.getData().stream().filter(queryResult -> queryResult.getLatest().get(EntityKeyType.ENTITY_FIELD).get("name").getValue().equals("Edge 1")).findFirst(); + assertThat(firstView).isPresent(); + assertThat(firstView.get().getEntityId()).isEqualTo(edge.getId()); + assertThat(firstView.get().getLatest().get(EntityKeyType.ENTITY_FIELD).get("createdTime").getValue()).isEqualTo(String.valueOf(edge.getCreatedTime())); + + result = repository.findEntityDataByQuery(tenantId, customerId, RepositoryUtils.ALL_READ_PERMISSIONS, getEdgeTypeQuery(List.of("edge v3"), null, null), false); + Assert.assertEquals(0, result.getTotalElements()); + } + + private Edge buildEdge(String type, String name) { + Edge edge = new Edge(); + edge.setId(new EdgeId(UUID.randomUUID())); + edge.setTenantId(tenantId); + edge.setType(type); + edge.setName(name); + edge.setCreatedTime(42L); + return edge; + } + + private static EntityDataQuery getEdgeTypeQuery(List edgeTypes, String edgeNameFilter, List keyFilters) { + EdgeTypeFilter filter = new EdgeTypeFilter(); + filter.setEdgeTypes(edgeTypes); + filter.setEdgeNameFilter(edgeNameFilter); + var pageLink = new EntityDataPageLink(20, 0, null, new EntityDataSortOrder(new EntityKey(EntityKeyType.ENTITY_FIELD, "createdTime"), EntityDataSortOrder.Direction.DESC), false); + + var entityFields = Arrays.asList(new EntityKey(EntityKeyType.ENTITY_FIELD, "name"), new EntityKey(EntityKeyType.ENTITY_FIELD, "createdTime")); + var latestValues = Arrays.asList(new EntityKey(EntityKeyType.TIME_SERIES, "state")); + + return new EntityDataQuery(filter, pageLink, entityFields, latestValues, keyFilters); + } + + private static KeyFilter getEntityViewNameKeyFilter(StringFilterPredicate.StringOperation operation, String predicateValue, boolean ignoreCase) { + KeyFilter nameFilter = new KeyFilter(); + nameFilter.setKey(new EntityKey(EntityKeyType.ENTITY_FIELD, "name")); + var predicate = new StringFilterPredicate(); + predicate.setIgnoreCase(ignoreCase); + predicate.setOperation(operation); + predicate.setValue(new FilterPredicateValue<>(predicateValue)); + nameFilter.setPredicate(predicate); + nameFilter.setValueType(EntityKeyValueType.STRING); + return nameFilter; + } + +} diff --git a/edqs/src/test/java/org/thingsboard/server/edqs/repo/EntitiesByGroupIdFilterTest.java b/edqs/src/test/java/org/thingsboard/server/edqs/repo/EntitiesByGroupIdFilterTest.java new file mode 100644 index 0000000000..0d0a623f7a --- /dev/null +++ b/edqs/src/test/java/org/thingsboard/server/edqs/repo/EntitiesByGroupIdFilterTest.java @@ -0,0 +1,161 @@ +/** + * Copyright © 2016-2024 ThingsBoard, Inc. + * + * 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.edqs.repo; + +import org.junit.After; +import org.junit.Assert; +import org.junit.Before; +import org.junit.Test; +import org.thingsboard.server.common.data.EntityType; +import org.thingsboard.server.common.data.id.CustomerId; +import org.thingsboard.server.common.data.id.EntityGroupId; +import org.thingsboard.server.common.data.permission.MergedGroupPermissionInfo; +import org.thingsboard.server.common.data.permission.MergedUserPermissions; +import org.thingsboard.server.common.data.permission.Operation; +import org.thingsboard.server.common.data.permission.Resource; +import org.thingsboard.server.common.data.query.EntityDataPageLink; +import org.thingsboard.server.common.data.query.EntityDataQuery; +import org.thingsboard.server.common.data.query.EntityDataSortOrder; +import org.thingsboard.server.common.data.query.EntityGroupFilter; +import org.thingsboard.server.common.data.query.EntityKey; +import org.thingsboard.server.common.data.query.EntityKeyType; +import org.thingsboard.server.common.data.query.EntityKeyValueType; +import org.thingsboard.server.common.data.query.FilterPredicateValue; +import org.thingsboard.server.common.data.query.KeyFilter; +import org.thingsboard.server.common.data.query.StringFilterPredicate; +import org.thingsboard.server.common.data.relation.RelationTypeGroup; +import org.thingsboard.server.edqs.util.RepositoryUtils; + +import java.util.Arrays; +import java.util.Collections; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.UUID; + +import static org.assertj.core.api.Assertions.assertThat; + +public class EntitiesByGroupIdFilterTest extends AbstractEDQTest { + + private UUID deviceId; + private UUID deviceId2; + private UUID deviceId3; + + private UUID groupAId; + private UUID groupBId; + + @Before + public void setUp() { + deviceId = createDevice(customerId, "Lora-1"); + deviceId2 = createDevice(customerId, "Lora-2"); + deviceId3 = createDevice(customerId, "Lora-3"); + + // add device and device 2 to Group A + groupAId = createGroup(customerId.getId(), EntityType.DEVICE, "Group A"); + createRelation(EntityType.ENTITY_GROUP, groupAId, EntityType.DEVICE, deviceId, RelationTypeGroup.FROM_ENTITY_GROUP, "Contains"); + createRelation(EntityType.ENTITY_GROUP, groupAId, EntityType.DEVICE, deviceId2, RelationTypeGroup.FROM_ENTITY_GROUP, "Contains"); + + // add device and device 3 to Group B + groupBId = createGroup(customerId.getId(), EntityType.DEVICE, "Group B"); + createRelation(EntityType.ENTITY_GROUP, groupBId, EntityType.DEVICE, deviceId3, RelationTypeGroup.FROM_ENTITY_GROUP, "Contains"); + } + + @After + public void tearDown() { + } + + @Test + public void testFindTenantEntitiesOfGroupA() { + // get devices of group A + var result = repository.findEntityDataByQuery(tenantId, null, RepositoryUtils.ALL_READ_PERMISSIONS, getEntitiesByGroupDataQuery(EntityType.DEVICE, new EntityGroupId(groupAId), null), false); + + Assert.assertEquals(2, result.getTotalElements()); + Assert.assertTrue(checkContains(result, deviceId)); + Assert.assertTrue(checkContains(result, deviceId2)); + + //get devices of non-existing group + result = repository.findEntityDataByQuery(tenantId, null, RepositoryUtils.ALL_READ_PERMISSIONS, getEntitiesByGroupDataQuery(EntityType.DEVICE, new EntityGroupId(UUID.randomUUID()), null), false); + Assert.assertEquals(0, result.getTotalElements()); + + //add name filter + KeyFilter nameFilter = getNameKeyFilter(StringFilterPredicate.StringOperation.CONTAINS, "humidity"); + result = repository.findEntityDataByQuery(tenantId, null, RepositoryUtils.ALL_READ_PERMISSIONS, getEntitiesByGroupDataQuery(EntityType.DEVICE, new EntityGroupId(groupAId), List.of(nameFilter)), false); + Assert.assertEquals(0, result.getTotalElements()); + } + + @Test + public void testFindCustomerEntitiesOfGroupA() { + var result = repository.findEntityDataByQuery(tenantId, new CustomerId(UUID.randomUUID()), RepositoryUtils.ALL_READ_PERMISSIONS, getEntitiesByGroupDataQuery(EntityType.DEVICE, new EntityGroupId(groupAId), null), false); + Assert.assertEquals(0, result.getTotalElements()); + + result = repository.findEntityDataByQuery(tenantId, customerId, RepositoryUtils.ALL_READ_PERMISSIONS, getEntitiesByGroupDataQuery(EntityType.DEVICE, new EntityGroupId(groupAId), null), false); + Assert.assertEquals(2, result.getTotalElements()); + List entityIds = result.getData().stream().map(queryResult -> queryResult.getEntityId().getId()).toList(); + assertThat(entityIds).containsOnly(deviceId, deviceId2); + } + + @Test + public void testFindCustomerEntitiesWithGroupPermission() { + MergedUserPermissions groupAPermission = new MergedUserPermissions( + Collections.emptyMap(), Map.of(new EntityGroupId(groupAId), new MergedGroupPermissionInfo(EntityType.DEVICE, Set.of(Operation.ALL)))); + var result = repository.findEntityDataByQuery(tenantId, customerId, groupAPermission, getEntitiesByGroupDataQuery(EntityType.DEVICE, new EntityGroupId(groupAId), null), false); + Assert.assertEquals(2, result.getTotalElements()); + List entityIds = result.getData().stream().map(queryResult -> queryResult.getEntityId().getId()).toList(); + assertThat(entityIds).containsOnly(deviceId, deviceId2); + + MergedUserPermissions groupBPermission = new MergedUserPermissions( + Collections.emptyMap(), Map.of(new EntityGroupId(groupBId), new MergedGroupPermissionInfo(EntityType.DEVICE, Set.of(Operation.ALL)))); + result = repository.findEntityDataByQuery(tenantId, customerId, groupBPermission, getEntitiesByGroupDataQuery(EntityType.DEVICE, new EntityGroupId(groupAId), null), false); + Assert.assertEquals(0, result.getTotalElements()); + } + + @Test + public void testFindCustomerEntitiesWithGenericAndGroupPermission() { + CustomerId subCustomer = new CustomerId(UUID.randomUUID()); + createCustomer(subCustomer.getId(), customerId.getId(), "Subcustomer A"); + + MergedUserPermissions groupPermission = new MergedUserPermissions( + Map.of(Resource.ALL, Set.of(Operation.ALL)), Map.of(new EntityGroupId(groupBId), new MergedGroupPermissionInfo(EntityType.DEVICE, Set.of(Operation.ALL)))); + var result = repository.findEntityDataByQuery(tenantId, subCustomer, groupPermission, + getEntitiesByGroupDataQuery(EntityType.DEVICE, new EntityGroupId(groupBId), null), false); + + Assert.assertEquals(1, result.getTotalElements()); + Assert.assertTrue(checkContains(result, deviceId3)); + } + + private static EntityDataQuery getEntitiesByGroupDataQuery(EntityType entityType, EntityGroupId groupId, List keyFilters) { + EntityGroupFilter filter = new EntityGroupFilter(); + filter.setGroupType(entityType); + filter.setEntityGroup(groupId.getId().toString()); + var pageLink = new EntityDataPageLink(20, 0, null, new EntityDataSortOrder(new EntityKey(EntityKeyType.ENTITY_FIELD, "createdTime"), EntityDataSortOrder.Direction.DESC), false); + + var entityFields = Arrays.asList(new EntityKey(EntityKeyType.ENTITY_FIELD, "name"), new EntityKey(EntityKeyType.ENTITY_FIELD, "createdTime")); + return new EntityDataQuery(filter, pageLink, entityFields, null, keyFilters); + } + + private static KeyFilter getNameKeyFilter(StringFilterPredicate.StringOperation operation, String value) { + KeyFilter nameFilter = new KeyFilter(); + nameFilter.setKey(new EntityKey(EntityKeyType.ENTITY_FIELD, "name")); + var predicate = new StringFilterPredicate(); + predicate.setIgnoreCase(false); + predicate.setOperation(operation); + predicate.setValue(new FilterPredicateValue<>(value)); + nameFilter.setPredicate(predicate); + nameFilter.setValueType(EntityKeyValueType.STRING); + return nameFilter; + } + +} diff --git a/edqs/src/test/java/org/thingsboard/server/edqs/repo/EntitiesByGroupNameFilterTest.java b/edqs/src/test/java/org/thingsboard/server/edqs/repo/EntitiesByGroupNameFilterTest.java new file mode 100644 index 0000000000..a6a81abd46 --- /dev/null +++ b/edqs/src/test/java/org/thingsboard/server/edqs/repo/EntitiesByGroupNameFilterTest.java @@ -0,0 +1,140 @@ +/** + * Copyright © 2016-2024 ThingsBoard, Inc. + * + * 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.edqs.repo; + +import org.junit.After; +import org.junit.Assert; +import org.junit.Before; +import org.junit.Test; +import org.thingsboard.server.common.data.EntityType; +import org.thingsboard.server.common.data.id.CustomerId; +import org.thingsboard.server.common.data.id.EntityGroupId; +import org.thingsboard.server.common.data.id.EntityId; +import org.thingsboard.server.common.data.permission.MergedGroupPermissionInfo; +import org.thingsboard.server.common.data.permission.MergedUserPermissions; +import org.thingsboard.server.common.data.permission.Operation; +import org.thingsboard.server.common.data.permission.Resource; +import org.thingsboard.server.common.data.query.EntitiesByGroupNameFilter; +import org.thingsboard.server.common.data.query.EntityDataPageLink; +import org.thingsboard.server.common.data.query.EntityDataQuery; +import org.thingsboard.server.common.data.query.EntityDataSortOrder; +import org.thingsboard.server.common.data.query.EntityKey; +import org.thingsboard.server.common.data.query.EntityKeyType; +import org.thingsboard.server.common.data.query.KeyFilter; +import org.thingsboard.server.common.data.relation.RelationTypeGroup; +import org.thingsboard.server.edqs.util.RepositoryUtils; + +import java.util.Arrays; +import java.util.Collections; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.UUID; + +import static org.assertj.core.api.Assertions.assertThat; + +public class EntitiesByGroupNameFilterTest extends AbstractEDQTest { + + private UUID deviceId; + private UUID deviceId2; + private UUID deviceId3; + + private UUID groupAId; + private UUID groupBId; + + @Before + public void setUp() { + deviceId = createDevice(customerId, "Lora-1"); + deviceId2 = createDevice(customerId, "Lora-2"); + deviceId3 = createDevice(customerId, "Lora-3"); + + // add device and device 2 to Group A + groupAId = createGroup(customerId.getId(), EntityType.DEVICE, "Group A"); + createRelation(EntityType.ENTITY_GROUP, groupAId, EntityType.DEVICE, deviceId, RelationTypeGroup.FROM_ENTITY_GROUP, "Contains"); + createRelation(EntityType.ENTITY_GROUP, groupAId, EntityType.DEVICE, deviceId2, RelationTypeGroup.FROM_ENTITY_GROUP, "Contains"); + + // add device and device 3 to Group B + groupBId = createGroup(customerId.getId(), EntityType.DEVICE, "Group B"); + createRelation(EntityType.ENTITY_GROUP, groupBId, EntityType.DEVICE, deviceId3, RelationTypeGroup.FROM_ENTITY_GROUP, "Contains"); + } + + @After + public void tearDown() { + } + + @Test + public void testFindTenantEntitiesOfGroupA() { + // get entity list + var result = repository.findEntityDataByQuery(tenantId, null, RepositoryUtils.ALL_READ_PERMISSIONS, getEntitiesByGroupNameDataQuery(EntityType.DEVICE, "Group A", null, null), false); + + Assert.assertEquals(2, result.getTotalElements()); + List entityIds = result.getData().stream().map(queryResult -> queryResult.getEntityId().getId()).toList(); + assertThat(entityIds).containsOnly(deviceId, deviceId2); + + result = repository.findEntityDataByQuery(tenantId, null, RepositoryUtils.ALL_READ_PERMISSIONS, getEntitiesByGroupNameDataQuery(EntityType.DEVICE, "Group B", null, null), false); + Assert.assertEquals(1, result.getTotalElements()); + } + + @Test + public void testFindCustomerEntitiesOfGroupA() { + var result = repository.findEntityDataByQuery(tenantId, new CustomerId(UUID.randomUUID()), RepositoryUtils.ALL_READ_PERMISSIONS, getEntitiesByGroupNameDataQuery(EntityType.DEVICE, "Group A", null, null), false); + Assert.assertEquals(0, result.getTotalElements()); + + result = repository.findEntityDataByQuery(tenantId, customerId, RepositoryUtils.ALL_READ_PERMISSIONS, getEntitiesByGroupNameDataQuery(EntityType.DEVICE, "Group A", null, null), false); + Assert.assertEquals(2, result.getTotalElements()); + List entityIds = result.getData().stream().map(queryResult -> queryResult.getEntityId().getId()).toList(); + assertThat(entityIds).containsOnly(deviceId, deviceId2); + } + + @Test + public void testFindCustomerEntitiesOfGroupAWithGroupPermission() { + MergedUserPermissions groupPermission = new MergedUserPermissions( + Collections.emptyMap(), Map.of(new EntityGroupId(groupAId), new MergedGroupPermissionInfo(EntityType.DEVICE, Set.of(Operation.ALL)))); + var result = repository.findEntityDataByQuery(tenantId, customerId, groupPermission, getEntitiesByGroupNameDataQuery(EntityType.DEVICE, "Group A", null, null), false); + Assert.assertEquals(2, result.getTotalElements()); + List entityIds = result.getData().stream().map(queryResult -> queryResult.getEntityId().getId()).toList(); + assertThat(entityIds).containsOnly(deviceId, deviceId2); + } + + @Test + public void testFindGroupWithGenericAndGroupPermission() { + CustomerId subCustomer = new CustomerId(UUID.randomUUID()); + createCustomer(subCustomer.getId(), customerId.getId(), "Subcustomer A"); + UUID subCustomerGroupId = createGroup(subCustomer.getId(), EntityType.DEVICE, "Group B"); + UUID deviceId4 = createDevice(subCustomer, "Lora-4"); + createRelation(EntityType.ENTITY_GROUP, subCustomerGroupId, EntityType.DEVICE, deviceId4, RelationTypeGroup.FROM_ENTITY_GROUP, "Contains"); + + MergedUserPermissions groupPermission = new MergedUserPermissions( + Map.of(Resource.ALL, Set.of(Operation.ALL)), Map.of(new EntityGroupId(groupBId), new MergedGroupPermissionInfo(EntityType.DEVICE, Set.of(Operation.ALL)))); + var result = repository.findEntityDataByQuery(tenantId, subCustomer, groupPermission, + getEntitiesByGroupNameDataQuery(EntityType.DEVICE, "Group B", null, null), false); + + Assert.assertEquals(1, result.getTotalElements()); + } + + private static EntityDataQuery getEntitiesByGroupNameDataQuery(EntityType entityType, String groupName, EntityId ownerId, List keyFilters) { + EntitiesByGroupNameFilter filter = new EntitiesByGroupNameFilter(); + filter.setGroupType(entityType); + filter.setEntityGroupNameFilter(groupName); + filter.setOwnerId(ownerId); + var pageLink = new EntityDataPageLink(20, 0, null, new EntityDataSortOrder(new EntityKey(EntityKeyType.ENTITY_FIELD, "createdTime"), EntityDataSortOrder.Direction.DESC), false); + + var entityFields = Arrays.asList(new EntityKey(EntityKeyType.ENTITY_FIELD, "name"), new EntityKey(EntityKeyType.ENTITY_FIELD, "createdTime")); + + return new EntityDataQuery(filter, pageLink, entityFields, null, keyFilters); + } + +} diff --git a/edqs/src/test/java/org/thingsboard/server/edqs/repo/EntityGroupListFilterTest.java b/edqs/src/test/java/org/thingsboard/server/edqs/repo/EntityGroupListFilterTest.java new file mode 100644 index 0000000000..971b9d52ad --- /dev/null +++ b/edqs/src/test/java/org/thingsboard/server/edqs/repo/EntityGroupListFilterTest.java @@ -0,0 +1,189 @@ +/** + * Copyright © 2016-2024 ThingsBoard, Inc. + * + * 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.edqs.repo; + +import org.junit.After; +import org.junit.Assert; +import org.junit.Before; +import org.junit.Test; +import org.thingsboard.server.common.data.EntityType; +import org.thingsboard.server.common.data.group.EntityGroup; +import org.thingsboard.server.common.data.id.CustomerId; +import org.thingsboard.server.common.data.id.EntityGroupId; +import org.thingsboard.server.common.data.permission.MergedGroupPermissionInfo; +import org.thingsboard.server.common.data.permission.MergedUserPermissions; +import org.thingsboard.server.common.data.permission.Operation; +import org.thingsboard.server.common.data.permission.Resource; +import org.thingsboard.server.common.data.query.EntityDataPageLink; +import org.thingsboard.server.common.data.query.EntityDataQuery; +import org.thingsboard.server.common.data.query.EntityDataSortOrder; +import org.thingsboard.server.common.data.query.EntityGroupListFilter; +import org.thingsboard.server.common.data.query.EntityKey; +import org.thingsboard.server.common.data.query.EntityKeyType; +import org.thingsboard.server.common.data.query.EntityKeyValueType; +import org.thingsboard.server.common.data.query.FilterPredicateValue; +import org.thingsboard.server.common.data.query.KeyFilter; +import org.thingsboard.server.common.data.query.StringFilterPredicate; +import org.thingsboard.server.edqs.util.RepositoryUtils; + +import java.util.Arrays; +import java.util.Collections; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.UUID; + +public class EntityGroupListFilterTest extends AbstractEDQTest { + + private EntityGroup deviceGroup; + private EntityGroup deviceGroup2; + private EntityGroup dashboardGroup; + + @Before + public void setUp() { + deviceGroup = buildEntityGroup(EntityType.DEVICE, "thermostats"); + deviceGroup2 = buildEntityGroup(EntityType.DEVICE, "humidity-sensors"); + dashboardGroup = buildEntityGroup(EntityType.DASHBOARD, "device dashboards"); + addOrUpdate(EntityType.ENTITY_GROUP, deviceGroup); + addOrUpdate(EntityType.ENTITY_GROUP, deviceGroup2); + addOrUpdate(EntityType.ENTITY_GROUP, dashboardGroup); + } + + @After + public void tearDown() { + } + + @Test + public void testFindTenantEntityGroups() { + // get entity list + var result = repository.findEntityDataByQuery(tenantId, null, RepositoryUtils.ALL_READ_PERMISSIONS, getEntityGroupListDataQuery(EntityType.DEVICE, List.of(deviceGroup.getId().getId().toString()), null), false); + + Assert.assertEquals(1, result.getTotalElements()); + var first = result.getData().get(0); + Assert.assertEquals(deviceGroup.getId(), first.getEntityId()); + Assert.assertEquals("thermostats", first.getLatest().get(EntityKeyType.ENTITY_FIELD).get("name").getValue()); + Assert.assertEquals("42", first.getLatest().get(EntityKeyType.ENTITY_FIELD).get("createdTime").getValue()); + + result = repository.findEntityDataByQuery(tenantId, null, RepositoryUtils.ALL_READ_PERMISSIONS, getEntityGroupListDataQuery(EntityType.DEVICE,List.of(deviceGroup.getId().getId().toString(), deviceGroup2.getId().getId().toString()), null), false); + Assert.assertEquals(2, result.getTotalElements()); + + result = repository.findEntityDataByQuery(tenantId, null, RepositoryUtils.ALL_READ_PERMISSIONS, getEntityGroupListDataQuery(EntityType.DEVICE, List.of(UUID.randomUUID().toString()), null), false); + Assert.assertEquals(0, result.getTotalElements()); + + //add name filter + KeyFilter nameFilter = getNameKeyFilter(StringFilterPredicate.StringOperation.CONTAINS, "humidity"); + result = repository.findEntityDataByQuery(tenantId, null, RepositoryUtils.ALL_READ_PERMISSIONS, getEntityGroupListDataQuery(EntityType.DEVICE, List.of(UUID.randomUUID().toString()), Arrays.asList(nameFilter)), false); + Assert.assertEquals(0, result.getTotalElements()); + } + + @Test + public void testFindTenantEntityGroupsWithGroupPermissionOnly() { + MergedUserPermissions groupPermission = new MergedUserPermissions( + Map.of(Resource.DEVICE_GROUP, Set.of(Operation.ALL)), Map.of(deviceGroup.getId(), new MergedGroupPermissionInfo(EntityType.DEVICE, Set.of(Operation.ALL)))); + var result = repository.findEntityDataByQuery(tenantId, null, groupPermission, getEntityGroupListDataQuery(EntityType.DEVICE, List.of(deviceGroup.getId().getId().toString()), null), false); + + Assert.assertEquals(1, result.getTotalElements()); + } + + @Test + public void testFindCustomerEntityGroups() { + var result = repository.findEntityDataByQuery(tenantId, customerId, RepositoryUtils.ALL_READ_PERMISSIONS, getEntityGroupListDataQuery(EntityType.DEVICE, List.of(deviceGroup.getId().getId().toString()), null), false); + Assert.assertEquals(0, result.getTotalElements()); + + deviceGroup.setOwnerId(customerId); + addOrUpdate(EntityType.ENTITY_GROUP, deviceGroup); + + result = repository.findEntityDataByQuery(tenantId, customerId, RepositoryUtils.ALL_READ_PERMISSIONS, getEntityGroupListDataQuery(EntityType.DEVICE, List.of(deviceGroup.getId().getId().toString()), null), false); + + Assert.assertEquals(1, result.getTotalElements()); + var first = result.getData().get(0); + Assert.assertEquals(deviceGroup.getId(), first.getEntityId()); + Assert.assertEquals(deviceGroup.getName(), first.getLatest().get(EntityKeyType.ENTITY_FIELD).get("name").getValue()); + Assert.assertEquals(String.valueOf(deviceGroup.getCreatedTime()), first.getLatest().get(EntityKeyType.ENTITY_FIELD).get("createdTime").getValue()); + } + + @Test + public void testFindCustomerDeviceGroupWithGroupPermission() { + CustomerId subCustomer = new CustomerId(UUID.randomUUID()); + createCustomer(subCustomer.getId(), customerId.getId(), "Sub Customer A"); + + EntityGroup deviceGroup3 = buildEntityGroup(EntityType.DEVICE, "sensors A"); + deviceGroup3.setOwnerId(subCustomer); + addOrUpdate(EntityType.ENTITY_GROUP, deviceGroup3); + + MergedUserPermissions groupPermission = new MergedUserPermissions( + Collections.emptyMap(), Map.of(deviceGroup3.getId(), new MergedGroupPermissionInfo(EntityType.DEVICE, Set.of(Operation.ALL)))); + var result = repository.findEntityDataByQuery(tenantId, customerId, groupPermission, getEntityGroupListDataQuery(EntityType.DEVICE, List.of(deviceGroup3.getId().getId().toString()), null), false); + + Assert.assertEquals(1, result.getTotalElements()); + var first = result.getData().get(0); + Assert.assertEquals(deviceGroup3.getId(), first.getEntityId()); + Assert.assertEquals(deviceGroup3.getName(), first.getLatest().get(EntityKeyType.ENTITY_FIELD).get("name").getValue()); + Assert.assertEquals(String.valueOf(deviceGroup3.getCreatedTime()), first.getLatest().get(EntityKeyType.ENTITY_FIELD).get("createdTime").getValue()); + } + + @Test + public void testFindGroupWithGenericAndGroupPermission() { + CustomerId subCustomer = new CustomerId(UUID.randomUUID()); + createCustomer(subCustomer.getId(), customerId.getId(), "Sub Customer A"); + + UUID customerGroupId = createGroup(customerId.getId(), EntityType.DEVICE, "customer group"); + UUID subCustomerGroupId = createGroup(subCustomer.getId(), EntityType.DEVICE, "subcustomer group"); + + MergedUserPermissions groupPermission = new MergedUserPermissions( + Map.of(Resource.ALL, Set.of(Operation.ALL)), Map.of(new EntityGroupId(customerGroupId), new MergedGroupPermissionInfo(EntityType.DEVICE, Set.of(Operation.ALL)))); + var result = repository.findEntityDataByQuery(tenantId, subCustomer, groupPermission, getEntityGroupListDataQuery(EntityType.DEVICE, List.of(subCustomerGroupId.toString(), customerGroupId.toString()), null), false); + + Assert.assertEquals(2, result.getTotalElements()); + Assert.assertTrue(checkContains(result, customerGroupId)); + Assert.assertTrue(checkContains(result, subCustomerGroupId)); + } + + private EntityGroup buildEntityGroup(EntityType entityType, String name) { + EntityGroup entityGroup = new EntityGroup(); + entityGroup.setId(new EntityGroupId(UUID.randomUUID())); + entityGroup.setTenantId(tenantId); + entityGroup.setOwnerId(tenantId); + entityGroup.setName(name); + entityGroup.setType(entityType); + entityGroup.setCreatedTime(42L); + return entityGroup; + } + + private static EntityDataQuery getEntityGroupListDataQuery(EntityType entityType, List ids, List keyFilters) { + EntityGroupListFilter filter = new EntityGroupListFilter(); + filter.setGroupType(entityType); + filter.setEntityGroupList(ids); + var pageLink = new EntityDataPageLink(20, 0, null, new EntityDataSortOrder(new EntityKey(EntityKeyType.ENTITY_FIELD, "createdTime"), EntityDataSortOrder.Direction.DESC), false); + + var entityFields = Arrays.asList(new EntityKey(EntityKeyType.ENTITY_FIELD, "name"), new EntityKey(EntityKeyType.ENTITY_FIELD, "createdTime")); + + return new EntityDataQuery(filter, pageLink, entityFields, null, keyFilters); + } + + private static KeyFilter getNameKeyFilter(StringFilterPredicate.StringOperation operation, String value) { + KeyFilter nameFilter = new KeyFilter(); + nameFilter.setKey(new EntityKey(EntityKeyType.ENTITY_FIELD, "name")); + var predicate = new StringFilterPredicate(); + predicate.setIgnoreCase(false); + predicate.setOperation(operation); + predicate.setValue(new FilterPredicateValue<>(value)); + nameFilter.setPredicate(predicate); + nameFilter.setValueType(EntityKeyValueType.STRING); + return nameFilter; + } + +} diff --git a/edqs/src/test/java/org/thingsboard/server/edqs/repo/EntityGroupNameFilterTest.java b/edqs/src/test/java/org/thingsboard/server/edqs/repo/EntityGroupNameFilterTest.java new file mode 100644 index 0000000000..7ad296e946 --- /dev/null +++ b/edqs/src/test/java/org/thingsboard/server/edqs/repo/EntityGroupNameFilterTest.java @@ -0,0 +1,186 @@ +/** + * Copyright © 2016-2024 ThingsBoard, Inc. + * + * 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.edqs.repo; + +import org.junit.After; +import org.junit.Assert; +import org.junit.Before; +import org.junit.Test; +import org.thingsboard.server.common.data.EntityType; +import org.thingsboard.server.common.data.edqs.query.QueryResult; +import org.thingsboard.server.common.data.group.EntityGroup; +import org.thingsboard.server.common.data.id.CustomerId; +import org.thingsboard.server.common.data.id.EntityGroupId; +import org.thingsboard.server.common.data.permission.MergedGroupPermissionInfo; +import org.thingsboard.server.common.data.permission.MergedUserPermissions; +import org.thingsboard.server.common.data.permission.Operation; +import org.thingsboard.server.common.data.permission.Resource; +import org.thingsboard.server.common.data.query.EntityDataPageLink; +import org.thingsboard.server.common.data.query.EntityDataQuery; +import org.thingsboard.server.common.data.query.EntityDataSortOrder; +import org.thingsboard.server.common.data.query.EntityGroupNameFilter; +import org.thingsboard.server.common.data.query.EntityKey; +import org.thingsboard.server.common.data.query.EntityKeyType; +import org.thingsboard.server.common.data.query.EntityKeyValueType; +import org.thingsboard.server.common.data.query.FilterPredicateValue; +import org.thingsboard.server.common.data.query.KeyFilter; +import org.thingsboard.server.common.data.query.StringFilterPredicate; +import org.thingsboard.server.edqs.util.RepositoryUtils; + +import java.util.Arrays; +import java.util.Collections; +import java.util.List; +import java.util.Map; +import java.util.Optional; +import java.util.Set; +import java.util.UUID; + +import static org.assertj.core.api.Assertions.assertThat; + +public class EntityGroupNameFilterTest extends AbstractEDQTest { + + private EntityGroup deviceGroup; + private EntityGroup deviceGroup2; + private EntityGroup dashboardGroup; + + @Before + public void setUp() { + deviceGroup = buildEntityGroup(EntityType.DEVICE, "thermostats"); + deviceGroup2 = buildEntityGroup(EntityType.DEVICE, "thermostats 2"); + dashboardGroup = buildEntityGroup(EntityType.DASHBOARD, "device dashboards"); + addOrUpdate(EntityType.ENTITY_GROUP, deviceGroup); + addOrUpdate(EntityType.ENTITY_GROUP, deviceGroup2); + addOrUpdate(EntityType.ENTITY_GROUP, dashboardGroup); + } + + @After + public void tearDown() { + } + + @Test + public void testFindTenantEntityGroups() { + // get entity list + var result = repository.findEntityDataByQuery(tenantId, null, RepositoryUtils.ALL_READ_PERMISSIONS, getEntityGroupNameDataQuery(EntityType.DEVICE, "thermo", null), false); + + Assert.assertEquals(2, result.getTotalElements()); + Optional group = result.getData().stream().filter(queryResult -> queryResult.getLatest().get(EntityKeyType.ENTITY_FIELD).get("name").getValue().equals(deviceGroup.getName())).findFirst(); + assertThat(group).isPresent(); + var first = group.get(); + Assert.assertEquals(deviceGroup.getId(), first.getEntityId()); + Assert.assertEquals(deviceGroup.getName(), first.getLatest().get(EntityKeyType.ENTITY_FIELD).get("name").getValue()); + Assert.assertEquals(String.valueOf(deviceGroup.getCreatedTime()), first.getLatest().get(EntityKeyType.ENTITY_FIELD).get("createdTime").getValue()); + + result = repository.findEntityDataByQuery(tenantId, null, RepositoryUtils.ALL_READ_PERMISSIONS, getEntityGroupNameDataQuery(EntityType.DEVICE, "thermostats 2", null), false); + Assert.assertEquals(1, result.getTotalElements()); + + result = repository.findEntityDataByQuery(tenantId, null, RepositoryUtils.ALL_READ_PERMISSIONS, getEntityGroupNameDataQuery(EntityType.DEVICE, "humidity", null), false); + Assert.assertEquals(0, result.getTotalElements()); + + //add name filter + KeyFilter nameFilter = getNameKeyFilter(StringFilterPredicate.StringOperation.CONTAINS, "humidity"); + result = repository.findEntityDataByQuery(tenantId, null, RepositoryUtils.ALL_READ_PERMISSIONS, getEntityGroupNameDataQuery(EntityType.DEVICE, "thermo", List.of(nameFilter)), false); + Assert.assertEquals(0, result.getTotalElements()); + } + + @Test + public void testFindCustomerEntityGroups() { + var result = repository.findEntityDataByQuery(tenantId, customerId, RepositoryUtils.ALL_READ_PERMISSIONS, getEntityGroupNameDataQuery(EntityType.DEVICE, "thermo", null), false); + Assert.assertEquals(0, result.getTotalElements()); + + deviceGroup.setOwnerId(customerId); + addOrUpdate(EntityType.ENTITY_GROUP, deviceGroup); + + result = repository.findEntityDataByQuery(tenantId, customerId, RepositoryUtils.ALL_READ_PERMISSIONS, getEntityGroupNameDataQuery(EntityType.DEVICE, "thermo", null), false); + + Assert.assertEquals(1, result.getTotalElements()); + var first = result.getData().get(0); + Assert.assertEquals(deviceGroup.getId(), first.getEntityId()); + Assert.assertEquals(deviceGroup.getName(), first.getLatest().get(EntityKeyType.ENTITY_FIELD).get("name").getValue()); + Assert.assertEquals(String.valueOf(deviceGroup.getCreatedTime()), first.getLatest().get(EntityKeyType.ENTITY_FIELD).get("createdTime").getValue()); + } + + @Test + public void testFindCustomerDeviceGroupWithGroupPermission() { + CustomerId subCustomer = new CustomerId(UUID.randomUUID()); + createCustomer(subCustomer.getId(), customerId.getId(), "Sub Customer A"); + + EntityGroup deviceGroup3 = buildEntityGroup(EntityType.DEVICE, "sensors A"); + deviceGroup3.setOwnerId(subCustomer); + addOrUpdate(EntityType.ENTITY_GROUP, deviceGroup3); + + MergedUserPermissions groupPermission = new MergedUserPermissions( + Collections.emptyMap(), Map.of(deviceGroup3.getId(), new MergedGroupPermissionInfo(EntityType.DEVICE, Set.of(Operation.ALL)))); + var result = repository.findEntityDataByQuery(tenantId, customerId, groupPermission, getEntityGroupNameDataQuery(EntityType.DEVICE,"sensors", null), false); + + Assert.assertEquals(1, result.getTotalElements()); + var first = result.getData().get(0); + Assert.assertEquals(deviceGroup3.getId(), first.getEntityId()); + Assert.assertEquals(deviceGroup3.getName(), first.getLatest().get(EntityKeyType.ENTITY_FIELD).get("name").getValue()); + Assert.assertEquals(String.valueOf(deviceGroup3.getCreatedTime()), first.getLatest().get(EntityKeyType.ENTITY_FIELD).get("createdTime").getValue()); + } + + @Test + public void testFindGroupWithGenericAndGroupPermission() { + CustomerId subCustomer = new CustomerId(UUID.randomUUID()); + createCustomer(subCustomer.getId(), customerId.getId(), "Sub Customer A"); + + UUID customerGroupId = createGroup(customerId.getId(), EntityType.DEVICE, "sensors A"); + UUID subCustomerGroupId = createGroup(subCustomer.getId(), EntityType.DEVICE, "sensors A"); + + MergedUserPermissions groupPermission = new MergedUserPermissions( + Map.of(Resource.ALL, Set.of(Operation.ALL)), Map.of(new EntityGroupId(customerGroupId), new MergedGroupPermissionInfo(EntityType.DEVICE, Set.of(Operation.ALL)))); + var result = repository.findEntityDataByQuery(tenantId, subCustomer, groupPermission, + getEntityGroupNameDataQuery(EntityType.DEVICE, "sensors A", null), false); + + Assert.assertEquals(2, result.getTotalElements()); + Assert.assertTrue(checkContains(result, customerGroupId)); + Assert.assertTrue(checkContains(result, subCustomerGroupId)); + } + + private EntityGroup buildEntityGroup(EntityType entityType, String name) { + EntityGroup entityGroup = new EntityGroup(); + entityGroup.setId(new EntityGroupId(UUID.randomUUID())); + entityGroup.setTenantId(tenantId); + entityGroup.setOwnerId(tenantId); + entityGroup.setName(name); + entityGroup.setType(entityType); + entityGroup.setCreatedTime(42L); + return entityGroup; + } + + private static EntityDataQuery getEntityGroupNameDataQuery(EntityType entityType, String groupName, List keyFilters) { + EntityGroupNameFilter filter = new EntityGroupNameFilter(); + filter.setGroupType(entityType); + filter.setEntityGroupNameFilter(groupName); + var pageLink = new EntityDataPageLink(20, 0, null, new EntityDataSortOrder(new EntityKey(EntityKeyType.ENTITY_FIELD, "createdTime"), EntityDataSortOrder.Direction.DESC), false); + + var entityFields = Arrays.asList(new EntityKey(EntityKeyType.ENTITY_FIELD, "name"), new EntityKey(EntityKeyType.ENTITY_FIELD, "createdTime")); + return new EntityDataQuery(filter, pageLink, entityFields, null, keyFilters); + } + + private static KeyFilter getNameKeyFilter(StringFilterPredicate.StringOperation operation, String value) { + KeyFilter nameFilter = new KeyFilter(); + nameFilter.setKey(new EntityKey(EntityKeyType.ENTITY_FIELD, "name")); + var predicate = new StringFilterPredicate(); + predicate.setIgnoreCase(false); + predicate.setOperation(operation); + predicate.setValue(new FilterPredicateValue<>(value)); + nameFilter.setPredicate(predicate); + nameFilter.setValueType(EntityKeyValueType.STRING); + return nameFilter; + } + +} diff --git a/edqs/src/test/java/org/thingsboard/server/edqs/repo/EntityListFilterTest.java b/edqs/src/test/java/org/thingsboard/server/edqs/repo/EntityListFilterTest.java new file mode 100644 index 0000000000..d47252933f --- /dev/null +++ b/edqs/src/test/java/org/thingsboard/server/edqs/repo/EntityListFilterTest.java @@ -0,0 +1,202 @@ +/** + * Copyright © 2016-2024 ThingsBoard, Inc. + * + * 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.edqs.repo; + +import org.junit.After; +import org.junit.Assert; +import org.junit.Before; +import org.junit.Test; +import org.thingsboard.server.common.data.Device; +import org.thingsboard.server.common.data.EntityType; +import org.thingsboard.server.common.data.edqs.LatestTsKv; +import org.thingsboard.server.common.data.group.EntityGroup; +import org.thingsboard.server.common.data.id.CustomerId; +import org.thingsboard.server.common.data.id.DeviceId; +import org.thingsboard.server.common.data.id.DeviceProfileId; +import org.thingsboard.server.common.data.id.EntityGroupId; +import org.thingsboard.server.common.data.kv.BasicTsKvEntry; +import org.thingsboard.server.common.data.kv.BooleanDataEntry; +import org.thingsboard.server.common.data.kv.StringDataEntry; +import org.thingsboard.server.common.data.permission.MergedGroupPermissionInfo; +import org.thingsboard.server.common.data.permission.MergedUserPermissions; +import org.thingsboard.server.common.data.permission.Operation; +import org.thingsboard.server.common.data.permission.Resource; +import org.thingsboard.server.common.data.query.EntityDataPageLink; +import org.thingsboard.server.common.data.query.EntityDataQuery; +import org.thingsboard.server.common.data.query.EntityDataSortOrder; +import org.thingsboard.server.common.data.query.EntityKey; +import org.thingsboard.server.common.data.query.EntityKeyType; +import org.thingsboard.server.common.data.query.EntityKeyValueType; +import org.thingsboard.server.common.data.query.EntityListFilter; +import org.thingsboard.server.common.data.query.FilterPredicateValue; +import org.thingsboard.server.common.data.query.KeyFilter; +import org.thingsboard.server.common.data.query.StringFilterPredicate; +import org.thingsboard.server.common.data.relation.RelationTypeGroup; +import org.thingsboard.server.edqs.util.RepositoryUtils; + +import java.util.Arrays; +import java.util.Collections; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.UUID; + +public class EntityListFilterTest extends AbstractEDQTest { + + private Device device; + private Device device2; + private Device device3; + private EntityGroup deviceGroup; + + + @Before + public void setUp() { + device = buildDevice("LoRa-1"); + device2 = buildDevice("LoRa-2"); + device3 = buildDevice("Parking-Sensor-1"); + + addOrUpdate(EntityType.DEVICE, device); + addOrUpdate(EntityType.DEVICE, device2); + addOrUpdate(EntityType.DEVICE, device3); + + addOrUpdate(new LatestTsKv(device.getId(), new BasicTsKvEntry(43, new StringDataEntry("state", "enabled")), 0L)); + addOrUpdate(new LatestTsKv(device2.getId(), new BasicTsKvEntry(43, new StringDataEntry("state", "disabled")), 0L)); + addOrUpdate(new LatestTsKv(device3.getId(), new BasicTsKvEntry(43, new BooleanDataEntry("free", true)), 0L)); + } + + @After + public void tearDown() { + } + + @Test + public void testFindTenantDevice() { + // get entity list + var result = repository.findEntityDataByQuery(tenantId, null, RepositoryUtils.ALL_READ_PERMISSIONS, getEntityListDataQuery(EntityType.DEVICE, List.of(device.getId().getId().toString())), false); + + Assert.assertEquals(1, result.getTotalElements()); + var first = result.getData().get(0); + Assert.assertEquals(device.getId(), first.getEntityId()); + Assert.assertEquals("LoRa-1", first.getLatest().get(EntityKeyType.ENTITY_FIELD).get("name").getValue()); + Assert.assertEquals("42", first.getLatest().get(EntityKeyType.ENTITY_FIELD).get("createdTime").getValue()); + Assert.assertEquals("enabled", first.getLatest().get(EntityKeyType.TIME_SERIES).get("state").getValue()); + + result = repository.findEntityDataByQuery(tenantId, null, RepositoryUtils.ALL_READ_PERMISSIONS, getEntityListDataQuery(EntityType.DEVICE,List.of(device.getId().getId().toString(), device2.getId().getId().toString())), false); + Assert.assertEquals(2, result.getTotalElements()); + + result = repository.findEntityDataByQuery(tenantId, null, RepositoryUtils.ALL_READ_PERMISSIONS, getEntityListDataQuery(EntityType.DEVICE, List.of(UUID.randomUUID().toString())), false); + Assert.assertEquals(0, result.getTotalElements()); + } + + @Test + public void testFindCustomerDevice() { + var result = repository.findEntityDataByQuery(tenantId, customerId, RepositoryUtils.ALL_READ_PERMISSIONS,getEntityListDataQuery(EntityType.DEVICE, List.of(device.getId().getId().toString())), false); + Assert.assertEquals(0, result.getTotalElements()); + + device.setCustomerId(customerId); + addOrUpdate(EntityType.DEVICE, device); + + result = repository.findEntityDataByQuery(tenantId, customerId, RepositoryUtils.ALL_READ_PERMISSIONS, getEntityListDataQuery(EntityType.DEVICE, List.of(device.getId().getId().toString())), false); + + Assert.assertEquals(1, result.getTotalElements()); + var first = result.getData().get(0); + Assert.assertEquals(device.getId(), first.getEntityId()); + Assert.assertEquals("LoRa-1", first.getLatest().get(EntityKeyType.ENTITY_FIELD).get("name").getValue()); + Assert.assertEquals("42", first.getLatest().get(EntityKeyType.ENTITY_FIELD).get("createdTime").getValue()); + Assert.assertEquals("enabled", first.getLatest().get(EntityKeyType.TIME_SERIES).get("state").getValue()); + } + + @Test + public void testFindCustomerDeviceWithGroupPermission() { + CustomerId subCustomer = new CustomerId(UUID.randomUUID()); + createCustomer(subCustomer.getId(), customerId.getId(), "Sub Customer A"); + device.setCustomerId(subCustomer); + addOrUpdate(EntityType.DEVICE, device); + + // add device to customer device group + UUID deviceGroupId = createGroup(subCustomer.getId(), EntityType.DEVICE, "Group A"); + createRelation(EntityType.ENTITY_GROUP, deviceGroupId, EntityType.DEVICE, device.getUuidId(), RelationTypeGroup.FROM_ENTITY_GROUP, "Contains"); + + MergedUserPermissions groupPermission = new MergedUserPermissions( + Collections.emptyMap(), Map.of(new EntityGroupId(deviceGroupId), new MergedGroupPermissionInfo(EntityType.DEVICE, Set.of(Operation.ALL)))); + var result = repository.findEntityDataByQuery(tenantId, customerId, groupPermission, getEntityListDataQuery(EntityType.DEVICE, List.of(device.getId().getId().toString())), false); + + Assert.assertEquals(1, result.getTotalElements()); + var first = result.getData().get(0); + Assert.assertEquals(device.getId(), first.getEntityId()); + Assert.assertEquals("LoRa-1", first.getLatest().get(EntityKeyType.ENTITY_FIELD).get("name").getValue()); + Assert.assertEquals("42", first.getLatest().get(EntityKeyType.ENTITY_FIELD).get("createdTime").getValue()); + Assert.assertEquals("enabled", first.getLatest().get(EntityKeyType.TIME_SERIES).get("state").getValue()); + } + + @Test + public void testFindCustomerDeviceWithGenericAndGroupPermission() { + CustomerId subCustomer = new CustomerId(UUID.randomUUID()); + createCustomer(subCustomer.getId(), customerId.getId(), "Sub Customer A"); + device.setCustomerId(subCustomer); + addOrUpdate(EntityType.DEVICE, device); + + // add device to customer device group + UUID deviceGroupId = createGroup(subCustomer.getId(), EntityType.DEVICE, "Group A"); + createRelation(EntityType.ENTITY_GROUP, deviceGroupId, EntityType.DEVICE, device.getUuidId(), RelationTypeGroup.FROM_ENTITY_GROUP, "Contains"); + + MergedUserPermissions groupPermission = new MergedUserPermissions( + Map.of(Resource.ALL, Set.of(Operation.ALL)), Map.of(new EntityGroupId(deviceGroupId), new MergedGroupPermissionInfo(EntityType.DEVICE, Set.of(Operation.ALL)))); + var result = repository.findEntityDataByQuery(tenantId, customerId, groupPermission, getEntityListDataQuery(EntityType.DEVICE, List.of(device.getId().getId().toString())), false); + + Assert.assertEquals(1, result.getTotalElements()); + var first = result.getData().get(0); + Assert.assertEquals(device.getId(), first.getEntityId()); + Assert.assertEquals("LoRa-1", first.getLatest().get(EntityKeyType.ENTITY_FIELD).get("name").getValue()); + Assert.assertEquals("42", first.getLatest().get(EntityKeyType.ENTITY_FIELD).get("createdTime").getValue()); + Assert.assertEquals("enabled", first.getLatest().get(EntityKeyType.TIME_SERIES).get("state").getValue()); + } + + private Device buildDevice(String name) { + Device device = new Device(); + device.setId(new DeviceId(UUID.randomUUID())); + device.setTenantId(tenantId); + device.setName(name); + device.setCreatedTime(42L); + device.setDeviceProfileId(new DeviceProfileId(defaultDeviceProfileId)); + return device; + } + + private static EntityDataQuery getEntityListDataQuery(EntityType entityType, List ids) { + EntityListFilter filter = new EntityListFilter(); + filter.setEntityType(entityType); + filter.setEntityList(ids); + var pageLink = new EntityDataPageLink(20, 0, null, new EntityDataSortOrder(new EntityKey(EntityKeyType.TIME_SERIES, "state"), EntityDataSortOrder.Direction.DESC), false); + + var entityFields = Arrays.asList(new EntityKey(EntityKeyType.ENTITY_FIELD, "name"), new EntityKey(EntityKeyType.ENTITY_FIELD, "createdTime")); + var latestValues = Arrays.asList(new EntityKey(EntityKeyType.TIME_SERIES, "state")); + KeyFilter nameFilter = getNameKeyFilter(StringFilterPredicate.StringOperation.CONTAINS, "LoRa-"); + + return new EntityDataQuery(filter, pageLink, entityFields, latestValues, Arrays.asList(nameFilter)); + } + + private static KeyFilter getNameKeyFilter(StringFilterPredicate.StringOperation operation, String value) { + KeyFilter nameFilter = new KeyFilter(); + nameFilter.setKey(new EntityKey(EntityKeyType.ENTITY_FIELD, "name")); + var predicate = new StringFilterPredicate(); + predicate.setIgnoreCase(false); + predicate.setOperation(operation); + predicate.setValue(new FilterPredicateValue<>(value)); + nameFilter.setPredicate(predicate); + nameFilter.setValueType(EntityKeyValueType.STRING); + return nameFilter; + } + +} diff --git a/edqs/src/test/java/org/thingsboard/server/edqs/repo/EntityNameFilterTest.java b/edqs/src/test/java/org/thingsboard/server/edqs/repo/EntityNameFilterTest.java new file mode 100644 index 0000000000..8b17c30f2d --- /dev/null +++ b/edqs/src/test/java/org/thingsboard/server/edqs/repo/EntityNameFilterTest.java @@ -0,0 +1,132 @@ +/** + * Copyright © 2016-2024 ThingsBoard, Inc. + * + * 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.edqs.repo; + +import org.junit.After; +import org.junit.Assert; +import org.junit.Before; +import org.junit.Test; +import org.thingsboard.server.common.data.Device; +import org.thingsboard.server.common.data.EntityType; +import org.thingsboard.server.common.data.edqs.LatestTsKv; +import org.thingsboard.server.common.data.id.DeviceId; +import org.thingsboard.server.common.data.id.DeviceProfileId; +import org.thingsboard.server.common.data.kv.BasicTsKvEntry; +import org.thingsboard.server.common.data.kv.StringDataEntry; +import org.thingsboard.server.common.data.query.EntityDataPageLink; +import org.thingsboard.server.common.data.query.EntityDataQuery; +import org.thingsboard.server.common.data.query.EntityDataSortOrder; +import org.thingsboard.server.common.data.query.EntityKey; +import org.thingsboard.server.common.data.query.EntityKeyType; +import org.thingsboard.server.common.data.query.EntityKeyValueType; +import org.thingsboard.server.common.data.query.EntityNameFilter; +import org.thingsboard.server.common.data.query.FilterPredicateValue; +import org.thingsboard.server.common.data.query.KeyFilter; +import org.thingsboard.server.common.data.query.StringFilterPredicate; +import org.thingsboard.server.edqs.util.RepositoryUtils; + +import java.util.Arrays; +import java.util.UUID; + +public class EntityNameFilterTest extends AbstractEDQTest { + + @Before + public void setUp() { + } + + @After + public void tearDown() { + } + + @Test + public void testFindTenantDevice() { + DeviceId deviceId = new DeviceId(UUID.randomUUID()); + Device device = new Device(); + device.setId(deviceId); + device.setTenantId(tenantId); + device.setName("LoRa-1"); + device.setCreatedTime(42L); + device.setDeviceProfileId(new DeviceProfileId(defaultDeviceProfileId)); + addOrUpdate(EntityType.DEVICE, device); + + var result = repository.findEntityDataByQuery(tenantId, null, RepositoryUtils.ALL_READ_PERMISSIONS, getDeviceNameQuery("LoRa"), false); + + Assert.assertEquals(1, result.getTotalElements()); + var first = result.getData().get(0); + Assert.assertEquals(deviceId, first.getEntityId()); + Assert.assertEquals("LoRa-1", first.getLatest().get(EntityKeyType.ENTITY_FIELD).get("name").getValue()); + Assert.assertEquals("42", first.getLatest().get(EntityKeyType.ENTITY_FIELD).get("createdTime").getValue()); + + result = repository.findEntityDataByQuery(tenantId, null, RepositoryUtils.ALL_READ_PERMISSIONS, getDeviceNameQuery("Not LoRa"), false); + Assert.assertEquals(0, result.getTotalElements()); + + device.setCustomerId(customerId); + addOrUpdate(EntityType.DEVICE, device); + + result = repository.findEntityDataByQuery(tenantId, null, RepositoryUtils.ALL_READ_PERMISSIONS, getDeviceNameQuery("%1"), false); + Assert.assertEquals(1, result.getTotalElements()); + result = repository.findEntityDataByQuery(tenantId, null, RepositoryUtils.ALL_READ_PERMISSIONS, getDeviceNameQuery("L%"), false); + Assert.assertEquals(1, result.getTotalElements()); + } + + @Test + public void testFindCustomerDevice() { + DeviceId deviceId = new DeviceId(UUID.randomUUID()); + Device device = new Device(); + device.setId(deviceId); + device.setTenantId(tenantId); + device.setName("LoRa-1"); + device.setCreatedTime(42L); + device.setDeviceProfileId(new DeviceProfileId(defaultDeviceProfileId)); + addOrUpdate(EntityType.DEVICE, device); + addOrUpdate(new LatestTsKv(deviceId, new BasicTsKvEntry(43, new StringDataEntry("state", "TEST")), 0L)); + + var result = repository.findEntityDataByQuery(tenantId, customerId, RepositoryUtils.ALL_READ_PERMISSIONS, getDeviceNameQuery("LoRa"), false); + Assert.assertEquals(0, result.getTotalElements()); + + device.setCustomerId(customerId); + addOrUpdate(EntityType.DEVICE, device); + + result = repository.findEntityDataByQuery(tenantId, customerId, RepositoryUtils.ALL_READ_PERMISSIONS, getDeviceNameQuery("LoRa"), false); + + Assert.assertEquals(1, result.getTotalElements()); + var first = result.getData().get(0); + Assert.assertEquals(deviceId, first.getEntityId()); + Assert.assertEquals("LoRa-1", first.getLatest().get(EntityKeyType.ENTITY_FIELD).get("name").getValue()); + Assert.assertEquals("42", first.getLatest().get(EntityKeyType.ENTITY_FIELD).get("createdTime").getValue()); + } + + private static EntityDataQuery getDeviceNameQuery(String entityNameFilter) { + EntityNameFilter filter = new EntityNameFilter(); + filter.setEntityType(EntityType.DEVICE); + filter.setEntityNameFilter(entityNameFilter); + var pageLink = new EntityDataPageLink(20, 0, null, new EntityDataSortOrder(new EntityKey(EntityKeyType.TIME_SERIES, "state"), EntityDataSortOrder.Direction.DESC), false); + + var entityFields = Arrays.asList(new EntityKey(EntityKeyType.ENTITY_FIELD, "name"), new EntityKey(EntityKeyType.ENTITY_FIELD, "createdTime")); + var latestValues = Arrays.asList(new EntityKey(EntityKeyType.TIME_SERIES, "state")); + KeyFilter nameFilter = new KeyFilter(); + nameFilter.setKey(new EntityKey(EntityKeyType.ENTITY_FIELD, "name")); + var predicate = new StringFilterPredicate(); + predicate.setIgnoreCase(false); + predicate.setOperation(StringFilterPredicate.StringOperation.CONTAINS); + predicate.setValue(new FilterPredicateValue<>("LoRa-")); + nameFilter.setPredicate(predicate); + nameFilter.setValueType(EntityKeyValueType.STRING); + + return new EntityDataQuery(filter, pageLink, entityFields, latestValues, Arrays.asList(nameFilter)); + } + +} diff --git a/edqs/src/test/java/org/thingsboard/server/edqs/repo/EntityTypeFilterTest.java b/edqs/src/test/java/org/thingsboard/server/edqs/repo/EntityTypeFilterTest.java new file mode 100644 index 0000000000..a2b53b1f7d --- /dev/null +++ b/edqs/src/test/java/org/thingsboard/server/edqs/repo/EntityTypeFilterTest.java @@ -0,0 +1,147 @@ +/** + * Copyright © 2016-2024 ThingsBoard, Inc. + * + * 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.edqs.repo; + +import org.junit.After; +import org.junit.Assert; +import org.junit.Before; +import org.junit.Test; +import org.thingsboard.server.common.data.Device; +import org.thingsboard.server.common.data.EntityType; +import org.thingsboard.server.common.data.edqs.LatestTsKv; +import org.thingsboard.server.common.data.id.DeviceId; +import org.thingsboard.server.common.data.id.DeviceProfileId; +import org.thingsboard.server.common.data.kv.BasicTsKvEntry; +import org.thingsboard.server.common.data.kv.BooleanDataEntry; +import org.thingsboard.server.common.data.kv.StringDataEntry; +import org.thingsboard.server.common.data.query.EntityDataPageLink; +import org.thingsboard.server.common.data.query.EntityDataQuery; +import org.thingsboard.server.common.data.query.EntityDataSortOrder; +import org.thingsboard.server.common.data.query.EntityKey; +import org.thingsboard.server.common.data.query.EntityKeyType; +import org.thingsboard.server.common.data.query.EntityKeyValueType; +import org.thingsboard.server.common.data.query.EntityTypeFilter; +import org.thingsboard.server.common.data.query.FilterPredicateValue; +import org.thingsboard.server.common.data.query.KeyFilter; +import org.thingsboard.server.common.data.query.StringFilterPredicate; +import org.thingsboard.server.edqs.util.RepositoryUtils; + +import java.util.Arrays; +import java.util.List; +import java.util.UUID; + +import static org.assertj.core.api.Assertions.assertThat; + +public class EntityTypeFilterTest extends AbstractEDQTest { + + private Device device; + private Device device2; + private Device device3; + + @Before + public void setUp() { + device = buildDevice("LoRa-1"); + device2 = buildDevice("LoRa-2"); + device3 = buildDevice("Parking-Sensor-1"); + addOrUpdate(EntityType.DEVICE, device); + addOrUpdate(EntityType.DEVICE, device2); + addOrUpdate(EntityType.DEVICE, device3); + addOrUpdate(new LatestTsKv(device.getId(), new BasicTsKvEntry(43, new StringDataEntry("state", "enabled")), 0L)); + addOrUpdate(new LatestTsKv(device2.getId(), new BasicTsKvEntry(43, new StringDataEntry("state", "disabled")), 0L)); + addOrUpdate(new LatestTsKv(device3.getId(), new BasicTsKvEntry(43, new BooleanDataEntry("free", true)), 0L)); + } + + @After + public void tearDown() { + } + + @Test + public void testFindTenantDeviceEntities() { + // find all tenant devices + var result = repository.findEntityDataByQuery(tenantId, null, RepositoryUtils.ALL_READ_PERMISSIONS, getEntityTypeQuery(EntityType.DEVICE, null), false); + + Assert.assertEquals(3, result.getTotalElements()); + var first = result.getData().stream().filter(queryResult -> queryResult.getLatest().get(EntityKeyType.ENTITY_FIELD).get("name").getValue().equals("LoRa-1")).findAny(); + assertThat(first).isPresent(); + assertThat(first.get().getEntityId()).isEqualTo(device.getId()); + assertThat(first.get().getLatest().get(EntityKeyType.ENTITY_FIELD).get("createdTime").getValue()).isEqualTo(String.valueOf(device.getCreatedTime())); + assertThat(first.get().getLatest().get(EntityKeyType.TIME_SERIES).get("state").getValue()).isEqualTo("enabled"); + + // find all tenant devices with filter by name + KeyFilter keyFilter = getDeviceNameKeyFilter(StringFilterPredicate.StringOperation.CONTAINS, "Lora", true); + result = repository.findEntityDataByQuery(tenantId, null, RepositoryUtils.ALL_READ_PERMISSIONS, getEntityTypeQuery(EntityType.DEVICE, List.of(keyFilter)), false); + Assert.assertEquals(2, result.getTotalElements()); + + // find asset entities + result = repository.findEntityDataByQuery(tenantId, null, RepositoryUtils.ALL_READ_PERMISSIONS, getEntityTypeQuery(EntityType.ASSET, null), false); + Assert.assertEquals(0, result.getTotalElements()); + } + + @Test + public void testFindCustomerDeviceEntities() { + var result = repository.findEntityDataByQuery(tenantId, customerId, RepositoryUtils.ALL_READ_PERMISSIONS, getEntityTypeQuery(EntityType.DEVICE, null), false); + Assert.assertEquals(0, result.getTotalElements()); + + device.setCustomerId(customerId); + addOrUpdate(EntityType.DEVICE, device); + + result = repository.findEntityDataByQuery(tenantId, customerId, RepositoryUtils.ALL_READ_PERMISSIONS, getEntityTypeQuery(EntityType.DEVICE, null), false); + + Assert.assertEquals(1, result.getTotalElements()); + var first = result.getData().get(0); + Assert.assertEquals(device.getId(), first.getEntityId()); + Assert.assertEquals("LoRa-1", first.getLatest().get(EntityKeyType.ENTITY_FIELD).get("name").getValue()); + Assert.assertEquals("42", first.getLatest().get(EntityKeyType.ENTITY_FIELD).get("createdTime").getValue()); + Assert.assertEquals("enabled", first.getLatest().get(EntityKeyType.TIME_SERIES).get("state").getValue()); + + result = repository.findEntityDataByQuery(tenantId, customerId, RepositoryUtils.ALL_READ_PERMISSIONS, getEntityTypeQuery(EntityType.ASSET, null), false); + Assert.assertEquals(0, result.getTotalElements()); + } + + private Device buildDevice(String name) { + Device device = new Device(); + device.setId(new DeviceId(UUID.randomUUID())); + device.setTenantId(tenantId); + device.setName(name); + device.setCreatedTime(42L); + device.setDeviceProfileId(new DeviceProfileId(defaultDeviceProfileId)); + return device; + } + + private static EntityDataQuery getEntityTypeQuery(EntityType entityType, List keyFilters) { + EntityTypeFilter filter = new EntityTypeFilter(); + filter.setEntityType(entityType); + var pageLink = new EntityDataPageLink(20, 0, null, new EntityDataSortOrder(new EntityKey(EntityKeyType.ENTITY_FIELD, "createdTime"), EntityDataSortOrder.Direction.DESC), false); + + var entityFields = Arrays.asList(new EntityKey(EntityKeyType.ENTITY_FIELD, "name"), new EntityKey(EntityKeyType.ENTITY_FIELD, "createdTime")); + var latestValues = Arrays.asList(new EntityKey(EntityKeyType.TIME_SERIES, "state")); + + return new EntityDataQuery(filter, pageLink, entityFields, latestValues, keyFilters); + } + + private static KeyFilter getDeviceNameKeyFilter(StringFilterPredicate.StringOperation operation, String predicateValue, boolean ignoreCase) { + KeyFilter nameFilter = new KeyFilter(); + nameFilter.setKey(new EntityKey(EntityKeyType.ENTITY_FIELD, "name")); + var predicate = new StringFilterPredicate(); + predicate.setIgnoreCase(ignoreCase); + predicate.setOperation(operation); + predicate.setValue(new FilterPredicateValue<>(predicateValue)); + nameFilter.setPredicate(predicate); + nameFilter.setValueType(EntityKeyValueType.STRING); + return nameFilter; + } + +} diff --git a/edqs/src/test/java/org/thingsboard/server/edqs/repo/EntityViewSearchQueryFilterTest.java b/edqs/src/test/java/org/thingsboard/server/edqs/repo/EntityViewSearchQueryFilterTest.java new file mode 100644 index 0000000000..b984fe76b9 --- /dev/null +++ b/edqs/src/test/java/org/thingsboard/server/edqs/repo/EntityViewSearchQueryFilterTest.java @@ -0,0 +1,221 @@ +/** + * Copyright © 2016-2024 ThingsBoard, Inc. + * + * 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.edqs.repo; + +import org.junit.Assert; +import org.junit.Before; +import org.junit.Test; +import org.thingsboard.server.common.data.EntityType; +import org.thingsboard.server.common.data.edqs.query.QueryResult; +import org.thingsboard.server.common.data.id.AssetId; +import org.thingsboard.server.common.data.id.CustomerId; +import org.thingsboard.server.common.data.id.EntityGroupId; +import org.thingsboard.server.common.data.id.EntityId; +import org.thingsboard.server.common.data.page.PageData; +import org.thingsboard.server.common.data.permission.MergedGroupPermissionInfo; +import org.thingsboard.server.common.data.permission.MergedUserPermissions; +import org.thingsboard.server.common.data.permission.Operation; +import org.thingsboard.server.common.data.permission.Resource; +import org.thingsboard.server.common.data.query.EntityDataPageLink; +import org.thingsboard.server.common.data.query.EntityDataQuery; +import org.thingsboard.server.common.data.query.EntityKeyType; +import org.thingsboard.server.common.data.query.EntityViewSearchQueryFilter; +import org.thingsboard.server.common.data.query.KeyFilter; +import org.thingsboard.server.common.data.query.StringFilterPredicate; +import org.thingsboard.server.common.data.relation.EntitySearchDirection; +import org.thingsboard.server.common.data.relation.RelationTypeGroup; +import org.thingsboard.server.edqs.util.RepositoryUtils; + +import java.util.Arrays; +import java.util.Collections; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.UUID; + +public class EntityViewSearchQueryFilterTest extends AbstractEDQTest { + + @Before + public void setUp() { + } + + @Test + public void testFindTenantEntityViews() { + UUID asset1 = createAsset( "A1"); + UUID device1 = createDevice("D1"); + UUID device2 = createDevice("D2"); + UUID deviceView1 = createView("V1"); + UUID deviceView2 = createView("V2"); + + createRelation(EntityType.ASSET, asset1, EntityType.DEVICE, device1, "Contains"); + createRelation(EntityType.ASSET, asset1, EntityType.DEVICE, device2, "Contains"); + createRelation(EntityType.DEVICE, device1, EntityType.ENTITY_VIEW, deviceView1, "Contains"); + createRelation(EntityType.DEVICE, device2, EntityType.ENTITY_VIEW, deviceView2, "Contains"); + + // find all entity views of asset A1 + PageData relationsResult = findData(RepositoryUtils.ALL_READ_PERMISSIONS, null, new AssetId(asset1), + EntitySearchDirection.FROM, "Contains", 2, false, Arrays.asList("default")); + Assert.assertEquals(2, relationsResult.getData().size()); + Assert.assertTrue(checkContains(relationsResult, deviceView1)); + Assert.assertTrue(checkContains(relationsResult, deviceView2)); + + // find all entity views with max level = 1 + relationsResult = findData(RepositoryUtils.ALL_READ_PERMISSIONS, null, new AssetId(asset1), + EntitySearchDirection.FROM, "Contains", 1, false, Arrays.asList("default")); + Assert.assertEquals(0, relationsResult.getData().size()); + + // find all entity views with type "day 1" + relationsResult = findData(RepositoryUtils.ALL_READ_PERMISSIONS, null, new AssetId(asset1), + EntitySearchDirection.FROM, "Contains", 1, false, Arrays.asList("day 1")); + Assert.assertEquals(0, relationsResult.getData().size()); + + // find all entity views last level only, level = 2 + relationsResult = findData(RepositoryUtils.ALL_READ_PERMISSIONS, null, new AssetId(asset1), + EntitySearchDirection.FROM, "Contains", 2, true, Arrays.asList("default")); + Assert.assertEquals(2, relationsResult.getData().size()); + Assert.assertTrue(checkContains(relationsResult, deviceView1)); + Assert.assertTrue(checkContains(relationsResult, deviceView2)); + } + + @Test + public void testFindTenantDevicesWithGroupPermissionOnly() { + UUID eg1 = createGroup(EntityType.ENTITY_VIEW, "Group A"); + + UUID asset1 = createAsset( "A1"); + UUID device1 = createDevice("D1"); + UUID device2 = createDevice("D2"); + UUID deviceView1 = createView("V1"); + UUID deviceView2 = createView("V2"); + createRelation(EntityType.ENTITY_GROUP, eg1, EntityType.ENTITY_VIEW, deviceView1, RelationTypeGroup.FROM_ENTITY_GROUP, "Contains"); + + createRelation(EntityType.ASSET, asset1, EntityType.DEVICE, device1, "Contains"); + createRelation(EntityType.ASSET, asset1, EntityType.DEVICE, device2, "Contains"); + createRelation(EntityType.DEVICE, device1, EntityType.ENTITY_VIEW, deviceView1, "Contains"); + createRelation(EntityType.DEVICE, device2, EntityType.ENTITY_VIEW, deviceView2, "Contains"); + + // find all devices with group permission only + MergedUserPermissions readGroupPermissions = new MergedUserPermissions(Collections.emptyMap(), Collections.singletonMap(new EntityGroupId(eg1), + new MergedGroupPermissionInfo(EntityType.ENTITY_VIEW, new HashSet<>(Arrays.asList(Operation.READ, Operation.READ_ATTRIBUTES, Operation.READ_TELEMETRY))))); + PageData relationsResult = findData(readGroupPermissions, null, new AssetId(asset1), + EntitySearchDirection.FROM, "Contains", 2, false, Arrays.asList("default")); + Assert.assertEquals(1, relationsResult.getData().size()); + Assert.assertTrue(checkContains(relationsResult, deviceView1)); + } + + @Test + public void testFindCustomerDevices() { + UUID asset1 = createAsset(customerId.getId(), defaultAssetProfileId, "A1"); + UUID device1 = createDevice(customerId.getId(), defaultDeviceProfileId, "D1"); + UUID device2 = createDevice(customerId.getId(), defaultDeviceProfileId,"D2"); + UUID deviceView1 = createView(customerId.getId(),"day 1", "V1"); + UUID deviceView2 = createView(customerId.getId(), "day 1", "V2"); + + createRelation(EntityType.ASSET, asset1, EntityType.DEVICE, device1, "Contains"); + createRelation(EntityType.ASSET, asset1, EntityType.DEVICE, device2, "Contains"); + createRelation(EntityType.DEVICE, device1, EntityType.ENTITY_VIEW, deviceView1, "Contains"); + createRelation(EntityType.DEVICE, device2, EntityType.ENTITY_VIEW, deviceView2, "Contains"); + + // find all entity views of type "day 1" + PageData relationsResult = findData(RepositoryUtils.ALL_READ_PERMISSIONS, customerId, new AssetId(asset1), + EntitySearchDirection.FROM, "Contains", 2, false, Arrays.asList("day 1")); + Assert.assertEquals(2, relationsResult.getData().size()); + Assert.assertTrue(checkContains(relationsResult, deviceView1)); + Assert.assertTrue(checkContains(relationsResult, deviceView2)); + + // find all entity views of type "day 2" + relationsResult = findData(RepositoryUtils.ALL_READ_PERMISSIONS, customerId, new AssetId(asset1), + EntitySearchDirection.FROM, "Contains", 2, false, Arrays.asList("day 2")); + Assert.assertEquals(0, relationsResult.getData().size()); + + // find all entity views with other customer + relationsResult = findData(RepositoryUtils.ALL_READ_PERMISSIONS, new CustomerId(UUID.randomUUID()), new AssetId(asset1), + EntitySearchDirection.FROM, "Contains", 2, false, Arrays.asList("thermostat")); + Assert.assertEquals(0, relationsResult.getData().size()); + } + + @Test + public void testFindCustomerEntityViewsWithGroupPermission() { + UUID eg1 = createGroup(EntityType.ENTITY_VIEW, "Group A"); + + UUID asset1 = createAsset(customerId.getId(), defaultAssetProfileId, "A1"); + UUID device1 = createDevice(customerId.getId(), defaultDeviceProfileId, "D1"); + UUID device2 = createDevice(customerId.getId(), defaultDeviceProfileId,"D2"); + UUID deviceView1 = createView(customerId.getId(),"day 1", "V1"); + UUID deviceView2 = createView(customerId.getId(), "day 1", "V2"); + + createRelation(EntityType.ENTITY_GROUP, eg1, EntityType.ENTITY_VIEW, deviceView1, RelationTypeGroup.FROM_ENTITY_GROUP, "Contains"); + + createRelation(EntityType.ASSET, asset1, EntityType.DEVICE, device1, "Contains"); + createRelation(EntityType.ASSET, asset1, EntityType.DEVICE, device2, "Contains"); + createRelation(EntityType.DEVICE, device1, EntityType.ENTITY_VIEW, deviceView1, "Contains"); + createRelation(EntityType.DEVICE, device2, EntityType.ENTITY_VIEW, deviceView2, "Contains"); + + // find all entity views with group permission only + MergedUserPermissions readGroupPermissions = new MergedUserPermissions(Collections.emptyMap(), Collections.singletonMap(new EntityGroupId(eg1), + new MergedGroupPermissionInfo(EntityType.ENTITY_VIEW, new HashSet<>(Arrays.asList(Operation.READ, Operation.READ_ATTRIBUTES, Operation.READ_TELEMETRY))))); + PageData relationsResult = findData(readGroupPermissions, customerId, new AssetId(asset1), + EntitySearchDirection.FROM, "Contains", 2, false, Arrays.asList("day 1")); + Assert.assertEquals(1, relationsResult.getData().size()); + Assert.assertTrue(checkContains(relationsResult, deviceView1)); + } + + @Test + public void testFindCustomerAssetsWithGenericAndGroupPermission() { + CustomerId customerB = new CustomerId(UUID.randomUUID()); + createCustomer(customerB.getId(), customerId.getId(), "Customer B"); + + UUID eg1 = createGroup(customerB.getId(), EntityType.ENTITY_VIEW, "Group A"); + + UUID asset1 = createAsset(customerId.getId(), defaultAssetProfileId, "A1"); + UUID device1 = createDevice(customerId.getId(), defaultDeviceProfileId, "D1"); + UUID device2 = createDevice(customerB.getId(), defaultDeviceProfileId,"D2"); + UUID deviceView1 = createView(customerId.getId(),"day 1", "V1"); + UUID deviceView2 = createView(customerB.getId(), "day 1", "V2"); + + createRelation(EntityType.ENTITY_GROUP, eg1, EntityType.ENTITY_VIEW, deviceView1, RelationTypeGroup.FROM_ENTITY_GROUP, "Contains"); + + createRelation(EntityType.ASSET, asset1, EntityType.DEVICE, device1, "Contains"); + createRelation(EntityType.ASSET, asset1, EntityType.DEVICE, device2, "Contains"); + createRelation(EntityType.DEVICE, device1, EntityType.ENTITY_VIEW, deviceView1, "Contains"); + createRelation(EntityType.DEVICE, device2, EntityType.ENTITY_VIEW, deviceView2, "Contains"); + + // find all entity views with generic and group permission + MergedUserPermissions readGroupPermissions = new MergedUserPermissions(Map.of(Resource.ALL, Set.of(Operation.ALL)), Collections.singletonMap(new EntityGroupId(eg1), + new MergedGroupPermissionInfo(EntityType.ENTITY_VIEW, new HashSet<>(Arrays.asList(Operation.READ, Operation.READ_ATTRIBUTES, Operation.READ_TELEMETRY))))); + PageData relationsResult = findData(readGroupPermissions, customerId, new AssetId(asset1), + EntitySearchDirection.FROM, "Contains", 2, false, Arrays.asList("day 1")); + Assert.assertEquals(2, relationsResult.getData().size()); + Assert.assertTrue(checkContains(relationsResult, deviceView1)); + Assert.assertTrue(checkContains(relationsResult, deviceView2)); + } + + private PageData findData(MergedUserPermissions permissions, CustomerId customerId, EntityId rootId, + EntitySearchDirection direction, String relationType, int maxLevel, boolean lastLevelOnly, List entityViewTypes) { + EntityViewSearchQueryFilter filter = new EntityViewSearchQueryFilter(); + filter.setRootEntity(rootId); + filter.setDirection(direction); + filter.setRelationType(relationType); + filter.setEntityViewTypes(entityViewTypes); + filter.setFetchLastLevelOnly(lastLevelOnly); + filter.setMaxLevel(maxLevel); + EntityDataPageLink pageLink = new EntityDataPageLink(10, 0, null, null); + List keyFiltersEqualString = createStringKeyFilters("name", EntityKeyType.ENTITY_FIELD, StringFilterPredicate.StringOperation.STARTS_WITH, "V"); + EntityDataQuery query = new EntityDataQuery(filter, pageLink, Collections.emptyList(), Collections.emptyList(), keyFiltersEqualString); + return repository.findEntityDataByQuery(tenantId, customerId, permissions, query, false); + } + +} diff --git a/edqs/src/test/java/org/thingsboard/server/edqs/repo/EntityViewTypeFilterTest.java b/edqs/src/test/java/org/thingsboard/server/edqs/repo/EntityViewTypeFilterTest.java new file mode 100644 index 0000000000..4b086f1ef5 --- /dev/null +++ b/edqs/src/test/java/org/thingsboard/server/edqs/repo/EntityViewTypeFilterTest.java @@ -0,0 +1,177 @@ +/** + * Copyright © 2016-2024 ThingsBoard, Inc. + * + * 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.edqs.repo; + +import org.junit.After; +import org.junit.Assert; +import org.junit.Before; +import org.junit.Test; +import org.thingsboard.server.common.data.EntityType; +import org.thingsboard.server.common.data.EntityView; +import org.thingsboard.server.common.data.edqs.LatestTsKv; +import org.thingsboard.server.common.data.edqs.query.QueryResult; +import org.thingsboard.server.common.data.id.EntityViewId; +import org.thingsboard.server.common.data.kv.BasicTsKvEntry; +import org.thingsboard.server.common.data.kv.StringDataEntry; +import org.thingsboard.server.common.data.query.EntityDataPageLink; +import org.thingsboard.server.common.data.query.EntityDataQuery; +import org.thingsboard.server.common.data.query.EntityDataSortOrder; +import org.thingsboard.server.common.data.query.EntityKey; +import org.thingsboard.server.common.data.query.EntityKeyType; +import org.thingsboard.server.common.data.query.EntityKeyValueType; +import org.thingsboard.server.common.data.query.EntityViewTypeFilter; +import org.thingsboard.server.common.data.query.FilterPredicateValue; +import org.thingsboard.server.common.data.query.KeyFilter; +import org.thingsboard.server.common.data.query.StringFilterPredicate; +import org.thingsboard.server.edqs.util.RepositoryUtils; + +import java.util.Arrays; +import java.util.Collections; +import java.util.List; +import java.util.Optional; +import java.util.UUID; + +import static org.assertj.core.api.AssertionsForClassTypes.assertThat; + +public class EntityViewTypeFilterTest extends AbstractEDQTest { + + private EntityView entityView; + private EntityView entityView2; + private EntityView entityView3; + + + @Before + public void setUp() { + entityView = buildEntityView("day 1", "day 1 lora 1 view"); + entityView2 = buildEntityView("day 1", "day 1 lora 2 view"); + entityView3 = buildEntityView("day 2", "day 2 lora 1 view"); + addOrUpdate(EntityType.ENTITY_VIEW, entityView); + addOrUpdate(EntityType.ENTITY_VIEW, entityView2); + addOrUpdate(EntityType.ENTITY_VIEW, entityView3); + } + + @After + public void tearDown() { + } + + @Test + public void testFindTenantEntityView() { + // find entity view with type "day 1" + var result = repository.findEntityDataByQuery(tenantId, null, RepositoryUtils.ALL_READ_PERMISSIONS, getEntityViewTypeQuery(Collections.singletonList("day 1"), null, null), false); + + Assert.assertEquals(2, result.getTotalElements()); + Optional firstView = result.getData().stream().filter(queryResult -> queryResult.getLatest().get(EntityKeyType.ENTITY_FIELD).get("name").getValue().equals("day 1 lora 1 view")).findFirst(); + assertThat(firstView).isPresent(); + assertThat(firstView.get().getEntityId()).isEqualTo(entityView.getId()); + assertThat(firstView.get().getLatest().get(EntityKeyType.ENTITY_FIELD).get("createdTime").getValue()).isEqualTo(String.valueOf(entityView.getCreatedTime())); + + // find entity view with types "day 1" and "day 2" + result = repository.findEntityDataByQuery(tenantId, null, RepositoryUtils.ALL_READ_PERMISSIONS, getEntityViewTypeQuery(Arrays.asList("day 1", "day 2"), null, null), false); + + Assert.assertEquals(3, result.getTotalElements()); + Optional thirdView = result.getData().stream().filter(queryResult -> queryResult.getLatest().get(EntityKeyType.ENTITY_FIELD).get("name").getValue().equals("day 2 lora 1 view")).findFirst(); + assertThat(thirdView).isPresent(); + assertThat(thirdView.get().getEntityId()).isEqualTo(entityView3.getId()); + assertThat(thirdView.get().getLatest().get(EntityKeyType.ENTITY_FIELD).get("createdTime").getValue()).isEqualTo(String.valueOf(entityView.getCreatedTime())); + + // find entity view with type "day 3" + result = repository.findEntityDataByQuery(tenantId, null, RepositoryUtils.ALL_READ_PERMISSIONS, getEntityViewTypeQuery(Collections.singletonList("day 3"), null, null), false); + Assert.assertEquals(0, result.getTotalElements()); + + // find entity view with name "%Lora%" + result = repository.findEntityDataByQuery(tenantId, null, RepositoryUtils.ALL_READ_PERMISSIONS, getEntityViewTypeQuery(Collections.singletonList("day 1"), "%day 1 lora%", null), false); + Assert.assertEquals(2, result.getTotalElements()); + + // find entity view with name "Lora 1 device view" + result = repository.findEntityDataByQuery(tenantId, null, RepositoryUtils.ALL_READ_PERMISSIONS, getEntityViewTypeQuery(Collections.singletonList("day 1"), "day 1 lora 1 view", null), false); + Assert.assertEquals(1, result.getTotalElements()); + + // find entity view with name "%Parking sensor%" + result = repository.findEntityDataByQuery(tenantId, null, RepositoryUtils.ALL_READ_PERMISSIONS, getEntityViewTypeQuery(Collections.singletonList("day 1"), "%day 3 lora%", null), false); + Assert.assertEquals(0, result.getTotalElements()); + + // find entity view with key filter: name contains "Lora" + KeyFilter containsNameFilter = getEntityViewNameKeyFilter(StringFilterPredicate.StringOperation.CONTAINS, "Lora", true); + result = repository.findEntityDataByQuery(tenantId, null, RepositoryUtils.ALL_READ_PERMISSIONS, getEntityViewTypeQuery(Collections.singletonList("day 1"), null, Arrays.asList(containsNameFilter)), false); + Assert.assertEquals(2, result.getTotalElements()); + + // find entity view with key filter: name starts with "lora" and matches case + KeyFilter startsWithNameFilter = getEntityViewNameKeyFilter(StringFilterPredicate.StringOperation.STARTS_WITH, "lora", false); + result = repository.findEntityDataByQuery(tenantId, null, RepositoryUtils.ALL_READ_PERMISSIONS, getEntityViewTypeQuery(Collections.singletonList("day 1"), null, Arrays.asList(startsWithNameFilter)), false); + Assert.assertEquals(0, result.getTotalElements()); + } + + @Test + public void testFindCustomerEntityView() { + addOrUpdate(new LatestTsKv(entityView.getId(), new BasicTsKvEntry(43, new StringDataEntry("state", "TEST")), 0L)); + + var result = repository.findEntityDataByQuery(tenantId, customerId, RepositoryUtils.ALL_READ_PERMISSIONS, getEntityViewTypeQuery(Collections.singletonList("day 1"), null, null), false); + Assert.assertEquals(0, result.getTotalElements()); + + entityView.setCustomerId(customerId); + entityView2.setCustomerId(customerId); + entityView3.setCustomerId(customerId); + addOrUpdate(EntityType.ENTITY_VIEW, entityView); + addOrUpdate(EntityType.ENTITY_VIEW, entityView2); + addOrUpdate(EntityType.ENTITY_VIEW, entityView3); + + result = repository.findEntityDataByQuery(tenantId, customerId, RepositoryUtils.ALL_READ_PERMISSIONS, getEntityViewTypeQuery(Collections.singletonList("day 1"), null, null), false); + + Assert.assertEquals(2, result.getTotalElements()); + Optional firstView = result.getData().stream().filter(queryResult -> queryResult.getLatest().get(EntityKeyType.ENTITY_FIELD).get("name").getValue().equals("day 1 lora 1 view")).findFirst(); + assertThat(firstView).isPresent(); + assertThat(firstView.get().getEntityId()).isEqualTo(entityView.getId()); + assertThat(firstView.get().getLatest().get(EntityKeyType.ENTITY_FIELD).get("createdTime").getValue()).isEqualTo(String.valueOf(entityView.getCreatedTime())); + + result = repository.findEntityDataByQuery(tenantId, customerId, RepositoryUtils.ALL_READ_PERMISSIONS, getEntityViewTypeQuery(Collections.singletonList("day 3"), null, null), false); + Assert.assertEquals(0, result.getTotalElements()); + } + + private EntityView buildEntityView(String type, String name) { + EntityView entityView = new EntityView(); + entityView.setId(new EntityViewId(UUID.randomUUID())); + entityView.setTenantId(tenantId); + entityView.setType(type); + entityView.setName(name); + entityView.setCreatedTime(42L); + return entityView; + } + + private static EntityDataQuery getEntityViewTypeQuery(List assetTypes, String entityViewNameFilter, List keyFilters) { + EntityViewTypeFilter filter = new EntityViewTypeFilter(); + filter.setEntityViewTypes(assetTypes); + filter.setEntityViewNameFilter(entityViewNameFilter); + var pageLink = new EntityDataPageLink(20, 0, null, new EntityDataSortOrder(new EntityKey(EntityKeyType.ENTITY_FIELD, "createdTime"), EntityDataSortOrder.Direction.DESC), false); + + var entityFields = Arrays.asList(new EntityKey(EntityKeyType.ENTITY_FIELD, "name"), new EntityKey(EntityKeyType.ENTITY_FIELD, "createdTime")); + var latestValues = Arrays.asList(new EntityKey(EntityKeyType.TIME_SERIES, "state")); + + return new EntityDataQuery(filter, pageLink, entityFields, latestValues, keyFilters); + } + + private static KeyFilter getEntityViewNameKeyFilter(StringFilterPredicate.StringOperation operation, String predicateValue, boolean ignoreCase) { + KeyFilter nameFilter = new KeyFilter(); + nameFilter.setKey(new EntityKey(EntityKeyType.ENTITY_FIELD, "name")); + var predicate = new StringFilterPredicate(); + predicate.setIgnoreCase(ignoreCase); + predicate.setOperation(operation); + predicate.setValue(new FilterPredicateValue<>(predicateValue)); + nameFilter.setPredicate(predicate); + nameFilter.setValueType(EntityKeyValueType.STRING); + return nameFilter; + } + +} diff --git a/edqs/src/test/java/org/thingsboard/server/edqs/repo/RelationsQueryFilterTest.java b/edqs/src/test/java/org/thingsboard/server/edqs/repo/RelationsQueryFilterTest.java new file mode 100644 index 0000000000..b0a90d9dce --- /dev/null +++ b/edqs/src/test/java/org/thingsboard/server/edqs/repo/RelationsQueryFilterTest.java @@ -0,0 +1,233 @@ +/** + * Copyright © 2016-2024 ThingsBoard, Inc. + * + * 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.edqs.repo; + +import org.junit.Assert; +import org.junit.Before; +import org.junit.Test; +import org.thingsboard.server.common.data.EntityType; +import org.thingsboard.server.common.data.edqs.query.QueryResult; +import org.thingsboard.server.common.data.id.AssetId; +import org.thingsboard.server.common.data.id.CustomerId; +import org.thingsboard.server.common.data.id.EntityGroupId; +import org.thingsboard.server.common.data.id.EntityId; +import org.thingsboard.server.common.data.page.PageData; +import org.thingsboard.server.common.data.permission.MergedGroupPermissionInfo; +import org.thingsboard.server.common.data.permission.MergedUserPermissions; +import org.thingsboard.server.common.data.permission.Operation; +import org.thingsboard.server.common.data.query.EntityDataPageLink; +import org.thingsboard.server.common.data.query.EntityDataQuery; +import org.thingsboard.server.common.data.query.EntityKeyType; +import org.thingsboard.server.common.data.query.KeyFilter; +import org.thingsboard.server.common.data.query.RelationsQueryFilter; +import org.thingsboard.server.common.data.query.StringFilterPredicate; +import org.thingsboard.server.common.data.relation.EntitySearchDirection; +import org.thingsboard.server.common.data.relation.RelationEntityTypeFilter; +import org.thingsboard.server.common.data.relation.RelationTypeGroup; +import org.thingsboard.server.edqs.util.RepositoryUtils; + +import java.util.Arrays; +import java.util.Collections; +import java.util.HashSet; +import java.util.List; +import java.util.UUID; + +public class RelationsQueryFilterTest extends AbstractEDQTest { + + @Before + public void setUp() { + } + + @Test + public void testFindTenantDevices() { + UUID ta1 = createAsset("T A1"); + UUID ta2 = createAsset("T A2"); + UUID da1 = createDevice("T D1"); + UUID da2 = createDevice(customerId, "T D2"); + UUID da3 = createDevice("NOT MATCHING D3"); + + // A1 --Contains--> A2, A1 --Contains--> D1. A1 --Manages--> D2. + createRelation(EntityType.ASSET, ta1, EntityType.ASSET, ta2, "Contains"); + createRelation(EntityType.ASSET, ta1, EntityType.DEVICE, da1, "Contains"); + createRelation(EntityType.ASSET, ta1, EntityType.DEVICE, da2, "Manages"); + createRelation(EntityType.ASSET, ta1, EntityType.DEVICE, da3, "Contains"); + + PageData relationsResult = filter(new AssetId(ta1), new RelationEntityTypeFilter("Contains", Arrays.asList(EntityType.DEVICE, EntityType.ASSET))); + Assert.assertEquals(2, relationsResult.getData().size()); + Assert.assertTrue(checkContains(relationsResult, ta2)); + Assert.assertTrue(checkContains(relationsResult, da1)); + + relationsResult = filter(new AssetId(ta1), new RelationEntityTypeFilter("Manages", Arrays.asList(EntityType.DEVICE, EntityType.ASSET))); + Assert.assertEquals(1, relationsResult.getData().size()); + Assert.assertTrue(checkContains(relationsResult, da2)); + } + + @Test + public void testFindTenantDevicesLastLevelOnly() { + UUID root = createAsset("T ROOT"); + + UUID ta1 = createAsset("T A1 NO MORE RELATIONS"); + UUID ta2 = createAsset("T A2"); + UUID da1 = createDevice("T D1"); + UUID da2 = createDevice(customerId, "T D2"); + UUID da3 = createDevice(customerId, "T D3"); + UUID da4 = createDevice(customerId, "T D4"); // Lvl 4 + + // ROOT --Contains--> A1, A2; A2 --Contains--> D1, D2; D2 --Contains--> D3. + createRelation(EntityType.ASSET, root, EntityType.ASSET, ta1, "Contains"); + createRelation(EntityType.ASSET, root, EntityType.ASSET, ta2, "Contains"); + createRelation(EntityType.ASSET, ta2, EntityType.DEVICE, da1, "Contains"); + createRelation(EntityType.ASSET, ta2, EntityType.DEVICE, da2, "Contains"); + createRelation(EntityType.ASSET, da2, EntityType.DEVICE, da3, "Contains"); + createRelation(EntityType.ASSET, da3, EntityType.DEVICE, da4, "Contains"); + + PageData relationsResult = filter(RepositoryUtils.ALL_READ_PERMISSIONS, null, new AssetId(root), 1, true, + new RelationEntityTypeFilter("Contains", Arrays.asList(EntityType.DEVICE, EntityType.ASSET))); + Assert.assertEquals(2, relationsResult.getData().size()); + Assert.assertTrue(checkContains(relationsResult, ta1)); + Assert.assertTrue(checkContains(relationsResult, ta2)); + + relationsResult = filter(RepositoryUtils.ALL_READ_PERMISSIONS, null, new AssetId(root), 2, true, + new RelationEntityTypeFilter("Contains", Arrays.asList(EntityType.DEVICE, EntityType.ASSET))); + Assert.assertEquals(3, relationsResult.getData().size()); + Assert.assertTrue(checkContains(relationsResult, ta1)); + Assert.assertTrue(checkContains(relationsResult, da1)); + Assert.assertTrue(checkContains(relationsResult, da2)); + + relationsResult = filter(RepositoryUtils.ALL_READ_PERMISSIONS, null, new AssetId(root), 3, true, + new RelationEntityTypeFilter("Contains", Arrays.asList(EntityType.DEVICE, EntityType.ASSET))); + Assert.assertEquals(3, relationsResult.getData().size()); + Assert.assertTrue(checkContains(relationsResult, ta1)); + Assert.assertTrue(checkContains(relationsResult, da1)); + Assert.assertTrue(checkContains(relationsResult, da3)); + + relationsResult = filter(RepositoryUtils.ALL_READ_PERMISSIONS, null, new AssetId(root), 4, true, + new RelationEntityTypeFilter("Contains", Arrays.asList(EntityType.DEVICE, EntityType.ASSET))); + Assert.assertEquals(3, relationsResult.getData().size()); + Assert.assertTrue(checkContains(relationsResult, ta1)); + Assert.assertTrue(checkContains(relationsResult, da1)); + Assert.assertTrue(checkContains(relationsResult, da4)); + + } + + @Test + public void testFindTenantDevicesGroupsOnly() { + UUID ta1 = createAsset("T A1"); + UUID ta2 = createAsset("T A2"); + UUID da1 = createDevice("T D1"); + UUID da2 = createDevice(customerId, "T D2"); + + UUID eg1 = createGroup(EntityType.DEVICE, "Group A"); + createRelation(EntityType.ENTITY_GROUP, eg1, EntityType.DEVICE, da1, RelationTypeGroup.FROM_ENTITY_GROUP, "Contains"); + + // A1 --Contains--> A2, A1 --Contains--> D1. A1 --Manages--> D2. + createRelation(EntityType.ASSET, ta1, EntityType.ASSET, ta2, "Contains"); + createRelation(EntityType.ASSET, ta1, EntityType.DEVICE, da1, "Contains"); + createRelation(EntityType.ASSET, ta1, EntityType.DEVICE, da2, "Manages"); + + MergedUserPermissions readGroupPermissions = new MergedUserPermissions(Collections.emptyMap(), Collections.singletonMap(new EntityGroupId(eg1), + new MergedGroupPermissionInfo(EntityType.DEVICE, new HashSet<>(Arrays.asList(Operation.READ, Operation.READ_ATTRIBUTES, Operation.READ_TELEMETRY))))); + + PageData relationsResult = filter(readGroupPermissions, new AssetId(ta1), new RelationEntityTypeFilter("Contains", Arrays.asList(EntityType.DEVICE, EntityType.ASSET))); + Assert.assertEquals(1, relationsResult.getData().size()); + Assert.assertTrue(checkContains(relationsResult, da1)); + + relationsResult = filter(readGroupPermissions, new AssetId(ta1), new RelationEntityTypeFilter("Manages", Arrays.asList(EntityType.DEVICE, EntityType.ASSET))); + Assert.assertEquals(0, relationsResult.getData().size()); + } + + @Test + public void testFindCustomerDevices() { + UUID ta1 = createAsset("T A1"); + UUID ta2 = createAsset("T A2"); + UUID da1 = createDevice(customerId, "T D1"); + UUID da2 = createDevice("T D2"); + + // A1 --Contains--> A2, A1 --Contains--> D1. A1 --Manages--> D2. + createRelation(EntityType.ASSET, ta1, EntityType.ASSET, ta2, "Contains"); + createRelation(EntityType.ASSET, ta1, EntityType.DEVICE, da1, "Contains"); + createRelation(EntityType.ASSET, ta1, EntityType.DEVICE, da2, "Manages"); + + PageData relationsResult = filter(customerId, new AssetId(ta1), new RelationEntityTypeFilter("Contains", Arrays.asList(EntityType.DEVICE, EntityType.ASSET))); + Assert.assertEquals(1, relationsResult.getData().size()); + Assert.assertTrue(checkContains(relationsResult, da1)); + + relationsResult = filter(customerId, new AssetId(ta1), new RelationEntityTypeFilter("Manages", Arrays.asList(EntityType.DEVICE, EntityType.ASSET))); + Assert.assertEquals(0, relationsResult.getData().size()); + } + + @Test + public void testFindCustomerDevicesGroupsOnly() { + UUID ta1 = createAsset("T A1"); + UUID ta2 = createAsset("T A2"); + UUID da1 = createDevice(customerId, "T D1"); + UUID da2 = createDevice(customerId, "T D2"); + UUID da3 = createDevice(customerId, "T D3"); + + UUID eg1 = createGroup(EntityType.DEVICE, "Group A"); + createRelation(EntityType.ENTITY_GROUP, eg1, EntityType.DEVICE, da1, RelationTypeGroup.FROM_ENTITY_GROUP, "Contains"); + createRelation(EntityType.ENTITY_GROUP, eg1, EntityType.DEVICE, da2, RelationTypeGroup.FROM_ENTITY_GROUP, "Contains"); + createRelation(EntityType.ENTITY_GROUP, eg1, EntityType.DEVICE, da3, RelationTypeGroup.FROM_ENTITY_GROUP, "Contains"); + + // A1 --Contains--> A2, A1 --Contains--> D1. A1 --Manages--> D2. + createRelation(EntityType.ASSET, ta1, EntityType.ASSET, ta2, "Contains"); + createRelation(EntityType.ASSET, ta1, EntityType.DEVICE, da1, "Contains"); + createRelation(EntityType.ASSET, ta1, EntityType.DEVICE, da2, "Manages"); + createRelation(EntityType.DEVICE, da2, EntityType.DEVICE, da3, "Contains"); + + MergedUserPermissions readGroupPermissions = new MergedUserPermissions(Collections.emptyMap(), Collections.singletonMap(new EntityGroupId(eg1), + new MergedGroupPermissionInfo(EntityType.DEVICE, new HashSet<>(Arrays.asList(Operation.READ, Operation.READ_ATTRIBUTES, Operation.READ_TELEMETRY))))); + + PageData relationsResult = filter(readGroupPermissions, customerId, new AssetId(ta1), new RelationEntityTypeFilter("Contains", Arrays.asList(EntityType.DEVICE, EntityType.ASSET))); + Assert.assertEquals(2, relationsResult.getData().size()); + Assert.assertTrue(checkContains(relationsResult, da1)); + Assert.assertTrue(checkContains(relationsResult, da3)); + + relationsResult = filter(readGroupPermissions, customerId, new AssetId(ta1), new RelationEntityTypeFilter("Manages", Arrays.asList(EntityType.DEVICE, EntityType.ASSET))); + Assert.assertEquals(1, relationsResult.getData().size()); + Assert.assertTrue(checkContains(relationsResult, da2)); + } + + private PageData filter(EntityId rootId, RelationEntityTypeFilter... relationEntityTypeFilters) { + return filter(RepositoryUtils.ALL_READ_PERMISSIONS, rootId, relationEntityTypeFilters); + } + + private PageData filter(MergedUserPermissions permissions, EntityId rootId, RelationEntityTypeFilter... relationEntityTypeFilters) { + return filter(permissions, null, rootId, relationEntityTypeFilters); + } + + private PageData filter(CustomerId customerId, EntityId rootId, RelationEntityTypeFilter... relationEntityTypeFilters) { + return filter(RepositoryUtils.ALL_READ_PERMISSIONS, customerId, rootId, relationEntityTypeFilters); + } + + private PageData filter(MergedUserPermissions permissions, CustomerId customerId, EntityId rootId, RelationEntityTypeFilter... relationEntityTypeFilters) { + return filter(permissions, customerId, rootId, 3, false, relationEntityTypeFilters); + } + + private PageData filter(MergedUserPermissions permissions, CustomerId customerId, EntityId rootId, int maxLevel, boolean lastLevelOnly, RelationEntityTypeFilter... relationEntityTypeFilters) { + RelationsQueryFilter filter = new RelationsQueryFilter(); + filter.setRootEntity(rootId); + filter.setFilters(Arrays.asList(relationEntityTypeFilters)); + filter.setDirection(EntitySearchDirection.FROM); + filter.setFetchLastLevelOnly(lastLevelOnly); + filter.setMaxLevel(maxLevel); + EntityDataPageLink pageLink = new EntityDataPageLink(10, 0, null, null); + List keyFiltersEqualString = createStringKeyFilters("name", EntityKeyType.ENTITY_FIELD, StringFilterPredicate.StringOperation.STARTS_WITH, "T"); + EntityDataQuery query = new EntityDataQuery(filter, pageLink, Collections.emptyList(), Collections.emptyList(), keyFiltersEqualString); + return repository.findEntityDataByQuery(tenantId, customerId, permissions, query, false); + } + +} diff --git a/edqs/src/test/java/org/thingsboard/server/edqs/repo/RepositoryUtilsTest.java b/edqs/src/test/java/org/thingsboard/server/edqs/repo/RepositoryUtilsTest.java new file mode 100644 index 0000000000..42706a796a --- /dev/null +++ b/edqs/src/test/java/org/thingsboard/server/edqs/repo/RepositoryUtilsTest.java @@ -0,0 +1,434 @@ +/** + * Copyright © 2016-2024 ThingsBoard, Inc. + * + * 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.edqs.repo; + +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.Arguments; +import org.junit.jupiter.params.provider.MethodSource; +import org.thingsboard.server.common.data.AttributeScope; +import org.thingsboard.server.common.data.EntityType; +import org.thingsboard.server.common.data.StringUtils; +import org.thingsboard.server.common.data.edqs.fields.DeviceFields; +import org.thingsboard.server.common.data.edqs.fields.DeviceProfileFields; +import org.thingsboard.server.common.data.query.BooleanFilterPredicate; +import org.thingsboard.server.common.data.query.BooleanFilterPredicate.BooleanOperation; +import org.thingsboard.server.common.data.query.ComplexFilterPredicate; +import org.thingsboard.server.common.data.query.ComplexFilterPredicate.ComplexOperation; +import org.thingsboard.server.common.data.query.EntityKeyType; +import org.thingsboard.server.common.data.query.EntityKeyValueType; +import org.thingsboard.server.common.data.query.FilterPredicateValue; +import org.thingsboard.server.common.data.query.NumericFilterPredicate; +import org.thingsboard.server.common.data.query.NumericFilterPredicate.NumericOperation; +import org.thingsboard.server.common.data.query.StringFilterPredicate; +import org.thingsboard.server.common.data.query.StringFilterPredicate.StringOperation; +import org.thingsboard.server.edqs.data.DeviceData; +import org.thingsboard.server.edqs.data.EntityProfileData; +import org.thingsboard.server.edqs.data.dp.BoolDataPoint; +import org.thingsboard.server.edqs.data.dp.DoubleDataPoint; +import org.thingsboard.server.edqs.data.dp.LongDataPoint; +import org.thingsboard.server.edqs.data.dp.StringDataPoint; +import org.thingsboard.server.edqs.query.DataKey; +import org.thingsboard.server.edqs.query.EdqsFilter; +import org.thingsboard.server.edqs.util.RepositoryUtils; + +import java.util.List; +import java.util.UUID; +import java.util.stream.Stream; + +import static org.assertj.core.api.Assertions.assertThat; + + +public class RepositoryUtilsTest { + + private static Stream deviceNameFilters() { + return Stream.of(Arguments.of(null, getNameFilter(StringOperation.STARTS_WITH, "lora"), true), + Arguments.of("loranet device 123", getNameFilter(StringOperation.STARTS_WITH, "lora"), true), + Arguments.of("loranet 123", getNameFilter(StringOperation.STARTS_WITH, "ra"), false), + Arguments.of("loranet 123", getNameFilter(StringOperation.ENDS_WITH, "123"), true), + Arguments.of("loranet 123", getNameFilter(StringOperation.ENDS_WITH, "device"), false), + Arguments.of("loranet 123", getNameFilter(StringOperation.EQUAL, "loranet 123"), true), + Arguments.of("loranet 123", getNameFilter(StringOperation.EQUAL, "loranet "), false), + Arguments.of("loranet 123", getNameFilter(StringOperation.NOT_EQUAL, "loranet"), true), + Arguments.of("loranet 123", getNameFilter(StringOperation.NOT_EQUAL, "loranet 123"), false), + Arguments.of("loranet 123", getNameFilter(StringOperation.CONTAINS, "loranet"), true), + Arguments.of("loranet 123", getNameFilter(StringOperation.CONTAINS, "loranet123"), false), + Arguments.of("loranet 123", getNameFilter(StringOperation.NOT_CONTAINS, "loranet123"), true), + Arguments.of("loranet 123", getNameFilter(StringOperation.NOT_CONTAINS, "loranet"), false), + Arguments.of("loranet 123", getNameFilter(StringOperation.IN, "loranet 123, loranet 124"), true), + Arguments.of("loranet 123", getNameFilter(StringOperation.IN, "loranet 125, loranet 126"), false), + Arguments.of("loranet 123", getNameFilter(StringOperation.NOT_IN, "loranet 125, loranet 126"), true), + Arguments.of("loranet 123", getNameFilter(StringOperation.NOT_IN, "loranet 123, loranet 126"), false) + ); + } + + @ParameterizedTest + @MethodSource("deviceNameFilters") + public void testFilterByDeviceName(String deviceName, EdqsFilter keyFilter, boolean result) { + DeviceData deviceData = new DeviceData(UUID.randomUUID()); + deviceData.setCustomerId(UUID.randomUUID()); + deviceData.setFields(DeviceFields.builder().name(deviceName).build()); + assertThat(RepositoryUtils.checkKeyFilters(deviceData, List.of(keyFilter))).isEqualTo(result); + } + + private static Stream createdTimeFilters() { + return Stream.of(Arguments.of(1000, getCreatedTimeFilter(NumericOperation.EQUAL, 1000), true), + Arguments.of(1000, getCreatedTimeFilter(NumericOperation.EQUAL, 1001), false), + Arguments.of(1000, getCreatedTimeFilter(NumericOperation.NOT_EQUAL, 1000), false), + Arguments.of(1000, getCreatedTimeFilter(NumericOperation.NOT_EQUAL, 1001), true), + Arguments.of(1000, getCreatedTimeFilter(NumericOperation.GREATER, 999), true), + Arguments.of(1000, getCreatedTimeFilter(NumericOperation.GREATER, 1000), false), + Arguments.of(1000, getCreatedTimeFilter(NumericOperation.GREATER_OR_EQUAL, 1000), true), + Arguments.of(1000, getCreatedTimeFilter(NumericOperation.GREATER_OR_EQUAL, 1001), false), + Arguments.of(1000, getCreatedTimeFilter(NumericOperation.LESS, 1001), true), + Arguments.of(1000, getCreatedTimeFilter(NumericOperation.LESS, 1000), false), + Arguments.of(1000, getCreatedTimeFilter(NumericOperation.LESS_OR_EQUAL, 1000), true), + Arguments.of(1000, getCreatedTimeFilter(NumericOperation.LESS_OR_EQUAL, 999), false) + ); + } + + @ParameterizedTest + @MethodSource("createdTimeFilters") + public void testFilterDevicesByCreatedTime(long createdTime, EdqsFilter keyFilter, boolean result) { + DeviceData deviceData = new DeviceData(UUID.randomUUID()); + deviceData.setCustomerId(UUID.randomUUID()); + deviceData.setFields(DeviceFields.builder().createdTime(createdTime).build()); + + assertThat(RepositoryUtils.checkKeyFilters(deviceData, List.of(keyFilter))).isEqualTo(result); + } + + private static Stream deviceNameAndTypeFilter() { + return Stream.of( + Arguments.of("loranet 123", "thermostat", List.of(getNameFilter(StringOperation.STARTS_WITH, "lo"), getTypeFilter(StringOperation.EQUAL, "thermostat")), true), + Arguments.of("loranet 123", "thermostat", List.of(getNameFilter(StringOperation.STARTS_WITH, "net"), getTypeFilter(StringOperation.EQUAL, "thermostat")), false), + Arguments.of("loranet 123", "thermostat", List.of(getNameFilter(StringOperation.STARTS_WITH, "lo"), getTypeFilter(StringOperation.EQUAL, "sensor1")), false), + Arguments.of("loranet 123", "thermostat", List.of(getNameFilter(StringOperation.STARTS_WITH, "net"), getTypeFilter(StringOperation.EQUAL, "sensor1")), false)); + } + + @ParameterizedTest + @MethodSource("deviceNameAndTypeFilter") + public void testFilterByDeviceNameAndDeviceType(String deviceName, String deviceType, List keyFilters, boolean result) { + UUID deviceProfileId = UUID.randomUUID(); + EntityProfileData deviceProfile = new EntityProfileData(deviceProfileId, EntityType.DEVICE_PROFILE); + deviceProfile.setFields(DeviceProfileFields.builder().name(deviceType).build()); + + DeviceData deviceData = new DeviceData(UUID.randomUUID()); + deviceData.setCustomerId(UUID.randomUUID()); + deviceData.setFields(DeviceFields.builder().name(deviceName).deviceProfileId(deviceProfileId).type(deviceType).build()); + + assertThat(RepositoryUtils.checkKeyFilters(deviceData, keyFilters)).isEqualTo(result); + } + + private static Stream deviceNameComplexFilters() { + return Stream.of(Arguments.of(null, List.of(getComplexComplexDeviceNameFilter(StringOperation.STARTS_WITH, "lo", ComplexOperation.AND, StringOperation.ENDS_WITH, "123")), true), + Arguments.of("loranet 123", List.of(getComplexComplexDeviceNameFilter(StringOperation.STARTS_WITH, "lo", ComplexOperation.AND, StringOperation.ENDS_WITH, "123")), true), + Arguments.of("loranet 123", List.of(getComplexComplexDeviceNameFilter(StringOperation.STARTS_WITH, "lo", ComplexOperation.AND, StringOperation.ENDS_WITH, "124")), false), + Arguments.of("loranet 123", List.of(getComplexComplexDeviceNameFilter(StringOperation.STARTS_WITH, "lo", ComplexOperation.OR, StringOperation.STARTS_WITH, "net")), true), + Arguments.of("loranet 123", List.of(getComplexComplexDeviceNameFilter(StringOperation.STARTS_WITH, "net", ComplexOperation.OR, StringOperation.STARTS_WITH, "the")), false), + Arguments.of("loranet123", List.of(getComplexComplexDeviceNameFilter(StringOperation.STARTS_WITH, "lo", ComplexOperation.OR, StringOperation.STARTS_WITH, "the", + ComplexOperation.AND, StringOperation.ENDS_WITH, "123")), true), + Arguments.of("loranet 123", List.of(getComplexComplexDeviceNameFilter(StringOperation.STARTS_WITH, "net", ComplexOperation.OR, StringOperation.STARTS_WITH, "the", + ComplexOperation.OR, StringOperation.ENDS_WITH, "123")), true), + Arguments.of("loranet 123", List.of(getComplexComplexDeviceNameFilter(StringOperation.STARTS_WITH, "net", ComplexOperation.OR, StringOperation.STARTS_WITH, "the", + ComplexOperation.AND, StringOperation.ENDS_WITH, "123")), false), + Arguments.of("loranet 123", List.of(getComplexComplexDeviceNameFilter(StringOperation.STARTS_WITH, "lo", ComplexOperation.OR, StringOperation.STARTS_WITH, "the", + ComplexOperation.AND, StringOperation.ENDS_WITH, "124")), false) + ); + } + + @ParameterizedTest + @MethodSource("deviceNameComplexFilters") + public void testFilterByDeviceNameComplexFilters(String deviceName, List keyFilters, boolean result) { + DeviceData deviceData = new DeviceData(UUID.randomUUID()); + deviceData.setCustomerId(UUID.randomUUID()); + deviceData.setFields(DeviceFields.builder().name(deviceName).build()); + + assertThat(RepositoryUtils.checkKeyFilters(deviceData, keyFilters)).isEqualTo(result); + } + + private static Stream deviceTemperatureFilters() { + return Stream.of(Arguments.of(22.8, getTemperatureFilter(NumericOperation.EQUAL, 22.8), true), + Arguments.of(22.8, getTemperatureFilter(NumericOperation.EQUAL, 22.9), false), + Arguments.of(22.8, getTemperatureFilter(NumericOperation.NOT_EQUAL, 22.8), false), + Arguments.of(22.8, getTemperatureFilter(NumericOperation.NOT_EQUAL, 22.9), true), + Arguments.of(22.8, getTemperatureFilter(NumericOperation.GREATER, 22.0), true), + Arguments.of(22.8, getTemperatureFilter(NumericOperation.GREATER, 23.0), false), + Arguments.of(22.8, getTemperatureFilter(NumericOperation.GREATER_OR_EQUAL, 22.8), true), + Arguments.of(22.8, getTemperatureFilter(NumericOperation.GREATER_OR_EQUAL, 23.0), false), + Arguments.of(22.8, getTemperatureFilter(NumericOperation.LESS, 23.0), true), + Arguments.of(22.8, getTemperatureFilter(NumericOperation.LESS, 22.0), false), + Arguments.of(22.8, getTemperatureFilter(NumericOperation.LESS_OR_EQUAL, 22.0), false), + Arguments.of(22.8, getTemperatureFilter(NumericOperation.LESS_OR_EQUAL, 22.8), true) + ); + } + + @ParameterizedTest + @MethodSource("deviceTemperatureFilters") + public void testFilterByDeviceTemperature(double tempValue, EdqsFilter keyFilter, boolean result) { + DeviceData deviceData = new DeviceData(UUID.randomUUID()); + deviceData.setCustomerId(UUID.randomUUID()); + deviceData.setFields(DeviceFields.builder().name(StringUtils.randomAlphabetic(10)).build()); + deviceData.putTs(5, new DoubleDataPoint(System.currentTimeMillis(), tempValue)); + + assertThat(RepositoryUtils.checkKeyFilters(deviceData, List.of(keyFilter))).isEqualTo(result); + } + + private static Stream deviceTemperatureComplexFilters() { + return Stream.of(Arguments.of(22.8, getComplexTemperatureFilter(NumericOperation.GREATER_OR_EQUAL, 22.8, ComplexOperation.AND, NumericOperation.LESS_OR_EQUAL, 30), true), + Arguments.of(22.8, getComplexTemperatureFilter(NumericOperation.GREATER, 23.5, ComplexOperation.AND, NumericOperation.LESS_OR_EQUAL, 30), false), + Arguments.of(22.8, getComplexComplexTemperatureFilter(NumericOperation.GREATER, 22.0, ComplexOperation.AND, NumericOperation.LESS_OR_EQUAL, 30, ComplexOperation.OR, NumericOperation.GREATER, 35), true), + Arguments.of(22.8, getComplexComplexTemperatureFilter(NumericOperation.GREATER, 22.0, ComplexOperation.AND, NumericOperation.LESS_OR_EQUAL, 30, ComplexOperation.AND, NumericOperation.EQUAL, 22.8), true) + ); + } + + @ParameterizedTest + @MethodSource("deviceTemperatureComplexFilters") + public void testComplexFilterByDeviceTemperature(double tempValue, EdqsFilter keyFilter, boolean result) { + DeviceData deviceData = new DeviceData(UUID.randomUUID()); + deviceData.setCustomerId(UUID.randomUUID()); + deviceData.setFields(DeviceFields.builder().name(StringUtils.randomAlphabetic(10)).build()); + deviceData.putTs(5, new DoubleDataPoint(System.currentTimeMillis(), tempValue)); + + assertThat(RepositoryUtils.checkKeyFilters(deviceData, List.of(keyFilter))).isEqualTo(result); + } + + private static Stream deviceHumidityFilters() { + return Stream.of(Arguments.of(60, getHumidityFilter(NumericOperation.EQUAL, 60), true), + Arguments.of(60, getHumidityFilter(NumericOperation.EQUAL, 61), false), + Arguments.of(60, getHumidityFilter(NumericOperation.NOT_EQUAL, 60), false), + Arguments.of(60, getHumidityFilter(NumericOperation.NOT_EQUAL, 61), true), + Arguments.of(60, getHumidityFilter(NumericOperation.GREATER, 59), true), + Arguments.of(60, getHumidityFilter(NumericOperation.GREATER, 60), false), + Arguments.of(60, getHumidityFilter(NumericOperation.GREATER_OR_EQUAL, 60), true), + Arguments.of(60, getHumidityFilter(NumericOperation.GREATER_OR_EQUAL, 61), false), + Arguments.of(60, getHumidityFilter(NumericOperation.LESS, 61), true), + Arguments.of(60, getHumidityFilter(NumericOperation.LESS, 60), false), + Arguments.of(60, getHumidityFilter(NumericOperation.LESS_OR_EQUAL, 59), false), + Arguments.of(60, getHumidityFilter(NumericOperation.LESS_OR_EQUAL, 60), true) + ); + } + + @ParameterizedTest + @MethodSource("deviceHumidityFilters") + public void testFilterByDeviceHumidity(long humidityValue, EdqsFilter keyFilter, boolean result) { + DeviceData deviceData = new DeviceData(UUID.randomUUID()); + deviceData.setCustomerId(UUID.randomUUID()); + deviceData.setFields(DeviceFields.builder().name(StringUtils.randomAlphabetic(10)).build()); + deviceData.putTs(6, new LongDataPoint(System.currentTimeMillis(), humidityValue)); + + assertThat(RepositoryUtils.checkKeyFilters(deviceData, List.of(keyFilter))).isEqualTo(result); + } + + private static Stream deviceTemperatureAndHumidityFilters() { + return Stream.of(Arguments.of(22.8, 60, List.of(getTemperatureFilter(NumericOperation.EQUAL, 22.8), getHumidityFilter(NumericOperation.EQUAL, 60)), true), + Arguments.of(22.8, 60, List.of(getTemperatureFilter(NumericOperation.EQUAL, 22.8), getHumidityFilter(NumericOperation.GREATER_OR_EQUAL, 61)), false), + Arguments.of(22.8, 60, List.of(getTemperatureFilter(NumericOperation.GREATER, 23), getHumidityFilter(NumericOperation.GREATER_OR_EQUAL, 60)), false), + Arguments.of(22.8, 60, List.of(getTemperatureFilter(NumericOperation.GREATER_OR_EQUAL, 22.9), getHumidityFilter(NumericOperation.GREATER_OR_EQUAL, 61)), false) + ); + } + + @ParameterizedTest + @MethodSource("deviceTemperatureAndHumidityFilters") + public void testFilterByDeviceTemperatureAndHumidity(double tempValue, long humidityValue, List keyFilters, boolean result) { + DeviceData deviceData = new DeviceData(UUID.randomUUID()); + deviceData.setCustomerId(UUID.randomUUID()); + deviceData.setFields(DeviceFields.builder().name(StringUtils.randomAlphabetic(10)).build()); + deviceData.putTs(5, new DoubleDataPoint(System.currentTimeMillis(), tempValue)); + deviceData.putTs(6, new LongDataPoint(System.currentTimeMillis(), humidityValue)); + + assertThat(RepositoryUtils.checkKeyFilters(deviceData, keyFilters)).isEqualTo(result); + } + + private static Stream deviceVersionAttributeFilters() { + return Stream.of(Arguments.of(true, getActiveAttributeFilter(BooleanOperation.EQUAL, true), true), + Arguments.of(true, getActiveAttributeFilter(BooleanOperation.EQUAL, false), false), + Arguments.of(true, getActiveAttributeFilter(BooleanOperation.NOT_EQUAL, true), false), + Arguments.of(true, getActiveAttributeFilter(BooleanOperation.NOT_EQUAL, false), true) + ); + } + + @ParameterizedTest + @MethodSource("deviceVersionAttributeFilters") + public void testFilterByDeviceVersionAttribute(Boolean active, EdqsFilter keyFilter, boolean result) { + DeviceData deviceData = new DeviceData(UUID.randomUUID()); + deviceData.setCustomerId(UUID.randomUUID()); + deviceData.setFields(DeviceFields.builder().name(StringUtils.randomAlphabetic(10)).build()); + deviceData.putAttr(2, AttributeScope.SERVER_SCOPE, new BoolDataPoint(System.currentTimeMillis(), active)); + + assertThat(RepositoryUtils.checkKeyFilters(deviceData, List.of(keyFilter))).isEqualTo(result); + } + + private static Stream deviceActiveAndVersionFilters() { + return Stream.of(Arguments.of(true, "3.2.1", List.of(getActiveAttributeFilter(BooleanOperation.EQUAL, true), getVersionAttributeFilter(StringOperation.EQUAL, "3.2.1")), true), + Arguments.of(true, "3.2.1", List.of(getActiveAttributeFilter(BooleanOperation.EQUAL, true), getVersionAttributeFilter(StringOperation.EQUAL, "3.2.2")), false), + Arguments.of(true, "3.2.1", List.of(getActiveAttributeFilter(BooleanOperation.EQUAL, false), getVersionAttributeFilter(StringOperation.EQUAL, "3.2.1")), false), + Arguments.of(true, "3.2.1", List.of(getActiveAttributeFilter(BooleanOperation.EQUAL, false), getVersionAttributeFilter(StringOperation.EQUAL, "3.2.2")), false) + ); + } + + @ParameterizedTest + @MethodSource("deviceActiveAndVersionFilters") + public void testFilterByActiveAndVersionAttributes(Boolean active, String version, List keyFilters, boolean result) { + DeviceData deviceData = new DeviceData(UUID.randomUUID()); + deviceData.setCustomerId(UUID.randomUUID()); + deviceData.setFields(DeviceFields.builder().name(StringUtils.randomAlphabetic(10)).build()); + deviceData.putAttr(1, AttributeScope.CLIENT_SCOPE, new StringDataPoint(System.currentTimeMillis(), version)); + deviceData.putAttr(2, AttributeScope.SERVER_SCOPE, new BoolDataPoint(System.currentTimeMillis(), active)); + + assertThat(RepositoryUtils.checkKeyFilters(deviceData, keyFilters)).isEqualTo(result); + } + + private static EdqsFilter getVersionAttributeFilter(StringOperation operation, String predicateValue) { + StringFilterPredicate filterPredicate = new StringFilterPredicate(); + filterPredicate.setOperation(operation); + filterPredicate.setValue(FilterPredicateValue.fromString(predicateValue)); + + DataKey key = new DataKey(EntityKeyType.CLIENT_ATTRIBUTE, "version", 1); + return new EdqsFilter(key, EntityKeyValueType.STRING, filterPredicate); + } + + + private static EdqsFilter getActiveAttributeFilter(BooleanOperation operation, boolean predicateValue) { + BooleanFilterPredicate filterPredicate = new BooleanFilterPredicate(); + filterPredicate.setOperation(operation); + filterPredicate.setValue(FilterPredicateValue.fromBoolean(predicateValue)); + + DataKey key = new DataKey(EntityKeyType.SERVER_ATTRIBUTE, "active", 2); + return new EdqsFilter(key, EntityKeyValueType.BOOLEAN, filterPredicate); + } + + private static EdqsFilter getTemperatureFilter(NumericOperation operation, double predicateValue) { + return getTimeseriesFilter("temperature", 5, operation, predicateValue); + } + + private static EdqsFilter getHumidityFilter(NumericOperation operation, double predicateValue) { + return getTimeseriesFilter("humidity", 6, operation, predicateValue); + } + + private static EdqsFilter getTimeseriesFilter(String key, Integer keysId, NumericOperation operation, double predicateValue) { + NumericFilterPredicate filterPredicate = new NumericFilterPredicate(); + filterPredicate.setOperation(operation); + filterPredicate.setValue(FilterPredicateValue.fromDouble(predicateValue)); + + DataKey newKey = new DataKey(EntityKeyType.TIME_SERIES, key, keysId); + return new EdqsFilter(newKey, EntityKeyValueType.NUMERIC, filterPredicate); + } + + private static EdqsFilter getNameFilter(StringOperation operation, String predicateValue) { + return getStringEntityFieldFilter("name", operation, predicateValue); + } + + private static EdqsFilter getTypeFilter(StringOperation operation, String predicateValue) { + return getStringEntityFieldFilter("type", operation, predicateValue); + } + + private static EdqsFilter getStringEntityFieldFilter(String fieldName, StringOperation operation, String predicateValue) { + StringFilterPredicate filterPredicate = new StringFilterPredicate(); + filterPredicate.setOperation(operation); + filterPredicate.setValue(FilterPredicateValue.fromString(predicateValue)); + + DataKey key = new DataKey(EntityKeyType.ENTITY_FIELD, fieldName, 3); + return new EdqsFilter(key, EntityKeyValueType.STRING, filterPredicate); + } + + private static EdqsFilter getCreatedTimeFilter(NumericOperation operation, double predicateValue) { + return getDatetimeEntityFieldFilter("createdTime", operation, predicateValue); + } + + private static EdqsFilter getDatetimeEntityFieldFilter(String fieldName, NumericOperation operation, double predicateValue) { + NumericFilterPredicate filterPredicate = new NumericFilterPredicate(); + filterPredicate.setOperation(operation); + filterPredicate.setValue(FilterPredicateValue.fromDouble(predicateValue)); + + DataKey key = new DataKey(EntityKeyType.ENTITY_FIELD, fieldName, 3); + return new EdqsFilter(key, EntityKeyValueType.DATE_TIME, filterPredicate); + } + + private static EdqsFilter getComplexTemperatureFilter(NumericOperation operation, double predicateValue, ComplexOperation complexOperation, NumericOperation operation2, double predicateValue2) { + ComplexFilterPredicate complexFilterPredicate = getComplexNumericFilterPredicate(operation, predicateValue, complexOperation, operation2, predicateValue2); + + DataKey key = new DataKey(EntityKeyType.TIME_SERIES, "temperature", 5); + return new EdqsFilter(key, EntityKeyValueType.NUMERIC, complexFilterPredicate); + } + + private static EdqsFilter getComplexComplexTemperatureFilter(NumericOperation operation, double predicateValue, ComplexOperation complexOperation, NumericOperation operation2, double predicateValue2, + ComplexOperation complexOperation2, NumericOperation operation3, double predicateValue3) { + ComplexFilterPredicate complexFilterPredicate = getComplexNumericFilterPredicate(operation, predicateValue, complexOperation, operation2, predicateValue2); + + NumericFilterPredicate filterPredicate = new NumericFilterPredicate(); + filterPredicate.setOperation(operation); + filterPredicate.setValue(FilterPredicateValue.fromDouble(predicateValue)); + + ComplexFilterPredicate mainComplexFilterPredicate = new ComplexFilterPredicate(); + mainComplexFilterPredicate.setOperation(complexOperation2); + mainComplexFilterPredicate.setPredicates(List.of(complexFilterPredicate, filterPredicate)); + + DataKey key = new DataKey(EntityKeyType.TIME_SERIES, "temperature", 5); + return new EdqsFilter(key, EntityKeyValueType.NUMERIC, mainComplexFilterPredicate); + } + + private static ComplexFilterPredicate getComplexNumericFilterPredicate(NumericOperation operation, double predicateValue, ComplexOperation complexOperation, NumericOperation operation2, double predicateValue2) { + NumericFilterPredicate filterPredicate = new NumericFilterPredicate(); + filterPredicate.setOperation(operation); + filterPredicate.setValue(FilterPredicateValue.fromDouble(predicateValue)); + + NumericFilterPredicate filterPredicate2 = new NumericFilterPredicate(); + filterPredicate2.setOperation(operation2); + filterPredicate2.setValue(FilterPredicateValue.fromDouble(predicateValue2)); + + ComplexFilterPredicate complexFilterPredicate = new ComplexFilterPredicate(); + complexFilterPredicate.setOperation(complexOperation); + complexFilterPredicate.setPredicates(List.of(filterPredicate, filterPredicate2)); + return complexFilterPredicate; + } + + private static EdqsFilter getComplexComplexDeviceNameFilter(StringOperation operation, String predicateValue, ComplexOperation complexOperation, StringOperation operation2, String predicateValue2) { + ComplexFilterPredicate complexFilterPredicate = getComplexStringFilterPredicate(operation, predicateValue, complexOperation, operation2, predicateValue2); + DataKey key = new DataKey(EntityKeyType.ENTITY_FIELD, "name", 3); + return new EdqsFilter(key, EntityKeyValueType.STRING, complexFilterPredicate); + } + + private static EdqsFilter getComplexComplexDeviceNameFilter(StringOperation operation, String predicateValue, ComplexOperation complexOperation, StringOperation operation2, String predicateValue2, + ComplexOperation complexOperation2, StringOperation operation3, String predicateValue3) { + ComplexFilterPredicate complexFilterPredicate = getComplexStringFilterPredicate(operation, predicateValue, complexOperation, operation2, predicateValue2); + + StringFilterPredicate filterPredicate = new StringFilterPredicate(); + filterPredicate.setOperation(operation3); + filterPredicate.setValue(FilterPredicateValue.fromString(predicateValue3)); + + ComplexFilterPredicate mainComplexFilterPredicate = new ComplexFilterPredicate(); + mainComplexFilterPredicate.setOperation(complexOperation2); + mainComplexFilterPredicate.setPredicates(List.of(complexFilterPredicate, filterPredicate)); + + DataKey key = new DataKey(EntityKeyType.ENTITY_FIELD, "name", 3); + return new EdqsFilter(key, EntityKeyValueType.STRING, mainComplexFilterPredicate); + } + + private static ComplexFilterPredicate getComplexStringFilterPredicate(StringOperation operation, String predicateValue, ComplexOperation complexOperation, StringOperation operation2, String predicateValue2) { + StringFilterPredicate filterPredicate = new StringFilterPredicate(); + filterPredicate.setOperation(operation); + filterPredicate.setValue(FilterPredicateValue.fromString(predicateValue)); + + StringFilterPredicate filterPredicate2 = new StringFilterPredicate(); + filterPredicate2.setOperation(operation2); + filterPredicate2.setValue(FilterPredicateValue.fromString(predicateValue2)); + + ComplexFilterPredicate complexFilterPredicate = new ComplexFilterPredicate(); + complexFilterPredicate.setOperation(complexOperation); + complexFilterPredicate.setPredicates(List.of(filterPredicate, filterPredicate2)); + return complexFilterPredicate; + } + +} diff --git a/edqs/src/test/java/org/thingsboard/server/edqs/repo/SchedulerEventFilterTest.java b/edqs/src/test/java/org/thingsboard/server/edqs/repo/SchedulerEventFilterTest.java new file mode 100644 index 0000000000..8ebbff534d --- /dev/null +++ b/edqs/src/test/java/org/thingsboard/server/edqs/repo/SchedulerEventFilterTest.java @@ -0,0 +1,129 @@ +/** + * Copyright © 2016-2024 ThingsBoard, Inc. + * + * 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.edqs.repo; + +import org.junit.After; +import org.junit.Assert; +import org.junit.Before; +import org.junit.Test; +import org.thingsboard.server.common.data.id.DashboardId; +import org.thingsboard.server.common.data.id.DeviceId; +import org.thingsboard.server.common.data.id.EntityId; +import org.thingsboard.server.common.data.query.EntityDataPageLink; +import org.thingsboard.server.common.data.query.EntityDataQuery; +import org.thingsboard.server.common.data.query.EntityDataSortOrder; +import org.thingsboard.server.common.data.query.EntityKey; +import org.thingsboard.server.common.data.query.EntityKeyType; +import org.thingsboard.server.common.data.query.EntityKeyValueType; +import org.thingsboard.server.common.data.query.FilterPredicateValue; +import org.thingsboard.server.common.data.query.KeyFilter; +import org.thingsboard.server.common.data.query.SchedulerEventFilter; +import org.thingsboard.server.common.data.query.StringFilterPredicate; +import org.thingsboard.server.edqs.util.RepositoryUtils; + +import java.util.Arrays; +import java.util.List; +import java.util.UUID; + +public class SchedulerEventFilterTest extends AbstractEDQTest { + + @Before + public void setUp() { + } + + @After + public void tearDown() { + } + + @Test + public void testFindTenantSchedulerEvents() { + UUID dashboardId = createDashboard("test dashboard"); + UUID deviceId = createDevice("test device"); + + UUID eventId1 = createSchedulerEvent("Update attributes", new DeviceId(deviceId), "Turn off device"); + UUID eventId2 = createSchedulerEvent("Generate report", new DashboardId(dashboardId), "Generate morning report"); + UUID eventId3 = createSchedulerEvent("Generate report", new DashboardId(dashboardId), "Generate evening report"); + + // find all scheduler events with type "Generate report" + var result = repository.findEntityDataByQuery(tenantId, null, RepositoryUtils.ALL_READ_PERMISSIONS, getSchedulerEventQuery("Generate report", null, null), false); + Assert.assertEquals(2, result.getTotalElements()); + Assert.assertTrue(checkContains(result, eventId2)); + Assert.assertTrue(checkContains(result, eventId3)); + + // find all scheduler events for device originator + result = repository.findEntityDataByQuery(tenantId, null, RepositoryUtils.ALL_READ_PERMISSIONS, getSchedulerEventQuery(null, new DeviceId(deviceId), null), false); + Assert.assertEquals(1, result.getTotalElements()); + Assert.assertTrue(checkContains(result, eventId1)); + + // find all scheduler events with name "%morning%" + KeyFilter containsNameFilter = getSchedulerEventNameKeyFilter(StringFilterPredicate.StringOperation.CONTAINS, "morning", true); + result = repository.findEntityDataByQuery(tenantId, null, RepositoryUtils.ALL_READ_PERMISSIONS, getSchedulerEventQuery(null, null, List.of(containsNameFilter)), false); + Assert.assertEquals(1, result.getTotalElements()); + Assert.assertTrue(checkContains(result, eventId2)); + } + + @Test + public void testFindCustomerEdges() { + UUID dashboardId = createDashboard( "test dashboard"); + UUID deviceId = createDevice("test device"); + + UUID eventId1 = createSchedulerEvent(customerId.getId(), "Update attributes", new DeviceId(deviceId), "Turn off device"); + UUID eventId2 = createSchedulerEvent(customerId.getId(), "Generate report", new DashboardId(dashboardId), "Generate morning report"); + UUID eventId3 = createSchedulerEvent(customerId.getId(), "Generate report", new DashboardId(dashboardId), "Generate evening report"); + + // find all scheduler events with type "Generate report" + var result = repository.findEntityDataByQuery(tenantId, customerId, RepositoryUtils.ALL_READ_PERMISSIONS, getSchedulerEventQuery("Generate report", null, null), false); + Assert.assertEquals(2, result.getTotalElements()); + Assert.assertTrue(checkContains(result, eventId2)); + Assert.assertTrue(checkContains(result, eventId3)); + + // find all scheduler events for device originator + result = repository.findEntityDataByQuery(tenantId, customerId, RepositoryUtils.ALL_READ_PERMISSIONS, getSchedulerEventQuery(null, new DeviceId(deviceId), null), false); + Assert.assertEquals(1, result.getTotalElements()); + Assert.assertTrue(checkContains(result, eventId1)); + + // find all scheduler events with name "%morning%" + KeyFilter containsNameFilter = getSchedulerEventNameKeyFilter(StringFilterPredicate.StringOperation.CONTAINS, "morning", true); + result = repository.findEntityDataByQuery(tenantId, customerId, RepositoryUtils.ALL_READ_PERMISSIONS, getSchedulerEventQuery(null, null, List.of(containsNameFilter)), false); + Assert.assertEquals(1, result.getTotalElements()); + Assert.assertTrue(checkContains(result, eventId2)); + } + + private static EntityDataQuery getSchedulerEventQuery(String eventType, EntityId entityId, List keyFilters) { + SchedulerEventFilter filter = new SchedulerEventFilter(); + filter.setEventType(eventType); + filter.setOriginator(entityId); + var pageLink = new EntityDataPageLink(20, 0, null, new EntityDataSortOrder(new EntityKey(EntityKeyType.ENTITY_FIELD, "createdTime"), EntityDataSortOrder.Direction.DESC), false); + + var entityFields = Arrays.asList(new EntityKey(EntityKeyType.ENTITY_FIELD, "name"), new EntityKey(EntityKeyType.ENTITY_FIELD, "createdTime")); + var latestValues = Arrays.asList(new EntityKey(EntityKeyType.TIME_SERIES, "state")); + + return new EntityDataQuery(filter, pageLink, entityFields, latestValues, keyFilters); + } + + private static KeyFilter getSchedulerEventNameKeyFilter(StringFilterPredicate.StringOperation operation, String predicateValue, boolean ignoreCase) { + KeyFilter nameFilter = new KeyFilter(); + nameFilter.setKey(new EntityKey(EntityKeyType.ENTITY_FIELD, "name")); + var predicate = new StringFilterPredicate(); + predicate.setIgnoreCase(ignoreCase); + predicate.setOperation(operation); + predicate.setValue(new FilterPredicateValue<>(predicateValue)); + nameFilter.setPredicate(predicate); + nameFilter.setValueType(EntityKeyValueType.STRING); + return nameFilter; + } + +} diff --git a/edqs/src/test/java/org/thingsboard/server/edqs/repo/SingleEntityFilterTest.java b/edqs/src/test/java/org/thingsboard/server/edqs/repo/SingleEntityFilterTest.java new file mode 100644 index 0000000000..8755191e39 --- /dev/null +++ b/edqs/src/test/java/org/thingsboard/server/edqs/repo/SingleEntityFilterTest.java @@ -0,0 +1,176 @@ +/** + * Copyright © 2016-2024 ThingsBoard, Inc. + * + * 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.edqs.repo; + +import org.junit.After; +import org.junit.Assert; +import org.junit.Before; +import org.junit.Test; +import org.thingsboard.server.common.data.Device; +import org.thingsboard.server.common.data.EntityType; +import org.thingsboard.server.common.data.edqs.LatestTsKv; +import org.thingsboard.server.common.data.edqs.query.QueryResult; +import org.thingsboard.server.common.data.id.DeviceId; +import org.thingsboard.server.common.data.id.DeviceProfileId; +import org.thingsboard.server.common.data.id.EntityGroupId; +import org.thingsboard.server.common.data.kv.BasicTsKvEntry; +import org.thingsboard.server.common.data.kv.StringDataEntry; +import org.thingsboard.server.common.data.permission.MergedGroupPermissionInfo; +import org.thingsboard.server.common.data.permission.MergedUserPermissions; +import org.thingsboard.server.common.data.permission.Operation; +import org.thingsboard.server.common.data.permission.Resource; +import org.thingsboard.server.common.data.query.EntityDataPageLink; +import org.thingsboard.server.common.data.query.EntityDataQuery; +import org.thingsboard.server.common.data.query.EntityDataSortOrder; +import org.thingsboard.server.common.data.query.EntityKey; +import org.thingsboard.server.common.data.query.EntityKeyType; +import org.thingsboard.server.common.data.query.EntityKeyValueType; +import org.thingsboard.server.common.data.query.FilterPredicateValue; +import org.thingsboard.server.common.data.query.KeyFilter; +import org.thingsboard.server.common.data.query.SingleEntityFilter; +import org.thingsboard.server.common.data.query.StringFilterPredicate; +import org.thingsboard.server.common.data.relation.RelationTypeGroup; +import org.thingsboard.server.edqs.util.RepositoryUtils; + +import java.util.Arrays; +import java.util.Map; +import java.util.Set; +import java.util.UUID; + +public class SingleEntityFilterTest extends AbstractEDQTest { + + @Before + public void setUp() { + } + + @After + public void tearDown() { + } + + @Test + public void testFindTenantDevice() { + DeviceId deviceId = new DeviceId(UUID.randomUUID()); + Device device = new Device(); + device.setId(deviceId); + device.setTenantId(tenantId); + device.setName("LoRa-1"); + device.setCreatedTime(42L); + device.setDeviceProfileId(new DeviceProfileId(defaultDeviceProfileId)); + addOrUpdate(EntityType.DEVICE, device); + addOrUpdate(new LatestTsKv(deviceId, new BasicTsKvEntry(43, new StringDataEntry("state", "TEST")), 0L)); + + var result = repository.findEntityDataByQuery(tenantId, null, RepositoryUtils.ALL_READ_PERMISSIONS, getEntityDataQuery(device.getId()), false); + + Assert.assertEquals(1, result.getTotalElements()); + var first = result.getData().get(0); + Assert.assertEquals(deviceId, first.getEntityId()); + Assert.assertEquals("LoRa-1", first.getLatest().get(EntityKeyType.ENTITY_FIELD).get("name").getValue()); + Assert.assertEquals("42", first.getLatest().get(EntityKeyType.ENTITY_FIELD).get("createdTime").getValue()); + + result = repository.findEntityDataByQuery(tenantId, null, RepositoryUtils.ALL_READ_PERMISSIONS, getEntityDataQuery(new DeviceId(UUID.randomUUID())), false); + Assert.assertEquals(0, result.getTotalElements()); + + device.setCustomerId(customerId); + addOrUpdate(EntityType.DEVICE, device); + + result = repository.findEntityDataByQuery(tenantId, null, RepositoryUtils.ALL_READ_PERMISSIONS, getEntityDataQuery(device.getId()), false); + Assert.assertEquals(1, result.getTotalElements()); + first = result.getData().get(0); + Assert.assertEquals(deviceId, first.getEntityId()); + Assert.assertEquals("LoRa-1", first.getLatest().get(EntityKeyType.ENTITY_FIELD).get("name").getValue()); + Assert.assertEquals("42", first.getLatest().get(EntityKeyType.ENTITY_FIELD).get("createdTime").getValue()); + } + + @Test + public void testFindTenantDeviceWithGenericAndGroupPermission() { + UUID deviceId = createDevice(customerId, "LoRa-customer-1"); + UUID deviceId2 = createDevice(customerId, "LoRa-customer-2"); + UUID deviceId3 = createDevice(customerId, "LoRa-customer-3"); + + // add device and device 2 to Group A + UUID groupAId = createGroup(customerId.getId(), EntityType.DEVICE, "Group A"); + createRelation(EntityType.ENTITY_GROUP, groupAId, EntityType.DEVICE, deviceId, RelationTypeGroup.FROM_ENTITY_GROUP, "Contains"); + createRelation(EntityType.ENTITY_GROUP, groupAId, EntityType.DEVICE, deviceId2, RelationTypeGroup.FROM_ENTITY_GROUP, "Contains"); + + // add device and device 2 to Group A + UUID groupBId = createGroup(customerId.getId(), EntityType.DEVICE, "Group B"); + createRelation(EntityType.ENTITY_GROUP, groupAId, EntityType.DEVICE, deviceId3, RelationTypeGroup.FROM_ENTITY_GROUP, "Contains"); + + MergedUserPermissions genericAndGroupAPermission = new MergedUserPermissions( + Map.of(Resource.ALL, Set.of(Operation.ALL)), Map.of(new EntityGroupId(groupAId), new MergedGroupPermissionInfo(EntityType.DEVICE, Set.of(Operation.ALL)))); + var result = repository.findEntityDataByQuery(tenantId, null, genericAndGroupAPermission, getEntityDataQuery(new DeviceId(deviceId2)), false); + Assert.assertEquals(1, result.getTotalElements()); + QueryResult queryResult = result.getData().get(0); + Assert.assertEquals(deviceId2, queryResult.getEntityId().getId()); + Assert.assertEquals("LoRa-customer-2", queryResult.getLatest().get(EntityKeyType.ENTITY_FIELD).get("name").getValue()); + + // find device without permission + MergedUserPermissions genericAndGroupBPermission = new MergedUserPermissions( + Map.of(Resource.ALL, Set.of(Operation.ALL)), Map.of(new EntityGroupId(groupAId), new MergedGroupPermissionInfo(EntityType.DEVICE, Set.of(Operation.ALL)))); + result = repository.findEntityDataByQuery(tenantId, null, genericAndGroupBPermission, getEntityDataQuery(new DeviceId(deviceId3)), false); + Assert.assertEquals(1, result.getTotalElements()); + queryResult = result.getData().get(0); + Assert.assertEquals(deviceId3, queryResult.getEntityId().getId()); + Assert.assertEquals("LoRa-customer-3", queryResult.getLatest().get(EntityKeyType.ENTITY_FIELD).get("name").getValue()); + } + + @Test + public void testFindCustomerDevice() { + DeviceId deviceId = new DeviceId(UUID.randomUUID()); + Device device = new Device(); + device.setId(deviceId); + device.setTenantId(tenantId); + device.setName("LoRa-1"); + device.setCreatedTime(42L); + device.setDeviceProfileId(new DeviceProfileId(defaultDeviceProfileId)); + addOrUpdate(EntityType.DEVICE, device); + addOrUpdate(new LatestTsKv(deviceId, new BasicTsKvEntry(43, new StringDataEntry("state", "TEST")), 0L)); + + var result = repository.findEntityDataByQuery(tenantId, customerId, RepositoryUtils.ALL_READ_PERMISSIONS, getEntityDataQuery(device.getId()), false); + Assert.assertEquals(0, result.getTotalElements()); + + device.setCustomerId(customerId); + addOrUpdate(EntityType.DEVICE, device); + + result = repository.findEntityDataByQuery(tenantId, customerId, RepositoryUtils.ALL_READ_PERMISSIONS, getEntityDataQuery(device.getId()), false); + + Assert.assertEquals(1, result.getTotalElements()); + var first = result.getData().get(0); + Assert.assertEquals(deviceId, first.getEntityId()); + Assert.assertEquals("LoRa-1", first.getLatest().get(EntityKeyType.ENTITY_FIELD).get("name").getValue()); + Assert.assertEquals("42", first.getLatest().get(EntityKeyType.ENTITY_FIELD).get("createdTime").getValue()); + } + + private static EntityDataQuery getEntityDataQuery(DeviceId deviceId) { + SingleEntityFilter filter = new SingleEntityFilter(); + filter.setSingleEntity(deviceId); + var pageLink = new EntityDataPageLink(20, 0, null, new EntityDataSortOrder(new EntityKey(EntityKeyType.TIME_SERIES, "state"), EntityDataSortOrder.Direction.DESC), false); + + var entityFields = Arrays.asList(new EntityKey(EntityKeyType.ENTITY_FIELD, "name"), new EntityKey(EntityKeyType.ENTITY_FIELD, "createdTime")); + var latestValues = Arrays.asList(new EntityKey(EntityKeyType.TIME_SERIES, "state")); + KeyFilter nameFilter = new KeyFilter(); + nameFilter.setKey(new EntityKey(EntityKeyType.ENTITY_FIELD, "name")); + var predicate = new StringFilterPredicate(); + predicate.setIgnoreCase(false); + predicate.setOperation(StringFilterPredicate.StringOperation.CONTAINS); + predicate.setValue(new FilterPredicateValue<>("LoRa-")); + nameFilter.setPredicate(predicate); + nameFilter.setValueType(EntityKeyValueType.STRING); + + return new EntityDataQuery(filter, pageLink, entityFields, latestValues, Arrays.asList(nameFilter)); + } + +} diff --git a/edqs/src/test/java/org/thingsboard/server/edqs/repo/StateEntityOwnerFilterTest.java b/edqs/src/test/java/org/thingsboard/server/edqs/repo/StateEntityOwnerFilterTest.java new file mode 100644 index 0000000000..f0bb4c4e0b --- /dev/null +++ b/edqs/src/test/java/org/thingsboard/server/edqs/repo/StateEntityOwnerFilterTest.java @@ -0,0 +1,81 @@ +/** + * Copyright © 2016-2024 ThingsBoard, Inc. + * + * 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.edqs.repo; + +import org.junit.After; +import org.junit.Assert; +import org.junit.Before; +import org.junit.Test; +import org.thingsboard.server.common.data.id.CustomerId; +import org.thingsboard.server.common.data.id.DeviceId; +import org.thingsboard.server.common.data.query.EntityDataPageLink; +import org.thingsboard.server.common.data.query.EntityDataQuery; +import org.thingsboard.server.common.data.query.EntityDataSortOrder; +import org.thingsboard.server.common.data.query.EntityKey; +import org.thingsboard.server.common.data.query.EntityKeyType; +import org.thingsboard.server.common.data.query.EntityKeyValueType; +import org.thingsboard.server.common.data.query.FilterPredicateValue; +import org.thingsboard.server.common.data.query.KeyFilter; +import org.thingsboard.server.common.data.query.StateEntityOwnerFilter; +import org.thingsboard.server.common.data.query.StringFilterPredicate; +import org.thingsboard.server.edqs.util.RepositoryUtils; + +import java.util.Arrays; +import java.util.UUID; + +public class StateEntityOwnerFilterTest extends AbstractEDQTest { + + @Before + public void setUp() { + } + + @After + public void tearDown() { + } + + @Test + public void testFindCustomerDeviceOwner() { + UUID customerId = UUID.randomUUID(); + createCustomer(customerId, null, "Customer A"); + UUID deviceId = createDevice(new CustomerId(customerId), "LoRa-1"); + + var result = repository.findEntityDataByQuery(tenantId, null, RepositoryUtils.ALL_READ_PERMISSIONS, getEntityDataQuery(new DeviceId(deviceId)), false); + + Assert.assertEquals(1, result.getTotalElements()); + var customer = result.getData().get(0); + Assert.assertEquals(customerId, customer.getEntityId().getId()); + Assert.assertEquals("Customer A", customer.getLatest().get(EntityKeyType.ENTITY_FIELD).get("name").getValue()); + } + + private static EntityDataQuery getEntityDataQuery(DeviceId deviceId) { + StateEntityOwnerFilter filter = new StateEntityOwnerFilter(); + filter.setSingleEntity(deviceId); + var pageLink = new EntityDataPageLink(20, 0, null, new EntityDataSortOrder(new EntityKey(EntityKeyType.TIME_SERIES, "name"), EntityDataSortOrder.Direction.DESC), false); + + var entityFields = Arrays.asList(new EntityKey(EntityKeyType.ENTITY_FIELD, "name"), new EntityKey(EntityKeyType.ENTITY_FIELD, "createdTime")); + KeyFilter nameFilter = new KeyFilter(); + nameFilter.setKey(new EntityKey(EntityKeyType.ENTITY_FIELD, "name")); + var predicate = new StringFilterPredicate(); + predicate.setIgnoreCase(false); + predicate.setOperation(StringFilterPredicate.StringOperation.CONTAINS); + predicate.setValue(new FilterPredicateValue<>("LoRa-")); + nameFilter.setPredicate(predicate); + nameFilter.setValueType(EntityKeyValueType.STRING); + + return new EntityDataQuery(filter, pageLink, entityFields, null, Arrays.asList(nameFilter)); + } + +} diff --git a/edqs/src/test/resources/edq-test.properties b/edqs/src/test/resources/edq-test.properties new file mode 100644 index 0000000000..8a041c7407 --- /dev/null +++ b/edqs/src/test/resources/edq-test.properties @@ -0,0 +1,2 @@ +zk.enabled=false +service.type=edqs diff --git a/pom.xml b/pom.xml index e70a57fbbf..42747bae46 100755 --- a/pom.xml +++ b/pom.xml @@ -165,6 +165,8 @@ 1.6.1 2.19.0 9.2.0 + 1.1.10.5 + 9.4.0 @@ -172,6 +174,7 @@ common rule-engine dao + edqs transport ui-ngx tools @@ -1031,6 +1034,11 @@ coap-server ${project.version} + + org.thingsboard.common + edqs + ${project.version} + org.thingsboard.common.script script-api @@ -2279,6 +2287,16 @@ metadata-extractor ${drewnoakes-metadata-extractor.version} + + org.xerial.snappy + snappy-java + ${snappy.version} + + + org.rocksdb + rocksdbjni + ${rocksdbjni.version} + From 4ab9fb9f7b44aa5d351ca56b14cb6a78f0e67c7f Mon Sep 17 00:00:00 2001 From: Igor Kulikov Date: Mon, 27 Jan 2025 17:56:33 +0200 Subject: [PATCH 036/345] UI: Map editor toolbar. --- .../lib/maps/data-layer/circles-data-layer.ts | 15 ++- .../lib/maps/data-layer/map-data-layer.ts | 96 ++++++++++++-- .../lib/maps/data-layer/markers-data-layer.ts | 47 +++++-- .../maps/data-layer/polygons-data-layer.ts | 25 +++- .../widget/lib/maps/leaflet/leaflet-tb.ts | 90 +++++++++++++- .../home/components/widget/lib/maps/map.scss | 72 +++++++++-- .../home/components/widget/lib/maps/map.ts | 117 ++++++++++++++---- ui-ngx/src/typings/leaflet-extend-tb.d.ts | 28 ++++- 8 files changed, 425 insertions(+), 65 deletions(-) diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/maps/data-layer/circles-data-layer.ts b/ui-ngx/src/app/modules/home/components/widget/lib/maps/data-layer/circles-data-layer.ts index caccea87a7..c0dcb8e980 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/maps/data-layer/circles-data-layer.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/maps/data-layer/circles-data-layer.ts @@ -45,6 +45,7 @@ class TbCircleDataLayerItem extends TbDataLayerItem, _dsData: FormattedData[]): void { - this.dataLayer.getMap().circleClick(this.circle, data.$datasource); + this.dataLayer.getMap().circleClick(this, data.$datasource); } protected unbindLabel() { @@ -62,7 +63,7 @@ class TbCircleDataLayerItem extends TbDataLayerItem, center: L.LatLng, radius: number): void { - const converted = this.map.coordinatesToCircleData(center, radius); + const converted = center ? this.map.coordinatesToCircleData(center, radius) : null; const circleData = [ { dataKey: this.settings.circleKey, diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/maps/data-layer/map-data-layer.ts b/ui-ngx/src/app/modules/home/components/widget/lib/maps/data-layer/map-data-layer.ts index f08fd0bca8..9597464e9e 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/maps/data-layer/map-data-layer.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/maps/data-layer/map-data-layer.ts @@ -39,17 +39,19 @@ import { parseTbFunction, safeExecuteTbFunction } from '@core/utils'; -import L, { LatLngBounds } from 'leaflet'; +import L from 'leaflet'; import { CompiledTbFunction } from '@shared/models/js-function.models'; import { map } from 'rxjs/operators'; import { WidgetContext } from '@home/models/widget-component.models'; import { CustomTranslatePipe } from '@shared/pipe/custom-translate.pipe'; -export abstract class TbDataLayerItem, L extends L.Layer = L.Layer> { +export abstract class TbDataLayerItem = TbMapDataLayer, L extends L.Layer = L.Layer> { protected layer: L; protected tooltip: L.Popup; protected data: FormattedData; + protected selected = false; protected constructor(data: FormattedData, dsData: FormattedData[], @@ -61,6 +63,7 @@ export abstract class TbDataLayerItem { + if (!this.isEditing()) { + this.dataLayer.getMap().selectItem(this); + } + }); + this.layer.on('remove', () => { + if (this.selected) { + this.dataLayer.getMap().deselectItem(); + } + }); + } + } + protected enableEdit(): void { if (this.dataLayer.isHoverable()) { this.addItemClass('tb-hoverable'); @@ -110,16 +128,57 @@ export abstract class TbDataLayerItem { + this.removeDataItem(); + }, + iconClass: 'tb-remove' + }); + } + return buttons; + } else { + return []; + } + } + + public deselect() { + if (this.selected) { + this.selected = false; + this.layer.closePopup(); + this.updateSelectedState(); + } + } + + public isSelected() { + return this.selected; + } + public editModeUpdated() { if (this.dataLayer.isEditMode()) { this.enableEdit(); } else { this.disableEdit(); } + this.updateSelectedState(); } public update(data: FormattedData, dsData: FormattedData[]): void { @@ -128,14 +187,25 @@ export abstract class TbDataLayerItem, dsData: FormattedData[]) { if (this.settings.tooltip.show) { let tooltipTemplate = this.dataLayer.dataLayerTooltipProcessor.processPattern(data, dsData); @@ -157,13 +227,25 @@ export abstract class TbDataLayerItem { + if (this.tooltip.isOpen()) { + this.layer.closePopup(); + } else if (!this.isEditing()) { + this.layer.openPopup(); + } + }); + } else if (this.settings.tooltip.trigger === DataLayerTooltipTrigger.hover) { this.layer.on('mouseover', () => { - this.layer.openPopup(); + if (!this.isEditing()) { + this.layer.openPopup(); + } }); this.layer.on('mousemove', (e) => { this.tooltip.setLatLng(e.latlng); @@ -345,7 +427,7 @@ export abstract class TbMapDataLayer, _dsData: FormattedData[]): void { - this.dataLayer.getMap().markerClick(this.marker, data.$datasource); + this.dataLayer.getMap().markerClick(this, data.$datasource); } protected unbindLabel() { @@ -122,7 +128,7 @@ class TbMarkerDataLayerItem extends TbDataLayerItem, dsData: FormattedData[]) { this.dataLayer.markerIconProcessor.createMarkerIcon(data, dsData).subscribe( (iconInfo) => { - iconInfo.icon.options.className = this.updateIconClasses(iconInfo.icon.options.className); - this.marker.setIcon(iconInfo.icon); - const anchor = iconInfo.icon.options.iconAnchor; + let icon: L.Icon | L.DivIcon; + const options = deepClone(iconInfo.icon.options); + options.className = this.updateIconClasses(options.className); + if (iconInfo.icon instanceof L.Icon) { + icon = L.icon(options as L.IconOptions); + } else { + icon = L.divIcon(options); + } + this.marker.setIcon(icon); + const anchor = options.iconAnchor; if (anchor && Array.isArray(anchor)) { this.labelOffset = [iconInfo.size[0] / 2 - anchor[0], 10 - anchor[1]]; } else { @@ -195,11 +216,17 @@ class TbMarkerDataLayerItem extends TbDataLayerItem { if (!classes.includes(clazz)) { classes.push(clazz); diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/maps/data-layer/polygons-data-layer.ts b/ui-ngx/src/app/modules/home/components/widget/lib/maps/data-layer/polygons-data-layer.ts index b16952a317..58d51782fa 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/maps/data-layer/polygons-data-layer.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/maps/data-layer/polygons-data-layer.ts @@ -48,7 +48,8 @@ class TbPolygonDataLayerItem extends TbDataLayerItem, _dsData: FormattedData[]): void { - this.dataLayer.getMap().polygonClick(this.polygonContainer, data.$datasource); + this.dataLayer.getMap().polygonClick(this, data.$datasource); } protected unbindLabel() { @@ -103,7 +104,12 @@ class TbPolygonDataLayerItem extends TbDataLayerItem { this.editing = true; }); - this.polygon.on('pm:dragend', () => { + this.polygon.on('pm:drag', () => { + if (this.tooltip?.isOpen()) { + this.tooltip.setLatLng(this.polygon.getBounds().getCenter()); + } + }); + this.polygon.on('pm:dragend', (e) => { this.savePolygonCoordinates(); this.editing = false; }); @@ -115,6 +121,14 @@ class TbPolygonDataLayerItem extends TbDataLayerItem, coordinates: TbPolygonCoordinates): void { - const converted = this.map.coordinatesToPolygonData(coordinates); + const converted = coordinates ? this.map.coordinatesToPolygonData(coordinates) : null; const polygonData = [ { dataKey: this.settings.polygonKey, diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/maps/leaflet/leaflet-tb.ts b/ui-ngx/src/app/modules/home/components/widget/lib/maps/leaflet/leaflet-tb.ts index 0aafc0889c..f316975bf3 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/maps/leaflet/leaflet-tb.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/maps/leaflet/leaflet-tb.ts @@ -14,7 +14,7 @@ /// limitations under the License. /// -import L, { Coords, TB, TileLayerOptions } from 'leaflet'; +import L, { TB } from 'leaflet'; import { guid } from '@core/utils'; import 'leaflet-providers'; import '@geoman-io/leaflet-geoman-free'; @@ -276,6 +276,81 @@ class GroupsControl extends SidebarPaneControl { } } +class ToolbarButton extends L.Control { + private readonly button: JQuery; + constructor(options: TB.ToolbarButtonOptions) { + super(options); + + this.button = $("") + .attr('class', 'tb-control-button') + .attr('href', '#') + .attr('role', 'button') + .attr('title', this.options.title) + .html('
'); + + this.button.on('click', (e) => { + e.stopPropagation(); + e.preventDefault(); + this.options.click(e.originalEvent, this); + }); + } + + addToToolbar(toolbar: BottomToolbarControl): void { + this.button.appendTo(toolbar.container); + } +} + +class BottomToolbarControl extends L.Control { + + private readonly buttonContainer: JQuery; + + container: HTMLElement; + + constructor(options: TB.BottomToolbarControlOptions) { + super(options); + const controlContainer = $('.leaflet-control-container', options.mapElement); + const toolbar = $('
'); + toolbar.appendTo(controlContainer); + this.buttonContainer = $('
'); + this.buttonContainer.appendTo(toolbar); + this.container = this.buttonContainer[0]; + } + + addTo(map: L.Map): this { + return this; + } + + open(buttons: TB.ToolbarButtonOptions[]): void { + + buttons.forEach(buttonOption => { + const button = new ToolbarButton(buttonOption); + button.addToToolbar(this); + }); + + const closeButton = $("
") + .attr('class', 'tb-control-button') + .attr('href', '#') + .attr('role', 'button') + .attr('title', this.options.closeTitle) + .html('
'); + + closeButton.on('click', (e) => { + e.stopPropagation(); + e.preventDefault(); + this.close(); + }); + closeButton.appendTo(this.buttonContainer); + } + + close(): void { + this.buttonContainer.empty(); + if (this.options.onClose) { + this.options.onClose(); + } + } + +} + const sidebar = (options: TB.SidebarControlOptions): SidebarControl => { return new SidebarControl(options); } @@ -292,6 +367,10 @@ const groups = (options: TB.GroupsControlOptions): GroupsControl => { return new GroupsControl(options); } +const bottomToolbar = (options: TB.BottomToolbarControlOptions): BottomToolbarControl => { + return new BottomToolbarControl(options); +} + class ChinaProvider extends L.TileLayer { static chinaProviders: L.TB.TileLayer.ChinaProvidersData = { @@ -303,7 +382,7 @@ class ChinaProvider extends L.TileLayer { } }; - constructor(type: string, options?: TileLayerOptions) { + constructor(type: string, options?: L.TileLayerOptions) { options = options || {}; const parts = type.split('.'); @@ -316,7 +395,7 @@ class ChinaProvider extends L.TileLayer { super(url, options); } - getTileUrl(coords: Coords): string { + getTileUrl(coords: L.Coords): string { const data = { s: this._getSubdomain(coords), x: coords.x, @@ -338,7 +417,7 @@ class ChinaProvider extends L.TileLayer { } } -const chinaProvider = (type: string, options?: TileLayerOptions): ChinaProvider => { +const chinaProvider = (type: string, options?: L.TileLayerOptions): ChinaProvider => { return new ChinaProvider(type, options); } @@ -347,10 +426,13 @@ L.TB = L.TB || { SidebarPaneControl, LayersControl, GroupsControl, + ToolbarButton, + BottomToolbarControl, sidebar, sidebarPane, layers, groups, + bottomToolbar, TileLayer: { ChinaProvider }, diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/maps/map.scss b/ui-ngx/src/app/modules/home/components/widget/lib/maps/map.scss index 238d91f03f..fadb85b201 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/maps/map.scss +++ b/ui-ngx/src/app/modules/home/components/widget/lib/maps/map.scss @@ -17,6 +17,7 @@ //$map-element-hover-color: #307FE5; $map-element-hover-color: rgba(0,0,0,0.56); +$map-element-selected-color: #307FE5; .tb-map-layout { display: flex; @@ -79,6 +80,17 @@ $map-element-hover-color: rgba(0,0,0,0.56); } } } + .tb-map-bottom-toolbar { + left: 0; + right: 0; + display: flex; + flex-direction: row; + justify-content: center; + .leaflet-bar { + display: flex; + flex-direction: row; + } + } } .leaflet-control { .tb-control-button { @@ -100,6 +112,27 @@ $map-element-hover-color: rgba(0,0,0,0.56); &.tb-groups { mask-image: url('data:image/svg+xml,'); } + &.tb-remove { + mask-image: url('data:image/svg+xml,'); + } + &.tb-close { + background: #D12730; + mask-image: url('data:image/svg+xml,'); + } + } + } + } + .leaflet-map-pane:not(.leaflet-zoom-anim) { + .leaflet-marker-icon { + &.tb-hoverable:not(.tb-selected) { + svg { + transition: filter 0.2s; + } + } + } + img.leaflet-marker-icon, path { + &.tb-hoverable:not(.tb-selected) { + transition: filter 0.2s; } } } @@ -110,28 +143,35 @@ $map-element-hover-color: rgba(0,0,0,0.56); &.tb-draggable { cursor: move; } - &.tb-hoverable { - svg { - transition: filter 0.2s; - } + &.tb-hoverable:not(.tb-selected) { &:hover { svg { - filter: drop-shadow( 0 0 4px $map-element-hover-color); + //filter: drop-shadow( 0 0 4px $map-element-hover-color); + filter: brightness(0.8) drop-shadow( 0 0 4px $map-element-hover-color); } } } + &.tb-selected { + svg { + filter: brightness(0.8); + //animation: tb-selected-animation 0.5s linear 0s infinite alternate; + } + } } } img.leaflet-marker-icon, path { &.tb-draggable { cursor: move; } - &.tb-hoverable { - transition: filter 0.2s; + &.tb-hoverable:not(.tb-selected) { &:hover { - filter: drop-shadow( 0 0 4px $map-element-hover-color); + filter: brightness(0.8) drop-shadow( 0 0 4px $map-element-hover-color); } } + &.tb-selected { + filter: brightness(0.8); + //animation: tb-selected-animation 0.5s linear 0s infinite alternate; + } } .tb-cluster-marker-container { border: none; @@ -144,6 +184,11 @@ $map-element-hover-color: rgba(0,0,0,0.56); width: 40px; height: 40px; } + .tb-marker-label, .tb-polygon-label, .tb-circle-label { + border: none; + background: none; + box-shadow: none; + } } .tb-map-sidebar { .tb-layers, .tb-groups { @@ -267,3 +312,14 @@ $map-element-hover-color: rgba(0,0,0,0.56); } } } + +@keyframes tb-selected-animation { + 0% { + //filter: drop-shadow( 0 0 2px $map-element-selected-color); + filter: brightness(1); + } + 100% { + //filter: drop-shadow( 0 0 4px $map-element-selected-color) drop-shadow( 0 0 4px $map-element-selected-color); + filter: brightness(0.8); + } +} 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 672682591c..dc36f64bd2 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 @@ -32,7 +32,11 @@ import L from 'leaflet'; import { forkJoin, Observable, of } from 'rxjs'; import { switchMap } from 'rxjs/operators'; import '@home/components/widget/lib/maps/leaflet/leaflet-tb'; -import { MapDataLayerType, TbMapDataLayer, } from '@home/components/widget/lib/maps/data-layer/map-data-layer'; +import { + MapDataLayerType, + TbDataLayerItem, + TbMapDataLayer, +} from '@home/components/widget/lib/maps/data-layer/map-data-layer'; import { IWidgetSubscription, WidgetSubscriptionOptions } from '@core/api/widget-api.models'; import { FormattedData, WidgetActionDescriptor, widgetType } from '@shared/models/widget.models'; import { EntityDataPageLink } from '@shared/models/query/query.models'; @@ -44,6 +48,9 @@ import { AttributeService } from '@core/http/attribute.service'; import { AttributeData, AttributeScope, DataKeyType, LatestTelemetry } from '@shared/models/telemetry/telemetry.models'; import { EntityId } from '@shared/models/id/entity-id'; import ITooltipsterInstance = JQueryTooltipster.ITooltipsterInstance; +import TooltipPositioningSide = JQueryTooltipster.TooltipPositioningSide; + +type TooltipInstancesData = {root: HTMLElement, instances: ITooltipsterInstance[]}; export abstract class TbMap { @@ -59,10 +66,14 @@ export abstract class TbMap { protected dataLayers: TbMapDataLayer[]; protected dsData: FormattedData[]; + protected selectedDataItem: TbDataLayerItem; + protected mapElement: HTMLElement; protected sidebar: L.TB.SidebarControl; + protected editToolbar: L.TB.BottomToolbarControl; + private readonly mapResize$: ResizeObserver; private readonly tooltipActions: { [name: string]: MapActionHandler }; @@ -70,7 +81,7 @@ export abstract class TbMap { private readonly polygonClickActions: { [name: string]: MapActionHandler }; private readonly circleClickActions: { [name: string]: MapActionHandler }; - private tooltipInstances: ITooltipsterInstance[] = []; + private tooltipInstances: TooltipInstancesData[] = []; protected constructor(protected ctx: WidgetContext, protected inputSettings: DeepPartial, @@ -133,7 +144,7 @@ export abstract class TbMap { } this.setupDataLayers(); this.setupEditMode(); - this.createdControlButtonTooltip(); + this.createdControlButtonTooltip(this.mapElement, ['topleft', 'bottomleft'].includes(this.settings.controlsPosition) ? 'right' : 'left'); } private setupDataLayers() { @@ -231,21 +242,36 @@ export abstract class TbMap { } private setupEditMode() { - const dragEnabled = this.dataLayers.some(dl => dl.isDragEnabled()); - if (dragEnabled) { - //this.map.pm.enableGlobalDragMode(); - } + this.editToolbar = L.TB.bottomToolbar({ + mapElement: $(this.mapElement), + closeTitle: this.ctx.translate.instant('action.cancel'), + onClose: () => { + this.deselectItem(); + } + }).addTo(this.map); + + this.map.on('click', () => { + this.deselectItem(); + }); } - private createdControlButtonTooltip() { + private createdControlButtonTooltip(root: HTMLElement, side: TooltipPositioningSide) { import('tooltipster').then(() => { + let tooltipData = this.tooltipInstances.find(d => d.root === root); + if (!tooltipData) { + tooltipData = { + root, + instances: [] + } + this.tooltipInstances.push(tooltipData); + } if ($.tooltipster) { - this.tooltipInstances.forEach((instance) => { + tooltipData.instances.forEach((instance) => { instance.destroy(); }); - this.tooltipInstances = []; + tooltipData.instances = []; } - $(this.mapElement) + $(root) .find('a[role="button"]:not(.leaflet-pm-action)') .each((_index, element) => { let title: string; @@ -267,7 +293,7 @@ export abstract class TbMap { scroll: true, mouseleave: true }, - side: ['topleft', 'bottomleft'].includes(this.settings.controlsPosition) ? 'right' : 'left', + side, distance: 2, trackOrigin: true, functionBefore: (_instance, helper) => { @@ -277,7 +303,14 @@ export abstract class TbMap { }, } ); - this.tooltipInstances.push(tooltip.tooltipster('instance')); + const instance = tooltip.tooltipster('instance'); + tooltipData.instances.push(instance); + instance.on('destroyed', () => { + const index = tooltipData.instances.indexOf(instance); + if (index > -1) { + tooltipData.instances.splice(index, 1); + } + }); }); }); } @@ -385,36 +418,64 @@ export abstract class TbMap { } } - public markerClick(marker: L.Layer, datasource: TbMapDatasource): void { + public markerClick(marker: TbDataLayerItem, datasource: TbMapDatasource): void { if (Object.keys(this.markerClickActions).length) { - marker.on('click', (event: L.LeafletMouseEvent) => { - for (const action in this.markerClickActions) { - this.markerClickActions[action](event.originalEvent, datasource); + marker.getLayer().on('click', (event: L.LeafletMouseEvent) => { + if (!marker.isEditing()) { + for (const action in this.markerClickActions) { + this.markerClickActions[action](event.originalEvent, datasource); + } } }); } } - public polygonClick(polygon: L.Layer, datasource: TbMapDatasource): void { + public polygonClick(polygon: TbDataLayerItem, datasource: TbMapDatasource): void { if (Object.keys(this.polygonClickActions).length) { - polygon.on('click', (event: L.LeafletMouseEvent) => { - for (const action in this.polygonClickActions) { - this.polygonClickActions[action](event.originalEvent, datasource); + polygon.getLayer().on('click', (event: L.LeafletMouseEvent) => { + if (!polygon.isEditing()) { + for (const action in this.polygonClickActions) { + this.polygonClickActions[action](event.originalEvent, datasource); + } } }); } } - public circleClick(circle: L.Layer, datasource: TbMapDatasource): void { + public circleClick(circle: TbDataLayerItem, datasource: TbMapDatasource): void { if (Object.keys(this.circleClickActions).length) { - circle.on('click', (event: L.LeafletMouseEvent) => { - for (const action in this.circleClickActions) { - this.circleClickActions[action](event.originalEvent, datasource); + circle.getLayer().on('click', (event: L.LeafletMouseEvent) => { + if (!circle.isEditing()) { + for (const action in this.circleClickActions) { + this.circleClickActions[action](event.originalEvent, datasource); + } } }); } } + public selectItem(item: TbDataLayerItem): void { + if (this.selectedDataItem) { + this.selectedDataItem.deselect(); + this.selectedDataItem = null; + this.editToolbar.close(); + } + this.selectedDataItem = item; + if (this.selectedDataItem) { + const buttons = this.selectedDataItem.select(); + this.editToolbar.open(buttons); + this.createdControlButtonTooltip(this.editToolbar.container, 'top'); + } + } + + public deselectItem(): void { + this.selectItem(null); + } + + public getSelectedDataItem(): TbDataLayerItem { + return this.selectedDataItem; + } + public saveItemData(datasource: TbMapDatasource, data: DataKeyValuePair[]): Observable { const attributeService = this.ctx.$injector.get(AttributeService); const attributes: AttributeData[] = []; @@ -466,8 +527,10 @@ export abstract class TbMap { if (this.map) { this.map.remove(); } - this.tooltipInstances.forEach((instance) => { - instance.destroy(); + this.tooltipInstances.forEach((data) => { + data.instances.forEach(instance => { + instance.destroy(); + }) }); } diff --git a/ui-ngx/src/typings/leaflet-extend-tb.d.ts b/ui-ngx/src/typings/leaflet-extend-tb.d.ts index b5d40d3264..ab108c5c73 100644 --- a/ui-ngx/src/typings/leaflet-extend-tb.d.ts +++ b/ui-ngx/src/typings/leaflet-extend-tb.d.ts @@ -15,7 +15,7 @@ /// import { FormattedData } from '@shared/models/widget.models'; -import L from 'leaflet'; +import L, { Control, ControlOptions } from 'leaflet'; import { TbMapDatasource } from '@home/components/widget/lib/maps/models/map.models'; // redeclare module, maintains compatibility with @types/leaflet @@ -89,6 +89,30 @@ declare module 'leaflet' { constructor(options: GroupsControlOptions); } + interface ToolbarButtonOptions extends ControlOptions{ + title: string; + click: (e: MouseEvent, button: ToolbarButton) => void; + iconClass: string; + } + + class ToolbarButton extends Control{ + constructor(options: ToolbarButtonOptions); + addToToolbar(toolbar: BottomToolbarControl): void; + } + + interface BottomToolbarControlOptions extends ControlOptions { + mapElement: JQuery; + closeTitle: string; + onClose: () => void; + } + + class BottomToolbarControl extends Control { + constructor(options: BottomToolbarControlOptions); + open(buttons: ToolbarButtonOptions[]): void; + close(): void; + container: HTMLElement; + } + function sidebar(options: SidebarControlOptions): SidebarControl; function sidebarPane(options: O): SidebarPaneControl; @@ -97,6 +121,8 @@ declare module 'leaflet' { function groups(options: GroupsControlOptions): GroupsControl; + function bottomToolbar(options: BottomToolbarControlOptions): BottomToolbarControl; + namespace TileLayer { interface ChinaProvidersData { From cec50eac8029628fb5cf1f6f37c8edf0c2318089 Mon Sep 17 00:00:00 2001 From: Igor Kulikov Date: Tue, 28 Jan 2025 15:51:44 +0200 Subject: [PATCH 037/345] UI: Maps polygons edit mode. --- .../lib/maps/data-layer/circles-data-layer.ts | 23 +++ .../lib/maps/data-layer/map-data-layer.ts | 35 +++- .../lib/maps/data-layer/markers-data-layer.ts | 4 + .../maps/data-layer/polygons-data-layer.ts | 170 ++++++++++++++++++ .../widget/lib/maps/leaflet/leaflet-tb.ts | 57 +++++- .../home/components/widget/lib/maps/map.scss | 12 +- .../home/components/widget/lib/maps/map.ts | 35 ++-- .../assets/locale/locale.constant-en_US.json | 14 +- ui-ngx/src/typings/leaflet-extend-tb.d.ts | 11 +- 9 files changed, 333 insertions(+), 28 deletions(-) diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/maps/data-layer/circles-data-layer.ts b/ui-ngx/src/app/modules/home/components/widget/lib/maps/data-layer/circles-data-layer.ts index c0dcb8e980..7f877dbb00 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/maps/data-layer/circles-data-layer.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/maps/data-layer/circles-data-layer.ts @@ -111,6 +111,29 @@ class TbCircleDataLayerItem extends TbDataLayerItem this.editing = true); + this.circle.on('pm:markerdragend', () => this.editing = false); + this.circle.on('pm:edit', (e) => this.saveCircleCoordinates()); + this.circle.pm.enable(); + } + return []; + } + + protected onDeselected(): void { + if (this.dataLayer.isEditEnabled()) { + this.circle.pm.disable(); + this.circle.off('pm:markerdragstart'); + this.circle.off('pm:markerdragend'); + this.circle.off('pm:edit'); + } + } + + protected removeDataItemTitle(): string { + return this.dataLayer.getCtx().translate.instant('widgets.maps.data-layer.circle.remove-circle-for', {entityName: this.data.entityName}); + } + protected removeDataItem(): void { this.dataLayer.saveCircleCoordinates(this.data, null, null); } diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/maps/data-layer/map-data-layer.ts b/ui-ngx/src/app/modules/home/components/widget/lib/maps/data-layer/map-data-layer.ts index 9597464e9e..04672173f4 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/maps/data-layer/map-data-layer.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/maps/data-layer/map-data-layer.ts @@ -52,6 +52,7 @@ export abstract class TbDataLayerItem; protected selected = false; + protected removed = false; protected constructor(data: FormattedData, dsData: FormattedData[], @@ -143,11 +144,13 @@ export abstract class TbDataLayerItem { this.removeDataItem(); }, @@ -160,12 +163,19 @@ export abstract class TbDataLayerItem { + const map = this.dataLayer.getMap().getMap(); + if (!map.pm.globalCutModeEnabled()) { + this.disablePolygonRotateMode(); + this.disablePolygonEditMode(); + this.enablePolygonCutMode(button); + } else { + this.disablePolygonCutMode(button); + this.enablePolygonEditMode(); + } + } + }, + { + id: 'rotate', + title: this.getDataLayer().getCtx().translate.instant('widgets.maps.data-layer.polygon.rotate'), + iconClass: 'tb-rotate', + click: (e, button) => { + if (!this.polygon.pm.rotateEnabled()) { + this.disablePolygonCutMode(); + this.disablePolygonEditMode(); + this.enablePolygonRotateMode(button); + } else { + this.disablePolygonRotateMode(button); + this.enablePolygonEditMode(); + } + } + } + ); + } + return buttons; + } + + protected onDeselected(): void { + if (this.dataLayer.isEditEnabled()) { + this.disablePolygonEditMode(); + this.disablePolygonCutMode(); + this.disablePolygonRotateMode(); + } + } + + protected canDeselect(cancel = false): boolean { + if (!this.removed) { + const map = this.dataLayer.getMap().getMap(); + if (map.pm.globalCutModeEnabled()) { + if (cancel) { + this.disablePolygonCutMode(); + } + return false; + } else if (this.polygon.pm.rotateEnabled()) { + if (cancel) { + this.disablePolygonRotateMode(); + } + return false; + } + } + return true; + } + + protected removeDataItemTitle(): string { + return this.dataLayer.getCtx().translate.instant('widgets.maps.data-layer.polygon.remove-polygon-for', {entityName: this.data.entityName}); + } + protected removeDataItem(): void { this.dataLayer.savePolygonCoordinates(this.data, null); } @@ -129,6 +200,105 @@ class TbPolygonDataLayerItem extends TbDataLayerItem this.editing = true); + this.polygon.on('pm:markerdragend', () => this.editing = false); + this.polygon.on('pm:edit', (e) => this.savePolygonCoordinates()); + this.polygon.pm.enable(); + const map = this.dataLayer.getMap(); + map.getEditToolbar().getButton('remove')?.setDisabled(false); + } + + private disablePolygonEditMode() { + this.polygon.pm.disable(); + this.polygon.off('pm:markerdragstart'); + this.polygon.off('pm:markerdragend'); + this.polygon.off('pm:edit'); + const map = this.dataLayer.getMap(); + map.getEditToolbar().getButton('remove')?.setDisabled(true); + } + + private enablePolygonCutMode(cutButton?: L.TB.ToolbarButton) { + this.polygonContainer.closePopup(); + this.editing = true; + this.polygon.options.bubblingMouseEvents = true; + this.polygon.once('pm:cut', (e) => { + if (this.polygon instanceof L.Rectangle) { + this.polygonContainer.removeLayer(this.polygon); + // @ts-ignore + this.polygon = L.polygon(e.layer.getLatLngs(), { + ...this.polygonStyle, + snapIgnore: !this.dataLayer.isSnappable(), + bubblingMouseEvents: false + }); + this.polygon.addTo(this.polygonContainer); + } else { + // @ts-ignore + this.polygon.setLatLngs(e.layer.getLatLngs()); + } + // @ts-ignore + e.layer._pmTempLayer = true; + e.layer.remove(); + this.polygonContainer.removeLayer(this.polygon); + // @ts-ignore + this.polygon._pmTempLayer = false; + this.polygon.addTo(this.polygonContainer); + this.updateSelectedState(); + cutButton?.setActive(false); + this.savePolygonCoordinates() + }); + const map = this.dataLayer.getMap().getMap(); + map.pm.setLang('en', { + tooltips: { + firstVertex: this.getDataLayer().getCtx().translate.instant('widgets.maps.data-layer.polygon.firstVertex-cut'), + continueLine: this.getDataLayer().getCtx().translate.instant('widgets.maps.data-layer.polygon.continueLine-cut'), + finishPoly: this.getDataLayer().getCtx().translate.instant('widgets.maps.data-layer.polygon.finishPoly-cut') + } + }, 'en'); + map.pm.enableGlobalCutMode({ + // @ts-ignore + layersToCut: [this.polygon] + }); + cutButton?.setActive(true); + map.once('pm:globalcutmodetoggled', (e) => { + if (!e.enabled) { + this.disablePolygonCutMode(cutButton); + this.enablePolygonEditMode(); + } + }); + } + + private disablePolygonCutMode(cutButton?: L.TB.ToolbarButton) { + this.editing = false; + this.polygon.options.bubblingMouseEvents = false; + this.polygon.off('pm:cut'); + const map = this.dataLayer.getMap().getMap(); + map.pm.disableGlobalCutMode(); + cutButton?.setActive(false); + } + + private enablePolygonRotateMode(rotateButton?: L.TB.ToolbarButton) { + this.polygonContainer.closePopup(); + this.editing = true; + this.polygon.on('pm:rotateend', (e) => { + this.savePolygonCoordinates(); + }); + this.polygon.pm.enableRotate(); + rotateButton?.setActive(true); + this.polygon.on('pm:rotatedisable', (e) => { + this.disablePolygonRotateMode(rotateButton); + this.enablePolygonEditMode(); + }); + } + + private disablePolygonRotateMode(rotateButton?: L.TB.ToolbarButton) { + this.editing = false; + this.polygon.pm.disableRotate(); + this.polygon.off('pm:rotateend'); + this.polygon.off('pm:rotatedisable'); + rotateButton?.setActive(false); + } + private savePolygonCoordinates() { let coordinates: TbPolygonCoordinates = this.polygon.getLatLngs(); if (coordinates.length === 1) { diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/maps/leaflet/leaflet-tb.ts b/ui-ngx/src/app/modules/home/components/widget/lib/maps/leaflet/leaflet-tb.ts index f316975bf3..6c23323c86 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/maps/leaflet/leaflet-tb.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/maps/leaflet/leaflet-tb.ts @@ -277,10 +277,14 @@ class GroupsControl extends SidebarPaneControl { } class ToolbarButton extends L.Control { + private readonly id: string; private readonly button: JQuery; + private active = false; + private disabled = false; + constructor(options: TB.ToolbarButtonOptions) { super(options); - + this.id = options.id; this.button = $("
") .attr('class', 'tb-control-button') .attr('href', '#') @@ -295,14 +299,51 @@ class ToolbarButton extends L.Control { }); } + setActive(active: boolean): void { + if (this.active !== active) { + this.active = active; + if (this.active) { + L.DomUtil.addClass(this.button[0], 'active'); + } else { + L.DomUtil.removeClass(this.button[0], 'active'); + } + } + } + + isActive(): boolean { + return this.active; + } + + setDisabled(disabled: boolean): void { + if (this.disabled !== disabled) { + this.disabled = disabled; + if (this.disabled) { + L.DomUtil.addClass(this.button[0], 'leaflet-disabled'); + this.button[0].setAttribute('aria-disabled', 'true'); + } else { + L.DomUtil.removeClass(this.button[0], 'leaflet-disabled'); + this.button[0].setAttribute('aria-disabled', 'false'); + } + } + } + + isDisabled(): boolean { + return this.disabled; + } + addToToolbar(toolbar: BottomToolbarControl): void { this.button.appendTo(toolbar.container); } + + getId(): string { + return this.id; + } } class BottomToolbarControl extends L.Control { private readonly buttonContainer: JQuery; + private toolbarButtons: ToolbarButton[] = []; container: HTMLElement; @@ -320,10 +361,17 @@ class BottomToolbarControl extends L.Control { return this; } + getButton(id: string): ToolbarButton { + return this.toolbarButtons.find(b => b.getId() === id); + } + open(buttons: TB.ToolbarButtonOptions[]): void { + this.toolbarButtons.length = 0; + buttons.forEach(buttonOption => { const button = new ToolbarButton(buttonOption); + this.toolbarButtons.push(button); button.addToToolbar(this); }); @@ -343,9 +391,12 @@ class BottomToolbarControl extends L.Control { } close(): void { - this.buttonContainer.empty(); if (this.options.onClose) { - this.options.onClose(); + if (this.options.onClose()) { + this.buttonContainer.empty(); + } + } else { + this.buttonContainer.empty(); } } diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/maps/map.scss b/ui-ngx/src/app/modules/home/components/widget/lib/maps/map.scss index fadb85b201..2fc1baede6 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/maps/map.scss +++ b/ui-ngx/src/app/modules/home/components/widget/lib/maps/map.scss @@ -55,10 +55,14 @@ $map-element-selected-color: #307FE5; position: relative; background: transparent; &.leaflet-disabled { + pointer-events: none; color: rgba(0, 0, 0, 0.18); + > div { + background: rgba(0, 0, 0, 0.18); + } } &:not(.leaflet-disabled) { - &:hover { + &:hover, &.active { border-bottom: none; color: $tb-primary-color; // primary color &:before { @@ -115,6 +119,12 @@ $map-element-selected-color: #307FE5; &.tb-remove { mask-image: url('data:image/svg+xml,'); } + &.tb-cut { + mask-image: url('data:image/svg+xml,'); + } + &.tb-rotate { + mask-image: url('data:image/svg+xml,'); + } &.tb-close { background: #D12730; mask-image: url('data:image/svg+xml,'); 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 dc36f64bd2..f391754674 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 @@ -246,7 +246,7 @@ export abstract class TbMap { mapElement: $(this.mapElement), closeTitle: this.ctx.translate.instant('action.cancel'), onClose: () => { - this.deselectItem(); + return this.deselectItem(true); } }).addTo(this.map); @@ -454,28 +454,39 @@ export abstract class TbMap { } } - public selectItem(item: TbDataLayerItem): void { + public selectItem(item: TbDataLayerItem, cancel = false): boolean { + let deselected = true; if (this.selectedDataItem) { - this.selectedDataItem.deselect(); - this.selectedDataItem = null; - this.editToolbar.close(); + deselected = this.selectedDataItem.deselect(cancel); + if (deselected) { + this.selectedDataItem = null; + this.editToolbar.close(); + } } - this.selectedDataItem = item; - if (this.selectedDataItem) { - const buttons = this.selectedDataItem.select(); - this.editToolbar.open(buttons); - this.createdControlButtonTooltip(this.editToolbar.container, 'top'); + if (deselected) { + this.selectedDataItem = item; + if (this.selectedDataItem) { + const buttons = this.selectedDataItem.select(); + this.editToolbar.open(buttons); + this.createdControlButtonTooltip(this.editToolbar.container, 'top'); + } } + this.ignoreUpdateBounds = !!this.selectedDataItem; + return deselected; } - public deselectItem(): void { - this.selectItem(null); + public deselectItem(cancel = false): boolean { + return this.selectItem(null, cancel); } public getSelectedDataItem(): TbDataLayerItem { return this.selectedDataItem; } + public getEditToolbar(): L.TB.BottomToolbarControl { + return this.editToolbar; + } + public saveItemData(datasource: TbMapDatasource, data: DataKeyValuePair[]): Observable { const attributeService = this.ctx.$injector.get(AttributeService); const attributes: AttributeData[] = []; 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 94147c8b20..33d8797a2d 100644 --- a/ui-ngx/src/assets/locale/locale.constant-en_US.json +++ b/ui-ngx/src/assets/locale/locale.constant-en_US.json @@ -7716,7 +7716,8 @@ "use-cluster-marker-color-function": "Use cluster markers color function", "marker-color-function": "Marker color function" }, - "edit": "Edit marker" + "edit": "Edit marker", + "remove-marker-for": "Remove marker for '{{entityName}}'" }, "polygon": { "polygon-key": "Polygon key", @@ -7725,7 +7726,13 @@ "add-polygon": "Add polygon", "polygon-configuration": "Polygon configuration", "remove-polygon": "Remove polygon", - "edit": "Edit polygon" + "edit": "Edit polygon", + "remove-polygon-for": "Remove polygon for '{{entityName}}'", + "cut": "Cut polygon area", + "rotate": "Rotate polygon", + "firstVertex-cut": "Click to place first point", + "continueLine-cut": "Click to continue drawing", + "finishPoly-cut": "Click first marker to finish and save" }, "circle": { "circle-key": "Circle key", @@ -7734,7 +7741,8 @@ "add-circle": "Add circle", "circle-configuration": "Circle configuration", "remove-circle": "Remove circle", - "edit": "Edit circle" + "edit": "Edit circle", + "remove-circle-for": "Remove circle for '{{entityName}}'" } }, "select-entity": "Select entity", diff --git a/ui-ngx/src/typings/leaflet-extend-tb.d.ts b/ui-ngx/src/typings/leaflet-extend-tb.d.ts index ab108c5c73..5ae83c1679 100644 --- a/ui-ngx/src/typings/leaflet-extend-tb.d.ts +++ b/ui-ngx/src/typings/leaflet-extend-tb.d.ts @@ -89,7 +89,8 @@ declare module 'leaflet' { constructor(options: GroupsControlOptions); } - interface ToolbarButtonOptions extends ControlOptions{ + interface ToolbarButtonOptions extends ControlOptions { + id: string; title: string; click: (e: MouseEvent, button: ToolbarButton) => void; iconClass: string; @@ -97,17 +98,21 @@ declare module 'leaflet' { class ToolbarButton extends Control{ constructor(options: ToolbarButtonOptions); - addToToolbar(toolbar: BottomToolbarControl): void; + setActive(active: boolean): void; + isActive(): boolean; + setDisabled(disabled: boolean): void; + isDisabled(): boolean; } interface BottomToolbarControlOptions extends ControlOptions { mapElement: JQuery; closeTitle: string; - onClose: () => void; + onClose: () => boolean; } class BottomToolbarControl extends Control { constructor(options: BottomToolbarControlOptions); + getButton(id: string): ToolbarButton | undefined; open(buttons: ToolbarButtonOptions[]): void; close(): void; container: HTMLElement; From f8751003b58b88f864dd52d2793f75d8826ba3b8 Mon Sep 17 00:00:00 2001 From: Igor Kulikov Date: Tue, 28 Jan 2025 20:11:46 +0200 Subject: [PATCH 038/345] UI: Map - improve edit mode states. Add draw map items buttons. --- .../lib/maps/data-layer/map-data-layer.ts | 48 ++++++----- .../lib/maps/data-layer/markers-data-layer.ts | 6 +- .../maps/data-layer/polygons-data-layer.ts | 31 ++++--- .../widget/lib/maps/leaflet/leaflet-tb.ts | 44 ++++++++-- .../home/components/widget/lib/maps/map.scss | 77 +++++++++-------- .../home/components/widget/lib/maps/map.ts | 82 +++++++++++++++++-- .../assets/locale/locale.constant-en_US.json | 10 ++- ui-ngx/src/typings/leaflet-extend-tb.d.ts | 7 ++ 8 files changed, 222 insertions(+), 83 deletions(-) diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/maps/data-layer/map-data-layer.ts b/ui-ngx/src/app/modules/home/components/widget/lib/maps/data-layer/map-data-layer.ts index 04672173f4..4dfa17d9f2 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/maps/data-layer/map-data-layer.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/maps/data-layer/map-data-layer.ts @@ -52,7 +52,6 @@ export abstract class TbDataLayerItem; protected selected = false; - protected removed = false; protected constructor(data: FormattedData, dsData: FormattedData[], @@ -101,11 +100,6 @@ export abstract class TbDataLayerItem { - if (this.selected) { - this.dataLayer.getMap().deselectItem(); - } - }); } } @@ -163,9 +157,9 @@ export abstract class TbDataLayerItem[] = []; + public dataLayerLabelProcessor: DataLayerPatternProcessor; public dataLayerTooltipProcessor: DataLayerPatternProcessor; @@ -504,6 +499,12 @@ export abstract class TbMapDataLayer[]) { + this.unplacedItems.length = 0; const layerData = dsData.filter(d => d.$datasource.mapDataIds.includes(this.mapDataId)); - const rawItems = layerData.filter(d => this.isValidLayerData(d)); const toDelete = new Set(Array.from(this.layerItems.keys())); const updatedItems: TbDataLayerItem[] = []; - rawItems.forEach((data) => { - let layerItem = this.layerItems.get(data.entityId); - if (layerItem) { - layerItem.update(data, dsData); - updatedItems.push(layerItem); + layerData.forEach((data) => { + if (this.isValidLayerData(data)) { + let layerItem = this.layerItems.get(data.entityId); + if (layerItem) { + layerItem.update(data, dsData); + updatedItems.push(layerItem); + } else { + layerItem = this.createLayerItem(data, dsData); + this.layerItems.set(data.entityId, layerItem); + } + toDelete.delete(data.entityId); } else { - layerItem = this.createLayerItem(data, dsData); - this.layerItems.set(data.entityId, layerItem); + this.unplacedItems.push(data); } - toDelete.delete(data.entityId); }); toDelete.forEach((key) => { const item = this.layerItems.get(key); @@ -545,10 +550,15 @@ export abstract class TbMapDataLayer { return this.map; } + public hasUnplacedItems(): boolean { + return !!this.unplacedItems.length; + } + protected createDataLayerContainer(): L.FeatureGroup { return L.featureGroup([], {snapIgnore: !this.snappable}); } 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 fc635e1c7c..e3fee244cb 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 @@ -202,10 +202,10 @@ class TbMarkerDataLayerItem extends TbDataLayerItem, _dsData: FormattedData[]): void { @@ -171,19 +173,17 @@ class TbPolygonDataLayerItem extends TbDataLayerItem { if (this.polygon instanceof L.Rectangle) { this.polygonContainer.removeLayer(this.polygon); @@ -271,6 +274,8 @@ class TbPolygonDataLayerItem extends TbDataLayerItem { class ToolbarButton extends L.Control { private readonly id: string; - private readonly button: JQuery; + private readonly button: JQuery; private active = false; private disabled = false; @@ -331,13 +331,39 @@ class ToolbarButton extends L.Control { return this.disabled; } - addToToolbar(toolbar: BottomToolbarControl): void { - this.button.appendTo(toolbar.container); - } - getId(): string { return this.id; } + + getButtonElement(): JQuery { + return this.button; + } +} + +class ToolbarControl extends L.Control { + + private buttonContainer: JQuery; + + constructor(options: L.ControlOptions) { + super(options); + } + + toolbarButton(options: TB.ToolbarButtonOptions): ToolbarButton { + const button = new ToolbarButton(options); + button.getButtonElement().appendTo(this.buttonContainer); + return button; + } + + onAdd(map: L.Map): HTMLElement { + this.buttonContainer = $(" +
+
widgets.maps.data-layer.behavior
+
+
widgets.maps.data-layer.on-click
+ + +
+
{{ 'widgets.maps.data-layer.groups' | translate }}
Date: Fri, 31 Jan 2025 12:22:28 +0200 Subject: [PATCH 043/345] Process whole widget default config for images --- .../org/thingsboard/server/dao/resource/BaseImageService.java | 1 + 1 file changed, 1 insertion(+) diff --git a/dao/src/main/java/org/thingsboard/server/dao/resource/BaseImageService.java b/dao/src/main/java/org/thingsboard/server/dao/resource/BaseImageService.java index 172b91a32c..a376c5785e 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/resource/BaseImageService.java +++ b/dao/src/main/java/org/thingsboard/server/dao/resource/BaseImageService.java @@ -385,6 +385,7 @@ public class BaseImageService extends BaseResourceService implements ImageServic JsonNode defaultConfig = widgetTypeDetails.getDefaultConfig(); if (defaultConfig != null) { updated |= convertToImageUrlsByMapping(tenantId, WIDGET_TYPE_BASE64_MAPPING, Collections.singletonMap("prefix", prefix), defaultConfig, imagesLinks); + updated |= convertToImageUrls(tenantId, prefix, defaultConfig, imagesLinks); widgetTypeDetails.setDefaultConfig(defaultConfig); } } From 66608d3e2b004339340ef47bbeca0d82c459c348 Mon Sep 17 00:00:00 2001 From: Igor Kulikov Date: Fri, 31 Jan 2025 15:03:15 +0200 Subject: [PATCH 044/345] UI: Update map widgets --- .../json/system/widget_types/image_map.json | 46 +------------------ .../data/json/system/widget_types/map.json | 46 +------------------ 2 files changed, 2 insertions(+), 90 deletions(-) diff --git a/application/src/main/data/json/system/widget_types/image_map.json b/application/src/main/data/json/system/widget_types/image_map.json index 27af2bc724..b7e8644662 100644 --- a/application/src/main/data/json/system/widget_types/image_map.json +++ b/application/src/main/data/json/system/widget_types/image_map.json @@ -17,7 +17,7 @@ "settingsDirective": "tb-map-widget-settings", "hasBasicMode": true, "basicModeDirective": "tb-map-basic-config", - "defaultConfig": "{\"datasources\":[],\"timewindow\":{\"realtime\":{\"timewindowMs\":60000}},\"showTitle\":true,\"backgroundColor\":\"rgba(0, 0, 0, 0)\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"0px\",\"settings\":{\"mapType\":\"image\",\"layers\":[],\"markers\":[{\"dsType\":\"function\",\"dsLabel\":\"First point\",\"dsDeviceId\":null,\"dsEntityAliasId\":null,\"dsFilterId\":null,\"additionalDataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"temperature\",\"color\":\"#2196f3\",\"settings\":{},\"_hash\":0.8239425680406081,\"funcBody\":\"var value = prevValue + Math.random() * 40 - 20;\\nif (value < -60) {\\n\\tvalue = -60;\\n} else if (value > 60) {\\n\\tvalue = 60;\\n}\\nreturn value;\",\"aggregationType\":null,\"units\":null,\"decimals\":null,\"usePostProcessing\":null,\"postFuncBody\":null}],\"label\":{\"show\":true,\"type\":\"pattern\",\"pattern\":\"${entityName}\"},\"tooltip\":{\"show\":true,\"type\":\"pattern\",\"pattern\":\"${entityName}

X Pos: ${xPos:2}
Y Pos: ${yPos:2}
Temperature: ${temperature} °C
See tooltip settings for details\",\"patternFunction\":null,\"trigger\":\"click\",\"autoclose\":true,\"offsetX\":0,\"offsetY\":-1},\"groups\":null,\"xKey\":{\"name\":\"f(x)\",\"label\":\"latitude\",\"type\":\"function\",\"funcBody\":\"var value = prevValue || 0.2;\\nif (time % 5000 < 500) {\\n value += Math.random() * 0.05 - 0.025;\\n}\\nreturn value;\",\"settings\":{},\"color\":\"#2196f3\",\"aggregationType\":null,\"units\":null,\"decimals\":null,\"usePostProcessing\":null,\"postFuncBody\":null},\"yKey\":{\"name\":\"f(x)\",\"label\":\"longitude\",\"type\":\"function\",\"funcBody\":\"var value = prevValue || 0.3;\\nif (time % 5000 < 500) {\\n value += Math.random() * 0.05 - 0.025;\\n}\\nreturn value;\",\"settings\":{},\"color\":\"#2196f3\",\"aggregationType\":null,\"units\":null,\"decimals\":null,\"usePostProcessing\":null,\"postFuncBody\":null},\"markerType\":\"shape\",\"markerShape\":{\"shape\":\"markerShape1\",\"size\":34,\"color\":{\"type\":\"function\",\"color\":\"#307FE5\",\"colorFunction\":\"var temperature = data.temperature;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\\n\"}},\"markerIcon\":{\"icon\":\"mdi:lightbulb-on\",\"size\":34,\"color\":{\"type\":\"constant\",\"color\":\"#307FE5\"}},\"markerImage\":{\"type\":\"image\",\"image\":\"data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9Ii0xOTEuMzUgLTM1MS4xOCAxMDgzLjU4IDE3MzAuNDYiPjxwYXRoIGZpbGwtcnVsZT0iZXZlbm9kZCIgY2xpcC1ydWxlPSJldmVub2RkIiBmaWxsPSIjZmU3NTY5IiBzdHJva2U9IiMwMDAiIHN0cm9rZS13aWR0aD0iMzciIHN0cm9rZS1taXRlcmxpbWl0PSIxMCIgZD0iTTM1MS44MzMgMTM2MC43OGMtMzguNzY2LTE5MC4zLTEwNy4xMTYtMzQ4LjY2NS0xODkuOTAzLTQ5NS40NEMxMDAuNTIzIDc1Ni40NjkgMjkuMzg2IDY1NS45NzgtMzYuNDM0IDU1MC40MDRjLTIxLjk3Mi0zNS4yNDQtNDAuOTM0LTcyLjQ3Ny02Mi4wNDctMTA5LjA1NC00Mi4yMTYtNzMuMTM3LTc2LjQ0NC0xNTcuOTM1LTc0LjI2OS0yNjcuOTMyIDIuMTI1LTEwNy40NzMgMzMuMjA4LTE5My42ODUgNzguMDMtMjY0LjE3M0MtMjEtMjA2LjY5IDEwMi40ODEtMzAxLjc0NSAyNjguMTY0LTMyNi43MjRjMTM1LjQ2Ni0yMC40MjUgMjYyLjQ3NSAxNC4wODIgMzUyLjU0MyA2Ni43NDcgNzMuNiA0My4wMzggMTMwLjU5NiAxMDAuNTI4IDE3My45MiAxNjguMjggNDUuMjIgNzAuNzE2IDc2LjM2IDE1NC4yNiA3OC45NzEgMjYzLjIzMyAxLjMzNyA1NS44My03LjgwNSAxMDcuNTMyLTIwLjY4NCAxNTAuNDE3LTEzLjAzNCA0My40MS0zMy45OTYgNzkuNjk1LTUyLjY0NiAxMTguNDU1LTM2LjQwNiA3NS42NTktODIuMDQ5IDE0NC45ODEtMTI3Ljg1NSAyMTQuMzQ1LTEzNi40MzcgMjA2LjYwNi0yNjQuNDk2IDQxNy4zMS0zMjAuNTggNzA2LjAyOHoiLz48Y2lyY2xlIGZpbGwtcnVsZT0iZXZlbm9kZCIgY2xpcC1ydWxlPSJldmVub2RkIiBjeD0iMzUyLjg5MSIgY3k9IjIyNS43NzkiIHI9IjE4My4zMzIiLz48L3N2Zz4=\",\"imageSize\":34},\"markerOffsetX\":0.5,\"markerOffsetY\":1,\"positionFunction\":\"return {x: origXPos, y: origYPos};\",\"markerClustering\":{\"enable\":false,\"zoomOnClick\":true,\"maxZoom\":null,\"maxClusterRadius\":80,\"zoomAnimation\":true,\"showCoverageOnHover\":true,\"spiderfyOnMaxZoom\":false,\"chunkedLoad\":false,\"lazyLoad\":true,\"useClusterMarkerColorFunction\":false,\"clusterMarkerColorFunction\":null}},{\"dsType\":\"function\",\"dsLabel\":\"Second point\",\"dsDeviceId\":null,\"dsEntityAliasId\":null,\"dsFilterId\":null,\"additionalDataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"temperature\",\"color\":\"#2196f3\",\"settings\":{},\"_hash\":0.7826299113906372,\"funcBody\":\"var value = prevValue + Math.random() * 40 - 20;\\nif (value < -60) {\\n\\tvalue = -60;\\n} else if (value > 60) {\\n\\tvalue = 60;\\n}\\nreturn value;\",\"aggregationType\":null,\"units\":null,\"decimals\":null,\"usePostProcessing\":null,\"postFuncBody\":null}],\"label\":{\"show\":true,\"type\":\"pattern\",\"pattern\":\"${entityName}\"},\"tooltip\":{\"show\":true,\"type\":\"pattern\",\"pattern\":\"${entityName}

X Pos: ${xPos:2}
Y Pos: ${yPos:2}
Temperature: ${temperature} °C
See tooltip settings for details\",\"patternFunction\":null,\"trigger\":\"click\",\"autoclose\":true,\"offsetX\":0,\"offsetY\":-1},\"groups\":null,\"xKey\":{\"name\":\"f(x)\",\"label\":\"latitude\",\"type\":\"function\",\"funcBody\":\"var value = prevValue || 0.6;\\nif (time % 4000 < 500) {\\n value += Math.random() * 0.05 - 0.025;\\n}\\nreturn value;\",\"settings\":{},\"color\":\"#2196f3\",\"aggregationType\":null,\"units\":null,\"decimals\":null,\"usePostProcessing\":null,\"postFuncBody\":null},\"yKey\":{\"name\":\"f(x)\",\"label\":\"longitude\",\"type\":\"function\",\"funcBody\":\"var value = prevValue || 0.7;\\nif (time % 4000 < 500) {\\n value += Math.random() * 0.05 - 0.025;\\n}\\nreturn value;\",\"settings\":{},\"color\":\"#2196f3\",\"aggregationType\":null,\"units\":null,\"decimals\":null,\"usePostProcessing\":null,\"postFuncBody\":null},\"markerType\":\"image\",\"markerShape\":{\"shape\":\"markerShape1\",\"size\":34,\"color\":{\"type\":\"constant\",\"color\":\"#307FE5\"}},\"markerIcon\":{\"icon\":\"\",\"size\":34,\"color\":{\"type\":\"constant\",\"color\":\"#307FE5\"}},\"markerImage\":{\"type\":\"function\",\"image\":\"data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSIzNCIgaGVpZ2h0PSIzNCIgdmlld0JveD0iMCAwIDM0IDM0IiBmaWxsPSJub25lIj4KICA8ZyBmaWx0ZXI9InVybCgjZmlsdGVyMF9iZl84ODE2XzI2Mzg4NykiPgogICAgPHBhdGggZD0iTTE5IDI0LjVDMTcuNDA3NSAyNy40MTI1IDE3IDMzIDE3IDMzQzE3IDMzIDI3LjA4NTggMzIuMTk1NSAzMC45OTkyIDI3LjQ5OThDMzQgMjMuODk5MiAzMS45OTkyIDE5IDI3Ljk5OTIgMTlDMjMuOTk5MyAxOSAyMS4xOTI5IDIwLjQ4OTQgMTkgMjQuNVoiIGZpbGw9ImJsYWNrIiBmaWxsLW9wYWNpdHk9IjAuMjQiLz4KICA8L2c+CiAgPG1hc2sgaWQ9InBhdGgtMi1pbnNpZGUtMV84ODE2XzI2Mzg4NyIgZmlsbD0id2hpdGUiPgogICAgPHBhdGggZmlsbC1ydWxlPSJldmVub2RkIiBjbGlwLXJ1bGU9ImV2ZW5vZGQiIGQ9Ik0yOCAxMS45QzI4LjAwMzcgNS4zMjc2MSAyNC4yOTAyIDAgMTcgMEM5LjcwOTgzIDAgNS45OTYzIDUuMzI3NjEgNiAxMS45QzYuMDA0NzMgMjAuMjkzNyAxNyAzNCAxNyAzNEMxNyAzNCAyNy45OTUzIDIwLjI5MzcgMjggMTEuOVpNMjEuMjUgMTAuNjI1QzIxLjI1IDEyLjk3MjIgMTkuMzQ3MiAxNC44NzUgMTcgMTQuODc1QzE0LjY1MjggMTQuODc1IDEyLjc1IDEyLjk3MjIgMTIuNzUgMTAuNjI1QzEyLjc1IDguMjc3NzkgMTQuNjUyOCA2LjM3NSAxNyA2LjM3NUMxOS4zNDcyIDYuMzc1IDIxLjI1IDguMjc3NzkgMjEuMjUgMTAuNjI1WiIvPgogIDwvbWFzaz4KICA8cGF0aCBmaWxsLXJ1bGU9ImV2ZW5vZGQiIGNsaXAtcnVsZT0iZXZlbm9kZCIgZD0iTTI4IDExLjlDMjguMDAzNyA1LjMyNzYxIDI0LjI5MDIgMCAxNyAwQzkuNzA5ODMgMCA1Ljk5NjMgNS4zMjc2MSA2IDExLjlDNi4wMDQ3MyAyMC4yOTM3IDE3IDM0IDE3IDM0QzE3IDM0IDI3Ljk5NTMgMjAuMjkzNyAyOCAxMS45Wk0yMS4yNSAxMC42MjVDMjEuMjUgMTIuOTcyMiAxOS4zNDcyIDE0Ljg3NSAxNyAxNC44NzVDMTQuNjUyOCAxNC44NzUgMTIuNzUgMTIuOTcyMiAxMi43NSAxMC42MjVDMTIuNzUgOC4yNzc3OSAxNC42NTI4IDYuMzc1IDE3IDYuMzc1QzE5LjM0NzIgNi4zNzUgMjEuMjUgOC4yNzc3OSAyMS4yNSAxMC42MjVaIiBmaWxsPSIjMzA3ZmU1Ii8+CiAgPHBhdGggZD0iTTI4IDExLjlMMjkuMDYyNSAxMS45MDA2TDI4IDExLjlaTTYgMTEuOUw3LjA2MjUgMTEuODk5NEw2IDExLjlaTTE3IDM0TDE2LjE3MTIgMzQuNjY0OUwxNyAzNS42OThMMTcuODI4OCAzNC42NjQ5TDE3IDM0Wk0xNyAxLjA2MjVDMjAuMzY0MSAxLjA2MjUgMjIuODA4NSAyLjI4MDA2IDI0LjQyNzMgNC4xNzUzOUMyNi4wNjUyIDYuMDkzMjMgMjYuOTM5MiA4LjgwMzMxIDI2LjkzNzUgMTEuODk5NEwyOS4wNjI1IDExLjkwMDZDMjkuMDY0NCA4LjQyNDMgMjguMDgzNSA1LjE4NDM4IDI2LjA0MzEgMi43OTUzMkMyMy45ODM1IDAuMzgzNzQyIDIwLjkyNjEgLTEuMDYyNSAxNyAtMS4wNjI1VjEuMDYyNVpNNy4wNjI1IDExLjg5OTRDNy4wNjA3NiA4LjgwMzMxIDcuOTM0NzcgNi4wOTMyMyA5LjU3Mjc0IDQuMTc1MzlDMTEuMTkxNSAyLjI4MDA2IDEzLjYzNTkgMS4wNjI1IDE3IDEuMDYyNVYtMS4wNjI1QzEzLjA3MzkgLTEuMDYyNSAxMC4wMTY1IDAuMzgzNzQxIDcuOTU2ODYgMi43OTUzMkM1LjkxNjQ1IDUuMTg0MzggNC45MzU1NSA4LjQyNDMgNC45Mzc1IDExLjkwMDZMNy4wNjI1IDExLjg5OTRaTTE3IDM0QzE3LjgyODggMzMuMzM1MSAxNy44Mjg4IDMzLjMzNTIgMTcuODI4OCAzMy4zMzUyQzE3LjgyODggMzMuMzM1MiAxNy44Mjg4IDMzLjMzNTEgMTcuODI4NyAzMy4zMzVDMTcuODI4NSAzMy4zMzQ4IDE3LjgyODEgMzMuMzM0MyAxNy44Mjc2IDMzLjMzMzZDMTcuODI2NSAzMy4zMzIzIDE3LjgyNDggMzMuMzMwMSAxNy44MjI0IDMzLjMyNzFDMTcuODE3NiAzMy4zMjEyIDE3LjgxMDMgMzMuMzEyIDE3LjgwMDQgMzMuMjk5NUMxNy43ODA3IDMzLjI3NDcgMTcuNzUwOSAzMy4yMzcxIDE3LjcxMTggMzMuMTg3NEMxNy42MzM1IDMzLjA4NzggMTcuNTE3OCAzMi45Mzk1IDE3LjM3IDMyLjc0NzJDMTcuMDc0MiAzMi4zNjI0IDE2LjY1MDMgMzEuODAxNyAxNi4xNDA5IDMxLjEwMTlDMTUuMTIxNCAyOS43MDE0IDEzLjc2MzQgMjcuNzQ5NSAxMi40MDcxIDI1LjU0MTVDMTEuMDQ5IDIzLjMzMDYgOS43MDM4OCAyMC44ODEzIDguNzAwOTEgMTguNDg0MUM3LjY5MTA5IDE2LjA3MDYgNy4wNjM1NyAxMy43OTIxIDcuMDYyNSAxMS44OTk0TDQuOTM3NSAxMS45MDA2QzQuOTM4OCAxNC4yMDQ4IDUuNjg3NDYgMTYuNzg3MyA2Ljc0MDU4IDE5LjMwNDNDNy44MDA1NSAyMS44Mzc4IDkuMjA1MTYgMjQuMzg4OSAxMC41OTY0IDI2LjY1MzhDMTEuOTg5NSAyOC45MjE2IDEzLjM4MDcgMzAuOTIwOSAxNC40MjI5IDMyLjM1MjZDMTQuOTQ0NCAzMy4wNjg5IDE1LjM3OTUgMzMuNjQ0NiAxNS42ODUxIDM0LjA0MjJDMTUuODM3OSAzNC4yNDEgMTUuOTU4NCAzNC4zOTU0IDE2LjA0MTIgMzQuNTAwN0MxNi4wODI2IDM0LjU1MzQgMTYuMTE0NiAzNC41OTM4IDE2LjEzNjUgMzQuNjIxM0MxNi4xNDc0IDM0LjYzNTEgMTYuMTU1OSAzNC42NDU2IDE2LjE2MTcgMzQuNjUyOUMxNi4xNjQ2IDM0LjY1NjYgMTYuMTY2OCAzNC42NTk0IDE2LjE2ODQgMzQuNjYxNEMxNi4xNjkyIDM0LjY2MjQgMTYuMTY5OSAzNC42NjMyIDE2LjE3MDMgMzQuNjYzN0MxNi4xNzA2IDM0LjY2NCAxNi4xNzA4IDM0LjY2NDMgMTYuMTcwOSAzNC42NjQ1QzE2LjE3MTEgMzQuNjY0NyAxNi4xNzEyIDM0LjY2NDkgMTcgMzRaTTI2LjkzNzUgMTEuODk5NEMyNi45MzY0IDEzLjc5MjEgMjYuMzA4OSAxNi4wNzA2IDI1LjI5OTEgMTguNDg0MUMyNC4yOTYxIDIwLjg4MTMgMjIuOTUxIDIzLjMzMDYgMjEuNTkyOSAyNS41NDE1QzIwLjIzNjYgMjcuNzQ5NSAxOC44Nzg2IDI5LjcwMTQgMTcuODU5MSAzMS4xMDE5QzE3LjM0OTcgMzEuODAxNyAxNi45MjU4IDMyLjM2MjQgMTYuNjMgMzIuNzQ3MkMxNi40ODIyIDMyLjkzOTUgMTYuMzY2NSAzMy4wODc4IDE2LjI4ODIgMzMuMTg3NEMxNi4yNDkxIDMzLjIzNzEgMTYuMjE5MyAzMy4yNzQ3IDE2LjE5OTYgMzMuMjk5NUMxNi4xODk3IDMzLjMxMiAxNi4xODI0IDMzLjMyMTIgMTYuMTc3NiAzMy4zMjcxQzE2LjE3NTIgMzMuMzMwMSAxNi4xNzM1IDMzLjMzMjMgMTYuMTcyNCAzMy4zMzM2QzE2LjE3MTkgMzMuMzM0MyAxNi4xNzE1IDMzLjMzNDggMTYuMTcxMyAzMy4zMzVDMTYuMTcxMiAzMy4zMzUxIDE2LjE3MTIgMzMuMzM1MiAxNi4xNzEyIDMzLjMzNTJDMTYuMTcxMiAzMy4zMzUyIDE2LjE3MTIgMzMuMzM1MSAxNyAzNEMxNy44Mjg4IDM0LjY2NDkgMTcuODI4OSAzNC42NjQ3IDE3LjgyOTEgMzQuNjY0NUMxNy44MjkyIDM0LjY2NDMgMTcuODI5NCAzNC42NjQgMTcuODI5NyAzNC42NjM3QzE3LjgzMDEgMzQuNjYzMiAxNy44MzA4IDM0LjY2MjQgMTcuODMxNiAzNC42NjE0QzE3LjgzMzIgMzQuNjU5NCAxNy44MzU0IDM0LjY1NjYgMTcuODM4MyAzNC42NTI5QzE3Ljg0NDEgMzQuNjQ1NiAxNy44NTI2IDM0LjYzNTEgMTcuODYzNSAzNC42MjEzQzE3Ljg4NTQgMzQuNTkzOCAxNy45MTc0IDM0LjU1MzQgMTcuOTU4OCAzNC41MDA3QzE4LjA0MTYgMzQuMzk1NCAxOC4xNjIxIDM0LjI0MSAxOC4zMTQ5IDM0LjA0MjJDMTguNjIwNSAzMy42NDQ2IDE5LjA1NTYgMzMuMDY4OSAxOS41NzcxIDMyLjM1MjZDMjAuNjE5MyAzMC45MjA5IDIyLjAxMDUgMjguOTIxNiAyMy40MDM2IDI2LjY1MzhDMjQuNzk0OCAyNC4zODg5IDI2LjE5OTUgMjEuODM3OCAyNy4yNTk0IDE5LjMwNDNDMjguMzEyNSAxNi43ODczIDI5LjA2MTIgMTQuMjA0OCAyOS4wNjI1IDExLjkwMDZMMjYuOTM3NSAxMS44OTk0Wk0xNyAxNS45Mzc1QzE5LjkzNCAxNS45Mzc1IDIyLjMxMjUgMTMuNTU5IDIyLjMxMjUgMTAuNjI1SDIwLjE4NzVDMjAuMTg3NSAxMi4zODU0IDE4Ljc2MDQgMTMuODEyNSAxNyAxMy44MTI1VjE1LjkzNzVaTTExLjY4NzUgIDEwLjYyNUMxMS42ODc1IDEzLjU1OSAxNC4wNjYgMTUuOTM3NSAxNyAxNS45Mzc1VjEzLjgxMjVDMTUuMjM5NiAxMy44MTI1IDEzLjgxMjUgMTIuMzg1NCAxMy44MTI1IDEwLjYyNUgxMS42ODc1Wk0xNyA1LjMxMjVDMTQuMDY2IDUuMzEyNSAxMS42ODc1IDcuNjkwOTkgMTEuNjg3NSAxMC42MjVIMTMuODEyNUMxMy44MTI1IDguODY0NTkgMTUuMjM5NiA3LjQzNzUgMTcgNy40Mzc1VjUuMzEyNVpNMjIuMzEyNSAxMC42MjVDMjIuMzEyNSA3LjY5MDk5IDE5LjkzNCA1LjMxMjUgMTcgNS4zMTI1VjcuNDM3NUMxOC43NjA0IDcuNDM3NSAyMC4xODc1IDguODY0NTkgMjAuMTg3NSAxMC42MjVIMjIuMzEyNVoiIGZpbGw9ImJsYWNrIiBmaWxsLW9wYWNpdHk9IjAuMzgiIG1hc2s9InVybCgjcGF0aC0yLWluc2lkZS0xXzg4MTZfMjYzODg3KSIvPgogIDxkZWZzPgogICAgPGZpbHRlciBpZD0iZmlsdGVyMF9iZl84ODE2XzI2Mzg4NyIgeD0iMTIuNzUiIHk9IjE0Ljc1IiB3aWR0aD0iMjMuOTQzNCIgaGVpZ2h0PSIyMi41IiBmaWx0ZXJVbml0cz0idXNlclNwYWNlT25Vc2UiIGNvbG9yLWludGVycG9sYXRpb24tZmlsdGVycz0ic1JHQiI+CiAgICAgIDxmZUZsb29kIGZsb29kLW9wYWNpdHk9IjAiIHJlc3VsdD0iQmFja2dyb3VuZEltYWdlRml4Ii8+CiAgICAgIDxmZUdhdXNzaWFuQmx1ciBpbj0iQmFja2dyb3VuZEltYWdlRml4IiBzdGREZXZpYXRpb249IjIuMTI1Ii8+CiAgICAgIDxmZUNvbXBvc2l0ZSBpbjI9IlNvdXJjZUFscGhhIiBvcGVyYXRvcj0iaW4iIHJlc3VsdD0iZWZmZWN0MV9iYWNrZ3JvdW5kQmx1cl84ODE2XzI2Mzg4NyIvPgogICAgICA8ZmVCbGVuZCBtb2RlPSJub3JtYWwiIGluPSJTb3VyY2VHcmFwaGljIiBpbjI9ImVmZmVjdDFfYmFja2dyb3VuZEJsdXJfODgxNl8yNjM4ODciIHJlc3VsdD0ic2hhcGUiLz4KICAgICAgPGZlR2F1c3NpYW5CbHVyIHN0ZERldmlhdGlvbj0iMC41IiByZXN1bHQ9ImVmZmVjdDJfZm9yZWdyb3VuZEJsdXJfODgxNl8yNjM4ODciLz4KICAgIDwvZmlsdGVyPgogIDwvZGVmcz4KPC9zdmc+\",\"imageSize\":34,\"imageFunction\":\"var res = {\\n url: images[0],\\n size: 40\\n}\\nvar temperature = data.temperature;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120;\\n var index = Math.min(3, Math.floor(4 * percent));\\n res.url = images[index];\\n}\\nreturn res;\\n\",\"images\":[\"tb-image;/api/images/system/map_marker_image_0.png\",\"tb-image;/api/images/system/map_marker_image_1.png\",\"tb-image;/api/images/system/map_marker_image_2.png\",\"tb-image;/api/images/system/map_marker_image_3.png\"]},\"markerOffsetX\":0.5,\"markerOffsetY\":1,\"positionFunction\":\"return {x: origXPos, y: origYPos};\",\"markerClustering\":{\"enable\":false,\"zoomOnClick\":true,\"maxZoom\":null,\"maxClusterRadius\":80,\"zoomAnimation\":true,\"showCoverageOnHover\":true,\"spiderfyOnMaxZoom\":false,\"chunkedLoad\":false,\"lazyLoad\":true,\"useClusterMarkerColorFunction\":false,\"clusterMarkerColorFunction\":null}}],\"polygons\":[],\"circles\":[],\"additionalDataSources\":[],\"controlsPosition\":\"topleft\",\"zoomActions\":[\"scroll\",\"doubleClick\",\"controlButtons\"],\"fitMapBounds\":true,\"useDefaultCenterPosition\":false,\"defaultCenterPosition\":\"0,0\",\"defaultZoomLevel\":null,\"minZoomLevel\":16,\"mapPageSize\":16384,\"background\":{\"type\":\"color\",\"color\":\"#fff\",\"overlay\":{\"enabled\":false,\"color\":\"rgba(255,255,255,0.72)\",\"blur\":3}},\"padding\":\"8px\",\"imageSource\":{\"sourceType\":\"image\",\"url\":\"tb-image;/api/images/system/image_map_system_widget_map_image.svg\",\"entityAliasId\":null,\"entityKey\":null}},\"title\":\"Image Map\",\"useDashboardTimewindow\":true,\"displayTimewindow\":true,\"showTitleIcon\":false,\"titleTooltip\":\"\",\"dropShadow\":true,\"enableFullscreen\":true,\"widgetStyle\":{},\"widgetCss\":\"\",\"titleStyle\":{\"fontSize\":\"16px\",\"fontWeight\":400},\"pageSize\":1024,\"noDataDisplayMessage\":\"\",\"configMode\":\"basic\",\"titleFont\":null,\"titleColor\":null,\"margin\":\"0px\",\"borderRadius\":\"0px\",\"iconSize\":\"24px\",\"titleIcon\":\"map\",\"iconColor\":\"#1F6BDD\",\"actions\":{\"tooltipAction\":[]}}" + "defaultConfig": "{\"datasources\":[],\"timewindow\":{\"realtime\":{\"timewindowMs\":60000}},\"showTitle\":true,\"backgroundColor\":\"rgba(0, 0, 0, 0)\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"0px\",\"settings\":{\"mapType\":\"image\",\"layers\":[],\"imageSource\":{\"sourceType\":\"image\",\"url\":\"tb-image;/api/images/system/image_map_system_widget_map_image.svg\",\"entityAliasId\":null,\"entityKey\":null},\"markers\":[{\"dsType\":\"function\",\"dsLabel\":\"First point\",\"dsDeviceId\":null,\"dsEntityAliasId\":null,\"dsFilterId\":null,\"additionalDataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"temperature\",\"color\":\"#2196f3\",\"settings\":{},\"_hash\":0.8239425680406081,\"funcBody\":\"var value = prevValue + Math.random() * 40 - 20;\\nif (value < -60) {\\n\\tvalue = -60;\\n} else if (value > 60) {\\n\\tvalue = 60;\\n}\\nreturn value;\",\"aggregationType\":null,\"units\":null,\"decimals\":null,\"usePostProcessing\":null,\"postFuncBody\":null}],\"label\":{\"show\":true,\"type\":\"pattern\",\"pattern\":\"${entityName}\"},\"tooltip\":{\"show\":true,\"type\":\"pattern\",\"pattern\":\"${entityName}

X Pos: ${xPos:2}
Y Pos: ${yPos:2}
Temperature: ${temperature} °C
See tooltip settings for details\",\"patternFunction\":null,\"trigger\":\"click\",\"autoclose\":true,\"offsetX\":0,\"offsetY\":-1},\"groups\":null,\"xKey\":{\"name\":\"f(x)\",\"label\":\"latitude\",\"type\":\"function\",\"funcBody\":\"var value = prevValue || 0.2;\\nif (time % 5000 < 500) {\\n value += Math.random() * 0.05 - 0.025;\\n}\\nreturn value;\",\"settings\":{},\"color\":\"#2196f3\",\"aggregationType\":null,\"units\":null,\"decimals\":null,\"usePostProcessing\":null,\"postFuncBody\":null},\"yKey\":{\"name\":\"f(x)\",\"label\":\"longitude\",\"type\":\"function\",\"funcBody\":\"var value = prevValue || 0.3;\\nif (time % 5000 < 500) {\\n value += Math.random() * 0.05 - 0.025;\\n}\\nreturn value;\",\"settings\":{},\"color\":\"#2196f3\",\"aggregationType\":null,\"units\":null,\"decimals\":null,\"usePostProcessing\":null,\"postFuncBody\":null},\"markerType\":\"shape\",\"markerShape\":{\"shape\":\"markerShape1\",\"size\":34,\"color\":{\"type\":\"function\",\"color\":\"#307FE5\",\"colorFunction\":\"var temperature = data.temperature;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\\n\"}},\"markerIcon\":{\"icon\":\"mdi:lightbulb-on\",\"size\":34,\"color\":{\"type\":\"constant\",\"color\":\"#307FE5\"}},\"markerImage\":{\"type\":\"image\",\"image\":\"data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9Ii0xOTEuMzUgLTM1MS4xOCAxMDgzLjU4IDE3MzAuNDYiPjxwYXRoIGZpbGwtcnVsZT0iZXZlbm9kZCIgY2xpcC1ydWxlPSJldmVub2RkIiBmaWxsPSIjZmU3NTY5IiBzdHJva2U9IiMwMDAiIHN0cm9rZS13aWR0aD0iMzciIHN0cm9rZS1taXRlcmxpbWl0PSIxMCIgZD0iTTM1MS44MzMgMTM2MC43OGMtMzguNzY2LTE5MC4zLTEwNy4xMTYtMzQ4LjY2NS0xODkuOTAzLTQ5NS40NEMxMDAuNTIzIDc1Ni40NjkgMjkuMzg2IDY1NS45NzgtMzYuNDM0IDU1MC40MDRjLTIxLjk3Mi0zNS4yNDQtNDAuOTM0LTcyLjQ3Ny02Mi4wNDctMTA5LjA1NC00Mi4yMTYtNzMuMTM3LTc2LjQ0NC0xNTcuOTM1LTc0LjI2OS0yNjcuOTMyIDIuMTI1LTEwNy40NzMgMzMuMjA4LTE5My42ODUgNzguMDMtMjY0LjE3M0MtMjEtMjA2LjY5IDEwMi40ODEtMzAxLjc0NSAyNjguMTY0LTMyNi43MjRjMTM1LjQ2Ni0yMC40MjUgMjYyLjQ3NSAxNC4wODIgMzUyLjU0MyA2Ni43NDcgNzMuNiA0My4wMzggMTMwLjU5NiAxMDAuNTI4IDE3My45MiAxNjguMjggNDUuMjIgNzAuNzE2IDc2LjM2IDE1NC4yNiA3OC45NzEgMjYzLjIzMyAxLjMzNyA1NS44My03LjgwNSAxMDcuNTMyLTIwLjY4NCAxNTAuNDE3LTEzLjAzNCA0My40MS0zMy45OTYgNzkuNjk1LTUyLjY0NiAxMTguNDU1LTM2LjQwNiA3NS42NTktODIuMDQ5IDE0NC45ODEtMTI3Ljg1NSAyMTQuMzQ1LTEzNi40MzcgMjA2LjYwNi0yNjQuNDk2IDQxNy4zMS0zMjAuNTggNzA2LjAyOHoiLz48Y2lyY2xlIGZpbGwtcnVsZT0iZXZlbm9kZCIgY2xpcC1ydWxlPSJldmVub2RkIiBjeD0iMzUyLjg5MSIgY3k9IjIyNS43NzkiIHI9IjE4My4zMzIiLz48L3N2Zz4=\",\"imageSize\":34},\"markerOffsetX\":0.5,\"markerOffsetY\":1,\"positionFunction\":\"return {x: origXPos, y: origYPos};\",\"markerClustering\":{\"enable\":false,\"zoomOnClick\":true,\"maxZoom\":null,\"maxClusterRadius\":80,\"zoomAnimation\":true,\"showCoverageOnHover\":true,\"spiderfyOnMaxZoom\":false,\"chunkedLoad\":false,\"lazyLoad\":true,\"useClusterMarkerColorFunction\":false,\"clusterMarkerColorFunction\":null}},{\"dsType\":\"function\",\"dsLabel\":\"Second point\",\"dsDeviceId\":null,\"dsEntityAliasId\":null,\"dsFilterId\":null,\"additionalDataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"temperature\",\"color\":\"#2196f3\",\"settings\":{},\"_hash\":0.7826299113906372,\"funcBody\":\"var value = prevValue + Math.random() * 40 - 20;\\nif (value < -60) {\\n\\tvalue = -60;\\n} else if (value > 60) {\\n\\tvalue = 60;\\n}\\nreturn value;\",\"aggregationType\":null,\"units\":null,\"decimals\":null,\"usePostProcessing\":null,\"postFuncBody\":null}],\"label\":{\"show\":true,\"type\":\"pattern\",\"pattern\":\"${entityName}\"},\"tooltip\":{\"show\":true,\"trigger\":\"click\",\"autoclose\":true,\"type\":\"pattern\",\"pattern\":\"${entityName}

X Pos: ${xPos:2}
Y Pos: ${yPos:2}
Temperature: ${temperature} °C
See tooltip settings for details\",\"offsetX\":0,\"offsetY\":-1,\"patternFunction\":null},\"click\":{\"type\":\"doNothing\"},\"groups\":null,\"edit\":{\"enabledActions\":[],\"snappable\":false},\"xKey\":{\"name\":\"f(x)\",\"label\":\"latitude\",\"type\":\"function\",\"funcBody\":\"var value = prevValue || 0.6;\\nif (time % 4000 < 500) {\\n value += Math.random() * 0.05 - 0.025;\\n}\\nreturn value;\",\"settings\":{},\"color\":\"#2196f3\",\"aggregationType\":null,\"units\":null,\"decimals\":null,\"usePostProcessing\":null,\"postFuncBody\":null},\"yKey\":{\"name\":\"f(x)\",\"label\":\"longitude\",\"type\":\"function\",\"funcBody\":\"var value = prevValue || 0.7;\\nif (time % 4000 < 500) {\\n value += Math.random() * 0.05 - 0.025;\\n}\\nreturn value;\",\"settings\":{},\"color\":\"#2196f3\",\"aggregationType\":null,\"units\":null,\"decimals\":null,\"usePostProcessing\":null,\"postFuncBody\":null},\"markerType\":\"icon\",\"markerShape\":{\"shape\":\"markerShape1\",\"size\":34,\"color\":{\"type\":\"constant\",\"color\":\"#307FE5\"}},\"markerIcon\":{\"size\":40,\"color\":{\"type\":\"function\",\"color\":\"#307FE5\",\"colorFunction\":\"var colors = ['#488bc7','#549c5d','#ed7546','#be2b29'];\\nvar temperature = data.temperature;\\nvar res = colors[0];\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120;\\n var index = Math.min(3, Math.floor(4 * percent));\\n res = colors[index];\\n}\\nreturn res;\"},\"icon\":\"thermostat\"},\"markerImage\":{\"type\":\"function\",\"image\":\"/assets/markers/shape1.svg\",\"imageSize\":34,\"imageFunction\":\" \",\"images\":[]},\"markerOffsetX\":0.5,\"markerOffsetY\":1,\"positionFunction\":\"return {x: origXPos, y: origYPos};\",\"markerClustering\":{\"enable\":false,\"zoomOnClick\":true,\"maxZoom\":null,\"maxClusterRadius\":80,\"zoomAnimation\":true,\"showCoverageOnHover\":true,\"spiderfyOnMaxZoom\":false,\"chunkedLoad\":false,\"lazyLoad\":true,\"useClusterMarkerColorFunction\":false,\"clusterMarkerColorFunction\":null}}],\"polygons\":[],\"circles\":[],\"additionalDataSources\":[],\"controlsPosition\":\"topleft\",\"zoomActions\":[\"scroll\",\"doubleClick\",\"controlButtons\"],\"fitMapBounds\":true,\"useDefaultCenterPosition\":false,\"defaultCenterPosition\":\"0,0\",\"defaultZoomLevel\":null,\"mapPageSize\":16384,\"background\":{\"type\":\"color\",\"color\":\"#fff\",\"overlay\":{\"enabled\":false,\"color\":\"rgba(255,255,255,0.72)\",\"blur\":3}},\"padding\":\"8px\"},\"title\":\"Image Map\",\"useDashboardTimewindow\":true,\"displayTimewindow\":true,\"showTitleIcon\":false,\"titleTooltip\":\"\",\"dropShadow\":true,\"enableFullscreen\":true,\"widgetStyle\":{},\"widgetCss\":\"\",\"titleStyle\":{\"fontSize\":\"16px\",\"fontWeight\":400},\"pageSize\":1024,\"noDataDisplayMessage\":\"\",\"configMode\":\"basic\",\"titleFont\":null,\"titleColor\":null,\"margin\":\"0px\",\"borderRadius\":\"0px\",\"iconSize\":\"24px\",\"titleIcon\":\"map\",\"iconColor\":\"#1F6BDD\",\"actions\":{\"tooltipAction\":[]}}" }, "resources": [ { @@ -41,50 +41,6 @@ "mediaType": "image/svg+xml", "data": "PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0iVVRGLTgiPz4KPHN2ZyB3aWR0aD0iMTEzNC41IiBoZWlnaHQ9Ijc2Mi43OCIgdmVyc2lvbj0iMS4xIiB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciPgogPGcgdHJhbnNmb3JtPSJ0cmFuc2xhdGUoLTI3LjA3MSAtMzA3LjkpIj4KICA8ZyBmaWxsPSJub25lIj4KICAgPHBhdGggZD0ibTkwNi4wMyA3MDYuMTMgMy40MjkyIDE3Ljc5Nm0tODgwLjg5IDQxLjEyMWMxNTAuNDQgNi44MzM0IDE0Ni4zOS0yNi4zMzQgMTY2LjQzLTI5LjMyIDM2LjE0NC01LjM4NDggMTE0LjI5LTYuNTI1NCAxNDguMzMtOC42MjM1IDQzLjM3OC0yLjY3MzggMTQxLjc2LTExLjIzMSAxODguODYtMTkuODM0IDM5LjgxMS03LjI3MjggMjIxLjM3LTAuODYyMzUgMzE5LjA3LTAuODYyMzUgNzAuODI3IDAgMTQ2LjkyLTEuNzI0NyAyMTguMTgtMS43MjQ3LTMxLjYyIDAgMTE3Ljg2LTIuNTg3MSA4Ni4yMzYtMi41ODcxbS0yNS4wOTEtNjguMTI2Yy01Mi44IDM0Ljc4NS02NS44OTUgNTEuNzQ5LTk1LjYzOSA4MS40OTMtMjQuOTMxIDI0LjkzMS0xNDAuNC0xOS4xMzktMTc4Ljk0IDM2LjY1LTEyLjI4MSAxNy43NzctNDcuMDAzIDQ2LjU0Ny02NS4xMDggNTkuMDcxLTIwLjEwNSAxMy45MDgtNTYuMDM3IDQ0Ljk1Ny02Ny43NjkgNzMuMDc4LTQuODAxNSAxMS41MDktMTMuMzggMzUuOTkzLTIzLjQ0OSA0Ni4wNjItMTAuNDk3IDEwLjQ5Ny0zOC4zNzcgNi4zODU3LTQ0LjAyMyAxNy42NDgtMTkuMDA1IDM3LjkwOC0yNS40NjUgMTAwLjkyLTY3LjYxOCAxMDIuMDVtMTkuMjgyLTYyNC4wMWMzNC42NTktMS44NzM4IDg0LjAyNyA3LjM5MTMgMTA5LjktNC4yODU0IDEzLjI4Mi01Ljk5NDEgNDEuNDA3LTIuNDYxNCA2Ni44MjktMi4zMjA1IDM1LjMyMiAwLjE5NTc4IDY0LjM4MiAwLjYzNDc3IDEwMS45MiA1LjAyMzIgMjUuMDMgMi45MjY1IDQ0LjY2MyAzNC4yODcgNTguNTI3IDUwLjY0NCAxNy4wOTkgMjAuMTczIDYyLjc2NC0xLjcxNDcgNjYuMzA2IDMyLjEzNCA1LjEwMjcgNDguNzY2LTYuMzI4NCA3OC42MzcgNi4xNDExIDk3LjM0MiAxOS45NjkgMjkuOTU0IDUwLjQ4NiAxNy44NTYgNDQuNjE5IDgzLjk3MW0tNDcyLjQ1LTM3OC43OWM0LjY0MzUgMjMuNzI5IDE1LjA2OSA3Mi43NzYgMTkuMDYxIDEzMC42NCAwLjg3MjA2IDEyLjY0IDUuNDQ3MiAyNC45OTMgNC4yMjIzIDQ1LjI3OC0yLjUxNzIgNDEuNjg4LTE1LjcxNyA0My42NzctMTUuMDkxIDYwLjM2NSAxLjQzMiAzOC4xODIgMzAuNjE0IDkzLjgzNyAzMC42MTQgMTM5LjcgMCAyNC4xODEtMi42Njk2IDExNS4zOSA3LjMzIDEzNS4zOSAwLjE1OTExIDAuMzE4MjEgMTAuMDY1IDM1Ljg4MyAxMC43NzkgNDkuMTU0IDAuOTQzNzggMTcuNTI1LTI0LjQ3OCAzOS40Ny0yOC4wMjcgNDYuNTY3LTUuNDc3NyAxMC45NTUtMzYuOTczIDEwLjg4Mi00MC4xIDI0LjE0Ni0zLjg2ODggMTYuNDE1LTMuODY2MyA0My43OTcgNC4wNDY1IDU5LjQ0MW05Ny4zMzctNjkxLjAxYy01LjAxMzMgMzUuNTE2LTQzLjY1OSAxMS4zMTctNTguNTM5IDIzLjc4MS0yMS4zMyAxNy44NjktNjIuNSAzMS40MzItNzAuMTI0IDM1LjM2Ny0zNS4wODggMTguMTA4LTExMC40Ny0xNS4xNDItMTI1LjYxIDQuMjY4NC0xNS45NTEgMjAuNDQ3LTAuMDczNSA2MS40NjYtOS4xNDY3IDg0LjE0OS02LjAzNTcgMTUuMDg5LTE4Ljg3NyAyMy4wMTctMjcuNDQgMzIuOTI4LTE5Ljc0OCAyMi44NTYtNjkuOTc0IDY5LjgyNC04NC43NTkgMTAwLTcuNDk3NCAxNS4zMDQtMy4yODQzIDQ0LjQyLTMuNDcwNSA2My4zNDMtMC4xMjc5MyAxMi45OTQtMC44MTAxNSAyMy4xMDQgMi40MDM0IDI4LjI3NiA0Ljk2MTYgNy45ODU4IDIzLjcyIDI4LjExMiAyNC4yMzkgNTAuNjExIDAuMjk0MTEgMTIuNzcxIDAuMDEzMyA3OC41OTEgMy4wNDg5IDg3LjY1NSAyLjMxMjYgNi45MDU1IDQuMjIgMjYuNTY1IDEwLjIxNCAzNi41ODcgMTEuMzU0IDE4Ljk4NCA0LjM4NzQgNDAuMTU3IDI3Ljg5NyA1My41MDggMTkuMDUgMTAuODE5IDQ2Ljg3OCAxMi4yMTkgODEuOTI2IDE0LjQ2MSAzMy43MDMgMi4xNTU5IDYxLjUxMi0xLjQzMDQgNzYuOTIxIDYuMTQxMSAxMS41ODUgNS42OTI3IDguNTgxNSAxNy45MzMgMTQuMjk1IDI5LjM2MSA1LjY0MDQgMTEuMjgxIDMxLjUwMyAxMS4xNTYgNDEuODA0IDQzLjQ1NSA3LjYwNTkgMjMuODQ3IDMuMDg1OSA0NC4xNTcgNi43MDc2IDY1Ljg4NyIgc3Ryb2tlPSIjMzY0ZTU5IiBzdHJva2Utd2lkdGg9IjMiLz4KICAgPHBhdGggZD0ibTQzLjI3OCA1MTcuOTVzMjMwLjg1LTMuNjM4IDI1MC4wMS0zLjY1ODdjNy40ODIyLThlLTMgOC42MTk1IDUuMTUxOSAxNC4wMjEgMTEuNDU5IDI0LjU5NiAyOC43MTkgOTMuOTEgMTEyLjk0IDkzLjkxIDExMi45NCIgc3Ryb2tlPSIjMzM2IiBzdHJva2Utd2lkdGg9IjFweCIvPgogICA8cGF0aCBkPSJtMzUuOTYxIDU3Ny43czE2NS41Mi0xLjY4NDUgMjQ4Ljc4LTEuNjg0NWM0Ljk0NzUgMCA3LjcyOTktMi44ODMzIDEwLjUzOC01LjcyOTggOS42NjExLTkuNzk0MiAyNS42MzItMjguNTkgMjUuNjMyLTI4LjU5IiBzdHJva2U9IiMzMzYiIHN0cm9rZS13aWR0aD0iMXB4Ii8+CiAgPC9nPgogIDxwYXRoIGQ9Im0zOC40IDY0MS43MyAzOTMuMzEtNC4yNjg0IiBjb2xvcj0iIzAwMDAwMCIgZmlsbD0iIzMzNiIgc3Ryb2tlPSIjMzM2IiBzdHJva2Utd2lkdGg9IjFweCIvPgogIDxwYXRoIGQ9Im0zOS4wMDkgNzA0LjU0IDQ4NC4xNi02LjcwNzYiIGNvbG9yPSIjMDAwMDAwIiBmaWxsPSIjMzM2IiBzdHJva2U9IiMzMzYiIHN0cm9rZS13aWR0aD0iMXB4Ii8+CiAgPGcgZmlsbD0ibm9uZSIgc3Ryb2tlPSIjMzM2Ij4KICAgPGcgc3Ryb2tlLXdpZHRoPSIxcHgiPgogICAgPHBhdGggZD0ibTMwMy45NiA2ODIuNTkgMTQ2LjggMS44MjkzYzEwLjUzNCAwLjEzMTI3IDE0LjM0NC0yLjYzNzQgMjUuNDg3LTYuMzcyOCAxMC40MTItMy40OTAzIDMxLjQyNC0yLjY5OSA0MS4zODUtMi43NzM4bDQwNS41Ni0zLjA0ODkiIGNvbG9yPSIjMDAwMDAwIi8+CiAgICA8cGF0aCBkPSJtNDI2LjIyIDMxNC44OWMyLjA2NzUgOS4wNTI3IDEuODQxOCA1MS43MjggNi41MDc5IDc0LjgzNSAxLjY3NDggOC4yOTM0IDguNjc1MSAxNC4wNjYgMTAuMDU1IDE0Ljg1OSA0LjkwMTUgMi44MTQ2IDEwLjgxNSA4LjE0OTggMTMuMDQ2IDE2LjA4OCA2Ljc1NzggMjQuMDQ2IDAuODc5NzIgNjguNDUyIDAuODc5NzIgMTEwLjY5IDAgNi4wOTc4IDEuNjYwMSAzMC4xNDctMi4xNTU5IDMzLjk2My0yLjU0MDggMi41NDA4LTAuMjgxNjMgMTIuOTkxLTMuNDM2OCAxNi4xNDRsLTkuODQ5NCA5Ljg0MzFjLTEwLjM2NyAxMC4zNi0xMS41OSA2LjUyNjEtMTcuNzM4IDE4LjgyMy0zLjU2NzcgNy4xMzU0IDUuNDAyNCAyMC42NzIgNy4zNTQzIDI0LjU3NiAxLjkzMjEgMy44NjQzLTEuODQyMiA0Ljc3NzctMS43OTI0IDcuNDQ2MyAwLjI1Mjg2IDEzLjU0NSAyLjI5NzUgMzczLjkzIDIuMjk3NSAzNzMuOTMiIGNvbG9yPSIjMDAwMDAwIi8+CiAgICA8cGF0aCBkPSJtMzY1LjI0IDUxOS43OCA0LjExNiA1MDIuMTUiIGNvbG9yPSIjMDAwMDAwIi8+CiAgICA8cGF0aCBkPSJtMTE2LjUzIDUwNC4xOSAzLjg4MDYgMzEwLjk2IiBjb2xvcj0iIzAwMDAwMCIvPgogICAgPHBhdGggZD0ibTMxNy42OCA1NzYuNDkgMTMwLjE5IDEuNTI0NGM0LjUxMDggMy4yNDE3IDIwLjM0NSA3Ljk2ODUgMjcuNzQ1IDQuMjY4NCAzLjE1NTUtMS41Nzc3IDkuNDE5LTUuMzg4MiAxNC4wMjUtMy45NjM2IDQuMjY3IDEuMzE5OCA2LjAxNjkgMy4xMTYzIDEwLjM2NiAzLjA0ODkgMTAuMzA0LTAuMTU5NzUgMjAuMjEyIDAuMzg3NDEgMzAuNDg5IDAuMzA0ODkgMTc3Ljg5LTEuNDI4MyAzNTYuNTktMi4xMzI1IDUzNC43Ny0zLjA0ODkiIGNvbG9yPSIjMDAwMDAwIi8+CiAgICA8cGF0aCBkPSJtNDc1LjMxIDU4Mi44OWMtMy40NDQyIDExLjM1MS0yLjEwMzQgMTIuNDM0IDMuNjU4NiAyMS4wMzcgMy43OTQ0IDUuNjY1NiA1MC44NjMgMTMuMDM4IDQxLjQ2NSAyNy4xMzUtMTAuNTM3IDE1LjgwNS0yMi44OTctNS40Nzc3LTMzLjg0My0xLjgyOTMtNS40NTI0IDEuODE3NC03LjM0OSA1LjQ1NjMtMy42NTg3IDkuMTQ2NiAyLjgwNjggMi44MDY4IDQuMDQ4IDEuODA0IDYuNTIwMyA1LjEwMDQiIGNvbG9yPSIjMDAwMDAwIi8+CiAgICA8cGF0aCBkPSJtNDMyLjAxIDYzNi44NWM4LjMxOSAxMy4xMSAxOC44NDYgMTQuNjM1IDM1LjY3MiAxNC42MzUgMi45Mzg2IDAgNy44Ny0wLjkzMzcxIDEwLjY3MSAwIDExLjM1OSAzLjc4NjQgMjcuMTk0IDEwLjI3NiAzNi4yMDIgMjEuMTI5IDguMjggOS45NzY2IDEwLjI1MyAyMy44ODMgNy43MDIgMzcuMTA0LTYuMTY5OSAzMS45OC0xNi43MTQgNTYuOTg5LTE5LjA0NCA4Ni41NjktMS4zNDggMTcuMTE5IDQuNTA5NiAyMi41MzUgMTEuMDcxIDMzLjkyOSAxMC42NyAxOC41MjcgOC43MjQ1IDE0LjIgOC41NzE0IDM0LjI4Ni0wLjEzOTYzIDE4LjMxOSAwIDYwLjI2NCAwIDgwLjcxNCIgY29sb3I9IiMwMDAwMDAiLz4KICAgIDxwYXRoIGQ9Im01MjguNTEgNjU4Ljk2Yy0xMC42ODEgMC45MDQ1NC03LjEwOC01LjYwMjYtMTAuODI0LTguMDc5Ni00Ljc4NDUtMy4xODk3LTEyLjIyNy0xLjI1MS0xNi43NjktNS43OTI5LTAuNjY2MTItMC42NjYxMi04LjgwOTctNC4xMDg4LTEwLjE3NC0yLjc0NC04LjM2NDYgOC4zNjQ2LTMuMDQ4OSAyMC41NTItMy4wNDg5IDMzLjUzOGwzLjAyMiAzMzkuNyIgY29sb3I9IiMwMDAwMDAiLz4KICAgIDxwYXRoIGQ9Im01MTcuOTkgNjUxLjAzYy0wLjIyMTcxLTIuNzAxOCAxLjkwMzUtNS41NjIxIDMuMzUzOC03LjAxMjQgMS43OTk0LTEuNzk5NCA2LjkyMjkgMS4wMDQyIDguODQxOC0wLjkxNDY2IDAuMjg3NjUtMC4yODc2NiAwLjg0MzI5LTExLjE2NCAwLjIyODY2LTEzLjU2OC0yLjA2NDgtOC4wNzQyLTIuMDU4LTI4LjY1Ny0yLjA1OC0zOC43MjF2LTczLjE3MyIgY29sb3I9IiMwMDAwMDAiLz4KICAgIDxwYXRoIGQ9Im01MjguNjYgNjc1LjQyLTAuNDU3MzMtMzEuNTU2IiBjb2xvcj0iIzAwMDAwMCIvPgogICAgPHBhdGggZD0ibTc2Ni4zMiA1NzkuNjQgMC40MzExOCAxMy43OThjMy4xMzY0IDQuNjY5MiAzLjAxODIgOS42MDA3IDMuMDE4MiAxNi4zODV2MTU3LjM4IiBjb2xvcj0iIzAwMDAwMCIvPgogICAgPHBhdGggZD0ibTExMjIuOSA3NjUuOTFjLTIwMi4zMSA0LjY5MDUtNDAzLjc0LTEuMTEzOC02MDUuOTUgMy4zNTM5LTEwLjg2NCAwLjI0MDAyLTMuMzYxNS04LjU4NjMtMjguNTM3LTguNTg2MyIgY29sb3I9IiMwMDAwMDAiLz4KICAgIDxwYXRoIGQ9Im04NjAuMDEgNzM3LjA3cy05Ny40NDggMC44NTgwNi0xNDcuNTcgMC44NTgwNmMtNS4yNjg2IDAtNC41MTU1LTguMzI5OS03LjMwMDktOC4zMjk5LTMuOTc0NCAwLTguNjI5MiAwLjAyMDEtMTAuNTA5IDAuMDM1OS0yLjMzNDggMC4wMTk3LTEuODEwOSA4LjM2Ni00LjE0NTggOC4zNjY5LTQ2LjE2OSAwLjAxODgtMTY3LjQxLTEuMzA4LTE3NS4wNS0xLjMwOC00LjQyOTYgMC04LjU3NjMtNi40Mzk3LTEzLjEzMi02LjQzOTdoLTE0LjM5NSIgY29sb3I9IiMwMDAwMDAiLz4KICAgIDxwYXRoIGQ9Im02NzUuMDEgODMxLjE3LTAuNjA5NzgtNTIxLjc3IiBjb2xvcj0iIzAwMDAwMCIvPgogICAgPHBhdGggZD0ibTc5OS40IDMxMy4wNiAxLjIxOTYgNDk1Ljg3IiBjb2xvcj0iIzAwMDAwMCIvPgogICAgPHBhdGggZD0ibTczNi41OSAzMTIuNDUtMS4yMTk2IDcxNi40OSIgY29sb3I9IiMwMDAwMDAiLz4KICAgIDxwYXRoIGQ9Im01MzAuMDMgNjQzLjQ2IDM5Mi4zNy0zLjAxODIiIGNvbG9yPSIjMDAwMDAwIi8+CiAgICA8cGF0aCBkPSJtODU5LjQ1IDMxNC45IDEuMjkzNSA1MDcuOTgiIGNvbG9yPSIjMDAwMDAwIi8+CiAgIDwvZz4KICAgPHBhdGggZD0ibTkyMS41NCAzMTAuNTkgMS43MjQ3IDUzMS43NSIgY29sb3I9IiMwMDAwMDAiIHN0cm9rZS13aWR0aD0iMXB4Ii8+CiAgIDxnIHN0cm9rZS13aWR0aD0iMXB4Ij4KICAgIDxwYXRoIGQ9Im03MzYuMjkgNDUzLjMxIDE4NS42OC0wLjMwNDg5IiBjb2xvcj0iIzAwMDAwMCIvPgogICAgPHBhdGggZD0ibTEwNjAuOCA1MTQuOTdzLTM2My4yOC01LjYyNjItNTQ0LjY1IDIuNTIxOGMtNC4xNzc4IDAuMTg3NjktMTIuNSAxLjA2NzEtMTIuNSAxLjA2NzEtMS41NzEgMC4xMzQxLTIuMDAwOS0yLjMyNS0yLjU5MTYtMy41MDYyLTAuMDk2Ny0wLjE5MzQzLTcuMDYwOC0xLjkzMzQtNy42MjIyLTEuMzcyLTIuODkzMSAyLjg5MzEtNy42MzE3IDQuMjQ4Ny0xMi4xOTYgNC4xMTZsLTExMi4wNS0zLjI1NzgiIGNvbG9yPSIjMDAwMDAwIi8+CiAgICA8cGF0aCBkPSJtMzk5LjgyIDQ3OS42MSAxMS42NDIgNS42MDUzYzIuOTg0MSAxLjQzNjggNi41Mjg4LTAuNDc3MTIgOS45MTcxLTAuNDMxMThsMTI3LjIgMS43MjQ3IiBjb2xvcj0iIzAwMDAwMCIvPgogICAgPHBhdGggZD0ibTUxOS4yNSA1MTcuMTItMC40MzExOS0yMDguNjkiIGNvbG9yPSIjMDAwMDAwIi8+CiAgICA8cGF0aCBkPSJtNDMyLjkzIDM4OS43MWMxMS4wNDUgMCAzNS41MzMgMC42MTkyNyA0Mi41OC0xLjAwNCA4LjQwNTItMS45MzYyIDcuMDY2LTYuOTUzOCAxNC4xOTctNi45NTM4IDcuODA5NSAwIDYuNTQyOSA4LjA2MjQgMjAuMTQyIDguMDYyNCAxMy45OTEgMCA0NC45NzcgMC4zNzg4NiA2My45NCAwLjM3ODg2IDEyLjA4NCAwIDgyLjAwMyAwLjMwNDg5IDkzLjYwMSAwLjMwNDg5IDguNzYwNSAwIDEzLjE2LTIuMjg4MyAyMS4zNDItNy4wMTI0IDcuMTk1Mi00LjE1NDEgMi4wNTQ2LTkuNDkxNCAyMC40MjgtOC44NDE4IDIzLjE0NSAwLjgxODMzIDEyLjY0MyAxNC4wMjUgMzIuMzE4IDE0LjAyNWgxNTAuOTJjMTQuMzMyIDAtNC4xMTkxLTEzLjExIDI5LjI2OS0xMy40MTUiIGNvbG9yPSIjMDAwMDAwIi8+CiAgIDwvZz4KICA8L2c+CiAgPGcgZmlsbD0iIzAwMDAwMCIgZm9udC1mYW1pbHk9IlZlcmRhbmEiIGxldHRlci1zcGFjaW5nPSIwcHgiIHdvcmQtc3BhY2luZz0iMHB4Ij4KICAgPHRleHQgeD0iNTg4LjY3OTU3IiB5PSI3MzUuODA0NjMiIHN0eWxlPSJsaW5lLWhlaWdodDowJSIgeG1sOnNwYWNlPSJwcmVzZXJ2ZSI+PHRzcGFuIHg9IjU4OC42Nzk1NyIgeT0iNzM1LjgwNDYzIiBmb250LXNpemU9IjkuNjU4NHB4IiBzdHlsZT0ibGluZS1oZWlnaHQ6MS4yNSI+TGluY29sbjwvdHNwYW4+PC90ZXh0PgogICA8dGV4dCB4PSI2ODYuMzk4NSIgeT0iNzY1LjYyODQyIiBzdHlsZT0ibGluZS1oZWlnaHQ6MCUiIHhtbDpzcGFjZT0icHJlc2VydmUiPjx0c3BhbiB4PSI2ODYuMzk4NSIgeT0iNzY1LjYyODQyIiBmb250LXNpemU9IjkuNjU4NHB4IiBzdHlsZT0ibGluZS1oZWlnaHQ6MS4yNSI+SGFycnk8L3RzcGFuPjwvdGV4dD4KICAgPHRleHQgdHJhbnNmb3JtPSJyb3RhdGUoOTApIiB4PSI3MDkuODcxODMiIHk9Ii04MDIuMzc3MzgiIHN0eWxlPSJsaW5lLWhlaWdodDowJSIgeG1sOnNwYWNlPSJwcmVzZXJ2ZSI+PHRzcGFuIHg9IjcwOS44NzE4MyIgeT0iLTgwMi4zNzczOCIgZm9udC1zaXplPSI5LjY1ODRweCIgc3R5bGU9ImxpbmUtaGVpZ2h0OjEuMjUiPldvb2RsYXduPC90c3Bhbj48L3RleHQ+CiAgIDx0ZXh0IHRyYW5zZm9ybT0icm90YXRlKDkwKSIgeD0iNTYyLjExOTI2IiB5PSItNzcxLjk2ODE0IiBzdHlsZT0ibGluZS1oZWlnaHQ6MCUiIHhtbDpzcGFjZT0icHJlc2VydmUiPjx0c3BhbiB4PSI1NjIuMTE5MjYiIHk9Ii03NzEuOTY4MTQiIGZvbnQtc2l6ZT0iOS42NTg0cHgiIHN0eWxlPSJsaW5lLWhlaWdodDoxLjI1Ij5FZGdlbW9vcjwvdHNwYW4+PC90ZXh0PgogICA8dGV4dCB0cmFuc2Zvcm09InJvdGF0ZSg5MCkiIHg9IjU5OC4zMDQ4NyIgeT0iLTczOC4zNjY0NiIgc3R5bGU9ImxpbmUtaGVpZ2h0OjAlIiB4bWw6c3BhY2U9InByZXNlcnZlIj48dHNwYW4geD0iNTk4LjMwNDg3IiB5PSItNzM4LjM2NjQ2IiBmb250LXNpemU9IjkuNjU4NHB4IiBzdHlsZT0ibGluZS1oZWlnaHQ6MS4yNSI+T2xpdmVyPC90c3Bhbj48L3RleHQ+CiAgIDx0ZXh0IHRyYW5zZm9ybT0icm90YXRlKDkwKSIgeD0iNTkyLjEyMjg2IiB5PSItNjc3LjIwMzk4IiBzdHlsZT0ibGluZS1oZWlnaHQ6MCUiIHhtbDpzcGFjZT0icHJlc2VydmUiPjx0c3BhbiB4PSI1OTIuMTIyODYiIHk9Ii02NzcuMjAzOTgiIGZvbnQtc2l6ZT0iOS42NTg0cHgiIHN0eWxlPSJsaW5lLWhlaWdodDoxLjI1Ij5IaWxsc2lkZTwvdHNwYW4+PC90ZXh0PgogICA8dGV4dCB0cmFuc2Zvcm09InJvdGF0ZSg5MCkiIHg9IjU5Ny4zMjcwOSIgeT0iLTg2Mi42MTQwNyIgc3R5bGU9ImxpbmUtaGVpZ2h0OjAlIiB4bWw6c3BhY2U9InByZXNlcnZlIj48dHNwYW4geD0iNTk3LjMyNzA5IiB5PSItODYyLjYxNDA3IiBmb250LXNpemU9IjkuNjU4NHB4IiBzdHlsZT0ibGluZS1oZWlnaHQ6MS4yNSI+Um9jazwvdHNwYW4+PC90ZXh0PgogICA8dGV4dCB0cmFuc2Zvcm09InJvdGF0ZSg5MCkiIHg9IjU4Ny4zNzAxOCIgeT0iLTkyNi4xMzY2IiBzdHlsZT0ibGluZS1oZWlnaHQ6MCUiIHhtbDpzcGFjZT0icHJlc2VydmUiPjx0c3BhbiB4PSI1ODcuMzcwMTgiIHk9Ii05MjYuMTM2NiIgZm9udC1zaXplPSI5LjY1ODRweCIgc3R5bGU9ImxpbmUtaGVpZ2h0OjEuMjUiPldlYmI8L3RzcGFuPjwvdGV4dD4KICAgPHRleHQgeD0iODcxLjE2MTAxIiB5PSI2MzcuNTc1MiIgc3R5bGU9ImxpbmUtaGVpZ2h0OjAlIiB4bWw6c3BhY2U9InByZXNlcnZlIj48dHNwYW4geD0iODcxLjE2MTAxIiB5PSI2MzcuNTc1MiIgZm9udC1zaXplPSI5LjY1ODRweCIgc3R5bGU9ImxpbmUtaGVpZ2h0OjEuMjUiPkNlbnRyYWw8L3RzcGFuPjwvdGV4dD4KICAgPHRleHQgeD0iODczLjgzMjI4IiB5PSI1NzcuMDMyNDciIHN0eWxlPSJsaW5lLWhlaWdodDowJSIgeG1sOnNwYWNlPSJwcmVzZXJ2ZSI+PHRzcGFuIHg9Ijg3My44MzIyOCIgeT0iNTc3LjAzMjQ3IiBmb250LXNpemU9IjkuNjU4NHB4IiBzdHlsZT0ibGluZS1oZWlnaHQ6MS4yNSI+MTN0aDwvdHNwYW4+PC90ZXh0PgogICA8dGV4dCB4PSI4NzUuOTY2NDkiIHk9IjUxMC4yNjE4MSIgc3R5bGU9ImxpbmUtaGVpZ2h0OjAlIiB4bWw6c3BhY2U9InByZXNlcnZlIj48dHNwYW4geD0iODc1Ljk2NjQ5IiB5PSI1MTAuMjYxODEiIGZvbnQtc2l6ZT0iOS42NTg0cHgiIHN0eWxlPSJsaW5lLWhlaWdodDoxLjI1Ij4yMXN0PC90c3Bhbj48L3RleHQ+CiAgIDx0ZXh0IHg9Ijg4MS4zMTY1OSIgeT0iNDUwLjE5ODc2IiBzdHlsZT0ibGluZS1oZWlnaHQ6MCUiIHhtbDpzcGFjZT0icHJlc2VydmUiPjx0c3BhbiB4PSI4ODEuMzE2NTkiIHk9IjQ1MC4xOTg3NiIgZm9udC1zaXplPSI5LjY1ODRweCIgc3R5bGU9ImxpbmUtaGVpZ2h0OjEuMjUiPjI5dGg8L3RzcGFuPjwvdGV4dD4KICAgPHRleHQgeD0iNjE1Ljc5MjQ4IiB5PSIzODcuNzQ3MTYiIHN0eWxlPSJsaW5lLWhlaWdodDowJSIgeG1sOnNwYWNlPSJwcmVzZXJ2ZSI+PHRzcGFuIHg9IjYxNS43OTI0OCIgeT0iMzg3Ljc0NzE2IiBmb250LXNpemU9IjkuNjU4NHB4IiBzdHlsZT0ibGluZS1oZWlnaHQ6MS4yNSI+Mzd0aDwvdHNwYW4+PC90ZXh0PgogICA8dGV4dCB4PSI0ODQuNjkwMzciIHk9IjQ4MS42NTI4NiIgc3R5bGU9ImxpbmUtaGVpZ2h0OjAlIiB4bWw6c3BhY2U9InByZXNlcnZlIj48dHNwYW4geD0iNDg0LjY5MDM3IiB5PSI0ODEuNjUyODYiIGZvbnQtc2l6ZT0iOS42NTg0cHgiIHN0eWxlPSJsaW5lLWhlaWdodDoxLjI1Ij4yNXRoPC90c3Bhbj48L3RleHQ+CiAgIDx0ZXh0IHg9IjU2My4wNDY3NSIgeT0iNTEzLjM2MTMzIiBzdHlsZT0ibGluZS1oZWlnaHQ6MCUiIHhtbDpzcGFjZT0icHJlc2VydmUiPjx0c3BhbiB4PSI1NjMuMDQ2NzUiIHk9IjUxMy4zNjEzMyIgZm9udC1zaXplPSI5LjY1ODRweCIgc3R5bGU9ImxpbmUtaGVpZ2h0OjEuMjUiPjIxc3Q8L3RzcGFuPjwvdGV4dD4KICAgPHRleHQgeD0iNTY1Ljk3MTUiIHk9IjU3Ny44OTQ4NCIgc3R5bGU9ImxpbmUtaGVpZ2h0OjAlIiB4bWw6c3BhY2U9InByZXNlcnZlIj48dHNwYW4geD0iNTY1Ljk3MTUiIHk9IjU3Ny44OTQ4NCIgZm9udC1zaXplPSI5LjY1ODRweCIgc3R5bGU9ImxpbmUtaGVpZ2h0OjEuMjUiPjEzdGg8L3RzcGFuPjwvdGV4dD4KICAgPHRleHQgdHJhbnNmb3JtPSJyb3RhdGUoOTApIiB4PSI0MzMuNTgwNzUiIHk9Ii00NjAuNzMzMTIiIHN0eWxlPSJsaW5lLWhlaWdodDowJSIgeG1sOnNwYWNlPSJwcmVzZXJ2ZSI+PHRzcGFuIHg9IjQzMy41ODA3NSIgeT0iLTQ2MC43MzMxMiIgZm9udC1zaXplPSI5LjY1ODRweCIgc3R5bGU9ImxpbmUtaGVpZ2h0OjEuMjUiPkFtaWRvbjwvdHNwYW4+PC90ZXh0PgogICA8dGV4dCB0cmFuc2Zvcm09InJvdGF0ZSg5MCkiIHg9IjQwNS41MzA5OCIgeT0iLTUyMy41NDAxNiIgc3R5bGU9ImxpbmUtaGVpZ2h0OjAlIiB4bWw6c3BhY2U9InByZXNlcnZlIj48dHNwYW4geD0iNDA1LjUzMDk4IiB5PSItNTIzLjU0MDE2IiBmb250LXNpemU9IjkuNjU4NHB4IiBzdHlsZT0ibGluZS1oZWlnaHQ6MS4yNSI+QXJrYW5zYXM8L3RzcGFuPjwvdGV4dD4KICAgPHRleHQgdHJhbnNmb3JtPSJyb3RhdGUoOTApIiB4PSI3NDUuNDg0NjIiIHk9Ii0zNzIuNTg1OTQiIHN0eWxlPSJsaW5lLWhlaWdodDowJSIgeG1sOnNwYWNlPSJwcmVzZXJ2ZSI+PHRzcGFuIHg9Ijc0NS40ODQ2MiIgeT0iLTM3Mi41ODU5NCIgZm9udC1zaXplPSI5LjY1ODRweCIgc3R5bGU9ImxpbmUtaGVpZ2h0OjEuMjUiPldlc3Q8L3RzcGFuPjwvdGV4dD4KICAgPHRleHQgdHJhbnNmb3JtPSJyb3RhdGUoOTApIiB4PSI1OTYuNzI4MzMiIHk9Ii01MzEuMjU5MjgiIHN0eWxlPSJsaW5lLWhlaWdodDowJSIgeG1sOnNwYWNlPSJwcmVzZXJ2ZSI+PHRzcGFuIHg9IjU5Ni43MjgzMyIgeT0iLTUzMS4yNTkyOCIgZm9udC1zaXplPSI5LjY1ODRweCIgc3R5bGU9ImxpbmUtaGVpZ2h0OjEuMjUiPldhY288L3RzcGFuPjwvdGV4dD4KICAgPHRleHQgdHJhbnNmb3JtPSJyb3RhdGUoOTApIiB4PSI1OTUuNDM0ODEiIHk9Ii0xMjIuNTAyOTUiIHN0eWxlPSJsaW5lLWhlaWdodDowJSIgeG1sOnNwYWNlPSJwcmVzZXJ2ZSI+PHRzcGFuIHg9IjU5NS40MzQ4MSIgeT0iLTEyMi41MDI5NSIgZm9udC1zaXplPSI5LjY1ODRweCIgc3R5bGU9ImxpbmUtaGVpZ2h0OjEuMjUiPk1hemllPC90c3Bhbj48L3RleHQ+CiAgIDx0ZXh0IHRyYW5zZm9ybT0icm90YXRlKDQ1KSIgeD0iNjk1Ljc3Mjk1IiB5PSIxNjIuMDY4NzciIHN0eWxlPSJsaW5lLWhlaWdodDowJSIgeG1sOnNwYWNlPSJwcmVzZXJ2ZSI+PHRzcGFuIHg9IjY5NS43NzI5NSIgeT0iMTYyLjA2ODc3IiBmb250LXNpemU9IjkuNjU4NHB4IiBzdHlsZT0ibGluZS1oZWlnaHQ6MS4yNSI+Wm9vPC90c3Bhbj48L3RleHQ+CiAgIDx0ZXh0IHg9IjI0MC41ODk5NyIgeT0iNTc0LjQ0NTQzIiBzdHlsZT0ibGluZS1oZWlnaHQ6MCUiIHhtbDpzcGFjZT0icHJlc2VydmUiPjx0c3BhbiB4PSIyNDAuNTg5OTciIHk9IjU3NC40NDU0MyIgZm9udC1zaXplPSI5LjY1ODRweCIgc3R5bGU9ImxpbmUtaGVpZ2h0OjEuMjUiPjEzdGg8L3RzcGFuPjwvdGV4dD4KICAgPHRleHQgeD0iMjA2LjAzMTc1IiB5PSI1MTEuNjM2NjMiIHN0eWxlPSJsaW5lLWhlaWdodDowJSIgeG1sOnNwYWNlPSJwcmVzZXJ2ZSI+PHRzcGFuIHg9IjIwNi4wMzE3NSIgeT0iNTExLjYzNjYzIiBmb250LXNpemU9IjkuNjU4NHB4IiBzdHlsZT0ibGluZS1oZWlnaHQ6MS4yNSI+MjFzdDwvdHNwYW4+PC90ZXh0PgogICA8dGV4dCB0cmFuc2Zvcm09InJvdGF0ZSg5MCkiIHg9IjYyMC40NDMxMiIgeT0iLTUwNi42ODIxOSIgc3R5bGU9ImxpbmUtaGVpZ2h0OjAlIiB4bWw6c3BhY2U9InByZXNlcnZlIj48dHNwYW4geD0iNjIwLjQ0MzEyIiB5PSItNTA2LjY4MjE5IiBmb250LXNpemU9IjkuNjU4NHB4IiBzdHlsZT0ibGluZS1oZWlnaHQ6MS4yNSI+TmltczwvdHNwYW4+PC90ZXh0PgogICA8dGV4dCB4PSIzNzAuMjE2ODYiIHk9IjY5OC44NDAwOSIgc3R5bGU9ImxpbmUtaGVpZ2h0OjAlIiB4bWw6c3BhY2U9InByZXNlcnZlIj48dHNwYW4geD0iMzcwLjIxNjg2IiB5PSI2OTguODQwMDkiIGZvbnQtc2l6ZT0iOS42NTg0cHgiIHN0eWxlPSJsaW5lLWhlaWdodDoxLjI1Ij5NYXBsZTwvdHNwYW4+PC90ZXh0PgogICA8dGV4dCB4PSIzODQuMDg0MiIgeT0iNjgwLjg1MTM4IiBzdHlsZT0ibGluZS1oZWlnaHQ6MCUiIHhtbDpzcGFjZT0icHJlc2VydmUiPjx0c3BhbiB4PSIzODQuMDg0MiIgeT0iNjgwLjg1MTM4IiBmb250LXNpemU9IjkuNjU4NHB4IiBzdHlsZT0ibGluZS1oZWlnaHQ6MS4yNSI+RG91Z2xhczwvdHNwYW4+PC90ZXh0PgogIDwvZz4KICA8cGF0aCBkPSJtMzY3LjkxIDEwMTBoMjYzLjAyIiBjb2xvcj0iIzAwMDAwMCIgZmlsbD0ibm9uZSIgc3Ryb2tlPSIjMzM2IiBzdHJva2Utd2lkdGg9IjFweCIvPgogIDxnIGZpbGw9IiMwMDAwMDAiIGZvbnQtZmFtaWx5PSJWZXJkYW5hIiBsZXR0ZXItc3BhY2luZz0iMHB4IiB3b3JkLXNwYWNpbmc9IjBweCI+CiAgIDx0ZXh0IHRyYW5zZm9ybT0icm90YXRlKDkwKSIgeD0iNzM2LjI2NzQ2IiB5PSItNDMzLjEzNzc2IiBzdHlsZT0ibGluZS1oZWlnaHQ6MCUiIHhtbDpzcGFjZT0icHJlc2VydmUiPjx0c3BhbiB4PSI3MzYuMjY3NDYiIHk9Ii00MzMuMTM3NzYiIGZvbnQtc2l6ZT0iOS42NTg0cHgiIHN0eWxlPSJsaW5lLWhlaWdodDoxLjI1Ij5NZXJpZGlhbjwvdHNwYW4+PC90ZXh0PgogICA8dGV4dCB4PSI1NzIuODMyMTUiIHk9IjY0MC4yMDUyNiIgc3R5bGU9ImxpbmUtaGVpZ2h0OjAlIiB4bWw6c3BhY2U9InByZXNlcnZlIj48dHNwYW4geD0iNTcyLjgzMjE1IiB5PSI2NDAuMjA1MjYiIGZvbnQtc2l6ZT0iOS42NTg0cHgiIHN0eWxlPSJsaW5lLWhlaWdodDoxLjI1Ij5DZW50cmFsPC90c3Bhbj48L3RleHQ+CiAgIDx0ZXh0IHg9IjU3NS4wODk2NiIgeT0iNjcwLjkwMzUiIHN0eWxlPSJsaW5lLWhlaWdodDowJSIgeG1sOnNwYWNlPSJwcmVzZXJ2ZSI+PHRzcGFuIHg9IjU3NS4wODk2NiIgeT0iNjcwLjkwMzUiIGZvbnQtc2l6ZT0iOS42NTg0cHgiIHN0eWxlPSJsaW5lLWhlaWdodDoxLjI1Ij5Eb3VnbGFzPC90c3Bhbj48L3RleHQ+CiAgIDx0ZXh0IHg9IjQ5OS40ODk2MiIgeT0iMTAwOC42MDY5IiBzdHlsZT0ibGluZS1oZWlnaHQ6MCUiIHhtbDpzcGFjZT0icHJlc2VydmUiPjx0c3BhbiB4PSI0OTkuNDg5NjIiIHk9IjEwMDguNjA2OSIgZm9udC1zaXplPSI5LjY1ODRweCIgc3R5bGU9ImxpbmUtaGVpZ2h0OjEuMjUiPjQ3dGg8L3RzcGFuPjwvdGV4dD4KICAgPHRleHQgeD0iMjE2LjY0NTQzIiB5PSI3MjUuOTgyOTciIHN0eWxlPSJsaW5lLWhlaWdodDowJSIgeG1sOnNwYWNlPSJwcmVzZXJ2ZSI+PHRzcGFuIHg9IjIxNi42NDU0MyIgeT0iNzI1Ljk4Mjk3IiBmb250LXNpemU9IjkuNjU4NHB4IiBzdHlsZT0ibGluZS1oZWlnaHQ6MS4yNSI+S2VsbG9nZzwvdHNwYW4+PC90ZXh0PgogICA8dGV4dCB0cmFuc2Zvcm09InJvdGF0ZSg5MCkiIHg9Ijc3NC44NzU2MSIgeT0iLTUwOC4xODk3MyIgc3R5bGU9ImxpbmUtaGVpZ2h0OjAlIiB4bWw6c3BhY2U9InByZXNlcnZlIj48dHNwYW4geD0iNzc0Ljg3NTYxIiB5PSItNTA4LjE4OTczIiBmb250LXNpemU9IjkuNjU4NHB4IiBzdHlsZT0ibGluZS1oZWlnaHQ6MS4yNSI+TWNDbGVhbjwvdHNwYW4+PC90ZXh0PgogIDwvZz4KICA8cGF0aCB0cmFuc2Zvcm09InRyYW5zbGF0ZSgwIDI4Ny4zNikiIGQ9Im0zNjQuMTYgNjU4LjQzIDI5OS41MS0xLjAxMDJjNi40OTg3LTAuMDIxOSA2Ljk3NzIgOS4yNTQxIDE2LjU5NiA5LjM5MjUgMTIuMDU0IDAuMTczMzkgMjkuMTExLTAuNTM1NzIgNTQuMTE0LTAuMzAxMSIgY29sb3I9IiMwMDAwMDAiIGZpbGw9Im5vbmUiIHN0cm9rZT0iIzMzNiIgc3Ryb2tlLXdpZHRoPSIxcHgiLz4KICA8dGV4dCB4PSIzNzMuOTkzMDQiIHk9Ijk0NC4zNTc1NCIgZmlsbD0iIzAwMDAwMCIgZm9udC1mYW1pbHk9IlZlcmRhbmEiIGxldHRlci1zcGFjaW5nPSIwcHgiIHdvcmQtc3BhY2luZz0iMHB4IiBzdHlsZT0ibGluZS1oZWlnaHQ6MCUiIHhtbDpzcGFjZT0icHJlc2VydmUiPjx0c3BhbiB4PSIzNzMuOTkzMDQiIHk9Ijk0NC4zNTc1NCIgZm9udC1zaXplPSI5LjY1ODRweCIgc3R5bGU9ImxpbmUtaGVpZ2h0OjEuMjUiPk1hY0FydGh1cjwvdHNwYW4+PC90ZXh0PgogIDx0ZXh0IHRyYW5zZm9ybT0icm90YXRlKDkwKSIgeD0iNzgwLjg0NjA3IiB5PSItNDkwLjI0NTk3IiBmaWxsPSIjMDAwMDAwIiBmb250LWZhbWlseT0iVmVyZGFuYSIgbGV0dGVyLXNwYWNpbmc9IjBweCIgd29yZC1zcGFjaW5nPSIwcHgiIHN0eWxlPSJsaW5lLWhlaWdodDowJSIgeG1sOnNwYWNlPSJwcmVzZXJ2ZSI+PHRzcGFuIHg9Ijc4MC44NDYwNyIgeT0iLTQ5MC4yNDU5NyIgZm9udC1zaXplPSI5LjY1ODRweCIgc3R5bGU9ImxpbmUtaGVpZ2h0OjEuMjUiPlNlbmVjYTwvdHNwYW4+PC90ZXh0PgogIDxwYXRoIHRyYW5zZm9ybT0idHJhbnNsYXRlKDAgMjg3LjM2KSIgZD0ibTM2Ny43IDUzNy4yMSAxNDEuMjgtMS4wMTAyYzYuNDktMC4wNDY0IDEyLjc4MSA3LjIzNTQgMTkuMTkzIDcuMzIzNiA1NS45MjQgMC43Njg5IDE1OC42OS0wLjE3MzMzIDIzNi41MS0xLjAxMDIgNy44Mzk2LTAuMDg0MyAyMi42MzEtMTkuODU0IDMwLjMwNS0yMC40NTYgMjIuMjY2LTEuMzUxOCA0NS4xNzktMC41MDUwNyA2Ny42OC0wLjUwNTA3IDE2LjE0Ny0wLjYzMjQxIDMuNjEwMiAyMC43MDggMjYuNzY5IDIwLjcwOGwyNDMuNDUtMS4wMTAyIiBjb2xvcj0iIzAwMDAwMCIgZmlsbD0ibm9uZSIgc3Ryb2tlPSIjMzM2IiBzdHJva2Utd2lkdGg9IjFweCIvPgogIDx0ZXh0IHg9IjY4NS4yMDgxMyIgeT0iODI3LjUzMDgyIiBmaWxsPSIjMDAwMDAwIiBmb250LWZhbWlseT0iVmVyZGFuYSIgbGV0dGVyLXNwYWNpbmc9IjBweCIgd29yZC1zcGFjaW5nPSIwcHgiIHN0eWxlPSJsaW5lLWhlaWdodDowJSIgeG1sOnNwYWNlPSJwcmVzZXJ2ZSI+PHRzcGFuIHg9IjY4NS4yMDgxMyIgeT0iODI3LjUzMDgyIiBmb250LXNpemU9IjkuNjU4NHB4IiBzdHlsZT0ibGluZS1oZWlnaHQ6MS4yNSI+UGF3bmVlPC90c3Bhbj48L3RleHQ+CiAgPHBhdGggdHJhbnNmb3JtPSJ0cmFuc2xhdGUoMCAyODcuMzYpIiBkPSJtNTU0LjI5IDcyMS40My00LjI4NTctMTc4LjIxLTIuODU3MS00NDAuNzEtMC4zNTcxNC03OS4yODYiIGNvbG9yPSIjMDAwMDAwIiBmaWxsPSJub25lIiBzdHJva2U9IiMzMzYiIHN0cm9rZS13aWR0aD0iMXB4Ii8+CiAgPHRleHQgdHJhbnNmb3JtPSJyb3RhdGUoOTApIiB4PSI1MjkuNjI1MzEiIHk9Ii01NTAuODQ3NzgiIGZpbGw9IiMwMDAwMDAiIGZvbnQtZmFtaWx5PSJWZXJkYW5hIiBsZXR0ZXItc3BhY2luZz0iMHB4IiB3b3JkLXNwYWNpbmc9IjBweCIgc3R5bGU9ImxpbmUtaGVpZ2h0OjAlIiB4bWw6c3BhY2U9InByZXNlcnZlIj48dHNwYW4geD0iNTI5LjYyNTMxIiB5PSItNTUwLjg0Nzc4IiBmb250LXNpemU9IjkuNjU4NHB4IiBzdHlsZT0ibGluZS1oZWlnaHQ6MS4yNSI+QnJvYWR3YXk8L3RzcGFuPjwvdGV4dD4KIDwvZz4KPC9zdmc+Cg==", "public": true - }, - { - "link": "/api/images/system/map_marker_image_0.png", - "title": "Map marker image 0", - "type": "IMAGE", - "subType": "IMAGE", - "fileName": "map_marker_image_0.png", - "publicResourceKey": "CdCrVxsjA4EAiFaXK4a7K2MZFMeEuGeD", - "mediaType": "image/png", - "data": "iVBORw0KGgoAAAANSUhEUgAAAB4AAAB/CAYAAAD4mHJdAAAABHNCSVQICAgIfAhkiAAAAAlwSFlzAAACWAAAAlgB7MGOJQAAABl0RVh0U29mdHdhcmUAd3d3Lmlua3NjYXBlLm9yZ5vuPBoAAAwgSURBVGiB7Zt5cBT3lce/v18fc89oRoPEIRBCHIUxp2ywCAgIxLExvoidZIFNxXE2VXHirIO3aqtSseM43qpNeZfYKecox3bhpJykYgdjDkU2mBAB5vCamMNYAgQyURBCoxnNPd39O/aP7hGSEUR24L/uqqf+zfR77/Pe69/Rv6kWwcgPLRIJfZUAa7xez2xd90QBwDSNZKlkHJHAK+l09mUA7BP4vPpRUVExMVoRef+L998njxx9X57vPi/PnTsnO850yPaT7XLXrrflqjtWymhF+HA0Gp0wEp/kHymEQqG4ptJDGzf+um5RUxMSiV7Z3Lyt88L5nozgHJWj4pGmpqZav99PWve04onHHuswmViQzWb7ruZX+Udgv8/z3A+f/NGye1evxssvb+wo5PMfTZs6bfqcuXNHL7hlweh58+ZVAOTUpk2b0p9dvjyqqmrs/b8ejpUMc+unzjgUCsXjsYruE+2n1JY/NedM0zCi0VjA7/d7/f4AAgE//H4/vF4fOjvP9h5695C/oaEhcN/q1SyTzVdnMpnklXzTq4EplUsXfmaRCgC7du3cOn78+KfGj59Add3z1Md1vV7vqPa2D1sA4MYbZ6qUiqVX9X21i4TQcfX19QCA6urquN/vn0kAPRQKpYbTnzRpUhgAampqAEFrPjVYSql7fD4AgK5r2tV0AcDj8WkAoOk6JJGeTw2+nocLdsEu2AW7YBfsgl2wC3bBLtgFu2AX7IJdsAt2wS7YBbtgF+yCXbALdsEu2AW7YBfsgl2wC76mh/ppjIQgXVloPxVSBRV0rBe455P6+kTKBYF3tonxY/IWarry7DvI298Tgp0PR9RzACaN1NeIS100+EdvKXW3cMZvF8wCK10Sq2it2NAzakmukP/wmoP/KuId3BRUMg5uCfCSNVSKVn1rNto7Un8jLrUVqJ4Fi2eEQiEYBzOsy3SYL37TNQdzi8Q5FxkqJIQBsNLlYMGF/zqAJWBxSEogDAY+DJibYqTuRg4WFgO3OKhCYTExbKk5G/mbkSPP2DQhLA5IO/NhSz1MMP882BDgnAFQwdiVSs2vPVhYDIJLUMkBgw1favM6lJoZDDAYhKbAYsOX+rqAhcXAuQSIAKzhSy2vS8YmB7NYH4WCfM7kw5VaWtdpOO3bfWZJZVXgPxMX898bVsm6RhkTIseX29yyIErm/J5z5vwr6pvmsLYjBgeDwSpVJS/OmT1n1de+9qANZgLc4q9Dyj2qQhUhSSUAUCL7GBcchCymTEYBYNWqVXj30MGHT586PZEJ+WAul7ts8bjspd9QKDRNU2nz4z94YtI3H3oI+XwB//3j/9m77eRUUJ9/0eh4APGoDz6vCi4ksgUTmYyBC4k8RLGwtzF+EGu+tHqRqqrYtm0rXnzhhQ7G5cpsNnvyiuBIJFKnqvSd55772eilS5fhwIH9ye+/dPaEf1T9otW3T8GtiyYgGNBBymYEgLSbvakidu8/h01vnkYhcab1gcVs5tx5c6PHjh7DU0/9qFsINPb3939UZg28X11dXR0Qwtr9g8efqGtc+Bn89re/O7FhR9BXNaFm+n98uxHTZ1SDKQqKAihweZlITUVtXQwNs8fg+Bmzdk+bnmPdf/7bwsbGeO2ECaED+9/5XCxWuTGbzVpDwJpGNtx+28o77rr7bmzZsu3k7z+cMlHzeiPrvnoTwtVhFAVQHAZY4HBEoiAAeDXUjI/gyJGeQEd6TFj2tHYuXNgYy2azVe0fngiWDLNloHNFo4FZkXDsoTVr1+KD4x8U/3Ci1qP5PV7N74FeFUbClKDEriy57A5JANL5a68hnqoINL8OAPqbXbNp7clTxTVr1/oOHjr0MFXxq2Qy9wEFACnoY//6la9QAHj+9Q/eUL2RWkVXoWgqkhZBypRImkDKBFIWkLIk+h1JWdL+zrmeNCWSDFB0DYquQvWG637TcnozAKxbt45yTr8PAGowGBwVDAbvmT9/Pvbu3dddijV9WdUUUE0BUQm6kwaCYe+ljK/w8ruUdsYCBLlMEUQhoJoCygWM+LIvHTx4sGfevIbqYMD3BSFkJVUUrG5oaFABoPXwhd1UVUBVahtpKtoOnEV/gSHHgBwDso5c6XO6yNF24CNQTbV9qBRUUenuwz1/BoCZM2dplOJeSggWL1myFEII9IeXziIKBVUUW1QKo2Ci41Anei9kkWcY6Ex5R8qfc0wi0ZPF6QNnYeQNB2j7IQpFOtg0WwiBxoWNIBKLVQI6Z8rUqTh69FiWaFNmEIWgLFShoM5TZbIzgVxvFp6ID5rfA6JQgBAIxsGLJkrpAsycAcH4gN1gX0QPTW9vP5Grr58cJJTOpbqmjgWAnp6ei4QSEEJAKAGh1BbHCS2DLAFmMAgmICwObjDnyYMMAtJL9oN89vRc7KWUQtOUsSqhSggA8sWivSEh9qBxTiCEAGRwQARUVaB67Hf5pZAQlA0Ayrq2LTCogVyhlLURNEw55yYABP2+4ED3vHSClBKQ9jiFdHqvEBCMQzAOKYSt6/RqSGnbDPJRbgT93hAAcM4NyhjrBYDKylhswEEZJgYJFxDchnGTwSqasIomuMnsIDiH5GKIzUAQTsCVlZUxB9xLIUVbKpVEff3kiLTMfimEA7HP5bZgHMJ07mnJAiuaYEXT3jcZDMLkTgBD7exgBKRp9NfVTQwnk0kIKduoJGRH8/ZmhMNh4skc3DnEkDlAi4GbtjDDguVAmZM1M6yB68JyKsCGBqD373s7GAySnTt3gBDyFhWCvPHee/8HAJhTU5g0BMg4uMXBTT4AZSUTrGjBKpiwCnablQbDbZuyfTmAuRPMegA4euQopCRbaCaTOd2XSLzX3d2Nu+64bR7PnP3LJSCDMBm4YW9FWcmyQYMytsW+Zpfdsm1MdimAdMc7K29bMedCdzeSyeS76XT6jLNI4PGf/+w5aLqOu25IjOOWKcSg0jJjcLZ2ecsZD5TdybqsOxC0ZYpbJ58frek6nn/+eVBJHgecjXkqk2nu7Ozcdfz4cdx556rJN5C3m8v3jBt2xpdnazjysawNy5lUbKkrbmtZsWL5pGNHj6Or62+7k5lMy5CFNRQKTfN6tAMvvvhSRe3EOqx/4oXXLvia7qO6CsVZrey5154KB5YpKSG5tHs+5/ZsZnEIk6Ei1fLH73373i/09fXi0fWPpgyTLchkMqeGgAEgHA5/vjJWsf2PmzYr1dXV+K8fP7vjLxduWkY8ilpetQZPg+UJxh63lzqlNDi7gTa3fuPraz6bzxXw79/5FutP51am0+kdZdaQ/2kzDKNDUci51179w8pbP3er8sAD6+pnVCWy+/fs21LAqBnlMT50qJXFLq2a2L/5gaVy7N133j69u7sb67/7iFHIFf4tlU6/Ppg1kLGU8hYAywBMeOWV33gfXb9+1Q+ffDL+4Ne/AcYY/tS8PbV5++4Dhy+MopY2ZrLiidQDgDBSp5TS+Y7psS65ZOHsW26++eYosxje2PwGNm586eKzz/x027+sXWsBOAfgbULIQQAgUspaAA8BGAfnsamrq4u0tZ0Q333kkdGmZS3f8JNnlBXLV0AOilRKCS7sWYlxjlKxgHw+j5Y3W/C/Tz/NQ6Hgjp9seKZ31py5ajwe4wAtz9zdAH5OpJTPAqgEgL5USkpu4eLFHloqFXniYh9t3bunauuWrStisSi5//4vYnHTEkyZOhWqokBICcuy0N7ehr2trXjt1VeRzqTl3ffc81bjgsZELF4pQ6EAqa4eI6UEicfj5dhTKoCikynx6Bop5C14dJ2XcjmouipvvGFGoSJaWfr738/7tmzdjl/88pfIZjKwnH2SpmkIhSMYW1ODhvmNGFcztjhudFXR69Wgck58Hg+XEorH5ylDJYA8kVKOckpdB0ADIBOJhOzv70OhUFILuTzPZLNcSE6SfSlvJp0O5A1DN0qGDxLS4/OUAh6PGQqHC5XxeJEQgkgoRH1+L/wBP6LRuIjH4+Uf8gSAUwB+MbhzzQSwCMA0p/QUQADgNJ/PJ/v7+wnnnFiWkJZhKCYzKADoqiZUXeW67iGcSxKPx2QoFAo7AybnuE8COAZgHyHkxGXjeFAQEQCzANQCqAIQBeAH4AXgcex052w45TMcyQHIAOgBcBbAUUJI5uOM/wcaHmf3g9UM7QAAAABJRU5ErkJggg==", - "public": true - }, - { - "link": "/api/images/system/map_marker_image_1.png", - "title": "Map marker image 1", - "type": "IMAGE", - "subType": "IMAGE", - "fileName": "map_marker_image_1.png", - "publicResourceKey": "DF3fuPXua9Vi3o3d9Nz2I1LXDTwEs2Tv", - "mediaType": "image/png", - "data": "iVBORw0KGgoAAAANSUhEUgAAAB4AAAB/CAYAAAD4mHJdAAAABHNCSVQICAgIfAhkiAAAAAlwSFlzAAACWAAAAlgB7MGOJQAAABl0RVh0U29mdHdhcmUAd3d3Lmlua3NjYXBlLm9yZ5vuPBoAAA3vSURBVGiB7Vt7cFzVef+dc+/d90OrJyO/JSO/4ncxxfULMCYIAyEW08amJJgmM4GmnZjJdNq4gcSGzLQxk3bsaWcaaIHyR8CJrWAbpjgG/AhINsbYxkaSDY6xJFvSrrS7Wu3uvfecr3+cu1pbXhkJs/4nujNndufec77f+d7fd+4uw8gvIxwOfocBaz0e91yXyx0BgKyZiWUz5kcEvBKPJ18EYI+C5rWvkpKSyZGS8LGHGtbQR8ePUUdnB50/f57OfnqWWlpbaN++39O99fdQpCR0NBKJTBwJTfZFE4LBYLmh8+YXXvifKctWrEBPTze9+cbu8/3JVMoWNjwer3/ZsuUTvV4P239gP36yceNZW9CtyWQyei262hcB+7zurU/99Ge3r1nTgJdfevFsqr8/Wlc3rWbGzFkV8+fPr1iwYEEJgLadO3cmbr/jjohh6KXHPjxamsmar39pjoPBYHl5aUnnqZY2/b1Dh9LdPd39kUgk6PP5PD6fH36/Dz6fDx6PF+fOfdZ9+pPTgbq6Ou+aBx+0k/0DVYlEIjYcbX4tYM5pxeK/WKIDwM7Gxt0TJox/dtLESXC53JuHzvV4PBVHDjfvAYDZs+fonMsV16R9rYeM8XG1tbUAgMrKsrDP659DRJ5gMNhbaH5NTU0IAMaPHw9IPv5LAxORy+31AgBcLsO41lwAcLu9BgAYLheIkftLAxfzGgMeAx4DHgMeAx4DHgMeAx4DHgMeAx4DHgMeAx4D/lME1ke7gDF8ltbOHe3W923oEwYi1jxftWfZWgAziwacZkd2pfyN96XN5IIu7dMtIKA9/TI+zqCnFps2Alg5UlojFnVqIHZUlO2sl4RyC4CU+SEEylux8Z/iyc7mrxw4U7UnYwvGpXMYKIgNGdwXC/76C48oRw3sDWfnCgIkARJXcpwbvpA1e6T0Rq5jDr8EAHKA6OpjUOJwfeXAJAEhAXAGgEPKq+dIMVJqowDO4RAAC0rHV21u5LijAJaABAOIAY5Oh15iFMgj1zEpcUuuXjpIWeCouxjAtnIZcGKA5AVFbRfazPUC50QrKe8+Qy8qiqjBYIODA5DgBd1pBO9WRg9sy7yOhXBca+icYrgTOUGOiKnIVdCdisAxJGBTPsYW0nHRrJqgfNmGVtiqaeR1xchF7Vgz40q/BUNmISlcL7CUgJAMnOUiVwEdF0PURIAAVHaC8ucbAiwcQAb1KQpwXMjFrhtYMcOVO8lhOB457ujcKZd9hBguSYwcelTupKyaQWKYJFEU4xJw/Dhfcw29ilSBcNjEoTucFnSnkeOOvvTJpcVC1cYoGB5NAGEQTukjMAzHoghJghyWCRjenYoTuZjKx8xJiwU4LrSZ6waWpIoBjTuRqxDHRUkSUMWAJAZp6QU5FqOw65HHapG3bGVcBTZXDI5VnFaFgBL1yC34uoBJqEJeIwD2MMY1ilZidAFEMlDOqm9UdpJ0ZawumI+LU9ArwhyqWxyNz14XsBAMUnLVH0ttGB0XococdCGWE3XhOV85MF1WV2OY3omK0S2SkxgYAZYYJoAUpcqEEjG/Ru80isA1ysMXYNCnCum4aKUPgTu90w3sFinXL6nO/MadCAhiKloxBjFMeSuK0S1Kylv1cE1bUVoYyHwhoI6bCswpjjuxK5u2G2lcti2jzNCRTluioHEVw52EBA5/2LKsLBL+h2gs/o+Fjpa+MqtmjCbkqQJSYFF3T3zRsPMvA75i7UiBA4FApa6z5+fNnbd6/frHADghk7QdlhAHdMY0KXkZAHAuozaRMDRtKYMdAYDVq1fjcHPTD860nZlsS3qsv7+/+6pNDr0RDAanGTrf85Onnq75/uNPIJ1O4+dbnj34Ot6B4eFLqksqUeEvgcflAREhZabR09+Li/EorLQ4eFv317D2oW8t0XUdu3a9jud/9auztqD6ZDLZOixwOByeouv8D1u3brtpxYrb0XS4Kfbj3//8VHC8d0nDLXfj67OWIeQJgDGADfoOAxHQl05i14l92PHBXiTPp/c/OrFh9vwF8yMnjp/A5s2bOqXEbX19fX+8CriqqspvmunDTz/10xkr71qFnY07Tr1i7aqsLg2Vb6h/GOPCpdAYgTPlNLmF5AzpvBRp74viX3a/hO6+ge47+hZG61fVTz9y+DCee27Lx15fYFFHR8cAcNkPuw2DPXfP1+vvvf+BB7Br967WX9Mbk70eCn33zlWoCrsgKAFBCdgy/2nLBCyZgCUSMGUSpkzC0G1MrKzE0XMt/la9I0QnM+cWL15cmkwmK1tOnwpksuabg8YVifjnhEOlj69dtw6nT51Kv2q96fYG4fG7gbJwFhn7cxicIJgEZwAfEiokGASpWG1KhvIwg1/91ti1N9DEJ7ZOzKxdt87T1Nz8A67jv2Kx/o85AJDk//zXjzzCAeA/D7zU6PZjkkuXcBuEjN2OrGiHabfDFB2w7HZYoh3mVaMDWWdu1m6Hy5Bw6RIuP6b87+HXdgDAww8/zIXgGwFADwQCFYFA4BuLFi3CoUN/6LRmyL/y6gSXTtC4QDTVgQo/B5iEJFJ6Rt64lI6Vfi3JYBFHd1JA5wIunUNIQvpr/C+bm5u65s9fWBnwe9dISWVc0/DNhQsX6gDwTuuhd3WNYOSGTjjSehGp7EVYsguWuJQfssu51wVTXIIpLsGWlzBgXsSRM5dg6Hk6uk787Zb39gHA7NlzDM7xoM4Yli5fvgJSSiRmmbP9HNA0Qm4D6axEc6uJ6eOzuCloQuOOjlneqiUx2BK4lDBwut2DTFaHoXFYGilaHEjMMOdKKXHb4tvw/nvvL9UZ+Lyb6+pw/PjxpOZhsziX0DigcYLG1QaEBD69ZKA7wRHx2/C7BDSNwEi9AEmZGmJJA/1Z9SJM12hwvcYBzgmaj89obW3pr62dGmCcz+cuQ68GgEtdl7oYU40CZwSeW+As1rmy5KzNkbY1WILDlOp71ubgnKA7czVO4NyhwQhcFS7o6urq5pzDMLRqnXEtCACpdCrFHOHlAsTgYEq0nCnj0jnBY6i8KCTLBxbmzB2yPkczmU4lAYAxHtKFECYAPeDzBQZD4GU+motMueXklECWc7QkSaVDGoTAVetz8AGfLwQAQoisbtt2N4BJZaVlpZQjkntdS8w5UFOFni0YLMGhWfny1rbVPVuoOVKyK9ZeTrMsUl7qAHdzkPyktzeG2tqbw8KihCQlPjVUl2hLBkswmDZD1mJIWxwDWTXSFkfWUs8sZ64QzlqHjiRA2tQ7ZcqUYCwWgyT6hBNjb+3ZvQehUIi52tje3M6FyHHIYNkOqM2RsTjS2cuAs+pe1uYKPLcBkduA+m60sH1+v5/t3fsWGGP/x6VkjR98cAQAMNc7bXJepAyWzWHaimjW4siYDGmTY8DkGMhqapgcaVM9yw5ugMOyeX4DkmGub1otABz/6DiI2O94IpE4E+3p+aCzsxP333PfAvOi2G8JBtMRbU68GZMj44Ao0BzXmgOsRk7spq1oWILB6rQP3nt3/byLnZ2IxWKH4/H4pxoAeFzuC21tretW3rUKnk5mtWiflzAGxhgDQ66IYyrnOnqzBFfDZjAdLk1HMnkpMWRNLldmFomamtrIL/71F+iPJ/8mnc2e4QDQm0jsOXfu3L6TJ0/ivtX3T607M26P6SzMWI5eB7ktPHLPc/MV5xwTjpe9sfLOu2pOHD+JCxc+fyeWSLyZdzCoWsvjNpqef/6F8KTJU/DDLT/a3jM90eDWCS5dqmDvxF7NCRSAOikQhCuMUXHMEDjm3v7jb/+oIRrtxpMbnuzNmvatiUSi7QpgAAiFQneXlZbs3rGjUauorMSmLc+8dShy7HbDELqeA3bC4GCScHxWSMDOgVuaPb2t+t3vPfK9O1P9A/j7v3vC7ov318fj8bdyWFf8YCSbzZ7VNHb+tVdfrV911ypt/bcfq52J2uTBg+//LhWwZ0nJYTtWf6WrcccDGFgLdn5nwkPVD9Q/MLOzsxNPbvhhNpUc+G5vPL7jcqxBjonozwEsBzD5lVde9jy5YcPqTZufKX90/WOwbRv7330nsffDt08dSB41EkZyHPfwmwBAZuTFsBm48GeuWfai2oUzp02fFjKzJhp3NuLFF/+765e//Pfd31q71gLwGYC3GWNNAMCIaBKAJwBUO3uQnZ2d/MyZNv1vn/j+LUuXLq/Z/MyzCIfDTmxW8Y+IVFyWqjKRQkDYNqKxGDb97GkcOXLk7LZt/9F8c12dqKqqYM4LYALQCWAbI6J/A1AGgKK9vSBhoa8vEe+N9TwejcZYU1MTfrN9O6puqkJDw0NYtnwFpk6dCsZUMrFtG22trTiw/11s3/4aotEo1jQ04NZFt6KsrJTCoZKtJaWRiGG4KBKJ5BJWnw4gDedAx+0yMJCywLnQGWOSMabV1NbikUfX40J7B367sxFbt25DMhGHZZkgAC7DhWAojOpx4zF3wS0YP64aVZUVYCoQSN2la4bhIsNlcOS73H5GRBUAHgcwBYABAD09PZROp1gq2V8WTybq4vH4xEQ8oSWSSfSnUkinM7As9RdUw9Dh9XoR8PsQCgYRCodESTj0x1Aw2OrxBXsDgYBdXl6eM2IB4CyAbZcb12wASwBMB1Dq7C4ACJZIJHstM5PWdC2TTmcom80wEtySAFwupum6wbxeDxeCuT0et8/v94UBTTrSJABRAKcAHGCMnbrKjy/bRBjAHAATAFQ5NuAF4IFqAtyOKzKo83MLgAkgA2AAQB+ADgCfAzjBGIsPxfh/6wbDK7xbMFYAAAAASUVORK5CYII=", - "public": true - }, - { - "link": "/api/images/system/map_marker_image_2.png", - "title": "Map marker image 2", - "type": "IMAGE", - "subType": "IMAGE", - "fileName": "map_marker_image_2.png", - "publicResourceKey": "rz5SFAw2Sg5T2EyXNdwLycoDwf4QbMiZ", - "mediaType": "image/png", - "data": "iVBORw0KGgoAAAANSUhEUgAAAB4AAAB/CAYAAAD4mHJdAAAABHNCSVQICAgIfAhkiAAAAAlwSFlzAAACWAAAAlgB7MGOJQAAABl0RVh0U29mdHdhcmUAd3d3Lmlua3NjYXBlLm9yZ5vuPBoAAAyUSURBVGiB7Zp7kFRVesB/5/S9PdMz/ZoHMwo4MICDuoGVIYICIuzGcn0vC+oWGuNjs8mua9ySP4wpgyaiVVupbHYTsLJmNT7WNXExwqqzrq8g4oNxdXUgyEMQARmZd3fPTE/3vfd8+ePenhlgBsFlrFSqb9Wpvn3vd77f+b7zne87ffsqjv+wE4nYDQqWl5aWfDUcLqkAyOUHunID+Q8EnkilMo8C7gnoPPaRTCYnVyQT71+1bKl80PK+HGw9KPv27ZPde3bLjp075NVXX5FLL7lYKpLx9yoqKuqOR6f6PIFYLFZtW7r54YcfqV+4aBEdHe3ywm+e39eb6etzPZfS0kj5woUX1EUipWrj6xtZedddu11P5mYymc5j6Q19HrgsUrL67r/7+8VLly7j8cce3d3X29vZ0DB9yplnfWXcrFmzxjU2NiaBXevWrUsv/trXKmzbqnz/9+9VDuTyz35hi2OxWHV1ZbJ1245d1ltvvpFtb293Kyoq7LKystKysnLKy8soKyujtDTCxx/vSW3fsT3c0NAQWbpkiZvp7a9Np9Ndo+nWxwJrLYvmzV9gAaxbt/75urrxd592Wp0Oh0tWHSkbiUQSv3unuQlgxoyZltZm0TF1H+umUnrC1KlTAaipqUpESmMzFIRjsVj3SPJTpkyJA0ycOBGMnviFwSISLolEAAiHbftYsgAlJREbwA6HESUlXxg8lkcRXAQXwUVwEVwEF8FFcBH8/xhsnZC0ksw49eQPI5mmNtP54ccAIvqgqbz4aYn8zYoTUXXcFnueyZ8eXtleZt75iQnpU0VUvYiqB5mvu5p+XH9w8RtgnJMOLut/7rd4+fpRBcS52hz65csnHdxQ8clZnyuT3NV40sHRUnfq58mUWFJ70sEn+yiCi+AiuAgugovgIrgILoKL4CK4CC6Ci+D/Q+Djf/higk8Jzs0IMjIGYDGAp0AUeBbiHf3Xs/HGAHyYlYaRX0EYC4txNeIFugvWHyXzua8cnDjYGMBoQIFhRFfLmLjaCxqAw8iuHing/nCwGlLuMrKrveNfnccPFnyLtQ8c0a1jElye8sGFAYwUSCN54Q8GB4ljKKpHkBmLOZbB4FLgjhLVYxNcDFnkMXJUj03m0kOKR0sgYzLHRvlwpcDYI7oaGYvl5HB4ZRrJ1cf9fP5E/5NwQUKM7uoTOI4/ql38kmgUOCMnEHMCL819sag2jJJAxgIs+HNY6PGlpUxXDQWXw5dXjxH8SFZBPf7SyqKrMQLKG7b/OkpmTBJI0BSjbwTGYo6Ni5+ZjMJDj1wkxmQ5iV+VsBh9BzImKbNQFhWjp8wx21c7dKIV9A94IxaJsdplZt9574JQVcUdpr3rzlEHdzLASslpg19EofLMMa3dc0Z9c9YMXT+s7/GCo9FojWWph87+6tmX3XTTzT7XA/F4xutXr4fyOuQZVQUQ0tLphY1nlcn5YqgAuOyyy3inefOtH+36aLJr5Obe3t72o4w68kIsFptuW7pp5d33TPne928hm83yLz+6b9PVb/4niRK9QNfUoquqUaUREEEG+jGd7Zi2Dnpy3qYHGr7OFdcsX2BZFs899ywP/fznu11PLslkMjtHBScSiXrL0m+uXr3mlEWLFrN58+auxD+u2HZWhb0gcvkyShZ/Ax2N+70KPcVvJpMm999NZJ99mi1dzsb3rviLGbNmz6rY0rKFVavubTWG83p6ej4psAbfr66trS03xtlw98p76s+bN5+nnvzFtouevK/s1AnJM+I/vB37j6aDziJeCtxhzUkhTgoYwJpchz3zbJI7fj/pzA829f6iR/bPPW9e9aS6utjbb715YWVl1SOZTMY5DGzb6scXf+OSS6+48kqanntu55+99shkOyLx8uuvIjSuDEzq6Ob5TdzgPJ9GhT2sCbV4W1vK57R+FP9lOrT33PnzKjOZTM2OD7dFB3L5FwaDq6KifGYiXvn95ddey4fbtmWv2fhIiVUqpbpMEao2SH4fiKCMgAbRggSuVkKwEQz22q4iVKtQEYUtJvzdlvX6+bq67PJrr41sbm6+VVv8W1dX7/9oADH6b//0+us1QO/jD6xPhGWSCgsqLJj8PsTdjzj7Ma7fxDkAzn5wjry+H3H2YfL7UGGDCguJEqnPPf3YOoDrrrtOe56+C8CKRqPjotHoN+fMmcObb7zRelsk9W1lC4QFCRlM9yfoKnsoEgOLVWCxDLfYBRwwnXmwDIQVyoMbo6lrfrq5+dCsxsbaaHlkqTFSpUMhvjV79mwLwHvjldewBGxQlqBswXn3Y6T/EDhtiNOGuG2I2444QXPb/WtOGzhtmL7PcN7di7IFFegiJDq3+ZVXAWbMmGlrzRJLKc6/4IJFGGO4MdQ+gxAQEn/2LcH0u+Sa27HO0IRq/V+MSqnBOUZARMAD75DB2w4mq8AKWkggpPiOtJ3dYgznzTuPt996+3xLoc8+vaGBlpaWzFybrygtqCPgeODtcTFtBl1hUBHfGgl+wNGv8FIayWjE6KCfD1UhBVqotPWZO3Zs7506dVpUaT1Lh21rPED7oUNtKH8OUYLSoHTwWRiEAsmBDIA4gCPIAJh8YL3lyw7vi5JAJ7QdamvXWmPbofGW0qEYQL4/0zeYjdTRTQ0Oxp9/Svx9jvKAkBocsCh1dP9AZ76vNwOglI5bnuflAaukPBo9bM8UpMIjvxeiWAUbATHK3/yNJM/h30vKozEAz/Ny2nXddoCKyqrKwc5GDYFMUJmM8peLqyCvkH6FZP1zXP+eGBXIFvQcrquyqroyALdrxGzv7u5i6rTTE3lX0gUL/DIYPPfwFDh+k5xCBhSS1Ui/9s9zQ/cLz0rEGxqEGMWAK92T6yfHu7q6MCLbtSj1UtPzTcTjcfW0E3t5EBSkv0FgPgAMQgtWa/9azpcZHICrhvR48B+52CvRaFS9/PJLKKVe1Mao9e+++zsAtk9rnIwbLBFHIQ5IACWvkJxGBjSSDeDZ4HxAIznty+SV38chGIA/PXumzZoK0PJBCyLq1zqdTn/U2dHxbmtrKxddfmXj1r7QRr9jMH/5Ye4d8OdV+odZ3F+AqyG3F/oFelr62PQnl14667PWVrq6ut5JpVJ7giLBygfWrMYOh3ll/pLx4iojR7p3QMGgpQX4kPUE8OFuF0chrjIvzL78VDsc5sEHH0SLWkmQLuhOp5v27t376tatW7nk8iun/UN8VhM5BblASS5w53BowdXD4L7Lg8EG7Z6SM36z+MILp25p2cqBA/s3dKXTLxRSBeDvtUpL7M0PPfRwYtLken791z9Y++fevmWE/WJBIelbgJbDtz4mePblBksrcPU/ubVrF65Yuayzs50Vt6/ozuXduel0etdhYIB4PH5RVWXy+WeeWR8aV1PDz+6/56W//PDFxbpELGULgwVEcwSYoWXkKExOuatqGl9b8p3vfb2vt5/b/uoWtyfVe0kqlXqpwDpql1lVlbwhUhr52VNPrQ3PPuccNm16PbXrR3f+9pvm0NV+pWEwhQKIqKHnm57iV9nydc6Smxc1zm5MHvj0AHfecUeuv7f/u509PY8N5wyCReRcYCEw6YknHi9bcfvtl9276r7qG2+6Gdd12bhhQ/rghhe3TdmywT4l2zkhEeIUgJTLZ62RygPbT5/rlv/xvLOmnzE9ns/lWb9uPY8++u9tP/3JPzd9e/nyLLAXeE0ptRlAicgk4BZgfDAGc/DgQb1790fWrT+45Zz58xdMue+++0kkk/5N8RO2iPiZ0BiMCMbz8FyXzq4u7l91L5ub3969Zs2/Np/eMM2rrT21YKQBPgPWKBFZAyQA093drTzPobu7uyPV3XNbR2enam5uZu3atdTW1LDsqqtYeMEipk2b5m8GANd12bVzJ69vfI2n1/6Kjo5OvrVsKefOPZeqqkpJJCtXJ5OJinBpRJLxeOF3bI8FZIAYoEN2SHmeJ6GQ2CiMUipUP2UK199wI59+2sp/rVvP6tVryKRTOE4eAcJ2mFg8wfgJE5nZeA4TJ4yntmYcSimUUsaydMi2wxIKKTXM6n4lIuMCV08m2O52dHSQzfbpvkxvZSqTbkinUnWpVDqUzvTS29dHNpvFcfy6aNsWkUgp0fJyYrEYiUTcSybin8RjiZ2lZeXd0WjUra6uDg2L/z3A6uHBNQNYAEwHqvAXTTl4Kp3O9HhOvk+FGMhmHXHdHGLEE8CytNY6rCKRsPY8VRoOh8tisfIkhFxgIAB2AtuA15VS20ZcTsEgEsBM4DTgFKASiAClQAnBig7EC8/8BoAc0AekgE+B/cAWpVTqSMb/AlY1WXIncMcxAAAAAElFTkSuQmCC", - "public": true - }, - { - "link": "/api/images/system/map_marker_image_3.png", - "title": "Map marker image 3", - "type": "IMAGE", - "subType": "IMAGE", - "fileName": "map_marker_image_3.png", - "publicResourceKey": "KfPfTuvKCeAnmTcKcrvZQHfdU0TPArWY", - "mediaType": "image/png", - "data": "iVBORw0KGgoAAAANSUhEUgAAAB4AAAB/CAYAAAD4mHJdAAAABHNCSVQICAgIfAhkiAAAAAlwSFlzAAACWAAAAlgB7MGOJQAAABl0RVh0U29mdHdhcmUAd3d3Lmlua3NjYXBlLm9yZ5vuPBoAAAxNSURBVGiB7Zp7kFTllcB/5/a93dMz3T0PemYIDgoCPhZ5iaD4wNkFjQjRRMlLTNbSlKlyzZpobSVbFRPUbNVWSRCWuKvlxqybtbIrukp4SATZCAgospEBgeElj4EZ5t3d0+++37d/9O2ZnqEHQZzZSlXfqlMz/c253+875zvfOefeHuH8L6u83P+AwH0lJZ4pbrenEiCVSnYmEsndGl4NhSKvAJkLmPPcV0VFxZjKivKPv77wXr274WN9uvm0PnHihD5y9IhuPNioN216Vy+Yf6eurAj8b2Vl5aXnM6d8loLf7w9apvHhyy//29jZ9fW0t7fpdWtWN7Wdao4qpaiqDpbdXF9fV1paKpu3bGbxk08eSWXU9ZFIpOPirC33v7xs+TIdiUT0Pz239NjeaTOTHXXjdb4cuP6W5DOLFx/7aNdH+oknfqQryv0vXZTFfr8/GKyqaN7XeMhc//ba6NSfPFXqS6fESJ29jdGAX69+9KHY9OnTyxbec08mHInWhsPhzsHmNs4FNgxdf+NNN5sAh3/7n40dCxeKedUsOr6x8CzdsnBEQu9sPABwzTWTTMNQ9eec+1x/FDEuGTduHABXtreOKutJYyiFqq4tqD+5O3wJQF1dHSij7nODtdZuj9cLgMfGOpcuQInSFoDldqNFez43eCivIrgILoKL4CK4CC6Ci+AiuAgugovgIrgILoKL4CK4CC6Ci+A/B7B5vor6Mz4PNnbRYAAtoCQLUMMFVobuBWOALWdjVIGxiwbbZC3WkrXWLqAzJBZrR5T0LWTgdSHfdF1YcIlG57t8oM5nfov1OcCKPmDW1Rfi2IsA5yI5F9WFXF0o0i8arARwggsBu4BbhwaM6g0ujXY+9b+GLqrzLR5E5wsH2ziB5QRXoW8lCy3mosH553iwlDlEe9znai2DpMyhAJ+PxUNTJMhZm51+WM9xvsWFXD2kx0nl9rjQ4oYC3C+4BoEMnasl39Vn6wxRdcqbXApXpwupWBcEVgLKGLw6DU1w5bkaCjcChcYuHozuLYtqEFfroXC1TZ67GcbjlEuZWjSIHr6ozjZ7/y/VSWOLdgJIF9zjQl3JFwDOXn1lsYDOULm6X+YaROcLB6s8+LC2tzqvoc+Wx0L2nT/6wlIm5y6LQ9bs5TLXsO5x7jG192lxuJq9bCOg0aIRGcYEkt9lCsPp6lxlMsBlFE4ghcYuGoxznHKFYNjKYq7Zy5XFYW32lMtCBGzbLlwWLwB83m/2NNC44R0iFaP503+8jO1UqHz5wiwW0aNzvysgdPJTQr/7dFD9fHD+vecN9vl8NaYpv546ZeqCBx98CMhGbPXEqZRfcTWmyySTjuO2TMora/B4Sji+832OnWoGYMGCBez88IMfHD50eExG6Yd6enraBjJcAwf8fv+Vbsv1Pz9f/NT1y1esQCnNPz6zeGuy6WBN+MRRrwp1YMR6MOIJMqEuOj49xNFd2zh5aD9SVpr44PCJXVOmXXvpHfPm4fP7rtz98Z/usSz3+lQq1e/fnvuFSHl5+VjTNLb96lfPj6yv/0t2bN/eufJnj+37Uql1c/1Xv8WM279CaZn/rJcBGoj1hNm+7k22rF5JcyK1edp3Hps0bfq0yj0Ne/jFL55pVopZ3d3dx88C19bWlqVS8Z2Lf/7U1XNvu51Vb72x7/irz9fUBEcEv/03PyFYPRJDgZHt9XpvzG8QlAFnWppY+S9LaOnsaPPOWdhxx7z5V320cydLl/7yE2+pb+bp06dj/VxtWbJ03h13zr/r7rtZu2bNwVP/9cKYMiHwtW8+QNAbwOiOIN09SCiChCKQL+EIKhxBhcN4EGpGjuJww66yxNH9gePac+zGm26sikQiNY379/kSydT63uCqrCybXB6oeuS+RYvYv29f/OTKFz1+dIlXXFQrCznRjNhkRfdJzmIMEAExsqbUmh68holWGXf43deMg6NHJ+5btKjkgw8//IFh8lJnZ88nBoBWxpPf+e53DYC1Ly5bVSb6Mo8WSrQgx5uRY6cHSDMcz0q/vx/PSTNeJXi04EOPfe93L70JcP/99xu2bfwUwPT5fNU+n++rM2fO5P3332+uS3V9y9KCG8FSmtjRo3iN0uz+qqylemDnLhpDQDsFJGrHMG2F2xAyGi5Nhr65Y8f21unTZ9T4yrz3KqVHGC4X91x33XUmwN7N775nApbuk90nD5BpbUbaWqG9Dd3eju5o6y/t7dDehrS1kmltYffJ/ViA25nDBcbeLZs2AUyaNNkyDL5minDL7Nm3opSiNtQ0yUQwESydlXg6xc70Sf5CewliYSD9TqHu/anpIMUnJIiLjSVCGjAFTA21odNTlFLMunEWO7bvuMUUjKkTrriCvXv3RDyiJxpacGVXSc56W2uO6DhtKkmFFsocHchmtKhoukURNrJPG5YDdAEuDYaAV/TVjY0HesaNG+8Tw5hmuC1zFEBLS0urkQ3QPtFgILgQTC0IkAZSgEJQCClnTBwdF4KBOPf2iQBnzrS2GYaBZblGmWK4/ADxWCzqoS85iDOZDFiMS2ddV5Kz2EkGhgwECYLOzqOzxy0W7YkAiBgBw7btFIC3tMw/2JsrnS9OI5B2pPdt0AC9gdVZZxkBANu2k0Ymk2kDCI6oqsw1c/nNu8rVW8l+2ZFCkxRNzMhKUjQpNBlnv23nXfbAeTRQHayudMBtBlod6OrqZNz4CeVprcKqd4KsZBxgGk1KNEmBmGiijsScsZRo0s4CMnn3284CMqJCY8aOCXR2dqK0PmBokQ3r1q7D7/dLq7tyY8axMCOatDNZFqhJiCbuWNsLNrJjCUcnt4C0ZOew0WTQnDYr3/X5fLJx4wZE5B1DKVm1a9dHAIyYesPYjEBa+vYwJZAUSAgkHAtjookaWcl9Togm4eim8u5PS9YDNVNmXg7QsLsBreX3RjgcPtzW1rarubmZ+QvumtahXJvzrUzmWRvrZ61yxNnvPKuTA6xvt13bvjxv/tSW5mY6Ozt3hkKhoy4Ar6ek6dChg4vm3nY7oZJAJnG4oUIQESdD5Ud0v30XSBlZC1OGdjyTA/darwK3LcxcPm585ZJnl9ATinwvnkweNgC6wuF1x44d27R3714WfOWucZGrb3g7kee+eJ6LewPLcXU0bzwuuf2G3P3NoyevnzP3tsv3NOylqenkHzvD4fWQ197aikeW/nJJd1dnJ4//9On57V+a8Hoib7K4kQeUAWL0D7RcsJ2oqHv9wUcfu7Orq5MVK5Z3KS0P53j96lsgEPjyiKqKtW/891uu2tpalvzDMxsTW96s9yhMC8HUOCkxm07JO/fZk5A9dkmDTOSqWe/99fcfmRPtifHY3z6a6Q5F7gyFQhsKggFGjKh4wFviffG11153T59xHVu3bg3968/+7g9V3ae+0Zv0kX49l3ISjA2ccpe/NXvR9+uvnX5tRdOpJv7+xz9OxnpiD3d0d/97PqcXrLWeBcwGLnv11d96n3j88QVPPf108KHvPUwmk+HttWu71q96Y0dozzajJBUfXyqMA4gpfShmeY54JkzX19/6VzfMmDmjMpPOsOqtVbzyym9alz23fM23Fy1KACeAP4rIBwCitb4MeAQY5SxEt7a2qIaGBn70wx+OTKXTc5Y+t8w1d85cdN5KtdbYSqGVImPbJOIxotEo6/+wniXPPmsH/L4Ny5etaJk46Rqprq7JPTgooBn4Z9FaPw9UAHR1dSnbTsuZMy1GMpnItLZ2GFu3bq5d/fvVc0ZUjZB7F36d2fW3MmHCFZguF0pr0uk0Bxsb2bL5PV5fuZLuUEjfdffdG2+66ebW6mCVLvP5qa4OAoYEg8Gcg7tNIAIEADHdJnbcxmNZ6UQ05nK7TT1x4sRYRVV1/FTTqdLVa9bywgsvEImESKfSAFiWhT9QzqhL6rh25g3UjbokPnJkTaKkxFRaa8NtGbaIy+Up8eS2VgEx0VpXO66+HKfdbW9vV93d7RKNJl3xeNQOd4d1Mp0i3B3yRCKRsmgiYSVTaa9orS23lfR5vany8vKYLxCIeyxLKqoqtddbKh6PSVVVtQ4Gg5IHPQI8nx9ck4CbgSuBarJnvARsiUai4XBPmGQyqbWGRCxh2VrZAKYYLtNjZUyXSxsuU6oqyg1fwO91nhUSzvQdwB5gm4h8UvA4OYsoByYDY4EaoBLwAN7sYiDvZ4LsqUo60uNIK3AY2CMioYGM/wPREY0iGUY58wAAAABJRU5ErkJggg==", - "public": true } ], "scada": false, diff --git a/application/src/main/data/json/system/widget_types/map.json b/application/src/main/data/json/system/widget_types/map.json index 8fecb89f70..7ec5b35320 100644 --- a/application/src/main/data/json/system/widget_types/map.json +++ b/application/src/main/data/json/system/widget_types/map.json @@ -17,53 +17,9 @@ "settingsDirective": "tb-map-widget-settings", "hasBasicMode": true, "basicModeDirective": "tb-map-basic-config", - "defaultConfig": "{\"datasources\":[],\"timewindow\":{\"realtime\":{\"timewindowMs\":60000}},\"showTitle\":true,\"backgroundColor\":\"rgba(0, 0, 0, 0)\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"0px\",\"settings\":{\"mapType\":\"geoMap\",\"layers\":[{\"provider\":\"openstreet\",\"layerType\":\"OpenStreetMap.Mapnik\"},{\"provider\":\"openstreet\",\"layerType\":\"OpenStreetMap.HOT\"},{\"provider\":\"openstreet\",\"layerType\":\"Esri.WorldStreetMap\"},{\"provider\":\"openstreet\",\"layerType\":\"Esri.WorldTopoMap\"},{\"provider\":\"openstreet\",\"layerType\":\"Esri.WorldImagery\"},{\"provider\":\"openstreet\",\"layerType\":\"CartoDB.Positron\"},{\"provider\":\"openstreet\",\"layerType\":\"CartoDB.DarkMatter\"}],\"markers\":[{\"dsType\":\"function\",\"dsLabel\":\"First point\",\"dsDeviceId\":null,\"dsEntityAliasId\":null,\"dsFilterId\":null,\"additionalDataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"temperature\",\"color\":\"#2196f3\",\"settings\":{},\"_hash\":0.8239425680406081,\"funcBody\":\"var value = prevValue + Math.random() * 40 - 20;\\nif (value < -60) {\\n\\tvalue = -60;\\n} else if (value > 60) {\\n\\tvalue = 60;\\n}\\nreturn value;\",\"aggregationType\":null,\"units\":null,\"decimals\":null,\"usePostProcessing\":null,\"postFuncBody\":null}],\"label\":{\"show\":true,\"type\":\"pattern\",\"pattern\":\"${entityName}\"},\"tooltip\":{\"show\":true,\"trigger\":\"click\",\"autoclose\":true,\"type\":\"pattern\",\"pattern\":\"${entityName}

Latitude: ${latitude:7}
Longitude: ${longitude:7}
Temperature: ${temperature} °C
See tooltip settings for details\",\"offsetX\":0,\"offsetY\":-1},\"groups\":null,\"xKey\":{\"name\":\"f(x)\",\"label\":\"latitude\",\"type\":\"function\",\"funcBody\":\"var value = prevValue || 15.833293;\\nif (time % 500 < 500) {\\n value += Math.random() * 0.05 - 0.025;\\n}\\nreturn value;\",\"settings\":{},\"color\":\"#2196f3\"},\"yKey\":{\"name\":\"f(x)\",\"label\":\"longitude\",\"type\":\"function\",\"funcBody\":\"var value = prevValue || -90.454350;\\nif (time % 500 < 500) {\\n value += Math.random() * 0.05 - 0.025;\\n}\\nreturn value;\",\"settings\":{},\"color\":\"#2196f3\"},\"markerType\":\"shape\",\"markerShape\":{\"shape\":\"markerShape1\",\"size\":34,\"color\":{\"type\":\"function\",\"color\":\"#307FE5\",\"colorFunction\":\"var temperature = data.temperature;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\\n\"}},\"markerIcon\":{\"icon\":\"mdi:lightbulb-on\",\"size\":34,\"color\":{\"type\":\"constant\",\"color\":\"#307FE5\"}},\"markerImage\":{\"type\":\"image\",\"image\":\"data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9Ii0xOTEuMzUgLTM1MS4xOCAxMDgzLjU4IDE3MzAuNDYiPjxwYXRoIGZpbGwtcnVsZT0iZXZlbm9kZCIgY2xpcC1ydWxlPSJldmVub2RkIiBmaWxsPSIjZmU3NTY5IiBzdHJva2U9IiMwMDAiIHN0cm9rZS13aWR0aD0iMzciIHN0cm9rZS1taXRlcmxpbWl0PSIxMCIgZD0iTTM1MS44MzMgMTM2MC43OGMtMzguNzY2LTE5MC4zLTEwNy4xMTYtMzQ4LjY2NS0xODkuOTAzLTQ5NS40NEMxMDAuNTIzIDc1Ni40NjkgMjkuMzg2IDY1NS45NzgtMzYuNDM0IDU1MC40MDRjLTIxLjk3Mi0zNS4yNDQtNDAuOTM0LTcyLjQ3Ny02Mi4wNDctMTA5LjA1NC00Mi4yMTYtNzMuMTM3LTc2LjQ0NC0xNTcuOTM1LTc0LjI2OS0yNjcuOTMyIDIuMTI1LTEwNy40NzMgMzMuMjA4LTE5My42ODUgNzguMDMtMjY0LjE3M0MtMjEtMjA2LjY5IDEwMi40ODEtMzAxLjc0NSAyNjguMTY0LTMyNi43MjRjMTM1LjQ2Ni0yMC40MjUgMjYyLjQ3NSAxNC4wODIgMzUyLjU0MyA2Ni43NDcgNzMuNiA0My4wMzggMTMwLjU5NiAxMDAuNTI4IDE3My45MiAxNjguMjggNDUuMjIgNzAuNzE2IDc2LjM2IDE1NC4yNiA3OC45NzEgMjYzLjIzMyAxLjMzNyA1NS44My03LjgwNSAxMDcuNTMyLTIwLjY4NCAxNTAuNDE3LTEzLjAzNCA0My40MS0zMy45OTYgNzkuNjk1LTUyLjY0NiAxMTguNDU1LTM2LjQwNiA3NS42NTktODIuMDQ5IDE0NC45ODEtMTI3Ljg1NSAyMTQuMzQ1LTEzNi40MzcgMjA2LjYwNi0yNjQuNDk2IDQxNy4zMS0zMjAuNTggNzA2LjAyOHoiLz48Y2lyY2xlIGZpbGwtcnVsZT0iZXZlbm9kZCIgY2xpcC1ydWxlPSJldmVub2RkIiBjeD0iMzUyLjg5MSIgY3k9IjIyNS43NzkiIHI9IjE4My4zMzIiLz48L3N2Zz4=\",\"imageSize\":34},\"markerOffsetX\":0.5,\"markerOffsetY\":1},{\"dsType\":\"function\",\"dsLabel\":\"Second point\",\"dsDeviceId\":null,\"dsEntityAliasId\":null,\"dsFilterId\":null,\"additionalDataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"temperature\",\"color\":\"#2196f3\",\"settings\":{},\"_hash\":0.7826299113906372,\"funcBody\":\"var value = prevValue + Math.random() * 40 - 20;\\nif (value < -60) {\\n\\tvalue = -60;\\n} else if (value > 60) {\\n\\tvalue = 60;\\n}\\nreturn value;\",\"aggregationType\":null,\"units\":null,\"decimals\":null,\"usePostProcessing\":null,\"postFuncBody\":null}],\"label\":{\"show\":true,\"type\":\"pattern\",\"pattern\":\"${entityName}\"},\"tooltip\":{\"show\":true,\"trigger\":\"click\",\"autoclose\":true,\"type\":\"pattern\",\"pattern\":\"${entityName}

Latitude: ${latitude:7}
Longitude: ${longitude:7}
Temperature: ${temperature} °C
See tooltip settings for details\",\"offsetX\":0,\"offsetY\":-1},\"groups\":null,\"xKey\":{\"name\":\"f(x)\",\"label\":\"latitude\",\"type\":\"function\",\"funcBody\":\"var value = prevValue || 14.450463;\\nif (time % 500 < 500) {\\n value += Math.random() * 0.05 - 0.025;\\n}\\nreturn value;\",\"settings\":{},\"color\":\"#2196f3\",\"aggregationType\":null,\"units\":null,\"decimals\":null,\"usePostProcessing\":null,\"postFuncBody\":null},\"yKey\":{\"name\":\"f(x)\",\"label\":\"longitude\",\"type\":\"function\",\"funcBody\":\"var value = prevValue || -84.845334;\\nif (time % 500 < 500) {\\n value += Math.random() * 0.05 - 0.025;\\n}\\nreturn value;\",\"settings\":{},\"color\":\"#2196f3\",\"aggregationType\":null,\"units\":null,\"decimals\":null,\"usePostProcessing\":null,\"postFuncBody\":null},\"markerType\":\"image\",\"markerShape\":{\"shape\":\"markerShape1\",\"size\":34,\"color\":{\"type\":\"constant\",\"color\":\"#307FE5\"}},\"markerIcon\":{\"icon\":\"\",\"size\":34,\"color\":{\"type\":\"constant\",\"color\":\"#307FE5\"}},\"markerImage\":{\"type\":\"function\",\"image\":\"data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSIzNCIgaGVpZ2h0PSIzNCIgdmlld0JveD0iMCAwIDM0IDM0IiBmaWxsPSJub25lIj4KICA8ZyBmaWx0ZXI9InVybCgjZmlsdGVyMF9iZl84ODE2XzI2Mzg4NykiPgogICAgPHBhdGggZD0iTTE5IDI0LjVDMTcuNDA3NSAyNy40MTI1IDE3IDMzIDE3IDMzQzE3IDMzIDI3LjA4NTggMzIuMTk1NSAzMC45OTkyIDI3LjQ5OThDMzQgMjMuODk5MiAzMS45OTkyIDE5IDI3Ljk5OTIgMTlDMjMuOTk5MyAxOSAyMS4xOTI5IDIwLjQ4OTQgMTkgMjQuNVoiIGZpbGw9ImJsYWNrIiBmaWxsLW9wYWNpdHk9IjAuMjQiLz4KICA8L2c+CiAgPG1hc2sgaWQ9InBhdGgtMi1pbnNpZGUtMV84ODE2XzI2Mzg4NyIgZmlsbD0id2hpdGUiPgogICAgPHBhdGggZmlsbC1ydWxlPSJldmVub2RkIiBjbGlwLXJ1bGU9ImV2ZW5vZGQiIGQ9Ik0yOCAxMS45QzI4LjAwMzcgNS4zMjc2MSAyNC4yOTAyIDAgMTcgMEM5LjcwOTgzIDAgNS45OTYzIDUuMzI3NjEgNiAxMS45QzYuMDA0NzMgMjAuMjkzNyAxNyAzNCAxNyAzNEMxNyAzNCAyNy45OTUzIDIwLjI5MzcgMjggMTEuOVpNMjEuMjUgMTAuNjI1QzIxLjI1IDEyLjk3MjIgMTkuMzQ3MiAxNC44NzUgMTcgMTQuODc1QzE0LjY1MjggMTQuODc1IDEyLjc1IDEyLjk3MjIgMTIuNzUgMTAuNjI1QzEyLjc1IDguMjc3NzkgMTQuNjUyOCA2LjM3NSAxNyA2LjM3NUMxOS4zNDcyIDYuMzc1IDIxLjI1IDguMjc3NzkgMjEuMjUgMTAuNjI1WiIvPgogIDwvbWFzaz4KICA8cGF0aCBmaWxsLXJ1bGU9ImV2ZW5vZGQiIGNsaXAtcnVsZT0iZXZlbm9kZCIgZD0iTTI4IDExLjlDMjguMDAzNyA1LjMyNzYxIDI0LjI5MDIgMCAxNyAwQzkuNzA5ODMgMCA1Ljk5NjMgNS4zMjc2MSA2IDExLjlDNi4wMDQ3MyAyMC4yOTM3IDE3IDM0IDE3IDM0QzE3IDM0IDI3Ljk5NTMgMjAuMjkzNyAyOCAxMS45Wk0yMS4yNSAxMC42MjVDMjEuMjUgMTIuOTcyMiAxOS4zNDcyIDE0Ljg3NSAxNyAxNC44NzVDMTQuNjUyOCAxNC44NzUgMTIuNzUgMTIuOTcyMiAxMi43NSAxMC42MjVDMTIuNzUgOC4yNzc3OSAxNC42NTI4IDYuMzc1IDE3IDYuMzc1QzE5LjM0NzIgNi4zNzUgMjEuMjUgOC4yNzc3OSAyMS4yNSAxMC42MjVaIiBmaWxsPSIjMzA3ZmU1Ii8+CiAgPHBhdGggZD0iTTI4IDExLjlMMjkuMDYyNSAxMS45MDA2TDI4IDExLjlaTTYgMTEuOUw3LjA2MjUgMTEuODk5NEw2IDExLjlaTTE3IDM0TDE2LjE3MTIgMzQuNjY0OUwxNyAzNS42OThMMTcuODI4OCAzNC42NjQ5TDE3IDM0Wk0xNyAxLjA2MjVDMjAuMzY0MSAxLjA2MjUgMjIuODA4NSAyLjI4MDA2IDI0LjQyNzMgNC4xNzUzOUMyNi4wNjUyIDYuMDkzMjMgMjYuOTM5MiA4LjgwMzMxIDI2LjkzNzUgMTEuODk5NEwyOS4wNjI1IDExLjkwMDZDMjkuMDY0NCA4LjQyNDMgMjguMDgzNSA1LjE4NDM4IDI2LjA0MzEgMi43OTUzMkMyMy45ODM1IDAuMzgzNzQyIDIwLjkyNjEgLTEuMDYyNSAxNyAtMS4wNjI1VjEuMDYyNVpNNy4wNjI1IDExLjg5OTRDNy4wNjA3NiA4LjgwMzMxIDcuOTM0NzcgNi4wOTMyMyA5LjU3Mjc0IDQuMTc1MzlDMTEuMTkxNSAyLjI4MDA2IDEzLjYzNTkgMS4wNjI1IDE3IDEuMDYyNVYtMS4wNjI1QzEzLjA3MzkgLTEuMDYyNSAxMC4wMTY1IDAuMzgzNzQxIDcuOTU2ODYgMi43OTUzMkM1LjkxNjQ1IDUuMTg0MzggNC45MzU1NSA4LjQyNDMgNC45Mzc1IDExLjkwMDZMNy4wNjI1IDExLjg5OTRaTTE3IDM0QzE3LjgyODggMzMuMzM1MSAxNy44Mjg4IDMzLjMzNTIgMTcuODI4OCAzMy4zMzUyQzE3LjgyODggMzMuMzM1MiAxNy44Mjg4IDMzLjMzNTEgMTcuODI4NyAzMy4zMzVDMTcuODI4NSAzMy4zMzQ4IDE3LjgyODEgMzMuMzM0MyAxNy44Mjc2IDMzLjMzMzZDMTcuODI2NSAzMy4zMzIzIDE3LjgyNDggMzMuMzMwMSAxNy44MjI0IDMzLjMyNzFDMTcuODE3NiAzMy4zMjEyIDE3LjgxMDMgMzMuMzEyIDE3LjgwMDQgMzMuMjk5NUMxNy43ODA3IDMzLjI3NDcgMTcuNzUwOSAzMy4yMzcxIDE3LjcxMTggMzMuMTg3NEMxNy42MzM1IDMzLjA4NzggMTcuNTE3OCAzMi45Mzk1IDE3LjM3IDMyLjc0NzJDMTcuMDc0MiAzMi4zNjI0IDE2LjY1MDMgMzEuODAxNyAxNi4xNDA5IDMxLjEwMTlDMTUuMTIxNCAyOS43MDE0IDEzLjc2MzQgMjcuNzQ5NSAxMi40MDcxIDI1LjU0MTVDMTEuMDQ5IDIzLjMzMDYgOS43MDM4OCAyMC44ODEzIDguNzAwOTEgMTguNDg0MUM3LjY5MTA5IDE2LjA3MDYgNy4wNjM1NyAxMy43OTIxIDcuMDYyNSAxMS44OTk0TDQuOTM3NSAxMS45MDA2QzQuOTM4OCAxNC4yMDQ4IDUuNjg3NDYgMTYuNzg3MyA2Ljc0MDU4IDE5LjMwNDNDNy44MDA1NSAyMS44Mzc4IDkuMjA1MTYgMjQuMzg4OSAxMC41OTY0IDI2LjY1MzhDMTEuOTg5NSAyOC45MjE2IDEzLjM4MDcgMzAuOTIwOSAxNC40MjI5IDMyLjM1MjZDMTQuOTQ0NCAzMy4wNjg5IDE1LjM3OTUgMzMuNjQ0NiAxNS42ODUxIDM0LjA0MjJDMTUuODM3OSAzNC4yNDEgMTUuOTU4NCAzNC4zOTU0IDE2LjA0MTIgMzQuNTAwN0MxNi4wODI2IDM0LjU1MzQgMTYuMTE0NiAzNC41OTM4IDE2LjEzNjUgMzQuNjIxM0MxNi4xNDc0IDM0LjYzNTEgMTYuMTU1OSAzNC42NDU2IDE2LjE2MTcgMzQuNjUyOUMxNi4xNjQ2IDM0LjY1NjYgMTYuMTY2OCAzNC42NTk0IDE2LjE2ODQgMzQuNjYxNEMxNi4xNjkyIDM0LjY2MjQgMTYuMTY5OSAzNC42NjMyIDE2LjE3MDMgMzQuNjYzN0MxNi4xNzA2IDM0LjY2NCAxNi4xNzA4IDM0LjY2NDMgMTYuMTcwOSAzNC42NjQ1QzE2LjE3MTEgMzQuNjY0NyAxNi4xNzEyIDM0LjY2NDkgMTcgMzRaTTI2LjkzNzUgMTEuODk5NEMyNi45MzY0IDEzLjc5MjEgMjYuMzA4OSAxNi4wNzA2IDI1LjI5OTEgMTguNDg0MUMyNC4yOTYxIDIwLjg4MTMgMjIuOTUxIDIzLjMzMDYgMjEuNTkyOSAyNS41NDE1QzIwLjIzNjYgMjcuNzQ5NSAxOC44Nzg2IDI5LjcwMTQgMTcuODU5MSAzMS4xMDE5QzE3LjM0OTcgMzEuODAxNyAxNi45MjU4IDMyLjM2MjQgMTYuNjMgMzIuNzQ3MkMxNi40ODIyIDMyLjkzOTUgMTYuMzY2NSAzMy4wODc4IDE2LjI4ODIgMzMuMTg3NEMxNi4yNDkxIDMzLjIzNzEgMTYuMjE5MyAzMy4yNzQ3IDE2LjE5OTYgMzMuMjk5NUMxNi4xODk3IDMzLjMxMiAxNi4xODI0IDMzLjMyMTIgMTYuMTc3NiAzMy4zMjcxQzE2LjE3NTIgMzMuMzMwMSAxNi4xNzM1IDMzLjMzMjMgMTYuMTcyNCAzMy4zMzM2QzE2LjE3MTkgMzMuMzM0MyAxNi4xNzE1IDMzLjMzNDggMTYuMTcxMyAzMy4zMzVDMTYuMTcxMiAzMy4zMzUxIDE2LjE3MTIgMzMuMzM1MiAxNi4xNzEyIDMzLjMzNTJDMTYuMTcxMiAzMy4zMzUyIDE2LjE3MTIgMzMuMzM1MSAxNyAzNEMxNy44Mjg4IDM0LjY2NDkgMTcuODI4OSAzNC42NjQ3IDE3LjgyOTEgMzQuNjY0NUMxNy44MjkyIDM0LjY2NDMgMTcuODI5NCAzNC42NjQgMTcuODI5NyAzNC42NjM3QzE3LjgzMDEgMzQuNjYzMiAxNy44MzA4IDM0LjY2MjQgMTcuODMxNiAzNC42NjE0QzE3LjgzMzIgMzQuNjU5NCAxNy44MzU0IDM0LjY1NjYgMTcuODM4MyAzNC42NTI5QzE3Ljg0NDEgMzQuNjQ1NiAxNy44NTI2IDM0LjYzNTEgMTcuODYzNSAzNC42MjEzQzE3Ljg4NTQgMzQuNTkzOCAxNy45MTc0IDM0LjU1MzQgMTcuOTU4OCAzNC41MDA3QzE4LjA0MTYgMzQuMzk1NCAxOC4xNjIxIDM0LjI0MSAxOC4zMTQ5IDM0LjA0MjJDMTguNjIwNSAzMy42NDQ2IDE5LjA1NTYgMzMuMDY4OSAxOS41NzcxIDMyLjM1MjZDMjAuNjE5MyAzMC45MjA5IDIyLjAxMDUgMjguOTIxNiAyMy40MDM2IDI2LjY1MzhDMjQuNzk0OCAyNC4zODg5IDI2LjE5OTUgMjEuODM3OCAyNy4yNTk0IDE5LjMwNDNDMjguMzEyNSAxNi43ODczIDI5LjA2MTIgMTQuMjA0OCAyOS4wNjI1IDExLjkwMDZMMjYuOTM3NSAxMS44OTk0Wk0xNyAxNS45Mzc1QzE5LjkzNCAxNS45Mzc1IDIyLjMxMjUgMTMuNTU5IDIyLjMxMjUgMTAuNjI1SDIwLjE4NzVDMjAuMTg3NSAxMi4zODU0IDE4Ljc2MDQgMTMuODEyNSAxNyAxMy44MTI1VjE1LjkzNzVaTTExLjY4NzUgIDEwLjYyNUMxMS42ODc1IDEzLjU1OSAxNC4wNjYgMTUuOTM3NSAxNyAxNS45Mzc1VjEzLjgxMjVDMTUuMjM5NiAxMy44MTI1IDEzLjgxMjUgMTIuMzg1NCAxMy44MTI1IDEwLjYyNUgxMS42ODc1Wk0xNyA1LjMxMjVDMTQuMDY2IDUuMzEyNSAxMS42ODc1IDcuNjkwOTkgMTEuNjg3NSAxMC42MjVIMTMuODEyNUMxMy44MTI1IDguODY0NTkgMTUuMjM5NiA3LjQzNzUgMTcgNy40Mzc1VjUuMzEyNVpNMjIuMzEyNSAxMC42MjVDMjIuMzEyNSA3LjY5MDk5IDE5LjkzNCA1LjMxMjUgMTcgNS4zMTI1VjcuNDM3NUMxOC43NjA0IDcuNDM3NSAyMC4xODc1IDguODY0NTkgMjAuMTg3NSAxMC42MjVIMjIuMzEyNVoiIGZpbGw9ImJsYWNrIiBmaWxsLW9wYWNpdHk9IjAuMzgiIG1hc2s9InVybCgjcGF0aC0yLWluc2lkZS0xXzg4MTZfMjYzODg3KSIvPgogIDxkZWZzPgogICAgPGZpbHRlciBpZD0iZmlsdGVyMF9iZl84ODE2XzI2Mzg4NyIgeD0iMTIuNzUiIHk9IjE0Ljc1IiB3aWR0aD0iMjMuOTQzNCIgaGVpZ2h0PSIyMi41IiBmaWx0ZXJVbml0cz0idXNlclNwYWNlT25Vc2UiIGNvbG9yLWludGVycG9sYXRpb24tZmlsdGVycz0ic1JHQiI+CiAgICAgIDxmZUZsb29kIGZsb29kLW9wYWNpdHk9IjAiIHJlc3VsdD0iQmFja2dyb3VuZEltYWdlRml4Ii8+CiAgICAgIDxmZUdhdXNzaWFuQmx1ciBpbj0iQmFja2dyb3VuZEltYWdlRml4IiBzdGREZXZpYXRpb249IjIuMTI1Ii8+CiAgICAgIDxmZUNvbXBvc2l0ZSBpbjI9IlNvdXJjZUFscGhhIiBvcGVyYXRvcj0iaW4iIHJlc3VsdD0iZWZmZWN0MV9iYWNrZ3JvdW5kQmx1cl84ODE2XzI2Mzg4NyIvPgogICAgICA8ZmVCbGVuZCBtb2RlPSJub3JtYWwiIGluPSJTb3VyY2VHcmFwaGljIiBpbjI9ImVmZmVjdDFfYmFja2dyb3VuZEJsdXJfODgxNl8yNjM4ODciIHJlc3VsdD0ic2hhcGUiLz4KICAgICAgPGZlR2F1c3NpYW5CbHVyIHN0ZERldmlhdGlvbj0iMC41IiByZXN1bHQ9ImVmZmVjdDJfZm9yZWdyb3VuZEJsdXJfODgxNl8yNjM4ODciLz4KICAgIDwvZmlsdGVyPgogIDwvZGVmcz4KPC9zdmc+\",\"imageSize\":34,\"imageFunction\":\"var res = {\\n url: images[0],\\n size: 40\\n}\\nvar temperature = data.temperature;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120;\\n var index = Math.min(3, Math.floor(4 * percent));\\n res.url = images[index];\\n}\\nreturn res;\\n\",\"images\":[\"tb-image;/api/images/system/map_marker_image_0.png\",\"tb-image;/api/images/system/map_marker_image_1.png\",\"tb-image;/api/images/system/map_marker_image_2.png\",\"tb-image;/api/images/system/map_marker_image_3.png\"]},\"markerOffsetX\":0.5,\"markerOffsetY\":1}],\"polygons\":[],\"circles\":[],\"additionalDataSources\":[],\"controlsPosition\":\"topleft\",\"zoomActions\":[\"scroll\",\"doubleClick\",\"controlButtons\"],\"fitMapBounds\":true,\"useDefaultCenterPosition\":false,\"defaultCenterPosition\":\"0,0\",\"defaultZoomLevel\":null,\"minZoomLevel\":16,\"mapPageSize\":16384,\"background\":{\"type\":\"color\",\"color\":\"#fff\",\"overlay\":{\"enabled\":false,\"color\":\"rgba(255,255,255,0.72)\",\"blur\":3}},\"padding\":\"8px\",\"imageSourceType\":null,\"imageUrl\":null,\"imageEntityAlias\":null,\"imageUrlAttribute\":null},\"title\":\"Map\",\"useDashboardTimewindow\":true,\"displayTimewindow\":true,\"showTitleIcon\":false,\"titleTooltip\":\"\",\"dropShadow\":true,\"enableFullscreen\":true,\"widgetStyle\":{},\"widgetCss\":\"\",\"titleStyle\":{\"fontSize\":\"16px\",\"fontWeight\":400},\"pageSize\":1024,\"noDataDisplayMessage\":\"\",\"configMode\":\"basic\",\"titleFont\":null,\"titleColor\":null,\"margin\":\"0px\",\"borderRadius\":\"0px\",\"iconSize\":\"24px\",\"titleIcon\":\"map\",\"iconColor\":\"#1F6BDD\",\"actions\":{\"tooltipAction\":[]}}" + "defaultConfig": "{\"datasources\":[],\"timewindow\":{\"realtime\":{\"timewindowMs\":60000}},\"showTitle\":true,\"backgroundColor\":\"rgba(0, 0, 0, 0)\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"0px\",\"settings\":{\"mapType\":\"geoMap\",\"layers\":[{\"provider\":\"openstreet\",\"layerType\":\"OpenStreetMap.Mapnik\"},{\"provider\":\"openstreet\",\"layerType\":\"OpenStreetMap.HOT\"},{\"provider\":\"openstreet\",\"layerType\":\"Esri.WorldStreetMap\"},{\"provider\":\"openstreet\",\"layerType\":\"Esri.WorldTopoMap\"},{\"provider\":\"openstreet\",\"layerType\":\"Esri.WorldImagery\"},{\"provider\":\"openstreet\",\"layerType\":\"CartoDB.Positron\"},{\"provider\":\"openstreet\",\"layerType\":\"CartoDB.DarkMatter\"}],\"imageSource\":null,\"markers\":[{\"dsType\":\"function\",\"dsLabel\":\"First point\",\"dsDeviceId\":null,\"dsEntityAliasId\":null,\"dsFilterId\":null,\"additionalDataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"temperature\",\"color\":\"#2196f3\",\"settings\":{},\"_hash\":0.8239425680406081,\"funcBody\":\"var value = prevValue + Math.random() * 40 - 20;\\nif (value < -60) {\\n\\tvalue = -60;\\n} else if (value > 60) {\\n\\tvalue = 60;\\n}\\nreturn value;\",\"aggregationType\":null,\"units\":null,\"decimals\":null,\"usePostProcessing\":null,\"postFuncBody\":null}],\"label\":{\"show\":true,\"type\":\"pattern\",\"pattern\":\"${entityName}\"},\"tooltip\":{\"show\":true,\"trigger\":\"click\",\"autoclose\":true,\"type\":\"pattern\",\"pattern\":\"${entityName}

Latitude: ${latitude:7}
Longitude: ${longitude:7}
Temperature: ${temperature} °C
See tooltip settings for details\",\"offsetX\":0,\"offsetY\":-1},\"groups\":null,\"xKey\":{\"name\":\"f(x)\",\"label\":\"latitude\",\"type\":\"function\",\"funcBody\":\"var value = prevValue || 15.833293;\\nif (time % 500 < 500) {\\n value += Math.random() * 0.05 - 0.025;\\n}\\nreturn value;\",\"settings\":{},\"color\":\"#2196f3\"},\"yKey\":{\"name\":\"f(x)\",\"label\":\"longitude\",\"type\":\"function\",\"funcBody\":\"var value = prevValue || -90.454350;\\nif (time % 500 < 500) {\\n value += Math.random() * 0.05 - 0.025;\\n}\\nreturn value;\",\"settings\":{},\"color\":\"#2196f3\"},\"markerType\":\"shape\",\"markerShape\":{\"shape\":\"markerShape1\",\"size\":34,\"color\":{\"type\":\"function\",\"color\":\"#307FE5\",\"colorFunction\":\"var temperature = data.temperature;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\\n\"}},\"markerIcon\":{\"icon\":\"mdi:lightbulb-on\",\"size\":34,\"color\":{\"type\":\"constant\",\"color\":\"#307FE5\"}},\"markerImage\":{\"type\":\"image\",\"image\":\"data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9Ii0xOTEuMzUgLTM1MS4xOCAxMDgzLjU4IDE3MzAuNDYiPjxwYXRoIGZpbGwtcnVsZT0iZXZlbm9kZCIgY2xpcC1ydWxlPSJldmVub2RkIiBmaWxsPSIjZmU3NTY5IiBzdHJva2U9IiMwMDAiIHN0cm9rZS13aWR0aD0iMzciIHN0cm9rZS1taXRlcmxpbWl0PSIxMCIgZD0iTTM1MS44MzMgMTM2MC43OGMtMzguNzY2LTE5MC4zLTEwNy4xMTYtMzQ4LjY2NS0xODkuOTAzLTQ5NS40NEMxMDAuNTIzIDc1Ni40NjkgMjkuMzg2IDY1NS45NzgtMzYuNDM0IDU1MC40MDRjLTIxLjk3Mi0zNS4yNDQtNDAuOTM0LTcyLjQ3Ny02Mi4wNDctMTA5LjA1NC00Mi4yMTYtNzMuMTM3LTc2LjQ0NC0xNTcuOTM1LTc0LjI2OS0yNjcuOTMyIDIuMTI1LTEwNy40NzMgMzMuMjA4LTE5My42ODUgNzguMDMtMjY0LjE3M0MtMjEtMjA2LjY5IDEwMi40ODEtMzAxLjc0NSAyNjguMTY0LTMyNi43MjRjMTM1LjQ2Ni0yMC40MjUgMjYyLjQ3NSAxNC4wODIgMzUyLjU0MyA2Ni43NDcgNzMuNiA0My4wMzggMTMwLjU5NiAxMDAuNTI4IDE3My45MiAxNjguMjggNDUuMjIgNzAuNzE2IDc2LjM2IDE1NC4yNiA3OC45NzEgMjYzLjIzMyAxLjMzNyA1NS44My03LjgwNSAxMDcuNTMyLTIwLjY4NCAxNTAuNDE3LTEzLjAzNCA0My40MS0zMy45OTYgNzkuNjk1LTUyLjY0NiAxMTguNDU1LTM2LjQwNiA3NS42NTktODIuMDQ5IDE0NC45ODEtMTI3Ljg1NSAyMTQuMzQ1LTEzNi40MzcgMjA2LjYwNi0yNjQuNDk2IDQxNy4zMS0zMjAuNTggNzA2LjAyOHoiLz48Y2lyY2xlIGZpbGwtcnVsZT0iZXZlbm9kZCIgY2xpcC1ydWxlPSJldmVub2RkIiBjeD0iMzUyLjg5MSIgY3k9IjIyNS43NzkiIHI9IjE4My4zMzIiLz48L3N2Zz4=\",\"imageSize\":34},\"markerOffsetX\":0.5,\"markerOffsetY\":1},{\"dsType\":\"function\",\"dsLabel\":\"Second point\",\"dsDeviceId\":null,\"dsEntityAliasId\":null,\"dsFilterId\":null,\"additionalDataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"temperature\",\"color\":\"#2196f3\",\"settings\":{},\"_hash\":0.7826299113906372,\"funcBody\":\"var value = prevValue + Math.random() * 40 - 20;\\nif (value < -60) {\\n\\tvalue = -60;\\n} else if (value > 60) {\\n\\tvalue = 60;\\n}\\nreturn value;\",\"aggregationType\":null,\"units\":null,\"decimals\":null,\"usePostProcessing\":null,\"postFuncBody\":null}],\"label\":{\"show\":true,\"type\":\"pattern\",\"pattern\":\"${entityName}\"},\"tooltip\":{\"show\":true,\"trigger\":\"click\",\"autoclose\":true,\"type\":\"pattern\",\"pattern\":\"${entityName}

Latitude: ${latitude:7}
Longitude: ${longitude:7}
Temperature: ${temperature} °C
See tooltip settings for details\",\"offsetX\":0,\"offsetY\":-1},\"click\":{\"type\":\"doNothing\"},\"groups\":null,\"edit\":{\"enabledActions\":[],\"snappable\":false},\"xKey\":{\"name\":\"f(x)\",\"label\":\"latitude\",\"type\":\"function\",\"funcBody\":\"var value = prevValue || 14.450463;\\nif (time % 500 < 500) {\\n value += Math.random() * 0.05 - 0.025;\\n}\\nreturn value;\",\"settings\":{},\"color\":\"#2196f3\",\"aggregationType\":null,\"units\":null,\"decimals\":null,\"usePostProcessing\":null,\"postFuncBody\":null},\"yKey\":{\"name\":\"f(x)\",\"label\":\"longitude\",\"type\":\"function\",\"funcBody\":\"var value = prevValue || -84.845334;\\nif (time % 500 < 500) {\\n value += Math.random() * 0.05 - 0.025;\\n}\\nreturn value;\",\"settings\":{},\"color\":\"#2196f3\",\"aggregationType\":null,\"units\":null,\"decimals\":null,\"usePostProcessing\":null,\"postFuncBody\":null},\"markerType\":\"icon\",\"markerShape\":{\"shape\":\"markerShape1\",\"size\":34,\"color\":{\"type\":\"constant\",\"color\":\"#307FE5\"}},\"markerIcon\":{\"size\":40,\"color\":{\"type\":\"function\",\"color\":\"#307FE5\",\"colorFunction\":\"var colors = ['#488bc7','#549c5d','#ed7546','#be2b29'];\\nvar temperature = data.temperature;\\nvar res = colors[0];\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120;\\n var index = Math.min(3, Math.floor(4 * percent));\\n res = colors[index];\\n}\\nreturn res;\"},\"icon\":\"thermostat\"},\"markerImage\":{\"type\":\"function\",\"image\":\"/assets/markers/shape1.svg\",\"imageSize\":34,\"imageFunction\":\"\\n\",\"images\":[]},\"markerOffsetX\":0.5,\"markerOffsetY\":1,\"markerClustering\":{\"enable\":false,\"zoomOnClick\":true,\"maxZoom\":null,\"maxClusterRadius\":80,\"zoomAnimation\":true,\"showCoverageOnHover\":true,\"spiderfyOnMaxZoom\":false,\"chunkedLoad\":false,\"lazyLoad\":true,\"useClusterMarkerColorFunction\":false,\"clusterMarkerColorFunction\":null}}],\"polygons\":[],\"circles\":[],\"additionalDataSources\":[],\"controlsPosition\":\"topleft\",\"zoomActions\":[\"scroll\",\"doubleClick\",\"controlButtons\"],\"fitMapBounds\":true,\"useDefaultCenterPosition\":false,\"defaultCenterPosition\":\"0,0\",\"defaultZoomLevel\":null,\"mapPageSize\":16384,\"background\":{\"type\":\"color\",\"color\":\"#fff\",\"overlay\":{\"enabled\":false,\"color\":\"rgba(255,255,255,0.72)\",\"blur\":3}},\"padding\":\"8px\"},\"title\":\"Map\",\"useDashboardTimewindow\":true,\"displayTimewindow\":true,\"showTitleIcon\":false,\"titleTooltip\":\"\",\"dropShadow\":true,\"enableFullscreen\":true,\"widgetStyle\":{},\"widgetCss\":\"\",\"titleStyle\":{\"fontSize\":\"16px\",\"fontWeight\":400},\"pageSize\":1024,\"noDataDisplayMessage\":\"\",\"configMode\":\"basic\",\"titleFont\":null,\"titleColor\":null,\"margin\":\"0px\",\"borderRadius\":\"0px\",\"iconSize\":\"24px\",\"titleIcon\":\"map\",\"iconColor\":\"#1F6BDD\",\"actions\":{\"tooltipAction\":[]}}" }, "resources": [ - { - "link": "/api/images/system/map_marker_image_0.png", - "title": "Map marker image 0", - "type": "IMAGE", - "subType": "IMAGE", - "fileName": "map_marker_image_0.png", - "publicResourceKey": "CdCrVxsjA4EAiFaXK4a7K2MZFMeEuGeD", - "mediaType": "image/png", - "data": "iVBORw0KGgoAAAANSUhEUgAAAB4AAAB/CAYAAAD4mHJdAAAABHNCSVQICAgIfAhkiAAAAAlwSFlzAAACWAAAAlgB7MGOJQAAABl0RVh0U29mdHdhcmUAd3d3Lmlua3NjYXBlLm9yZ5vuPBoAAAwgSURBVGiB7Zt5cBT3lce/v18fc89oRoPEIRBCHIUxp2ywCAgIxLExvoidZIFNxXE2VXHirIO3aqtSseM43qpNeZfYKecox3bhpJykYgdjDkU2mBAB5vCamMNYAgQyURBCoxnNPd39O/aP7hGSEUR24L/uqqf+zfR77/Pe69/Rv6kWwcgPLRIJfZUAa7xez2xd90QBwDSNZKlkHJHAK+l09mUA7BP4vPpRUVExMVoRef+L998njxx9X57vPi/PnTsnO850yPaT7XLXrrflqjtWymhF+HA0Gp0wEp/kHymEQqG4ptJDGzf+um5RUxMSiV7Z3Lyt88L5nozgHJWj4pGmpqZav99PWve04onHHuswmViQzWb7ruZX+Udgv8/z3A+f/NGye1evxssvb+wo5PMfTZs6bfqcuXNHL7hlweh58+ZVAOTUpk2b0p9dvjyqqmrs/b8ejpUMc+unzjgUCsXjsYruE+2n1JY/NedM0zCi0VjA7/d7/f4AAgE//H4/vF4fOjvP9h5695C/oaEhcN/q1SyTzVdnMpnklXzTq4EplUsXfmaRCgC7du3cOn78+KfGj59Add3z1Md1vV7vqPa2D1sA4MYbZ6qUiqVX9X21i4TQcfX19QCA6urquN/vn0kAPRQKpYbTnzRpUhgAampqAEFrPjVYSql7fD4AgK5r2tV0AcDj8WkAoOk6JJGeTw2+nocLdsEu2AW7YBfsgl2wC3bBLtgFu2AX7IJdsAt2wS7YBbtgF+yCXbALdsEu2AW7YBfsgl2wC76mh/ppjIQgXVloPxVSBRV0rBe455P6+kTKBYF3tonxY/IWarry7DvI298Tgp0PR9RzACaN1NeIS100+EdvKXW3cMZvF8wCK10Sq2it2NAzakmukP/wmoP/KuId3BRUMg5uCfCSNVSKVn1rNto7Un8jLrUVqJ4Fi2eEQiEYBzOsy3SYL37TNQdzi8Q5FxkqJIQBsNLlYMGF/zqAJWBxSEogDAY+DJibYqTuRg4WFgO3OKhCYTExbKk5G/mbkSPP2DQhLA5IO/NhSz1MMP882BDgnAFQwdiVSs2vPVhYDIJLUMkBgw1favM6lJoZDDAYhKbAYsOX+rqAhcXAuQSIAKzhSy2vS8YmB7NYH4WCfM7kw5VaWtdpOO3bfWZJZVXgPxMX898bVsm6RhkTIseX29yyIErm/J5z5vwr6pvmsLYjBgeDwSpVJS/OmT1n1de+9qANZgLc4q9Dyj2qQhUhSSUAUCL7GBcchCymTEYBYNWqVXj30MGHT586PZEJ+WAul7ts8bjspd9QKDRNU2nz4z94YtI3H3oI+XwB//3j/9m77eRUUJ9/0eh4APGoDz6vCi4ksgUTmYyBC4k8RLGwtzF+EGu+tHqRqqrYtm0rXnzhhQ7G5cpsNnvyiuBIJFKnqvSd55772eilS5fhwIH9ye+/dPaEf1T9otW3T8GtiyYgGNBBymYEgLSbvakidu8/h01vnkYhcab1gcVs5tx5c6PHjh7DU0/9qFsINPb3939UZg28X11dXR0Qwtr9g8efqGtc+Bn89re/O7FhR9BXNaFm+n98uxHTZ1SDKQqKAihweZlITUVtXQwNs8fg+Bmzdk+bnmPdf/7bwsbGeO2ECaED+9/5XCxWuTGbzVpDwJpGNtx+28o77rr7bmzZsu3k7z+cMlHzeiPrvnoTwtVhFAVQHAZY4HBEoiAAeDXUjI/gyJGeQEd6TFj2tHYuXNgYy2azVe0fngiWDLNloHNFo4FZkXDsoTVr1+KD4x8U/3Ci1qP5PV7N74FeFUbClKDEriy57A5JANL5a68hnqoINL8OAPqbXbNp7clTxTVr1/oOHjr0MFXxq2Qy9wEFACnoY//6la9QAHj+9Q/eUL2RWkVXoWgqkhZBypRImkDKBFIWkLIk+h1JWdL+zrmeNCWSDFB0DYquQvWG637TcnozAKxbt45yTr8PAGowGBwVDAbvmT9/Pvbu3dddijV9WdUUUE0BUQm6kwaCYe+ljK/w8ruUdsYCBLlMEUQhoJoCygWM+LIvHTx4sGfevIbqYMD3BSFkJVUUrG5oaFABoPXwhd1UVUBVahtpKtoOnEV/gSHHgBwDso5c6XO6yNF24CNQTbV9qBRUUenuwz1/BoCZM2dplOJeSggWL1myFEII9IeXziIKBVUUW1QKo2Ci41Anei9kkWcY6Ex5R8qfc0wi0ZPF6QNnYeQNB2j7IQpFOtg0WwiBxoWNIBKLVQI6Z8rUqTh69FiWaFNmEIWgLFShoM5TZbIzgVxvFp6ID5rfA6JQgBAIxsGLJkrpAsycAcH4gN1gX0QPTW9vP5Grr58cJJTOpbqmjgWAnp6ei4QSEEJAKAGh1BbHCS2DLAFmMAgmICwObjDnyYMMAtJL9oN89vRc7KWUQtOUsSqhSggA8sWivSEh9qBxTiCEAGRwQARUVaB67Hf5pZAQlA0Ayrq2LTCogVyhlLURNEw55yYABP2+4ED3vHSClBKQ9jiFdHqvEBCMQzAOKYSt6/RqSGnbDPJRbgT93hAAcM4NyhjrBYDKylhswEEZJgYJFxDchnGTwSqasIomuMnsIDiH5GKIzUAQTsCVlZUxB9xLIUVbKpVEff3kiLTMfimEA7HP5bZgHMJ07mnJAiuaYEXT3jcZDMLkTgBD7exgBKRp9NfVTQwnk0kIKduoJGRH8/ZmhMNh4skc3DnEkDlAi4GbtjDDguVAmZM1M6yB68JyKsCGBqD373s7GAySnTt3gBDyFhWCvPHee/8HAJhTU5g0BMg4uMXBTT4AZSUTrGjBKpiwCnablQbDbZuyfTmAuRPMegA4euQopCRbaCaTOd2XSLzX3d2Nu+64bR7PnP3LJSCDMBm4YW9FWcmyQYMytsW+Zpfdsm1MdimAdMc7K29bMedCdzeSyeS76XT6jLNI4PGf/+w5aLqOu25IjOOWKcSg0jJjcLZ2ecsZD5TdybqsOxC0ZYpbJ58frek6nn/+eVBJHgecjXkqk2nu7Ozcdfz4cdx556rJN5C3m8v3jBt2xpdnazjysawNy5lUbKkrbmtZsWL5pGNHj6Or62+7k5lMy5CFNRQKTfN6tAMvvvhSRe3EOqx/4oXXLvia7qO6CsVZrey5154KB5YpKSG5tHs+5/ZsZnEIk6Ei1fLH73373i/09fXi0fWPpgyTLchkMqeGgAEgHA5/vjJWsf2PmzYr1dXV+K8fP7vjLxduWkY8ilpetQZPg+UJxh63lzqlNDi7gTa3fuPraz6bzxXw79/5FutP51am0+kdZdaQ/2kzDKNDUci51179w8pbP3er8sAD6+pnVCWy+/fs21LAqBnlMT50qJXFLq2a2L/5gaVy7N133j69u7sb67/7iFHIFf4tlU6/Ppg1kLGU8hYAywBMeOWV33gfXb9+1Q+ffDL+4Ne/AcYY/tS8PbV5++4Dhy+MopY2ZrLiidQDgDBSp5TS+Y7psS65ZOHsW26++eYosxje2PwGNm586eKzz/x027+sXWsBOAfgbULIQQAgUspaAA8BGAfnsamrq4u0tZ0Q333kkdGmZS3f8JNnlBXLV0AOilRKCS7sWYlxjlKxgHw+j5Y3W/C/Tz/NQ6Hgjp9seKZ31py5ajwe4wAtz9zdAH5OpJTPAqgEgL5USkpu4eLFHloqFXniYh9t3bunauuWrStisSi5//4vYnHTEkyZOhWqokBICcuy0N7ehr2trXjt1VeRzqTl3ffc81bjgsZELF4pQ6EAqa4eI6UEicfj5dhTKoCikynx6Bop5C14dJ2XcjmouipvvGFGoSJaWfr738/7tmzdjl/88pfIZjKwnH2SpmkIhSMYW1ODhvmNGFcztjhudFXR69Wgck58Hg+XEorH5ylDJYA8kVKOckpdB0ADIBOJhOzv70OhUFILuTzPZLNcSE6SfSlvJp0O5A1DN0qGDxLS4/OUAh6PGQqHC5XxeJEQgkgoRH1+L/wBP6LRuIjH4+Uf8gSAUwB+MbhzzQSwCMA0p/QUQADgNJ/PJ/v7+wnnnFiWkJZhKCYzKADoqiZUXeW67iGcSxKPx2QoFAo7AybnuE8COAZgHyHkxGXjeFAQEQCzANQCqAIQBeAH4AXgcex052w45TMcyQHIAOgBcBbAUUJI5uOM/wcaHmf3g9UM7QAAAABJRU5ErkJggg==", - "public": true - }, - { - "link": "/api/images/system/map_marker_image_1.png", - "title": "Map marker image 1", - "type": "IMAGE", - "subType": "IMAGE", - "fileName": "map_marker_image_1.png", - "publicResourceKey": "DF3fuPXua9Vi3o3d9Nz2I1LXDTwEs2Tv", - "mediaType": "image/png", - "data": "iVBORw0KGgoAAAANSUhEUgAAAB4AAAB/CAYAAAD4mHJdAAAABHNCSVQICAgIfAhkiAAAAAlwSFlzAAACWAAAAlgB7MGOJQAAABl0RVh0U29mdHdhcmUAd3d3Lmlua3NjYXBlLm9yZ5vuPBoAAA3vSURBVGiB7Vt7cFzVef+dc+/d90OrJyO/JSO/4ncxxfULMCYIAyEW08amJJgmM4GmnZjJdNq4gcSGzLQxk3bsaWcaaIHyR8CJrWAbpjgG/AhINsbYxkaSDY6xJFvSrrS7Wu3uvfecr3+cu1pbXhkJs/4nujNndufec77f+d7fd+4uw8gvIxwOfocBaz0e91yXyx0BgKyZiWUz5kcEvBKPJ18EYI+C5rWvkpKSyZGS8LGHGtbQR8ePUUdnB50/f57OfnqWWlpbaN++39O99fdQpCR0NBKJTBwJTfZFE4LBYLmh8+YXXvifKctWrEBPTze9+cbu8/3JVMoWNjwer3/ZsuUTvV4P239gP36yceNZW9CtyWQyei262hcB+7zurU/99Ge3r1nTgJdfevFsqr8/Wlc3rWbGzFkV8+fPr1iwYEEJgLadO3cmbr/jjohh6KXHPjxamsmar39pjoPBYHl5aUnnqZY2/b1Dh9LdPd39kUgk6PP5PD6fH36/Dz6fDx6PF+fOfdZ9+pPTgbq6Ou+aBx+0k/0DVYlEIjYcbX4tYM5pxeK/WKIDwM7Gxt0TJox/dtLESXC53JuHzvV4PBVHDjfvAYDZs+fonMsV16R9rYeM8XG1tbUAgMrKsrDP659DRJ5gMNhbaH5NTU0IAMaPHw9IPv5LAxORy+31AgBcLsO41lwAcLu9BgAYLheIkftLAxfzGgMeAx4DHgMeAx4DHgMeAx4DHgMeAx4DHgMeAx4D/lME1ke7gDF8ltbOHe3W923oEwYi1jxftWfZWgAziwacZkd2pfyN96XN5IIu7dMtIKA9/TI+zqCnFps2Alg5UlojFnVqIHZUlO2sl4RyC4CU+SEEylux8Z/iyc7mrxw4U7UnYwvGpXMYKIgNGdwXC/76C48oRw3sDWfnCgIkARJXcpwbvpA1e6T0Rq5jDr8EAHKA6OpjUOJwfeXAJAEhAXAGgEPKq+dIMVJqowDO4RAAC0rHV21u5LijAJaABAOIAY5Oh15iFMgj1zEpcUuuXjpIWeCouxjAtnIZcGKA5AVFbRfazPUC50QrKe8+Qy8qiqjBYIODA5DgBd1pBO9WRg9sy7yOhXBca+icYrgTOUGOiKnIVdCdisAxJGBTPsYW0nHRrJqgfNmGVtiqaeR1xchF7Vgz40q/BUNmISlcL7CUgJAMnOUiVwEdF0PURIAAVHaC8ucbAiwcQAb1KQpwXMjFrhtYMcOVO8lhOB457ujcKZd9hBguSYwcelTupKyaQWKYJFEU4xJw/Dhfcw29ilSBcNjEoTucFnSnkeOOvvTJpcVC1cYoGB5NAGEQTukjMAzHoghJghyWCRjenYoTuZjKx8xJiwU4LrSZ6waWpIoBjTuRqxDHRUkSUMWAJAZp6QU5FqOw65HHapG3bGVcBTZXDI5VnFaFgBL1yC34uoBJqEJeIwD2MMY1ilZidAFEMlDOqm9UdpJ0ZawumI+LU9ArwhyqWxyNz14XsBAMUnLVH0ttGB0XococdCGWE3XhOV85MF1WV2OY3omK0S2SkxgYAZYYJoAUpcqEEjG/Ru80isA1ysMXYNCnCum4aKUPgTu90w3sFinXL6nO/MadCAhiKloxBjFMeSuK0S1Kylv1cE1bUVoYyHwhoI6bCswpjjuxK5u2G2lcti2jzNCRTluioHEVw52EBA5/2LKsLBL+h2gs/o+Fjpa+MqtmjCbkqQJSYFF3T3zRsPMvA75i7UiBA4FApa6z5+fNnbd6/frHADghk7QdlhAHdMY0KXkZAHAuozaRMDRtKYMdAYDVq1fjcHPTD860nZlsS3qsv7+/+6pNDr0RDAanGTrf85Onnq75/uNPIJ1O4+dbnj34Ot6B4eFLqksqUeEvgcflAREhZabR09+Li/EorLQ4eFv317D2oW8t0XUdu3a9jud/9auztqD6ZDLZOixwOByeouv8D1u3brtpxYrb0XS4Kfbj3//8VHC8d0nDLXfj67OWIeQJgDGADfoOAxHQl05i14l92PHBXiTPp/c/OrFh9vwF8yMnjp/A5s2bOqXEbX19fX+8CriqqspvmunDTz/10xkr71qFnY07Tr1i7aqsLg2Vb6h/GOPCpdAYgTPlNLmF5AzpvBRp74viX3a/hO6+ge47+hZG61fVTz9y+DCee27Lx15fYFFHR8cAcNkPuw2DPXfP1+vvvf+BB7Br967WX9Mbk70eCn33zlWoCrsgKAFBCdgy/2nLBCyZgCUSMGUSpkzC0G1MrKzE0XMt/la9I0QnM+cWL15cmkwmK1tOnwpksuabg8YVifjnhEOlj69dtw6nT51Kv2q96fYG4fG7gbJwFhn7cxicIJgEZwAfEiokGASpWG1KhvIwg1/91ti1N9DEJ7ZOzKxdt87T1Nz8A67jv2Kx/o85AJDk//zXjzzCAeA/D7zU6PZjkkuXcBuEjN2OrGiHabfDFB2w7HZYoh3mVaMDWWdu1m6Hy5Bw6RIuP6b87+HXdgDAww8/zIXgGwFADwQCFYFA4BuLFi3CoUN/6LRmyL/y6gSXTtC4QDTVgQo/B5iEJFJ6Rt64lI6Vfi3JYBFHd1JA5wIunUNIQvpr/C+bm5u65s9fWBnwe9dISWVc0/DNhQsX6gDwTuuhd3WNYOSGTjjSehGp7EVYsguWuJQfssu51wVTXIIpLsGWlzBgXsSRM5dg6Hk6uk787Zb39gHA7NlzDM7xoM4Yli5fvgJSSiRmmbP9HNA0Qm4D6axEc6uJ6eOzuCloQuOOjlneqiUx2BK4lDBwut2DTFaHoXFYGilaHEjMMOdKKXHb4tvw/nvvL9UZ+Lyb6+pw/PjxpOZhsziX0DigcYLG1QaEBD69ZKA7wRHx2/C7BDSNwEi9AEmZGmJJA/1Z9SJM12hwvcYBzgmaj89obW3pr62dGmCcz+cuQ68GgEtdl7oYU40CZwSeW+As1rmy5KzNkbY1WILDlOp71ubgnKA7czVO4NyhwQhcFS7o6urq5pzDMLRqnXEtCACpdCrFHOHlAsTgYEq0nCnj0jnBY6i8KCTLBxbmzB2yPkczmU4lAYAxHtKFECYAPeDzBQZD4GU+motMueXklECWc7QkSaVDGoTAVetz8AGfLwQAQoisbtt2N4BJZaVlpZQjkntdS8w5UFOFni0YLMGhWfny1rbVPVuoOVKyK9ZeTrMsUl7qAHdzkPyktzeG2tqbw8KihCQlPjVUl2hLBkswmDZD1mJIWxwDWTXSFkfWUs8sZ64QzlqHjiRA2tQ7ZcqUYCwWgyT6hBNjb+3ZvQehUIi52tje3M6FyHHIYNkOqM2RsTjS2cuAs+pe1uYKPLcBkduA+m60sH1+v5/t3fsWGGP/x6VkjR98cAQAMNc7bXJepAyWzWHaimjW4siYDGmTY8DkGMhqapgcaVM9yw5ugMOyeX4DkmGub1otABz/6DiI2O94IpE4E+3p+aCzsxP333PfAvOi2G8JBtMRbU68GZMj44Ao0BzXmgOsRk7spq1oWILB6rQP3nt3/byLnZ2IxWKH4/H4pxoAeFzuC21tretW3rUKnk5mtWiflzAGxhgDQ66IYyrnOnqzBFfDZjAdLk1HMnkpMWRNLldmFomamtrIL/71F+iPJ/8mnc2e4QDQm0jsOXfu3L6TJ0/ivtX3T607M26P6SzMWI5eB7ktPHLPc/MV5xwTjpe9sfLOu2pOHD+JCxc+fyeWSLyZdzCoWsvjNpqef/6F8KTJU/DDLT/a3jM90eDWCS5dqmDvxF7NCRSAOikQhCuMUXHMEDjm3v7jb/+oIRrtxpMbnuzNmvatiUSi7QpgAAiFQneXlZbs3rGjUauorMSmLc+8dShy7HbDELqeA3bC4GCScHxWSMDOgVuaPb2t+t3vPfK9O1P9A/j7v3vC7ov318fj8bdyWFf8YCSbzZ7VNHb+tVdfrV911ypt/bcfq52J2uTBg+//LhWwZ0nJYTtWf6WrcccDGFgLdn5nwkPVD9Q/MLOzsxNPbvhhNpUc+G5vPL7jcqxBjonozwEsBzD5lVde9jy5YcPqTZufKX90/WOwbRv7330nsffDt08dSB41EkZyHPfwmwBAZuTFsBm48GeuWfai2oUzp02fFjKzJhp3NuLFF/+765e//Pfd31q71gLwGYC3GWNNAMCIaBKAJwBUO3uQnZ2d/MyZNv1vn/j+LUuXLq/Z/MyzCIfDTmxW8Y+IVFyWqjKRQkDYNqKxGDb97GkcOXLk7LZt/9F8c12dqKqqYM4LYALQCWAbI6J/A1AGgKK9vSBhoa8vEe+N9TwejcZYU1MTfrN9O6puqkJDw0NYtnwFpk6dCsZUMrFtG22trTiw/11s3/4aotEo1jQ04NZFt6KsrJTCoZKtJaWRiGG4KBKJ5BJWnw4gDedAx+0yMJCywLnQGWOSMabV1NbikUfX40J7B367sxFbt25DMhGHZZkgAC7DhWAojOpx4zF3wS0YP64aVZUVYCoQSN2la4bhIsNlcOS73H5GRBUAHgcwBYABAD09PZROp1gq2V8WTybq4vH4xEQ8oSWSSfSnUkinM7As9RdUw9Dh9XoR8PsQCgYRCodESTj0x1Aw2OrxBXsDgYBdXl6eM2IB4CyAbZcb12wASwBMB1Dq7C4ACJZIJHstM5PWdC2TTmcom80wEtySAFwupum6wbxeDxeCuT0et8/v94UBTTrSJABRAKcAHGCMnbrKjy/bRBjAHAATAFQ5NuAF4IFqAtyOKzKo83MLgAkgA2AAQB+ADgCfAzjBGIsPxfh/6wbDK7xbMFYAAAAASUVORK5CYII=", - "public": true - }, - { - "link": "/api/images/system/map_marker_image_2.png", - "title": "Map marker image 2", - "type": "IMAGE", - "subType": "IMAGE", - "fileName": "map_marker_image_2.png", - "publicResourceKey": "rz5SFAw2Sg5T2EyXNdwLycoDwf4QbMiZ", - "mediaType": "image/png", - "data": "iVBORw0KGgoAAAANSUhEUgAAAB4AAAB/CAYAAAD4mHJdAAAABHNCSVQICAgIfAhkiAAAAAlwSFlzAAACWAAAAlgB7MGOJQAAABl0RVh0U29mdHdhcmUAd3d3Lmlua3NjYXBlLm9yZ5vuPBoAAAyUSURBVGiB7Zp7kFRVesB/5/S9PdMz/ZoHMwo4MICDuoGVIYICIuzGcn0vC+oWGuNjs8mua9ySP4wpgyaiVVupbHYTsLJmNT7WNXExwqqzrq8g4oNxdXUgyEMQARmZd3fPTE/3vfd8+ePenhlgBsFlrFSqb9Wpvn3vd77f+b7zne87ffsqjv+wE4nYDQqWl5aWfDUcLqkAyOUHunID+Q8EnkilMo8C7gnoPPaRTCYnVyQT71+1bKl80PK+HGw9KPv27ZPde3bLjp075NVXX5FLL7lYKpLx9yoqKuqOR6f6PIFYLFZtW7r54YcfqV+4aBEdHe3ywm+e39eb6etzPZfS0kj5woUX1EUipWrj6xtZedddu11P5mYymc5j6Q19HrgsUrL67r/7+8VLly7j8cce3d3X29vZ0DB9yplnfWXcrFmzxjU2NiaBXevWrUsv/trXKmzbqnz/9+9VDuTyz35hi2OxWHV1ZbJ1245d1ltvvpFtb293Kyoq7LKystKysnLKy8soKyujtDTCxx/vSW3fsT3c0NAQWbpkiZvp7a9Np9Ndo+nWxwJrLYvmzV9gAaxbt/75urrxd592Wp0Oh0tWHSkbiUQSv3unuQlgxoyZltZm0TF1H+umUnrC1KlTAaipqUpESmMzFIRjsVj3SPJTpkyJA0ycOBGMnviFwSISLolEAAiHbftYsgAlJREbwA6HESUlXxg8lkcRXAQXwUVwEVwEF8FFcBH8/xhsnZC0ksw49eQPI5mmNtP54ccAIvqgqbz4aYn8zYoTUXXcFnueyZ8eXtleZt75iQnpU0VUvYiqB5mvu5p+XH9w8RtgnJMOLut/7rd4+fpRBcS52hz65csnHdxQ8clZnyuT3NV40sHRUnfq58mUWFJ70sEn+yiCi+AiuAgugovgIrgILoKL4CK4CC6Ci+D/Q+Djf/higk8Jzs0IMjIGYDGAp0AUeBbiHf3Xs/HGAHyYlYaRX0EYC4txNeIFugvWHyXzua8cnDjYGMBoQIFhRFfLmLjaCxqAw8iuHing/nCwGlLuMrKrveNfnccPFnyLtQ8c0a1jElye8sGFAYwUSCN54Q8GB4ljKKpHkBmLOZbB4FLgjhLVYxNcDFnkMXJUj03m0kOKR0sgYzLHRvlwpcDYI7oaGYvl5HB4ZRrJ1cf9fP5E/5NwQUKM7uoTOI4/ql38kmgUOCMnEHMCL819sag2jJJAxgIs+HNY6PGlpUxXDQWXw5dXjxH8SFZBPf7SyqKrMQLKG7b/OkpmTBJI0BSjbwTGYo6Ni5+ZjMJDj1wkxmQ5iV+VsBh9BzImKbNQFhWjp8wx21c7dKIV9A94IxaJsdplZt9574JQVcUdpr3rzlEHdzLASslpg19EofLMMa3dc0Z9c9YMXT+s7/GCo9FojWWph87+6tmX3XTTzT7XA/F4xutXr4fyOuQZVQUQ0tLphY1nlcn5YqgAuOyyy3inefOtH+36aLJr5Obe3t72o4w68kIsFptuW7pp5d33TPne928hm83yLz+6b9PVb/4niRK9QNfUoquqUaUREEEG+jGd7Zi2Dnpy3qYHGr7OFdcsX2BZFs899ywP/fznu11PLslkMjtHBScSiXrL0m+uXr3mlEWLFrN58+auxD+u2HZWhb0gcvkyShZ/Ax2N+70KPcVvJpMm999NZJ99mi1dzsb3rviLGbNmz6rY0rKFVavubTWG83p6ej4psAbfr66trS03xtlw98p76s+bN5+nnvzFtouevK/s1AnJM+I/vB37j6aDziJeCtxhzUkhTgoYwJpchz3zbJI7fj/pzA829f6iR/bPPW9e9aS6utjbb715YWVl1SOZTMY5DGzb6scXf+OSS6+48kqanntu55+99shkOyLx8uuvIjSuDEzq6Ob5TdzgPJ9GhT2sCbV4W1vK57R+FP9lOrT33PnzKjOZTM2OD7dFB3L5FwaDq6KifGYiXvn95ddey4fbtmWv2fhIiVUqpbpMEao2SH4fiKCMgAbRggSuVkKwEQz22q4iVKtQEYUtJvzdlvX6+bq67PJrr41sbm6+VVv8W1dX7/9oADH6b//0+us1QO/jD6xPhGWSCgsqLJj8PsTdjzj7Ma7fxDkAzn5wjry+H3H2YfL7UGGDCguJEqnPPf3YOoDrrrtOe56+C8CKRqPjotHoN+fMmcObb7zRelsk9W1lC4QFCRlM9yfoKnsoEgOLVWCxDLfYBRwwnXmwDIQVyoMbo6lrfrq5+dCsxsbaaHlkqTFSpUMhvjV79mwLwHvjldewBGxQlqBswXn3Y6T/EDhtiNOGuG2I2444QXPb/WtOGzhtmL7PcN7di7IFFegiJDq3+ZVXAWbMmGlrzRJLKc6/4IJFGGO4MdQ+gxAQEn/2LcH0u+Sa27HO0IRq/V+MSqnBOUZARMAD75DB2w4mq8AKWkggpPiOtJ3dYgznzTuPt996+3xLoc8+vaGBlpaWzFybrygtqCPgeODtcTFtBl1hUBHfGgl+wNGv8FIayWjE6KCfD1UhBVqotPWZO3Zs7506dVpUaT1Lh21rPED7oUNtKH8OUYLSoHTwWRiEAsmBDIA4gCPIAJh8YL3lyw7vi5JAJ7QdamvXWmPbofGW0qEYQL4/0zeYjdTRTQ0Oxp9/Svx9jvKAkBocsCh1dP9AZ76vNwOglI5bnuflAaukPBo9bM8UpMIjvxeiWAUbATHK3/yNJM/h30vKozEAz/Ny2nXddoCKyqrKwc5GDYFMUJmM8peLqyCvkH6FZP1zXP+eGBXIFvQcrquyqroyALdrxGzv7u5i6rTTE3lX0gUL/DIYPPfwFDh+k5xCBhSS1Ui/9s9zQ/cLz0rEGxqEGMWAK92T6yfHu7q6MCLbtSj1UtPzTcTjcfW0E3t5EBSkv0FgPgAMQgtWa/9azpcZHICrhvR48B+52CvRaFS9/PJLKKVe1Mao9e+++zsAtk9rnIwbLBFHIQ5IACWvkJxGBjSSDeDZ4HxAIznty+SV38chGIA/PXumzZoK0PJBCyLq1zqdTn/U2dHxbmtrKxddfmXj1r7QRr9jMH/5Ye4d8OdV+odZ3F+AqyG3F/oFelr62PQnl14667PWVrq6ut5JpVJ7giLBygfWrMYOh3ll/pLx4iojR7p3QMGgpQX4kPUE8OFuF0chrjIvzL78VDsc5sEHH0SLWkmQLuhOp5v27t376tatW7nk8iun/UN8VhM5BblASS5w53BowdXD4L7Lg8EG7Z6SM36z+MILp25p2cqBA/s3dKXTLxRSBeDvtUpL7M0PPfRwYtLken791z9Y++fevmWE/WJBIelbgJbDtz4mePblBksrcPU/ubVrF65Yuayzs50Vt6/ozuXduel0etdhYIB4PH5RVWXy+WeeWR8aV1PDz+6/56W//PDFxbpELGULgwVEcwSYoWXkKExOuatqGl9b8p3vfb2vt5/b/uoWtyfVe0kqlXqpwDpql1lVlbwhUhr52VNPrQ3PPuccNm16PbXrR3f+9pvm0NV+pWEwhQKIqKHnm57iV9nydc6Smxc1zm5MHvj0AHfecUeuv7f/u509PY8N5wyCReRcYCEw6YknHi9bcfvtl9276r7qG2+6Gdd12bhhQ/rghhe3TdmywT4l2zkhEeIUgJTLZ62RygPbT5/rlv/xvLOmnzE9ns/lWb9uPY8++u9tP/3JPzd9e/nyLLAXeE0ptRlAicgk4BZgfDAGc/DgQb1790fWrT+45Zz58xdMue+++0kkk/5N8RO2iPiZ0BiMCMbz8FyXzq4u7l91L5ub3969Zs2/Np/eMM2rrT21YKQBPgPWKBFZAyQA093drTzPobu7uyPV3XNbR2enam5uZu3atdTW1LDsqqtYeMEipk2b5m8GANd12bVzJ69vfI2n1/6Kjo5OvrVsKefOPZeqqkpJJCtXJ5OJinBpRJLxeOF3bI8FZIAYoEN2SHmeJ6GQ2CiMUipUP2UK199wI59+2sp/rVvP6tVryKRTOE4eAcJ2mFg8wfgJE5nZeA4TJ4yntmYcSimUUsaydMi2wxIKKTXM6n4lIuMCV08m2O52dHSQzfbpvkxvZSqTbkinUnWpVDqUzvTS29dHNpvFcfy6aNsWkUgp0fJyYrEYiUTcSybin8RjiZ2lZeXd0WjUra6uDg2L/z3A6uHBNQNYAEwHqvAXTTl4Kp3O9HhOvk+FGMhmHXHdHGLEE8CytNY6rCKRsPY8VRoOh8tisfIkhFxgIAB2AtuA15VS20ZcTsEgEsBM4DTgFKASiAClQAnBig7EC8/8BoAc0AekgE+B/cAWpVTqSMb/AlY1WXIncMcxAAAAAElFTkSuQmCC", - "public": true - }, - { - "link": "/api/images/system/map_marker_image_3.png", - "title": "Map marker image 3", - "type": "IMAGE", - "subType": "IMAGE", - "fileName": "map_marker_image_3.png", - "publicResourceKey": "KfPfTuvKCeAnmTcKcrvZQHfdU0TPArWY", - "mediaType": "image/png", - "data": "iVBORw0KGgoAAAANSUhEUgAAAB4AAAB/CAYAAAD4mHJdAAAABHNCSVQICAgIfAhkiAAAAAlwSFlzAAACWAAAAlgB7MGOJQAAABl0RVh0U29mdHdhcmUAd3d3Lmlua3NjYXBlLm9yZ5vuPBoAAAxNSURBVGiB7Zp7kFTllcB/5/a93dMz3T0PemYIDgoCPhZ5iaD4wNkFjQjRRMlLTNbSlKlyzZpobSVbFRPUbNVWSRCWuKvlxqybtbIrukp4SATZCAgospEBgeElj4EZ5t3d0+++37d/9O2ZnqEHQZzZSlXfqlMz/c253+875zvfOefeHuH8L6u83P+AwH0lJZ4pbrenEiCVSnYmEsndGl4NhSKvAJkLmPPcV0VFxZjKivKPv77wXr274WN9uvm0PnHihD5y9IhuPNioN216Vy+Yf6eurAj8b2Vl5aXnM6d8loLf7w9apvHhyy//29jZ9fW0t7fpdWtWN7Wdao4qpaiqDpbdXF9fV1paKpu3bGbxk08eSWXU9ZFIpOPirC33v7xs+TIdiUT0Pz239NjeaTOTHXXjdb4cuP6W5DOLFx/7aNdH+oknfqQryv0vXZTFfr8/GKyqaN7XeMhc//ba6NSfPFXqS6fESJ29jdGAX69+9KHY9OnTyxbec08mHInWhsPhzsHmNs4FNgxdf+NNN5sAh3/7n40dCxeKedUsOr6x8CzdsnBEQu9sPABwzTWTTMNQ9eec+1x/FDEuGTduHABXtreOKutJYyiFqq4tqD+5O3wJQF1dHSij7nODtdZuj9cLgMfGOpcuQInSFoDldqNFez43eCivIrgILoKL4CK4CC6Ci+AiuAgugovgIrgILoKL4CK4CC6Ci+A/B7B5vor6Mz4PNnbRYAAtoCQLUMMFVobuBWOALWdjVIGxiwbbZC3WkrXWLqAzJBZrR5T0LWTgdSHfdF1YcIlG57t8oM5nfov1OcCKPmDW1Rfi2IsA5yI5F9WFXF0o0i8arARwggsBu4BbhwaM6g0ujXY+9b+GLqrzLR5E5wsH2ziB5QRXoW8lCy3mosH553iwlDlEe9znai2DpMyhAJ+PxUNTJMhZm51+WM9xvsWFXD2kx0nl9rjQ4oYC3C+4BoEMnasl39Vn6wxRdcqbXApXpwupWBcEVgLKGLw6DU1w5bkaCjcChcYuHozuLYtqEFfroXC1TZ67GcbjlEuZWjSIHr6ozjZ7/y/VSWOLdgJIF9zjQl3JFwDOXn1lsYDOULm6X+YaROcLB6s8+LC2tzqvoc+Wx0L2nT/6wlIm5y6LQ9bs5TLXsO5x7jG192lxuJq9bCOg0aIRGcYEkt9lCsPp6lxlMsBlFE4ghcYuGoxznHKFYNjKYq7Zy5XFYW32lMtCBGzbLlwWLwB83m/2NNC44R0iFaP503+8jO1UqHz5wiwW0aNzvysgdPJTQr/7dFD9fHD+vecN9vl8NaYpv546ZeqCBx98CMhGbPXEqZRfcTWmyySTjuO2TMora/B4Sji+832OnWoGYMGCBez88IMfHD50eExG6Yd6enraBjJcAwf8fv+Vbsv1Pz9f/NT1y1esQCnNPz6zeGuy6WBN+MRRrwp1YMR6MOIJMqEuOj49xNFd2zh5aD9SVpr44PCJXVOmXXvpHfPm4fP7rtz98Z/usSz3+lQq1e/fnvuFSHl5+VjTNLb96lfPj6yv/0t2bN/eufJnj+37Uql1c/1Xv8WM279CaZn/rJcBGoj1hNm+7k22rF5JcyK1edp3Hps0bfq0yj0Ne/jFL55pVopZ3d3dx88C19bWlqVS8Z2Lf/7U1XNvu51Vb72x7/irz9fUBEcEv/03PyFYPRJDgZHt9XpvzG8QlAFnWppY+S9LaOnsaPPOWdhxx7z5V320cydLl/7yE2+pb+bp06dj/VxtWbJ03h13zr/r7rtZu2bNwVP/9cKYMiHwtW8+QNAbwOiOIN09SCiChCKQL+EIKhxBhcN4EGpGjuJww66yxNH9gePac+zGm26sikQiNY379/kSydT63uCqrCybXB6oeuS+RYvYv29f/OTKFz1+dIlXXFQrCznRjNhkRfdJzmIMEAExsqbUmh68holWGXf43deMg6NHJ+5btKjkgw8//IFh8lJnZ88nBoBWxpPf+e53DYC1Ly5bVSb6Mo8WSrQgx5uRY6cHSDMcz0q/vx/PSTNeJXi04EOPfe93L70JcP/99xu2bfwUwPT5fNU+n++rM2fO5P3332+uS3V9y9KCG8FSmtjRo3iN0uz+qqylemDnLhpDQDsFJGrHMG2F2xAyGi5Nhr65Y8f21unTZ9T4yrz3KqVHGC4X91x33XUmwN7N775nApbuk90nD5BpbUbaWqG9Dd3eju5o6y/t7dDehrS1kmltYffJ/ViA25nDBcbeLZs2AUyaNNkyDL5minDL7Nm3opSiNtQ0yUQwESydlXg6xc70Sf5CewliYSD9TqHu/anpIMUnJIiLjSVCGjAFTA21odNTlFLMunEWO7bvuMUUjKkTrriCvXv3RDyiJxpacGVXSc56W2uO6DhtKkmFFsocHchmtKhoukURNrJPG5YDdAEuDYaAV/TVjY0HesaNG+8Tw5hmuC1zFEBLS0urkQ3QPtFgILgQTC0IkAZSgEJQCClnTBwdF4KBOPf2iQBnzrS2GYaBZblGmWK4/ADxWCzqoS85iDOZDFiMS2ddV5Kz2EkGhgwECYLOzqOzxy0W7YkAiBgBw7btFIC3tMw/2JsrnS9OI5B2pPdt0AC9gdVZZxkBANu2k0Ymk2kDCI6oqsw1c/nNu8rVW8l+2ZFCkxRNzMhKUjQpNBlnv23nXfbAeTRQHayudMBtBlod6OrqZNz4CeVprcKqd4KsZBxgGk1KNEmBmGiijsScsZRo0s4CMnn3284CMqJCY8aOCXR2dqK0PmBokQ3r1q7D7/dLq7tyY8axMCOatDNZFqhJiCbuWNsLNrJjCUcnt4C0ZOew0WTQnDYr3/X5fLJx4wZE5B1DKVm1a9dHAIyYesPYjEBa+vYwJZAUSAgkHAtjookaWcl9Togm4eim8u5PS9YDNVNmXg7QsLsBreX3RjgcPtzW1rarubmZ+QvumtahXJvzrUzmWRvrZ61yxNnvPKuTA6xvt13bvjxv/tSW5mY6Ozt3hkKhoy4Ar6ek6dChg4vm3nY7oZJAJnG4oUIQESdD5Ud0v30XSBlZC1OGdjyTA/darwK3LcxcPm585ZJnl9ATinwvnkweNgC6wuF1x44d27R3714WfOWucZGrb3g7kee+eJ6LewPLcXU0bzwuuf2G3P3NoyevnzP3tsv3NOylqenkHzvD4fWQ197aikeW/nJJd1dnJ4//9On57V+a8Hoib7K4kQeUAWL0D7RcsJ2oqHv9wUcfu7Orq5MVK5Z3KS0P53j96lsgEPjyiKqKtW/891uu2tpalvzDMxsTW96s9yhMC8HUOCkxm07JO/fZk5A9dkmDTOSqWe/99fcfmRPtifHY3z6a6Q5F7gyFQhsKggFGjKh4wFviffG11153T59xHVu3bg3968/+7g9V3ae+0Zv0kX49l3ISjA2ccpe/NXvR9+uvnX5tRdOpJv7+xz9OxnpiD3d0d/97PqcXrLWeBcwGLnv11d96n3j88QVPPf108KHvPUwmk+HttWu71q96Y0dozzajJBUfXyqMA4gpfShmeY54JkzX19/6VzfMmDmjMpPOsOqtVbzyym9alz23fM23Fy1KACeAP4rIBwCitb4MeAQY5SxEt7a2qIaGBn70wx+OTKXTc5Y+t8w1d85cdN5KtdbYSqGVImPbJOIxotEo6/+wniXPPmsH/L4Ny5etaJk46Rqprq7JPTgooBn4Z9FaPw9UAHR1dSnbTsuZMy1GMpnItLZ2GFu3bq5d/fvVc0ZUjZB7F36d2fW3MmHCFZguF0pr0uk0Bxsb2bL5PV5fuZLuUEjfdffdG2+66ebW6mCVLvP5qa4OAoYEg8Gcg7tNIAIEADHdJnbcxmNZ6UQ05nK7TT1x4sRYRVV1/FTTqdLVa9bywgsvEImESKfSAFiWhT9QzqhL6rh25g3UjbokPnJkTaKkxFRaa8NtGbaIy+Up8eS2VgEx0VpXO66+HKfdbW9vV93d7RKNJl3xeNQOd4d1Mp0i3B3yRCKRsmgiYSVTaa9orS23lfR5vany8vKYLxCIeyxLKqoqtddbKh6PSVVVtQ4Gg5IHPQI8nx9ck4CbgSuBarJnvARsiUai4XBPmGQyqbWGRCxh2VrZAKYYLtNjZUyXSxsuU6oqyg1fwO91nhUSzvQdwB5gm4h8UvA4OYsoByYDY4EaoBLwAN7sYiDvZ4LsqUo60uNIK3AY2CMioYGM/wPREY0iGUY58wAAAABJRU5ErkJggg==", - "public": true - }, { "link": "/api/images/system/openstreet_map_system_widget_image.png", "title": "\"OpenStreet Map\" system widget image", From f290246aade5d0d4302179373dd74b0e2442e0d2 Mon Sep 17 00:00:00 2001 From: Igor Kulikov Date: Fri, 31 Jan 2025 16:54:33 +0200 Subject: [PATCH 045/345] UI: Maps - implement tooltip tag actions. --- .../json/system/widget_types/image_map.json | 2 +- .../data/json/system/widget_types/map.json | 2 +- .../lib/maps/data-layer/map-data-layer.ts | 22 ++- .../home/components/widget/lib/maps/map.ts | 36 ---- .../widget/lib/maps/models/map.models.ts | 3 +- ...idget-action-settings-panel.component.html | 6 +- .../widget-action-settings-panel.component.ts | 36 ++-- .../widget-action-settings.component.ts | 1 - .../action/widget-action.component.html | 17 ++ .../common/action/widget-action.component.ts | 39 +++- ...data-layer-pattern-settings.component.html | 4 + .../data-layer-pattern-settings.component.ts | 5 + .../map/map-data-layer-dialog.component.html | 4 +- .../map/map-data-layer-dialog.component.ts | 10 +- .../map-tooltip-tag-actions.component.html | 67 +++++++ .../map-tooltip-tag-actions.component.scss | 49 +++++ .../map/map-tooltip-tag-actions.component.ts | 176 ++++++++++++++++++ .../common/widget-settings-common.module.ts | 4 + ui-ngx/src/app/shared/models/widget.models.ts | 1 + .../assets/locale/locale.constant-en_US.json | 4 + 20 files changed, 407 insertions(+), 81 deletions(-) create mode 100644 ui-ngx/src/app/modules/home/components/widget/lib/settings/common/map/map-tooltip-tag-actions.component.html create mode 100644 ui-ngx/src/app/modules/home/components/widget/lib/settings/common/map/map-tooltip-tag-actions.component.scss create mode 100644 ui-ngx/src/app/modules/home/components/widget/lib/settings/common/map/map-tooltip-tag-actions.component.ts diff --git a/application/src/main/data/json/system/widget_types/image_map.json b/application/src/main/data/json/system/widget_types/image_map.json index b7e8644662..0c10599757 100644 --- a/application/src/main/data/json/system/widget_types/image_map.json +++ b/application/src/main/data/json/system/widget_types/image_map.json @@ -11,7 +11,7 @@ "resources": [], "templateHtml": "\n", "templateCss": "", - "controllerScript": "self.onInit = function() {\n self.ctx.$scope.mapWidget.onInit();\n};\n\nself.typeParameters = function() {\n return {\n hideDataTab: true,\n hideDataSettings: true,\n previewWidth: '80%',\n embedTitlePanel: true,\n datasourcesOptional: true\n };\n}\n\nself.actionSources = function() {\n return {\n tooltipAction: {\n name: 'widget-action.tooltip-tag-action',\n multiple: true\n }\n };\n}\n", + "controllerScript": "self.onInit = function() {\n self.ctx.$scope.mapWidget.onInit();\n};\n\nself.typeParameters = function() {\n return {\n hideDataTab: true,\n hideDataSettings: true,\n previewWidth: '80%',\n embedTitlePanel: true,\n datasourcesOptional: true\n };\n}\n", "settingsForm": [], "dataKeySettingsForm": [], "settingsDirective": "tb-map-widget-settings", diff --git a/application/src/main/data/json/system/widget_types/map.json b/application/src/main/data/json/system/widget_types/map.json index 7ec5b35320..fb19914741 100644 --- a/application/src/main/data/json/system/widget_types/map.json +++ b/application/src/main/data/json/system/widget_types/map.json @@ -11,7 +11,7 @@ "resources": [], "templateHtml": "\n", "templateCss": "", - "controllerScript": "self.onInit = function() {\n self.ctx.$scope.mapWidget.onInit();\n};\n\nself.typeParameters = function() {\n return {\n hideDataTab: true,\n hideDataSettings: true,\n previewWidth: '80%',\n embedTitlePanel: true,\n datasourcesOptional: true\n };\n}\n\nself.actionSources = function() {\n return {\n tooltipAction: {\n name: 'widget-action.tooltip-tag-action',\n multiple: true\n }\n };\n}\n", + "controllerScript": "self.onInit = function() {\n self.ctx.$scope.mapWidget.onInit();\n};\n\nself.typeParameters = function() {\n return {\n hideDataTab: true,\n hideDataSettings: true,\n previewWidth: '80%',\n embedTitlePanel: true,\n datasourcesOptional: true\n };\n}\n", "settingsForm": [], "dataKeySettingsForm": [], "settingsDirective": "tb-map-widget-settings", diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/maps/data-layer/map-data-layer.ts b/ui-ngx/src/app/modules/home/components/widget/lib/maps/data-layer/map-data-layer.ts index 861c02a560..ebee9f8c59 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/maps/data-layer/map-data-layer.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/maps/data-layer/map-data-layer.ts @@ -60,7 +60,7 @@ export abstract class TbDataLayerItem; - private createTooltip(datasource: TbMapDatasource) { + private createTooltip() { this.tooltip = L.popup(); this.layer.bindPopup(this.tooltip, {autoClose: this.settings.tooltip.autoclose, closeOnClick: false}); this.layer.off('click'); @@ -279,22 +279,30 @@ export abstract class TbDataLayerItem { - this.bindTooltipActions(datasource); + this.bindTooltipActions(); (this.layer as any)._popup._closeButton.addEventListener('click', (event: Event) => { event.preventDefault(); }); }); } - private bindTooltipActions(datasource: TbMapDatasource) { + private bindTooltipActions() { const actions = this.tooltip.getElement().getElementsByClassName('tb-custom-action'); Array.from(actions).forEach( (element: HTMLElement) => { const actionName = element.getAttribute('data-action-name'); - this.dataLayer.getMap().tooltipElementClick(element, actionName, datasource); + if (this.settings.tooltip?.tagActions) { + const action = this.settings.tooltip.tagActions.find(action => action.name === actionName); + if (action) { + element.onclick = ($event) => + { + this.dataLayer.getMap().dataItemClick($event, action, this.data.$datasource); + return false; + }; + } + } }); } - } export enum MapDataLayerType { 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 58f042f475..1e7165a85f 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 @@ -18,7 +18,6 @@ import { additionalMapDataSourcesToDatasources, BaseMapSettings, DataKeyValuePair, - MapActionHandler, MapType, mergeMapDatasources, mergeUnplacedDataItemsArrays, @@ -99,8 +98,6 @@ export abstract class TbMap { private readonly mapResize$: ResizeObserver; - private readonly tooltipActions: { [name: string]: MapActionHandler }; - private tooltipInstances: TooltipInstancesData[] = []; private currentPopover: TbPopoverComponent; @@ -115,8 +112,6 @@ export abstract class TbMap { protected containerElement: HTMLElement) { this.settings = mergeDeepIgnoreArray({} as S, this.defaultSettings(), this.inputSettings as S); - this.tooltipActions = this.loadActions('tooltipAction'); - $(containerElement).empty(); $(containerElement).addClass('tb-map-layout'); const mapElement = $('
'); @@ -595,27 +590,6 @@ export abstract class TbMap { } } - private loadActions(name: string): { [name: string]: MapActionHandler } { - const descriptors = this.ctx.actionsApi.getActionDescriptors(name); - const actions: { [name: string]: MapActionHandler } = {}; - descriptors.forEach(descriptor => { - actions[descriptor.name] = ($event: Event, datasource: TbMapDatasource) => this.onCustomAction(descriptor, $event, datasource); - }); - return actions; - } - - private onCustomAction(descriptor: WidgetActionDescriptor, $event: Event, entityInfo: TbMapDatasource) { - if ($event) { - $event.preventDefault(); - $event.stopPropagation(); - } - const { entityId, entityName, entityLabel, entityType } = entityInfo; - this.ctx.actionsApi.handleWidgetAction($event, descriptor, { - entityType, - id: entityId - }, entityName, null, entityLabel); - } - private updateAddButtonsStates() { if (this.currentAddButton) { if (this.addMarkerButton && this.addMarkerButton !== this.currentAddButton) { @@ -705,16 +679,6 @@ export abstract class TbMap { }, entityName, null, entityLabel); } - public tooltipElementClick(element: HTMLElement, action: string, datasource: TbMapDatasource): void { - if (element && this.tooltipActions[action]) { - element.onclick = ($event) => - { - this.tooltipActions[action]($event, datasource); - return false; - }; - } - } - public selectItem(item: TbDataLayerItem, cancel = false, force = false): boolean { if (this.isPlacingItem) { return false; diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/maps/models/map.models.ts b/ui-ngx/src/app/modules/home/components/widget/lib/maps/models/map.models.ts index 76b8566692..947d394c2d 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/maps/models/map.models.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/maps/models/map.models.ts @@ -95,6 +95,7 @@ export interface DataLayerTooltipSettings extends DataLayerPatternSettings { autoclose: boolean; offsetX: number; offsetY: number; + tagActions?: WidgetAction[]; } export enum DataLayerEditAction { @@ -879,8 +880,6 @@ export type MapSetting = GeoMapSettings & ImageMapSettings; export const defaultMapSettings: MapSetting = defaultGeoMapSettings; -export type MapActionHandler = ($event: Event, datasource: TbMapDatasource) => void; - export interface MarkerImageInfo { url: string; size: number; diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/action/widget-action-settings-panel.component.html b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/action/widget-action-settings-panel.component.html index 3fffd3f424..7a7900214e 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/action/widget-action-settings-panel.component.html +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/action/widget-action-settings-panel.component.html @@ -21,7 +21,9 @@ + [widgetType]="widgetType" + [withName]="withName" + [actionNames]="actionNames">
@@ -36,7 +38,7 @@ type="button" (click)="applyWidgetAction()" [disabled]="widgetActionFormGroup.invalid || !widgetActionFormGroup.dirty"> - {{ 'action.apply' | translate }} + {{ applyTitle }}
") + .attr('class', 'leaflet-bar'); + return this.buttonContainer[0]; + } + + addTo(map: L.Map): this { + return super.addTo(map); + } + } class BottomToolbarControl extends L.Control { @@ -372,7 +398,7 @@ class BottomToolbarControl extends L.Control { buttons.forEach(buttonOption => { const button = new ToolbarButton(buttonOption); this.toolbarButtons.push(button); - button.addToToolbar(this); + button.getButtonElement().appendTo(this.container); }); const closeButton = $("") @@ -418,6 +444,10 @@ const groups = (options: TB.GroupsControlOptions): GroupsControl => { return new GroupsControl(options); } +const toolbar = (options: L.ControlOptions): ToolbarControl => { + return new ToolbarControl(options); +} + const bottomToolbar = (options: TB.BottomToolbarControlOptions): BottomToolbarControl => { return new BottomToolbarControl(options); } @@ -478,11 +508,13 @@ L.TB = L.TB || { LayersControl, GroupsControl, ToolbarButton, + ToolbarControl, BottomToolbarControl, sidebar, sidebarPane, layers, groups, + toolbar, bottomToolbar, TileLayer: { ChinaProvider diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/maps/map.scss b/ui-ngx/src/app/modules/home/components/widget/lib/maps/map.scss index 2fc1baede6..4dcb17668d 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/maps/map.scss +++ b/ui-ngx/src/app/modules/home/components/widget/lib/maps/map.scss @@ -15,10 +15,6 @@ */ @import '../../../../../../../scss/constants'; -//$map-element-hover-color: #307FE5; -$map-element-hover-color: rgba(0,0,0,0.56); -$map-element-selected-color: #307FE5; - .tb-map-layout { display: flex; width: 100%; @@ -36,19 +32,11 @@ $map-element-selected-color: #307FE5; flex: 1; &.leaflet-touch { .leaflet-bar { - border: none; + border: 1px solid rgba(0,0,0,0.38); border-radius: 15px; - box-shadow: 4px 4px 4px 0 rgba(0, 0, 0, 0.15); + box-shadow: 0 4px 6px 0 rgba(0, 0, 0, 0.32); background: #fff; position: relative; - &:before { - content: ""; - position: absolute; - inset: 0; - border-radius: 15px; - background: $tb-primary-color; // primary color - opacity: 0.05; - } a { color: rgba(0, 0, 0, 0.54); border-bottom: none; @@ -111,10 +99,10 @@ $map-element-selected-color: #307FE5; mask-repeat: no-repeat; mask-position: center; &.tb-layers { - mask-image: url('data:image/svg+xml,'); + mask-image: url('data:image/svg+xml,'); } &.tb-groups { - mask-image: url('data:image/svg+xml,'); + mask-image: url('data:image/svg+xml,'); } &.tb-remove { mask-image: url('data:image/svg+xml,'); @@ -125,6 +113,18 @@ $map-element-selected-color: #307FE5; &.tb-rotate { mask-image: url('data:image/svg+xml,'); } + &.tb-place-marker { + mask-image: url('data:image/svg+xml,'); + } + &.tb-draw-rectangle { + mask-image: url('data:image/svg+xml,'); + } + &.tb-draw-polygon { + mask-image: url('data:image/svg+xml,'); + } + &.tb-draw-circle { + mask-image: url('data:image/svg+xml,'); + } &.tb-close { background: #D12730; mask-image: url('data:image/svg+xml,'); @@ -156,15 +156,19 @@ $map-element-selected-color: #307FE5; &.tb-hoverable:not(.tb-selected) { &:hover { svg { - //filter: drop-shadow( 0 0 4px $map-element-hover-color); - filter: brightness(0.8) drop-shadow( 0 0 4px $map-element-hover-color); + filter: brightness(1.3) + drop-shadow( 0 0 4px rgba(0,0,0,0.56)) + drop-shadow( 0 0 4px rgba(255,255,255,0.56)); } } } &.tb-selected { svg { - filter: brightness(0.8); - //animation: tb-selected-animation 0.5s linear 0s infinite alternate; + filter: brightness(1.3) + drop-shadow( 0 0 2px rgba(0,0,0,.6)) + drop-shadow( 0 0 4px rgba(255,255,255,.7)) + drop-shadow( 0 0 6px rgba(0,0,0,.8)) + drop-shadow( 0 0 8px rgba(255,255,255,.9)); } } } @@ -175,12 +179,28 @@ $map-element-selected-color: #307FE5; } &.tb-hoverable:not(.tb-selected) { &:hover { - filter: brightness(0.8) drop-shadow( 0 0 4px $map-element-hover-color); + filter: brightness(1.3) + drop-shadow( 0 0 4px rgba(0,0,0,0.56)) + drop-shadow( 0 0 4px rgba(255,255,255,0.56)); } } + } + img.leaflet-marker-icon { &.tb-selected { - filter: brightness(0.8); - //animation: tb-selected-animation 0.5s linear 0s infinite alternate; + filter: brightness(1.3) + drop-shadow( 0 0 2px rgba(0,0,0,.6)) + drop-shadow( 0 0 4px rgba(255,255,255,.7)) + drop-shadow( 0 0 6px rgba(0,0,0,.8)) + drop-shadow( 0 0 8px rgba(255,255,255,.9)); + } + } + path { + &.tb-selected:not(.tb-cut-mode) { + filter: brightness(1.3) + drop-shadow( 0 0 4px rgba(0,0,0,.4)) + drop-shadow( 0 0 4px rgba(255,255,255,.3)) + drop-shadow( 0 0 8px rgba(0,0,0,.6)) + drop-shadow( 0 0 8px rgba(255,255,255,.5)); } } .tb-cluster-marker-container { @@ -322,14 +342,3 @@ $map-element-selected-color: #307FE5; } } } - -@keyframes tb-selected-animation { - 0% { - //filter: drop-shadow( 0 0 2px $map-element-selected-color); - filter: brightness(1); - } - 100% { - //filter: drop-shadow( 0 0 4px $map-element-selected-color) drop-shadow( 0 0 4px $map-element-selected-color); - filter: brightness(0.8); - } -} 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 f391754674..346387ee6b 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 @@ -23,7 +23,9 @@ import { mergeMapDatasources, parseCenterPosition, TbCircleData, - TbMapDatasource, TbPolygonCoordinates, TbPolygonRawCoordinates + TbMapDatasource, + TbPolygonCoordinates, + TbPolygonRawCoordinates } from '@home/components/widget/lib/maps/models/map.models'; import { WidgetContext } from '@home/models/widget-component.models'; import { formattedDataFormDatasourceData, isDefinedAndNotNull, mergeDeepIgnoreArray } from '@core/utils'; @@ -74,6 +76,15 @@ export abstract class TbMap { protected editToolbar: L.TB.BottomToolbarControl; + protected addMarkerButton: L.TB.ToolbarButton; + protected addRectangleButton: L.TB.ToolbarButton; + protected addPolygonButton: L.TB.ToolbarButton; + protected addCircleButton: L.TB.ToolbarButton; + + protected addMarkerDataLayers: TbMapDataLayer[]; + protected addPolygonDataLayers: TbMapDataLayer[]; + protected addCircleDataLayers: TbMapDataLayer[]; + private readonly mapResize$: ResizeObserver; private readonly tooltipActions: { [name: string]: MapActionHandler }; @@ -253,6 +264,51 @@ export abstract class TbMap { this.map.on('click', () => { this.deselectItem(); }); + + const addSupportedDataLayers = this.dataLayers.filter(dl => dl.isAddEnabled()); + + if (addSupportedDataLayers.length) { + const drawToolbar = L.TB.toolbar({ + position: this.settings.controlsPosition + }).addTo(this.map); + this.addMarkerDataLayers = addSupportedDataLayers.filter(dl => dl.dataLayerType() === MapDataLayerType.marker); + if (this.addMarkerDataLayers.length) { + this.addMarkerButton = drawToolbar.toolbarButton({ + id: 'addMarker', + title: this.ctx.translate.instant('widgets.maps.data-layer.marker.place-marker'), + iconClass: 'tb-place-marker', + click: (e, button) => {} + }); + this.addMarkerButton.setDisabled(true); + } + this.addPolygonDataLayers = addSupportedDataLayers.filter(dl => dl.dataLayerType() === MapDataLayerType.polygon); + if (this.addPolygonDataLayers.length) { + this.addRectangleButton = drawToolbar.toolbarButton({ + id: 'addRectangle', + title: this.ctx.translate.instant('widgets.maps.data-layer.polygon.draw-rectangle'), + iconClass: 'tb-draw-rectangle', + click: (e, button) => {} + }); + this.addRectangleButton.setDisabled(true); + this.addPolygonButton = drawToolbar.toolbarButton({ + id: 'addPolygon', + title: this.ctx.translate.instant('widgets.maps.data-layer.polygon.draw-polygon'), + iconClass: 'tb-draw-polygon', + click: (e, button) => {} + }); + this.addPolygonButton.setDisabled(true); + } + this.addCircleDataLayers = addSupportedDataLayers.filter(dl => dl.dataLayerType() === MapDataLayerType.circle); + if (this.addCircleDataLayers.length) { + this.addCircleButton = drawToolbar.toolbarButton({ + id: 'addCircle', + title: this.ctx.translate.instant('widgets.maps.data-layer.circle.draw-circle'), + iconClass: 'tb-draw-circle', + click: (e, button) => {} + }); + this.addCircleButton.setDisabled(true); + } + } } private createdControlButtonTooltip(root: HTMLElement, side: TooltipPositioningSide) { @@ -320,6 +376,7 @@ export abstract class TbMap { undefined, undefined, el => el.datasource.entityId + el.datasource.mapDataIds[0]); this.dataLayers.forEach(dl => dl.updateData(this.dsData)); this.updateBounds(); + this.updateAddButtonsStates(); } private resize() { @@ -365,6 +422,21 @@ export abstract class TbMap { }, entityName, null, entityLabel); } + private updateAddButtonsStates() { + if (this.addMarkerButton) { + this.addMarkerButton.setDisabled(!this.addMarkerDataLayers.some(dl => dl.hasUnplacedItems())); + } + if (this.addRectangleButton) { + this.addRectangleButton.setDisabled(!this.addPolygonDataLayers.some(dl => dl.hasUnplacedItems())); + } + if (this.addPolygonButton) { + this.addPolygonButton.setDisabled(!this.addPolygonDataLayers.some(dl => dl.hasUnplacedItems())); + } + if (this.addCircleButton) { + this.addCircleButton.setDisabled(!this.addCircleDataLayers.some(dl => dl.hasUnplacedItems())); + } + } + protected abstract defaultSettings(): S; protected abstract createMap(): Observable; @@ -454,10 +526,10 @@ export abstract class TbMap { } } - public selectItem(item: TbDataLayerItem, cancel = false): boolean { + public selectItem(item: TbDataLayerItem, cancel = false, force = false): boolean { let deselected = true; if (this.selectedDataItem) { - deselected = this.selectedDataItem.deselect(cancel); + deselected = this.selectedDataItem.deselect(cancel, force); if (deselected) { this.selectedDataItem = null; this.editToolbar.close(); @@ -475,8 +547,8 @@ export abstract class TbMap { return deselected; } - public deselectItem(cancel = false): boolean { - return this.selectItem(null, cancel); + public deselectItem(cancel = false, force = false): boolean { + return this.selectItem(null, cancel, force); } public getSelectedDataItem(): TbDataLayerItem { 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 33d8797a2d..260999408a 100644 --- a/ui-ngx/src/assets/locale/locale.constant-en_US.json +++ b/ui-ngx/src/assets/locale/locale.constant-en_US.json @@ -7717,7 +7717,8 @@ "marker-color-function": "Marker color function" }, "edit": "Edit marker", - "remove-marker-for": "Remove marker for '{{entityName}}'" + "remove-marker-for": "Remove marker for '{{entityName}}'", + "place-marker": "Place marker" }, "polygon": { "polygon-key": "Polygon key", @@ -7732,7 +7733,9 @@ "rotate": "Rotate polygon", "firstVertex-cut": "Click to place first point", "continueLine-cut": "Click to continue drawing", - "finishPoly-cut": "Click first marker to finish and save" + "finishPoly-cut": "Click first marker to finish and save", + "draw-rectangle": "Draw rectangle", + "draw-polygon": "Draw polygon" }, "circle": { "circle-key": "Circle key", @@ -7742,7 +7745,8 @@ "circle-configuration": "Circle configuration", "remove-circle": "Remove circle", "edit": "Edit circle", - "remove-circle-for": "Remove circle for '{{entityName}}'" + "remove-circle-for": "Remove circle for '{{entityName}}'", + "draw-circle": "Draw circle" } }, "select-entity": "Select entity", diff --git a/ui-ngx/src/typings/leaflet-extend-tb.d.ts b/ui-ngx/src/typings/leaflet-extend-tb.d.ts index 5ae83c1679..b75743c139 100644 --- a/ui-ngx/src/typings/leaflet-extend-tb.d.ts +++ b/ui-ngx/src/typings/leaflet-extend-tb.d.ts @@ -104,6 +104,11 @@ declare module 'leaflet' { isDisabled(): boolean; } + class ToolbarControl extends Control { + constructor(options: ControlOptions); + toolbarButton(options: ToolbarButtonOptions): ToolbarButton; + } + interface BottomToolbarControlOptions extends ControlOptions { mapElement: JQuery; closeTitle: string; @@ -126,6 +131,8 @@ declare module 'leaflet' { function groups(options: GroupsControlOptions): GroupsControl; + function toolbar(options: ControlOptions): ToolbarControl; + function bottomToolbar(options: BottomToolbarControlOptions): BottomToolbarControl; namespace TileLayer { From 0940e373bbbb04d787ae37cb59c196e33482799b Mon Sep 17 00:00:00 2001 From: Igor Kulikov Date: Wed, 29 Jan 2025 21:06:11 +0200 Subject: [PATCH 039/345] UI: Map items add/draw mode. --- .../lib/maps/data-layer/circles-data-layer.ts | 8 +- .../lib/maps/data-layer/map-data-layer.ts | 45 +++- .../lib/maps/data-layer/markers-data-layer.ts | 6 +- .../maps/data-layer/polygons-data-layer.ts | 20 +- .../widget/lib/maps/leaflet/leaflet-tb.ts | 38 ++-- .../home/components/widget/lib/maps/map.scss | 117 +++++----- .../home/components/widget/lib/maps/map.ts | 200 ++++++++++++++++-- .../widget/lib/maps/models/map.models.ts | 11 + .../select-map-entity-panel.component.html | 44 ++++ .../select-map-entity-panel.component.scss | 39 ++++ .../select-map-entity-panel.component.ts | 69 ++++++ .../widget/widget-components.module.ts | 4 + .../components/widget/widget.component.ts | 2 + .../home/models/widget-component.models.ts | 12 +- .../assets/locale/locale.constant-en_US.json | 7 +- ui-ngx/src/typings/leaflet-extend-tb.d.ts | 3 +- 16 files changed, 531 insertions(+), 94 deletions(-) create mode 100644 ui-ngx/src/app/modules/home/components/widget/lib/maps/panels/select-map-entity-panel.component.html create mode 100644 ui-ngx/src/app/modules/home/components/widget/lib/maps/panels/select-map-entity-panel.component.scss create mode 100644 ui-ngx/src/app/modules/home/components/widget/lib/maps/panels/select-map-entity-panel.component.ts diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/maps/data-layer/circles-data-layer.ts b/ui-ngx/src/app/modules/home/components/widget/lib/maps/data-layer/circles-data-layer.ts index 7f877dbb00..3f5368d95c 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/maps/data-layer/circles-data-layer.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/maps/data-layer/circles-data-layer.ts @@ -45,7 +45,7 @@ class TbCircleDataLayerItem extends TbDataLayerItem this.editing = true); this.circle.on('pm:markerdragend', () => this.editing = false); - this.circle.on('pm:edit', (e) => this.saveCircleCoordinates()); + this.circle.on('pm:edit', () => this.saveCircleCoordinates()); this.circle.pm.enable(); } return []; @@ -142,6 +142,10 @@ class TbCircleDataLayerItem extends TbDataLayerItem { @@ -177,12 +179,13 @@ export abstract class TbDataLayerItem, dsData: FormattedData[]): void { @@ -365,7 +368,12 @@ export class DataLayerColorProcessor { } -export abstract class TbMapDataLayer, L extends L.Layer = L.Layer> implements L.TB.DataLayer { +export interface UnplacedMapDataItem { + entity: FormattedData; + dataLayer: TbMapDataLayer; +} + +export abstract class TbMapDataLayer = any, L extends L.Layer = L.Layer> implements L.TB.DataLayer { protected settings: S; @@ -386,13 +394,14 @@ export abstract class TbMapDataLayer[] = []; + private unplacedItems: UnplacedMapDataItem[] = []; public dataLayerLabelProcessor: DataLayerPatternProcessor; public dataLayerTooltipProcessor: DataLayerPatternProcessor; @@ -412,6 +421,7 @@ export abstract class TbMapDataLayer { @@ -559,6 +577,25 @@ export abstract class TbMapDataLayer { + this.polygon.on('pm:dragend', () => { this.savePolygonCoordinates(); this.editing = false; }); @@ -200,10 +200,14 @@ class TbPolygonDataLayerItem extends TbDataLayerItem this.editing = true); this.polygon.on('pm:markerdragend', () => this.editing = false); - this.polygon.on('pm:edit', (e) => this.savePolygonCoordinates()); + this.polygon.on('pm:edit', () => this.savePolygonCoordinates()); this.polygon.pm.enable(); const map = this.dataLayer.getMap(); map.getEditToolbar().getButton('remove')?.setDisabled(false); @@ -232,7 +236,7 @@ class TbPolygonDataLayerItem extends TbDataLayerItem { + this.polygon.on('pm:rotateend', () => { this.savePolygonCoordinates(); }); this.polygon.pm.enableRotate(); rotateButton?.setActive(true); - this.polygon.on('pm:rotatedisable', (e) => { + this.polygon.on('pm:rotatedisable', () => { this.disablePolygonRotateMode(rotateButton); this.enablePolygonEditMode(); }); @@ -330,7 +334,7 @@ class TbPolygonDataLayerItem extends TbDataLayerItem { constructor(options: TB.ToolbarButtonOptions) { super(options); this.id = options.id; + + const buttonText = this.options.showText ? this.options.title : null; this.button = $("") .attr('class', 'tb-control-button') .attr('href', '#') .attr('role', 'button') - .attr('title', this.options.title) - .html('
'); + .html('
' + (buttonText ? `
${buttonText}
` : '')); + if (this.options.showText) { + L.DomUtil.addClass(this.button[0], 'tb-control-text-button'); + } else { + this.button.attr('title', this.options.title); + } this.button.on('click', (e) => { e.stopPropagation(); @@ -391,7 +397,7 @@ class BottomToolbarControl extends L.Control { return this.toolbarButtons.find(b => b.getId() === id); } - open(buttons: TB.ToolbarButtonOptions[]): void { + open(buttons: TB.ToolbarButtonOptions[], showCloseButton = true): void { this.toolbarButtons.length = 0; @@ -401,19 +407,21 @@ class BottomToolbarControl extends L.Control { button.getButtonElement().appendTo(this.container); }); - const closeButton = $("
") - .attr('class', 'tb-control-button') - .attr('href', '#') - .attr('role', 'button') - .attr('title', this.options.closeTitle) - .html('
'); + if (showCloseButton) { + const closeButton = $("
") + .attr('class', 'tb-control-button') + .attr('href', '#') + .attr('role', 'button') + .attr('title', this.options.closeTitle) + .html('
'); - closeButton.on('click', (e) => { - e.stopPropagation(); - e.preventDefault(); - this.close(); - }); - closeButton.appendTo(this.buttonContainer); + closeButton.on('click', (e) => { + e.stopPropagation(); + e.preventDefault(); + this.close(); + }); + closeButton.appendTo(this.buttonContainer); + } } close(): void { diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/maps/map.scss b/ui-ngx/src/app/modules/home/components/widget/lib/maps/map.scss index 4dcb17668d..da96bcc3e9 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/maps/map.scss +++ b/ui-ngx/src/app/modules/home/components/widget/lib/maps/map.scss @@ -16,6 +16,7 @@ @import '../../../../../../../scss/constants'; .tb-map-layout { + position: relative; display: flex; width: 100%; height: 100%; @@ -29,6 +30,7 @@ flex-direction: row; } .tb-map { + position: relative; flex: 1; &.leaflet-touch { .leaflet-bar { @@ -70,6 +72,73 @@ border-bottom-left-radius: 50%; border-bottom-right-radius: 50%; } + &.tb-control-button { + &.tb-control-text-button { + display: flex; + width: auto; + padding-right: 14px; + padding-left: 2px; + div.tb-control-text { + width: auto; + background: transparent; + font-family: Roboto; + font-size: 12px; + font-style: normal; + font-weight: 500; + } + } + &:not(.leaflet-disabled) { + &.active, &:hover { + &:before { + border-radius: 15px; + } + > div:not(.tb-control-text):not(.tb-close) { + background: $tb-primary-color; // primary color + } + } + > div { + background: rgba(0, 0, 0, 0.54); + } + } + > div { + width: 30px; + height: 30px; + line-height: 30px; + mask-repeat: no-repeat; + mask-position: center; + &.tb-layers { + mask-image: url('data:image/svg+xml,'); + } + &.tb-groups { + mask-image: url('data:image/svg+xml,'); + } + &.tb-remove { + mask-image: url('data:image/svg+xml,'); + } + &.tb-cut { + mask-image: url('data:image/svg+xml,'); + } + &.tb-rotate { + mask-image: url('data:image/svg+xml,'); + } + &.tb-place-marker { + mask-image: url('data:image/svg+xml,'); + } + &.tb-draw-rectangle { + mask-image: url('data:image/svg+xml,'); + } + &.tb-draw-polygon { + mask-image: url('data:image/svg+xml,'); + } + &.tb-draw-circle { + mask-image: url('data:image/svg+xml,'); + } + &.tb-close { + background: #D12730; + mask-image: url('data:image/svg+xml,'); + } + } + } } } .tb-map-bottom-toolbar { @@ -84,54 +153,6 @@ } } } - .leaflet-control { - .tb-control-button { - &.active, &:hover { - > div { - background: $tb-primary-color; // primary color - } - } - > div { - width: 30px; - height: 30px; - background: rgba(0, 0, 0, 0.54); - line-height: 30px; - mask-repeat: no-repeat; - mask-position: center; - &.tb-layers { - mask-image: url('data:image/svg+xml,'); - } - &.tb-groups { - mask-image: url('data:image/svg+xml,'); - } - &.tb-remove { - mask-image: url('data:image/svg+xml,'); - } - &.tb-cut { - mask-image: url('data:image/svg+xml,'); - } - &.tb-rotate { - mask-image: url('data:image/svg+xml,'); - } - &.tb-place-marker { - mask-image: url('data:image/svg+xml,'); - } - &.tb-draw-rectangle { - mask-image: url('data:image/svg+xml,'); - } - &.tb-draw-polygon { - mask-image: url('data:image/svg+xml,'); - } - &.tb-draw-circle { - mask-image: url('data:image/svg+xml,'); - } - &.tb-close { - background: #D12730; - mask-image: url('data:image/svg+xml,'); - } - } - } - } .leaflet-map-pane:not(.leaflet-zoom-anim) { .leaflet-marker-icon { &.tb-hoverable:not(.tb-selected) { 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 346387ee6b..a6b91d9008 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 @@ -20,6 +20,7 @@ import { DataKeyValuePair, MapActionHandler, MapType, + mergeUnplacedDataItemsArrays, mergeMapDatasources, parseCenterPosition, TbCircleData, @@ -32,12 +33,12 @@ import { formattedDataFormDatasourceData, isDefinedAndNotNull, mergeDeepIgnoreAr import { DeepPartial } from '@shared/models/common'; import L from 'leaflet'; import { forkJoin, Observable, of } from 'rxjs'; -import { switchMap } from 'rxjs/operators'; +import { map, switchMap, tap } from 'rxjs/operators'; import '@home/components/widget/lib/maps/leaflet/leaflet-tb'; import { MapDataLayerType, TbDataLayerItem, - TbMapDataLayer, + TbMapDataLayer, UnplacedMapDataItem, } from '@home/components/widget/lib/maps/data-layer/map-data-layer'; import { IWidgetSubscription, WidgetSubscriptionOptions } from '@core/api/widget-api.models'; import { FormattedData, WidgetActionDescriptor, widgetType } from '@shared/models/widget.models'; @@ -51,6 +52,15 @@ import { AttributeData, AttributeScope, DataKeyType, LatestTelemetry } from '@sh import { EntityId } from '@shared/models/id/entity-id'; import ITooltipsterInstance = JQueryTooltipster.ITooltipsterInstance; import TooltipPositioningSide = JQueryTooltipster.TooltipPositioningSide; +import { TbPopoverService } from '@shared/components/popover.service'; +import { + SelectMapEntityPanelComponent +} from '@home/components/widget/lib/maps/panels/select-map-entity-panel.component'; +import { TbPopoverComponent } from '@shared/components/popover.component'; +import { createColorMarkerShapeURI, MarkerShape } from '@home/components/widget/lib/maps/models/marker-shape.models'; +import { MatIconRegistry } from '@angular/material/icon'; +import { DomSanitizer } from '@angular/platform-browser'; +import tinycolor from 'tinycolor2'; type TooltipInstancesData = {root: HTMLElement, instances: ITooltipsterInstance[]}; @@ -61,6 +71,7 @@ export abstract class TbMap { protected defaultCenterPosition: [number, number]; protected ignoreUpdateBounds = false; + protected bounds: L.LatLngBounds; protected southWest = new L.LatLng(-L.Projection.SphericalMercator['MAX_LATITUDE'], -180); protected northEast = new L.LatLng(L.Projection.SphericalMercator['MAX_LATITUDE'], 180); @@ -94,6 +105,9 @@ export abstract class TbMap { private tooltipInstances: TooltipInstancesData[] = []; + private currentPopover: TbPopoverComponent; + private isAddingItem = false; + protected constructor(protected ctx: WidgetContext, protected inputSettings: DeepPartial, protected containerElement: HTMLElement) { @@ -152,6 +166,9 @@ export abstract class TbMap { }); if (this.settings.useDefaultCenterPosition) { this.map.panTo(this.defaultCenterPosition); + this.bounds = this.map.getBounds(); + } else { + this.bounds = new L.LatLngBounds(null, null); } this.setupDataLayers(); this.setupEditMode(); @@ -265,6 +282,11 @@ export abstract class TbMap { this.deselectItem(); }); + if (this.dataLayers.some(dl => dl.isEditable())) { + this.map.pm.setGlobalOptions({ snappable: false } as L.PM.GlobalOptions); + this.map.pm.applyGlobalOptions(); + } + const addSupportedDataLayers = this.dataLayers.filter(dl => dl.isAddEnabled()); if (addSupportedDataLayers.length) { @@ -277,9 +299,25 @@ export abstract class TbMap { id: 'addMarker', title: this.ctx.translate.instant('widgets.maps.data-layer.marker.place-marker'), iconClass: 'tb-place-marker', - click: (e, button) => {} + click: (e, button) => { + this.placeMarker(e, button); + } }); this.addMarkerButton.setDisabled(true); + createColorMarkerShapeURI(this.getCtx().$injector.get(MatIconRegistry), this.getCtx().$injector.get(DomSanitizer), MarkerShape.markerShape1, tinycolor('rgba(255,255,255,0.75)')).subscribe( + ((iconUrl) => { + const icon = L.icon({ + iconUrl, + iconSize: [40, 40], + iconAnchor: [20, 40] + }); + this.map.pm.setGlobalOptions({ + markerStyle: { + icon + } + }); + }) + ); } this.addPolygonDataLayers = addSupportedDataLayers.filter(dl => dl.dataLayerType() === MapDataLayerType.polygon); if (this.addPolygonDataLayers.length) { @@ -287,14 +325,18 @@ export abstract class TbMap { id: 'addRectangle', title: this.ctx.translate.instant('widgets.maps.data-layer.polygon.draw-rectangle'), iconClass: 'tb-draw-rectangle', - click: (e, button) => {} + click: (e, button) => { + this.drawRectangle(e, button); + } }); this.addRectangleButton.setDisabled(true); this.addPolygonButton = drawToolbar.toolbarButton({ id: 'addPolygon', title: this.ctx.translate.instant('widgets.maps.data-layer.polygon.draw-polygon'), iconClass: 'tb-draw-polygon', - click: (e, button) => {} + click: (e, button) => { + this.drawPolygon(e, button); + } }); this.addPolygonButton.setDisabled(true); } @@ -304,13 +346,138 @@ export abstract class TbMap { id: 'addCircle', title: this.ctx.translate.instant('widgets.maps.data-layer.circle.draw-circle'), iconClass: 'tb-draw-circle', - click: (e, button) => {} + click: (e, button) => { + this.drawCircle(e, button); + } }); this.addCircleButton.setDisabled(true); } } } + private placeMarker(e: MouseEvent, button: L.TB.ToolbarButton): void { + this.placeItem(e, button, this.addMarkerDataLayers, + (entity) => { + this.map.pm.enableDraw('Marker'); + const hintText = this.ctx.translate.instant('widgets.maps.data-layer.marker.place-marker-hint', {entityName: entity.entity.entityDisplayName}); + // @ts-ignore + this.map.pm.Draw.Marker._hintMarker.setTooltipContent(hintText); + }, + (entity, layer) => { + (entity.dataLayer as TbMarkersDataLayer).saveMarkerLocation(entity.entity, (layer as L.Marker).getLatLng()); + } + ); + } + + private drawRectangle(e: MouseEvent, button: L.TB.ToolbarButton): void { + this.placeItem(e, button, this.addPolygonDataLayers, + (entity) => { + + }, + (entity, layer) => { + } + ); + } + + private drawPolygon(e: MouseEvent, button: L.TB.ToolbarButton): void { + this.placeItem(e, button, this.addPolygonDataLayers, + (entity) => { + + }, + (entity, layer) => { + } + ); + } + + private drawCircle(e: MouseEvent, button: L.TB.ToolbarButton): void { + this.placeItem(e, button, this.addCircleDataLayers, + (entity) => { + + }, + (entity, layer) => { + } + ); + } + + private placeItem(e: MouseEvent, button: L.TB.ToolbarButton, dataLayers: TbMapDataLayer[], + prepareDrawMode: (entity: UnplacedMapDataItem) => void, + saveLocation: (entity: UnplacedMapDataItem, layer: L.Layer) => void): void { + if (this.isAddingItem) { + return; + } + button.setActive(true); + this.deselectItem(false, true); + this.isAddingItem = true; + const items = mergeUnplacedDataItemsArrays(dataLayers.filter(dl => dl.isEnabled()).map(dl => dl.getUnplacedItems())).sort((entity1, entity2) => { + return entity1.entity.entityDisplayName.localeCompare(entity2.entity.entityDisplayName); + }); + this.selectEntityToPlace(e, items).subscribe((entity) => { + if (entity) { + + const finishAdd = () => { + this.map.off('pm:create'); + this.map.pm.disableDraw(); + this.dataLayers.forEach(dl => dl.enableEditMode()); + button.setActive(false); + this.isAddingItem = false; + this.editToolbar.close(); + }; + + this.map.once('pm:create', (e) => { + saveLocation(entity, e.layer); + // @ts-ignore + e.layer._pmTempLayer = true; + e.layer.remove(); + finishAdd(); + }); + + this.dataLayers.forEach(dl => dl.disableEditMode()); + + prepareDrawMode(entity); + + this.editToolbar.open([ + { + id: 'cancel', + iconClass: 'tb-close', + title: this.ctx.translate.instant('action.cancel'), + showText: true, + click: finishAdd + } + ], false); + } else { + button.setActive(false); + this.isAddingItem = false; + } + }); + } + + private selectEntityToPlace(e: MouseEvent, entities: UnplacedMapDataItem[]): Observable { + if (entities.length === 1) { + return of(entities[0]); + } else { + if (e) { + e.stopPropagation(); + } + const trigger = (e.target || e.srcElement || e.currentTarget) as Element; + const popoverService = this.ctx.$injector.get(TbPopoverService); + const ctx: any = { + entities + }; + const popoverPosition = ['topleft', 'bottomleft'].includes(this.settings.controlsPosition) ? 'rightTop' : 'leftTop'; + const selectMapEntityPanelPopover = popoverService.displayPopover(trigger, this.ctx.renderer, + this.ctx.widgetContentContainer, SelectMapEntityPanelComponent, popoverPosition, true, null, + ctx, + {}, + {}, {}, false); + this.currentPopover = selectMapEntityPanelPopover; + return selectMapEntityPanelPopover.tbComponentRef.instance.entitySelected.asObservable().pipe( + tap(() => { + this.currentPopover = null; + }) + ); + } + } + private createdControlButtonTooltip(root: HTMLElement, side: TooltipPositioningSide) { import('tooltipster').then(() => { let tooltipData = this.tooltipInstances.find(d => d.root === root); @@ -382,6 +549,7 @@ export abstract class TbMap { private resize() { this.onResize(); this.map?.invalidateSize(); + this.currentPopover?.updatePosition(); } private updateBounds() { @@ -392,8 +560,9 @@ export abstract class TbMap { bounds = new L.LatLngBounds(null, null); dataLayersBounds.forEach(b => bounds.extend(b)); const mapBounds = this.map.getBounds(); - if (bounds.isValid() && this.settings.fitMapBounds && !mapBounds.contains(bounds)) { - if (!this.ignoreUpdateBounds) { + if (bounds.isValid() && (!this.bounds || !this.bounds.isValid() || !this.bounds.equals(bounds) && this.settings.fitMapBounds && !mapBounds.contains(bounds))) { + this.bounds = bounds; + if (!this.ignoreUpdateBounds && !this.isAddingItem) { this.fitBounds(bounds); } } @@ -424,16 +593,16 @@ export abstract class TbMap { private updateAddButtonsStates() { if (this.addMarkerButton) { - this.addMarkerButton.setDisabled(!this.addMarkerDataLayers.some(dl => dl.hasUnplacedItems())); + this.addMarkerButton.setDisabled(!this.addMarkerDataLayers.some(dl => dl.isEnabled() && dl.hasUnplacedItems())); } if (this.addRectangleButton) { - this.addRectangleButton.setDisabled(!this.addPolygonDataLayers.some(dl => dl.hasUnplacedItems())); + this.addRectangleButton.setDisabled(!this.addPolygonDataLayers.some(dl => dl.isEnabled() && dl.hasUnplacedItems())); } if (this.addPolygonButton) { - this.addPolygonButton.setDisabled(!this.addPolygonDataLayers.some(dl => dl.hasUnplacedItems())); + this.addPolygonButton.setDisabled(!this.addPolygonDataLayers.some(dl => dl.isEnabled() && dl.hasUnplacedItems())); } if (this.addCircleButton) { - this.addCircleButton.setDisabled(!this.addCircleDataLayers.some(dl => dl.hasUnplacedItems())); + this.addCircleButton.setDisabled(!this.addCircleDataLayers.some(dl => dl.isEnabled() && dl.hasUnplacedItems())); } } @@ -480,6 +649,10 @@ export abstract class TbMap { return this.settings.mapType; } + public enabledDataLayersUpdated() { + this.updateAddButtonsStates(); + } + public tooltipElementClick(element: HTMLElement, action: string, datasource: TbMapDatasource): void { if (element && this.tooltipActions[action]) { element.onclick = ($event) => @@ -527,6 +700,9 @@ export abstract class TbMap { } public selectItem(item: TbDataLayerItem, cancel = false, force = false): boolean { + if (this.isAddingItem) { + return false; + } let deselected = true; if (this.selectedDataItem) { deselected = this.selectedDataItem.deselect(cancel, force); diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/maps/models/map.models.ts b/ui-ngx/src/app/modules/home/components/widget/lib/maps/models/map.models.ts index 1f43e83c85..aa343e6520 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/maps/models/map.models.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/maps/models/map.models.ts @@ -25,6 +25,7 @@ import { Observable, Observer, of, switchMap } from 'rxjs'; import { map } from 'rxjs/operators'; import { ImagePipe } from '@shared/pipe/image.pipe'; import { MarkerShape } from '@home/components/widget/lib/maps/models/marker-shape.models'; +import { UnplacedMapDataItem } from '@home/components/widget/lib/maps/data-layer/map-data-layer'; export enum MapType { geoMap = 'geoMap', @@ -1019,6 +1020,16 @@ const mergeMapDatasource = (target: TbMapDatasource, source: TbMapDatasource): T return target; } +export const mergeUnplacedDataItemsArrays = (dataItemsArrays: UnplacedMapDataItem[][]): UnplacedMapDataItem[] => { + const itemsMap = new Map(); + dataItemsArrays.forEach(dataItems => { + dataItems.forEach(dataItem => { + itemsMap.set(dataItem.entity.$datasource.entityId, dataItem); + }); + }); + return Array.from(itemsMap.values()); +} + const imageAspectMap: {[key: string]: ImageWithAspect} = {}; const imageLoader = (imageUrl: string): Observable => new Observable((observer: Observer) => { diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/maps/panels/select-map-entity-panel.component.html b/ui-ngx/src/app/modules/home/components/widget/lib/maps/panels/select-map-entity-panel.component.html new file mode 100644 index 0000000000..afa4f23d97 --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/widget/lib/maps/panels/select-map-entity-panel.component.html @@ -0,0 +1,44 @@ + +
+ + {{ (selectEntityFormGroup.get('entity').value ? 'entity.entity' : 'widgets.maps.data-layer.select-entity') | translate }} + + + {{ entity.entity.entityDisplayName }} + + + widgets.maps.data-layer.select-entity-hint + +
+ + + +
+
diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/maps/panels/select-map-entity-panel.component.scss b/ui-ngx/src/app/modules/home/components/widget/lib/maps/panels/select-map-entity-panel.component.scss new file mode 100644 index 0000000000..63ff621ecd --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/widget/lib/maps/panels/select-map-entity-panel.component.scss @@ -0,0 +1,39 @@ +/** + * Copyright © 2016-2024 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +@import '../../../../../../../../scss/constants'; + +.tb-select-map-entity-panel { + width: 320px; + max-width: 90vw; + display: flex; + flex-direction: column; + gap: 16px; + @media #{$mat-xs} { + width: 90vw; + } + .mat-mdc-form-field-hint-wrapper { + padding: 0; + } + .tb-select-map-entity-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/maps/panels/select-map-entity-panel.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/maps/panels/select-map-entity-panel.component.ts new file mode 100644 index 0000000000..7b0ec943a0 --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/widget/lib/maps/panels/select-map-entity-panel.component.ts @@ -0,0 +1,69 @@ +/// +/// Copyright © 2016-2024 The Thingsboard Authors +/// +/// Licensed under the Apache License, Version 2.0 (the "License"); +/// you may not use this file except in compliance with the License. +/// You may obtain a copy of the License at +/// +/// http://www.apache.org/licenses/LICENSE-2.0 +/// +/// Unless required by applicable law or agreed to in writing, software +/// distributed under the License is distributed on an "AS IS" BASIS, +/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +/// See the License for the specific language governing permissions and +/// limitations under the License. +/// + +import { Component, EventEmitter, Input, OnInit, Output, ViewEncapsulation } from '@angular/core'; +import { PageComponent } from '@shared/components/page.component'; +import { UntypedFormBuilder, UntypedFormGroup, Validators } from '@angular/forms'; +import { Store } from '@ngrx/store'; +import { AppState } from '@core/core.state'; +import { TbPopoverComponent } from '@shared/components/popover.component'; +import { UnplacedMapDataItem } from '@home/components/widget/lib/maps/data-layer/map-data-layer'; + +@Component({ + selector: 'tb-select-map-entity-panel', + templateUrl: './select-map-entity-panel.component.html', + providers: [], + styleUrls: ['./select-map-entity-panel.component.scss'], + encapsulation: ViewEncapsulation.None +}) +export class SelectMapEntityPanelComponent extends PageComponent implements OnInit { + + @Input() + entities: UnplacedMapDataItem[]; + + @Output() + entitySelected = new EventEmitter(); + + selectEntityFormGroup: UntypedFormGroup; + + selectedEntity: UnplacedMapDataItem = null; + + constructor(private fb: UntypedFormBuilder, + protected store: Store, + private popover: TbPopoverComponent) { + super(store); + } + + ngOnInit(): void { + this.selectEntityFormGroup = this.fb.group( + { + entity: ['', Validators.required] + } + ); + this.popover.tbDestroy.subscribe(() => { + this.entitySelected.emit(this.selectedEntity); + }); + } + + cancel() { + this.popover.hide(); + } + + selectEntity() { + this.selectedEntity = this.selectEntityFormGroup.value.entity; + this.popover.hide(); + } +} diff --git a/ui-ngx/src/app/modules/home/components/widget/widget-components.module.ts b/ui-ngx/src/app/modules/home/components/widget/widget-components.module.ts index 1530561924..acd07ad0da 100644 --- a/ui-ngx/src/app/modules/home/components/widget/widget-components.module.ts +++ b/ui-ngx/src/app/modules/home/components/widget/widget-components.module.ts @@ -89,6 +89,9 @@ import { EllipsisChipListDirective } from '@shared/directives/ellipsis-chip-list import { ScadaSymbolWidgetComponent } from '@home/components/widget/lib/scada/scada-symbol-widget.component'; import { TwoSegmentButtonWidgetComponent } from '@home/components/widget/lib/button/two-segment-button-widget.component'; import { MapWidgetComponent } from '@home/components/widget/lib/maps/map-widget.component'; +import { + SelectMapEntityPanelComponent +} from '@home/components/widget/lib/maps/panels/select-map-entity-panel.component'; @NgModule({ declarations: [ @@ -143,6 +146,7 @@ import { MapWidgetComponent } from '@home/components/widget/lib/maps/map-widget. UnreadNotificationWidgetComponent, NotificationTypeFilterPanelComponent, ScadaSymbolWidgetComponent, + SelectMapEntityPanelComponent, MapWidgetComponent ], imports: [ diff --git a/ui-ngx/src/app/modules/home/components/widget/widget.component.ts b/ui-ngx/src/app/modules/home/components/widget/widget.component.ts index eaf6b6105e..5dc88a0bce 100644 --- a/ui-ngx/src/app/modules/home/components/widget/widget.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/widget.component.ts @@ -248,6 +248,8 @@ export class WidgetComponent extends PageComponent implements OnInit, OnChanges, this.widgetContext.isPreview = this.isPreview; this.widgetContext.isMobile = this.isMobile; this.widgetContext.toastTargetId = this.toastTargetId; + this.widgetContext.renderer = this.renderer; + this.widgetContext.widgetContentContainer = this.widgetContentContainer; this.widgetContext.subscriptionApi = { createSubscription: this.createSubscription.bind(this), diff --git a/ui-ngx/src/app/modules/home/models/widget-component.models.ts b/ui-ngx/src/app/modules/home/models/widget-component.models.ts index c599fbf16d..5380f1ea84 100644 --- a/ui-ngx/src/app/modules/home/models/widget-component.models.ts +++ b/ui-ngx/src/app/modules/home/models/widget-component.models.ts @@ -46,7 +46,15 @@ import { WidgetActionsApi, WidgetSubscriptionApi } from '@core/api/widget-api.models'; -import { ChangeDetectorRef, InjectionToken, Injector, NgZone, TemplateRef, Type } from '@angular/core'; +import { + ChangeDetectorRef, + InjectionToken, + Injector, + NgZone, Renderer2, + TemplateRef, + Type, + ViewContainerRef +} from '@angular/core'; import { HttpClient, HttpErrorResponse } from '@angular/common/http'; import { RafService } from '@core/services/raf.service'; import { WidgetTypeId } from '@shared/models/id/widget-type-id'; @@ -215,6 +223,8 @@ export class WidgetContext { http: HttpClient; sanitizer: DomSanitizer; router: Router; + renderer: Renderer2; + widgetContentContainer: ViewContainerRef; private changeDetectorValue: ChangeDetectorRef; private containerChangeDetectorValue: ChangeDetectorRef; 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 260999408a..9fa662c127 100644 --- a/ui-ngx/src/assets/locale/locale.constant-en_US.json +++ b/ui-ngx/src/assets/locale/locale.constant-en_US.json @@ -7718,7 +7718,8 @@ }, "edit": "Edit marker", "remove-marker-for": "Remove marker for '{{entityName}}'", - "place-marker": "Place marker" + "place-marker": "Place marker", + "place-marker-hint": "Click to place '{{entityName}}' entity" }, "polygon": { "polygon-key": "Polygon key", @@ -7747,7 +7748,9 @@ "edit": "Edit circle", "remove-circle-for": "Remove circle for '{{entityName}}'", "draw-circle": "Draw circle" - } + }, + "select-entity": "Select entity", + "select-entity-hint": "Hint: after selection click at the map to set position" }, "select-entity": "Select entity", "select-entity-hint": "Hint: after selection click at the map to set position", diff --git a/ui-ngx/src/typings/leaflet-extend-tb.d.ts b/ui-ngx/src/typings/leaflet-extend-tb.d.ts index b75743c139..e24c632a3b 100644 --- a/ui-ngx/src/typings/leaflet-extend-tb.d.ts +++ b/ui-ngx/src/typings/leaflet-extend-tb.d.ts @@ -94,6 +94,7 @@ declare module 'leaflet' { title: string; click: (e: MouseEvent, button: ToolbarButton) => void; iconClass: string; + showText?: boolean; } class ToolbarButton extends Control{ @@ -118,7 +119,7 @@ declare module 'leaflet' { class BottomToolbarControl extends Control { constructor(options: BottomToolbarControlOptions); getButton(id: string): ToolbarButton | undefined; - open(buttons: ToolbarButtonOptions[]): void; + open(buttons: ToolbarButtonOptions[], showCloseButton?: boolean): void; close(): void; container: HTMLElement; } From c8db304ce6e067dcd00791054ca58fa6b0ad9123 Mon Sep 17 00:00:00 2001 From: ViacheslavKlimov Date: Thu, 30 Jan 2025 12:43:14 +0200 Subject: [PATCH 040/345] Optimized postgres sync (#3208) * optimized postgres sync * Fix consumer stopping when stopWhenRead enabled * Fix NPE on EDQS repartitioning * fixed EntityServiceTest * Fix EDQS yml props --------- Co-authored-by: ViacheslavKlimov --- .../service/edqs/DefaultEdqsService.java | 1 + .../server/service/edqs/EdqsDataLoader.java | 539 ------------------ .../server/service/edqs/EdqsSyncService.java | 102 +++- .../service/edqs/KafkaEdqsSyncService.java | 2 +- .../src/main/resources/thingsboard.yml | 5 +- .../service/entitiy/EntityServiceTest.java | 267 ++++++++- .../common/data/edqs/fields/FieldsUtil.java | 2 + .../server/edqs/processor/EdqsProcessor.java | 30 +- .../ApiUsageStateQueryProcessor.java | 3 +- .../edqs/state/KafkaEdqsStateService.java | 18 +- .../queue/discovery/HashPartitionService.java | 2 +- .../queue/kafka/TbKafkaConsumerTemplate.java | 16 +- .../common/stats/DefaultStatsFactory.java | 2 +- .../server/dao/attributes/AttributesDao.java | 7 +- .../server/dao/relation/RelationDao.java | 4 - .../sql/attributes/AttributeKvRepository.java | 8 + .../dao/sql/attributes/JpaAttributeDao.java | 10 +- .../query/DefaultEntityQueryRepository.java | 4 + .../dao/sql/relation/JpaRelationDao.java | 6 - .../dao/sql/relation/RelationRepository.java | 13 +- .../CachedRedisSqlTimeseriesLatestDao.java | 4 - .../dao/sqlts/SqlTimeseriesLatestDao.java | 4 - .../sqlts/latest/TsKvLatestRepository.java | 8 + .../CassandraBaseTimeseriesLatestDao.java | 4 - .../dao/timeseries/TimeseriesLatestDao.java | 1 - .../edqs/ThingsboardEdqsApplication.java | 10 +- edqs/src/main/resources/edqs.yml | 7 +- 27 files changed, 426 insertions(+), 653 deletions(-) delete mode 100644 application/src/main/java/org/thingsboard/server/service/edqs/EdqsDataLoader.java diff --git a/application/src/main/java/org/thingsboard/server/service/edqs/DefaultEdqsService.java b/application/src/main/java/org/thingsboard/server/service/edqs/DefaultEdqsService.java index 7582d036e2..160d4bb565 100644 --- a/application/src/main/java/org/thingsboard/server/service/edqs/DefaultEdqsService.java +++ b/application/src/main/java/org/thingsboard/server/service/edqs/DefaultEdqsService.java @@ -125,6 +125,7 @@ public class DefaultEdqsService implements EdqsService { executor.submit(() -> { try { EdqsSyncState syncState = getSyncState(); + // FIXME: Slavik smart events check if (edqsSyncService.isSyncNeeded() || syncState == null || syncState.getStatus() != EdqsSyncStatus.FINISHED) { if (hashPartitionService.isSystemPartitionMine(ServiceType.TB_CORE)) { processSystemRequest(ToCoreEdqsRequest.builder() diff --git a/application/src/main/java/org/thingsboard/server/service/edqs/EdqsDataLoader.java b/application/src/main/java/org/thingsboard/server/service/edqs/EdqsDataLoader.java deleted file mode 100644 index 69a3f6108d..0000000000 --- a/application/src/main/java/org/thingsboard/server/service/edqs/EdqsDataLoader.java +++ /dev/null @@ -1,539 +0,0 @@ -/** - * Copyright © 2016-2024 The Thingsboard Authors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package org.thingsboard.server.service.edqs; - -import com.fasterxml.jackson.databind.MappingIterator; -import com.fasterxml.jackson.dataformat.csv.CsvMapper; -import com.fasterxml.jackson.dataformat.csv.CsvSchema; -import lombok.RequiredArgsConstructor; -import lombok.extern.slf4j.Slf4j; -import org.thingsboard.common.util.ThingsBoardThreadFactory; -import org.thingsboard.server.common.data.ApiUsageState; -import org.thingsboard.server.common.data.AttributeScope; -import org.thingsboard.server.common.data.Customer; -import org.thingsboard.server.common.data.Dashboard; -import org.thingsboard.server.common.data.Device; -import org.thingsboard.server.common.data.DeviceProfile; -import org.thingsboard.server.common.data.DeviceProfileType; -import org.thingsboard.server.common.data.EntityType; -import org.thingsboard.server.common.data.EntityView; -import org.thingsboard.server.common.data.ObjectType; -import org.thingsboard.server.common.data.StringUtils; -import org.thingsboard.server.common.data.Tenant; -import org.thingsboard.server.common.data.User; -import org.thingsboard.server.common.data.asset.Asset; -import org.thingsboard.server.common.data.asset.AssetProfile; -import org.thingsboard.server.common.data.converter.Converter; -import org.thingsboard.server.common.data.converter.ConverterType; -import org.thingsboard.server.common.data.edge.Edge; -import org.thingsboard.server.common.data.edqs.AttributeKv; -import org.thingsboard.server.common.data.edqs.LatestTsKv; -import org.thingsboard.server.common.data.group.EntityGroup; -import org.thingsboard.server.common.data.id.ApiUsageStateId; -import org.thingsboard.server.common.data.id.AssetId; -import org.thingsboard.server.common.data.id.AssetProfileId; -import org.thingsboard.server.common.data.id.ConverterId; -import org.thingsboard.server.common.data.id.CustomerId; -import org.thingsboard.server.common.data.id.DashboardId; -import org.thingsboard.server.common.data.id.DeviceId; -import org.thingsboard.server.common.data.id.DeviceProfileId; -import org.thingsboard.server.common.data.id.EdgeId; -import org.thingsboard.server.common.data.id.EntityGroupId; -import org.thingsboard.server.common.data.id.EntityId; -import org.thingsboard.server.common.data.id.EntityIdFactory; -import org.thingsboard.server.common.data.id.EntityViewId; -import org.thingsboard.server.common.data.id.IntegrationId; -import org.thingsboard.server.common.data.id.RoleId; -import org.thingsboard.server.common.data.id.RuleChainId; -import org.thingsboard.server.common.data.id.SchedulerEventId; -import org.thingsboard.server.common.data.id.TenantId; -import org.thingsboard.server.common.data.id.TenantProfileId; -import org.thingsboard.server.common.data.id.UserId; -import org.thingsboard.server.common.data.id.WidgetTypeId; -import org.thingsboard.server.common.data.id.WidgetsBundleId; -import org.thingsboard.server.common.data.integration.Integration; -import org.thingsboard.server.common.data.integration.IntegrationType; -import org.thingsboard.server.common.data.kv.AttributeKvEntry; -import org.thingsboard.server.common.data.kv.BaseAttributeKvEntry; -import org.thingsboard.server.common.data.kv.BasicTsKvEntry; -import org.thingsboard.server.common.data.kv.BooleanDataEntry; -import org.thingsboard.server.common.data.kv.DoubleDataEntry; -import org.thingsboard.server.common.data.kv.JsonDataEntry; -import org.thingsboard.server.common.data.kv.KvEntry; -import org.thingsboard.server.common.data.kv.LongDataEntry; -import org.thingsboard.server.common.data.kv.StringDataEntry; -import org.thingsboard.server.common.data.relation.EntityRelation; -import org.thingsboard.server.common.data.relation.RelationTypeGroup; -import org.thingsboard.server.common.data.role.Role; -import org.thingsboard.server.common.data.role.RoleType; -import org.thingsboard.server.common.data.rule.RuleChain; -import org.thingsboard.server.common.data.scheduler.SchedulerEvent; -import org.thingsboard.server.common.data.widget.WidgetType; -import org.thingsboard.server.common.data.widget.WidgetsBundle; -import org.thingsboard.server.common.msg.edqs.EdqsService; -import org.thingsboard.server.edqs.processor.EdqsConverter; - -import java.io.FileReader; -import java.util.Map; -import java.util.UUID; -import java.util.concurrent.ExecutorService; -import java.util.concurrent.Executors; -import java.util.function.Consumer; - -import static org.thingsboard.common.util.JacksonUtil.toJsonNode; - - -@RequiredArgsConstructor -@Slf4j -//@Service -public class EdqsDataLoader { - - private final EdqsService edqsService; - private final EdqsConverter edqsConverter; - - public final static TenantId MAIN = TenantId.fromUUID(UUID.fromString("2a209df0-c7ff-11ea-a3e0-f321b0429d60")); - - private final String folder = "/home/viacheslav/Downloads/schwarz"; - - private ExecutorService executor = Executors.newFixedThreadPool(5, ThingsBoardThreadFactory.forName("edqs-publisher")); - -// @AfterStartUp(order = 100) - public void load() throws Exception { - loadCustomers(); - loadDeviceProfile(); - loadDevices(); - loadAssets(); - loadEdges(); - loadEntityViews(); - loadTenants(); - loadUsers(); - loadDashboards(); - loadRuleChains(); - loadWidgetType(); - loadWidgetBundle(); - loadConverters(); - loadIntegrations(); - loadSchedulerEvents(); - loadRoles(); - loadApiUsageStates(); - loadAssetProfile(); - loadEntityGroups(); - loadRelations(); - - loadAttributes(); - loadTs(); - } - - private void loadCustomers() throws Exception { - load("customer.csv", (values) -> { - Customer customer = new Customer(); - customer.setTitle(values.get("title")); - customer.setId(new CustomerId(UUID.fromString(values.get("id")))); - customer.setCreatedTime(Long.parseLong(values.get("created_time"))); - customer.setTenantId(tenantId(values.get("tenant_id"))); - var parentCustomerId = values.get("parent_customer_id"); - if (StringUtils.isNotEmpty(parentCustomerId)) { - customer.setParentCustomerId(new CustomerId(UUID.fromString(parentCustomerId))); - } - edqsService.onUpdate(customer.getTenantId(), customer.getId(), customer); - }); - } - - private void loadDevices() throws Exception { - load("device.csv", (values) -> { - Device device = new Device(); - device.setType(values.get("type")); - device.setName(values.get("name")); - device.setLabel(values.get("label")); - device.setId(new DeviceId(uuid(values.get("id")))); - device.setCreatedTime(parseLong(values.get("created_time"))); - device.setCustomerId(customerId(values.get("customer_id"))); - device.setTenantId(tenantId(values.get("tenant_id"))); - device.setDeviceProfileId(new DeviceProfileId(uuid(values.get("device_profile_id")))); - device.setAdditionalInfo(toJsonNode(values.get("additional_info"))); - - edqsService.onUpdate(device.getTenantId(), device.getId(), device); - }); - } - - private void loadAssets() throws Exception { - load("asset.csv", (values) -> { - Asset asset = new Asset(); - asset.setType(values.get("type")); - asset.setName(values.get("name")); - asset.setLabel(values.get("label")); - asset.setId(new AssetId(uuid(values.get("id")))); - asset.setCreatedTime(parseLong(values.get("created_time"))); - asset.setCustomerId(customerId(values.get("customer_id"))); - asset.setTenantId(tenantId(values.get("tenant_id"))); - asset.setAssetProfileId(new AssetProfileId(uuid(values.get("asset_profile_id")))); - asset.setAdditionalInfo(toJsonNode(values.get("additional_info"))); - - edqsService.onUpdate(asset.getTenantId(), asset.getId(), asset); - }); - } - - private void loadEdges() throws Exception { - load("edge.csv", (values) -> { - Edge edge = new Edge(); - edge.setId(new EdgeId(uuid(values.get("id")))); - edge.setCreatedTime(parseLong(values.get("created_time"))); - edge.setType(values.get("type")); - edge.setName(values.get("name")); - edge.setLabel(values.get("label")); - edge.setCustomerId(customerId(values.get("customer_id"))); - edge.setTenantId(tenantId(values.get("tenant_id"))); - edge.setAdditionalInfo(toJsonNode(values.get("additional_info"))); - - edqsService.onUpdate(edge.getTenantId(), edge.getId(), edge); - }); - } - - private void loadEntityViews() throws Exception { - load("entity_view.csv", (values) -> { - EntityView entityView = new EntityView(); - entityView.setId(new EntityViewId(uuid(values.get("id")))); - entityView.setCreatedTime(parseLong(values.get("created_time"))); - entityView.setType(values.get("type")); - entityView.setName(values.get("name")); - entityView.setCustomerId(customerId(values.get("customer_id"))); - entityView.setTenantId(tenantId(values.get("tenant_id"))); - entityView.setAdditionalInfo(toJsonNode(values.get("additional_info"))); - - edqsService.onUpdate(entityView.getTenantId(), entityView.getId(), entityView); - }); - } - - private void loadTenants() throws Exception { - load("tenant.csv", (values) -> { - Tenant tenant = new Tenant(); - tenant.setId(new TenantId(uuid(values.get("id")))); - tenant.setCreatedTime(parseLong(values.get("created_time"))); - tenant.setEmail(values.get("email")); - tenant.setTitle(values.get("title")); - tenant.setCountry(values.get("country")); - tenant.setState(values.get("state")); - tenant.setCity(values.get("city")); - tenant.setAddress(values.get("address")); - tenant.setAddress2(values.get("address2")); - tenant.setZip(values.get("zip")); - tenant.setPhone(values.get("phone")); - tenant.setRegion(values.get("region")); - tenant.setTenantProfileId(new TenantProfileId(uuid(values.get("tenant_profile_id")))); - tenant.setAdditionalInfo(toJsonNode(values.get("additional_info"))); - edqsService.onUpdate(MAIN, tenant.getId(), tenant); - }); - } - - private void loadUsers() throws Exception { - load("user.csv", (values) -> { - User user = new User(); - user.setId(new UserId(uuid(values.get("id")))); - user.setCreatedTime(parseLong(values.get("created_time"))); - user.setTenantId(tenantId(values.get("tenant_id"))); - user.setFirstName(values.get("first_name")); - user.setLastName(values.get("last_name")); - user.setEmail(values.get("email")); - user.setPhone(values.get("phone")); - user.setAdditionalInfo(toJsonNode(values.get("additional_info"))); - - edqsService.onUpdate(user.getTenantId(), user.getId(), user); - }); - } - - private void loadDashboards() throws Exception { - load("dashboard.csv", (values) -> { - Dashboard dashboard = new Dashboard(); - dashboard.setId(new DashboardId(uuid(values.get("id")))); - dashboard.setCreatedTime(parseLong(values.get("created_time"))); - dashboard.setTenantId(tenantId(values.get("tenant_id"))); - dashboard.setTitle(values.get("title")); - - edqsService.onUpdate(dashboard.getTenantId(), dashboard.getId(), dashboard); - }); - } - - private void loadEntityGroups() throws Exception { - load("entity_group.csv", (values) -> { - EntityGroup entityGroup = new EntityGroup(); - entityGroup.setId(new EntityGroupId(uuid(values.get("id")))); - entityGroup.setCreatedTime(parseLong(values.get("created_time"))); - entityGroup.setName(values.get("name")); - entityGroup.setOwnerId(entityId(values.get("owner_type"), values.get("owner_id"))); - entityGroup.setType(EntityType.valueOf(values.get("type"))); - edqsService.onUpdate(MAIN, entityGroup.getId(), entityGroup); - }); - } - - private void loadRelations() throws Exception { - load("relation.csv", (values) -> { - EntityRelation entityRelation = new EntityRelation(); - entityRelation.setFrom(entityId(values.get("from_type"), values.get("from_id"))); - entityRelation.setTo(entityId(values.get("to_type"), values.get("to_id"))); - entityRelation.setTypeGroup(RelationTypeGroup.valueOf(values.get("relation_type_group"))); - entityRelation.setType(values.get("relation_type")); - edqsService.onUpdate(MAIN, ObjectType.RELATION, entityRelation); - }); - } - - private void loadRuleChains() throws Exception { - load("rule_chain.csv", (values) -> { - RuleChain ruleChain = new RuleChain(); - ruleChain.setId(new RuleChainId(uuid(values.get("id")))); - ruleChain.setCreatedTime(parseLong(values.get("created_time"))); - ruleChain.setName(values.get("name")); - ruleChain.setTenantId(tenantId(values.get("tenant_id"))); - ruleChain.setAdditionalInfo(toJsonNode(values.get("additional_info"))); - - edqsService.onUpdate(ruleChain.getTenantId(), ruleChain.getId(), ruleChain); - }); - } - - private void loadWidgetType() throws Exception { - load("widget_type.csv", (values) -> { - WidgetType widgetType = new WidgetType(); - widgetType.setId(new WidgetTypeId(uuid(values.get("id")))); - widgetType.setCreatedTime(parseLong(values.get("created_time"))); - widgetType.setName(values.get("name")); - widgetType.setTenantId(tenantId(values.get("tenant_id"))); - - edqsService.onUpdate(widgetType.getTenantId(), widgetType.getId(), widgetType); - }); - } - - private void loadWidgetBundle() throws Exception { - load("widgets_bundle.csv", (values) -> { - WidgetsBundle widgetsBundle = new WidgetsBundle(); - widgetsBundle.setId(new WidgetsBundleId(uuid(values.get("id")))); - widgetsBundle.setCreatedTime(parseLong(values.get("created_time"))); - widgetsBundle.setTitle(values.get("title")); - widgetsBundle.setTenantId(tenantId(values.get("tenant_id"))); - - edqsService.onUpdate(widgetsBundle.getTenantId(), widgetsBundle.getId(), widgetsBundle); - }); - } - - private void loadConverters() throws Exception { - load("converter.csv", (values) -> { - Converter converter = new Converter(); - converter.setId(new ConverterId(uuid(values.get("id")))); - converter.setCreatedTime(parseLong(values.get("created_time"))); - converter.setName(values.get("name")); - converter.setType(ConverterType.valueOf(values.get("type"))); - converter.setTenantId(tenantId(values.get("tenant_id"))); - converter.setEdgeTemplate(parseBoolean(values.get("is_edge_template"))); - converter.setAdditionalInfo(toJsonNode(values.get("additional_info"))); - - edqsService.onUpdate(converter.getTenantId(), converter.getId(), converter); - }); - } - - private void loadIntegrations() throws Exception { - load("integration.csv", (values) -> { - Integration integration = new Integration(); - integration.setId(new IntegrationId(uuid(values.get("id")))); - integration.setCreatedTime(parseLong(values.get("created_time"))); - integration.setName(values.get("name")); - integration.setType(IntegrationType.valueOf(values.get("type"))); - integration.setTenantId(tenantId(values.get("tenant_id"))); - integration.setEdgeTemplate(parseBoolean(values.get("is_edge_template"))); - integration.setAdditionalInfo(toJsonNode(values.get("additional_info"))); - - edqsService.onUpdate(integration.getTenantId(), integration.getId(), integration); - }); - } - - private void loadSchedulerEvents() throws Exception { - load("scheduler_event.csv", (values) -> { - SchedulerEvent schedulerEvent = new SchedulerEvent(); - schedulerEvent.setId(new SchedulerEventId(uuid(values.get("id")))); - schedulerEvent.setCreatedTime(parseLong(values.get("created_time"))); - schedulerEvent.setName(values.get("name")); - schedulerEvent.setType(values.get("type")); - schedulerEvent.setTenantId(tenantId(values.get("tenant_id"))); - schedulerEvent.setConfiguration(toJsonNode(values.get("configuration"))); - schedulerEvent.setSchedule(toJsonNode(values.get("schedule"))); - schedulerEvent.setOriginatorId(entityId(values.get("originator_type"), values.get("originator_id"))); - schedulerEvent.setAdditionalInfo(toJsonNode(values.get("additional_info"))); - - edqsService.onUpdate(schedulerEvent.getTenantId(), schedulerEvent.getId(), schedulerEvent); - }); - } - - private void loadRoles() throws Exception { - load("role.csv", (values) -> { - Role role = new Role(); - role.setId(new RoleId(uuid(values.get("id")))); - role.setCreatedTime(parseLong(values.get("created_time"))); - role.setName(values.get("name")); - role.setType(RoleType.valueOf(values.get("type"))); - role.setTenantId(tenantId(values.get("tenant_id"))); - role.setAdditionalInfo(toJsonNode(values.get("additional_info"))); - - edqsService.onUpdate(role.getTenantId(), role.getId(), role); - }); - } - - private void loadApiUsageStates() throws Exception { - load("api_usage_state.csv", (values) -> { - ApiUsageState apiUsageState = new ApiUsageState(); - apiUsageState.setId(new ApiUsageStateId(uuid(values.get("id")))); - apiUsageState.setCreatedTime(parseLong(values.get("created_time"))); - apiUsageState.setEntityId(entityId(values.get("entity_type"), values.get("entity_id"))); - apiUsageState.setTenantId(tenantId(values.get("tenant_id"))); - - edqsService.onUpdate(apiUsageState.getTenantId(), apiUsageState.getId(), apiUsageState); - }); - } - - private void loadDeviceProfile() throws Exception { - load("device_profile.csv", (values) -> { - DeviceProfile deviceProfile = new DeviceProfile(); - deviceProfile.setId(new DeviceProfileId(uuid(values.get("id")))); - deviceProfile.setCreatedTime(parseLong(values.get("created_time"))); - deviceProfile.setName(values.get("name")); - deviceProfile.setType(DeviceProfileType.valueOf(values.get("type"))); - deviceProfile.setTenantId(tenantId(values.get("tenant_id"))); - - edqsService.onUpdate(deviceProfile.getTenantId(), deviceProfile.getId(), deviceProfile); - }); - } - - private void loadAssetProfile() throws Exception { - load("asset_profile.csv", (values) -> { - AssetProfile assetProfile = new AssetProfile(); - assetProfile.setId(new AssetProfileId(uuid(values.get("id")))); - assetProfile.setCreatedTime(parseLong(values.get("created_time"))); - assetProfile.setName(values.get("name")); - assetProfile.setTenantId(tenantId(values.get("tenant_id"))); - - edqsService.onUpdate(assetProfile.getTenantId(), assetProfile.getId(), assetProfile); - }); - } - - private void loadAttributes() throws Exception { - load("attribute.csv", (values) -> { - EntityId entityId = EntityIdFactory.getByTypeAndId(values.get("entity_type"), values.get("entity_id")); - long ts = parseLong(values.get("last_update_ts")); - AttributeScope scope = AttributeScope.valueOf(values.get("attribute_type")); - String key = values.get("attribute_key"); - KvEntry kvEntry; - if (StringUtils.isNotEmpty(values.get("bool_v"))) { - kvEntry = new BooleanDataEntry(key, "t".equals(values.get("bool_v"))); - } else if (StringUtils.isNotEmpty(values.get("str_v"))) { - kvEntry = new StringDataEntry(key, values.get("str_v")); - } else if (StringUtils.isNotEmpty(values.get("long_v"))) { - kvEntry = new LongDataEntry(key, parseLong(values.get("long_v"))); - } else if (StringUtils.isNotEmpty(values.get("dbl_v"))) { - kvEntry = new DoubleDataEntry(key, Double.parseDouble(values.get("dbl_v"))); - } else if (StringUtils.isNotEmpty(values.get("json_v"))) { - kvEntry = new JsonDataEntry(key, values.get("json_v")); - } else { - kvEntry = new StringDataEntry(key, ""); - } - AttributeKvEntry attributeKvEntry = new BaseAttributeKvEntry(ts, kvEntry); - AttributeKv attributeKv = new AttributeKv(entityId, scope, attributeKvEntry, 0); - edqsService.onUpdate(MAIN, ObjectType.ATTRIBUTE_KV, attributeKv); - }); - } - - private void loadTs() throws Exception { - load("ts_kv.csv", (values) -> { - var entityTypeStr = values.get("find_entity_type"); - if (StringUtils.isEmpty(entityTypeStr)) { - return; - } - EntityId entityId = EntityIdFactory.getByTypeAndId(values.get("find_entity_type"), values.get("entity_id")); - long ts = parseLong(values.get("ts")); - String key = values.get("key"); - KvEntry kvEntry; - if (StringUtils.isNotEmpty(values.get("bool_v"))) { - kvEntry = new BooleanDataEntry(key, "t".equals(values.get("bool_v"))); - } else if (StringUtils.isNotEmpty(values.get("str_v"))) { - kvEntry = new StringDataEntry(key, values.get("str_v")); - } else if (StringUtils.isNotEmpty(values.get("long_v"))) { - kvEntry = new LongDataEntry(key, parseLong(values.get("long_v"))); - } else if (StringUtils.isNotEmpty(values.get("dbl_v"))) { - kvEntry = new DoubleDataEntry(key, Double.parseDouble(values.get("dbl_v"))); - } else if (StringUtils.isNotEmpty(values.get("json_v"))) { - kvEntry = new JsonDataEntry(key, values.get("json_v")); - } else { - kvEntry = new StringDataEntry(key, ""); - } - BasicTsKvEntry tsKvEntry = new BasicTsKvEntry(ts, kvEntry); - edqsService.onUpdate(MAIN, ObjectType.LATEST_TS_KV, new LatestTsKv(entityId, tsKvEntry, 0L)); - }); - } - - private void load(String file, Consumer> function) throws Exception { - Executors.newSingleThreadExecutor(ThingsBoardThreadFactory.forName("loader-" + file)).submit(() -> { - try { - long ts = System.currentTimeMillis(); - CsvSchema schema = CsvSchema.emptySchema().withHeader().withColumnSeparator('|'); - CsvMapper mapper = new CsvMapper(); - MappingIterator> it = mapper - .readerFor(Map.class) - .with(schema) - .readValues(new FileReader(folder + "/" + file)); - - int success = 0; - int failure = 0; - while (it.hasNextValue()) { - Map row = it.nextValue(); - try { - function.accept(row); - success++; - if (success % 1000 == 0) { - log.info("Loaded [{}] from [{}]", success, file); - } - } catch (Exception e) { - log.error("Failed to parse str: [{}]", row, e); - failure++; - } - } - log.info("Loaded [{}] from [{}] in {}ms. Failures {}", success, file, (System.currentTimeMillis() - ts), failure); - } catch (Throwable t) { - log.error("Failed to load data from [{}]", file, t); - } - }); - } - - private static TenantId tenantId(String id) { - return TenantId.fromUUID(UUID.fromString(id)); - } - - private static CustomerId customerId(String id) { - var c = new CustomerId(UUID.fromString(id)); - return c.isNullUid() ? null : c; - } - - private static EntityId entityId(String type, String id) { - return EntityIdFactory.getByTypeAndId(type, id); - } - - private static UUID uuid(String id) { - return UUID.fromString(id); - } - - private static long parseLong(String time) { - return Long.parseLong(time); - } - - private static boolean parseBoolean(String value) { - return Boolean.parseBoolean(value); - } - -} diff --git a/application/src/main/java/org/thingsboard/server/service/edqs/EdqsSyncService.java b/application/src/main/java/org/thingsboard/server/service/edqs/EdqsSyncService.java index 8e12da56e0..bf8e096059 100644 --- a/application/src/main/java/org/thingsboard/server/service/edqs/EdqsSyncService.java +++ b/application/src/main/java/org/thingsboard/server/service/edqs/EdqsSyncService.java @@ -32,8 +32,6 @@ import org.thingsboard.server.common.data.id.EntityId; import org.thingsboard.server.common.data.id.EntityIdFactory; import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.common.data.page.PageDataIterable; -import org.thingsboard.server.common.data.page.SortOrder; -import org.thingsboard.server.common.data.relation.EntityRelation; import org.thingsboard.server.common.data.relation.RelationTypeGroup; import org.thingsboard.server.dao.Dao; import org.thingsboard.server.dao.attributes.AttributesDao; @@ -41,13 +39,15 @@ import org.thingsboard.server.dao.dictionary.KeyDictionaryDao; import org.thingsboard.server.dao.entity.EntityDaoRegistry; import org.thingsboard.server.dao.group.EntityGroupDao; import org.thingsboard.server.dao.model.sql.AttributeKvEntity; +import org.thingsboard.server.dao.model.sql.RelationEntity; import org.thingsboard.server.dao.model.sqlts.dictionary.KeyDictionaryEntry; import org.thingsboard.server.dao.model.sqlts.latest.TsKvLatestEntity; -import org.thingsboard.server.dao.relation.RelationDao; +import org.thingsboard.server.dao.sql.relation.RelationRepository; +import org.thingsboard.server.dao.sqlts.latest.TsKvLatestRepository; import org.thingsboard.server.dao.tenant.TenantDao; -import org.thingsboard.server.dao.timeseries.TimeseriesLatestDao; import java.util.EnumSet; +import java.util.List; import java.util.Map; import java.util.Set; import java.util.UUID; @@ -92,11 +92,11 @@ public abstract class EdqsSyncService { @Autowired private KeyDictionaryDao keyDictionaryDao; @Autowired - private RelationDao relationDao; + private RelationRepository relationRepository; @Autowired private EntityGroupDao entityGroupDao; @Autowired - private TimeseriesLatestDao timeseriesLatestDao; + private TsKvLatestRepository tsKvLatestRepository; @Autowired @Lazy private DefaultEdqsService edqsService; @@ -171,7 +171,7 @@ public abstract class EdqsSyncService { private void syncEntityGroups() { log.info("Synchronizing entity groups to EDQS"); long ts = System.currentTimeMillis(); - var entityGroups = new PageDataIterable<>(entityGroupDao::findAllFields, 10000); + var entityGroups = new PageDataIterable<>(entityGroupDao::findAllFields, 30000); for (EntityFields groupFields : entityGroups) { EntityIdInfo entityIdInfo = entityInfoMap.get(groupFields.getOwnerId()); if (entityIdInfo != null) { @@ -187,18 +187,43 @@ public abstract class EdqsSyncService { private void syncRelations() { log.info("Synchronizing relations to EDQS"); long ts = System.currentTimeMillis(); - var relations = new PageDataIterable<>(relationDao::findAll, 10000); - for (EntityRelation relation : relations) { - if (relation.getTypeGroup() == RelationTypeGroup.COMMON || relation.getTypeGroup() == RelationTypeGroup.FROM_ENTITY_GROUP) { - EntityIdInfo entityIdInfo = entityInfoMap.get(relation.getFrom().getId()); + UUID lastFromEntityId = UUID.fromString("00000000-0000-0000-0000-000000000000"); + String lastFromEntityType = ""; + String lastRelationTypeGroup = ""; + String lastRelationType = ""; + UUID lastToEntityId = UUID.fromString("00000000-0000-0000-0000-000000000000"); + String lastToEntityType = ""; + + while (true) { + List batch = relationRepository.findNextBatch(lastFromEntityId, lastFromEntityType, lastRelationTypeGroup, + lastRelationType, lastToEntityId, lastToEntityType, 10000); + if (batch.isEmpty()) { + break; + } + processRelationBatch(batch); + + RelationEntity lastRecord = batch.get(batch.size() - 1); + lastFromEntityId = lastRecord.getFromId(); + lastFromEntityType = lastRecord.getFromType(); + lastRelationTypeGroup = lastRecord.getRelationTypeGroup(); + lastRelationType = lastRecord.getRelationType(); + lastToEntityId = lastRecord.getToId(); + lastToEntityType = lastRecord.getToType(); + } + log.info("Finished synchronizing relations to EDQS in {} ms", (System.currentTimeMillis() - ts)); + } + + private void processRelationBatch(List relations) { + for (RelationEntity relation : relations) { + if (RelationTypeGroup.COMMON.name().equals(relation.getRelationTypeGroup()) || (RelationTypeGroup.FROM_ENTITY_GROUP.name().equals(relation.getRelationTypeGroup()))) { + EntityIdInfo entityIdInfo = entityInfoMap.get(relation.getFromId()); if (entityIdInfo != null) { - process(entityIdInfo.tenantId(), RELATION, relation); + process(entityIdInfo.tenantId(), RELATION, relation.toData()); } else { - log.info("Relation from entity not found: " + relation.getFrom()); + log.info("Relation from entity not found: " + relation.getFromId()); } } } - log.info("Finished synchronizing relations to EDQS in {} ms", (System.currentTimeMillis() - ts)); } private void loadKeyDictionary() { @@ -214,8 +239,28 @@ public abstract class EdqsSyncService { private void syncAttributes() { log.info("Synchronizing attributes to EDQS"); long ts = System.currentTimeMillis(); - var attributes = new PageDataIterable<>(attributesDao::findAll, 10000); - for (AttributeKvEntity attribute : attributes) { + + UUID lastEntityId = UUID.fromString("00000000-0000-0000-0000-000000000000"); + int lastAttributeType = Integer.MIN_VALUE; + int lastAttributeKey = Integer.MIN_VALUE; + + while (true) { + List batch = attributesDao.findNextBatch(lastEntityId, lastAttributeType, lastAttributeKey, 10000); + if (batch.isEmpty()) { + break; + } + processAttributeBatch(batch); + + AttributeKvEntity lastRecord = batch.get(batch.size() - 1); + lastEntityId = lastRecord.getId().getEntityId(); + lastAttributeType = lastRecord.getId().getAttributeType(); + lastAttributeKey = lastRecord.getId().getAttributeKey(); + } + log.info("Finished synchronizing attributes to EDQS in {} ms", (System.currentTimeMillis() - ts)); + } + + private void processAttributeBatch(List batch) { + for (AttributeKvEntity attribute : batch) { attribute.setStrKey(getStrKeyOrFetchFromDb(attribute.getId().getAttributeKey())); UUID entityId = attribute.getId().getEntityId(); EntityIdInfo entityIdInfo = entityInfoMap.get(entityId); @@ -230,13 +275,29 @@ public abstract class EdqsSyncService { attribute.getVersion()); process(entityIdInfo.tenantId(), ATTRIBUTE_KV, attributeKv); } - log.info("Finished synchronizing attributes to EDQS in {} ms", (System.currentTimeMillis() - ts)); } private void syncLatestTimeseries() { log.info("Synchronizing latest timeseries to EDQS"); long ts = System.currentTimeMillis(); - var tsKvLatestEntities = new PageDataIterable<>(pageLink -> timeseriesLatestDao.findAllLatest(pageLink), 10000); + UUID lastEntityId = UUID.fromString("00000000-0000-0000-0000-000000000000"); + int lastKey = Integer.MIN_VALUE; + + while (true) { + List batch = tsKvLatestRepository.findNextBatch(lastEntityId, lastKey, 10000); + if (batch.isEmpty()) { + break; + } + processTsKvLatestBatch(batch); + + TsKvLatestEntity lastRecord = batch.get(batch.size() - 1); + lastEntityId = lastRecord.getEntityId(); + lastKey = lastRecord.getKey(); + } + log.info("Finished synchronizing latest timeseries to EDQS in {} ms", (System.currentTimeMillis() - ts)); + } + + private void processTsKvLatestBatch(List tsKvLatestEntities) { for (TsKvLatestEntity tsKvLatestEntity : tsKvLatestEntities) { try { String strKey = getStrKeyOrFetchFromDb(tsKvLatestEntity.getKey()); @@ -256,7 +317,6 @@ public abstract class EdqsSyncService { log.error("Failed to sync latest timeseries: {}", tsKvLatestEntity, e); } } - log.info("Finished synchronizing latest timeseries to EDQS in {} ms", (System.currentTimeMillis() - ts)); } private String getStrKeyOrFetchFromDb(int key) { @@ -265,7 +325,9 @@ public abstract class EdqsSyncService { return strKey; } else { strKey = keyDictionaryDao.getKey(key); - keys.putIfAbsent(key, strKey); + if (strKey != null) { + keys.put(key, strKey); + } } return strKey; } diff --git a/application/src/main/java/org/thingsboard/server/service/edqs/KafkaEdqsSyncService.java b/application/src/main/java/org/thingsboard/server/service/edqs/KafkaEdqsSyncService.java index d51d87ed01..f4e9a02b45 100644 --- a/application/src/main/java/org/thingsboard/server/service/edqs/KafkaEdqsSyncService.java +++ b/application/src/main/java/org/thingsboard/server/service/edqs/KafkaEdqsSyncService.java @@ -40,7 +40,7 @@ public class KafkaEdqsSyncService extends EdqsSyncService { @Override public boolean isSyncNeeded() { - return kafkaAdmin.isTopicEmpty(EdqsQueue.STATE.getTopic()); + return kafkaAdmin.isTopicEmpty(EdqsQueue.EVENTS.getTopic()); } diff --git a/application/src/main/resources/thingsboard.yml b/application/src/main/resources/thingsboard.yml index 9b182ce3a7..dd0e5948c7 100644 --- a/application/src/main/resources/thingsboard.yml +++ b/application/src/main/resources/thingsboard.yml @@ -1616,11 +1616,11 @@ queue: # Kafka properties for Edge event topic edge-event: "${TB_QUEUE_KAFKA_EDGE_EVENT_TOPIC_PROPERTIES:retention.ms:2592000000;segment.bytes:52428800;retention.bytes:1048576000;partitions:1;min.insync.replicas:1}" # Kafka properties for EDQS events topics. Partitions number must be the same as queue.edqs.partitions - edqs-events: "${TB_QUEUE_KAFKA_EDQS_EVENTS_TOPIC_PROPERTIES:retention.ms:604800000;segment.bytes:52428800;retention.bytes:1048576000;partitions:12;min.insync.replicas:1}" + edqs-events: "${TB_QUEUE_KAFKA_EDQS_EVENTS_TOPIC_PROPERTIES:retention.ms:604800000;segment.bytes:52428800;retention.bytes:-1;partitions:12;min.insync.replicas:1}" # Kafka properties for EDQS requests topic (default: 3 minutes retention). Partitions number must be the same as queue.edqs.partitions edqs-requests: "${TB_QUEUE_KAFKA_EDQS_REQUESTS_TOPIC_PROPERTIES:retention.ms:180000;segment.bytes:52428800;retention.bytes:1048576000;partitions:12;min.insync.replicas:1}" # Kafka properties for EDQS state topic (infinite retention, compaction). Partitions number must be the same as queue.edqs.partitions - edqs-state: "${TB_QUEUE_KAFKA_EDQS_LATEST_EVENTS_TOPIC_PROPERTIES:retention.ms:-1;segment.bytes:52428800;retention.bytes:1048576000;partitions:12;min.insync.replicas:1;cleanup.policy:compact}" + edqs-state: "${TB_QUEUE_KAFKA_EDQS_STATE_TOPIC_PROPERTIES:retention.ms:-1;segment.bytes:52428800;retention.bytes:-1;partitions:12;min.insync.replicas:1;cleanup.policy:compact}" consumer-stats: # Prints lag between consumer group offset and last messages offset in Kafka topics enabled: "${TB_QUEUE_KAFKA_CONSUMER_STATS_ENABLED:true}" @@ -1701,6 +1701,7 @@ queue: local: rocksdb_path: "${TB_EDQS_ROCKSDB_PATH:/tmp/edqs-backup}" partitions: "${TB_EDQS_PARTITIONS:12}" + partitioning_strategy: "${TB_EDQS_PARTITIONING_STRATEGY:tenant}" # tenant or none. For 'none', each instance handles all partitions and duplicates all the data requests_topic: "${TB_EDQS_REQUESTS_TOPIC:edqs.requests}" responses_topic: "${TB_EDQS_RESPONSES_TOPIC:edqs.responses}" poll_interval: "${TB_EDQS_POLL_INTERVAL_MS:125}" 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 dc29cec3cc..04bad95be7 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 @@ -210,7 +210,6 @@ public class EntityServiceTest extends AbstractControllerTest { countByQueryAndCheck(countQuery, 0); } - @Test public void testCountHierarchicalEntitiesByQuery() throws InterruptedException { List assets = new ArrayList<>(); @@ -463,6 +462,7 @@ public class EntityServiceTest extends AbstractControllerTest { deviceService.deleteDevicesByTenantId(tenantId); } + // fails for sql implementation until we fix the issue with the relation query @Test public void testCountHierarchicalEntitiesByMultiRootQuery() throws InterruptedException { List buildings = new ArrayList<>(); @@ -1443,18 +1443,29 @@ public class EntityServiceTest extends AbstractControllerTest { String deviceName = result.getData().get(0).getLatest().get(EntityKeyType.ENTITY_FIELD).get("name").getValue(); assertThat(deviceName).isEqualTo(customerDevices.get(0).getName()); + // find by customer user with generic permission + MergedUserPermissions mergedGenericPermission = new MergedUserPermissions(Map.of(Resource.DEVICE, Set.of(Operation.READ)), Collections.emptyMap()); + PageData customerResults = findByQueryAndCheck(customerId, mergedGenericPermission, query, 1); + + String cutomerDeviceName = customerResults.getData().get(0).getLatest().get(EntityKeyType.ENTITY_FIELD).get("name").getValue(); + assertThat(cutomerDeviceName).isEqualTo(customerDevices.get(0).getName()); + + // find by customer user with group permission + MergedUserPermissions mergedGroupOnlyPermission = new MergedUserPermissions(Collections.emptyMap(), Map.of(customerDeviceGroup.getId(), new MergedGroupPermissionInfo(EntityType.DEVICE, Set.of(Operation.READ)))); + PageData result2 = findByQueryAndCheck(customerId, mergedGroupOnlyPermission, query, 1); + + String resultDeviceName2 = result2.getData().get(0).getLatest().get(EntityKeyType.ENTITY_FIELD).get("name").getValue(); + assertThat(resultDeviceName2).isEqualTo(customerDevices.get(0).getName()); + // try to find tenant device by customer user SingleEntityFilter tenantDeviceFilter = new SingleEntityFilter(); tenantDeviceFilter.setSingleEntity(tenantDevices.get(0).getId()); EntityDataQuery customerQuery2 = new EntityDataQuery(tenantDeviceFilter, pageLink, entityFields, null, null); - PageData customerResults2 = entityService.findEntityDataByQuery(tenantId, customerId, customerQuery2); - - assertEquals(0, customerResults2.getTotalElements()); + findByQueryAndCheck(customerId, mergedGenericPermission, customerQuery2, 0); // find by tenant user with group permission - PageData results3 = entityService.findEntityDataByQuery(tenantId, new CustomerId(EntityId.NULL_UUID), query); + PageData results3 = findByQueryAndCheck(new CustomerId(EntityId.NULL_UUID), mergedGroupOnlyPermission, query, 1); - assertEquals(1, results3.getTotalElements()); String deviceName3 = results3.getData().get(0).getLatest().get(EntityKeyType.ENTITY_FIELD).get("name").getValue(); assertThat(deviceName3).isEqualTo(customerDevices.get(0).getName()); } @@ -1488,16 +1499,14 @@ public class EntityServiceTest extends AbstractControllerTest { // find by customer user with generic permissions apiUsageStateService.createDefaultApiUsageState(tenantId, customerId); - PageData customerResult = entityService.findEntityDataByQuery(tenantId, customerId, query); + PageData customerResult = findByQueryAndCheck(customerId, query, 1); - assertEquals(1, customerResult.getTotalElements()); String customerResultName = customerResult.getData().get(0).getLatest().get(EntityKeyType.ENTITY_FIELD).get("name").getValue(); assertThat(customerResultName).isEqualTo(TEST_CUSTOMER_NAME); // find by tenant user with customerId filter apiUsageStateFilter.setCustomerId(customerId); - PageData tenantResult = searchEntities(query); - assertEquals(1, tenantResult.getTotalElements()); + PageData tenantResult = findByQueryAndCheck(query, 1); String tenantResultName = tenantResult.getData().get(0).getLatest().get(EntityKeyType.ENTITY_FIELD).get("name").getValue(); assertThat(tenantResultName).isEqualTo(TEST_CUSTOMER_NAME); } @@ -1625,6 +1634,231 @@ public class EntityServiceTest extends AbstractControllerTest { deviceService.deleteDevicesByTenantId(tenantId); } + @Test + public void testFindEntityDataByRelationQuery_blobEntity_customerLevel() { + final int deviceCnt = 2; + final int relationsCnt = 3; + final int blobEntitiesCnt = deviceCnt * relationsCnt; + + Customer customer = new Customer(); + customer.setTenantId(tenantId); + customer.setTitle("Customer Relation Query"); + customer = customerService.saveCustomer(customer); + + List devices = new ArrayList<>(); + for (int i = 0; i < deviceCnt; i++) { + Device device = new Device(); + device.setTenantId(tenantId); + device.setName("Device relation query " + i); + device.setCustomerId(customer.getId()); + device.setType("default"); + devices.add(deviceService.saveDevice(device)); + } + + List blobEntities = new ArrayList<>(); + for (int i = 0; i < blobEntitiesCnt; i++) { + BlobEntity blobEntity = new BlobEntity(); + blobEntity.setName("Blob relation query " + i); + blobEntity.setTenantId(tenantId); + blobEntity.setContentType("image/png"); + blobEntity.setData(ByteBuffer.allocate(1024)); + blobEntity.setCustomerId(customer.getId()); + blobEntity.setType("Report"); + blobEntities.add(blobEntityService.saveBlobEntity(blobEntity)); + } + + for (int i = 0; i < deviceCnt; i++) { + for (int j = 0; j < relationsCnt; j++) { + EntityRelation relationEntity = new EntityRelation(); + relationEntity.setFrom(devices.get(i).getId()); + relationEntity.setTo(blobEntities.get(j + (i * relationsCnt)).getId()); + relationEntity.setTypeGroup(RelationTypeGroup.COMMON); + relationEntity.setType("fileAttached"); + relationService.saveRelation(tenantId, relationEntity); + } + } + + MergedUserPermissions mergedUserPermissions = new MergedUserPermissions(Map.of(ALL, Set.of(Operation.ALL)), Collections.emptyMap()); + + RelationEntityTypeFilter relationEntityTypeFilter = new RelationEntityTypeFilter("fileAttached", Collections.singletonList(EntityType.BLOB_ENTITY)); + RelationsQueryFilter filter = new RelationsQueryFilter(); + filter.setFilters(Collections.singletonList(relationEntityTypeFilter)); + filter.setDirection(EntitySearchDirection.FROM); + EntityDataPageLink pageLink = new EntityDataPageLink(10, 0, null, null); + + for (Device device : devices) { + filter.setRootEntity(device.getId()); + + EntityDataQuery query = new EntityDataQuery(filter, pageLink, Collections.emptyList(), Collections.emptyList(), Collections.emptyList()); + findByQueryAndCheck(customer.getId(), mergedUserPermissions, query, relationsCnt); + countByQueryAndCheck(customer.getId(), mergedUserPermissions, query, relationsCnt); + /* + In order to be careful with updating Relation Query while adding new Entity Type, + this checkup will help to find place, where you could check the correctness of building query + */ + Assert.assertEquals(38, EntityType.values().length); + } + } + + @Test + public void testFindEntitiesByRelationEntityTypeFilterWithTenantGroupPermission() { + final int assetCount = 2; + final int relationsCnt = 4; + final int deviceEntitiesCnt = assetCount * relationsCnt; + + EntityGroup deviceGroup = new EntityGroup(); + deviceGroup.setName("Device Tenant Level Group"); + deviceGroup.setOwnerId(tenantId); + deviceGroup.setTenantId(tenantId); + deviceGroup.setType(EntityType.DEVICE); + deviceGroup = entityGroupService.saveEntityGroup(tenantId, tenantId, deviceGroup); + + List assets = new ArrayList<>(); + for (int i = 0; i < assetCount; i++) { + Asset building = new Asset(); + building.setTenantId(tenantId); + building.setName("Building _" + i); + building.setType("building"); + building = assetService.saveAsset(building); + assets.add(building); + } + + List devices = new ArrayList<>(); + for (int i = 0; i < deviceEntitiesCnt; i++) { + Device device = new Device(); + device.setTenantId(tenantId); + device.setName("Test device " + i); + device.setType("default"); + Device savedDevice = deviceService.saveDevice(device); + devices.add(savedDevice); + if (i % 2 == 0) { + entityGroupService.addEntityToEntityGroup(tenantId, deviceGroup.getId(), savedDevice.getId()); + } + } + + for (int i = 0; i < assetCount; i++) { + for (int j = 0; j < relationsCnt; j++) { + EntityRelation relationEntity = new EntityRelation(); + relationEntity.setFrom(assets.get(i).getId()); + relationEntity.setTo(devices.get(j + (i * relationsCnt)).getId()); + relationEntity.setTypeGroup(RelationTypeGroup.COMMON); + relationEntity.setType("contains"); + relationService.saveRelation(tenantId, relationEntity); + } + } + + MergedUserPermissions groupOnlyPermission = new MergedUserPermissions(Collections.emptyMap(), + Map.of(deviceGroup.getId(), new MergedGroupPermissionInfo(EntityType.DEVICE, Set.of(Operation.READ)))); + + RelationEntityTypeFilter relationEntityTypeFilter = new RelationEntityTypeFilter("contains", Collections.singletonList(EntityType.DEVICE)); + RelationsQueryFilter filter = new RelationsQueryFilter(); + filter.setFilters(Collections.singletonList(relationEntityTypeFilter)); + filter.setDirection(EntitySearchDirection.FROM); + EntityDataPageLink pageLink = new EntityDataPageLink(10, 0, null, null); + List keyFiltersEqualString = createStringKeyFilters("name", EntityKeyType.ENTITY_FIELD, StringOperation.STARTS_WITH, "Test device "); + + for (Asset asset : assets) { + filter.setRootEntity(asset.getId()); + + EntityDataQuery query = new EntityDataQuery(filter, pageLink, Collections.emptyList(), Collections.emptyList(), keyFiltersEqualString); + findByQueryAndCheck(new CustomerId(EntityId.NULL_UUID), groupOnlyPermission, query, relationsCnt / 2); + countByQueryAndCheck(new CustomerId(EntityId.NULL_UUID), groupOnlyPermission, query, relationsCnt / 2); + } + } + + @Test + public void testFindEntitiesWithRelationEntityTypeFilterByCustomerUser() { + Customer customer = new Customer(); + customer.setTenantId(tenantId); + customer.setTitle("Customer Relation Query"); + customer = customerService.saveCustomer(customer); + + final int assetCount = 2; + final int relationsCnt = 4; + final int deviceEntitiesCnt = assetCount * relationsCnt; + + EntityGroup deviceGroup = new EntityGroup(); + deviceGroup.setName("Device Tenant Level Group"); + deviceGroup.setOwnerId(customer.getId()); + deviceGroup.setTenantId(tenantId); + deviceGroup.setType(EntityType.DEVICE); + deviceGroup = entityGroupService.saveEntityGroup(tenantId, tenantId, deviceGroup); + + List assets = new ArrayList<>(); + for (int i = 0; i < assetCount; i++) { + Asset building = new Asset(); + building.setTenantId(tenantId); + building.setCustomerId(customer.getId()); + building.setName("Building _" + i); + building.setType("building"); + building = assetService.saveAsset(building); + assets.add(building); + } + + List devices = new ArrayList<>(); + for (int i = 0; i < deviceEntitiesCnt; i++) { + Device device = new Device(); + device.setTenantId(tenantId); + device.setCustomerId(customer.getId()); + device.setName("Test device " + i); + device.setType("default"); + Device savedDevice = deviceService.saveDevice(device); + devices.add(savedDevice); + if (i % 2 == 0) { + entityGroupService.addEntityToEntityGroup(tenantId, deviceGroup.getId(), savedDevice.getId()); + } + } + + for (int i = 0; i < assetCount; i++) { + for (int j = 0; j < relationsCnt; j++) { + EntityRelation relationEntity = new EntityRelation(); + relationEntity.setFrom(assets.get(i).getId()); + relationEntity.setTo(devices.get(j + (i * relationsCnt)).getId()); + relationEntity.setTypeGroup(RelationTypeGroup.COMMON); + relationEntity.setType("contains"); + relationService.saveRelation(tenantId, relationEntity); + } + } + + MergedUserPermissions mergedGroupOnlyPermission = new MergedUserPermissions(Collections.emptyMap(), Map.of(deviceGroup.getId(), new MergedGroupPermissionInfo(EntityType.DEVICE, Set.of(Operation.ALL)))); + MergedUserPermissions mergedGenericOnlyPermission = new MergedUserPermissions(Map.of(Resource.ALL, Set.of(Operation.ALL)), Collections.emptyMap()); + MergedUserPermissions mergedGenericAndGroupPermission = new MergedUserPermissions(Map.of(Resource.ALL, Set.of(Operation.ALL)), Map.of(deviceGroup.getId(), new MergedGroupPermissionInfo(EntityType.DEVICE, Set.of(Operation.ALL)))); + RelationEntityTypeFilter relationEntityTypeFilter = new RelationEntityTypeFilter("contains", Collections.singletonList(EntityType.DEVICE)); + RelationsQueryFilter filter = new RelationsQueryFilter(); + filter.setFilters(Collections.singletonList(relationEntityTypeFilter)); + filter.setDirection(EntitySearchDirection.FROM); + EntityDataPageLink pageLink = new EntityDataPageLink(10, 0, null, null); + List keyFiltersEqualString = createStringKeyFilters("name", EntityKeyType.ENTITY_FIELD, StringOperation.STARTS_WITH, "Test device "); + + EntityDataQuery query = new EntityDataQuery(filter, pageLink, Collections.emptyList(), Collections.emptyList(), keyFiltersEqualString); + + for (Asset asset : assets) { + filter.setRootEntity(asset.getId()); + + //check by user with generic permission + PageData relationsResult = findByQueryAndCheck(customer.getId(), mergedGenericOnlyPermission, query, relationsCnt); + countByQueryAndCheck(customer.getId(), mergedGenericOnlyPermission, query, relationsCnt); + + //check by user with generic and group permission + PageData relationsResult1 = findByQueryAndCheck(customer.getId(), mergedGenericAndGroupPermission, query, relationsCnt); + countByQueryAndCheck(customer.getId(), mergedGenericAndGroupPermission, query, relationsCnt); + + //check by other customer user with group only permission + PageData relationsResult2 = findByQueryAndCheck(otherCustomerId, mergedGroupOnlyPermission, query, relationsCnt / 2); + long relationsResultCnt2 = countByQueryAndCheck(otherCustomerId, mergedGroupOnlyPermission, query, relationsCnt / 2); + + Assert.assertEquals(relationsCnt / 2, relationsResult2.getData().size()); + Assert.assertEquals(relationsCnt / 2, relationsResultCnt2); + + //check by other customer user with generic and group only permission + PageData relationsResult3 = findByQueryAndCheck(otherCustomerId, mergedGenericAndGroupPermission, query, relationsCnt / 2); + long relationsResultCnt3 = countByQueryAndCheck(otherCustomerId, mergedGenericAndGroupPermission, query, relationsCnt / 2); + + Assert.assertEquals(relationsCnt / 2, relationsResult3.getData().size()); + Assert.assertEquals(relationsCnt / 2, relationsResultCnt3); + } + } + @Test public void testBuildNumericPredicateQueryOperations() throws ExecutionException, InterruptedException { @@ -2422,7 +2656,7 @@ public class EntityServiceTest extends AbstractControllerTest { return timeseriesService.save(tenantId, entityId, timeseries); } - private void createMultiRootHierarchy(List buildings, List apartments, + protected void createMultiRootHierarchy(List buildings, List apartments, Map> entityNameByTypeMap, Map childParentRelationMap) throws InterruptedException { for (int k = 0; k < 3; k++) { @@ -2507,7 +2741,7 @@ public class EntityServiceTest extends AbstractControllerTest { entityView.setEndTimeMs(256); entityView.setExternalId(new EntityViewId(UUID.randomUUID())); entityView.setAdditionalInfo(JacksonUtil.newObjectNode().put("test", "test")); - entityView = entityViewDao.save(tenantId, entityView); + entityView = entityViewService.saveEntityView(entityView); EntityViewTypeFilter entityViewTypeFilter = new EntityViewTypeFilter(); entityViewTypeFilter.setEntityViewNameFilter("test"); @@ -2518,21 +2752,18 @@ public class EntityServiceTest extends AbstractControllerTest { ); EntityDataQuery query = new EntityDataQuery(entityViewTypeFilter, pageLink, entityFields, Collections.emptyList(), null); - PageData relationsResult = entityService.findEntityDataByQuery(tenantId, new CustomerId(EntityId.NULL_UUID), query); - assertThat(relationsResult.getData()).hasSize(1); + PageData relationsResult = findByQueryAndCheck(new CustomerId(EntityId.NULL_UUID), query, 1); assertThat(relationsResult.getData().get(0).getLatest().get(EntityKeyType.ENTITY_FIELD).get("name").getValue()).isEqualTo(entityView.getName()); // find with non existing name entityViewTypeFilter.setEntityViewNameFilter("non-existing"); - PageData relationsResult2 = entityService.findEntityDataByQuery(tenantId, new CustomerId(EntityId.NULL_UUID), query); - assertThat(relationsResult2.getData()).hasSize(0); + findByQueryAndCheck(new CustomerId(EntityId.NULL_UUID), query, 0); // find with non existing type entityViewTypeFilter.setEntityViewNameFilter(null); entityViewTypeFilter.setEntityViewTypes(Collections.singletonList("non-existing")); - PageData relationsResult3 = entityService.findEntityDataByQuery(tenantId, new CustomerId(EntityId.NULL_UUID), query); - assertThat(relationsResult3.getData()).hasSize(0); + findByQueryAndCheck(new CustomerId(EntityId.NULL_UUID), query, 0); } private PageData findByQuery(EntityDataQuery query) { diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/edqs/fields/FieldsUtil.java b/common/data/src/main/java/org/thingsboard/server/common/data/edqs/fields/FieldsUtil.java index 5a0db392ef..f1cf51bf8b 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/edqs/fields/FieldsUtil.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/edqs/fields/FieldsUtil.java @@ -22,6 +22,7 @@ import org.thingsboard.server.common.data.Dashboard; import org.thingsboard.server.common.data.Device; import org.thingsboard.server.common.data.DeviceProfile; import org.thingsboard.server.common.data.DeviceProfileType; +import org.thingsboard.server.common.data.EntityType; import org.thingsboard.server.common.data.EntityView; import org.thingsboard.server.common.data.Tenant; import org.thingsboard.server.common.data.TenantProfile; @@ -275,6 +276,7 @@ public class FieldsUtil { return ApiUsageStateFields.builder() .id(entity.getUuidId()) .createdTime(entity.getCreatedTime()) + .customerId(entity.getEntityId().getEntityType() == EntityType.CUSTOMER ? entity.getEntityId().getId() : null) .entityId(entity.getEntityId()) .transportState(entity.getTransportState()) .dbStorageState(entity.getDbStorageState()) diff --git a/common/edqs/src/main/java/org/thingsboard/server/edqs/processor/EdqsProcessor.java b/common/edqs/src/main/java/org/thingsboard/server/edqs/processor/EdqsProcessor.java index 5383311e1f..24526205d8 100644 --- a/common/edqs/src/main/java/org/thingsboard/server/edqs/processor/EdqsProcessor.java +++ b/common/edqs/src/main/java/org/thingsboard/server/edqs/processor/EdqsProcessor.java @@ -42,10 +42,11 @@ import org.thingsboard.server.common.data.id.CustomerId; import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.common.data.page.PageData; import org.thingsboard.server.common.data.queue.QueueConfig; +import org.thingsboard.server.common.data.util.CollectionsUtil; import org.thingsboard.server.common.msg.queue.ServiceType; import org.thingsboard.server.common.msg.queue.TopicPartitionInfo; -import org.thingsboard.server.edqs.state.EdqsStateService; import org.thingsboard.server.edqs.repo.EdqRepository; +import org.thingsboard.server.edqs.state.EdqsStateService; import org.thingsboard.server.edqs.util.EdqsPartitionService; import org.thingsboard.server.edqs.util.VersionsStore; import org.thingsboard.server.gen.transport.TransportProtos; @@ -70,6 +71,7 @@ import java.util.UUID; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; import java.util.concurrent.ScheduledExecutorService; +import java.util.concurrent.atomic.AtomicInteger; import java.util.stream.Collectors; @EdqsComponent @@ -96,6 +98,8 @@ public class EdqsProcessor implements TbQueueHandler, private final VersionsStore versionsStore = new VersionsStore(); + private final AtomicInteger counter = new AtomicInteger(); // FIXME: TMP + @PostConstruct private void init() { consumersExecutor = Executors.newCachedThreadPool(ThingsBoardThreadFactory.forName("edqs-consumer")); @@ -152,15 +156,17 @@ public class EdqsProcessor implements TbQueueHandler, responseTemplate.subscribe(withTopic(partitions, config.getRequestsTopic())); Set oldPartitions = event.getOldPartitions().get(new QueueKey(ServiceType.EDQS)); - Set removedPartitions = Sets.difference(oldPartitions, newPartitions).stream() - .map(tpi -> tpi.getPartition().orElse(-1)).collect(Collectors.toSet()); - if (config.getPartitioningStrategy() != EdqsPartitioningStrategy.TENANT && !removedPartitions.isEmpty()) { - log.warn("Partitions {} were removed but shouldn't be (due to NONE partitioning strategy)", removedPartitions); + if (CollectionsUtil.isNotEmpty(oldPartitions)) { + Set removedPartitions = Sets.difference(oldPartitions, newPartitions).stream() + .map(tpi -> tpi.getPartition().orElse(-1)).collect(Collectors.toSet()); + if (config.getPartitioningStrategy() != EdqsPartitioningStrategy.TENANT && !removedPartitions.isEmpty()) { + log.warn("Partitions {} were removed but shouldn't be (due to NONE partitioning strategy)", removedPartitions); + } + repository.clearIf(tenantId -> { + Integer partition = partitionService.resolvePartition(tenantId); + return partition != null && removedPartitions.contains(partition); + }); } - repository.clearIf(tenantId -> { - Integer partition = partitionService.resolvePartition(tenantId); - return partition != null && removedPartitions.contains(partition); - }); } catch (Throwable t) { log.error("Failed to handle partition change event {}", event, t); } @@ -221,7 +227,11 @@ public class EdqsProcessor implements TbQueueHandler, } EdqsObject object = converter.deserialize(objectType, eventMsg.getData().toByteArray()); - log.info("[{}] Processing event [{}] [{}] [{}] [{}]", tenantId, objectType, eventType, key, version); + log.debug("[{}] Processing event [{}] [{}] [{}] [{}]", tenantId, objectType, eventType, key, version); + int count = counter.incrementAndGet(); + if (count % 100000 == 0) { + log.info("Processed {} events", count); + } EdqsEvent event = EdqsEvent.builder() .tenantId(tenantId) diff --git a/common/edqs/src/main/java/org/thingsboard/server/edqs/query/processor/ApiUsageStateQueryProcessor.java b/common/edqs/src/main/java/org/thingsboard/server/edqs/query/processor/ApiUsageStateQueryProcessor.java index c2821161c7..7dbda46068 100644 --- a/common/edqs/src/main/java/org/thingsboard/server/edqs/query/processor/ApiUsageStateQueryProcessor.java +++ b/common/edqs/src/main/java/org/thingsboard/server/edqs/query/processor/ApiUsageStateQueryProcessor.java @@ -76,8 +76,7 @@ public class ApiUsageStateQueryProcessor extends AbstractSingleEntityTypeQueryPr @Override protected boolean matches(EntityData ed) { ApiUsageStateFields entityFields = (ApiUsageStateFields) ed.getFields(); - return super.matches(ed) && (filter.getCustomerId() != null ? entityFields.getEntityId().equals(filter.getCustomerId()) : - entityFields.getEntityId().equals(repository.getTenantId())); + return super.matches(ed) && (filter.getCustomerId() == null || filter.getCustomerId().equals(entityFields.getEntityId())); } @Override diff --git a/common/edqs/src/main/java/org/thingsboard/server/edqs/state/KafkaEdqsStateService.java b/common/edqs/src/main/java/org/thingsboard/server/edqs/state/KafkaEdqsStateService.java index e8e8ee610e..04ef73faa1 100644 --- a/common/edqs/src/main/java/org/thingsboard/server/edqs/state/KafkaEdqsStateService.java +++ b/common/edqs/src/main/java/org/thingsboard/server/edqs/state/KafkaEdqsStateService.java @@ -71,7 +71,8 @@ public class KafkaEdqsStateService implements EdqsStateService { private ScheduledExecutorService scheduler; private final VersionsStore versionsStore = new VersionsStore(); - private final AtomicInteger restoredCount = new AtomicInteger(); + private final AtomicInteger stateReadCount = new AtomicInteger(); + private final AtomicInteger eventsReadCount = new AtomicInteger(); @PostConstruct private void init() { @@ -79,7 +80,7 @@ public class KafkaEdqsStateService implements EdqsStateService { mgmtExecutor = ThingsBoardExecutors.newWorkStealingPool(4, "edqs-backup-consumer-mgmt"); scheduler = ThingsBoardExecutors.newSingleThreadScheduledExecutor("edqs-backup-scheduler"); - stateConsumer = MainQueueConsumerManager., QueueConfig>builder() + stateConsumer = MainQueueConsumerManager., QueueConfig>builder() // FIXME Slavik: if topic is empty .queueKey(new QueueKey(ServiceType.EDQS, EdqsQueue.STATE.getTopic())) .config(QueueConfig.of(true, config.getPollInterval())) .msgPackProcessor((msgs, consumer, config) -> { @@ -88,8 +89,8 @@ public class KafkaEdqsStateService implements EdqsStateService { ToEdqsMsg msg = queueMsg.getValue(); log.trace("Processing message: {}", msg); edqsProcessor.process(msg, EdqsQueue.STATE); - if (restoredCount.incrementAndGet() % 1000 == 0) { - log.info("Processed {} msgs", restoredCount.get()); + if (stateReadCount.incrementAndGet() % 100000 == 0) { + log.info("[state] Processed {} msgs", stateReadCount.get()); } } catch (Throwable t) { log.error("Failed to process message: {}", queueMsg, t); @@ -103,7 +104,7 @@ public class KafkaEdqsStateService implements EdqsStateService { .scheduler(scheduler) .build(); - eventsConsumer = QueueConsumerManager.>builder() + eventsConsumer = QueueConsumerManager.>builder() // FIXME Slavik writes to the state while we read it, slows down the start .name("edqs-events-to-backup-consumer") .pollInterval(config.getPollInterval()) .msgPackProcessor((msgs, consumer) -> { @@ -115,6 +116,9 @@ public class KafkaEdqsStateService implements EdqsStateService { if (msg.hasEventMsg()) { EdqsEventMsg eventMsg = msg.getEventMsg(); String key = eventMsg.getKey(); + if (eventsReadCount.incrementAndGet() % 100000 == 0) { + log.info("[events-to-backup] Processed {} msgs", eventsReadCount.get()); + } if (eventMsg.hasVersion()) { if (!versionsStore.isNew(key, eventMsg.getVersion())) { return; @@ -153,14 +157,14 @@ public class KafkaEdqsStateService implements EdqsStateService { @Override public void restore(Set partitions) { - restoredCount.set(0); + stateReadCount.set(0); //TODO Slavik: do not support remote mode in monolith setup long startTs = System.currentTimeMillis(); log.info("Restore started for partitions {}", partitions.stream().map(tpi -> tpi.getPartition().orElse(-1)).sorted().toList()); stateConsumer.doUpdate(partitions); // calling blocking doUpdate instead of update stateConsumer.awaitStop(0); // consumers should stop on their own because EdqsQueue.STATE.stopWhenRead is true, we just need to wait - log.info("Restore finished in {} ms. Processed {} msgs", (System.currentTimeMillis() - startTs), restoredCount.get()); + log.info("Restore finished in {} ms. Processed {} msgs", (System.currentTimeMillis() - startTs), stateReadCount.get()); } @Override diff --git a/common/queue/src/main/java/org/thingsboard/server/queue/discovery/HashPartitionService.java b/common/queue/src/main/java/org/thingsboard/server/queue/discovery/HashPartitionService.java index 165a54b300..ea9ba3f245 100644 --- a/common/queue/src/main/java/org/thingsboard/server/queue/discovery/HashPartitionService.java +++ b/common/queue/src/main/java/org/thingsboard/server/queue/discovery/HashPartitionService.java @@ -482,7 +482,7 @@ public class HashPartitionService implements PartitionService { private Set toTpiList(QueueKey queueKey, List partitions) { if (partitions == null) { - return null; + return Collections.emptySet(); } return partitions.stream() .map(partition -> buildTopicPartitionInfo(queueKey, partition)) diff --git a/common/queue/src/main/java/org/thingsboard/server/queue/kafka/TbKafkaConsumerTemplate.java b/common/queue/src/main/java/org/thingsboard/server/queue/kafka/TbKafkaConsumerTemplate.java index 167fbb1751..47c776c742 100644 --- a/common/queue/src/main/java/org/thingsboard/server/queue/kafka/TbKafkaConsumerTemplate.java +++ b/common/queue/src/main/java/org/thingsboard/server/queue/kafka/TbKafkaConsumerTemplate.java @@ -144,10 +144,11 @@ public class TbKafkaConsumerTemplate extends AbstractTbQue stopWatch.stop(); log.trace("poll topic {} took {}ms", getTopic(), stopWatch.getTotalTimeMillis()); + List> recordList; if (records.isEmpty()) { - return Collections.emptyList(); + recordList = Collections.emptyList(); } else { - List> recordList = new ArrayList<>(256); + recordList = new ArrayList<>(256); records.forEach(record -> { recordList.add(record); if (stopWhenRead) { @@ -163,17 +164,18 @@ public class TbKafkaConsumerTemplate extends AbstractTbQue } } }); - if (endOffsets != null && endOffsets.isEmpty()) { - log.info("Reached end offsets for {}, stopping consumer", consumer.assignment()); - stop(); - } - return recordList; } + if (stopWhenRead && endOffsets.isEmpty()) { + log.info("Reached end offset for {}, stopping consumer", consumer.assignment()); + stop(); + } + return recordList; } private void onPartitionsAssigned() { if (stopWhenRead) { endOffsets = consumer.endOffsets(consumer.assignment()).entrySet().stream() + .filter(entry -> entry.getValue() > 0) .collect(Collectors.toMap(entry -> entry.getKey().partition(), Map.Entry::getValue)); } } diff --git a/common/stats/src/main/java/org/thingsboard/server/common/stats/DefaultStatsFactory.java b/common/stats/src/main/java/org/thingsboard/server/common/stats/DefaultStatsFactory.java index ca97792186..2fc7970fc9 100644 --- a/common/stats/src/main/java/org/thingsboard/server/common/stats/DefaultStatsFactory.java +++ b/common/stats/src/main/java/org/thingsboard/server/common/stats/DefaultStatsFactory.java @@ -38,7 +38,7 @@ public class DefaultStatsFactory implements StatsFactory { private static final Counter STUB_COUNTER = new StubCounter(); - @Autowired + @Autowired(required = false) // FIXME Slavik !!! private MeterRegistry meterRegistry; @Value("${metrics.enabled:false}") diff --git a/dao/src/main/java/org/thingsboard/server/dao/attributes/AttributesDao.java b/dao/src/main/java/org/thingsboard/server/dao/attributes/AttributesDao.java index 2c37788173..6678be6009 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/attributes/AttributesDao.java +++ b/dao/src/main/java/org/thingsboard/server/dao/attributes/AttributesDao.java @@ -22,14 +22,13 @@ import org.thingsboard.server.common.data.id.DeviceProfileId; import org.thingsboard.server.common.data.id.EntityId; import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.common.data.kv.AttributeKvEntry; -import org.thingsboard.server.common.data.page.PageData; -import org.thingsboard.server.common.data.page.PageLink; import org.thingsboard.server.common.data.util.TbPair; import org.thingsboard.server.dao.model.sql.AttributeKvEntity; import java.util.Collection; import java.util.List; import java.util.Optional; +import java.util.UUID; /** * @author Andrew Shvayka @@ -42,14 +41,14 @@ public interface AttributesDao { List findAll(TenantId tenantId, EntityId entityId, AttributeScope attributeScope); - PageData findAll(PageLink pageLink); - ListenableFuture save(TenantId tenantId, EntityId entityId, AttributeScope attributeScope, AttributeKvEntry attribute); List> removeAll(TenantId tenantId, EntityId entityId, AttributeScope attributeScope, List keys); List>> removeAllWithVersions(TenantId tenantId, EntityId entityId, AttributeScope attributeScope, List keys); + List findNextBatch(UUID entityId, int attributeType, int attributeKey, int batchSize); + List findAllKeysByDeviceProfileId(TenantId tenantId, DeviceProfileId deviceProfileId); List findAllKeysByEntityIds(TenantId tenantId, List entityIds); diff --git a/dao/src/main/java/org/thingsboard/server/dao/relation/RelationDao.java b/dao/src/main/java/org/thingsboard/server/dao/relation/RelationDao.java index d29b57c5c3..d668f51dbd 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/relation/RelationDao.java +++ b/dao/src/main/java/org/thingsboard/server/dao/relation/RelationDao.java @@ -18,8 +18,6 @@ package org.thingsboard.server.dao.relation; import com.google.common.util.concurrent.ListenableFuture; import org.thingsboard.server.common.data.id.EntityId; import org.thingsboard.server.common.data.id.TenantId; -import org.thingsboard.server.common.data.page.PageData; -import org.thingsboard.server.common.data.page.PageLink; import org.thingsboard.server.common.data.relation.EntityRelation; import org.thingsboard.server.common.data.relation.RelationTypeGroup; import org.thingsboard.server.common.data.rule.RuleChainType; @@ -43,8 +41,6 @@ public interface RelationDao { List findAllByToAndType(TenantId tenantId, EntityId to, String relationType, RelationTypeGroup typeGroup); - PageData findAll(PageLink pageLink); - ListenableFuture checkRelationAsync(TenantId tenantId, EntityId from, EntityId to, String relationType, RelationTypeGroup typeGroup); boolean checkRelation(TenantId tenantId, EntityId from, EntityId to, String relationType, RelationTypeGroup typeGroup); diff --git a/dao/src/main/java/org/thingsboard/server/dao/sql/attributes/AttributeKvRepository.java b/dao/src/main/java/org/thingsboard/server/dao/sql/attributes/AttributeKvRepository.java index 90f49c57fd..de5651be00 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/sql/attributes/AttributeKvRepository.java +++ b/dao/src/main/java/org/thingsboard/server/dao/sql/attributes/AttributeKvRepository.java @@ -59,4 +59,12 @@ public interface AttributeKvRepository extends JpaRepository findAllKeysByEntityIdsAndAttributeType(@Param("entityIds") List entityIds, @Param("attributeType") int attributeType); + + @Query(value = "SELECT attribute_key, attribute_type, entity_id, bool_v, dbl_v, json_v, last_update_ts, long_v, str_v, version FROM attribute_kv WHERE (entity_id, attribute_type, attribute_key) > " + + "(:entityId, :attributeType, :attributeKey) ORDER BY entity_id, attribute_type, attribute_key LIMIT :batchSize", nativeQuery = true) + List findNextBatch(@Param("entityId") UUID entityId, + @Param("attributeType") int attributeType, + @Param("attributeKey") int attributeKey, + @Param("batchSize") int batchSize); + } diff --git a/dao/src/main/java/org/thingsboard/server/dao/sql/attributes/JpaAttributeDao.java b/dao/src/main/java/org/thingsboard/server/dao/sql/attributes/JpaAttributeDao.java index b344a0ca41..db166c44b7 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/sql/attributes/JpaAttributeDao.java +++ b/dao/src/main/java/org/thingsboard/server/dao/sql/attributes/JpaAttributeDao.java @@ -23,7 +23,6 @@ import lombok.extern.slf4j.Slf4j; import org.apache.commons.lang3.tuple.Pair; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Value; -import org.springframework.data.domain.Page; import org.springframework.stereotype.Component; import org.springframework.transaction.annotation.Transactional; import org.thingsboard.server.common.data.AttributeScope; @@ -31,8 +30,6 @@ import org.thingsboard.server.common.data.id.DeviceProfileId; import org.thingsboard.server.common.data.id.EntityId; import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.common.data.kv.AttributeKvEntry; -import org.thingsboard.server.common.data.page.PageData; -import org.thingsboard.server.common.data.page.PageLink; import org.thingsboard.server.common.data.util.TbPair; import org.thingsboard.server.common.stats.StatsFactory; import org.thingsboard.server.dao.DaoUtil; @@ -51,8 +48,8 @@ import java.util.ArrayList; import java.util.Collection; import java.util.Comparator; import java.util.List; -import java.util.Map; import java.util.Optional; +import java.util.UUID; import java.util.function.Function; import java.util.stream.Collectors; @@ -157,9 +154,8 @@ public class JpaAttributeDao extends JpaAbstractDaoListeningExecutorService impl } @Override - public PageData findAll(PageLink pageLink) { - Page attributes = attributeKvRepository.findAll(DaoUtil.toPageable(pageLink)); - return DaoUtil.pageToPageData(attributes); + public List findNextBatch(UUID entityId, int attributeType, int attributeKey, int batchSize) { + return attributeKvRepository.findNextBatch(entityId, attributeType, attributeKey, batchSize); } @Override diff --git a/dao/src/main/java/org/thingsboard/server/dao/sql/query/DefaultEntityQueryRepository.java b/dao/src/main/java/org/thingsboard/server/dao/sql/query/DefaultEntityQueryRepository.java index 31443f7f47..c54b5561ed 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/sql/query/DefaultEntityQueryRepository.java +++ b/dao/src/main/java/org/thingsboard/server/dao/sql/query/DefaultEntityQueryRepository.java @@ -693,6 +693,10 @@ public class DefaultEntityQueryRepository implements EntityQueryRepository { SELECT_ADDRESS + ", " + SELECT_ADDRESS_2 + ", " + SELECT_ZIP + ", " + SELECT_PHONE + ", " + SELECT_ADDITIONAL_INFO + (entityFilter.isMultiRoot() ? (", " + SELECT_RELATED_PARENT_ID) : "") + ", entity.entity_type as entity_type"; + /* + * FIXME: + * target entities are duplicated in result list, if search direction is TO and multiple relations are references to target entity + * */ String from = getQueryTemplate(entityFilter.getDirection(), entityFilter.isMultiRoot()); if (entityFilter.isMultiRoot()) { diff --git a/dao/src/main/java/org/thingsboard/server/dao/sql/relation/JpaRelationDao.java b/dao/src/main/java/org/thingsboard/server/dao/sql/relation/JpaRelationDao.java index 1df7115518..118c3c1bcc 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/sql/relation/JpaRelationDao.java +++ b/dao/src/main/java/org/thingsboard/server/dao/sql/relation/JpaRelationDao.java @@ -24,8 +24,6 @@ import org.springframework.util.CollectionUtils; import org.thingsboard.server.common.data.id.EntityId; import org.thingsboard.server.common.data.id.EntityIdFactory; import org.thingsboard.server.common.data.id.TenantId; -import org.thingsboard.server.common.data.page.PageData; -import org.thingsboard.server.common.data.page.PageLink; import org.thingsboard.server.common.data.relation.EntityRelation; import org.thingsboard.server.common.data.relation.RelationTypeGroup; import org.thingsboard.server.common.data.rule.RuleChainType; @@ -129,10 +127,6 @@ public class JpaRelationDao extends JpaAbstractDaoListeningExecutorService imple typeGroup.name())); } - @Override - public PageData findAll(PageLink pageLink) { - return DaoUtil.toPageData(relationRepository.findAll(DaoUtil.toPageable(pageLink))); - } @Override public ListenableFuture checkRelationAsync(TenantId tenantId, EntityId from, EntityId to, String relationType, RelationTypeGroup typeGroup) { diff --git a/dao/src/main/java/org/thingsboard/server/dao/sql/relation/RelationRepository.java b/dao/src/main/java/org/thingsboard/server/dao/sql/relation/RelationRepository.java index d945f1f640..3c69ae095f 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/sql/relation/RelationRepository.java +++ b/dao/src/main/java/org/thingsboard/server/dao/sql/relation/RelationRepository.java @@ -85,6 +85,15 @@ public interface RelationRepository @Query("DELETE FROM RelationEntity r where r.fromId = :fromId and r.fromType = :fromType and r.relationTypeGroup in :relationTypeGroups") void deleteByFromIdAndFromTypeAndRelationTypeGroupIn(@Param("fromId") UUID fromId, @Param("fromType") String fromType, @Param("relationTypeGroups") List relationTypeGroups); - @Query("SELECT e FROM RelationEntity e ORDER BY e.fromId, e.fromType, e.toId, e.toType, e.relationType, e.relationTypeGroup") - Page findAll(Pageable pageable); + @Query(value = "SELECT from_id, from_type, relation_type_group, relation_type, to_id, to_type, additional_info, version FROM relation" + + " WHERE (from_id, from_type, relation_type_group, relation_type, to_id, to_type) > " + + "(:fromId, :fromType, :relationTypeGroup, :relationType, :toId, :toType) ORDER BY " + + "from_id, from_type, relation_type_group, relation_type, to_id, to_type LIMIT :batchSize", nativeQuery = true) + List findNextBatch(@Param("fromId") UUID fromId, + @Param("fromType") String fromType, + @Param("relationTypeGroup") String relationTypeGroup, + @Param("relationType") String relationType, + @Param("toId") UUID toId, + @Param("toType") String toType, + @Param("batchSize") int batchSize); } diff --git a/dao/src/main/java/org/thingsboard/server/dao/sqlts/CachedRedisSqlTimeseriesLatestDao.java b/dao/src/main/java/org/thingsboard/server/dao/sqlts/CachedRedisSqlTimeseriesLatestDao.java index 0e9cebd150..e6175409de 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/sqlts/CachedRedisSqlTimeseriesLatestDao.java +++ b/dao/src/main/java/org/thingsboard/server/dao/sqlts/CachedRedisSqlTimeseriesLatestDao.java @@ -171,9 +171,5 @@ public class CachedRedisSqlTimeseriesLatestDao extends BaseAbstractSqlTimeseries return sqlDao.findAllKeysByEntityIds(tenantId, entityIds); } - @Override - public PageData findAllLatest(PageLink pageLink) { - return null; - } } diff --git a/dao/src/main/java/org/thingsboard/server/dao/sqlts/SqlTimeseriesLatestDao.java b/dao/src/main/java/org/thingsboard/server/dao/sqlts/SqlTimeseriesLatestDao.java index fd1b180eef..dde89e107d 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/sqlts/SqlTimeseriesLatestDao.java +++ b/dao/src/main/java/org/thingsboard/server/dao/sqlts/SqlTimeseriesLatestDao.java @@ -188,10 +188,6 @@ public class SqlTimeseriesLatestDao extends BaseAbstractSqlTimeseriesDao impleme return tsKvLatestRepository.findAllKeysByEntityIds(entityIds.stream().map(EntityId::getId).collect(Collectors.toList())); } - @Override - public PageData findAllLatest(PageLink pageLink) { - return DaoUtil.pageToPageData(tsKvLatestRepository.findAll(DaoUtil.toPageable(pageLink, "entityId", "key"))); - } private ListenableFuture getNewLatestEntryFuture(TenantId tenantId, EntityId entityId, DeleteTsKvQuery query) { ListenableFuture> future = findNewLatestEntryFuture(tenantId, entityId, query); diff --git a/dao/src/main/java/org/thingsboard/server/dao/sqlts/latest/TsKvLatestRepository.java b/dao/src/main/java/org/thingsboard/server/dao/sqlts/latest/TsKvLatestRepository.java index 833ffd185e..2c97ba30ba 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/sqlts/latest/TsKvLatestRepository.java +++ b/dao/src/main/java/org/thingsboard/server/dao/sqlts/latest/TsKvLatestRepository.java @@ -20,6 +20,7 @@ import org.springframework.data.domain.Pageable; import org.springframework.data.jpa.repository.JpaRepository; import org.springframework.data.jpa.repository.Query; import org.springframework.data.repository.query.Param; +import org.thingsboard.server.dao.model.sql.AttributeKvEntity; import org.thingsboard.server.dao.model.sqlts.latest.TsKvLatestCompositeKey; import org.thingsboard.server.dao.model.sqlts.latest.TsKvLatestEntity; @@ -46,4 +47,11 @@ public interface TsKvLatestRepository extends JpaRepository findAll(Pageable pageable); + + @Query(value = "SELECT entity_id, key, ts, bool_v, str_v, long_v, dbl_v, json_v, version FROM ts_kv_latest WHERE (entity_id, key) > " + + "(:entityId, :key) ORDER BY entity_id, key LIMIT :batchSize", nativeQuery = true) + List findNextBatch(@Param("entityId") UUID entityId, + @Param("key") int key, + @Param("batchSize") int batchSize); + } diff --git a/dao/src/main/java/org/thingsboard/server/dao/timeseries/CassandraBaseTimeseriesLatestDao.java b/dao/src/main/java/org/thingsboard/server/dao/timeseries/CassandraBaseTimeseriesLatestDao.java index 99bf57a1db..6f00437672 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/timeseries/CassandraBaseTimeseriesLatestDao.java +++ b/dao/src/main/java/org/thingsboard/server/dao/timeseries/CassandraBaseTimeseriesLatestDao.java @@ -103,10 +103,6 @@ public class CassandraBaseTimeseriesLatestDao extends AbstractCassandraBaseTimes return Collections.emptyList(); } - @Override - public PageData findAllLatest(PageLink pageLink) { - return null; - } @Override public ListenableFuture saveLatest(TenantId tenantId, EntityId entityId, TsKvEntry tsKvEntry) { diff --git a/dao/src/main/java/org/thingsboard/server/dao/timeseries/TimeseriesLatestDao.java b/dao/src/main/java/org/thingsboard/server/dao/timeseries/TimeseriesLatestDao.java index 1372f2f5d8..b2d7c0eff5 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/timeseries/TimeseriesLatestDao.java +++ b/dao/src/main/java/org/thingsboard/server/dao/timeseries/TimeseriesLatestDao.java @@ -54,5 +54,4 @@ public interface TimeseriesLatestDao { List findAllKeysByEntityIds(TenantId tenantId, List entityIds); - PageData findAllLatest(PageLink pageLink); } diff --git a/edqs/src/main/java/org/thingsboard/server/edqs/ThingsboardEdqsApplication.java b/edqs/src/main/java/org/thingsboard/server/edqs/ThingsboardEdqsApplication.java index 1157f74de1..7fed88b0c3 100644 --- a/edqs/src/main/java/org/thingsboard/server/edqs/ThingsboardEdqsApplication.java +++ b/edqs/src/main/java/org/thingsboard/server/edqs/ThingsboardEdqsApplication.java @@ -30,7 +30,7 @@ import java.util.Arrays; @EnableAsync @EnableScheduling @ComponentScan({"org.thingsboard.server.edqs", "org.thingsboard.server.queue.edqs", "org.thingsboard.server.queue.discovery", "org.thingsboard.server.queue.kafka", - "org.thingsboard.server.queue.settings", "org.thingsboard.server.queue.environment"}) + "org.thingsboard.server.queue.settings", "org.thingsboard.server.queue.environment", "org.thingsboard.server.common.stats"}) @Slf4j public class ThingsboardEdqsApplication { @@ -42,8 +42,8 @@ public class ThingsboardEdqsApplication { } // @Bean - public ApplicationRunner runner(CSVLoader loader, EdqRepository edqRepository) { - return args -> { +// public ApplicationRunner runner(CSVLoader loader, EdqRepository edqRepository) { +// return args -> { // long startTs = System.currentTimeMillis(); // var loader = new TenantRepoLoader(new TenantRepo(TenantId.fromUUID(UUID.fromString("2a209df0-c7ff-11ea-a3e0-f321b0429d60")))); // loader.load(); @@ -103,8 +103,8 @@ public class ThingsboardEdqsApplication { // }); // Thread.sleep(5000); // } - }; - } +// }; +// } private static String[] updateArguments(String[] args) { if (Arrays.stream(args).noneMatch(arg -> arg.startsWith(SPRING_CONFIG_NAME_KEY))) { diff --git a/edqs/src/main/resources/edqs.yml b/edqs/src/main/resources/edqs.yml index f5c83178c3..143a42fb68 100644 --- a/edqs/src/main/resources/edqs.yml +++ b/edqs/src/main/resources/edqs.yml @@ -52,15 +52,14 @@ queue: # For debug level print-interval-ms: "${TB_QUEUE_IN_MEMORY_STATS_PRINT_INTERVAL_MS:60000}" edqs: - enabled: "${TB_EDQS_ENABLED:true}" mode: "${TB_EDQS_MODE:local}" partitions: "${TB_EDQS_PARTITIONS:12}" + partitioning_strategy: "${TB_EDQS_PARTITIONING_STRATEGY:tenant}" # tenant or none. For 'none', each instance handles all partitions and duplicates all the data requests_topic: "${TB_EDQS_REQUESTS_TOPIC:edqs.requests}" responses_topic: "${TB_EDQS_RESPONSES_TOPIC:edqs.responses}" poll_interval: "${TB_EDQS_POLL_INTERVAL_MS:125}" max_pending_requests: "${TB_EDQS_MAX_PENDING_REQUESTS:10000}" max_request_timeout: "${TB_EDQS_MAX_REQUEST_TIMEOUT:10000}" - partitioning_strategy: "${TB_EDQS_PARTITIONING_STRATEGY:tenant}" # tenant or none. For 'none', each instance handles all partitions and duplicates all the data kafka: # Kafka Bootstrap nodes in "host:port" format bootstrap.servers: "${TB_KAFKA_SERVERS:localhost:9092}" @@ -171,11 +170,11 @@ queue: # Kafka properties for Housekeeper reprocessing topic; retention.ms is set to 90 days; partitions is set to 1 since only one reprocessing service is running at a time housekeeper-reprocessing: "${TB_QUEUE_KAFKA_HOUSEKEEPER_REPROCESSING_TOPIC_PROPERTIES:retention.ms:7776000000;segment.bytes:52428800;retention.bytes:1048576000;partitions:1;min.insync.replicas:1}" # Kafka properties for EDQS events topics. Partitions number must be the same as queue.edqs.partitions - edqs-events: "${TB_QUEUE_KAFKA_EDQS_EVENTS_TOPIC_PROPERTIES:retention.ms:604800000;segment.bytes:52428800;retention.bytes:1048576000;partitions:12;min.insync.replicas:1}" + edqs-events: "${TB_QUEUE_KAFKA_EDQS_EVENTS_TOPIC_PROPERTIES:retention.ms:604800000;segment.bytes:52428800;retention.bytes:-1;partitions:12;min.insync.replicas:1}" # Kafka properties for EDQS requests topic (default: 3 minutes retention). Partitions number must be the same as queue.edqs.partitions edqs-requests: "${TB_QUEUE_KAFKA_EDQS_REQUESTS_TOPIC_PROPERTIES:retention.ms:180000;segment.bytes:52428800;retention.bytes:1048576000;partitions:12;min.insync.replicas:1}" # Kafka properties for EDQS state topic (infinite retention, compaction). Partitions number must be the same as queue.edqs.partitions - edqs-state: "${TB_QUEUE_KAFKA_EDQS_LATEST_EVENTS_TOPIC_PROPERTIES:retention.ms:-1;segment.bytes:52428800;retention.bytes:1048576000;partitions:12;min.insync.replicas:1;cleanup.policy:compact}" + edqs-state: "${TB_QUEUE_KAFKA_EDQS_STATE_TOPIC_PROPERTIES:retention.ms:-1;segment.bytes:52428800;retention.bytes:-1;partitions:12;min.insync.replicas:1;cleanup.policy:compact}" consumer-stats: # Prints lag between consumer group offset and last messages offset in Kafka topics enabled: "${TB_QUEUE_KAFKA_CONSUMER_STATS_ENABLED:true}" From ab465bf651685355c91ee00b1c40b574580b6cdb Mon Sep 17 00:00:00 2001 From: Igor Kulikov Date: Thu, 30 Jan 2025 13:17:01 +0200 Subject: [PATCH 041/345] UI: Map - implement map items draw/placement. --- .../lib/maps/data-layer/circles-data-layer.ts | 34 ++++- .../lib/maps/data-layer/map-data-layer.ts | 38 +++-- .../lib/maps/data-layer/markers-data-layer.ts | 29 +++- .../maps/data-layer/polygons-data-layer.ts | 80 ++++++++--- .../home/components/widget/lib/maps/map.scss | 15 +- .../home/components/widget/lib/maps/map.ts | 135 ++++++++++++------ .../assets/locale/locale.constant-en_US.json | 17 ++- 7 files changed, 253 insertions(+), 95 deletions(-) diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/maps/data-layer/circles-data-layer.ts b/ui-ngx/src/app/modules/home/components/widget/lib/maps/data-layer/circles-data-layer.ts index 3f5368d95c..f7580b313d 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/maps/data-layer/circles-data-layer.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/maps/data-layer/circles-data-layer.ts @@ -25,7 +25,12 @@ import { TbShapesDataLayer } from '@home/components/widget/lib/maps/data-layer/s import { TbMap } from '@home/components/widget/lib/maps/map'; import { Observable } from 'rxjs'; import { isNotEmptyStr } from '@core/utils'; -import { MapDataLayerType, TbDataLayerItem } from '@home/components/widget/lib/maps/data-layer/map-data-layer'; +import { + MapDataLayerType, + TbDataLayerItem, + UnplacedMapDataItem +} from '@home/components/widget/lib/maps/data-layer/map-data-layer'; +import { map } from 'rxjs/operators'; class TbCircleDataLayerItem extends TbDataLayerItem { @@ -134,8 +139,8 @@ class TbCircleDataLayerItem extends TbDataLayerItem { + return this.dataLayer.saveCircleCoordinates(this.data, null, null); } public isEditing() { @@ -149,7 +154,7 @@ class TbCircleDataLayerItem extends TbDataLayerItem) { @@ -178,6 +183,21 @@ export class TbCirclesDataLayer extends TbShapesDataLayer { + item.entity[this.settings.circleKey.label] = JSON.stringify(converted); + this.createItemFromUnplaced(item); + } + ); + } else { + console.warn('Unable to place item, layer is not a circle.'); + } + } + protected setupDatasource(datasource: TbMapDatasource): TbMapDatasource { datasource.dataKeys.push(this.settings.circleKey); return datasource; @@ -204,7 +224,7 @@ export class TbCirclesDataLayer extends TbShapesDataLayer, center: L.LatLng, radius: number): void { + public saveCircleCoordinates(data: FormattedData, center: L.LatLng, radius: number): Observable { const converted = center ? this.map.coordinatesToCircleData(center, radius) : null; const circleData = [ { @@ -212,6 +232,8 @@ export class TbCirclesDataLayer extends TbShapesDataLayer converted) + ); } } diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/maps/data-layer/map-data-layer.ts b/ui-ngx/src/app/modules/home/components/widget/lib/maps/data-layer/map-data-layer.ts index 00c4076d2d..393131e932 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/maps/data-layer/map-data-layer.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/maps/data-layer/map-data-layer.ts @@ -148,7 +148,9 @@ export abstract class TbDataLayerItem { - this.removeDataItem(); + this.removeDataItem().subscribe( + () => this.dataLayer.removeItem(this.data.entityId) + ); }, iconClass: 'tb-remove' }); @@ -246,7 +248,7 @@ export abstract class TbDataLayerItem; private createTooltip(datasource: TbMapDatasource) { this.tooltip = L.popup(); @@ -552,15 +554,31 @@ export abstract class TbMapDataLayer { - const item = this.layerItems.get(key); - item.remove(); - this.layerItems.delete(key); + this.removeItem(key); }); if (updatedItems.length) { this.layerItemsUpdated(updatedItems); } } + public removeItem(key: string): void { + const item = this.layerItems.get(key); + if (item) { + item.remove(); + this.layerItems.delete(key); + } + } + + protected createItemFromUnplaced(unplacedItem: UnplacedMapDataItem): void { + const index = this.unplacedItems.indexOf(unplacedItem); + if (index > -1) { + this.unplacedItems.splice(index, 1); + const layerItem = this.createLayerItem(unplacedItem.entity, this.map.getData()); + this.layerItems.set(unplacedItem.entity.entityId, layerItem); + this.map.enabledDataLayersUpdated(); + } + } + public invalidateCoordinates(): void { this.layerItems.forEach(item => item.invalidateCoordinates()); } @@ -612,9 +630,11 @@ export abstract class TbMapDataLayer): Partial; protected abstract doSetup(): Observable; 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 db037b69d9..a466d1ec3f 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 @@ -61,7 +61,7 @@ import { DomSanitizer } from '@angular/platform-browser'; import { MapDataLayerType, TbDataLayerItem, - TbMapDataLayer + TbMapDataLayer, UnplacedMapDataItem } from '@home/components/widget/lib/maps/data-layer/map-data-layer'; import { TbImageMap } from '@home/components/widget/lib/maps/image-map'; @@ -176,8 +176,8 @@ class TbMarkerDataLayerItem extends TbDataLayerItem { + return this.dataLayer.saveMarkerLocation(this.data, null); } public isEditing() { @@ -190,7 +190,7 @@ class TbMarkerDataLayerItem extends TbDataLayerItem, dsData: FormattedData[]) { @@ -452,6 +452,21 @@ export class TbMarkersDataLayer extends TbMapDataLayer { + item.entity[this.settings.xKey.label] = converted.x; + item.entity[this.settings.yKey.label] = converted.y; + this.createItemFromUnplaced(item); + } + ); + } else { + console.warn('Unable to place item, layer is not a marker.'); + } + } + protected createDataLayerContainer(): FeatureGroup { if (this.settings.markerClustering?.enable) { return this.createMarkersClusterContainer(); @@ -645,7 +660,7 @@ export class TbMarkersDataLayer extends TbMapDataLayer, position: L.LatLng): void { + public saveMarkerLocation(data: FormattedData, position: L.LatLng): Observable<{x: number; y: number}> { const converted = this.map.latLngToLocationData(position); const locationData = [ { @@ -657,7 +672,9 @@ export class TbMarkersDataLayer extends TbMapDataLayer converted) + ); } } diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/maps/data-layer/polygons-data-layer.ts b/ui-ngx/src/app/modules/home/components/widget/lib/maps/data-layer/polygons-data-layer.ts index 5157b5922c..b6ca13d555 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/maps/data-layer/polygons-data-layer.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/maps/data-layer/polygons-data-layer.ts @@ -26,7 +26,12 @@ import { TbShapesDataLayer } from '@home/components/widget/lib/maps/data-layer/s import { TbMap } from '@home/components/widget/lib/maps/map'; import { Observable } from 'rxjs'; import { isNotEmptyStr, isString } from '@core/utils'; -import { MapDataLayerType, TbDataLayerItem } from '@home/components/widget/lib/maps/data-layer/map-data-layer'; +import { + MapDataLayerType, + TbDataLayerItem, + UnplacedMapDataItem +} from '@home/components/widget/lib/maps/data-layer/map-data-layer'; +import { map } from 'rxjs/operators'; class TbPolygonDataLayerItem extends TbDataLayerItem { @@ -184,6 +189,8 @@ class TbPolygonDataLayerItem extends TbDataLayerItem { + return this.dataLayer.savePolygonCoordinates(this.data, null); } public isEditing() { @@ -206,7 +213,9 @@ class TbPolygonDataLayerItem extends TbDataLayerItem this.editing = true); - this.polygon.on('pm:markerdragend', () => this.editing = false); + this.polygon.on('pm:markerdragend', () => setTimeout(() => { + this.editing = false; + }) ); this.polygon.on('pm:edit', () => this.savePolygonCoordinates()); this.polygon.pm.enable(); const map = this.dataLayer.getMap(); @@ -230,18 +239,18 @@ class TbPolygonDataLayerItem extends TbDataLayerItem { - if (this.polygon instanceof L.Rectangle) { - this.polygonContainer.removeLayer(this.polygon); - // @ts-ignore - this.polygon = L.polygon(e.layer.getLatLngs(), { - ...this.polygonStyle, - snapIgnore: !this.dataLayer.isSnappable(), - bubblingMouseEvents: !this.dataLayer.isEditMode() - }); - this.polygon.addTo(this.polygonContainer); - } else { - // @ts-ignore - this.polygon.setLatLngs(e.layer.getLatLngs()); + if (e.layer instanceof L.Polygon) { + if (this.polygon instanceof L.Rectangle) { + this.polygonContainer.removeLayer(this.polygon); + this.polygon = L.polygon(e.layer.getLatLngs(), { + ...this.polygonStyle, + snapIgnore: !this.dataLayer.isSnappable(), + bubblingMouseEvents: !this.dataLayer.isEditMode() + }); + this.polygon.addTo(this.polygonContainer); + } else { + this.polygon.setLatLngs(e.layer.getLatLngs()); + } } // @ts-ignore e.layer._pmTempLayer = true; @@ -257,15 +266,17 @@ class TbPolygonDataLayerItem extends TbDataLayerItem { if (!e.enabled) { @@ -320,7 +331,7 @@ class TbPolygonDataLayerItem extends TbDataLayerItem) { @@ -360,6 +371,29 @@ export class TbPolygonsDataLayer extends TbShapesDataLayer { + item.entity[this.settings.polygonKey.label] = JSON.stringify(converted); + this.createItemFromUnplaced(item); + } + ); + } else { + console.warn('Unable to place item, layer is not a polygon.'); + } + } + protected setupDatasource(datasource: TbMapDatasource): TbMapDatasource { datasource.dataKeys.push(this.settings.polygonKey); return datasource; @@ -390,7 +424,7 @@ export class TbPolygonsDataLayer extends TbShapesDataLayer, coordinates: TbPolygonCoordinates): void { + public savePolygonCoordinates(data: FormattedData, coordinates: TbPolygonCoordinates): Observable { const converted = coordinates ? this.map.coordinatesToPolygonData(coordinates) : null; const polygonData = [ { @@ -398,6 +432,8 @@ export class TbPolygonsDataLayer extends TbShapesDataLayer converted) + ); } } diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/maps/map.scss b/ui-ngx/src/app/modules/home/components/widget/lib/maps/map.scss index da96bcc3e9..270732d25b 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/maps/map.scss +++ b/ui-ngx/src/app/modules/home/components/widget/lib/maps/map.scss @@ -81,7 +81,7 @@ div.tb-control-text { width: auto; background: transparent; - font-family: Roboto; + font-family: Roboto, "Helvetica Neue", sans-serif; font-size: 12px; font-style: normal; font-weight: 500; @@ -240,6 +240,19 @@ background: none; box-shadow: none; } + .tb-place-item-label { + border: none; + box-shadow: none; + border-radius: 4px; + background: rgba(0,0,0,0.56); + backdrop-filter: blur(4px); + padding: 4px 8px; + color: #fff; + font-family: Roboto, "Helvetica Neue", sans-serif; + font-size: 12px; + font-style: normal; + font-weight: 400; + } } .tb-map-sidebar { .tb-layers, .tb-groups { 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 a6b91d9008..16eb3ddd58 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 @@ -33,7 +33,7 @@ import { formattedDataFormDatasourceData, isDefinedAndNotNull, mergeDeepIgnoreAr import { DeepPartial } from '@shared/models/common'; import L from 'leaflet'; import { forkJoin, Observable, of } from 'rxjs'; -import { map, switchMap, tap } from 'rxjs/operators'; +import { switchMap, tap } from 'rxjs/operators'; import '@home/components/widget/lib/maps/leaflet/leaflet-tb'; import { MapDataLayerType, @@ -106,7 +106,11 @@ export abstract class TbMap { private tooltipInstances: TooltipInstancesData[] = []; private currentPopover: TbPopoverComponent; - private isAddingItem = false; + private currentAddButton: L.TB.ToolbarButton; + + private get isPlacingItem(): boolean { + return !!this.currentAddButton; + } protected constructor(protected ctx: WidgetContext, protected inputSettings: DeepPartial, @@ -283,7 +287,7 @@ export abstract class TbMap { }); if (this.dataLayers.some(dl => dl.isEditable())) { - this.map.pm.setGlobalOptions({ snappable: false } as L.PM.GlobalOptions); + this.map.pm.setGlobalOptions({ snappable: false }); this.map.pm.applyGlobalOptions(); } @@ -358,13 +362,14 @@ export abstract class TbMap { private placeMarker(e: MouseEvent, button: L.TB.ToolbarButton): void { this.placeItem(e, button, this.addMarkerDataLayers, (entity) => { + this.map.pm.setLang('en', { + tooltips: { + placeMarker: this.ctx.translate.instant('widgets.maps.data-layer.marker.place-marker-hint', {entityName: entity.entity.entityDisplayName}) + } + }, 'en'); this.map.pm.enableDraw('Marker'); - const hintText = this.ctx.translate.instant('widgets.maps.data-layer.marker.place-marker-hint', {entityName: entity.entity.entityDisplayName}); // @ts-ignore - this.map.pm.Draw.Marker._hintMarker.setTooltipContent(hintText); - }, - (entity, layer) => { - (entity.dataLayer as TbMarkersDataLayer).saveMarkerLocation(entity.entity, (layer as L.Marker).getLatLng()); + L.DomUtil.addClass(this.map.pm.Draw.Marker._hintMarker.getTooltip()._container, 'tb-place-item-label'); } ); } @@ -372,9 +377,15 @@ export abstract class TbMap { private drawRectangle(e: MouseEvent, button: L.TB.ToolbarButton): void { this.placeItem(e, button, this.addPolygonDataLayers, (entity) => { - - }, - (entity, layer) => { + this.map.pm.setLang('en', { + tooltips: { + firstVertex: this.ctx.translate.instant('widgets.maps.data-layer.polygon.rectangle-place-first-point-hint', {entityName: entity.entity.entityDisplayName}), + finishRect: this.ctx.translate.instant('widgets.maps.data-layer.polygon.finish-rectangle-hint', {entityName: entity.entity.entityDisplayName}) + } + }, 'en'); + this.map.pm.enableDraw('Rectangle'); + // @ts-ignore + L.DomUtil.addClass(this.map.pm.Draw.Rectangle._hintMarker.getTooltip()._container, 'tb-place-item-label'); } ); } @@ -382,9 +393,16 @@ export abstract class TbMap { private drawPolygon(e: MouseEvent, button: L.TB.ToolbarButton): void { this.placeItem(e, button, this.addPolygonDataLayers, (entity) => { - - }, - (entity, layer) => { + this.map.pm.setLang('en', { + tooltips: { + firstVertex: this.ctx.translate.instant('widgets.maps.data-layer.polygon.polygon-place-first-point-hint', {entityName: entity.entity.entityDisplayName}), + continueLine: this.ctx.translate.instant('widgets.maps.data-layer.polygon.continue-polygon-hint', {entityName: entity.entity.entityDisplayName}), + finishPoly: this.ctx.translate.instant('widgets.maps.data-layer.polygon.finish-polygon-hint', {entityName: entity.entity.entityDisplayName}) + } + }, 'en'); + this.map.pm.enableDraw('Polygon'); + // @ts-ignore + L.DomUtil.addClass(this.map.pm.Draw.Polygon._hintMarker.getTooltip()._container, 'tb-place-item-label'); } ); } @@ -392,22 +410,25 @@ export abstract class TbMap { private drawCircle(e: MouseEvent, button: L.TB.ToolbarButton): void { this.placeItem(e, button, this.addCircleDataLayers, (entity) => { - - }, - (entity, layer) => { + this.map.pm.setLang('en', { + tooltips: { + startCircle: this.ctx.translate.instant('widgets.maps.data-layer.circle.place-circle-center-hint', {entityName: entity.entity.entityDisplayName}), + finishCircle: this.ctx.translate.instant('widgets.maps.data-layer.circle.finish-circle-hint', {entityName: entity.entity.entityDisplayName}), + } + }, 'en'); + this.map.pm.enableDraw('Circle'); + // @ts-ignore + L.DomUtil.addClass(this.map.pm.Draw.Circle._hintMarker.getTooltip()._container, 'tb-place-item-label'); } ); } private placeItem(e: MouseEvent, button: L.TB.ToolbarButton, dataLayers: TbMapDataLayer[], - prepareDrawMode: (entity: UnplacedMapDataItem) => void, - saveLocation: (entity: UnplacedMapDataItem, layer: L.Layer) => void): void { - if (this.isAddingItem) { + prepareDrawMode: (entity: UnplacedMapDataItem) => void): void { + if (this.isPlacingItem) { return; } - button.setActive(true); - this.deselectItem(false, true); - this.isAddingItem = true; + this.updatePlaceItemState(button); const items = mergeUnplacedDataItemsArrays(dataLayers.filter(dl => dl.isEnabled()).map(dl => dl.getUnplacedItems())).sort((entity1, entity2) => { return entity1.entity.entityDisplayName.localeCompare(entity2.entity.entityDisplayName); }); @@ -418,23 +439,22 @@ export abstract class TbMap { this.map.off('pm:create'); this.map.pm.disableDraw(); this.dataLayers.forEach(dl => dl.enableEditMode()); - button.setActive(false); - this.isAddingItem = false; + this.updatePlaceItemState(); this.editToolbar.close(); }; this.map.once('pm:create', (e) => { - saveLocation(entity, e.layer); + entity.dataLayer.placeItem(entity, e.layer); // @ts-ignore e.layer._pmTempLayer = true; e.layer.remove(); finishAdd(); }); - this.dataLayers.forEach(dl => dl.disableEditMode()); - prepareDrawMode(entity); + this.dataLayers.forEach(dl => dl.disableEditMode()); + this.editToolbar.open([ { id: 'cancel', @@ -445,8 +465,7 @@ export abstract class TbMap { } ], false); } else { - button.setActive(false); - this.isAddingItem = false; + this.updatePlaceItemState(); } }); } @@ -478,6 +497,17 @@ export abstract class TbMap { } } + private updatePlaceItemState(addButton?: L.TB.ToolbarButton): void { + if (addButton) { + this.deselectItem(false, true); + addButton.setActive(true); + } else if (this.currentAddButton) { + this.currentAddButton.setActive(false); + } + this.currentAddButton = addButton; + this.updateAddButtonsStates(); + } + private createdControlButtonTooltip(root: HTMLElement, side: TooltipPositioningSide) { import('tooltipster').then(() => { let tooltipData = this.tooltipInstances.find(d => d.root === root); @@ -562,7 +592,7 @@ export abstract class TbMap { const mapBounds = this.map.getBounds(); if (bounds.isValid() && (!this.bounds || !this.bounds.isValid() || !this.bounds.equals(bounds) && this.settings.fitMapBounds && !mapBounds.contains(bounds))) { this.bounds = bounds; - if (!this.ignoreUpdateBounds && !this.isAddingItem) { + if (!this.ignoreUpdateBounds && !this.isPlacingItem) { this.fitBounds(bounds); } } @@ -592,17 +622,32 @@ export abstract class TbMap { } private updateAddButtonsStates() { - if (this.addMarkerButton) { - this.addMarkerButton.setDisabled(!this.addMarkerDataLayers.some(dl => dl.isEnabled() && dl.hasUnplacedItems())); - } - if (this.addRectangleButton) { - this.addRectangleButton.setDisabled(!this.addPolygonDataLayers.some(dl => dl.isEnabled() && dl.hasUnplacedItems())); - } - if (this.addPolygonButton) { - this.addPolygonButton.setDisabled(!this.addPolygonDataLayers.some(dl => dl.isEnabled() && dl.hasUnplacedItems())); - } - if (this.addCircleButton) { - this.addCircleButton.setDisabled(!this.addCircleDataLayers.some(dl => dl.isEnabled() && dl.hasUnplacedItems())); + if (this.currentAddButton) { + if (this.addMarkerButton && this.addMarkerButton !== this.currentAddButton) { + this.addMarkerButton.setDisabled(true); + } + if (this.addRectangleButton && this.addRectangleButton !== this.currentAddButton) { + this.addRectangleButton.setDisabled(true); + } + if (this.addPolygonButton && this.addPolygonButton !== this.currentAddButton) { + this.addPolygonButton.setDisabled(true); + } + if (this.addCircleButton && this.addCircleButton !== this.currentAddButton) { + this.addCircleButton.setDisabled(true); + } + } else { + if (this.addMarkerButton) { + this.addMarkerButton.setDisabled(!this.addMarkerDataLayers.some(dl => dl.isEnabled() && dl.hasUnplacedItems())); + } + if (this.addRectangleButton) { + this.addRectangleButton.setDisabled(!this.addPolygonDataLayers.some(dl => dl.isEnabled() && dl.hasUnplacedItems())); + } + if (this.addPolygonButton) { + this.addPolygonButton.setDisabled(!this.addPolygonDataLayers.some(dl => dl.isEnabled() && dl.hasUnplacedItems())); + } + if (this.addCircleButton) { + this.addCircleButton.setDisabled(!this.addCircleDataLayers.some(dl => dl.isEnabled() && dl.hasUnplacedItems())); + } } } @@ -700,7 +745,7 @@ export abstract class TbMap { } public selectItem(item: TbDataLayerItem, cancel = false, force = false): boolean { - if (this.isAddingItem) { + if (this.isPlacingItem) { return false; } let deselected = true; @@ -727,10 +772,6 @@ export abstract class TbMap { return this.selectItem(null, cancel, force); } - public getSelectedDataItem(): TbDataLayerItem { - return this.selectedDataItem; - } - public getEditToolbar(): L.TB.BottomToolbarControl { return this.editToolbar; } 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 9fa662c127..0a89ada264 100644 --- a/ui-ngx/src/assets/locale/locale.constant-en_US.json +++ b/ui-ngx/src/assets/locale/locale.constant-en_US.json @@ -7732,11 +7732,16 @@ "remove-polygon-for": "Remove polygon for '{{entityName}}'", "cut": "Cut polygon area", "rotate": "Rotate polygon", - "firstVertex-cut": "Click to place first point", - "continueLine-cut": "Click to continue drawing", - "finishPoly-cut": "Click first marker to finish and save", "draw-rectangle": "Draw rectangle", - "draw-polygon": "Draw polygon" + "draw-polygon": "Draw polygon", + "polygon-place-first-point-cut-hint": "Click to place first point", + "continue-polygon-cut-hint": "Click to continue drawing", + "finish-polygon-cut-hint": "Click first marker to finish and save", + "polygon-place-first-point-hint": "Polygon for '{{entityName}}': click to place first point", + "continue-polygon-hint": "Polygon for '{{entityName}}': click to continue drawing", + "finish-polygon-hint": "Polygon for '{{entityName}}': click first marker to finish and save", + "rectangle-place-first-point-hint": "Rectangle for '{{entityName}}': click to place first point", + "finish-rectangle-hint": "Rectangle for '{{entityName}}': click to finish and save" }, "circle": { "circle-key": "Circle key", @@ -7747,7 +7752,9 @@ "remove-circle": "Remove circle", "edit": "Edit circle", "remove-circle-for": "Remove circle for '{{entityName}}'", - "draw-circle": "Draw circle" + "draw-circle": "Draw circle", + "place-circle-center-hint": "Circle for '{{entityName}}': click to place circle center", + "finish-circle-hint": "Circle for '{{entityName}}': click to finish and save circle" }, "select-entity": "Select entity", "select-entity-hint": "Hint: after selection click at the map to set position" From 3c94c86098fb0ef1e4a66537c7b06cac58caba60 Mon Sep 17 00:00:00 2001 From: Igor Kulikov Date: Thu, 30 Jan 2025 16:48:15 +0200 Subject: [PATCH 042/345] UI: Map data layer click action --- .../json/system/widget_types/image_map.json | 4 +- .../data/json/system/widget_types/map.json | 4 +- ui-ngx/src/app/core/api/widget-api.models.ts | 2 +- .../lib/maps/data-layer/circles-data-layer.ts | 8 +-- .../lib/maps/data-layer/map-data-layer.ts | 11 ++-- .../lib/maps/data-layer/markers-data-layer.ts | 4 -- .../maps/data-layer/polygons-data-layer.ts | 4 -- .../home/components/widget/lib/maps/map.ts | 65 +++++-------------- .../widget/lib/maps/models/map.models.ts | 15 ++++- .../action-settings-button.component.html | 1 + .../map/map-data-layer-dialog.component.html | 12 ++++ .../map/map-data-layer-dialog.component.ts | 1 + .../assets/locale/locale.constant-en_US.json | 3 + 13 files changed, 63 insertions(+), 71 deletions(-) diff --git a/application/src/main/data/json/system/widget_types/image_map.json b/application/src/main/data/json/system/widget_types/image_map.json index 3330045721..27af2bc724 100644 --- a/application/src/main/data/json/system/widget_types/image_map.json +++ b/application/src/main/data/json/system/widget_types/image_map.json @@ -11,13 +11,13 @@ "resources": [], "templateHtml": "\n", "templateCss": "", - "controllerScript": "self.onInit = function() {\n self.ctx.$scope.mapWidget.onInit();\n};\n\nself.typeParameters = function() {\n return {\n hideDataTab: true,\n hideDataSettings: true,\n previewWidth: '80%',\n embedTitlePanel: true,\n datasourcesOptional: true\n };\n}\n\nself.actionSources = function() {\n return {\n markerClick: {\n name: 'widget-action.marker-click',\n multiple: false\n },\n polygonClick: {\n name: 'widget-action.polygon-click',\n multiple: false\n },\n circleClick: {\n name: 'widget-action.circle-click',\n multiple: false\n },\n tooltipAction: {\n name: 'widget-action.tooltip-tag-action',\n multiple: true\n }\n };\n}\n", + "controllerScript": "self.onInit = function() {\n self.ctx.$scope.mapWidget.onInit();\n};\n\nself.typeParameters = function() {\n return {\n hideDataTab: true,\n hideDataSettings: true,\n previewWidth: '80%',\n embedTitlePanel: true,\n datasourcesOptional: true\n };\n}\n\nself.actionSources = function() {\n return {\n tooltipAction: {\n name: 'widget-action.tooltip-tag-action',\n multiple: true\n }\n };\n}\n", "settingsForm": [], "dataKeySettingsForm": [], "settingsDirective": "tb-map-widget-settings", "hasBasicMode": true, "basicModeDirective": "tb-map-basic-config", - "defaultConfig": "{\"datasources\":[],\"timewindow\":{\"realtime\":{\"timewindowMs\":60000}},\"showTitle\":true,\"backgroundColor\":\"rgba(0, 0, 0, 0)\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"0px\",\"settings\":{\"mapType\":\"image\",\"layers\":[],\"imageSource\":{\"sourceType\":\"image\",\"url\":\"tb-image;/api/images/system/image_map_system_widget_map_image.svg\",\"entityAliasId\":null,\"entityKey\":null},\"markers\":[{\"dsType\":\"function\",\"dsLabel\":\"First point\",\"dsDeviceId\":null,\"dsEntityAliasId\":null,\"dsFilterId\":null,\"additionalDataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"temperature\",\"color\":\"#2196f3\",\"settings\":{},\"_hash\":0.8239425680406081,\"funcBody\":\"var value = prevValue + Math.random() * 40 - 20;\\nif (value < -60) {\\n\\tvalue = -60;\\n} else if (value > 60) {\\n\\tvalue = 60;\\n}\\nreturn value;\",\"aggregationType\":null,\"units\":null,\"decimals\":null,\"usePostProcessing\":null,\"postFuncBody\":null}],\"label\":{\"show\":true,\"type\":\"pattern\",\"pattern\":\"${entityName}\"},\"tooltip\":{\"show\":true,\"type\":\"pattern\",\"pattern\":\"${entityName}

X Pos: ${xPos:2}
Y Pos: ${yPos:2}
Temperature: ${temperature} °C
See tooltip settings for details\",\"patternFunction\":null,\"trigger\":\"click\",\"autoclose\":true,\"offsetX\":0,\"offsetY\":-1},\"groups\":null,\"xKey\":{\"name\":\"f(x)\",\"label\":\"latitude\",\"type\":\"function\",\"funcBody\":\"var value = prevValue || 0.2;\\nif (time % 5000 < 500) {\\n value += Math.random() * 0.05 - 0.025;\\n}\\nreturn value;\",\"settings\":{},\"color\":\"#2196f3\",\"aggregationType\":null,\"units\":null,\"decimals\":null,\"usePostProcessing\":null,\"postFuncBody\":null},\"yKey\":{\"name\":\"f(x)\",\"label\":\"longitude\",\"type\":\"function\",\"funcBody\":\"var value = prevValue || 0.3;\\nif (time % 5000 < 500) {\\n value += Math.random() * 0.05 - 0.025;\\n}\\nreturn value;\",\"settings\":{},\"color\":\"#2196f3\",\"aggregationType\":null,\"units\":null,\"decimals\":null,\"usePostProcessing\":null,\"postFuncBody\":null},\"markerType\":\"shape\",\"markerShape\":{\"shape\":\"markerShape1\",\"size\":34,\"color\":{\"type\":\"function\",\"color\":\"#307FE5\",\"colorFunction\":\"var temperature = data.temperature;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\\n\"}},\"markerIcon\":{\"icon\":\"mdi:lightbulb-on\",\"size\":34,\"color\":{\"type\":\"constant\",\"color\":\"#307FE5\"}},\"markerImage\":{\"type\":\"image\",\"image\":\"data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9Ii0xOTEuMzUgLTM1MS4xOCAxMDgzLjU4IDE3MzAuNDYiPjxwYXRoIGZpbGwtcnVsZT0iZXZlbm9kZCIgY2xpcC1ydWxlPSJldmVub2RkIiBmaWxsPSIjZmU3NTY5IiBzdHJva2U9IiMwMDAiIHN0cm9rZS13aWR0aD0iMzciIHN0cm9rZS1taXRlcmxpbWl0PSIxMCIgZD0iTTM1MS44MzMgMTM2MC43OGMtMzguNzY2LTE5MC4zLTEwNy4xMTYtMzQ4LjY2NS0xODkuOTAzLTQ5NS40NEMxMDAuNTIzIDc1Ni40NjkgMjkuMzg2IDY1NS45NzgtMzYuNDM0IDU1MC40MDRjLTIxLjk3Mi0zNS4yNDQtNDAuOTM0LTcyLjQ3Ny02Mi4wNDctMTA5LjA1NC00Mi4yMTYtNzMuMTM3LTc2LjQ0NC0xNTcuOTM1LTc0LjI2OS0yNjcuOTMyIDIuMTI1LTEwNy40NzMgMzMuMjA4LTE5My42ODUgNzguMDMtMjY0LjE3M0MtMjEtMjA2LjY5IDEwMi40ODEtMzAxLjc0NSAyNjguMTY0LTMyNi43MjRjMTM1LjQ2Ni0yMC40MjUgMjYyLjQ3NSAxNC4wODIgMzUyLjU0MyA2Ni43NDcgNzMuNiA0My4wMzggMTMwLjU5NiAxMDAuNTI4IDE3My45MiAxNjguMjggNDUuMjIgNzAuNzE2IDc2LjM2IDE1NC4yNiA3OC45NzEgMjYzLjIzMyAxLjMzNyA1NS44My03LjgwNSAxMDcuNTMyLTIwLjY4NCAxNTAuNDE3LTEzLjAzNCA0My40MS0zMy45OTYgNzkuNjk1LTUyLjY0NiAxMTguNDU1LTM2LjQwNiA3NS42NTktODIuMDQ5IDE0NC45ODEtMTI3Ljg1NSAyMTQuMzQ1LTEzNi40MzcgMjA2LjYwNi0yNjQuNDk2IDQxNy4zMS0zMjAuNTggNzA2LjAyOHoiLz48Y2lyY2xlIGZpbGwtcnVsZT0iZXZlbm9kZCIgY2xpcC1ydWxlPSJldmVub2RkIiBjeD0iMzUyLjg5MSIgY3k9IjIyNS43NzkiIHI9IjE4My4zMzIiLz48L3N2Zz4=\",\"imageSize\":34},\"markerOffsetX\":0.5,\"markerOffsetY\":1,\"positionFunction\":\"return {x: origXPos, y: origYPos};\",\"markerClustering\":{\"enable\":false,\"zoomOnClick\":true,\"maxZoom\":null,\"maxClusterRadius\":80,\"zoomAnimation\":true,\"showCoverageOnHover\":true,\"spiderfyOnMaxZoom\":false,\"chunkedLoad\":false,\"lazyLoad\":true,\"useClusterMarkerColorFunction\":false,\"clusterMarkerColorFunction\":null}},{\"dsType\":\"function\",\"dsLabel\":\"Second point\",\"dsDeviceId\":null,\"dsEntityAliasId\":null,\"dsFilterId\":null,\"additionalDataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"temperature\",\"color\":\"#2196f3\",\"settings\":{},\"_hash\":0.7826299113906372,\"funcBody\":\"var value = prevValue + Math.random() * 40 - 20;\\nif (value < -60) {\\n\\tvalue = -60;\\n} else if (value > 60) {\\n\\tvalue = 60;\\n}\\nreturn value;\",\"aggregationType\":null,\"units\":null,\"decimals\":null,\"usePostProcessing\":null,\"postFuncBody\":null}],\"label\":{\"show\":true,\"type\":\"pattern\",\"pattern\":\"${entityName}\"},\"tooltip\":{\"show\":true,\"type\":\"pattern\",\"pattern\":\"${entityName}

X Pos: ${xPos:2}
Y Pos: ${yPos:2}
Temperature: ${temperature} °C
See tooltip settings for details\",\"patternFunction\":null,\"trigger\":\"click\",\"autoclose\":true,\"offsetX\":0,\"offsetY\":-1},\"groups\":null,\"xKey\":{\"name\":\"f(x)\",\"label\":\"latitude\",\"type\":\"function\",\"funcBody\":\"var value = prevValue || 0.6;\\nif (time % 4000 < 500) {\\n value += Math.random() * 0.05 - 0.025;\\n}\\nreturn value;\",\"settings\":{},\"color\":\"#2196f3\",\"aggregationType\":null,\"units\":null,\"decimals\":null,\"usePostProcessing\":null,\"postFuncBody\":null},\"yKey\":{\"name\":\"f(x)\",\"label\":\"longitude\",\"type\":\"function\",\"funcBody\":\"var value = prevValue || 0.7;\\nif (time % 4000 < 500) {\\n value += Math.random() * 0.05 - 0.025;\\n}\\nreturn value;\",\"settings\":{},\"color\":\"#2196f3\",\"aggregationType\":null,\"units\":null,\"decimals\":null,\"usePostProcessing\":null,\"postFuncBody\":null},\"markerType\":\"image\",\"markerShape\":{\"shape\":\"markerShape1\",\"size\":34,\"color\":{\"type\":\"constant\",\"color\":\"#307FE5\"}},\"markerIcon\":{\"icon\":\"\",\"size\":34,\"color\":{\"type\":\"constant\",\"color\":\"#307FE5\"}},\"markerImage\":{\"type\":\"function\",\"image\":\"data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSIzNCIgaGVpZ2h0PSIzNCIgdmlld0JveD0iMCAwIDM0IDM0IiBmaWxsPSJub25lIj4KICA8ZyBmaWx0ZXI9InVybCgjZmlsdGVyMF9iZl84ODE2XzI2Mzg4NykiPgogICAgPHBhdGggZD0iTTE5IDI0LjVDMTcuNDA3NSAyNy40MTI1IDE3IDMzIDE3IDMzQzE3IDMzIDI3LjA4NTggMzIuMTk1NSAzMC45OTkyIDI3LjQ5OThDMzQgMjMuODk5MiAzMS45OTkyIDE5IDI3Ljk5OTIgMTlDMjMuOTk5MyAxOSAyMS4xOTI5IDIwLjQ4OTQgMTkgMjQuNVoiIGZpbGw9ImJsYWNrIiBmaWxsLW9wYWNpdHk9IjAuMjQiLz4KICA8L2c+CiAgPG1hc2sgaWQ9InBhdGgtMi1pbnNpZGUtMV84ODE2XzI2Mzg4NyIgZmlsbD0id2hpdGUiPgogICAgPHBhdGggZmlsbC1ydWxlPSJldmVub2RkIiBjbGlwLXJ1bGU9ImV2ZW5vZGQiIGQ9Ik0yOCAxMS45QzI4LjAwMzcgNS4zMjc2MSAyNC4yOTAyIDAgMTcgMEM5LjcwOTgzIDAgNS45OTYzIDUuMzI3NjEgNiAxMS45QzYuMDA0NzMgMjAuMjkzNyAxNyAzNCAxNyAzNEMxNyAzNCAyNy45OTUzIDIwLjI5MzcgMjggMTEuOVpNMjEuMjUgMTAuNjI1QzIxLjI1IDEyLjk3MjIgMTkuMzQ3MiAxNC44NzUgMTcgMTQuODc1QzE0LjY1MjggMTQuODc1IDEyLjc1IDEyLjk3MjIgMTIuNzUgMTAuNjI1QzEyLjc1IDguMjc3NzkgMTQuNjUyOCA2LjM3NSAxNyA2LjM3NUMxOS4zNDcyIDYuMzc1IDIxLjI1IDguMjc3NzkgMjEuMjUgMTAuNjI1WiIvPgogIDwvbWFzaz4KICA8cGF0aCBmaWxsLXJ1bGU9ImV2ZW5vZGQiIGNsaXAtcnVsZT0iZXZlbm9kZCIgZD0iTTI4IDExLjlDMjguMDAzNyA1LjMyNzYxIDI0LjI5MDIgMCAxNyAwQzkuNzA5ODMgMCA1Ljk5NjMgNS4zMjc2MSA2IDExLjlDNi4wMDQ3MyAyMC4yOTM3IDE3IDM0IDE3IDM0QzE3IDM0IDI3Ljk5NTMgMjAuMjkzNyAyOCAxMS45Wk0yMS4yNSAxMC42MjVDMjEuMjUgMTIuOTcyMiAxOS4zNDcyIDE0Ljg3NSAxNyAxNC44NzVDMTQuNjUyOCAxNC44NzUgMTIuNzUgMTIuOTcyMiAxMi43NSAxMC42MjVDMTIuNzUgOC4yNzc3OSAxNC42NTI4IDYuMzc1IDE3IDYuMzc1QzE5LjM0NzIgNi4zNzUgMjEuMjUgOC4yNzc3OSAyMS4yNSAxMC42MjVaIiBmaWxsPSIjMzA3ZmU1Ii8+CiAgPHBhdGggZD0iTTI4IDExLjlMMjkuMDYyNSAxMS45MDA2TDI4IDExLjlaTTYgMTEuOUw3LjA2MjUgMTEuODk5NEw2IDExLjlaTTE3IDM0TDE2LjE3MTIgMzQuNjY0OUwxNyAzNS42OThMMTcuODI4OCAzNC42NjQ5TDE3IDM0Wk0xNyAxLjA2MjVDMjAuMzY0MSAxLjA2MjUgMjIuODA4NSAyLjI4MDA2IDI0LjQyNzMgNC4xNzUzOUMyNi4wNjUyIDYuMDkzMjMgMjYuOTM5MiA4LjgwMzMxIDI2LjkzNzUgMTEuODk5NEwyOS4wNjI1IDExLjkwMDZDMjkuMDY0NCA4LjQyNDMgMjguMDgzNSA1LjE4NDM4IDI2LjA0MzEgMi43OTUzMkMyMy45ODM1IDAuMzgzNzQyIDIwLjkyNjEgLTEuMDYyNSAxNyAtMS4wNjI1VjEuMDYyNVpNNy4wNjI1IDExLjg5OTRDNy4wNjA3NiA4LjgwMzMxIDcuOTM0NzcgNi4wOTMyMyA5LjU3Mjc0IDQuMTc1MzlDMTEuMTkxNSAyLjI4MDA2IDEzLjYzNTkgMS4wNjI1IDE3IDEuMDYyNVYtMS4wNjI1QzEzLjA3MzkgLTEuMDYyNSAxMC4wMTY1IDAuMzgzNzQxIDcuOTU2ODYgMi43OTUzMkM1LjkxNjQ1IDUuMTg0MzggNC45MzU1NSA4LjQyNDMgNC45Mzc1IDExLjkwMDZMNy4wNjI1IDExLjg5OTRaTTE3IDM0QzE3LjgyODggMzMuMzM1MSAxNy44Mjg4IDMzLjMzNTIgMTcuODI4OCAzMy4zMzUyQzE3LjgyODggMzMuMzM1MiAxNy44Mjg4IDMzLjMzNTEgMTcuODI4NyAzMy4zMzVDMTcuODI4NSAzMy4zMzQ4IDE3LjgyODEgMzMuMzM0MyAxNy44Mjc2IDMzLjMzMzZDMTcuODI2NSAzMy4zMzIzIDE3LjgyNDggMzMuMzMwMSAxNy44MjI0IDMzLjMyNzFDMTcuODE3NiAzMy4zMjEyIDE3LjgxMDMgMzMuMzEyIDE3LjgwMDQgMzMuMjk5NUMxNy43ODA3IDMzLjI3NDcgMTcuNzUwOSAzMy4yMzcxIDE3LjcxMTggMzMuMTg3NEMxNy42MzM1IDMzLjA4NzggMTcuNTE3OCAzMi45Mzk1IDE3LjM3IDMyLjc0NzJDMTcuMDc0MiAzMi4zNjI0IDE2LjY1MDMgMzEuODAxNyAxNi4xNDA5IDMxLjEwMTlDMTUuMTIxNCAyOS43MDE0IDEzLjc2MzQgMjcuNzQ5NSAxMi40MDcxIDI1LjU0MTVDMTEuMDQ5IDIzLjMzMDYgOS43MDM4OCAyMC44ODEzIDguNzAwOTEgMTguNDg0MUM3LjY5MTA5IDE2LjA3MDYgNy4wNjM1NyAxMy43OTIxIDcuMDYyNSAxMS44OTk0TDQuOTM3NSAxMS45MDA2QzQuOTM4OCAxNC4yMDQ4IDUuNjg3NDYgMTYuNzg3MyA2Ljc0MDU4IDE5LjMwNDNDNy44MDA1NSAyMS44Mzc4IDkuMjA1MTYgMjQuMzg4OSAxMC41OTY0IDI2LjY1MzhDMTEuOTg5NSAyOC45MjE2IDEzLjM4MDcgMzAuOTIwOSAxNC40MjI5IDMyLjM1MjZDMTQuOTQ0NCAzMy4wNjg5IDE1LjM3OTUgMzMuNjQ0NiAxNS42ODUxIDM0LjA0MjJDMTUuODM3OSAzNC4yNDEgMTUuOTU4NCAzNC4zOTU0IDE2LjA0MTIgMzQuNTAwN0MxNi4wODI2IDM0LjU1MzQgMTYuMTE0NiAzNC41OTM4IDE2LjEzNjUgMzQuNjIxM0MxNi4xNDc0IDM0LjYzNTEgMTYuMTU1OSAzNC42NDU2IDE2LjE2MTcgMzQuNjUyOUMxNi4xNjQ2IDM0LjY1NjYgMTYuMTY2OCAzNC42NTk0IDE2LjE2ODQgMzQuNjYxNEMxNi4xNjkyIDM0LjY2MjQgMTYuMTY5OSAzNC42NjMyIDE2LjE3MDMgMzQuNjYzN0MxNi4xNzA2IDM0LjY2NCAxNi4xNzA4IDM0LjY2NDMgMTYuMTcwOSAzNC42NjQ1QzE2LjE3MTEgMzQuNjY0NyAxNi4xNzEyIDM0LjY2NDkgMTcgMzRaTTI2LjkzNzUgMTEuODk5NEMyNi45MzY0IDEzLjc5MjEgMjYuMzA4OSAxNi4wNzA2IDI1LjI5OTEgMTguNDg0MUMyNC4yOTYxIDIwLjg4MTMgMjIuOTUxIDIzLjMzMDYgMjEuNTkyOSAyNS41NDE1QzIwLjIzNjYgMjcuNzQ5NSAxOC44Nzg2IDI5LjcwMTQgMTcuODU5MSAzMS4xMDE5QzE3LjM0OTcgMzEuODAxNyAxNi45MjU4IDMyLjM2MjQgMTYuNjMgMzIuNzQ3MkMxNi40ODIyIDMyLjkzOTUgMTYuMzY2NSAzMy4wODc4IDE2LjI4ODIgMzMuMTg3NEMxNi4yNDkxIDMzLjIzNzEgMTYuMjE5MyAzMy4yNzQ3IDE2LjE5OTYgMzMuMjk5NUMxNi4xODk3IDMzLjMxMiAxNi4xODI0IDMzLjMyMTIgMTYuMTc3NiAzMy4zMjcxQzE2LjE3NTIgMzMuMzMwMSAxNi4xNzM1IDMzLjMzMjMgMTYuMTcyNCAzMy4zMzM2QzE2LjE3MTkgMzMuMzM0MyAxNi4xNzE1IDMzLjMzNDggMTYuMTcxMyAzMy4zMzVDMTYuMTcxMiAzMy4zMzUxIDE2LjE3MTIgMzMuMzM1MiAxNi4xNzEyIDMzLjMzNTJDMTYuMTcxMiAzMy4zMzUyIDE2LjE3MTIgMzMuMzM1MSAxNyAzNEMxNy44Mjg4IDM0LjY2NDkgMTcuODI4OSAzNC42NjQ3IDE3LjgyOTEgMzQuNjY0NUMxNy44MjkyIDM0LjY2NDMgMTcuODI5NCAzNC42NjQgMTcuODI5NyAzNC42NjM3QzE3LjgzMDEgMzQuNjYzMiAxNy44MzA4IDM0LjY2MjQgMTcuODMxNiAzNC42NjE0QzE3LjgzMzIgMzQuNjU5NCAxNy44MzU0IDM0LjY1NjYgMTcuODM4MyAzNC42NTI5QzE3Ljg0NDEgMzQuNjQ1NiAxNy44NTI2IDM0LjYzNTEgMTcuODYzNSAzNC42MjEzQzE3Ljg4NTQgMzQuNTkzOCAxNy45MTc0IDM0LjU1MzQgMTcuOTU4OCAzNC41MDA3QzE4LjA0MTYgMzQuMzk1NCAxOC4xNjIxIDM0LjI0MSAxOC4zMTQ5IDM0LjA0MjJDMTguNjIwNSAzMy42NDQ2IDE5LjA1NTYgMzMuMDY4OSAxOS41NzcxIDMyLjM1MjZDMjAuNjE5MyAzMC45MjA5IDIyLjAxMDUgMjguOTIxNiAyMy40MDM2IDI2LjY1MzhDMjQuNzk0OCAyNC4zODg5IDI2LjE5OTUgMjEuODM3OCAyNy4yNTk0IDE5LjMwNDNDMjguMzEyNSAxNi43ODczIDI5LjA2MTIgMTQuMjA0OCAyOS4wNjI1IDExLjkwMDZMMjYuOTM3NSAxMS44OTk0Wk0xNyAxNS45Mzc1QzE5LjkzNCAxNS45Mzc1IDIyLjMxMjUgMTMuNTU5IDIyLjMxMjUgMTAuNjI1SDIwLjE4NzVDMjAuMTg3NSAxMi4zODU0IDE4Ljc2MDQgMTMuODEyNSAxNyAxMy44MTI1VjE1LjkzNzVaTTExLjY4NzUgIDEwLjYyNUMxMS42ODc1IDEzLjU1OSAxNC4wNjYgMTUuOTM3NSAxNyAxNS45Mzc1VjEzLjgxMjVDMTUuMjM5NiAxMy44MTI1IDEzLjgxMjUgMTIuMzg1NCAxMy44MTI1IDEwLjYyNUgxMS42ODc1Wk0xNyA1LjMxMjVDMTQuMDY2IDUuMzEyNSAxMS42ODc1IDcuNjkwOTkgMTEuNjg3NSAxMC42MjVIMTMuODEyNUMxMy44MTI1IDguODY0NTkgMTUuMjM5NiA3LjQzNzUgMTcgNy40Mzc1VjUuMzEyNVpNMjIuMzEyNSAxMC42MjVDMjIuMzEyNSA3LjY5MDk5IDE5LjkzNCA1LjMxMjUgMTcgNS4zMTI1VjcuNDM3NUMxOC43NjA0IDcuNDM3NSAyMC4xODc1IDguODY0NTkgMjAuMTg3NSAxMC42MjVIMjIuMzEyNVoiIGZpbGw9ImJsYWNrIiBmaWxsLW9wYWNpdHk9IjAuMzgiIG1hc2s9InVybCgjcGF0aC0yLWluc2lkZS0xXzg4MTZfMjYzODg3KSIvPgogIDxkZWZzPgogICAgPGZpbHRlciBpZD0iZmlsdGVyMF9iZl84ODE2XzI2Mzg4NyIgeD0iMTIuNzUiIHk9IjE0Ljc1IiB3aWR0aD0iMjMuOTQzNCIgaGVpZ2h0PSIyMi41IiBmaWx0ZXJVbml0cz0idXNlclNwYWNlT25Vc2UiIGNvbG9yLWludGVycG9sYXRpb24tZmlsdGVycz0ic1JHQiI+CiAgICAgIDxmZUZsb29kIGZsb29kLW9wYWNpdHk9IjAiIHJlc3VsdD0iQmFja2dyb3VuZEltYWdlRml4Ii8+CiAgICAgIDxmZUdhdXNzaWFuQmx1ciBpbj0iQmFja2dyb3VuZEltYWdlRml4IiBzdGREZXZpYXRpb249IjIuMTI1Ii8+CiAgICAgIDxmZUNvbXBvc2l0ZSBpbjI9IlNvdXJjZUFscGhhIiBvcGVyYXRvcj0iaW4iIHJlc3VsdD0iZWZmZWN0MV9iYWNrZ3JvdW5kQmx1cl84ODE2XzI2Mzg4NyIvPgogICAgICA8ZmVCbGVuZCBtb2RlPSJub3JtYWwiIGluPSJTb3VyY2VHcmFwaGljIiBpbjI9ImVmZmVjdDFfYmFja2dyb3VuZEJsdXJfODgxNl8yNjM4ODciIHJlc3VsdD0ic2hhcGUiLz4KICAgICAgPGZlR2F1c3NpYW5CbHVyIHN0ZERldmlhdGlvbj0iMC41IiByZXN1bHQ9ImVmZmVjdDJfZm9yZWdyb3VuZEJsdXJfODgxNl8yNjM4ODciLz4KICAgIDwvZmlsdGVyPgogIDwvZGVmcz4KPC9zdmc+\",\"imageSize\":34,\"imageFunction\":\"var res = {\\n url: images[0],\\n size: 40\\n}\\nvar temperature = data.temperature;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120;\\n var index = Math.min(3, Math.floor(4 * percent));\\n res.url = images[index];\\n}\\nreturn res;\\n\",\"images\":[\"tb-image;/api/images/system/map_marker_image_0.png\",\"tb-image;/api/images/system/map_marker_image_1.png\",\"tb-image;/api/images/system/map_marker_image_2.png\",\"tb-image;/api/images/system/map_marker_image_3.png\"]},\"markerOffsetX\":0.5,\"markerOffsetY\":1,\"positionFunction\":\"return {x: origXPos, y: origYPos};\",\"markerClustering\":{\"enable\":false,\"zoomOnClick\":true,\"maxZoom\":null,\"maxClusterRadius\":80,\"zoomAnimation\":true,\"showCoverageOnHover\":true,\"spiderfyOnMaxZoom\":false,\"chunkedLoad\":false,\"lazyLoad\":true,\"useClusterMarkerColorFunction\":false,\"clusterMarkerColorFunction\":null}}],\"polygons\":[],\"circles\":[],\"additionalDataSources\":[],\"controlsPosition\":\"topleft\",\"zoomActions\":[\"scroll\",\"doubleClick\",\"controlButtons\"],\"fitMapBounds\":true,\"useDefaultCenterPosition\":false,\"defaultCenterPosition\":\"0,0\",\"defaultZoomLevel\":null,\"mapPageSize\":16384,\"background\":{\"type\":\"color\",\"color\":\"#fff\",\"overlay\":{\"enabled\":false,\"color\":\"rgba(255,255,255,0.72)\",\"blur\":3}},\"padding\":\"8px\"},\"title\":\"Image Map\",\"useDashboardTimewindow\":true,\"displayTimewindow\":true,\"showTitleIcon\":false,\"titleTooltip\":\"\",\"dropShadow\":true,\"enableFullscreen\":true,\"widgetStyle\":{},\"widgetCss\":\"\",\"titleStyle\":{\"fontSize\":\"16px\",\"fontWeight\":400},\"pageSize\":1024,\"noDataDisplayMessage\":\"\",\"configMode\":\"basic\",\"titleFont\":null,\"titleColor\":null,\"margin\":\"0px\",\"borderRadius\":\"0px\",\"iconSize\":\"24px\",\"titleIcon\":\"map\",\"iconColor\":\"#1F6BDD\",\"actions\":{\"tooltipAction\":[{\"name\":\"testTag\",\"icon\":\"more_horiz\",\"useShowWidgetActionFunction\":null,\"showWidgetActionFunction\":\"return true;\",\"type\":\"custom\",\"customFunction\":\"console.log('It works!!!');\\n\\nconsole.log(entityName);\\n\\nconsole.log(additionalParams);\",\"openInSeparateDialog\":false,\"openInPopover\":false,\"id\":\"f9b4925a-818c-15d2-6220-cf2f317bc7fe\"}]}}" + "defaultConfig": "{\"datasources\":[],\"timewindow\":{\"realtime\":{\"timewindowMs\":60000}},\"showTitle\":true,\"backgroundColor\":\"rgba(0, 0, 0, 0)\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"0px\",\"settings\":{\"mapType\":\"image\",\"layers\":[],\"markers\":[{\"dsType\":\"function\",\"dsLabel\":\"First point\",\"dsDeviceId\":null,\"dsEntityAliasId\":null,\"dsFilterId\":null,\"additionalDataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"temperature\",\"color\":\"#2196f3\",\"settings\":{},\"_hash\":0.8239425680406081,\"funcBody\":\"var value = prevValue + Math.random() * 40 - 20;\\nif (value < -60) {\\n\\tvalue = -60;\\n} else if (value > 60) {\\n\\tvalue = 60;\\n}\\nreturn value;\",\"aggregationType\":null,\"units\":null,\"decimals\":null,\"usePostProcessing\":null,\"postFuncBody\":null}],\"label\":{\"show\":true,\"type\":\"pattern\",\"pattern\":\"${entityName}\"},\"tooltip\":{\"show\":true,\"type\":\"pattern\",\"pattern\":\"${entityName}

X Pos: ${xPos:2}
Y Pos: ${yPos:2}
Temperature: ${temperature} °C
See tooltip settings for details\",\"patternFunction\":null,\"trigger\":\"click\",\"autoclose\":true,\"offsetX\":0,\"offsetY\":-1},\"groups\":null,\"xKey\":{\"name\":\"f(x)\",\"label\":\"latitude\",\"type\":\"function\",\"funcBody\":\"var value = prevValue || 0.2;\\nif (time % 5000 < 500) {\\n value += Math.random() * 0.05 - 0.025;\\n}\\nreturn value;\",\"settings\":{},\"color\":\"#2196f3\",\"aggregationType\":null,\"units\":null,\"decimals\":null,\"usePostProcessing\":null,\"postFuncBody\":null},\"yKey\":{\"name\":\"f(x)\",\"label\":\"longitude\",\"type\":\"function\",\"funcBody\":\"var value = prevValue || 0.3;\\nif (time % 5000 < 500) {\\n value += Math.random() * 0.05 - 0.025;\\n}\\nreturn value;\",\"settings\":{},\"color\":\"#2196f3\",\"aggregationType\":null,\"units\":null,\"decimals\":null,\"usePostProcessing\":null,\"postFuncBody\":null},\"markerType\":\"shape\",\"markerShape\":{\"shape\":\"markerShape1\",\"size\":34,\"color\":{\"type\":\"function\",\"color\":\"#307FE5\",\"colorFunction\":\"var temperature = data.temperature;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\\n\"}},\"markerIcon\":{\"icon\":\"mdi:lightbulb-on\",\"size\":34,\"color\":{\"type\":\"constant\",\"color\":\"#307FE5\"}},\"markerImage\":{\"type\":\"image\",\"image\":\"data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9Ii0xOTEuMzUgLTM1MS4xOCAxMDgzLjU4IDE3MzAuNDYiPjxwYXRoIGZpbGwtcnVsZT0iZXZlbm9kZCIgY2xpcC1ydWxlPSJldmVub2RkIiBmaWxsPSIjZmU3NTY5IiBzdHJva2U9IiMwMDAiIHN0cm9rZS13aWR0aD0iMzciIHN0cm9rZS1taXRlcmxpbWl0PSIxMCIgZD0iTTM1MS44MzMgMTM2MC43OGMtMzguNzY2LTE5MC4zLTEwNy4xMTYtMzQ4LjY2NS0xODkuOTAzLTQ5NS40NEMxMDAuNTIzIDc1Ni40NjkgMjkuMzg2IDY1NS45NzgtMzYuNDM0IDU1MC40MDRjLTIxLjk3Mi0zNS4yNDQtNDAuOTM0LTcyLjQ3Ny02Mi4wNDctMTA5LjA1NC00Mi4yMTYtNzMuMTM3LTc2LjQ0NC0xNTcuOTM1LTc0LjI2OS0yNjcuOTMyIDIuMTI1LTEwNy40NzMgMzMuMjA4LTE5My42ODUgNzguMDMtMjY0LjE3M0MtMjEtMjA2LjY5IDEwMi40ODEtMzAxLjc0NSAyNjguMTY0LTMyNi43MjRjMTM1LjQ2Ni0yMC40MjUgMjYyLjQ3NSAxNC4wODIgMzUyLjU0MyA2Ni43NDcgNzMuNiA0My4wMzggMTMwLjU5NiAxMDAuNTI4IDE3My45MiAxNjguMjggNDUuMjIgNzAuNzE2IDc2LjM2IDE1NC4yNiA3OC45NzEgMjYzLjIzMyAxLjMzNyA1NS44My03LjgwNSAxMDcuNTMyLTIwLjY4NCAxNTAuNDE3LTEzLjAzNCA0My40MS0zMy45OTYgNzkuNjk1LTUyLjY0NiAxMTguNDU1LTM2LjQwNiA3NS42NTktODIuMDQ5IDE0NC45ODEtMTI3Ljg1NSAyMTQuMzQ1LTEzNi40MzcgMjA2LjYwNi0yNjQuNDk2IDQxNy4zMS0zMjAuNTggNzA2LjAyOHoiLz48Y2lyY2xlIGZpbGwtcnVsZT0iZXZlbm9kZCIgY2xpcC1ydWxlPSJldmVub2RkIiBjeD0iMzUyLjg5MSIgY3k9IjIyNS43NzkiIHI9IjE4My4zMzIiLz48L3N2Zz4=\",\"imageSize\":34},\"markerOffsetX\":0.5,\"markerOffsetY\":1,\"positionFunction\":\"return {x: origXPos, y: origYPos};\",\"markerClustering\":{\"enable\":false,\"zoomOnClick\":true,\"maxZoom\":null,\"maxClusterRadius\":80,\"zoomAnimation\":true,\"showCoverageOnHover\":true,\"spiderfyOnMaxZoom\":false,\"chunkedLoad\":false,\"lazyLoad\":true,\"useClusterMarkerColorFunction\":false,\"clusterMarkerColorFunction\":null}},{\"dsType\":\"function\",\"dsLabel\":\"Second point\",\"dsDeviceId\":null,\"dsEntityAliasId\":null,\"dsFilterId\":null,\"additionalDataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"temperature\",\"color\":\"#2196f3\",\"settings\":{},\"_hash\":0.7826299113906372,\"funcBody\":\"var value = prevValue + Math.random() * 40 - 20;\\nif (value < -60) {\\n\\tvalue = -60;\\n} else if (value > 60) {\\n\\tvalue = 60;\\n}\\nreturn value;\",\"aggregationType\":null,\"units\":null,\"decimals\":null,\"usePostProcessing\":null,\"postFuncBody\":null}],\"label\":{\"show\":true,\"type\":\"pattern\",\"pattern\":\"${entityName}\"},\"tooltip\":{\"show\":true,\"type\":\"pattern\",\"pattern\":\"${entityName}

X Pos: ${xPos:2}
Y Pos: ${yPos:2}
Temperature: ${temperature} °C
See tooltip settings for details\",\"patternFunction\":null,\"trigger\":\"click\",\"autoclose\":true,\"offsetX\":0,\"offsetY\":-1},\"groups\":null,\"xKey\":{\"name\":\"f(x)\",\"label\":\"latitude\",\"type\":\"function\",\"funcBody\":\"var value = prevValue || 0.6;\\nif (time % 4000 < 500) {\\n value += Math.random() * 0.05 - 0.025;\\n}\\nreturn value;\",\"settings\":{},\"color\":\"#2196f3\",\"aggregationType\":null,\"units\":null,\"decimals\":null,\"usePostProcessing\":null,\"postFuncBody\":null},\"yKey\":{\"name\":\"f(x)\",\"label\":\"longitude\",\"type\":\"function\",\"funcBody\":\"var value = prevValue || 0.7;\\nif (time % 4000 < 500) {\\n value += Math.random() * 0.05 - 0.025;\\n}\\nreturn value;\",\"settings\":{},\"color\":\"#2196f3\",\"aggregationType\":null,\"units\":null,\"decimals\":null,\"usePostProcessing\":null,\"postFuncBody\":null},\"markerType\":\"image\",\"markerShape\":{\"shape\":\"markerShape1\",\"size\":34,\"color\":{\"type\":\"constant\",\"color\":\"#307FE5\"}},\"markerIcon\":{\"icon\":\"\",\"size\":34,\"color\":{\"type\":\"constant\",\"color\":\"#307FE5\"}},\"markerImage\":{\"type\":\"function\",\"image\":\"data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSIzNCIgaGVpZ2h0PSIzNCIgdmlld0JveD0iMCAwIDM0IDM0IiBmaWxsPSJub25lIj4KICA8ZyBmaWx0ZXI9InVybCgjZmlsdGVyMF9iZl84ODE2XzI2Mzg4NykiPgogICAgPHBhdGggZD0iTTE5IDI0LjVDMTcuNDA3NSAyNy40MTI1IDE3IDMzIDE3IDMzQzE3IDMzIDI3LjA4NTggMzIuMTk1NSAzMC45OTkyIDI3LjQ5OThDMzQgMjMuODk5MiAzMS45OTkyIDE5IDI3Ljk5OTIgMTlDMjMuOTk5MyAxOSAyMS4xOTI5IDIwLjQ4OTQgMTkgMjQuNVoiIGZpbGw9ImJsYWNrIiBmaWxsLW9wYWNpdHk9IjAuMjQiLz4KICA8L2c+CiAgPG1hc2sgaWQ9InBhdGgtMi1pbnNpZGUtMV84ODE2XzI2Mzg4NyIgZmlsbD0id2hpdGUiPgogICAgPHBhdGggZmlsbC1ydWxlPSJldmVub2RkIiBjbGlwLXJ1bGU9ImV2ZW5vZGQiIGQ9Ik0yOCAxMS45QzI4LjAwMzcgNS4zMjc2MSAyNC4yOTAyIDAgMTcgMEM5LjcwOTgzIDAgNS45OTYzIDUuMzI3NjEgNiAxMS45QzYuMDA0NzMgMjAuMjkzNyAxNyAzNCAxNyAzNEMxNyAzNCAyNy45OTUzIDIwLjI5MzcgMjggMTEuOVpNMjEuMjUgMTAuNjI1QzIxLjI1IDEyLjk3MjIgMTkuMzQ3MiAxNC44NzUgMTcgMTQuODc1QzE0LjY1MjggMTQuODc1IDEyLjc1IDEyLjk3MjIgMTIuNzUgMTAuNjI1QzEyLjc1IDguMjc3NzkgMTQuNjUyOCA2LjM3NSAxNyA2LjM3NUMxOS4zNDcyIDYuMzc1IDIxLjI1IDguMjc3NzkgMjEuMjUgMTAuNjI1WiIvPgogIDwvbWFzaz4KICA8cGF0aCBmaWxsLXJ1bGU9ImV2ZW5vZGQiIGNsaXAtcnVsZT0iZXZlbm9kZCIgZD0iTTI4IDExLjlDMjguMDAzNyA1LjMyNzYxIDI0LjI5MDIgMCAxNyAwQzkuNzA5ODMgMCA1Ljk5NjMgNS4zMjc2MSA2IDExLjlDNi4wMDQ3MyAyMC4yOTM3IDE3IDM0IDE3IDM0QzE3IDM0IDI3Ljk5NTMgMjAuMjkzNyAyOCAxMS45Wk0yMS4yNSAxMC42MjVDMjEuMjUgMTIuOTcyMiAxOS4zNDcyIDE0Ljg3NSAxNyAxNC44NzVDMTQuNjUyOCAxNC44NzUgMTIuNzUgMTIuOTcyMiAxMi43NSAxMC42MjVDMTIuNzUgOC4yNzc3OSAxNC42NTI4IDYuMzc1IDE3IDYuMzc1QzE5LjM0NzIgNi4zNzUgMjEuMjUgOC4yNzc3OSAyMS4yNSAxMC42MjVaIiBmaWxsPSIjMzA3ZmU1Ii8+CiAgPHBhdGggZD0iTTI4IDExLjlMMjkuMDYyNSAxMS45MDA2TDI4IDExLjlaTTYgMTEuOUw3LjA2MjUgMTEuODk5NEw2IDExLjlaTTE3IDM0TDE2LjE3MTIgMzQuNjY0OUwxNyAzNS42OThMMTcuODI4OCAzNC42NjQ5TDE3IDM0Wk0xNyAxLjA2MjVDMjAuMzY0MSAxLjA2MjUgMjIuODA4NSAyLjI4MDA2IDI0LjQyNzMgNC4xNzUzOUMyNi4wNjUyIDYuMDkzMjMgMjYuOTM5MiA4LjgwMzMxIDI2LjkzNzUgMTEuODk5NEwyOS4wNjI1IDExLjkwMDZDMjkuMDY0NCA4LjQyNDMgMjguMDgzNSA1LjE4NDM4IDI2LjA0MzEgMi43OTUzMkMyMy45ODM1IDAuMzgzNzQyIDIwLjkyNjEgLTEuMDYyNSAxNyAtMS4wNjI1VjEuMDYyNVpNNy4wNjI1IDExLjg5OTRDNy4wNjA3NiA4LjgwMzMxIDcuOTM0NzcgNi4wOTMyMyA5LjU3Mjc0IDQuMTc1MzlDMTEuMTkxNSAyLjI4MDA2IDEzLjYzNTkgMS4wNjI1IDE3IDEuMDYyNVYtMS4wNjI1QzEzLjA3MzkgLTEuMDYyNSAxMC4wMTY1IDAuMzgzNzQxIDcuOTU2ODYgMi43OTUzMkM1LjkxNjQ1IDUuMTg0MzggNC45MzU1NSA4LjQyNDMgNC45Mzc1IDExLjkwMDZMNy4wNjI1IDExLjg5OTRaTTE3IDM0QzE3LjgyODggMzMuMzM1MSAxNy44Mjg4IDMzLjMzNTIgMTcuODI4OCAzMy4zMzUyQzE3LjgyODggMzMuMzM1MiAxNy44Mjg4IDMzLjMzNTEgMTcuODI4NyAzMy4zMzVDMTcuODI4NSAzMy4zMzQ4IDE3LjgyODEgMzMuMzM0MyAxNy44Mjc2IDMzLjMzMzZDMTcuODI2NSAzMy4zMzIzIDE3LjgyNDggMzMuMzMwMSAxNy44MjI0IDMzLjMyNzFDMTcuODE3NiAzMy4zMjEyIDE3LjgxMDMgMzMuMzEyIDE3LjgwMDQgMzMuMjk5NUMxNy43ODA3IDMzLjI3NDcgMTcuNzUwOSAzMy4yMzcxIDE3LjcxMTggMzMuMTg3NEMxNy42MzM1IDMzLjA4NzggMTcuNTE3OCAzMi45Mzk1IDE3LjM3IDMyLjc0NzJDMTcuMDc0MiAzMi4zNjI0IDE2LjY1MDMgMzEuODAxNyAxNi4xNDA5IDMxLjEwMTlDMTUuMTIxNCAyOS43MDE0IDEzLjc2MzQgMjcuNzQ5NSAxMi40MDcxIDI1LjU0MTVDMTEuMDQ5IDIzLjMzMDYgOS43MDM4OCAyMC44ODEzIDguNzAwOTEgMTguNDg0MUM3LjY5MTA5IDE2LjA3MDYgNy4wNjM1NyAxMy43OTIxIDcuMDYyNSAxMS44OTk0TDQuOTM3NSAxMS45MDA2QzQuOTM4OCAxNC4yMDQ4IDUuNjg3NDYgMTYuNzg3MyA2Ljc0MDU4IDE5LjMwNDNDNy44MDA1NSAyMS44Mzc4IDkuMjA1MTYgMjQuMzg4OSAxMC41OTY0IDI2LjY1MzhDMTEuOTg5NSAyOC45MjE2IDEzLjM4MDcgMzAuOTIwOSAxNC40MjI5IDMyLjM1MjZDMTQuOTQ0NCAzMy4wNjg5IDE1LjM3OTUgMzMuNjQ0NiAxNS42ODUxIDM0LjA0MjJDMTUuODM3OSAzNC4yNDEgMTUuOTU4NCAzNC4zOTU0IDE2LjA0MTIgMzQuNTAwN0MxNi4wODI2IDM0LjU1MzQgMTYuMTE0NiAzNC41OTM4IDE2LjEzNjUgMzQuNjIxM0MxNi4xNDc0IDM0LjYzNTEgMTYuMTU1OSAzNC42NDU2IDE2LjE2MTcgMzQuNjUyOUMxNi4xNjQ2IDM0LjY1NjYgMTYuMTY2OCAzNC42NTk0IDE2LjE2ODQgMzQuNjYxNEMxNi4xNjkyIDM0LjY2MjQgMTYuMTY5OSAzNC42NjMyIDE2LjE3MDMgMzQuNjYzN0MxNi4xNzA2IDM0LjY2NCAxNi4xNzA4IDM0LjY2NDMgMTYuMTcwOSAzNC42NjQ1QzE2LjE3MTEgMzQuNjY0NyAxNi4xNzEyIDM0LjY2NDkgMTcgMzRaTTI2LjkzNzUgMTEuODk5NEMyNi45MzY0IDEzLjc5MjEgMjYuMzA4OSAxNi4wNzA2IDI1LjI5OTEgMTguNDg0MUMyNC4yOTYxIDIwLjg4MTMgMjIuOTUxIDIzLjMzMDYgMjEuNTkyOSAyNS41NDE1QzIwLjIzNjYgMjcuNzQ5NSAxOC44Nzg2IDI5LjcwMTQgMTcuODU5MSAzMS4xMDE5QzE3LjM0OTcgMzEuODAxNyAxNi45MjU4IDMyLjM2MjQgMTYuNjMgMzIuNzQ3MkMxNi40ODIyIDMyLjkzOTUgMTYuMzY2NSAzMy4wODc4IDE2LjI4ODIgMzMuMTg3NEMxNi4yNDkxIDMzLjIzNzEgMTYuMjE5MyAzMy4yNzQ3IDE2LjE5OTYgMzMuMjk5NUMxNi4xODk3IDMzLjMxMiAxNi4xODI0IDMzLjMyMTIgMTYuMTc3NiAzMy4zMjcxQzE2LjE3NTIgMzMuMzMwMSAxNi4xNzM1IDMzLjMzMjMgMTYuMTcyNCAzMy4zMzM2QzE2LjE3MTkgMzMuMzM0MyAxNi4xNzE1IDMzLjMzNDggMTYuMTcxMyAzMy4zMzVDMTYuMTcxMiAzMy4zMzUxIDE2LjE3MTIgMzMuMzM1MiAxNi4xNzEyIDMzLjMzNTJDMTYuMTcxMiAzMy4zMzUyIDE2LjE3MTIgMzMuMzM1MSAxNyAzNEMxNy44Mjg4IDM0LjY2NDkgMTcuODI4OSAzNC42NjQ3IDE3LjgyOTEgMzQuNjY0NUMxNy44MjkyIDM0LjY2NDMgMTcuODI5NCAzNC42NjQgMTcuODI5NyAzNC42NjM3QzE3LjgzMDEgMzQuNjYzMiAxNy44MzA4IDM0LjY2MjQgMTcuODMxNiAzNC42NjE0QzE3LjgzMzIgMzQuNjU5NCAxNy44MzU0IDM0LjY1NjYgMTcuODM4MyAzNC42NTI5QzE3Ljg0NDEgMzQuNjQ1NiAxNy44NTI2IDM0LjYzNTEgMTcuODYzNSAzNC42MjEzQzE3Ljg4NTQgMzQuNTkzOCAxNy45MTc0IDM0LjU1MzQgMTcuOTU4OCAzNC41MDA3QzE4LjA0MTYgMzQuMzk1NCAxOC4xNjIxIDM0LjI0MSAxOC4zMTQ5IDM0LjA0MjJDMTguNjIwNSAzMy42NDQ2IDE5LjA1NTYgMzMuMDY4OSAxOS41NzcxIDMyLjM1MjZDMjAuNjE5MyAzMC45MjA5IDIyLjAxMDUgMjguOTIxNiAyMy40MDM2IDI2LjY1MzhDMjQuNzk0OCAyNC4zODg5IDI2LjE5OTUgMjEuODM3OCAyNy4yNTk0IDE5LjMwNDNDMjguMzEyNSAxNi43ODczIDI5LjA2MTIgMTQuMjA0OCAyOS4wNjI1IDExLjkwMDZMMjYuOTM3NSAxMS44OTk0Wk0xNyAxNS45Mzc1QzE5LjkzNCAxNS45Mzc1IDIyLjMxMjUgMTMuNTU5IDIyLjMxMjUgMTAuNjI1SDIwLjE4NzVDMjAuMTg3NSAxMi4zODU0IDE4Ljc2MDQgMTMuODEyNSAxNyAxMy44MTI1VjE1LjkzNzVaTTExLjY4NzUgIDEwLjYyNUMxMS42ODc1IDEzLjU1OSAxNC4wNjYgMTUuOTM3NSAxNyAxNS45Mzc1VjEzLjgxMjVDMTUuMjM5NiAxMy44MTI1IDEzLjgxMjUgMTIuMzg1NCAxMy44MTI1IDEwLjYyNUgxMS42ODc1Wk0xNyA1LjMxMjVDMTQuMDY2IDUuMzEyNSAxMS42ODc1IDcuNjkwOTkgMTEuNjg3NSAxMC42MjVIMTMuODEyNUMxMy44MTI1IDguODY0NTkgMTUuMjM5NiA3LjQzNzUgMTcgNy40Mzc1VjUuMzEyNVpNMjIuMzEyNSAxMC42MjVDMjIuMzEyNSA3LjY5MDk5IDE5LjkzNCA1LjMxMjUgMTcgNS4zMTI1VjcuNDM3NUMxOC43NjA0IDcuNDM3NSAyMC4xODc1IDguODY0NTkgMjAuMTg3NSAxMC42MjVIMjIuMzEyNVoiIGZpbGw9ImJsYWNrIiBmaWxsLW9wYWNpdHk9IjAuMzgiIG1hc2s9InVybCgjcGF0aC0yLWluc2lkZS0xXzg4MTZfMjYzODg3KSIvPgogIDxkZWZzPgogICAgPGZpbHRlciBpZD0iZmlsdGVyMF9iZl84ODE2XzI2Mzg4NyIgeD0iMTIuNzUiIHk9IjE0Ljc1IiB3aWR0aD0iMjMuOTQzNCIgaGVpZ2h0PSIyMi41IiBmaWx0ZXJVbml0cz0idXNlclNwYWNlT25Vc2UiIGNvbG9yLWludGVycG9sYXRpb24tZmlsdGVycz0ic1JHQiI+CiAgICAgIDxmZUZsb29kIGZsb29kLW9wYWNpdHk9IjAiIHJlc3VsdD0iQmFja2dyb3VuZEltYWdlRml4Ii8+CiAgICAgIDxmZUdhdXNzaWFuQmx1ciBpbj0iQmFja2dyb3VuZEltYWdlRml4IiBzdGREZXZpYXRpb249IjIuMTI1Ii8+CiAgICAgIDxmZUNvbXBvc2l0ZSBpbjI9IlNvdXJjZUFscGhhIiBvcGVyYXRvcj0iaW4iIHJlc3VsdD0iZWZmZWN0MV9iYWNrZ3JvdW5kQmx1cl84ODE2XzI2Mzg4NyIvPgogICAgICA8ZmVCbGVuZCBtb2RlPSJub3JtYWwiIGluPSJTb3VyY2VHcmFwaGljIiBpbjI9ImVmZmVjdDFfYmFja2dyb3VuZEJsdXJfODgxNl8yNjM4ODciIHJlc3VsdD0ic2hhcGUiLz4KICAgICAgPGZlR2F1c3NpYW5CbHVyIHN0ZERldmlhdGlvbj0iMC41IiByZXN1bHQ9ImVmZmVjdDJfZm9yZWdyb3VuZEJsdXJfODgxNl8yNjM4ODciLz4KICAgIDwvZmlsdGVyPgogIDwvZGVmcz4KPC9zdmc+\",\"imageSize\":34,\"imageFunction\":\"var res = {\\n url: images[0],\\n size: 40\\n}\\nvar temperature = data.temperature;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120;\\n var index = Math.min(3, Math.floor(4 * percent));\\n res.url = images[index];\\n}\\nreturn res;\\n\",\"images\":[\"tb-image;/api/images/system/map_marker_image_0.png\",\"tb-image;/api/images/system/map_marker_image_1.png\",\"tb-image;/api/images/system/map_marker_image_2.png\",\"tb-image;/api/images/system/map_marker_image_3.png\"]},\"markerOffsetX\":0.5,\"markerOffsetY\":1,\"positionFunction\":\"return {x: origXPos, y: origYPos};\",\"markerClustering\":{\"enable\":false,\"zoomOnClick\":true,\"maxZoom\":null,\"maxClusterRadius\":80,\"zoomAnimation\":true,\"showCoverageOnHover\":true,\"spiderfyOnMaxZoom\":false,\"chunkedLoad\":false,\"lazyLoad\":true,\"useClusterMarkerColorFunction\":false,\"clusterMarkerColorFunction\":null}}],\"polygons\":[],\"circles\":[],\"additionalDataSources\":[],\"controlsPosition\":\"topleft\",\"zoomActions\":[\"scroll\",\"doubleClick\",\"controlButtons\"],\"fitMapBounds\":true,\"useDefaultCenterPosition\":false,\"defaultCenterPosition\":\"0,0\",\"defaultZoomLevel\":null,\"minZoomLevel\":16,\"mapPageSize\":16384,\"background\":{\"type\":\"color\",\"color\":\"#fff\",\"overlay\":{\"enabled\":false,\"color\":\"rgba(255,255,255,0.72)\",\"blur\":3}},\"padding\":\"8px\",\"imageSource\":{\"sourceType\":\"image\",\"url\":\"tb-image;/api/images/system/image_map_system_widget_map_image.svg\",\"entityAliasId\":null,\"entityKey\":null}},\"title\":\"Image Map\",\"useDashboardTimewindow\":true,\"displayTimewindow\":true,\"showTitleIcon\":false,\"titleTooltip\":\"\",\"dropShadow\":true,\"enableFullscreen\":true,\"widgetStyle\":{},\"widgetCss\":\"\",\"titleStyle\":{\"fontSize\":\"16px\",\"fontWeight\":400},\"pageSize\":1024,\"noDataDisplayMessage\":\"\",\"configMode\":\"basic\",\"titleFont\":null,\"titleColor\":null,\"margin\":\"0px\",\"borderRadius\":\"0px\",\"iconSize\":\"24px\",\"titleIcon\":\"map\",\"iconColor\":\"#1F6BDD\",\"actions\":{\"tooltipAction\":[]}}" }, "resources": [ { diff --git a/application/src/main/data/json/system/widget_types/map.json b/application/src/main/data/json/system/widget_types/map.json index e4aba10c61..8fecb89f70 100644 --- a/application/src/main/data/json/system/widget_types/map.json +++ b/application/src/main/data/json/system/widget_types/map.json @@ -11,13 +11,13 @@ "resources": [], "templateHtml": "\n", "templateCss": "", - "controllerScript": "self.onInit = function() {\n self.ctx.$scope.mapWidget.onInit();\n};\n\nself.typeParameters = function() {\n return {\n hideDataTab: true,\n hideDataSettings: true,\n previewWidth: '80%',\n embedTitlePanel: true,\n datasourcesOptional: true\n };\n}\n\nself.actionSources = function() {\n return {\n markerClick: {\n name: 'widget-action.marker-click',\n multiple: false\n },\n polygonClick: {\n name: 'widget-action.polygon-click',\n multiple: false\n },\n circleClick: {\n name: 'widget-action.circle-click',\n multiple: false\n },\n tooltipAction: {\n name: 'widget-action.tooltip-tag-action',\n multiple: true\n }\n };\n}\n", + "controllerScript": "self.onInit = function() {\n self.ctx.$scope.mapWidget.onInit();\n};\n\nself.typeParameters = function() {\n return {\n hideDataTab: true,\n hideDataSettings: true,\n previewWidth: '80%',\n embedTitlePanel: true,\n datasourcesOptional: true\n };\n}\n\nself.actionSources = function() {\n return {\n tooltipAction: {\n name: 'widget-action.tooltip-tag-action',\n multiple: true\n }\n };\n}\n", "settingsForm": [], "dataKeySettingsForm": [], "settingsDirective": "tb-map-widget-settings", "hasBasicMode": true, "basicModeDirective": "tb-map-basic-config", - "defaultConfig": "{\"datasources\":[],\"timewindow\":{\"realtime\":{\"timewindowMs\":60000}},\"showTitle\":true,\"backgroundColor\":\"rgba(0, 0, 0, 0)\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"0px\",\"settings\":{\"mapType\":\"geoMap\",\"layers\":[{\"provider\":\"openstreet\",\"layerType\":\"OpenStreetMap.Mapnik\"},{\"provider\":\"openstreet\",\"layerType\":\"OpenStreetMap.HOT\"},{\"provider\":\"openstreet\",\"layerType\":\"Esri.WorldStreetMap\"},{\"provider\":\"openstreet\",\"layerType\":\"Esri.WorldTopoMap\"},{\"provider\":\"openstreet\",\"layerType\":\"Esri.WorldImagery\"},{\"provider\":\"openstreet\",\"layerType\":\"CartoDB.Positron\"},{\"provider\":\"openstreet\",\"layerType\":\"CartoDB.DarkMatter\"}],\"imageSourceType\":null,\"imageUrl\":null,\"imageEntityAlias\":null,\"imageUrlAttribute\":null,\"markers\":[{\"dsType\":\"function\",\"dsLabel\":\"First point\",\"dsDeviceId\":null,\"dsEntityAliasId\":null,\"dsFilterId\":null,\"additionalDataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"temperature\",\"color\":\"#2196f3\",\"settings\":{},\"_hash\":0.8239425680406081,\"funcBody\":\"var value = prevValue + Math.random() * 40 - 20;\\nif (value < -60) {\\n\\tvalue = -60;\\n} else if (value > 60) {\\n\\tvalue = 60;\\n}\\nreturn value;\",\"aggregationType\":null,\"units\":null,\"decimals\":null,\"usePostProcessing\":null,\"postFuncBody\":null}],\"label\":{\"show\":true,\"type\":\"pattern\",\"pattern\":\"${entityName}\"},\"tooltip\":{\"show\":true,\"trigger\":\"click\",\"autoclose\":true,\"type\":\"pattern\",\"pattern\":\"${entityName}

Latitude: ${latitude:7}
Longitude: ${longitude:7}
Temperature: ${temperature} °C
See tooltip settings for details\",\"offsetX\":0,\"offsetY\":-1},\"groups\":null,\"xKey\":{\"name\":\"f(x)\",\"label\":\"latitude\",\"type\":\"function\",\"funcBody\":\"var value = prevValue || 15.833293;\\nif (time % 500 < 500) {\\n value += Math.random() * 0.05 - 0.025;\\n}\\nreturn value;\",\"settings\":{},\"color\":\"#2196f3\"},\"yKey\":{\"name\":\"f(x)\",\"label\":\"longitude\",\"type\":\"function\",\"funcBody\":\"var value = prevValue || -90.454350;\\nif (time % 500 < 500) {\\n value += Math.random() * 0.05 - 0.025;\\n}\\nreturn value;\",\"settings\":{},\"color\":\"#2196f3\"},\"markerType\":\"shape\",\"markerShape\":{\"shape\":\"markerShape1\",\"size\":34,\"color\":{\"type\":\"function\",\"color\":\"#307FE5\",\"colorFunction\":\"var temperature = data.temperature;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\\n\"}},\"markerIcon\":{\"icon\":\"mdi:lightbulb-on\",\"size\":34,\"color\":{\"type\":\"constant\",\"color\":\"#307FE5\"}},\"markerImage\":{\"type\":\"image\",\"image\":\"data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9Ii0xOTEuMzUgLTM1MS4xOCAxMDgzLjU4IDE3MzAuNDYiPjxwYXRoIGZpbGwtcnVsZT0iZXZlbm9kZCIgY2xpcC1ydWxlPSJldmVub2RkIiBmaWxsPSIjZmU3NTY5IiBzdHJva2U9IiMwMDAiIHN0cm9rZS13aWR0aD0iMzciIHN0cm9rZS1taXRlcmxpbWl0PSIxMCIgZD0iTTM1MS44MzMgMTM2MC43OGMtMzguNzY2LTE5MC4zLTEwNy4xMTYtMzQ4LjY2NS0xODkuOTAzLTQ5NS40NEMxMDAuNTIzIDc1Ni40NjkgMjkuMzg2IDY1NS45NzgtMzYuNDM0IDU1MC40MDRjLTIxLjk3Mi0zNS4yNDQtNDAuOTM0LTcyLjQ3Ny02Mi4wNDctMTA5LjA1NC00Mi4yMTYtNzMuMTM3LTc2LjQ0NC0xNTcuOTM1LTc0LjI2OS0yNjcuOTMyIDIuMTI1LTEwNy40NzMgMzMuMjA4LTE5My42ODUgNzguMDMtMjY0LjE3M0MtMjEtMjA2LjY5IDEwMi40ODEtMzAxLjc0NSAyNjguMTY0LTMyNi43MjRjMTM1LjQ2Ni0yMC40MjUgMjYyLjQ3NSAxNC4wODIgMzUyLjU0MyA2Ni43NDcgNzMuNiA0My4wMzggMTMwLjU5NiAxMDAuNTI4IDE3My45MiAxNjguMjggNDUuMjIgNzAuNzE2IDc2LjM2IDE1NC4yNiA3OC45NzEgMjYzLjIzMyAxLjMzNyA1NS44My03LjgwNSAxMDcuNTMyLTIwLjY4NCAxNTAuNDE3LTEzLjAzNCA0My40MS0zMy45OTYgNzkuNjk1LTUyLjY0NiAxMTguNDU1LTM2LjQwNiA3NS42NTktODIuMDQ5IDE0NC45ODEtMTI3Ljg1NSAyMTQuMzQ1LTEzNi40MzcgMjA2LjYwNi0yNjQuNDk2IDQxNy4zMS0zMjAuNTggNzA2LjAyOHoiLz48Y2lyY2xlIGZpbGwtcnVsZT0iZXZlbm9kZCIgY2xpcC1ydWxlPSJldmVub2RkIiBjeD0iMzUyLjg5MSIgY3k9IjIyNS43NzkiIHI9IjE4My4zMzIiLz48L3N2Zz4=\",\"imageSize\":34},\"markerOffsetX\":0.5,\"markerOffsetY\":1},{\"dsType\":\"function\",\"dsLabel\":\"Second point\",\"dsDeviceId\":null,\"dsEntityAliasId\":null,\"dsFilterId\":null,\"additionalDataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"temperature\",\"color\":\"#2196f3\",\"settings\":{},\"_hash\":0.7826299113906372,\"funcBody\":\"var value = prevValue + Math.random() * 40 - 20;\\nif (value < -60) {\\n\\tvalue = -60;\\n} else if (value > 60) {\\n\\tvalue = 60;\\n}\\nreturn value;\",\"aggregationType\":null,\"units\":null,\"decimals\":null,\"usePostProcessing\":null,\"postFuncBody\":null}],\"label\":{\"show\":true,\"type\":\"pattern\",\"pattern\":\"${entityName}\"},\"tooltip\":{\"show\":true,\"trigger\":\"click\",\"autoclose\":true,\"type\":\"pattern\",\"pattern\":\"${entityName}

Latitude: ${latitude:7}
Longitude: ${longitude:7}
Temperature: ${temperature} °C
See tooltip settings for details\",\"offsetX\":0,\"offsetY\":-1},\"groups\":null,\"xKey\":{\"name\":\"f(x)\",\"label\":\"latitude\",\"type\":\"function\",\"funcBody\":\"var value = prevValue || 14.450463;\\nif (time % 500 < 500) {\\n value += Math.random() * 0.05 - 0.025;\\n}\\nreturn value;\",\"settings\":{},\"color\":\"#2196f3\",\"aggregationType\":null,\"units\":null,\"decimals\":null,\"usePostProcessing\":null,\"postFuncBody\":null},\"yKey\":{\"name\":\"f(x)\",\"label\":\"longitude\",\"type\":\"function\",\"funcBody\":\"var value = prevValue || -84.845334;\\nif (time % 500 < 500) {\\n value += Math.random() * 0.05 - 0.025;\\n}\\nreturn value;\",\"settings\":{},\"color\":\"#2196f3\",\"aggregationType\":null,\"units\":null,\"decimals\":null,\"usePostProcessing\":null,\"postFuncBody\":null},\"markerType\":\"image\",\"markerShape\":{\"shape\":\"markerShape1\",\"size\":34,\"color\":{\"type\":\"constant\",\"color\":\"#307FE5\"}},\"markerIcon\":{\"icon\":\"\",\"size\":34,\"color\":{\"type\":\"constant\",\"color\":\"#307FE5\"}},\"markerImage\":{\"type\":\"function\",\"image\":\"data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSIzNCIgaGVpZ2h0PSIzNCIgdmlld0JveD0iMCAwIDM0IDM0IiBmaWxsPSJub25lIj4KICA8ZyBmaWx0ZXI9InVybCgjZmlsdGVyMF9iZl84ODE2XzI2Mzg4NykiPgogICAgPHBhdGggZD0iTTE5IDI0LjVDMTcuNDA3NSAyNy40MTI1IDE3IDMzIDE3IDMzQzE3IDMzIDI3LjA4NTggMzIuMTk1NSAzMC45OTkyIDI3LjQ5OThDMzQgMjMuODk5MiAzMS45OTkyIDE5IDI3Ljk5OTIgMTlDMjMuOTk5MyAxOSAyMS4xOTI5IDIwLjQ4OTQgMTkgMjQuNVoiIGZpbGw9ImJsYWNrIiBmaWxsLW9wYWNpdHk9IjAuMjQiLz4KICA8L2c+CiAgPG1hc2sgaWQ9InBhdGgtMi1pbnNpZGUtMV84ODE2XzI2Mzg4NyIgZmlsbD0id2hpdGUiPgogICAgPHBhdGggZmlsbC1ydWxlPSJldmVub2RkIiBjbGlwLXJ1bGU9ImV2ZW5vZGQiIGQ9Ik0yOCAxMS45QzI4LjAwMzcgNS4zMjc2MSAyNC4yOTAyIDAgMTcgMEM5LjcwOTgzIDAgNS45OTYzIDUuMzI3NjEgNiAxMS45QzYuMDA0NzMgMjAuMjkzNyAxNyAzNCAxNyAzNEMxNyAzNCAyNy45OTUzIDIwLjI5MzcgMjggMTEuOVpNMjEuMjUgMTAuNjI1QzIxLjI1IDEyLjk3MjIgMTkuMzQ3MiAxNC44NzUgMTcgMTQuODc1QzE0LjY1MjggMTQuODc1IDEyLjc1IDEyLjk3MjIgMTIuNzUgMTAuNjI1QzEyLjc1IDguMjc3NzkgMTQuNjUyOCA2LjM3NSAxNyA2LjM3NUMxOS4zNDcyIDYuMzc1IDIxLjI1IDguMjc3NzkgMjEuMjUgMTAuNjI1WiIvPgogIDwvbWFzaz4KICA8cGF0aCBmaWxsLXJ1bGU9ImV2ZW5vZGQiIGNsaXAtcnVsZT0iZXZlbm9kZCIgZD0iTTI4IDExLjlDMjguMDAzNyA1LjMyNzYxIDI0LjI5MDIgMCAxNyAwQzkuNzA5ODMgMCA1Ljk5NjMgNS4zMjc2MSA2IDExLjlDNi4wMDQ3MyAyMC4yOTM3IDE3IDM0IDE3IDM0QzE3IDM0IDI3Ljk5NTMgMjAuMjkzNyAyOCAxMS45Wk0yMS4yNSAxMC42MjVDMjEuMjUgMTIuOTcyMiAxOS4zNDcyIDE0Ljg3NSAxNyAxNC44NzVDMTQuNjUyOCAxNC44NzUgMTIuNzUgMTIuOTcyMiAxMi43NSAxMC42MjVDMTIuNzUgOC4yNzc3OSAxNC42NTI4IDYuMzc1IDE3IDYuMzc1QzE5LjM0NzIgNi4zNzUgMjEuMjUgOC4yNzc3OSAyMS4yNSAxMC42MjVaIiBmaWxsPSIjMzA3ZmU1Ii8+CiAgPHBhdGggZD0iTTI4IDExLjlMMjkuMDYyNSAxMS45MDA2TDI4IDExLjlaTTYgMTEuOUw3LjA2MjUgMTEuODk5NEw2IDExLjlaTTE3IDM0TDE2LjE3MTIgMzQuNjY0OUwxNyAzNS42OThMMTcuODI4OCAzNC42NjQ5TDE3IDM0Wk0xNyAxLjA2MjVDMjAuMzY0MSAxLjA2MjUgMjIuODA4NSAyLjI4MDA2IDI0LjQyNzMgNC4xNzUzOUMyNi4wNjUyIDYuMDkzMjMgMjYuOTM5MiA4LjgwMzMxIDI2LjkzNzUgMTEuODk5NEwyOS4wNjI1IDExLjkwMDZDMjkuMDY0NCA4LjQyNDMgMjguMDgzNSA1LjE4NDM4IDI2LjA0MzEgMi43OTUzMkMyMy45ODM1IDAuMzgzNzQyIDIwLjkyNjEgLTEuMDYyNSAxNyAtMS4wNjI1VjEuMDYyNVpNNy4wNjI1IDExLjg5OTRDNy4wNjA3NiA4LjgwMzMxIDcuOTM0NzcgNi4wOTMyMyA5LjU3Mjc0IDQuMTc1MzlDMTEuMTkxNSAyLjI4MDA2IDEzLjYzNTkgMS4wNjI1IDE3IDEuMDYyNVYtMS4wNjI1QzEzLjA3MzkgLTEuMDYyNSAxMC4wMTY1IDAuMzgzNzQxIDcuOTU2ODYgMi43OTUzMkM1LjkxNjQ1IDUuMTg0MzggNC45MzU1NSA4LjQyNDMgNC45Mzc1IDExLjkwMDZMNy4wNjI1IDExLjg5OTRaTTE3IDM0QzE3LjgyODggMzMuMzM1MSAxNy44Mjg4IDMzLjMzNTIgMTcuODI4OCAzMy4zMzUyQzE3LjgyODggMzMuMzM1MiAxNy44Mjg4IDMzLjMzNTEgMTcuODI4NyAzMy4zMzVDMTcuODI4NSAzMy4zMzQ4IDE3LjgyODEgMzMuMzM0MyAxNy44Mjc2IDMzLjMzMzZDMTcuODI2NSAzMy4zMzIzIDE3LjgyNDggMzMuMzMwMSAxNy44MjI0IDMzLjMyNzFDMTcuODE3NiAzMy4zMjEyIDE3LjgxMDMgMzMuMzEyIDE3LjgwMDQgMzMuMjk5NUMxNy43ODA3IDMzLjI3NDcgMTcuNzUwOSAzMy4yMzcxIDE3LjcxMTggMzMuMTg3NEMxNy42MzM1IDMzLjA4NzggMTcuNTE3OCAzMi45Mzk1IDE3LjM3IDMyLjc0NzJDMTcuMDc0MiAzMi4zNjI0IDE2LjY1MDMgMzEuODAxNyAxNi4xNDA5IDMxLjEwMTlDMTUuMTIxNCAyOS43MDE0IDEzLjc2MzQgMjcuNzQ5NSAxMi40MDcxIDI1LjU0MTVDMTEuMDQ5IDIzLjMzMDYgOS43MDM4OCAyMC44ODEzIDguNzAwOTEgMTguNDg0MUM3LjY5MTA5IDE2LjA3MDYgNy4wNjM1NyAxMy43OTIxIDcuMDYyNSAxMS44OTk0TDQuOTM3NSAxMS45MDA2QzQuOTM4OCAxNC4yMDQ4IDUuNjg3NDYgMTYuNzg3MyA2Ljc0MDU4IDE5LjMwNDNDNy44MDA1NSAyMS44Mzc4IDkuMjA1MTYgMjQuMzg4OSAxMC41OTY0IDI2LjY1MzhDMTEuOTg5NSAyOC45MjE2IDEzLjM4MDcgMzAuOTIwOSAxNC40MjI5IDMyLjM1MjZDMTQuOTQ0NCAzMy4wNjg5IDE1LjM3OTUgMzMuNjQ0NiAxNS42ODUxIDM0LjA0MjJDMTUuODM3OSAzNC4yNDEgMTUuOTU4NCAzNC4zOTU0IDE2LjA0MTIgMzQuNTAwN0MxNi4wODI2IDM0LjU1MzQgMTYuMTE0NiAzNC41OTM4IDE2LjEzNjUgMzQuNjIxM0MxNi4xNDc0IDM0LjYzNTEgMTYuMTU1OSAzNC42NDU2IDE2LjE2MTcgMzQuNjUyOUMxNi4xNjQ2IDM0LjY1NjYgMTYuMTY2OCAzNC42NTk0IDE2LjE2ODQgMzQuNjYxNEMxNi4xNjkyIDM0LjY2MjQgMTYuMTY5OSAzNC42NjMyIDE2LjE3MDMgMzQuNjYzN0MxNi4xNzA2IDM0LjY2NCAxNi4xNzA4IDM0LjY2NDMgMTYuMTcwOSAzNC42NjQ1QzE2LjE3MTEgMzQuNjY0NyAxNi4xNzEyIDM0LjY2NDkgMTcgMzRaTTI2LjkzNzUgMTEuODk5NEMyNi45MzY0IDEzLjc5MjEgMjYuMzA4OSAxNi4wNzA2IDI1LjI5OTEgMTguNDg0MUMyNC4yOTYxIDIwLjg4MTMgMjIuOTUxIDIzLjMzMDYgMjEuNTkyOSAyNS41NDE1QzIwLjIzNjYgMjcuNzQ5NSAxOC44Nzg2IDI5LjcwMTQgMTcuODU5MSAzMS4xMDE5QzE3LjM0OTcgMzEuODAxNyAxNi45MjU4IDMyLjM2MjQgMTYuNjMgMzIuNzQ3MkMxNi40ODIyIDMyLjkzOTUgMTYuMzY2NSAzMy4wODc4IDE2LjI4ODIgMzMuMTg3NEMxNi4yNDkxIDMzLjIzNzEgMTYuMjE5MyAzMy4yNzQ3IDE2LjE5OTYgMzMuMjk5NUMxNi4xODk3IDMzLjMxMiAxNi4xODI0IDMzLjMyMTIgMTYuMTc3NiAzMy4zMjcxQzE2LjE3NTIgMzMuMzMwMSAxNi4xNzM1IDMzLjMzMjMgMTYuMTcyNCAzMy4zMzM2QzE2LjE3MTkgMzMuMzM0MyAxNi4xNzE1IDMzLjMzNDggMTYuMTcxMyAzMy4zMzVDMTYuMTcxMiAzMy4zMzUxIDE2LjE3MTIgMzMuMzM1MiAxNi4xNzEyIDMzLjMzNTJDMTYuMTcxMiAzMy4zMzUyIDE2LjE3MTIgMzMuMzM1MSAxNyAzNEMxNy44Mjg4IDM0LjY2NDkgMTcuODI4OSAzNC42NjQ3IDE3LjgyOTEgMzQuNjY0NUMxNy44MjkyIDM0LjY2NDMgMTcuODI5NCAzNC42NjQgMTcuODI5NyAzNC42NjM3QzE3LjgzMDEgMzQuNjYzMiAxNy44MzA4IDM0LjY2MjQgMTcuODMxNiAzNC42NjE0QzE3LjgzMzIgMzQuNjU5NCAxNy44MzU0IDM0LjY1NjYgMTcuODM4MyAzNC42NTI5QzE3Ljg0NDEgMzQuNjQ1NiAxNy44NTI2IDM0LjYzNTEgMTcuODYzNSAzNC42MjEzQzE3Ljg4NTQgMzQuNTkzOCAxNy45MTc0IDM0LjU1MzQgMTcuOTU4OCAzNC41MDA3QzE4LjA0MTYgMzQuMzk1NCAxOC4xNjIxIDM0LjI0MSAxOC4zMTQ5IDM0LjA0MjJDMTguNjIwNSAzMy42NDQ2IDE5LjA1NTYgMzMuMDY4OSAxOS41NzcxIDMyLjM1MjZDMjAuNjE5MyAzMC45MjA5IDIyLjAxMDUgMjguOTIxNiAyMy40MDM2IDI2LjY1MzhDMjQuNzk0OCAyNC4zODg5IDI2LjE5OTUgMjEuODM3OCAyNy4yNTk0IDE5LjMwNDNDMjguMzEyNSAxNi43ODczIDI5LjA2MTIgMTQuMjA0OCAyOS4wNjI1IDExLjkwMDZMMjYuOTM3NSAxMS44OTk0Wk0xNyAxNS45Mzc1QzE5LjkzNCAxNS45Mzc1IDIyLjMxMjUgMTMuNTU5IDIyLjMxMjUgMTAuNjI1SDIwLjE4NzVDMjAuMTg3NSAxMi4zODU0IDE4Ljc2MDQgMTMuODEyNSAxNyAxMy44MTI1VjE1LjkzNzVaTTExLjY4NzUgIDEwLjYyNUMxMS42ODc1IDEzLjU1OSAxNC4wNjYgMTUuOTM3NSAxNyAxNS45Mzc1VjEzLjgxMjVDMTUuMjM5NiAxMy44MTI1IDEzLjgxMjUgMTIuMzg1NCAxMy44MTI1IDEwLjYyNUgxMS42ODc1Wk0xNyA1LjMxMjVDMTQuMDY2IDUuMzEyNSAxMS42ODc1IDcuNjkwOTkgMTEuNjg3NSAxMC42MjVIMTMuODEyNUMxMy44MTI1IDguODY0NTkgMTUuMjM5NiA3LjQzNzUgMTcgNy40Mzc1VjUuMzEyNVpNMjIuMzEyNSAxMC42MjVDMjIuMzEyNSA3LjY5MDk5IDE5LjkzNCA1LjMxMjUgMTcgNS4zMTI1VjcuNDM3NUMxOC43NjA0IDcuNDM3NSAyMC4xODc1IDguODY0NTkgMjAuMTg3NSAxMC42MjVIMjIuMzEyNVoiIGZpbGw9ImJsYWNrIiBmaWxsLW9wYWNpdHk9IjAuMzgiIG1hc2s9InVybCgjcGF0aC0yLWluc2lkZS0xXzg4MTZfMjYzODg3KSIvPgogIDxkZWZzPgogICAgPGZpbHRlciBpZD0iZmlsdGVyMF9iZl84ODE2XzI2Mzg4NyIgeD0iMTIuNzUiIHk9IjE0Ljc1IiB3aWR0aD0iMjMuOTQzNCIgaGVpZ2h0PSIyMi41IiBmaWx0ZXJVbml0cz0idXNlclNwYWNlT25Vc2UiIGNvbG9yLWludGVycG9sYXRpb24tZmlsdGVycz0ic1JHQiI+CiAgICAgIDxmZUZsb29kIGZsb29kLW9wYWNpdHk9IjAiIHJlc3VsdD0iQmFja2dyb3VuZEltYWdlRml4Ii8+CiAgICAgIDxmZUdhdXNzaWFuQmx1ciBpbj0iQmFja2dyb3VuZEltYWdlRml4IiBzdGREZXZpYXRpb249IjIuMTI1Ii8+CiAgICAgIDxmZUNvbXBvc2l0ZSBpbjI9IlNvdXJjZUFscGhhIiBvcGVyYXRvcj0iaW4iIHJlc3VsdD0iZWZmZWN0MV9iYWNrZ3JvdW5kQmx1cl84ODE2XzI2Mzg4NyIvPgogICAgICA8ZmVCbGVuZCBtb2RlPSJub3JtYWwiIGluPSJTb3VyY2VHcmFwaGljIiBpbjI9ImVmZmVjdDFfYmFja2dyb3VuZEJsdXJfODgxNl8yNjM4ODciIHJlc3VsdD0ic2hhcGUiLz4KICAgICAgPGZlR2F1c3NpYW5CbHVyIHN0ZERldmlhdGlvbj0iMC41IiByZXN1bHQ9ImVmZmVjdDJfZm9yZWdyb3VuZEJsdXJfODgxNl8yNjM4ODciLz4KICAgIDwvZmlsdGVyPgogIDwvZGVmcz4KPC9zdmc+\",\"imageSize\":34,\"imageFunction\":\"var res = {\\n url: images[0],\\n size: 40\\n}\\nvar temperature = data.temperature;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120;\\n var index = Math.min(3, Math.floor(4 * percent));\\n res.url = images[index];\\n}\\nreturn res;\\n\",\"images\":[\"tb-image;/api/images/system/map_marker_image_0.png\",\"tb-image;/api/images/system/map_marker_image_1.png\",\"tb-image;/api/images/system/map_marker_image_2.png\",\"tb-image;/api/images/system/map_marker_image_3.png\"]},\"markerOffsetX\":0.5,\"markerOffsetY\":1}],\"polygons\":[],\"circles\":[],\"additionalDataSources\":[],\"controlsPosition\":\"topleft\",\"zoomActions\":[\"scroll\",\"doubleClick\",\"controlButtons\"],\"fitMapBounds\":true,\"useDefaultCenterPosition\":false,\"defaultCenterPosition\":\"0,0\",\"defaultZoomLevel\":null,\"mapPageSize\":16384,\"background\":{\"type\":\"color\",\"color\":\"#fff\",\"overlay\":{\"enabled\":false,\"color\":\"rgba(255,255,255,0.72)\",\"blur\":3}},\"padding\":\"8px\"},\"title\":\"Map\",\"useDashboardTimewindow\":true,\"displayTimewindow\":true,\"showTitleIcon\":false,\"titleTooltip\":\"\",\"dropShadow\":true,\"enableFullscreen\":true,\"widgetStyle\":{},\"widgetCss\":\"\",\"titleStyle\":{\"fontSize\":\"16px\",\"fontWeight\":400},\"pageSize\":1024,\"noDataDisplayMessage\":\"\",\"configMode\":\"basic\",\"titleFont\":null,\"titleColor\":null,\"margin\":\"0px\",\"borderRadius\":\"0px\",\"iconSize\":\"24px\",\"titleIcon\":\"map\",\"iconColor\":\"#1F6BDD\",\"actions\":{\"tooltipAction\":[{\"name\":\"testTag\",\"icon\":\"more_horiz\",\"useShowWidgetActionFunction\":null,\"showWidgetActionFunction\":\"return true;\",\"type\":\"custom\",\"customFunction\":\"console.log('It works!!!');\\n\\nconsole.log(entityName);\\n\\nconsole.log(additionalParams);\",\"openInSeparateDialog\":false,\"openInPopover\":false,\"id\":\"f9b4925a-818c-15d2-6220-cf2f317bc7fe\"}]}}" + "defaultConfig": "{\"datasources\":[],\"timewindow\":{\"realtime\":{\"timewindowMs\":60000}},\"showTitle\":true,\"backgroundColor\":\"rgba(0, 0, 0, 0)\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"0px\",\"settings\":{\"mapType\":\"geoMap\",\"layers\":[{\"provider\":\"openstreet\",\"layerType\":\"OpenStreetMap.Mapnik\"},{\"provider\":\"openstreet\",\"layerType\":\"OpenStreetMap.HOT\"},{\"provider\":\"openstreet\",\"layerType\":\"Esri.WorldStreetMap\"},{\"provider\":\"openstreet\",\"layerType\":\"Esri.WorldTopoMap\"},{\"provider\":\"openstreet\",\"layerType\":\"Esri.WorldImagery\"},{\"provider\":\"openstreet\",\"layerType\":\"CartoDB.Positron\"},{\"provider\":\"openstreet\",\"layerType\":\"CartoDB.DarkMatter\"}],\"markers\":[{\"dsType\":\"function\",\"dsLabel\":\"First point\",\"dsDeviceId\":null,\"dsEntityAliasId\":null,\"dsFilterId\":null,\"additionalDataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"temperature\",\"color\":\"#2196f3\",\"settings\":{},\"_hash\":0.8239425680406081,\"funcBody\":\"var value = prevValue + Math.random() * 40 - 20;\\nif (value < -60) {\\n\\tvalue = -60;\\n} else if (value > 60) {\\n\\tvalue = 60;\\n}\\nreturn value;\",\"aggregationType\":null,\"units\":null,\"decimals\":null,\"usePostProcessing\":null,\"postFuncBody\":null}],\"label\":{\"show\":true,\"type\":\"pattern\",\"pattern\":\"${entityName}\"},\"tooltip\":{\"show\":true,\"trigger\":\"click\",\"autoclose\":true,\"type\":\"pattern\",\"pattern\":\"${entityName}

Latitude: ${latitude:7}
Longitude: ${longitude:7}
Temperature: ${temperature} °C
See tooltip settings for details\",\"offsetX\":0,\"offsetY\":-1},\"groups\":null,\"xKey\":{\"name\":\"f(x)\",\"label\":\"latitude\",\"type\":\"function\",\"funcBody\":\"var value = prevValue || 15.833293;\\nif (time % 500 < 500) {\\n value += Math.random() * 0.05 - 0.025;\\n}\\nreturn value;\",\"settings\":{},\"color\":\"#2196f3\"},\"yKey\":{\"name\":\"f(x)\",\"label\":\"longitude\",\"type\":\"function\",\"funcBody\":\"var value = prevValue || -90.454350;\\nif (time % 500 < 500) {\\n value += Math.random() * 0.05 - 0.025;\\n}\\nreturn value;\",\"settings\":{},\"color\":\"#2196f3\"},\"markerType\":\"shape\",\"markerShape\":{\"shape\":\"markerShape1\",\"size\":34,\"color\":{\"type\":\"function\",\"color\":\"#307FE5\",\"colorFunction\":\"var temperature = data.temperature;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\\n\"}},\"markerIcon\":{\"icon\":\"mdi:lightbulb-on\",\"size\":34,\"color\":{\"type\":\"constant\",\"color\":\"#307FE5\"}},\"markerImage\":{\"type\":\"image\",\"image\":\"data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9Ii0xOTEuMzUgLTM1MS4xOCAxMDgzLjU4IDE3MzAuNDYiPjxwYXRoIGZpbGwtcnVsZT0iZXZlbm9kZCIgY2xpcC1ydWxlPSJldmVub2RkIiBmaWxsPSIjZmU3NTY5IiBzdHJva2U9IiMwMDAiIHN0cm9rZS13aWR0aD0iMzciIHN0cm9rZS1taXRlcmxpbWl0PSIxMCIgZD0iTTM1MS44MzMgMTM2MC43OGMtMzguNzY2LTE5MC4zLTEwNy4xMTYtMzQ4LjY2NS0xODkuOTAzLTQ5NS40NEMxMDAuNTIzIDc1Ni40NjkgMjkuMzg2IDY1NS45NzgtMzYuNDM0IDU1MC40MDRjLTIxLjk3Mi0zNS4yNDQtNDAuOTM0LTcyLjQ3Ny02Mi4wNDctMTA5LjA1NC00Mi4yMTYtNzMuMTM3LTc2LjQ0NC0xNTcuOTM1LTc0LjI2OS0yNjcuOTMyIDIuMTI1LTEwNy40NzMgMzMuMjA4LTE5My42ODUgNzguMDMtMjY0LjE3M0MtMjEtMjA2LjY5IDEwMi40ODEtMzAxLjc0NSAyNjguMTY0LTMyNi43MjRjMTM1LjQ2Ni0yMC40MjUgMjYyLjQ3NSAxNC4wODIgMzUyLjU0MyA2Ni43NDcgNzMuNiA0My4wMzggMTMwLjU5NiAxMDAuNTI4IDE3My45MiAxNjguMjggNDUuMjIgNzAuNzE2IDc2LjM2IDE1NC4yNiA3OC45NzEgMjYzLjIzMyAxLjMzNyA1NS44My03LjgwNSAxMDcuNTMyLTIwLjY4NCAxNTAuNDE3LTEzLjAzNCA0My40MS0zMy45OTYgNzkuNjk1LTUyLjY0NiAxMTguNDU1LTM2LjQwNiA3NS42NTktODIuMDQ5IDE0NC45ODEtMTI3Ljg1NSAyMTQuMzQ1LTEzNi40MzcgMjA2LjYwNi0yNjQuNDk2IDQxNy4zMS0zMjAuNTggNzA2LjAyOHoiLz48Y2lyY2xlIGZpbGwtcnVsZT0iZXZlbm9kZCIgY2xpcC1ydWxlPSJldmVub2RkIiBjeD0iMzUyLjg5MSIgY3k9IjIyNS43NzkiIHI9IjE4My4zMzIiLz48L3N2Zz4=\",\"imageSize\":34},\"markerOffsetX\":0.5,\"markerOffsetY\":1},{\"dsType\":\"function\",\"dsLabel\":\"Second point\",\"dsDeviceId\":null,\"dsEntityAliasId\":null,\"dsFilterId\":null,\"additionalDataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"temperature\",\"color\":\"#2196f3\",\"settings\":{},\"_hash\":0.7826299113906372,\"funcBody\":\"var value = prevValue + Math.random() * 40 - 20;\\nif (value < -60) {\\n\\tvalue = -60;\\n} else if (value > 60) {\\n\\tvalue = 60;\\n}\\nreturn value;\",\"aggregationType\":null,\"units\":null,\"decimals\":null,\"usePostProcessing\":null,\"postFuncBody\":null}],\"label\":{\"show\":true,\"type\":\"pattern\",\"pattern\":\"${entityName}\"},\"tooltip\":{\"show\":true,\"trigger\":\"click\",\"autoclose\":true,\"type\":\"pattern\",\"pattern\":\"${entityName}

Latitude: ${latitude:7}
Longitude: ${longitude:7}
Temperature: ${temperature} °C
See tooltip settings for details\",\"offsetX\":0,\"offsetY\":-1},\"groups\":null,\"xKey\":{\"name\":\"f(x)\",\"label\":\"latitude\",\"type\":\"function\",\"funcBody\":\"var value = prevValue || 14.450463;\\nif (time % 500 < 500) {\\n value += Math.random() * 0.05 - 0.025;\\n}\\nreturn value;\",\"settings\":{},\"color\":\"#2196f3\",\"aggregationType\":null,\"units\":null,\"decimals\":null,\"usePostProcessing\":null,\"postFuncBody\":null},\"yKey\":{\"name\":\"f(x)\",\"label\":\"longitude\",\"type\":\"function\",\"funcBody\":\"var value = prevValue || -84.845334;\\nif (time % 500 < 500) {\\n value += Math.random() * 0.05 - 0.025;\\n}\\nreturn value;\",\"settings\":{},\"color\":\"#2196f3\",\"aggregationType\":null,\"units\":null,\"decimals\":null,\"usePostProcessing\":null,\"postFuncBody\":null},\"markerType\":\"image\",\"markerShape\":{\"shape\":\"markerShape1\",\"size\":34,\"color\":{\"type\":\"constant\",\"color\":\"#307FE5\"}},\"markerIcon\":{\"icon\":\"\",\"size\":34,\"color\":{\"type\":\"constant\",\"color\":\"#307FE5\"}},\"markerImage\":{\"type\":\"function\",\"image\":\"data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSIzNCIgaGVpZ2h0PSIzNCIgdmlld0JveD0iMCAwIDM0IDM0IiBmaWxsPSJub25lIj4KICA8ZyBmaWx0ZXI9InVybCgjZmlsdGVyMF9iZl84ODE2XzI2Mzg4NykiPgogICAgPHBhdGggZD0iTTE5IDI0LjVDMTcuNDA3NSAyNy40MTI1IDE3IDMzIDE3IDMzQzE3IDMzIDI3LjA4NTggMzIuMTk1NSAzMC45OTkyIDI3LjQ5OThDMzQgMjMuODk5MiAzMS45OTkyIDE5IDI3Ljk5OTIgMTlDMjMuOTk5MyAxOSAyMS4xOTI5IDIwLjQ4OTQgMTkgMjQuNVoiIGZpbGw9ImJsYWNrIiBmaWxsLW9wYWNpdHk9IjAuMjQiLz4KICA8L2c+CiAgPG1hc2sgaWQ9InBhdGgtMi1pbnNpZGUtMV84ODE2XzI2Mzg4NyIgZmlsbD0id2hpdGUiPgogICAgPHBhdGggZmlsbC1ydWxlPSJldmVub2RkIiBjbGlwLXJ1bGU9ImV2ZW5vZGQiIGQ9Ik0yOCAxMS45QzI4LjAwMzcgNS4zMjc2MSAyNC4yOTAyIDAgMTcgMEM5LjcwOTgzIDAgNS45OTYzIDUuMzI3NjEgNiAxMS45QzYuMDA0NzMgMjAuMjkzNyAxNyAzNCAxNyAzNEMxNyAzNCAyNy45OTUzIDIwLjI5MzcgMjggMTEuOVpNMjEuMjUgMTAuNjI1QzIxLjI1IDEyLjk3MjIgMTkuMzQ3MiAxNC44NzUgMTcgMTQuODc1QzE0LjY1MjggMTQuODc1IDEyLjc1IDEyLjk3MjIgMTIuNzUgMTAuNjI1QzEyLjc1IDguMjc3NzkgMTQuNjUyOCA2LjM3NSAxNyA2LjM3NUMxOS4zNDcyIDYuMzc1IDIxLjI1IDguMjc3NzkgMjEuMjUgMTAuNjI1WiIvPgogIDwvbWFzaz4KICA8cGF0aCBmaWxsLXJ1bGU9ImV2ZW5vZGQiIGNsaXAtcnVsZT0iZXZlbm9kZCIgZD0iTTI4IDExLjlDMjguMDAzNyA1LjMyNzYxIDI0LjI5MDIgMCAxNyAwQzkuNzA5ODMgMCA1Ljk5NjMgNS4zMjc2MSA2IDExLjlDNi4wMDQ3MyAyMC4yOTM3IDE3IDM0IDE3IDM0QzE3IDM0IDI3Ljk5NTMgMjAuMjkzNyAyOCAxMS45Wk0yMS4yNSAxMC42MjVDMjEuMjUgMTIuOTcyMiAxOS4zNDcyIDE0Ljg3NSAxNyAxNC44NzVDMTQuNjUyOCAxNC44NzUgMTIuNzUgMTIuOTcyMiAxMi43NSAxMC42MjVDMTIuNzUgOC4yNzc3OSAxNC42NTI4IDYuMzc1IDE3IDYuMzc1QzE5LjM0NzIgNi4zNzUgMjEuMjUgOC4yNzc3OSAyMS4yNSAxMC42MjVaIiBmaWxsPSIjMzA3ZmU1Ii8+CiAgPHBhdGggZD0iTTI4IDExLjlMMjkuMDYyNSAxMS45MDA2TDI4IDExLjlaTTYgMTEuOUw3LjA2MjUgMTEuODk5NEw2IDExLjlaTTE3IDM0TDE2LjE3MTIgMzQuNjY0OUwxNyAzNS42OThMMTcuODI4OCAzNC42NjQ5TDE3IDM0Wk0xNyAxLjA2MjVDMjAuMzY0MSAxLjA2MjUgMjIuODA4NSAyLjI4MDA2IDI0LjQyNzMgNC4xNzUzOUMyNi4wNjUyIDYuMDkzMjMgMjYuOTM5MiA4LjgwMzMxIDI2LjkzNzUgMTEuODk5NEwyOS4wNjI1IDExLjkwMDZDMjkuMDY0NCA4LjQyNDMgMjguMDgzNSA1LjE4NDM4IDI2LjA0MzEgMi43OTUzMkMyMy45ODM1IDAuMzgzNzQyIDIwLjkyNjEgLTEuMDYyNSAxNyAtMS4wNjI1VjEuMDYyNVpNNy4wNjI1IDExLjg5OTRDNy4wNjA3NiA4LjgwMzMxIDcuOTM0NzcgNi4wOTMyMyA5LjU3Mjc0IDQuMTc1MzlDMTEuMTkxNSAyLjI4MDA2IDEzLjYzNTkgMS4wNjI1IDE3IDEuMDYyNVYtMS4wNjI1QzEzLjA3MzkgLTEuMDYyNSAxMC4wMTY1IDAuMzgzNzQxIDcuOTU2ODYgMi43OTUzMkM1LjkxNjQ1IDUuMTg0MzggNC45MzU1NSA4LjQyNDMgNC45Mzc1IDExLjkwMDZMNy4wNjI1IDExLjg5OTRaTTE3IDM0QzE3LjgyODggMzMuMzM1MSAxNy44Mjg4IDMzLjMzNTIgMTcuODI4OCAzMy4zMzUyQzE3LjgyODggMzMuMzM1MiAxNy44Mjg4IDMzLjMzNTEgMTcuODI4NyAzMy4zMzVDMTcuODI4NSAzMy4zMzQ4IDE3LjgyODEgMzMuMzM0MyAxNy44Mjc2IDMzLjMzMzZDMTcuODI2NSAzMy4zMzIzIDE3LjgyNDggMzMuMzMwMSAxNy44MjI0IDMzLjMyNzFDMTcuODE3NiAzMy4zMjEyIDE3LjgxMDMgMzMuMzEyIDE3LjgwMDQgMzMuMjk5NUMxNy43ODA3IDMzLjI3NDcgMTcuNzUwOSAzMy4yMzcxIDE3LjcxMTggMzMuMTg3NEMxNy42MzM1IDMzLjA4NzggMTcuNTE3OCAzMi45Mzk1IDE3LjM3IDMyLjc0NzJDMTcuMDc0MiAzMi4zNjI0IDE2LjY1MDMgMzEuODAxNyAxNi4xNDA5IDMxLjEwMTlDMTUuMTIxNCAyOS43MDE0IDEzLjc2MzQgMjcuNzQ5NSAxMi40MDcxIDI1LjU0MTVDMTEuMDQ5IDIzLjMzMDYgOS43MDM4OCAyMC44ODEzIDguNzAwOTEgMTguNDg0MUM3LjY5MTA5IDE2LjA3MDYgNy4wNjM1NyAxMy43OTIxIDcuMDYyNSAxMS44OTk0TDQuOTM3NSAxMS45MDA2QzQuOTM4OCAxNC4yMDQ4IDUuNjg3NDYgMTYuNzg3MyA2Ljc0MDU4IDE5LjMwNDNDNy44MDA1NSAyMS44Mzc4IDkuMjA1MTYgMjQuMzg4OSAxMC41OTY0IDI2LjY1MzhDMTEuOTg5NSAyOC45MjE2IDEzLjM4MDcgMzAuOTIwOSAxNC40MjI5IDMyLjM1MjZDMTQuOTQ0NCAzMy4wNjg5IDE1LjM3OTUgMzMuNjQ0NiAxNS42ODUxIDM0LjA0MjJDMTUuODM3OSAzNC4yNDEgMTUuOTU4NCAzNC4zOTU0IDE2LjA0MTIgMzQuNTAwN0MxNi4wODI2IDM0LjU1MzQgMTYuMTE0NiAzNC41OTM4IDE2LjEzNjUgMzQuNjIxM0MxNi4xNDc0IDM0LjYzNTEgMTYuMTU1OSAzNC42NDU2IDE2LjE2MTcgMzQuNjUyOUMxNi4xNjQ2IDM0LjY1NjYgMTYuMTY2OCAzNC42NTk0IDE2LjE2ODQgMzQuNjYxNEMxNi4xNjkyIDM0LjY2MjQgMTYuMTY5OSAzNC42NjMyIDE2LjE3MDMgMzQuNjYzN0MxNi4xNzA2IDM0LjY2NCAxNi4xNzA4IDM0LjY2NDMgMTYuMTcwOSAzNC42NjQ1QzE2LjE3MTEgMzQuNjY0NyAxNi4xNzEyIDM0LjY2NDkgMTcgMzRaTTI2LjkzNzUgMTEuODk5NEMyNi45MzY0IDEzLjc5MjEgMjYuMzA4OSAxNi4wNzA2IDI1LjI5OTEgMTguNDg0MUMyNC4yOTYxIDIwLjg4MTMgMjIuOTUxIDIzLjMzMDYgMjEuNTkyOSAyNS41NDE1QzIwLjIzNjYgMjcuNzQ5NSAxOC44Nzg2IDI5LjcwMTQgMTcuODU5MSAzMS4xMDE5QzE3LjM0OTcgMzEuODAxNyAxNi45MjU4IDMyLjM2MjQgMTYuNjMgMzIuNzQ3MkMxNi40ODIyIDMyLjkzOTUgMTYuMzY2NSAzMy4wODc4IDE2LjI4ODIgMzMuMTg3NEMxNi4yNDkxIDMzLjIzNzEgMTYuMjE5MyAzMy4yNzQ3IDE2LjE5OTYgMzMuMjk5NUMxNi4xODk3IDMzLjMxMiAxNi4xODI0IDMzLjMyMTIgMTYuMTc3NiAzMy4zMjcxQzE2LjE3NTIgMzMuMzMwMSAxNi4xNzM1IDMzLjMzMjMgMTYuMTcyNCAzMy4zMzM2QzE2LjE3MTkgMzMuMzM0MyAxNi4xNzE1IDMzLjMzNDggMTYuMTcxMyAzMy4zMzVDMTYuMTcxMiAzMy4zMzUxIDE2LjE3MTIgMzMuMzM1MiAxNi4xNzEyIDMzLjMzNTJDMTYuMTcxMiAzMy4zMzUyIDE2LjE3MTIgMzMuMzM1MSAxNyAzNEMxNy44Mjg4IDM0LjY2NDkgMTcuODI4OSAzNC42NjQ3IDE3LjgyOTEgMzQuNjY0NUMxNy44MjkyIDM0LjY2NDMgMTcuODI5NCAzNC42NjQgMTcuODI5NyAzNC42NjM3QzE3LjgzMDEgMzQuNjYzMiAxNy44MzA4IDM0LjY2MjQgMTcuODMxNiAzNC42NjE0QzE3LjgzMzIgMzQuNjU5NCAxNy44MzU0IDM0LjY1NjYgMTcuODM4MyAzNC42NTI5QzE3Ljg0NDEgMzQuNjQ1NiAxNy44NTI2IDM0LjYzNTEgMTcuODYzNSAzNC42MjEzQzE3Ljg4NTQgMzQuNTkzOCAxNy45MTc0IDM0LjU1MzQgMTcuOTU4OCAzNC41MDA3QzE4LjA0MTYgMzQuMzk1NCAxOC4xNjIxIDM0LjI0MSAxOC4zMTQ5IDM0LjA0MjJDMTguNjIwNSAzMy42NDQ2IDE5LjA1NTYgMzMuMDY4OSAxOS41NzcxIDMyLjM1MjZDMjAuNjE5MyAzMC45MjA5IDIyLjAxMDUgMjguOTIxNiAyMy40MDM2IDI2LjY1MzhDMjQuNzk0OCAyNC4zODg5IDI2LjE5OTUgMjEuODM3OCAyNy4yNTk0IDE5LjMwNDNDMjguMzEyNSAxNi43ODczIDI5LjA2MTIgMTQuMjA0OCAyOS4wNjI1IDExLjkwMDZMMjYuOTM3NSAxMS44OTk0Wk0xNyAxNS45Mzc1QzE5LjkzNCAxNS45Mzc1IDIyLjMxMjUgMTMuNTU5IDIyLjMxMjUgMTAuNjI1SDIwLjE4NzVDMjAuMTg3NSAxMi4zODU0IDE4Ljc2MDQgMTMuODEyNSAxNyAxMy44MTI1VjE1LjkzNzVaTTExLjY4NzUgIDEwLjYyNUMxMS42ODc1IDEzLjU1OSAxNC4wNjYgMTUuOTM3NSAxNyAxNS45Mzc1VjEzLjgxMjVDMTUuMjM5NiAxMy44MTI1IDEzLjgxMjUgMTIuMzg1NCAxMy44MTI1IDEwLjYyNUgxMS42ODc1Wk0xNyA1LjMxMjVDMTQuMDY2IDUuMzEyNSAxMS42ODc1IDcuNjkwOTkgMTEuNjg3NSAxMC42MjVIMTMuODEyNUMxMy44MTI1IDguODY0NTkgMTUuMjM5NiA3LjQzNzUgMTcgNy40Mzc1VjUuMzEyNVpNMjIuMzEyNSAxMC42MjVDMjIuMzEyNSA3LjY5MDk5IDE5LjkzNCA1LjMxMjUgMTcgNS4zMTI1VjcuNDM3NUMxOC43NjA0IDcuNDM3NSAyMC4xODc1IDguODY0NTkgMjAuMTg3NSAxMC42MjVIMjIuMzEyNVoiIGZpbGw9ImJsYWNrIiBmaWxsLW9wYWNpdHk9IjAuMzgiIG1hc2s9InVybCgjcGF0aC0yLWluc2lkZS0xXzg4MTZfMjYzODg3KSIvPgogIDxkZWZzPgogICAgPGZpbHRlciBpZD0iZmlsdGVyMF9iZl84ODE2XzI2Mzg4NyIgeD0iMTIuNzUiIHk9IjE0Ljc1IiB3aWR0aD0iMjMuOTQzNCIgaGVpZ2h0PSIyMi41IiBmaWx0ZXJVbml0cz0idXNlclNwYWNlT25Vc2UiIGNvbG9yLWludGVycG9sYXRpb24tZmlsdGVycz0ic1JHQiI+CiAgICAgIDxmZUZsb29kIGZsb29kLW9wYWNpdHk9IjAiIHJlc3VsdD0iQmFja2dyb3VuZEltYWdlRml4Ii8+CiAgICAgIDxmZUdhdXNzaWFuQmx1ciBpbj0iQmFja2dyb3VuZEltYWdlRml4IiBzdGREZXZpYXRpb249IjIuMTI1Ii8+CiAgICAgIDxmZUNvbXBvc2l0ZSBpbjI9IlNvdXJjZUFscGhhIiBvcGVyYXRvcj0iaW4iIHJlc3VsdD0iZWZmZWN0MV9iYWNrZ3JvdW5kQmx1cl84ODE2XzI2Mzg4NyIvPgogICAgICA8ZmVCbGVuZCBtb2RlPSJub3JtYWwiIGluPSJTb3VyY2VHcmFwaGljIiBpbjI9ImVmZmVjdDFfYmFja2dyb3VuZEJsdXJfODgxNl8yNjM4ODciIHJlc3VsdD0ic2hhcGUiLz4KICAgICAgPGZlR2F1c3NpYW5CbHVyIHN0ZERldmlhdGlvbj0iMC41IiByZXN1bHQ9ImVmZmVjdDJfZm9yZWdyb3VuZEJsdXJfODgxNl8yNjM4ODciLz4KICAgIDwvZmlsdGVyPgogIDwvZGVmcz4KPC9zdmc+\",\"imageSize\":34,\"imageFunction\":\"var res = {\\n url: images[0],\\n size: 40\\n}\\nvar temperature = data.temperature;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120;\\n var index = Math.min(3, Math.floor(4 * percent));\\n res.url = images[index];\\n}\\nreturn res;\\n\",\"images\":[\"tb-image;/api/images/system/map_marker_image_0.png\",\"tb-image;/api/images/system/map_marker_image_1.png\",\"tb-image;/api/images/system/map_marker_image_2.png\",\"tb-image;/api/images/system/map_marker_image_3.png\"]},\"markerOffsetX\":0.5,\"markerOffsetY\":1}],\"polygons\":[],\"circles\":[],\"additionalDataSources\":[],\"controlsPosition\":\"topleft\",\"zoomActions\":[\"scroll\",\"doubleClick\",\"controlButtons\"],\"fitMapBounds\":true,\"useDefaultCenterPosition\":false,\"defaultCenterPosition\":\"0,0\",\"defaultZoomLevel\":null,\"minZoomLevel\":16,\"mapPageSize\":16384,\"background\":{\"type\":\"color\",\"color\":\"#fff\",\"overlay\":{\"enabled\":false,\"color\":\"rgba(255,255,255,0.72)\",\"blur\":3}},\"padding\":\"8px\",\"imageSourceType\":null,\"imageUrl\":null,\"imageEntityAlias\":null,\"imageUrlAttribute\":null},\"title\":\"Map\",\"useDashboardTimewindow\":true,\"displayTimewindow\":true,\"showTitleIcon\":false,\"titleTooltip\":\"\",\"dropShadow\":true,\"enableFullscreen\":true,\"widgetStyle\":{},\"widgetCss\":\"\",\"titleStyle\":{\"fontSize\":\"16px\",\"fontWeight\":400},\"pageSize\":1024,\"noDataDisplayMessage\":\"\",\"configMode\":\"basic\",\"titleFont\":null,\"titleColor\":null,\"margin\":\"0px\",\"borderRadius\":\"0px\",\"iconSize\":\"24px\",\"titleIcon\":\"map\",\"iconColor\":\"#1F6BDD\",\"actions\":{\"tooltipAction\":[]}}" }, "resources": [ { diff --git a/ui-ngx/src/app/core/api/widget-api.models.ts b/ui-ngx/src/app/core/api/widget-api.models.ts index 82ac4e299c..10656b0c9b 100644 --- a/ui-ngx/src/app/core/api/widget-api.models.ts +++ b/ui-ngx/src/app/core/api/widget-api.models.ts @@ -93,7 +93,7 @@ export interface IWidgetUtils { export interface WidgetActionsApi { actionDescriptorsBySourceId: {[sourceId: string]: Array}; getActionDescriptors: (actionSourceId: string) => Array; - handleWidgetAction: ($event: Event, descriptor: WidgetActionDescriptor, + handleWidgetAction: ($event: Event, descriptor: WidgetAction, entityId?: EntityId, entityName?: string, additionalParams?: any, entityLabel?: string) => void; onWidgetAction: ($event: Event, action: WidgetAction) => void; elementClick: ($event: Event) => void; diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/maps/data-layer/circles-data-layer.ts b/ui-ngx/src/app/modules/home/components/widget/lib/maps/data-layer/circles-data-layer.ts index f7580b313d..c70ad98e8f 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/maps/data-layer/circles-data-layer.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/maps/data-layer/circles-data-layer.ts @@ -16,7 +16,9 @@ import { CirclesDataLayerSettings, - defaultBaseCirclesDataLayerSettings, isJSON, TbCircleData, + defaultBaseCirclesDataLayerSettings, + isJSON, + TbCircleData, TbMapDatasource } from '@home/components/widget/lib/maps/models/map.models'; import L from 'leaflet'; @@ -59,10 +61,6 @@ class TbCircleDataLayerItem extends TbDataLayerItem, _dsData: FormattedData[]): void { - this.dataLayer.getMap().circleClick(this, data.$datasource); - } - protected unbindLabel() { this.circle.unbindTooltip(); } diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/maps/data-layer/map-data-layer.ts b/ui-ngx/src/app/modules/home/components/widget/lib/maps/data-layer/map-data-layer.ts index 393131e932..861c02a560 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/maps/data-layer/map-data-layer.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/maps/data-layer/map-data-layer.ts @@ -29,7 +29,7 @@ import { TbMapDatasource } from '@home/components/widget/lib/maps/models/map.models'; import { TbMap } from '@home/components/widget/lib/maps/map'; -import { FormattedData } from '@shared/models/widget.models'; +import { FormattedData, WidgetActionType } from '@shared/models/widget.models'; import { forkJoin, Observable, of } from 'rxjs'; import { createLabelFromPattern, @@ -64,7 +64,6 @@ export abstract class TbDataLayerItem, dsData: FormattedData[]): void; - protected abstract addItemClass(clazz: string): void; protected abstract removeItemClass(clazz: string): void; @@ -103,6 +100,12 @@ export abstract class TbDataLayerItem { + this.dataLayer.getMap().dataItemClick(event.originalEvent, clickAction, this.data.$datasource); + }); + } } protected enableEdit(): void { 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 a466d1ec3f..fde9c97f1c 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 @@ -91,10 +91,6 @@ class TbMarkerDataLayerItem extends TbDataLayerItem, _dsData: FormattedData[]): void { - this.dataLayer.getMap().markerClick(this, data.$datasource); - } - protected unbindLabel() { this.marker.unbindTooltip(); } diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/maps/data-layer/polygons-data-layer.ts b/ui-ngx/src/app/modules/home/components/widget/lib/maps/data-layer/polygons-data-layer.ts index b6ca13d555..b2dd4c94a2 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/maps/data-layer/polygons-data-layer.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/maps/data-layer/polygons-data-layer.ts @@ -64,10 +64,6 @@ class TbPolygonDataLayerItem extends TbDataLayerItem, _dsData: FormattedData[]): void { - this.dataLayer.getMap().polygonClick(this, data.$datasource); - } - protected unbindLabel() { this.polygonContainer.unbindTooltip(); } 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 16eb3ddd58..58f042f475 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 @@ -20,8 +20,8 @@ import { DataKeyValuePair, MapActionHandler, MapType, - mergeUnplacedDataItemsArrays, mergeMapDatasources, + mergeUnplacedDataItemsArrays, parseCenterPosition, TbCircleData, TbMapDatasource, @@ -38,10 +38,11 @@ import '@home/components/widget/lib/maps/leaflet/leaflet-tb'; import { MapDataLayerType, TbDataLayerItem, - TbMapDataLayer, UnplacedMapDataItem, + TbMapDataLayer, + UnplacedMapDataItem, } from '@home/components/widget/lib/maps/data-layer/map-data-layer'; import { IWidgetSubscription, WidgetSubscriptionOptions } from '@core/api/widget-api.models'; -import { FormattedData, WidgetActionDescriptor, widgetType } from '@shared/models/widget.models'; +import { FormattedData, WidgetAction, WidgetActionDescriptor, 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'; @@ -50,8 +51,6 @@ import { TbCirclesDataLayer } from '@home/components/widget/lib/maps/data-layer/ import { AttributeService } from '@core/http/attribute.service'; import { AttributeData, AttributeScope, DataKeyType, LatestTelemetry } from '@shared/models/telemetry/telemetry.models'; import { EntityId } from '@shared/models/id/entity-id'; -import ITooltipsterInstance = JQueryTooltipster.ITooltipsterInstance; -import TooltipPositioningSide = JQueryTooltipster.TooltipPositioningSide; import { TbPopoverService } from '@shared/components/popover.service'; import { SelectMapEntityPanelComponent @@ -61,6 +60,8 @@ import { createColorMarkerShapeURI, MarkerShape } from '@home/components/widget/ import { MatIconRegistry } from '@angular/material/icon'; import { DomSanitizer } from '@angular/platform-browser'; import tinycolor from 'tinycolor2'; +import ITooltipsterInstance = JQueryTooltipster.ITooltipsterInstance; +import TooltipPositioningSide = JQueryTooltipster.TooltipPositioningSide; type TooltipInstancesData = {root: HTMLElement, instances: ITooltipsterInstance[]}; @@ -99,9 +100,6 @@ export abstract class TbMap { private readonly mapResize$: ResizeObserver; private readonly tooltipActions: { [name: string]: MapActionHandler }; - private readonly markerClickActions: { [name: string]: MapActionHandler }; - private readonly polygonClickActions: { [name: string]: MapActionHandler }; - private readonly circleClickActions: { [name: string]: MapActionHandler }; private tooltipInstances: TooltipInstancesData[] = []; @@ -118,9 +116,6 @@ export abstract class TbMap { this.settings = mergeDeepIgnoreArray({} as S, this.defaultSettings(), this.inputSettings as S); this.tooltipActions = this.loadActions('tooltipAction'); - this.markerClickActions = this.loadActions('markerClick'); - this.polygonClickActions = this.loadActions('polygonClick'); - this.circleClickActions = this.loadActions('circleClick'); $(containerElement).empty(); $(containerElement).addClass('tb-map-layout'); @@ -698,6 +693,18 @@ export abstract class TbMap { this.updateAddButtonsStates(); } + public dataItemClick($event: Event, action: WidgetAction, entityInfo: TbMapDatasource) { + if ($event) { + $event.preventDefault(); + $event.stopPropagation(); + } + const { entityId, entityName, entityLabel, entityType } = entityInfo; + this.ctx.actionsApi.handleWidgetAction($event, action, { + entityType, + id: entityId + }, entityName, null, entityLabel); + } + public tooltipElementClick(element: HTMLElement, action: string, datasource: TbMapDatasource): void { if (element && this.tooltipActions[action]) { element.onclick = ($event) => @@ -708,42 +715,6 @@ export abstract class TbMap { } } - public markerClick(marker: TbDataLayerItem, datasource: TbMapDatasource): void { - if (Object.keys(this.markerClickActions).length) { - marker.getLayer().on('click', (event: L.LeafletMouseEvent) => { - if (!marker.isEditing()) { - for (const action in this.markerClickActions) { - this.markerClickActions[action](event.originalEvent, datasource); - } - } - }); - } - } - - public polygonClick(polygon: TbDataLayerItem, datasource: TbMapDatasource): void { - if (Object.keys(this.polygonClickActions).length) { - polygon.getLayer().on('click', (event: L.LeafletMouseEvent) => { - if (!polygon.isEditing()) { - for (const action in this.polygonClickActions) { - this.polygonClickActions[action](event.originalEvent, datasource); - } - } - }); - } - } - - public circleClick(circle: TbDataLayerItem, datasource: TbMapDatasource): void { - if (Object.keys(this.circleClickActions).length) { - circle.getLayer().on('click', (event: L.LeafletMouseEvent) => { - if (!circle.isEditing()) { - for (const action in this.circleClickActions) { - this.circleClickActions[action](event.originalEvent, datasource); - } - } - }); - } - } - public selectItem(item: TbDataLayerItem, cancel = false, force = false): boolean { if (this.isPlacingItem) { return false; diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/maps/models/map.models.ts b/ui-ngx/src/app/modules/home/components/widget/lib/maps/models/map.models.ts index aa343e6520..76b8566692 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/maps/models/map.models.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/maps/models/map.models.ts @@ -14,12 +14,19 @@ /// limitations under the License. /// -import { DataKey, Datasource, DatasourceType, FormattedData } from '@shared/models/widget.models'; +import { + DataKey, + Datasource, + DatasourceType, + FormattedData, + WidgetAction, + WidgetActionType +} from '@shared/models/widget.models'; import { DataKeyType } from '@shared/models/telemetry/telemetry.models'; import { guid, hashCode, isDefinedAndNotNull, isNotEmptyStr, isString, mergeDeep } from '@core/utils'; import { AbstractControl, ValidationErrors, ValidatorFn } from '@angular/forms'; import { materialColors } from '@shared/models/material.models'; -import L, { LatLngExpression } from 'leaflet'; +import L from 'leaflet'; import { TbFunction } from '@shared/models/js-function.models'; import { Observable, Observer, of, switchMap } from 'rxjs'; import { map } from 'rxjs/operators'; @@ -117,6 +124,7 @@ export interface MapDataLayerSettings extends MapDataSourceSettings { additionalDataKeys?: DataKey[]; label: DataLayerPatternSettings; tooltip: DataLayerTooltipSettings; + click: WidgetAction; groups?: string[]; edit: DataLayerEditSettings; } @@ -138,6 +146,9 @@ export const defaultBaseDataLayerSettings = (mapType: MapType): Partial
diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/action/widget-action-settings-panel.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/action/widget-action-settings-panel.component.ts index 8ed53fd95e..a4df5e9250 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/action/widget-action-settings-panel.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/action/widget-action-settings-panel.component.ts @@ -15,25 +15,12 @@ /// import { Component, EventEmitter, Input, OnInit, Output, ViewEncapsulation } from '@angular/core'; -import { PageComponent } from '@shared/components/page.component'; import { TbPopoverComponent } from '@shared/components/popover.component'; -import { UntypedFormBuilder, UntypedFormGroup, Validators } from '@angular/forms'; -import { Store } from '@ngrx/store'; -import { AppState } from '@core/core.state'; -import { merge } from 'rxjs'; -import { - DataToValueType, - GetValueAction, - getValueActions, - getValueActionTranslations, - GetValueSettings -} from '@shared/models/action-widget-settings.models'; -import { ValueType } from '@shared/models/constants'; -import { TargetDevice, WidgetAction, widgetType } from '@shared/models/widget.models'; -import { AttributeScope, DataKeyType, telemetryTypeTranslationsShort } from '@shared/models/telemetry/telemetry.models'; -import { IAliasController } from '@core/api/widget-api.models'; -import { WidgetService } from '@core/http/widget.service'; +import { UntypedFormBuilder, UntypedFormGroup } from '@angular/forms'; +import { WidgetAction, widgetType } from '@shared/models/widget.models'; import { WidgetActionCallbacks } from '@home/components/widget/action/manage-widget-actions.component.models'; +import { coerceBoolean } from '@shared/decorators/coercion'; +import { TranslateService } from '@ngx-translate/core'; @Component({ selector: 'tb-widget-action-settings-panel', @@ -42,7 +29,7 @@ import { WidgetActionCallbacks } from '@home/components/widget/action/manage-wid styleUrls: ['./action-settings-panel.component.scss'], encapsulation: ViewEncapsulation.None }) -export class WidgetActionSettingsPanelComponent extends PageComponent implements OnInit { +export class WidgetActionSettingsPanelComponent implements OnInit { @Input() widgetAction: WidgetAction; @@ -57,7 +44,14 @@ export class WidgetActionSettingsPanelComponent extends PageComponent implements callbacks: WidgetActionCallbacks; @Input() - popover: TbPopoverComponent; + @coerceBoolean() + withName = false; + + @Input() + actionNames: string[]; + + @Input() + applyTitle = this.translate.instant('action.apply'); @Output() widgetActionApplied = new EventEmitter(); @@ -65,8 +59,8 @@ export class WidgetActionSettingsPanelComponent extends PageComponent implements widgetActionFormGroup: UntypedFormGroup; constructor(private fb: UntypedFormBuilder, - protected store: Store) { - super(store); + private translate: TranslateService, + private popover: TbPopoverComponent) { } ngOnInit(): void { diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/action/widget-action-settings.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/action/widget-action-settings.component.ts index 95ec0b1262..d860667d2a 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/action/widget-action-settings.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/action/widget-action-settings.component.ts @@ -118,7 +118,6 @@ export class WidgetActionSettingsComponent implements OnInit, ControlValueAccess ctx, {}, {}, {}, true); - widgetActionSettingsPanelPopover.tbComponentRef.instance.popover = widgetActionSettingsPanelPopover; widgetActionSettingsPanelPopover.tbComponentRef.instance.widgetActionApplied.subscribe((widgetAction) => { widgetActionSettingsPanelPopover.hide(); this.modelValue = widgetAction; 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 362e8eac2e..643b36e32a 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 @@ -16,6 +16,23 @@ -->
+
+
{{'widget-config.action-name' | translate}}*
+ + + + warning + + +
widget-config.action
diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/action/widget-action.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/action/widget-action.component.ts index 6c515ad0a7..a7ad209b85 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/action/widget-action.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/action/widget-action.component.ts @@ -15,13 +15,13 @@ /// import { - ControlValueAccessor, + ControlValueAccessor, FormControl, NG_VALIDATORS, NG_VALUE_ACCESSOR, UntypedFormBuilder, UntypedFormControl, UntypedFormGroup, - Validator, + Validator, ValidatorFn, Validators } from '@angular/forms'; import { Component, ElementRef, forwardRef, Input, OnInit, ViewChild } from '@angular/core'; @@ -46,6 +46,7 @@ import { CustomActionEditorCompleter, toCustomAction } from '@home/components/widget/lib/settings/common/action/custom-action.models'; +import { coerceBoolean } from '@shared/decorators/coercion'; const stateDisplayTypes = ['normal', 'separateDialog', 'popover'] as const; type stateDisplayTypeTuple = typeof stateDisplayTypes; @@ -89,6 +90,13 @@ export class WidgetActionComponent implements ControlValueAccessor, OnInit, Vali @Input() callbacks: WidgetActionCallbacks; + @Input() + @coerceBoolean() + withName = false; + + @Input() + actionNames: string[]; + widgetActionTypes = widgetActionTypes; widgetActionTypeTranslations = widgetActionTypeTranslationMap; widgetActionType = WidgetActionType; @@ -147,6 +155,10 @@ export class WidgetActionComponent implements ControlValueAccessor, OnInit, Vali ngOnInit() { this.widgetActionFormGroup = this.fb.group({}); + if (this.withName) { + this.widgetActionFormGroup.addControl('name', + this.fb.control(null, [this.validateActionName(), Validators.required])); + } this.widgetActionFormGroup.addControl('type', this.fb.control(null, [Validators.required])); this.widgetActionFormGroup.get('type').valueChanges.pipe( @@ -162,6 +174,11 @@ export class WidgetActionComponent implements ControlValueAccessor, OnInit, Vali } writeValue(widgetAction?: WidgetAction): void { + if (this.withName) { + this.widgetActionFormGroup.patchValue({ + name: widgetAction?.name + }, {emitEvent: false}); + } this.widgetActionFormGroup.patchValue({ type: widgetAction?.type }, {emitEvent: false}); @@ -457,6 +474,24 @@ export class WidgetActionComponent implements ControlValueAccessor, OnInit, Vali return res; } + private validateActionName(): ValidatorFn { + return (c: FormControl) => { + const newName = c.value; + const valid = this.checkActionName(newName); + return !valid ? { + actionNameNotUnique: true + } : null; + }; + } + + private checkActionName(name: string): boolean { + let actionNameIsUnique = true; + if (this.actionNames?.length) { + actionNameIsUnique = !this.actionNames.includes(name); + } + return actionNameIsUnique; + } + private widgetActionUpdated() { const type: WidgetActionType = this.widgetActionFormGroup.get('type').value; let result: WidgetAction; diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/map/data-layer-pattern-settings.component.html b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/map/data-layer-pattern-settings.component.html index 892558b047..b7d723b991 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/map/data-layer-pattern-settings.component.html +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/map/data-layer-pattern-settings.component.html @@ -79,6 +79,10 @@
+ + diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/map/data-layer-pattern-settings.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/map/data-layer-pattern-settings.component.ts index ee00d92043..dd57a7896b 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/map/data-layer-pattern-settings.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/map/data-layer-pattern-settings.component.ts @@ -34,6 +34,7 @@ import { DataLayerTooltipSettings, dataLayerTooltipTriggers, dataLayerTooltipTriggerTranslationMap } from '@home/components/widget/lib/maps/models/map.models'; import { coerceBoolean } from '@shared/decorators/coercion'; +import { MapSettingsContext } from '@home/components/widget/lib/settings/common/map/map-settings.component.models'; @Component({ selector: 'tb-data-layer-pattern-settings', @@ -74,6 +75,9 @@ export class DataLayerPatternSettingsComponent implements OnInit, ControlValueAc @coerceBoolean() hasTooltipOffset = false; + @Input() + context: MapSettingsContext; + private modelValue: DataLayerPatternSettings | DataLayerTooltipSettings; private propagateChange = null; @@ -100,6 +104,7 @@ export class DataLayerPatternSettingsComponent implements OnInit, ControlValueAc this.patternSettingsFormGroup.addControl('offsetX', this.fb.control(null, [])); this.patternSettingsFormGroup.addControl('offsetY', this.fb.control(null, [])); } + this.patternSettingsFormGroup.addControl('tagActions', this.fb.control(null, [])); } this.patternSettingsFormGroup.valueChanges.pipe( takeUntilDestroyed(this.destroyRef) diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/map/map-data-layer-dialog.component.html b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/map/map-data-layer-dialog.component.html index aaf04f5550..1845b4a793 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/map/map-data-layer-dialog.component.html +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/map/map-data-layer-dialog.component.html @@ -244,10 +244,12 @@ @@ -259,7 +261,7 @@
+ +
{{ 'widgets.maps.common.common-map-settings' | translate }} diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/map/map-settings.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/map/map-settings.component.ts index 3b5c57ecbe..32acc06c48 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/map/map-settings.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/map/map-settings.component.ts @@ -135,7 +135,8 @@ export class MapSettingsComponent implements OnInit, ControlValueAccessor, Valid useDefaultCenterPosition: [null, []], defaultCenterPosition: [null, []], defaultZoomLevel: [null, [Validators.min(0), Validators.max(20)]], - mapPageSize: [null, [Validators.min(1), Validators.required]] + mapPageSize: [null, [Validators.min(1), Validators.required]], + mapActionButtons: [null] }); this.mapSettingsFormGroup.valueChanges.pipe( takeUntilDestroyed(this.destroyRef) 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 351c203d3b..47680968a0 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 @@ -236,6 +236,12 @@ import { import { MapTooltipTagActionsComponent } from '@home/components/widget/lib/settings/common/map/map-tooltip-tag-actions.component'; +import { + MapActionButtonsSettingsComponent +} from '@home/components/widget/lib/settings/common/map/map-action-buttons-settings.component'; +import { + MapActionButtonRowComponent +} from '@home/components/widget/lib/settings/common/map/map-action-button-row.component'; @NgModule({ declarations: [ @@ -313,6 +319,8 @@ import { DataLayerColorSettingsComponent, DataLayerColorSettingsPanelComponent, MapTooltipTagActionsComponent, + MapActionButtonsSettingsComponent, + MapActionButtonRowComponent, DataLayerPatternSettingsComponent, MarkerShapeSettingsComponent, MarkerShapesComponent, diff --git a/ui-ngx/src/app/modules/home/components/widget/widget-component.service.ts b/ui-ngx/src/app/modules/home/components/widget/widget-component.service.ts index c16c61e264..31cae4717e 100644 --- a/ui-ngx/src/app/modules/home/components/widget/widget-component.service.ts +++ b/ui-ngx/src/app/modules/home/components/widget/widget-component.service.ts @@ -40,11 +40,12 @@ import { migrateWidgetTypeToDynamicForms, Widget, widgetActionSources, + WidgetActionType, WidgetControllerDescriptor, WidgetType } from '@shared/models/widget.models'; import { catchError, map, mergeMap, switchMap, tap } from 'rxjs/operators'; -import { isFunction, isUndefined } from '@core/utils'; +import { isDefinedAndNotNull, isFunction, isUndefined } from '@core/utils'; import { TranslateService } from '@ngx-translate/core'; import { DynamicWidgetComponent } from '@home/components/widget/dynamic-widget.component'; import { WidgetComponentsModule } from '@home/components/widget/widget-components.module'; @@ -654,6 +655,13 @@ export class WidgetComponentService { if (isUndefined(result.typeParameters.targetDeviceOptional)) { result.typeParameters.targetDeviceOptional = false; } + if (isDefinedAndNotNull(result.typeParameters.additionalWidgetActionTypes)) { + if (Array.isArray(result.typeParameters.additionalWidgetActionTypes)) { + result.typeParameters.additionalWidgetActionTypes = result.typeParameters.additionalWidgetActionTypes.filter(type => WidgetActionType[type]); + } else { + result.typeParameters.additionalWidgetActionTypes = null; + } + } if (isFunction(widgetTypeInstance.actionSources)) { result.actionSources = widgetTypeInstance.actionSources(); } else { diff --git a/ui-ngx/src/app/modules/home/components/widget/widget-config.component.html b/ui-ngx/src/app/modules/home/components/widget/widget-config.component.html index 9e2b1bf1fb..7af678217e 100644 --- a/ui-ngx/src/app/modules/home/components/widget/widget-config.component.html +++ b/ui-ngx/src/app/modules/home/components/widget/widget-config.component.html @@ -164,6 +164,7 @@ [callbacks]="widgetConfigCallbacks" [widgetType] = "modelValue.widgetType" [actionSources]="modelValue.actionSources" + [additionalWidgetActionTypes]="modelValue.typeParameters.additionalWidgetActionTypes" formControlName="actions">
diff --git a/ui-ngx/src/app/modules/home/components/widget/widget.component.ts b/ui-ngx/src/app/modules/home/components/widget/widget.component.ts index 5dc88a0bce..1850beb326 100644 --- a/ui-ngx/src/app/modules/home/components/widget/widget.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/widget.component.ts @@ -1149,6 +1149,7 @@ export class WidgetComponent extends PageComponent implements OnInit, OnChanges, ) } break; + case WidgetActionType.placeMapItem: case WidgetActionType.customPretty: const customPrettyFunction = descriptor.customFunction; const customHtml = descriptor.customHtml; diff --git a/ui-ngx/src/app/shared/models/widget.models.ts b/ui-ngx/src/app/shared/models/widget.models.ts index 36d8966c6a..860693fbf8 100644 --- a/ui-ngx/src/app/shared/models/widget.models.ts +++ b/ui-ngx/src/app/shared/models/widget.models.ts @@ -192,6 +192,7 @@ export interface WidgetTypeParameters { dataKeySettingsFunction?: DataKeySettingsFunction; displayRpcMessageToast?: boolean; targetDeviceOptional?: boolean; + additionalWidgetActionTypes?: WidgetActionType[]; } export interface WidgetControllerDescriptor { @@ -575,7 +576,8 @@ export enum WidgetActionType { custom = 'custom', customPretty = 'customPretty', mobileAction = 'mobileAction', - openURL = 'openURL' + openURL = 'openURL', + placeMapItem = 'placeMapItem' } export enum WidgetMobileActionType { @@ -589,7 +591,15 @@ export enum WidgetMobileActionType { takeScreenshot = 'takeScreenshot' } -export const widgetActionTypes = Object.keys(WidgetActionType) as WidgetActionType[]; +export enum MapItemType { + marker = 'marker', + polygon = 'polygon', + rectangle = 'rectangle', + circle = 'circle' +} + +export const widgetActionTypes = Object.keys(WidgetActionType) + .filter(value => value !== WidgetActionType.placeMapItem) as WidgetActionType[]; export const widgetActionTypeTranslationMap = new Map( [ @@ -600,7 +610,8 @@ export const widgetActionTypeTranslationMap = new Map( [ WidgetActionType.custom, 'widget-action.custom' ], [ WidgetActionType.customPretty, 'widget-action.custom-pretty' ], [ WidgetActionType.mobileAction, 'widget-action.mobile-action' ], - [ WidgetActionType.openURL, 'widget-action.open-URL' ] + [ WidgetActionType.openURL, 'widget-action.open-URL' ], + [ WidgetActionType.placeMapItem, 'widget-action.place-map-item' ], ] ); @@ -617,6 +628,15 @@ export const widgetMobileActionTypeTranslationMap = new Map( + [ + [ MapItemType.marker, 'widget-action.map-item.marker' ], + [ MapItemType.polygon, 'widget-action.map-item.polygon' ], + [ MapItemType.rectangle, 'widget-action.map-item.rectangle' ], + [ MapItemType.circle, 'widget-action.map-item.circle' ], + ] +) + export interface MobileLaunchResult { launched: boolean; } @@ -714,6 +734,7 @@ export interface WidgetAction extends CustomActionDescriptor { stateEntityParamName?: string; mobileAction?: WidgetMobileActionDescriptor; url?: string; + mapItemType?: MapItemType; } export interface WidgetActionDescriptor extends WidgetAction { 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 ee9d2b7262..9c7eb49684 100644 --- a/ui-ngx/src/assets/locale/locale.constant-en_US.json +++ b/ui-ngx/src/assets/locale/locale.constant-en_US.json @@ -6403,7 +6403,15 @@ "take-screenshot": "Take screenshot" }, "custom-action-function": "Custom action function", - "custom-pretty-function": "Custom action (with HTML template) function" + "custom-pretty-function": "Custom action (with HTML template) function", + "map-item-type": "Map item type", + "map-item": { + "marker": "Marker", + "polygon": "Polygon", + "rectangle": "Rectangle", + "circle": "Circle" + }, + "place-map-item": "Place map item" }, "widgets-bundle": { "current": "Current bundle", @@ -7638,6 +7646,18 @@ "zoom-double-click": "Double click", "zoom-control-buttons": "Control buttons" }, + "map-action": { + "map-action-buttons": "Map action buttons", + "label": "Label", + "icon": "Icon", + "color": "Color", + "action": "Action", + "add-button": "Add button", + "no-action-buttons-configured": "No action buttons configured", + "remove-action-button": "Remove action button", + "map-action-button": "Map action button", + "button-requires": "Button requires label or icon" + }, "common": { "common-map-settings": "Common map settings", "fit-map-bounds": "Fit map bounds to cover all markers", @@ -7796,7 +7816,8 @@ "edit": "Edit marker", "remove-marker-for": "Remove marker for '{{entityName}}'", "place-marker": "Place marker", - "place-marker-hint": "Click to place '{{entityName}}' entity" + "place-marker-hint": "Click to place marker", + "place-marker-hint-with-entity": "Click to place '{{entityName}}' entity" }, "polygon": { "polygon-key": "Polygon key", @@ -7814,11 +7835,16 @@ "polygon-place-first-point-cut-hint": "Click to place first point", "continue-polygon-cut-hint": "Click to continue drawing", "finish-polygon-cut-hint": "Click first marker to finish and save", - "polygon-place-first-point-hint": "Polygon for '{{entityName}}': click to place first point", - "continue-polygon-hint": "Polygon for '{{entityName}}': click to continue drawing", - "finish-polygon-hint": "Polygon for '{{entityName}}': click first marker to finish and save", - "rectangle-place-first-point-hint": "Rectangle for '{{entityName}}': click to place first point", - "finish-rectangle-hint": "Rectangle for '{{entityName}}': click to finish and save" + "polygon-place-first-point-hint": "Polygon: click to place first point", + "polygon-place-first-point-hint-with-entity": "Polygon for '{{entityName}}': click to place first point", + "continue-polygon-hint": "Polygon: click to continue drawing", + "continue-polygon-hint-with-entity": "Polygon for '{{entityName}}': click to continue drawing", + "finish-polygon-hint": "Polygon: click first marker to finish drawing", + "finish-polygon-hint-with-entity": "Polygon for '{{entityName}}': click first marker to finish and save", + "rectangle-place-first-point-hint": "Rectangle: click to place first point", + "rectangle-place-first-point-hint-with-entity": "Rectangle for '{{entityName}}': click to place first point", + "finish-rectangle-hint": "Rectangle: click to finish drawing", + "finish-rectangle-hint-with-entity": "Rectangle for '{{entityName}}': click to finish and save" }, "circle": { "circle-key": "Circle key", @@ -7830,8 +7856,10 @@ "edit": "Edit circle", "remove-circle-for": "Remove circle for '{{entityName}}'", "draw-circle": "Draw circle", - "place-circle-center-hint": "Circle for '{{entityName}}': click to place circle center", - "finish-circle-hint": "Circle for '{{entityName}}': click to finish and save circle" + "place-circle-center-hint-with-entity": "Circle for '{{entityName}}': click to place circle center", + "place-circle-center-hint": "Circle: click to place circle center", + "finish-circle-hint-with-entity": "Circle for '{{entityName}}': click to finish and save circle", + "finish-circle-hint": "Circle: click to finish drawing" }, "select-entity": "Select entity", "select-entity-hint": "Hint: after selection click at the map to set position" diff --git a/ui-ngx/src/typings/leaflet-extend-tb.d.ts b/ui-ngx/src/typings/leaflet-extend-tb.d.ts index 26084fabb5..dee27287c2 100644 --- a/ui-ngx/src/typings/leaflet-extend-tb.d.ts +++ b/ui-ngx/src/typings/leaflet-extend-tb.d.ts @@ -17,6 +17,7 @@ import { FormattedData } from '@shared/models/widget.models'; import L, { Control, ControlOptions } from 'leaflet'; import { TbMapDatasource } from '@home/components/widget/lib/maps/models/map.models'; +import { MatIconRegistry } from '@angular/material/icon'; // redeclare module, maintains compatibility with @types/leaflet declare module 'leaflet' { @@ -91,22 +92,28 @@ declare module 'leaflet' { interface TopToolbarButtonOptions { icon: string; - iconColor?: string; + color?: string; title: string; } class TopToolbarButton { - constructor(options: TopToolbarButtonOptions); - onClick(onClick: (e: MouseEvent) => void): void; + constructor(options: TopToolbarButtonOptions, iconRegistry: MatIconRegistry); + onClick(onClick: (e: MouseEvent, button: TopToolbarButton) => void): void; + setActive(active: boolean): void; + isActive(): boolean; + setDisabled(disabled: boolean): void; + isDisabled(): boolean; } interface TopToolbarControlOptions { mapElement: JQuery; + iconRegistry: MatIconRegistry; } class TopToolbarControl { constructor(options: TopToolbarControlOptions); toolbarButton(options: TopToolbarButtonOptions): TopToolbarButton; + setDisabled(disabled: boolean): void; } interface ToolbarButtonOptions { From 56fdaf173f0e8977366fc28a765f3ad4176398b4 Mon Sep 17 00:00:00 2001 From: Igor Kulikov Date: Wed, 26 Feb 2025 19:35:13 +0200 Subject: [PATCH 104/345] UI: Maps - Trips data layer. --- .../data/json/system/widget_bundles/maps.json | 1 + .../json/system/widget_types/trip_map.json | 47 ++ ui-ngx/src/app/core/utils.ts | 12 +- .../basic/map/map-basic-config.component.html | 3 + .../basic/map/map-basic-config.component.ts | 28 +- .../lib/maps/data-layer/map-data-layer.ts | 3 +- .../lib/maps/data-layer/trips-data-layer.ts | 416 ++++++++++++++++++ .../home/components/widget/lib/maps/map.scss | 9 +- .../home/components/widget/lib/maps/map.ts | 192 ++++++-- .../widget/lib/maps/models/map.models.ts | 102 ++++- .../panels/map-timeline-panel.component.html | 26 ++ .../panels/map-timeline-panel.component.scss | 19 + .../panels/map-timeline-panel.component.ts | 68 +++ .../common/map/map-settings.component.ts | 4 + .../widget/widget-components.module.ts | 2 + .../widget/widget-config.component.html | 6 + .../assets/locale/locale.constant-en_US.json | 4 + 17 files changed, 898 insertions(+), 44 deletions(-) create mode 100644 application/src/main/data/json/system/widget_types/trip_map.json create mode 100644 ui-ngx/src/app/modules/home/components/widget/lib/maps/data-layer/trips-data-layer.ts create mode 100644 ui-ngx/src/app/modules/home/components/widget/lib/maps/panels/map-timeline-panel.component.html create mode 100644 ui-ngx/src/app/modules/home/components/widget/lib/maps/panels/map-timeline-panel.component.scss create mode 100644 ui-ngx/src/app/modules/home/components/widget/lib/maps/panels/map-timeline-panel.component.ts diff --git a/application/src/main/data/json/system/widget_bundles/maps.json b/application/src/main/data/json/system/widget_bundles/maps.json index 7060499f24..3e12e5a662 100644 --- a/application/src/main/data/json/system/widget_bundles/maps.json +++ b/application/src/main/data/json/system/widget_bundles/maps.json @@ -11,6 +11,7 @@ "widgetTypeFqns": [ "map", "image_map", + "trip_map", "maps_v2.openstreetmap", "maps_v2.google_maps", "maps_v2.image_map", diff --git a/application/src/main/data/json/system/widget_types/trip_map.json b/application/src/main/data/json/system/widget_types/trip_map.json new file mode 100644 index 0000000000..e64d491779 --- /dev/null +++ b/application/src/main/data/json/system/widget_types/trip_map.json @@ -0,0 +1,47 @@ +{ + "fqn": "trip_map", + "name": "Trip Map", + "deprecated": false, + "image": "tb-image;/api/images/system/trip_animation_system_widget_image.png", + "description": "Displays the trip of the entity on the OpenStreetMap or other map providers. Allows to scroll and animate the movement of the entity. Highly customizable via custom markers, marker tooltips, and widget actions.", + "descriptor": { + "type": "timeseries", + "sizeX": 8.5, + "sizeY": 6, + "resources": [], + "templateHtml": "\n", + "templateCss": "", + "controllerScript": "self.onInit = function() {\n self.ctx.$scope.mapWidget.onInit();\n};\n\nself.typeParameters = function() {\n return {\n trip: true,\n hideDataTab: true,\n hideDataSettings: true,\n previewWidth: '80%',\n embedTitlePanel: true,\n datasourcesOptional: true\n };\n}", + "settingsForm": [], + "dataKeySettingsForm": [], + "latestDataKeySettingsForm": [], + "settingsDirective": "tb-map-widget-settings", + "dataKeySettingsDirective": "", + "latestDataKeySettingsDirective": "", + "hasBasicMode": true, + "basicModeDirective": "tb-map-basic-config", + "defaultConfig": "{\"datasources\":[],\"timewindow\":{\"history\":{\"interval\":1000,\"timewindowMs\":60000},\"aggregation\":{\"type\":\"NONE\",\"limit\":500}},\"showTitle\":true,\"backgroundColor\":\"rgba(0, 0, 0, 0)\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"0px\",\"settings\":{\"mapType\":\"geoMap\",\"layers\":[{\"provider\":\"openstreet\",\"layerType\":\"OpenStreetMap.Mapnik\"},{\"provider\":\"openstreet\",\"layerType\":\"OpenStreetMap.HOT\"},{\"provider\":\"openstreet\",\"layerType\":\"Esri.WorldStreetMap\"},{\"provider\":\"openstreet\",\"layerType\":\"Esri.WorldTopoMap\"},{\"provider\":\"openstreet\",\"layerType\":\"Esri.WorldImagery\"},{\"provider\":\"openstreet\",\"layerType\":\"CartoDB.Positron\"},{\"provider\":\"openstreet\",\"layerType\":\"CartoDB.DarkMatter\"}],\"trips\":[],\"markers\":[],\"polygons\":[],\"circles\":[],\"additionalDataSources\":[],\"controlsPosition\":\"topleft\",\"zoomActions\":[\"scroll\",\"doubleClick\",\"controlButtons\"],\"fitMapBounds\":true,\"useDefaultCenterPosition\":false,\"defaultCenterPosition\":\"0,0\",\"defaultZoomLevel\":null,\"minZoomLevel\":16,\"mapPageSize\":16384,\"tripTimeline\":{\"showTimelineControl\":true,\"timeStep\":1000,\"speedOptions\":[1,5,10,15,25],\"showTimestamp\":true,\"timestampFormat\":{\"format\":\"yyyy-MM-dd HH:mm:ss\",\"lastUpdateAgo\":false,\"custom\":false,\"auto\":false},\"snapToRealLocation\":false,\"locationSnapFilter\":\"return true;\"},\"background\":{\"type\":\"color\",\"color\":\"#fff\",\"overlay\":{\"enabled\":false,\"color\":\"rgba(255,255,255,0.72)\",\"blur\":3}},\"padding\":\"8px\"},\"title\":\"Trip Map\",\"dropShadow\":true,\"enableFullscreen\":true,\"titleStyle\":null,\"mobileHeight\":null,\"configMode\":\"basic\",\"actions\":{},\"showTitleIcon\":false,\"titleIcon\":\"assistant_navigation\",\"iconColor\":\"#1F6BDD\",\"useDashboardTimewindow\":false,\"displayTimewindow\":true,\"titleFont\":{\"size\":null,\"sizeUnit\":\"px\",\"family\":null,\"weight\":null,\"style\":null,\"lineHeight\":null},\"titleColor\":null,\"titleTooltip\":\"\",\"widgetStyle\":{},\"widgetCss\":\"\",\"pageSize\":1024,\"units\":\"\",\"decimals\":null,\"noDataDisplayMessage\":\"\",\"timewindowStyle\":{\"showIcon\":false,\"iconSize\":\"24px\",\"icon\":null,\"iconPosition\":\"left\",\"font\":{\"size\":12,\"sizeUnit\":\"px\",\"family\":\"Roboto\",\"weight\":\"400\",\"style\":\"normal\",\"lineHeight\":\"16px\"},\"color\":\"rgba(0, 0, 0, 0.38)\",\"displayTypePrefix\":true},\"margin\":\"0px\",\"borderRadius\":\"0px\",\"iconSize\":\"24px\"}" + }, + "resources": [ + { + "link": "/api/images/system/trip_animation_system_widget_image.png", + "title": "\"Trip Animation\" system widget image", + "type": "IMAGE", + "subType": "IMAGE", + "fileName": "trip_animation_system_widget_image.png", + "publicResourceKey": "3UKAE6mZvW6bnIhr7NAtYTG8FbIDFY1e", + "mediaType": "image/png", + "data": "iVBORw0KGgoAAAANSUhEUgAAAMgAAACgCAMAAAB+IdObAAAC/VBMVEX5+fnH+cv7+/vN6LD71aOq0t39/f2t0J0wVoDa08za0copef/w6+Xw7efY0Mnq5d7UycHWzcZ2dXXVycOq4Mrr5uD9d2nb1c7VzsjcycPXyMHe19Ds6OLRxrzZzsf09PPg2tTYysXO4q3z8vHm4Nq9vr339/bt6eN5gbHSx7/o6Ofj3tji3NX39fTJ88ngzsnO6bTr6+rb2drk4+N6enrv7u7o4tzz8O7e3t7Sx7nc1Mxubm3Rz83e99/P2avO4sDAmlHV09Dw5d7J78rI28fb68nPy8aw06jzzJfF4rvJysnH4sfm4t7x7enlz8n1977JnFbo9+bV+Nfv+e731tDdwpuCuXvk8dfM+M/W6c3R5sepqKf8v5bb4snX7sPn7ffZ0MLl9dzh8M/T17TZzqju083Lxr77tI9xtXDc5NPQ79Kt0tK5uba0o5X9gnDE9crK6Mna3b/s8+a81rDL78Pi++TO6MC2sataqlxHoEvh6Ove6N3R3MTW3NXa19SvtK7R2KWCgoO14LiMi4uYgmnj7OTX6L+7tJ2fvo/LsIzb4urExsSiyZjsxpCzo4vm2NLS3c88YIa97clEhfPH59TR5dHU67rjtprjvYXa6dbo6NP03K7ZtHq1r2/J1L7n6bjWz7fh1rfHzrLGuq+TxI2EY0Lj8eC11t7CwL7O4LbcxaZzkKa5wpfc79Wr0LzHtJnqmoTD19703NSImre4zZ79knrPoVv59+fi18Sqx63Swav8q4mpmYP9inXmuahoaWnlrJKJwISkknTO18z8yJzrpIzb8tu549P45svc16f8noI6mkC718pvg5nXspjIxYTXrm9osGi3xNKZmJO5o1S+xa66u4GLdFeMcFGEsfultMXuyLh9fnzMp2dhnP59rdvF2qgslDP38ODmxqzN0ZNOco3M3//O3+JTcpVISEgciyTanoqqpYTBqWL15+HFoWC2kkSurF+fvfGeosCMp6dtmuL5y6vokXQ3NzeWxMtUXWC+0/dVjmWkblwiIxuD1eZZAAAwP0lEQVR42qSZf0xbVRTHe9v1dTEaNdGoU9KlhZrV0dDWhk5hDWvxCd2M0FBaDIuuhQDVUGjXMVqYot1wYCzEWplaB+LYgEFxiwFiECRsJmOYbaLJpsnmXJhO5z9LNEaN5777Xl9/0C1zH2Dt4G15n33Pufe8OwElKLchlj2lFMfnaxi2Ix5zbgmwbt2M0WgUAxFpObodBsaXo1VGxOHWtueJMLIF2XCFrGJhViSTRZzO/mxdP0JeLyL4zK5pxOCYPbpx40a7SqXKyspyowRs5QpKIGA0Tp5EqNxCcexE7xOR9xHPE0+U0H88DqjVSiySJS1Ed4K2XUQ44uwfjmzevLlCJpPZndnZtfBDVfwDYN/NbgTAwwkiD6MkbAJBOX6ZmJhAOyneA6zWED5CHMNStX9KzDA0xLxIy9CdMBMgHrKLF5c2Y2QyH9LlZ08yCTgRwEkQZuWRSESlqtqUBYRQEuUQiA3l5JS2chL35lMUlvsoNRKTVGcSJyFVoDshT0Q4d3FmMyvijSmVNQerqrEIY7DAulxGQK4BUAWyCFUpkUASK6V6OecxOX2QoiwIeD9NRD2dLPJIrt9tVN2JiGxWJvpesbCLiEhk1dt0QaPXO4OAxEAMjshBVFbebDcYYlkcZpSIQE/xVFoseXl591DUQWi17YntrvKBSHBRjBmjXUTkZIOgLGwyu42N6H/gFRGWtiieJiKiUo2mVPe4vck560tsEfBwRLbkGo3lG6sDu7LiGJJEeA3L9jyGJgW1Mz0SlUnqJyISyZiYNemNmUymkF+nCbnR7eLczIosvfkPWPwKXxrg8LVrzsiCl0/E54wYHNVb78WLJTR7VgKGVUSklaDBINo7idudjyRVZExCi8UrrMvQoikWjWo0iv/bIyCy5IqSSHLHCzSaddegE+zIqyKJOJwLjtmntm6dAZHATO7CfYkmgWQRUlI8cvimLTUSEAmzzU6DSKs4kZUVcWuuDd0eKtHxCqdMJPp2yeViPKoPGeexCYgcHPYhgsMRQxe2bu0zDBsDWWkYeZGdy1EDGwV5CeqV0PrlRATzUaqIywVJiFOYeDsQvl0Trk0edi1ikUNGI2Oil2e/6AARFXz2O6qGh5VPNMH2sT7dY4OKF0FePopoVK/bWW4zH9xGCW14BSZwq1bINCTOTOkmrb/hFnfuNrfLg31v25P63akQCA6Ah9SIWYQ+2bVhg893FAH9w2XXnstuAI/eG6ev9Kxd23Pl9I3DnEeU9ahGDXoBSjCRy/VmHJahCVo/NRKz1BTsFd+MFa02hNIxGlWMxPz6J7Tq9tra2rfftiWaOEVN0+DRJC1kTMIaTW0tiPhmFxzO8ueee04OaXSMgESc0y8ndEjHudde0+tBhMGSH5o3vDTvRiw7mS7ZnrgpqvR67ybxTZnQag80pGqYMH4dsB5Ygas25fNLl4gBL1mPS6Wgwpi0Hz4c9V1eGHY4HOqC6zXXoiOsAa9SC3mQO30NOKd/XYBsDX16gUCgm08qcVi4UtsdRLziOGMSYHTZlWSiFX/6Kf57+FI60SAPhvEaDQYsrgQRH9bI8yJACsjLGohJ7Uyfg6FAUVSz58raNK7ksP0hz9kjLXqt9F9BsE+vbytSC/woCRgg++KReNNEaAlhfJTfVYAvxDOf5ufn+00MJ2C63D8EEZgWE5urFkR4oG4Qg9RiwSonSHVpsohIbq2gAKoqnZ4xhCmUy//c85de/6RgPmwrtEAi8ylDGEU9XoUj4ZvE5AcRLo5kaBcrcn3b6xOtYCLHPAKI0zhc9JgbpfNQtc3WoAYVS2GDUQkmUUf1hRp/bH4/8Ug36UXILcd8oVDU95EesRX65cbUSIQBWLj42mo0+RVdYoJkFY7nuGjJ9et/wT955GPgsHhVFu9XJHqoEGFyS5XNdkKp10oZsMmUryYbajJeVyNnWwWC1rMj8epq7LDswSJPU54+EGEpNKdFUo+Ql9RWHrORBHViQo4kA6PXt4AI4IK9ZnW0cjdZyRpLPHt4Zh4tt9mCSgaci06pLyiQQ32OcBrx+byV+9YNreILJpE9hYbhuIhenxy4qpBZgd/DHs/+xIjoFsSE4xlFxiW4ZzIysU7uNyNk7srJ6dIKBXKdWqn169vatHJFvgIWHKUyqASbgJ/kotmyZXEt4TxF7T92tafn6rH9FHWeLS6NUFMqn+8rLEwUadClRNJAURcQ+vpZLPI1AtwazUSmyiK8OQqKySonhxI92h4sMJtffrm0TasTCNMBNbVSeSIWblA1HMImWzaOcB7CY2tZjgnBhCDQPCkHjb4w3tnvQ4T5cHKXhClKoEI/YZE175EryrgWyWjCJcMWVm8jwLmc3dTW1tY7FRKuDgUfgLxgVyPDIyAS7u0hdUUJTydsIkJqhI3k5z8VhQcCATcW4XAHkyMJURSUcx5pEhVzxSF2I1ilprgX9nWUdqWJdIFHl0bIkdlFbYY/VTgP+yOaIrfeSpE8KIpkQrWyQfm1f1LIHVDxIoBGgxLpp6hmE0IfJE4pE/wmwt6wZDWOQ4nR7BMLX1sr4KEWslSKjgwsRwP1j0lj+zAfOyp5IVAJP+QHDzTCBrJ/LS8C7KfYH/j9T1IIYETCRWajKl0EUVQwitDzTG19RkTMS5kqix5zjdE0SeXNa0CbOImzSzoIhLOwWNhTB5lMFLsAzyTLMM4vh0CTY327VIoQImvvWRIIgURyltSW359jQ4ATi7j75Axad6pIfSBV5GwmERcBdspYbGHXdM2+c8ki0/X1nIdluwgDt16BX2MxeAfg31TfdXdcRfEQQqiHrayrySJXqVZOxH+QEwGM4VAolNLrXiitkBtE1iSIuMWYfVwFJSbCmUzPzIz/pdfWnjuZJFJfX892eYiRkBEXYMHKeHwtwn6VMHlxNZa/2IjIfSuonuTS6qEU5A3iABGeNBHcRN8kivSJMdc6O+HWuyVv/jE+mlhci9hk6Y3x8XfekQCJHrVBU1QIKEJ2cv+chrMiIkoleojNxHRLkQ1Rd0YRfpJXIJQiEmQ6d6wbRDo7uyWSgXHYOpI75eNFzi5xA4FZIygE6vcSBfiYjVWIZBX9FRW8AfvWZ1YICRa+tFiRlNIKbHjmViJ9ZJBH3yaK6BgRSKObiEB5gUcGXLxIdraplLkxooGpiMVicEZ6WbQKkYiHra+rfLPzInyzX3FveMZ4C5EgeW5Hx9fwOyIiw9a4pLsTQoFYCL8///u3knTG+MLSmbqYQLaDBkkEiyw4ZUCahjOCD4oKGJFbLL+nfbMJPdKIHwnSoCghwlxkRN4iQ/KDZEPs7gQkEAnDh88Dq5jQcZEZnYlZWaPHQeLVCtLos5HNoCEZxndNzplG2d5xDsOvsxdxKOdvviHe8B2Ji5gIxrQOIZWFvMzy+4EXAQ8uiRmmsMjoKCvyPOb3VeaVuEgoZFJikfYDl+OlJCMsRKDZS8QusWuopEKUDJhoM48omOUIfspMPmnMVWqiUUGZjXjUq6UWRHgLi3y9Gc+/nMhYJ4YV+RY0VouEHnfFH+VDLwsxL/b3x/uaFamurt4hYlbqkiSJvcREd9Oh8fSSI/3IlCAkt9+4Xq0OI4AM8gAe5MPtRWLCPvDgO2T1SI6P53CBqNu6GJG6OsdX07BTwBnjjh1EpK6uTiS6IYZIkjzeJZuL50hBxjEemPIiXgTu3AJfNhv5fx6BAf/QpFaviyAG1W+MCB7kQ0VEhGTikdyc8fFxMSGmbpvHHua64qqvluzAXtFLLxGT3TQtkgQwzTsuXCiGXRHAnvDGB3tO4FjmB6uRDrT6hthUA5cdwiZhtXqebSMvEfmQFeFNuNEx8zwvjouQ6eRUTTFQR4PJ4OApn2+hv3hwcMfrfXNz1kDAarUeGG1p2Q0WLVbRSy0W0VyJvbvlzNTVhEddhSLxUbejF3E0PpCy/FYrsIlzW3P5MCKUEJEfcGkREcIyP8BDFRkM4ykqvIjLoC5gev3MGexht8Nni6eY9i1Eiq2DHo+nZW5bYBBEfmxpoa102alJoEXRUlJlb2nxz3dlOnwY62hELL0PpIrYJyGTmcuwhXAiIiLyAa6yomeWF23sWH5DwjPt+NLgySiyCCLYw1BSJynpnpu0zzUPSoqLd8OZVvHuFtrj+aWp6ZR9AIvQR63WslOVk5PLg80eSORSU9MR69+rHwdNkUCIR5oIwp2yLZdCiE+E3xG/gweH+DG8Xi+h56O0Z2BgwG7/ZDluQodKaDKuwFE3npbNVWoNFrFXVc8BBsPu5jNVxfQgIyIQ7LBam/aesdutdElL1dFfKuesk76yusFDR+fCilOVTcVW69RqB3RjHR18XfEiPKpqvJBdQHGIyLNMQxUU6PSf4AH9/Av1+s/fgMOnhx/eNzCwbP/k81fqPZLOz7+UDBRJJH1BieSNromhldJSuNZQpdZikW5P1aWS7u5uenfzpWLoDjgupa3N2ysrZ2Z8NE3XlZS0HAURTx2IXGruo634OrhwamAqNRTo844OnENvI/EAkf8oNbcQJaIwAKvTbg9F9RBURCzYBWprsRbBBcvMzCbrYRTH1tDSJMZhI1PaHLsoNWpMJl3IZSO6re5iZTf2oUJiK9oIu1JhFFFR0IWeq4eC/jPjNM5ORX0yMMddZD7+859z5j/ntlrO9hNzQUQt8frXy25opXFnPF6Dob/ITgiFcrnc5GeTN7CnThXjxaJHNWt+pVL5sdFcLKZS0ehOI8txXe+RiGkOP6/v2WPzlcvXo+l0AHpWFgNgyMJ8gQDW3V2w2UqjR3vLbrcbRmSCjJAQqM2bC1ZrOfb90fXsvQaVA/eu8B4yVE0jTEwn1okisokkDSKtk4oLeBF2ZYQXyTybzGZqmUq8yBqNxbrIsz57KvW+iRvHcewALxJqAY9Idymvw/JpHkzA6uOH2q9Y/mQJ8XU9tHUgEMTyL05ipc1WwJd/PTg4eP75mQ/1bQWwiO6COChF4JhBI1vkIlARAo4jkdbWG/FaBkT2EyybA0DkRSWzLF7sQyLFvr4NZjPb3pRKQZ/aCDTxIv0aYMdLbCTaC2iOHy1MgXAlk+uFe2jIQCbL2gEIBQ+uVooot2RXw7Zum9RMLhfKpnTr3AmTavHa1ZCX7R9m2czG3KneIvtiajEev5FbxovkjILIsplNAwuKRY4ftcADOHUfk0CTu5I7yeRb7Hdota8HnyKRNbwGyoyDI0UOqhXATvvchhh1j+J52dpaqdWGaXMtM7dv4fBNlp3T19tbOXbsFIjkcoat3ABXq9XMk56ByI27TRc5FSdUXdYIryJBaaFewtN5TMnpZDJgxSR8kgioTJ3T3t4FFvwTK0VMaiVOqW9J+9RH3+mB4f3mEGwS6w2G+YhUaPt+qI5caolz3MDAAMdxFy+maAtN000cp6oXUDQIHyZBUFQh7dMpetCtzWmrtVsX1OnyPhgD3IwYPy1SOQG/slItMvFfRBzCGl780/H6AOxIzdfrzWbzzhnT583jPVqAHUCLSuVyHbYjll61WHpTNG00bmUFEbNQx2oUseH4m+iRI0cC0P1tWPAID0EQHh6/CJOXIoJpr2qAP4vIFaSQeBtyZhTPYnQ/xYywNwAl6DlzWubNN6Ts8+fb7R1Xw6n2BdvoKUajBmkkhIWvmBNJuEYTzsPhN1EbxTBuL8PAbFIlyf5YjNoFFj5P4RHux2KxmNvPBHR1D6Ty+P9FUEiOKPvWA7H9VV7tprfNCYUMBv30OvDVjlkGcDzcdHHt3hZFQAJZnLpOpeC5SZuXeRQhrweQCEUxnZffOD1ZiMUbUyFWxq4LCZPWAtiioUaRTf8gosgScQfuidg+LR52WQ0fB00THaHQMyKTGV6KCKMysd2g32a3iyX3BZhcxBSgqEjV4yEpL+Nxk9kAaeuMjd7FhG2xWZ/czihJemC9gvEMXXgrRGRR4P9FUEiGLVLTJYREK45jyANwRODjONVHhJDI54xjU5vFghYNamDZTo2IVeu6w/er01pe5E0gEK7OApEsWpm4yaiJgQVxp/9TOFbAq+txD0naPDGPDhMqkUMntTxYg8hBpQgaaJWM53faRbTi0QEJ5DIciSARgnhaFxEPJm2y2Pev0Ise7kGtK4hMTruCvMinbJbBPdXBQaZso667SesjpjNLRfxkuIDjVRzHSVje2yBfMERHD4Tk7IWeRVf+LrL+tyKnUTVIYrn8PJ2LD4kfPCobnLMIouIVRdogHJva7iYSW8SdAreODwYU4lzBZNCFRKhCNgqPS1FHVvVSVD8OdOO4398fxj3Wbooqo74FuKFjgUjHUEcHBiCRLZv+KPJ7gmObVaulZnKUtFHdAw5AXaTytIUgbhwTRZAKvQ0SXMSNKeBT+OSiF1Yr2OWt4ACAbl51WRxv81+s167lMR0fEcR6LDiERDTRP4mA5GpHv0Nhsk7V7JSvHBEPpX4lhKRcLvfJRCx0ZKSFVu4hPCycdagXSaV/8cJXSjoEhkogAvxJpAveQvXAU3xEEdvb3Lxenu7APq1aAnKkH0S2zyBubBdEZu7QSxYRcTIPYr9jT3rgoniflrsqRVBIcEkEUIgkujRdCf2CNe3Ns7Y4RiyCt8jnEikkPUgxqF4NAaHpcm7hDZQjE1qnSxYoNf5ID5oWSyWO61V6KHnLi/A5Aqw5WB9/G2Ny8OAmVddHzTfDxzWPE83ANotaohRpNKHrIXnAZ1CP7PiwY+HCm/sbYtHsdMuXUUPyoCQxIFByMwxuKqF8QW0k/mYJfytfhelAY1qPSyaiRNX+8fHuxOM1e7/phSO/UoYHoRARrN9L52df8hlyGi7x02Zp6+rappJiIT2GJHLu1aFDY8YcOvTqnNibPLHOXhPgRxAE8QIvxJgIUCpBK4D9otQB3Loliqj/IOL8+FGTSBhUGrCQq7RtblZ2Lihnj+TY/sb0/u2bxiuQEDn0CkN4Cp2dJkSVxP0ehiH1DLBk/KNHo70MXq2+lifJWW3Q5dIAs8NqJau3OOHA60/OzT22pSgM4HW6kFu3t22kihYzNYltTCSdSjyjU4mFmZpKW3SiCKJZJFs80j+qI7QkS6S2jq1jHhFLsGFDmEc6hE4YIiNm8Yd4jMRE/OP7zr3bbXtNwm/ZfWxZ8/3O933nnp2mhwsLG2YOzTywW6KCO9mSlBzpf7RPpbbLi4hIpksuYEs4/uQ1RJWf+O/6A7u9dNimuSeqSt8Wlq90LSqzWB4sKui41dGx4MPiYw8SJjszkgEQIDsq0bCw8HMZLfIdQ7DRlxelqKzCy4FlySqxtoS9osbGqRWE5FSspZPUlttT5LnJCcmg6Rgu4c2ZukVldvvqE5PLYYPuVmG5y1KKIgWLOjpOHL36ecWKD3KREioyhxfJO5uSDBxJQURkewNqiG0/WzSB8Xf11xYuFQv9zzJhpWskpL7N6Xe7t0YtPAF5Mt8lGsD32kVlF+z2Dyhy4lbh24a5pWWHLB8KOjoOHj169cIK74CIM9t7hp9/S7IJpWj5KqFkioqohSyHkNkgkqIiE1WSTFCEElJMvadUKv3+vImAn/hDO0MBd3t7e9Qftbx48cIlSoj5kOTE/XYjMLkc3sdbvLjcXVpaLB+20X2w4+gue9nhFZ9lcoGsrLm5ZoGE6R3WRZmEJwYbCt1TpUuUXfqhAg2bYTIQTaJpPEoefwg9OK/B778WirrPtdc5G4pPVlW0hY4MEz1+Dh+En3LwKHVG4U2WxYtt3rtU3+WWH9xnv/rw1OYGuYBq/ib5yjMl5pbGxhYjQbq7qcnl7lisutoa6VpG30FPFJEW2N5jZiOcZEWzly8vHNrdLzIdvpR+HXjUm2ET6vnzUAVkxNtWXHzSWxsKJaaE7/NfLyUilfC0cGWsj7pgsytpe8UVCJw6tXnz4wGRGb5IV7yrpqams5aOf3NzPJgmgYpIVXb2qxhBZAB+AkYLFKmFnboKr9fpboMtLiriBpGKUOiIpLBerlkjUXkjH4SeHocn/LjmKY+qORaPhINWaehSkb+rSEXQo6rkeT2IQD42uUs+nrTI57RHUcQJu3a5yQlB1gApKpVCCizritiSxzJK3jIg6EA8klD/TwQfMhIRYbl1Wql0+nxXigPOwJyKaxbLnI8ZuRnAORBxRUOXRI/zELCQEqnKBnitHJnsc5/H47m/baSAg6f1+sh/QxSRkmQyfsb80xEqElYqq3w+nzcjIzfgQgKoQYHrzh5a3bkuS/G6uosYsJASicobeC2IwIMkxr1t2/VtA9dwc/16kwBcwg/+UQS5ZxfafmL22BuTFs5tppVqXVA1xwdA/KASOAIngZ54/CvS29sbuxzr64tXDhdTIlGp7A8W4C8g5Pv3W1s9Hofjzp00EasVv639d7b9lMzMLz9GUjxLly79qwishIrR42Z9vRE+5WcDEcQn8KWnLob31bHuvrgjGAxH4uGm+2k8dwAPiIgpEVVEkW0YOkSOgadgDQaF2MORSMSKr46T1dkulGhsMSsyMxXALL1epRpEBLfnp3Z2gghwFnvQ6dvU9bi4rpkP0edbH/wE9DZDbTdBHI47aX8ubdEDUiJRSfsT1nAY4w12zaNcqSYkErHVZstMowF2Ghxa9q9VcCNMVMSoQo4PIrIePiZXgzwV6Ozs7O3tusvPJJ4BWqFHJaU9iMhrsd3evxZFYKSDeI4RMm9eDRAO22rVZBbDMDq9ngGIjOPUY9WzRiMFh/A40WAwZOWhyCzqoW0XROr6Xtkicat1WT+QS0cirQjtNbEHU7oORaAfscaxxPlSSSit90kaSGWMVLc8Coc5CiH5Ji2iUavVBBRUcJ2FIuM5bpT6kIKKyIx4NBlU4zTjUESHHsb0HhBphgclhGoVGDkwjzSNHBT0wJgxaLG+JQRtosjrFA0U0RCiVrMsq9WDiIzkM1SEQZF0hjHiDZzJRPitBhOCIlo8TjMYOI0mB0QYFElPxx7BSByepuRAgdSxxqgdtIlTe3JgOglGllGwTqBMoCe/JycENUS+U5FsFgCTHJK/h6ZEjyKQiiy8UTG6gjwOEUQK6MlgYDSaAuwRUUSsbWngCaOduEYIx96Z32FdX64OL5uH1DSaOaKhZCkV+fmEKJC1v8SEJGsgT1AETLQsa0KRPbp0LZCFIksYBm4AnV6LIjoty4vkYEIWGAwKjUaLIiYqUgUiQuTiBJ46A6JBBKNta9ufhsRUKrMZhoIbRQg3Zq2e4yHw2qP0pgJWoQERHQwWy06uFBMCGklUoi9nZLWQEwPHTZTpdDoVJgFFtAyjpyJwwtJKZwURGR4PGbQ4ZMkiEDjESuMVHzg2OgHiaCNDh44AsvVtbXxyQEQNf68YJSMcxzCTVdSF6GCQYB2mUoyDCPUKhgW+CQlBjRS+jcrHCcnAApM5Li8HRGhfTAARA0xaeG2EUw4HmNhpgghiMIKIXph+Tenp2SDym4+7C5VnjOMA/vSkqXnaMy/GNHP2mZ2dacasMTNtf+wOg+IUSWcX4UJuuDiKUrhR8poiiXNDIUJu5EZRSKJceLlBlJKXC6TcEHLlxvf3PDO76/VbZ8/+ze7Z57O/3/M8O3NOTnrkEwSd/clJH/z89EvPn/TGdW++/KZpCDybXgLrBgPERcvKd59QkO/scypAwrBQEDxIEkRqSC4ESpUIMUJO1yX5tWfsFuTCixVEjiimtcoAqakic0BOmHrZkpgr3KLMjreQPJKAWASpFCQhCLab5+FynKlh0qiWgYkYgWXRUHzczwARPnK13hHtIoGjCsNMQ/AcgiSumwBSCRFwXgtB7/UeSvJvgc0FpKB3ixJYJwgiqQhzz+ErvCymfm0JLFumZVELaghmuzzAFEGKTkFsDdETnyCHBJmNkByQxLI6fddcGZbn2iTpP6PYFBmGK05oqhtBKtcVgIAgOa9ojlD+41T3i1ENCHWNrZetGbVWh9c4poq0plp/c6yyJke9KzwGW8jbCnJ8ELkKgrLXGEndQ5Y2snQcsYHI0DRqLBW4m+C/+UbgeXIHUkQ2zfUw4iZBEAEI9e1IQUTB8wES/uvFh2/oiIbkIySxIhbo2S5jQBoNMSm8wlCqaFRjojKmV63AtNygqIRQvZF8riA5QSrHsw28vxG1U1kGRq7bN6CfaEjPs3Yg7ISaIuEh24UcAhJpSMsPN5DwX2ry+8Uq2LTpzUZqy+cSELTT3AOk0JCOZii39SSJARlWLTzUjYqlhuS7kMRxFcSnGhLEVhWZmUhiJK6XKIi+svX06SghIEkxQLD3ch8QX0Mi7gvh95Dgj79foPsVv/o/HZnqlRXJrUNe62VrngJCpa40hHNqXHlAjo6xiiSoSFm+XUTo4h1IRRDpxApCb6NdlqUR0Zswsk0kMKrQWxLkAgW54IJEt1bQbCEXc2pcW0MqHkHTQyx+Oi6Z7jDugEHlBEHkSK0WObd7CCQKgvtSQRa0/B50gKwZyzQkcMoviz0NWSZvPkuQmiCdggTmcoQQZE+9WblJYYeWVe9CntOrljU1iYvCEeTEtiIBP7GFhHzPND/94ht1EfuLH05AMISGKEaIbVV8NkDmDme7kExBjj2kYSUlUrdFi1dSFfnwQ4IkBBGOs4W4gLQWUicK0vj0o/4GSbwwbAniYxwEWek5UgFi8ZUQswFS0GK6h6BwOZ9uHSemFkLgyEp4u4Wsmn75VZCpghwQZPEXSPMvENdxOoLkA0S9hhkoSBYRagciqBlLJ1yYQlUkJ0irIUs4zuOLHci07SGocsX5eYdnDgUprH7ZmgHSaMgxIHWjN8SlguBR8cHBgQAk2kLcF4piLEy1aj2vILKHOASphtZqLGSkIa3qs7/NkaVXum1gqkW6psmeATLDRxQFyXYgbbaFJICcd7ZinHk649awkViSM71q5ZgkHZ4h9M4OCMeiCQj11pJ9uT+kAsQVV43HMpGAmCL4B8QBhCtIoiGLv7bWz5WN1GXYVnr5THA8a/Q+MlOQZmeOtM0WIglyHhiV5Tgtl7Q2UtKAcw3x5wieMc9Rkco0b+AFTfM0TvGtZmd6vcOZADJ21+NxmiS3XsakhsQ9pB4hohQa0k92dUqwM9m/XlJnmmWYZQDSboMR2oWG7HU9ZDlAssZ0J3IDwXR0XYdS015na0jMuSX0B0WCSNysibR/Q5F5m3TsTHNSjvf3x5MJQXQktnaW6M8opjoLCEy1qs9Ky2AK4msI+qzagTxEW09A3Q9uriFhzQCZY9lKCELL+gBpmFlOJhoSoFGcIS6f4ekaknLuOfMhzWqCW7KUN7DpFhKzM8+e6BDk7X0FEck9Z7JaxARJCDI1NSQInX6OzDSkwHiXyy2kpmdgiIz3+//FYVJPCXKsPv6eR2dWwQBhHNvXOJY2Vi2xhSBN0wnZdXIJSMEtJ56nsQJwXhKDCoJzgngDWbMzz7SU4tTfXj3ttJOHPHwWy4Xbf9ZSq7ccIZHnZFMFGUmCtDzwcDzf02dWr71p2kgYumiHTlSzZU1pCYKSzIRbMual+xRrvX8DZ+V4k+kuhPEuVUnStOGVQ6/ipfP5MbjkOT52rZbzdpDcfiY788SpZ312g5+dhgAy5EJ2KFKbMikdXvQQlOcw05CZghieg3TN1fpUd9+jkliux3knaSOv66SuF4DQi6+PynLa9JA4BaQpnQ3E5snGkXO+TIcwXgSOiicw+FW+XBV8yDRjnHLWmWed8Q5y1rUKctZ9939287W3vzCpz2IRKkKJPWGgIqofLM+RfUXaxYnEbBRk7ThX62umeG+SCmuii6rH4mhy89HRxNUVoazLMsMEJUcKD1XE20Ai7Df9DJlhZIWAIe6E8Dn9a8r4kKZhfBuWjexKivTHCz+7+YUx5VS01njItU+xUV8RjMsw9CenKPQcc6jIYrEwmdG/9hNP9OcjNSwKIuI4BWR/UtaFBYjK2GPM9cBIPQXhh+FYRyw5MvXzKt9jeoSrVYPbhiPZItuM3j8+Fp7KgvtdnOpce+FZZ4+HSJ4Od8+4jK16SNJDgsRPYkBW2BvW2G2EFGNmxMpRXv00Oa5+CJfRPMRUEGeiIvnMSec6eHujdN/OKsvzrBn/e5opo1umIXZedyhsyysXifcRsLxNqiwdctbZJ1967bhPnPNuuP/MZawVaSxzu4oJsjhbOB7iOGKFA3I+VmkM4ai8qSAfDRD0eBLHnnJ4EYbmH9Mk8Ru+SaH7nFGTD8mvu+46M7jOva5FcUTcR7LQdfd1fEzwDeRCgqznBCnTk09WkP31el9EvKa7PaQRqacDyNln9z3kWcxIN3Ru9BP0nEfUHAHEc12RMxr7qmDNtM34/8V23aCL85bainM4xPXv3XTLnbSpxJs0cPRZK0g/sIu5TNOrrlISQE7G2I+uQswFX9J9fM0BKQChZ6QEOfNsr5+ItmF0g2NlGE2nD4CB8P9MlqNLuhysRbrJlHaYkhIkwZkKcuNjN9302PvPcoaeIsQaXwWm2ProaE0Sai2qgoIUbeph4OsjDSEEcmQWfEYzRQIDyGa30a3l6CwNwxilQCR+y41t/gJh9GVLUwgsPW8knNe3xn0ifvYr8wESrQC5ar8sYzzybAW5/pSbbnp/gPRBa831EAFhljdfr9dzgmRQrenAWkHmR1dRRTpUpIXDth996613L2NGi6kmOimlMP4WPm2nbPvPYlVZGvLBc8izYWji3NxK+9yQZZNzrotVLJNd+MorrxwdHeEmrVpA1leJGT6GiTMU5PXX34PkWc5Rv1FUxRQu8DAa8HG3zxuY1efqHuItDwjy4mm05q7X+cGBad7Ni7GJj30Pasjfw5usnXIauHR04j2DPgCo9VdLnnvuqacACSV6vHfEN2TtZHKrkEJJFoD0iSWaJh2/gs9aUoi7ewj+tukmQALTpI/+ChK4rrWMogiGJsNNPUIGSB0doLV+AwQz/fgAEHEe45dalW3nTvLxh4CwbC+XeFqLu3k/9UVmGHDouB0f7l+uP2w999S3rxMk4MGaFB16K2DNZELnrZIg7YXdMcpx8BOd9WOI3ngPqYV14QA5//rrn72ywNEIBxKCSNclhx0IcYIgS6pI0F08JUgQRX7dvaohuYZk/NIwsSnff8x47QwpDOkN6YzCGRJbjSpH5z93wSUa8u3rrz8LieRmjGLgkoOIU86wCo+QDpDm7AChIWKozHfHCpILa6cit1z0XoOjPg4sCZK4VoTkiRA5IOqEJnZdDXEjChybiljntYBIBbn3Y7ZyNlkZuhzp0dpLWeYMca1M0rdo9vQFjyjI668T5NnrJBcilnQSEnQd52FoEcSOLas4vYcEdM7f9hB/B/LYY6c8/liGo0sciAhSuaGC0HW3BhC/hzSAmPZqnxbOc889dwzJpiKWqSDPf8ymnjPEpoqMvfHRVWvP20Lo/W3xvZrlD/UVGQ4FHD0Z7SEYNQNEjCh0AbXYhbAeMhLn7UDef+yxBY7mdAAQusJnA+LnQphFP0c6QApAPnhqtT/uIQhBzPMaHlpCQZ76GKtW56AIInWc2mhxAjTGwoElmTM8bTYKor0A769B5+Mz2/kb5E7uow16SMMtDZlZWLX4TmtlXABC6UINwa+YHqNciaP1AMG2WUUUIIp+1ZKAMECue3a1DwlBJgPkwoJjbEslAQSzmtJ5sTQMdDmtf2ksGkO43h6iLlADYs0QdWa1C5kKIXtIC4hqLZ/OCgFJCHKozme6HiIJYsZx/pSGTGvTTAZI5LqSHD4QTYAbOqMAhBPEU9v+DQQpe4iJCW5ZuYI8wwx2upSJPcOPEtyw9Clphx9iSNOk7s8tKzQGyDM7EHlwcCdn54lOjTAITgASzmiOqJPgQOYEGdEZJpc9pLYuVBDpv36NgrShKXtIhit8giA2DNNETxJUpAQE6Y5JQpDJAOEEqQbI2ZWUeAZBmIE3oNbrfW4kekHBYy8miLuFvHb++Vfc9vr1334LyKWWGGmIz0UY0pK5BGTBzyxLx9a9dQKQVEFyYfaQby+jOVKgywkyA4Q+AIR+D8ly3NgDxFRnfHCMT95pLWztyRbS3IW+GmlIY9RUULy4EAlBljQQK7x0A3logOjcydiloZjRo4Ig50EYql9kAHKCn9dfArKx5XHpuXiUunqHPhNx7l+GP0Z/cgMZEWTh6tm+JAj11+FoZBKEdhv84GNAvlKtdaQhZ3C5hSzO2EIyo8LTZwpiql8R03u9A1k+dMlfIDcyqoiqWxDUXGJn6SERQWjs1FsRlxiiUglR867rALkZeYxxai2kEskeXeHLh9Y6gZscowIkUxdQUWlA7teQVxTk7gaTve4h7dlbSLsDESzX83C3IscPqX3kiQHyJMMcGSASEDXbK0AOCYIDSGDavMOmTfcj+sGAJL6pIByQeq9PoyBIgMm+UFv70gWkmmrIASCfEaQ8QjpApsG2IlOqSKQhKwUZaUiD9UbqOaIgZjSb+Q/pX4c+0EPeY0xoSBUEJk8gQF/X+JYTBEVATPOQCwxxgDSHXSf9mym3E0QdGCVVxNx+/c3xIL21JwQRPWSGSXKtguwDsqaKbCC/sOnZUk92yxV7Rr2FTG1TLVvVALFRkjf6/yfdUBJW9JA8CC7k1FPYxchT8/OEuewhObcwxAGSRXWXbCHWrK+byQOC6JIUjdpHBEHiRs8Rmu23K0g5hgSQApM97yHtCUBsPMrxsOQmurWkej0NwfJL5+YY1nYj0ZD3bpT0eiLSkPO4H6pJIukbVSTvIRVHqyd0X20R0WGw7CFTHoYQ6sWNLQeIVH+MVVNn5SPPKnBQEmRcvnDuuaeVlPVR1d7NDrEh5nef9/ClgPzZ3vnzpg2Ecdi6wRKoAZWqsltQzxW2oRhUYUxjERYWBsupOnRj9+TOSB1AiC0M/RBZWKMs3fIJsjRblS/QrVK+QH/vHVenLR4q1CpCPFLIq5wS7rl770/uCHEzkYYQoVNPEmnIDd2bUhF7DMR2JvK9BqjHjuvS/KhMIl0YlKSIwUYQ2IhYlDRVOWtB5Gkcly0vIZEeG5ZgCGyINDCSNqmlMTrONx7TzYzBjktPsOcgkpNriBDJyj9m3HSmKaiVNa5DhGZ/bKpqOnVFWYpwWsoQlyFSdxG374l8plmSztiP74scm0CKlEjEUiIcIoaoLg32chzPvLgNkUnMZqWaEmGPcMECyJaxWZr6wzRdlUpHjIUhXUU0JcmTQRtHBewXtA6J2CSCZVU3aPaW12edUyVSKvQpS617Ir1zPCH1mBJ5SiKuCWqPYhNoRiZSpfmoQLFFdazFsZN4NjymLovJ2kzQqhGrO1FYX618IcL8DT3E89CfKZEvbBuaq0QoqYQIMtsjkWMSkWOkxhFXM5F3ZZonLaQbl7PDUYNEWMGkTUprjE+c8nojUqBZ6wVC+sG4EfLMCfgwTThj7mw463Hm9vvs49lH9faNAWd8rkRSlkYLQoq420U0EvEgMsC8LVKrLdatorv5XaENEbv+u0iVUpmyPxMZMdYWqTWdnmPXiCE2aNgWXnIy67JGaZx2Wv7QF2ejtsf+5OKyCRaBYILKj04kldG30eKeSJ8RQp3odO1BtfZYYyTSliK2GOwWrcZI5Y7sEcxwRauzERkoEdoT4fLE0LyicUTQOsIaEFmvp9NpYrNOYTU2PNwTMsDPzj7WZVvjI2Xyaxdf7y4XJxua4DpQoPKqwE95ijHS64VShLdGJjIRz7JmzEjxztjFp7XP2MaPDCXSFgsiJYSVicQQqTJDDPZqokTKNGeNE0vrF1BxEBkGWsf08FfuqPuvnN2JWi5UW0cLwXWTCCpBoESUxrwX8IBuS2moBJzzufhfA3KIcNN8+YkOcOmFBc+fD8pigpAiFpZN6gs9xqMh53tx19049TwLIroHqYaVKBG0yBDFLU3Tuoo5R3vLsCkItja2ShSVKpd3Xy9a4AK2+oY4jnlId7++b5rjPudhOAXn+KDWMnHyBRMpUpVLz0+RNvWFforHQsPGqQfmDVoQ2wXPw92eXh+t0jRJSn5TwCRBXevPW37kzG+Wi0nAl5Mrgag5KWQit4EkCkZBn8ChDTKr+YXud5E4qDZjY8IZhkIkNM3CyMRjzHny+PUzOq87hwizzLev3+IcT4q8sDIR7BrfFEFB7xTBK4Gup2kvKNjePC4ZuoDX/Y3Icnl7e3sV+EEr8G8VaOurTOTkZLm8uZnPoyjK1OHe0ls3JIKhD/RWGDkbhMjMjgGJzMz4FDRMAyJvJc9IpG+a78FrErGevxxIkQMHDhw4cOBhwP1ovY58rj0M+CQLfqNS0fLKQG/tCNYtbScmPHu+3DIV5NNbVrIgTyQry+g5zrDrut2h4+xmUln2ckVUmQryiQJ8twq2iOSW8bUTyih01nwnkUqwUlFemQry06oCVLBNJLfMd4YqHDr+TiJgwmWUV6aC/LQiRJAjklsWOV0Vdp1oVxFkjojyylSQn1YEBXkiuWVrx1Wh6/wVWyuLzKEot0wF/1Sk8x9F9ju19mew78/0u0cL4g5blNbD2qLsz6bxwIEDewrT9gKm6dpeoGu6vgd9wnT9B3pJtaz6GhaSAAAAAElFTkSuQmCC", + "public": true + } + ], + "scada": false, + "tags": [ + "mapping", + "gps", + "navigation", + "geolocation", + "satellite", + "directions" + ] +} \ No newline at end of file diff --git a/ui-ngx/src/app/core/utils.ts b/ui-ngx/src/app/core/utils.ts index 41a7c588ed..83c253f64c 100644 --- a/ui-ngx/src/app/core/utils.ts +++ b/ui-ngx/src/app/core/utils.ts @@ -512,18 +512,20 @@ export function formattedDataFormDatasourceData el.datasource.entityName) +export function formattedDataArrayFromDatasourceData(input: DatasourceData[], + groupFunction: (el: DatasourceData) => any = + (el) => el.datasource.entityName + el.datasource.entityType): FormattedData[][] { + return _(input).groupBy(groupFunction) .values().value().map((entityArray, dsIndex) => { - const timeDataMap: {[time: number]: FormattedData} = {}; + const timeDataMap: {[time: number]: FormattedData} = {}; entityArray.filter(e => e.data.length).forEach(entity => { entity.data.forEach(tsData => { const time = tsData[0]; const value = tsData[1]; let data = timeDataMap[time]; if (!data) { - const datasource = entity.datasource; - data = formattedDataFromDatasource(datasource, dsIndex); + const datasource = entity.datasource as D; + data = formattedDataFromDatasource(datasource, dsIndex); data.time = time; timeDataMap[time] = data; } diff --git a/ui-ngx/src/app/modules/home/components/widget/config/basic/map/map-basic-config.component.html b/ui-ngx/src/app/modules/home/components/widget/config/basic/map/map-basic-config.component.html index b2803c4d50..dcefd50801 100644 --- a/ui-ngx/src/app/modules/home/components/widget/config/basic/map/map-basic-config.component.html +++ b/ui-ngx/src/app/modules/home/components/widget/config/basic/map/map-basic-config.component.html @@ -16,7 +16,10 @@ --> + + , protected widgetConfigComponent: WidgetConfigComponent, private $injector: Injector, @@ -46,6 +52,14 @@ export class MapBasicConfigComponent extends BasicWidgetConfigComponent { return this.mapWidgetConfigForm; } + protected setupConfig(widgetConfig: WidgetConfigComponentData) { + const params = widgetConfig.typeParameters as any; + if (isDefinedAndNotNull(params.trip)) { + this.trip = params.trip === true; + } + super.setupConfig(widgetConfig); + } + protected setupDefaults(configData: WidgetConfigComponentData) { const settings = configData.config.settings as MapWidgetSettings; if (settings?.markers?.length) { @@ -57,6 +71,11 @@ export class MapBasicConfigComponent extends BasicWidgetConfigComponent { if (settings?.circles?.length) { settings.circles = []; } + if (this.trip) { + if (settings?.trips?.length) { + settings.trips = []; + } + } } protected onConfigSet(configData: WidgetConfigComponentData) { @@ -85,10 +104,15 @@ export class MapBasicConfigComponent extends BasicWidgetConfigComponent { actions: [configData.config.actions || {}, []] }); + if (this.trip) { + this.mapWidgetConfigForm.addControl('timewindowConfig', this.fb.control(getTimewindowConfig(configData.config))) + } } protected prepareOutputConfig(config: any): WidgetConfigComponentData { - + if (this.trip) { + setTimewindowConfig(this.widgetConfig.config, config.timewindowConfig); + } this.widgetConfig.config.settings = config.mapSettings || {}; this.widgetConfig.config.showTitle = config.showTitle; diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/maps/data-layer/map-data-layer.ts b/ui-ngx/src/app/modules/home/components/widget/lib/maps/data-layer/map-data-layer.ts index 2d29de3e79..8916931d2b 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/maps/data-layer/map-data-layer.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/maps/data-layer/map-data-layer.ts @@ -306,12 +306,13 @@ export abstract class TbDataLayerItem; private pattern: string; diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/maps/data-layer/trips-data-layer.ts b/ui-ngx/src/app/modules/home/components/widget/lib/maps/data-layer/trips-data-layer.ts new file mode 100644 index 0000000000..354fa4eb0f --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/widget/lib/maps/data-layer/trips-data-layer.ts @@ -0,0 +1,416 @@ +/// +/// 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 { + DataLayerColorProcessor, + DataLayerPatternProcessor, + MapDataLayerType, TbDataLayerItem, + TbMapDataLayer, + UnplacedMapDataItem +} from '@home/components/widget/lib/maps/data-layer/map-data-layer'; +import { + BaseMarkerShapeSettings, ClusterMarkerColorFunction, + DataLayerColorType, defaultBaseMarkersDataLayerSettings, defaultBaseTripsDataLayerSettings, + loadImageWithAspect, + MapStringFunction, MapType, + MarkerIconInfo, + MarkerIconSettings, + MarkerImageFunction, + MarkerImageInfo, + MarkerImageSettings, + MarkerImageType, + MarkerPositionFunction, MarkersDataLayerSettings, + MarkerShapeSettings, + MarkerType, + TbMapDatasource, + TripsDataLayerSettings +} from '@home/components/widget/lib/maps/models/map.models'; +import { forkJoin, Observable, of } from 'rxjs'; +import { FormattedData } from '@shared/models/widget.models'; +import { + createColorMarkerIconElement, + createColorMarkerShapeURI, + MarkerShape +} from '@home/components/widget/lib/maps/models/marker-shape.models'; +import tinycolor from 'tinycolor2'; +import { MatIconRegistry } from '@angular/material/icon'; +import { DomSanitizer } from '@angular/platform-browser'; +import { catchError, map, switchMap } from 'rxjs/operators'; +import L from 'leaflet'; +import { CompiledTbFunction } from '@shared/models/js-function.models'; +import { isDefined, isDefinedAndNotNull, parseTbFunction, safeExecuteTbFunction } from '@core/utils'; +import { ImagePipe } from '@shared/pipe/image.pipe'; +import { TbMap } from '@home/components/widget/lib/maps/map'; +import { DataKeyType } from '@shared/models/telemetry/telemetry.models'; + +abstract class TripMarkerIconProcessor { + + static fromSettings(dataLayer: TbTripsDataLayer, + settings: TripsDataLayerSettings): TripMarkerIconProcessor { + switch (settings.markerType) { + case MarkerType.shape: + return new ShapeTripMarkerIconProcessor(dataLayer, settings.markerShape); + case MarkerType.icon: + return new IconTripMarkerIconProcessor(dataLayer, settings.markerIcon); + case MarkerType.image: + return new ImageTripMarkerIconProcessor(dataLayer, settings.markerImage); + } + } + + protected constructor(protected dataLayer: TbTripsDataLayer, + protected settings: S) {} + + public abstract setup(): Observable; + + public abstract createMarkerIcon(data: FormattedData, + dsData: FormattedData[]): Observable; + +} + +abstract class BaseColorTripMarkerShapeProcessor extends TripMarkerIconProcessor { + + private markerColorFunction: CompiledTbFunction; + + protected constructor(protected dataLayer: TbTripsDataLayer, + protected settings: S) { + super(dataLayer, settings); + } + + public setup(): Observable { + const colorSettings = this.settings.color; + if (colorSettings.type === DataLayerColorType.function) { + return parseTbFunction(this.dataLayer.getCtx().http, colorSettings.colorFunction, ['data', 'dsData']).pipe( + map((parsed) => { + this.markerColorFunction = parsed; + return null; + }) + ); + } else { + return of(null); + } + } + + public createMarkerIcon(data: FormattedData, dsData: FormattedData[]): Observable { + const colorSettings = this.settings.color; + let color: tinycolor.Instance; + if (colorSettings.type === DataLayerColorType.function) { + const functionColor = safeExecuteTbFunction(this.markerColorFunction, [data, dsData]); + if (isDefinedAndNotNull(functionColor)) { + color = tinycolor(functionColor); + } else { + color = tinycolor(colorSettings.color); + } + } else { + color = tinycolor(colorSettings.color); + } + return this.createMarkerShape(color, data.rotationAngle, this.settings.size); + } + + protected abstract createMarkerShape(color: tinycolor.Instance, rotationAngle: number, size: number): Observable; +} + +class ShapeTripMarkerIconProcessor extends BaseColorTripMarkerShapeProcessor { + + constructor(protected dataLayer: TbTripsDataLayer, + protected settings: MarkerShapeSettings) { + super(dataLayer, settings); + } + + protected createMarkerShape(color: tinycolor.Instance, rotationAngle: number, size: number): Observable { + return this.dataLayer.createColoredMarkerShape(this.settings.shape, color, rotationAngle, size); + } + +} + +class IconTripMarkerIconProcessor extends BaseColorTripMarkerShapeProcessor { + + constructor(protected dataLayer: TbTripsDataLayer, + protected settings: MarkerIconSettings) { + super(dataLayer, settings); + } + + protected createMarkerShape(color: tinycolor.Instance, rotationAngle: number, size: number): Observable { + return this.dataLayer.createColoredMarkerIcon(this.settings.icon, color, rotationAngle, size); + } + +} + +class ImageTripMarkerIconProcessor extends TripMarkerIconProcessor { + + private markerImageFunction: CompiledTbFunction; + + constructor(protected dataLayer: TbTripsDataLayer, + protected settings: MarkerImageSettings) { + super(dataLayer, settings); + } + + public setup(): Observable { + if (this.settings.type === MarkerImageType.function) { + return parseTbFunction(this.dataLayer.getCtx().http, this.settings.imageFunction, ['data', 'images', 'dsData']).pipe( + map((parsed) => { + this.markerImageFunction = parsed; + return null; + }) + ); + } else { + return of(null); + } + } + + public createMarkerIcon(data: FormattedData, dsData: FormattedData[]): Observable { + if (this.settings.type === MarkerImageType.function) { + const currentImage: MarkerImageInfo = safeExecuteTbFunction(this.markerImageFunction, [data, this.settings.images, dsData]); + return this.loadMarkerIconInfo(currentImage, data.rotationAngle); + } else { + const currentImage: MarkerImageInfo = { + url: this.settings.image, + size: this.settings.imageSize || 34 + }; + return this.loadMarkerIconInfo(currentImage, data.rotationAngle); + } + } + + private loadMarkerIconInfo(image: MarkerImageInfo, rotationAngle = 0): Observable { + if (image && image.url) { + return loadImageWithAspect(this.dataLayer.getCtx().$injector.get(ImagePipe), image.url).pipe( + switchMap((aspectImage) => { + if (aspectImage?.aspect) { + let width: number; + let height: number; + if (aspectImage.aspect > 1) { + width = image.size; + height = image.size / aspectImage.aspect; + } else { + width = image.size * aspectImage.aspect; + height = image.size; + } + let iconAnchor = image.markerOffset; + let popupAnchor = image.tooltipOffset; + if (!iconAnchor) { + iconAnchor = [width * this.dataLayer.markerOffset[0], height * this.dataLayer.markerOffset[1]]; + } + if (!popupAnchor) { + popupAnchor = [width * this.dataLayer.tooltipOffset[0], height * this.dataLayer.tooltipOffset[1]]; + } + const style = `background-image: url(${aspectImage.url}); transform: rotate(${rotationAngle}deg); height: ${height}px; width: ${width}px;`; + const icon = L.divIcon({ + html: `
`, + className: 'tb-marker-div-icon', + iconSize: [width, height], + iconAnchor, + popupAnchor + }); + const iconInfo: MarkerIconInfo = { + size: [width, height], + icon + }; + return of(iconInfo); + } else { + return this.dataLayer.createDefaultMarkerIcon(rotationAngle); + } + }), + catchError(() => this.dataLayer.createDefaultMarkerIcon(rotationAngle)) + ); + } else { + return this.dataLayer.createDefaultMarkerIcon(rotationAngle); + } + } +} + +export class TbTripsDataLayer extends TbMapDataLayer { + + public tripMarkerIconProcessor: TripMarkerIconProcessor; + + public markerOffset: L.LatLngTuple; + public tooltipOffset: L.LatLngTuple; + + public pathStrokeColorProcessor: DataLayerColorProcessor; + public pointColorProcessor: DataLayerColorProcessor; + public pointTooltipProcessor: DataLayerPatternProcessor; + + private positionFunction: CompiledTbFunction; + + private rawTripsData: FormattedData[][]; + private latestTripsData: FormattedData[]; + + constructor(protected map: TbMap, + inputSettings: TripsDataLayerSettings) { + super(map, inputSettings); + } + + public dataLayerType(): MapDataLayerType { + return MapDataLayerType.trip; + } + + public placeItem(item: UnplacedMapDataItem, layer: L.Layer): void { + throw new Error('Not implemented!'); + } + + protected setupDatasource(datasource: TbMapDatasource): TbMapDatasource { + datasource.dataKeys = [this.settings.xKey, this.settings.yKey]; + if (this.settings.additionalDataKeys?.length) { + const tsKeys = this.settings.additionalDataKeys.filter(key => key.type === DataKeyType.timeseries); + const latestKeys = this.settings.additionalDataKeys.filter(key => key.type !== DataKeyType.timeseries); + datasource.dataKeys.push(...tsKeys); + if (latestKeys.length) { + datasource.latestDataKeys = latestKeys; + } + } + return datasource; + } + + protected defaultBaseSettings(map: TbMap): Partial { + return defaultBaseTripsDataLayerSettings(map.type()); + } + + protected doSetup(): Observable { + this.markerOffset = [ + isDefined(this.settings.markerOffsetX) ? this.settings.markerOffsetX : 0.5, + isDefined(this.settings.markerOffsetY) ? this.settings.markerOffsetY : 1, + ]; + this.tooltipOffset = [ + isDefined(this.settings.tooltip?.offsetX) ? this.settings.tooltip?.offsetX : 0, + isDefined(this.settings.tooltip?.offsetY) ? this.settings.tooltip?.offsetY : -1, + ]; + this.tripMarkerIconProcessor = TripMarkerIconProcessor.fromSettings(this, this.settings); + const setup$: Observable[] = []; + if (this.map.type() === MapType.image) { + setup$.push( + parseTbFunction(this.getCtx().http, this.settings.positionFunction, ['origXPos', 'origYPos', 'data', 'dsData', 'aspect']).pipe( + map((parsed) => { + this.positionFunction = parsed; + return null; + }) + ) + ); + } + setup$.push(this.tripMarkerIconProcessor.setup()); + + if (this.settings.showPath) { + this.pathStrokeColorProcessor = new DataLayerColorProcessor(this, this.settings.pathStrokeColor); + setup$.push(this.pathStrokeColorProcessor.setup()); + } + + if (this.settings.showPoints) { + this.pointColorProcessor = new DataLayerColorProcessor(this, this.settings.pointColor); + setup$.push(this.pointColorProcessor.setup()); + if (this.settings.pointTooltip?.show) { + this.pointTooltipProcessor = new DataLayerPatternProcessor(this, this.settings.pointTooltip); + setup$.push(this.pointTooltipProcessor.setup()); + } + } + return forkJoin(setup$).pipe(map(() => null)); + } + + protected isValidLayerData(_layerData: FormattedData): boolean { + throw new Error('Not implemented!'); + } + + protected createLayerItem(_data: FormattedData, _dsData: FormattedData[]): TbDataLayerItem { + throw new Error('Not implemented!'); + } + + public updateData(_dsData: FormattedData[]) { + throw new Error('Not implemented!'); + } + + public prepareTripsData(tripsData: FormattedData[][], tripsLatestData: FormattedData[]): {minTime: number; maxTime: number} { + let minTime = Infinity; + let maxTime = -Infinity; + this.rawTripsData = + tripsData.filter(d => d[0].$datasource.mapDataIds.includes(this.mapDataId)).map( + item => this.clearIncorrectFirsLastDatapoint(item)).filter(arr => arr.length); + this.latestTripsData = tripsLatestData.filter(d => d.$datasource.mapDataIds.includes(this.mapDataId)); + this.rawTripsData.forEach((dataSource) => { + minTime = Math.min(dataSource[0].time, minTime); + maxTime = Math.max(dataSource[dataSource.length - 1].time, maxTime); + }); + + return {minTime, maxTime}; + } + + public updateTrips(minTime: number, maxTime: number) { + console.log(`Update trips: min(${minTime}), max(${maxTime})`); + } + + public updateTripsLatestData(tripsLatestData: FormattedData[]) { + this.latestTripsData = tripsLatestData.filter(d => d.$datasource.mapDataIds.includes(this.mapDataId)); + } + + private clearIncorrectFirsLastDatapoint(dataSource: FormattedData[]): FormattedData[] { + const firstHistoricalDataIndexCoordinate = dataSource.findIndex(this.findFirstHistoricalDataIndexCoordinate); + if (firstHistoricalDataIndexCoordinate === -1) { + return []; + } + let lastIndex = dataSource.length - 1; + for (lastIndex; lastIndex > 0; lastIndex--) { + if (this.findFirstHistoricalDataIndexCoordinate(dataSource[lastIndex])) { + lastIndex++; + break; + } + } + if (firstHistoricalDataIndexCoordinate > 0 || lastIndex < dataSource.length) { + return dataSource.slice(firstHistoricalDataIndexCoordinate, lastIndex); + } + return dataSource; + } + + private findFirstHistoricalDataIndexCoordinate = (item: FormattedData): boolean => { + return isDefined(item[this.settings.xKey.label]) && isDefined(item[this.settings.yKey.label]); + } + + public createDefaultMarkerIcon(rotationAngle = 0): Observable { + const color = this.settings.markerShape?.color?.color || '#307FE5'; + return this.createColoredMarkerShape(MarkerShape.markerShape1, tinycolor(color), rotationAngle); + } + + public createColoredMarkerShape(shape: MarkerShape, color: tinycolor.Instance, rotationAngle = 0, size = 34): Observable { + return createColorMarkerShapeURI(this.getCtx().$injector.get(MatIconRegistry), this.getCtx().$injector.get(DomSanitizer), shape, color).pipe( + map((iconUrl) => { + const style = `background-image: url(${iconUrl}); transform: rotate(${rotationAngle}deg); height: ${size}px; width: ${size}px;`; + return { + size: [size, size], + icon: L.divIcon({ + html: `
`, + className: 'tb-marker-div-icon', + iconSize: [size, size], + iconAnchor: [size * this.markerOffset[0], size * this.markerOffset[1]], + popupAnchor: [size * this.tooltipOffset[0], size * this.tooltipOffset[1]] + }) + }; + }) + ); + } + + public createColoredMarkerIcon(icon: string, color: tinycolor.Instance, rotationAngle = 0, size = 34): Observable { + return createColorMarkerIconElement(this.getCtx().$injector.get(MatIconRegistry), this.getCtx().$injector.get(DomSanitizer), icon, color).pipe( + map((element) => { + element.style.transform = `rotate(${rotationAngle}deg)`; + return { + size: [size, size], + icon: L.divIcon({ + html: element.outerHTML, + className: 'tb-marker-div-icon', + iconSize: [size, size], + iconAnchor: [size * this.markerOffset[0], size * this.markerOffset[1]], + popupAnchor: [size * this.tooltipOffset[0], size * this.tooltipOffset[1]] + }) + }; + }) + ); + } + +} diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/maps/map.scss b/ui-ngx/src/app/modules/home/components/widget/lib/maps/map.scss index 1baeaffb87..ac8684e2bb 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/maps/map.scss +++ b/ui-ngx/src/app/modules/home/components/widget/lib/maps/map.scss @@ -15,7 +15,7 @@ */ @import '../../../../../../../scss/constants'; -.tb-map-layout { +.tb-map-container, .tb-map-layout { position: relative; display: flex; width: 100%; @@ -23,6 +23,13 @@ min-width: 0; min-height: 0; flex: 1; +} + +.tb-map-container { + flex-direction: column; +} + +.tb-map-layout { &.tb-sidebar-left { flex-direction: row-reverse; } 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 b3b2ebe343..968f911c1f 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 @@ -28,7 +28,13 @@ import { TbPolygonRawCoordinates } from '@home/components/widget/lib/maps/models/map.models'; import { WidgetContext } from '@home/models/widget-component.models'; -import { formattedDataFormDatasourceData, isDefinedAndNotNull, mergeDeepIgnoreArray } from '@core/utils'; +import { + formattedDataArrayFromDatasourceData, + formattedDataFormDatasourceData, + isDefined, + isDefinedAndNotNull, + mergeDeepIgnoreArray +} from '@core/utils'; import { DeepPartial } from '@shared/models/common'; import L from 'leaflet'; import { forkJoin, Observable, of } from 'rxjs'; @@ -61,6 +67,9 @@ import { DomSanitizer } from '@angular/platform-browser'; import tinycolor from 'tinycolor2'; import ITooltipsterInstance = JQueryTooltipster.ITooltipsterInstance; import TooltipPositioningSide = JQueryTooltipster.TooltipPositioningSide; +import { MapTimelinePanelComponent } from '@home/components/widget/lib/maps/panels/map-timeline-panel.component'; +import { ComponentRef } from '@angular/core'; +import { TbTripsDataLayer } from '@home/components/widget/lib/maps/data-layer/trips-data-layer'; type TooltipInstancesData = {root: HTMLElement, instances: ITooltipsterInstance[]}; @@ -76,11 +85,13 @@ export abstract class TbMap { protected southWest = new L.LatLng(-L.Projection.SphericalMercator['MAX_LATITUDE'], -180); protected northEast = new L.LatLng(L.Projection.SphericalMercator['MAX_LATITUDE'], 180); - protected dataLayers: TbMapDataLayer[]; + protected dataLayers: TbMapDataLayer[]; + protected tripDataLayers: TbTripsDataLayer[]; protected dsData: FormattedData[]; protected selectedDataItem: TbDataLayerItem; + protected mapLayoutElement: HTMLElement; protected mapElement: HTMLElement; protected sidebar: L.TB.SidebarControl; @@ -93,6 +104,9 @@ export abstract class TbMap { protected addPolygonButton: L.TB.ToolbarButton; protected addCircleButton: L.TB.ToolbarButton; + protected timeLineComponentRef: ComponentRef; + protected timeLineComponent: MapTimelinePanelComponent; + protected addMarkerDataLayers: TbMapDataLayer[]; protected addPolygonDataLayers: TbMapDataLayer[]; protected addCircleDataLayers: TbMapDataLayer[]; @@ -114,9 +128,27 @@ export abstract class TbMap { this.settings = mergeDeepIgnoreArray({} as S, this.defaultSettings(), this.inputSettings as S); $(containerElement).empty(); - $(containerElement).addClass('tb-map-layout'); + $(containerElement).addClass('tb-map-container'); + const mapLayoutElement = $('
'); + this.mapLayoutElement = mapLayoutElement[0]; + $(containerElement).append(mapLayoutElement); + + if (this.settings.tripTimeline?.showTimelineControl) { + this.timeLineComponentRef = this.ctx.widgetContentContainer.createComponent(MapTimelinePanelComponent); + this.timeLineComponent = this.timeLineComponentRef.instance; + this.timeLineComponent.settings = this.settings.tripTimeline; + this.timeLineComponent.timeChanged.subscribe((time) => { + console.log(`Time updated: ${time}`); + }); + const parentElement = this.timeLineComponentRef.instance.element.nativeElement; + const content = parentElement.firstChild; + parentElement.removeChild(content); + parentElement.style.display = 'none'; + containerElement.append(content); + } + const mapElement = $('
'); - $(containerElement).append(mapElement); + mapLayoutElement.append(mapElement); this.mapResize$ = new ResizeObserver(() => { this.resize(); @@ -173,6 +205,7 @@ export abstract class TbMap { private setupDataLayers() { this.dataLayers = []; + this.tripDataLayers = []; if (this.settings.markers) { this.dataLayers.push(...this.settings.markers.map(settings => new TbMarkersDataLayer(this, settings))); } @@ -182,10 +215,14 @@ export abstract class TbMap { if (this.settings.circles) { this.dataLayers.push(...this.settings.circles.map(settings => new TbCirclesDataLayer(this, settings))); } - if (this.dataLayers.length) { + if (this.settings.trips) { + this.tripDataLayers.push(...this.settings.trips.map(settings => new TbTripsDataLayer(this, settings))); + } + if (this.dataLayers.length || this.tripDataLayers.length) { const groupsMap = new Map(); const customTranslate = this.ctx.$injector.get(CustomTranslatePipe); - this.dataLayers.forEach(dl => { + const allDataLayers = [...this.dataLayers, ...this.tripDataLayers]; + allDataLayers.forEach(dl => { dl.getGroups().forEach(group => { let groupData = groupsMap.get(group); if (!groupData) { @@ -217,7 +254,7 @@ export abstract class TbMap { }); } - const setup = this.dataLayers.map(dl => dl.setup()); + const setup = allDataLayers.map(dl => dl.setup()); forkJoin(setup).subscribe( () => { let datasources: TbMapDatasource[]; @@ -231,35 +268,77 @@ export abstract class TbMap { } const additionalDatasources = additionalMapDataSourcesToDatasources(this.settings.additionalDataSources); datasources = mergeMapDatasources(datasources, additionalDatasources); - const dataLayersSubscriptionOptions: WidgetSubscriptionOptions = { - datasources, - hasDataPageLink: true, - useDashboardTimewindow: false, - type: widgetType.latest, - callbacks: { - onDataUpdated: (subscription) => { - this.update(subscription); + if (datasources.length) { + const dataLayersSubscriptionOptions: WidgetSubscriptionOptions = { + datasources, + hasDataPageLink: true, + useDashboardTimewindow: false, + type: widgetType.latest, + callbacks: { + onDataUpdated: (subscription) => { + this.update(subscription); + } } - } - }; - this.ctx.subscriptionApi.createSubscription(dataLayersSubscriptionOptions, false).subscribe( - (dataLayersSubscription) => { - let pageSize = this.settings.mapPageSize; - if (isDefinedAndNotNull(this.ctx.widgetConfig.pageSize)) { - pageSize = Math.max(pageSize, this.ctx.widgetConfig.pageSize); + }; + this.ctx.subscriptionApi.createSubscription(dataLayersSubscriptionOptions, false).subscribe( + (dataLayersSubscription) => { + let pageSize = this.settings.mapPageSize; + if (isDefinedAndNotNull(this.ctx.widgetConfig.pageSize)) { + pageSize = Math.max(pageSize, this.ctx.widgetConfig.pageSize); + } + const pageLink: EntityDataPageLink = { + page: 0, + pageSize, + textSearch: null, + dynamic: true + }; + dataLayersSubscription.paginatedDataSubscriptionUpdated.subscribe(() => { + // this.map.resetState(); + }); + dataLayersSubscription.subscribeAllForPaginatedData(pageLink, null); } - const pageLink: EntityDataPageLink = { - page: 0, - pageSize, - textSearch: null, - dynamic: true - }; - dataLayersSubscription.paginatedDataSubscriptionUpdated.subscribe(() => { - // this.map.resetState(); - }); - dataLayersSubscription.subscribeAllForPaginatedData(pageLink, null); + ); + } + if (this.tripDataLayers.length) { + const tripDatasources = this.tripDataLayers.map(dl => dl.getDatasource()); + const tripDataLayersSubscriptionOptions: WidgetSubscriptionOptions = { + datasources: tripDatasources, + hasDataPageLink: true, + useDashboardTimewindow: isDefined(this.ctx.widgetConfig.useDashboardTimewindow) + ? this.ctx.widgetConfig.useDashboardTimewindow : true, + type: widgetType.timeseries, + callbacks: { + onDataUpdated: (subscription) => { + this.updateTrips(subscription); + }, + onLatestDataUpdated: (subscription) => { + this.updateTripsWithLatestData(subscription); + } + } + }; + if (!tripDataLayersSubscriptionOptions.useDashboardTimewindow) { + tripDataLayersSubscriptionOptions.timeWindowConfig = this.ctx.widgetConfig.timewindow; } - ); + + this.ctx.subscriptionApi.createSubscription(tripDataLayersSubscriptionOptions, false).subscribe( + (tripDataLayersSubscription) => { + let pageSize = this.settings.mapPageSize; + if (isDefinedAndNotNull(this.ctx.widgetConfig.pageSize)) { + pageSize = Math.max(pageSize, this.ctx.widgetConfig.pageSize); + } + const pageLink: EntityDataPageLink = { + page: 0, + pageSize, + textSearch: null, + dynamic: true + }; + tripDataLayersSubscription.paginatedDataSubscriptionUpdated.subscribe(() => { + // this.map.resetState(); + }); + tripDataLayersSubscription.subscribeAllForPaginatedData(pageLink, null); + } + ); + } } ); } @@ -582,10 +661,51 @@ export abstract class TbMap { this.dsData = formattedDataFormDatasourceData(subscription.data, undefined, undefined, el => el.datasource.entityId + el.datasource.mapDataIds[0]); this.dataLayers.forEach(dl => dl.updateData(this.dsData)); + this.updateTripsAppearance(); + this.updateTripsTimeline(); this.updateBounds(); this.updateAddButtonsStates(); } + private updateTrips(subscription: IWidgetSubscription) { + const tripsData = formattedDataArrayFromDatasourceData(subscription.data, el => el.datasource.entityId + el.datasource.mapDataIds[0]); + const tripsLatestData = formattedDataFormDatasourceData(subscription.latestData, + undefined, undefined, el => el.datasource.entityId + el.datasource.mapDataIds[0]); + + let minTime = Infinity; + let maxTime = -Infinity; + for (const tripsDataLayer of this.tripDataLayers) { + const minMax = tripsDataLayer.prepareTripsData(tripsData, tripsLatestData); + minTime = Math.min(minMax.minTime, minTime); + maxTime = Math.max(minMax.maxTime, maxTime); + } + this.tripDataLayers.forEach(dl => dl.updateTrips(minTime, maxTime)); + this.updateTripsTimeline(minTime, maxTime); + this.updateBounds(); + } + + private updateTripsWithLatestData(subscription: IWidgetSubscription) { + const tripsLatestData = formattedDataFormDatasourceData(subscription.latestData, + undefined, undefined, el => el.datasource.entityId + el.datasource.mapDataIds[0]); + this.tripDataLayers.forEach(dl => dl.updateTripsLatestData(tripsLatestData)); + this.updateTripsAppearance(); + this.updateTripsTimeline(); + } + + private updateTripsAppearance() {} + + private updateTripsTimeline(minTime?: number, maxTime?: number) { + if (this.settings.tripTimeline?.showTimelineControl) { + if (isDefinedAndNotNull(minTime) && isDefinedAndNotNull(maxTime)) { + this.timeLineComponent.min = minTime; + this.timeLineComponent.max = maxTime; + } + if (this.settings.tripTimeline.snapToRealLocation) { + // Recalculate anchors only for enabled layers + } + } + } + private resize() { this.onResize(); this.map?.invalidateSize(); @@ -659,7 +779,7 @@ export abstract class TbMap { protected getSidebar(): L.TB.SidebarControl { if (!this.sidebar) { this.sidebar = L.TB.sidebar({ - container: $(this.containerElement), + container: $(this.mapLayoutElement), position: this.settings.controlsPosition, paneWidth: 220 }).addTo(this.map); @@ -685,6 +805,7 @@ export abstract class TbMap { public enabledDataLayersUpdated() { this.updateAddButtonsStates(); + this.updateTripsTimeline(); } public dataItemClick($event: Event, action: WidgetAction, entityInfo: TbMapDatasource) { @@ -787,6 +908,9 @@ export abstract class TbMap { instance.destroy(); }) }); + if (this.timeLineComponentRef) { + this.timeLineComponentRef.destroy(); + } } public abstract locationDataToLatLng(position: {x: number; y: number}): L.LatLng; diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/maps/models/map.models.ts b/ui-ngx/src/app/modules/home/components/widget/lib/maps/models/map.models.ts index 31428c682c..69c38bfbf7 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/maps/models/map.models.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/maps/models/map.models.ts @@ -33,6 +33,7 @@ import { map } from 'rxjs/operators'; import { ImagePipe } from '@shared/pipe/image.pipe'; import { MarkerShape } from '@home/components/widget/lib/maps/models/marker-shape.models'; import { UnplacedMapDataItem } from '@home/components/widget/lib/maps/data-layer/map-data-layer'; +import { DateFormatSettings, simpleDateFormat } from '@shared/models/widget-settings.models'; export enum MapType { geoMap = 'geoMap', @@ -368,6 +369,83 @@ export const defaultBaseMarkersDataLayerSettings = (mapType: MapType): Partial( + [ + [PathDecoratorSymbol.arrowHead, 'widgets.maps.data-layer.decorator-symbol-arrow-head'], + [PathDecoratorSymbol.dash, 'widgets.maps.data-layer.decorator-symbol-dash'] + ] +); + +export interface TripsDataLayerSettings extends MarkersDataLayerSettings { + rotateMarker: boolean; + offsetAngle: number; + showPath: boolean; + pathStrokeWeight?: number; + pathStrokeColor?: DataLayerColorSettings; + usePathDecorator?: boolean; + pathDecoratorSymbol?: PathDecoratorSymbol; + pathDecoratorSymbolSize?: number; + pathDecoratorSymbolColor?: string; + pathDecoratorOffset?: number; + pathEndDecoratorOffset?: number; + pathDecoratorRepeat?: number; + showPoints: boolean; + pointSize?: number; + pointColor?: DataLayerColorSettings; + pointTooltip?: DataLayerTooltipSettings; +} + +export const defaultBaseTripsDataLayerSettings = (mapType: MapType): Partial => mergeDeep( + defaultBaseMarkersDataLayerSettings(mapType), + { + tooltip: { + pattern: mapType === MapType.geoMap ? + '${entityName}

Latitude: ${latitude:7}
Longitude: ${longitude:7}
End Time: ${maxTime}
Start Time: ${minTime}' + : '${entityName}

X Pos: ${xPos:2}
Y Pos: ${yPos:2}
End Time: ${maxTime}
Start Time: ${minTime}', + }, + rotateMarker: true, + offsetAngle: 0, + markerOffsetX: 0.5, + markerOffsetY: 0.5, + showPath: true, + pathStrokeWeight: 2, + pathStrokeColor: { + type: DataLayerColorType.constant, + color: '#307FE5', + }, + usePathDecorator: false, + pathDecoratorSymbol: PathDecoratorSymbol.arrowHead, + pathDecoratorSymbolSize: 10, + pathDecoratorSymbolColor: '#307FE5', + pathDecoratorOffset: 20, + pathEndDecoratorOffset: 20, + pathDecoratorRepeat: 20, + showPoints: false, + pointSize: 10, + pointColor: { + type: DataLayerColorType.constant, + color: '#307FE5', + }, + pointTooltip: { + show: true, + trigger: DataLayerTooltipTrigger.click, + autoclose: true, + type: DataLayerPatternType.pattern, + pattern: mapType === MapType.geoMap ? + '${entityName}

Latitude: ${latitude:7}
Longitude: ${longitude:7}
End Time: ${maxTime}
Start Time: ${minTime}' + : '${entityName}

X Pos: ${xPos:2}
Y Pos: ${yPos:2}
End Time: ${maxTime}
Start Time: ${minTime}', + offsetX: 0, + offsetY: -1 + }, + } as TripsDataLayerSettings); + export interface ShapeDataLayerSettings extends MapDataLayerSettings { fillColor: DataLayerColorSettings; strokeColor: DataLayerColorSettings; @@ -525,8 +603,19 @@ export const mapZoomActionTranslationMap = new Map( ] ); +export interface TripTimelineSettings { + showTimelineControl: boolean; + timeStep: number; + speedOptions: number[]; + showTimestamp: boolean; + timestampFormat: DateFormatSettings; + snapToRealLocation: boolean; + locationSnapFilter: TbFunction; +} + export interface BaseMapSettings { mapType: MapType; + trips?: TripsDataLayerSettings[]; markers: MarkersDataLayerSettings[]; polygons: PolygonsDataLayerSettings[]; circles: CirclesDataLayerSettings[]; @@ -539,6 +628,7 @@ export interface BaseMapSettings { defaultZoomLevel: number; minZoomLevel: number; mapPageSize: number; + tripTimeline?: TripTimelineSettings; } export const DEFAULT_MAP_PAGE_SIZE = 16384; @@ -546,6 +636,7 @@ export const DEFAULT_ZOOM_LEVEL = 8; export const defaultBaseMapSettings: BaseMapSettings = { mapType: MapType.geoMap, + trips: [], markers: [], polygons: [], circles: [], @@ -557,7 +648,16 @@ export const defaultBaseMapSettings: BaseMapSettings = { defaultCenterPosition: '0,0', defaultZoomLevel: null, minZoomLevel: 16, - mapPageSize: DEFAULT_MAP_PAGE_SIZE + mapPageSize: DEFAULT_MAP_PAGE_SIZE, + tripTimeline: { + showTimelineControl: false, + timeStep: 1000, + speedOptions: [1,5,10,15,25], + showTimestamp: true, + timestampFormat: simpleDateFormat('yyyy-MM-dd HH:mm:ss'), + snapToRealLocation: false, + locationSnapFilter: 'return true;' + } }; export enum MapProvider { diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/maps/panels/map-timeline-panel.component.html b/ui-ngx/src/app/modules/home/components/widget/lib/maps/panels/map-timeline-panel.component.html new file mode 100644 index 0000000000..543f07a3d7 --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/widget/lib/maps/panels/map-timeline-panel.component.html @@ -0,0 +1,26 @@ + +
+ + + +
diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/maps/panels/map-timeline-panel.component.scss b/ui-ngx/src/app/modules/home/components/widget/lib/maps/panels/map-timeline-panel.component.scss new file mode 100644 index 0000000000..9e38394deb --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/widget/lib/maps/panels/map-timeline-panel.component.scss @@ -0,0 +1,19 @@ +/** + * 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. + */ +.tb-map-timeline-panel { + display: flex; + flex-direction: column; +} diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/maps/panels/map-timeline-panel.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/maps/panels/map-timeline-panel.component.ts new file mode 100644 index 0000000000..33cac7c012 --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/widget/lib/maps/panels/map-timeline-panel.component.ts @@ -0,0 +1,68 @@ +/// +/// 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, + ElementRef, + EventEmitter, + Input, + OnDestroy, + OnInit, + Output, + ViewEncapsulation +} from '@angular/core'; +import { TripTimelineSettings } from '@home/components/widget/lib/maps/models/map.models'; + +@Component({ + selector: 'tb-map-timeline-panel', + templateUrl: './map-timeline-panel.component.html', + styleUrls: ['./map-timeline-panel.component.scss'], + encapsulation: ViewEncapsulation.None +}) +export class MapTimelinePanelComponent implements OnInit, OnDestroy { + + @Input() + settings: TripTimelineSettings; + + @Input() + disabled = false; + + @Input() + min = 0; + + @Input() + max = 10000; + + @Output() + timeChanged = new EventEmitter(); + + currentTime = 0; + + constructor(public element: ElementRef) { + } + + ngOnInit() { + } + + ngOnDestroy() { + } + + public onTimeChange() { + //this.updateValueText(); + this.timeChanged.next(this.currentTime); + } + +} diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/map/map-settings.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/map/map-settings.component.ts index e92a39b863..710af3478b 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/map/map-settings.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/map/map-settings.component.ts @@ -79,6 +79,10 @@ export class MapSettingsComponent implements OnInit, ControlValueAccessor, Valid MapType = MapType; + @Input() + @coerceBoolean() + trip = false; + @Input() disabled: boolean; diff --git a/ui-ngx/src/app/modules/home/components/widget/widget-components.module.ts b/ui-ngx/src/app/modules/home/components/widget/widget-components.module.ts index 58a3144f9e..cdc829a96f 100644 --- a/ui-ngx/src/app/modules/home/components/widget/widget-components.module.ts +++ b/ui-ngx/src/app/modules/home/components/widget/widget-components.module.ts @@ -93,6 +93,7 @@ import { MapWidgetComponent } from '@home/components/widget/lib/maps/map-widget. import { SelectMapEntityPanelComponent } from '@home/components/widget/lib/maps/panels/select-map-entity-panel.component'; +import { MapTimelinePanelComponent } from '@home/components/widget/lib/maps/panels/map-timeline-panel.component'; @NgModule({ declarations: [ @@ -149,6 +150,7 @@ import { NotificationTypeFilterPanelComponent, ScadaSymbolWidgetComponent, SelectMapEntityPanelComponent, + MapTimelinePanelComponent, MapWidgetComponent ], imports: [ diff --git a/ui-ngx/src/app/modules/home/components/widget/widget-config.component.html b/ui-ngx/src/app/modules/home/components/widget/widget-config.component.html index 9943f1ec83..a3d924fe08 100644 --- a/ui-ngx/src/app/modules/home/components/widget/widget-config.component.html +++ b/ui-ngx/src/app/modules/home/components/widget/widget-config.component.html @@ -278,6 +278,12 @@
+ + + +
widget-config.data-settings
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 aece3d525f..dfe60066e0 100644 --- a/ui-ngx/src/assets/locale/locale.constant-en_US.json +++ b/ui-ngx/src/assets/locale/locale.constant-en_US.json @@ -7858,6 +7858,10 @@ "place-marker": "Place marker", "place-marker-hint": "Click to place '{{entityName}}' entity" }, + "path": { + "decorator-symbol-arrow-head": "Arrow", + "decorator-symbol-dash": "Dash" + }, "polygon": { "polygon-key": "Polygon key", "polygon-key-required": "Polygon key required", From b9674cadd1c5f45ab243ae6225565ed007142e2b Mon Sep 17 00:00:00 2001 From: dashevchenko Date: Thu, 27 Feb 2025 11:50:07 +0200 Subject: [PATCH 105/345] added edqs volumes --- docker/compose-utils.sh | 1 + docker/docker-compose.edqs.volumes.yml | 30 +++++++++++ docker/docker-compose.edqs.yml | 18 +++++++ docker/docker-install-tb.sh | 8 ++- docker/docker-update-service.sh | 8 ++- docker/docker-upgrade-tb.sh | 11 ++-- docker/edqs.env | 1 + docker/edqs/conf/edqs.conf | 22 ++++++++ docker/edqs/conf/logback.xml | 52 +++++++++++++++++++ docker/tb-node-edqs.env | 4 ++ docker/tb-node.env | 3 +- .../server/msa/ContainerTestSuite.java | 11 ++-- .../server/msa/ThingsBoardDbInstaller.java | 9 ++++ 13 files changed, 162 insertions(+), 16 deletions(-) create mode 100644 docker/docker-compose.edqs.volumes.yml create mode 100644 docker/edqs/conf/edqs.conf create mode 100644 docker/edqs/conf/logback.xml create mode 100644 docker/tb-node-edqs.env diff --git a/docker/compose-utils.sh b/docker/compose-utils.sh index 933e40d57c..bd04657339 100755 --- a/docker/compose-utils.sh +++ b/docker/compose-utils.sh @@ -151,6 +151,7 @@ function permissionList() { 799 799 tb-transports/coap/log 799 799 tb-vc-executor/log 999 999 tb-node/postgres + 799 799 edqs/log " source .env diff --git a/docker/docker-compose.edqs.volumes.yml b/docker/docker-compose.edqs.volumes.yml new file mode 100644 index 0000000000..9d2ce946c8 --- /dev/null +++ b/docker/docker-compose.edqs.volumes.yml @@ -0,0 +1,30 @@ +# +# 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. +# + +version: '3.0' + +services: + tb-edqs-1: + volumes: + - tb-edqs-log-volume:/var/log/edqs + tb-edqs-2: + volumes: + - tb-edqs-log-volume:/var/log/edqs + +volumes: + tb-edqs-log-volume: + external: + name: ${TB_EDQS_LOG_VOLUME} diff --git a/docker/docker-compose.edqs.yml b/docker/docker-compose.edqs.yml index c50740faeb..2f0e836240 100644 --- a/docker/docker-compose.edqs.yml +++ b/docker/docker-compose.edqs.yml @@ -17,17 +17,35 @@ version: '3.0' services: + tb-core1: + env_file: + - tb-node-edqs.env + tb-core2: + env_file: + - tb-node-edqs.env tb-edqs-1: restart: always image: "${DOCKER_REPO}/${EDQS_DOCKER_NAME}:${TB_VERSION}" env_file: - edqs.env + volumes: + - ./edqs/conf:/usr/share/edqs/conf + - ./edqs/log:/var/log/edqs + ports: + - "8080" depends_on: - zookeeper + - kafka tb-edqs-2: restart: always image: "${DOCKER_REPO}/${EDQS_DOCKER_NAME}:${TB_VERSION}" env_file: - edqs.env + volumes: + - ./edqs/conf:/usr/share/edqs/conf + - ./edqs/log:/var/log/edqs + ports: + - "8080" depends_on: - zookeeper + - kafka diff --git a/docker/docker-install-tb.sh b/docker/docker-install-tb.sh index 1956e50eac..da09684d4f 100755 --- a/docker/docker-install-tb.sh +++ b/docker/docker-install-tb.sh @@ -49,6 +49,8 @@ ADDITIONAL_COMPOSE_ARGS=$(additionalComposeArgs) || exit $? ADDITIONAL_CACHE_ARGS=$(additionalComposeCacheArgs) || exit $? +ADDITIONAL_COMPOSE_EDQS_ARGS=$(additionalComposeEdqsArgs) || exit $? + ADDITIONAL_STARTUP_SERVICES=$(additionalStartupServices) || exit $? checkFolders --create || exit $? @@ -56,7 +58,8 @@ checkFolders --create || exit $? if [ ! -z "${ADDITIONAL_STARTUP_SERVICES// }" ]; then COMPOSE_ARGS="\ - -f docker-compose.yml ${ADDITIONAL_CACHE_ARGS} ${ADDITIONAL_COMPOSE_ARGS} ${ADDITIONAL_COMPOSE_QUEUE_ARGS} \ + -f docker-compose.yml ${ADDITIONAL_CACHE_ARGS} ${ADDITIONAL_COMPOSE_ARGS} ${ADDITIONAL_COMPOSE_QUEUE_ARGS} + ${ADDITIONAL_COMPOSE_EDQS_ARGS} \ up -d ${ADDITIONAL_STARTUP_SERVICES}" case $COMPOSE_VERSION in @@ -73,7 +76,8 @@ if [ ! -z "${ADDITIONAL_STARTUP_SERVICES// }" ]; then fi COMPOSE_ARGS="\ - -f docker-compose.yml ${ADDITIONAL_CACHE_ARGS} ${ADDITIONAL_COMPOSE_ARGS} ${ADDITIONAL_COMPOSE_QUEUE_ARGS} \ + -f docker-compose.yml ${ADDITIONAL_CACHE_ARGS} ${ADDITIONAL_COMPOSE_ARGS} ${ADDITIONAL_COMPOSE_QUEUE_ARGS} + ${ADDITIONAL_COMPOSE_EDQS_ARGS} \ run --no-deps --rm -e INSTALL_TB=true -e LOAD_DEMO=${loadDemo} \ tb-core1" diff --git a/docker/docker-update-service.sh b/docker/docker-update-service.sh index 7a77241e48..de1fe0a89a 100755 --- a/docker/docker-update-service.sh +++ b/docker/docker-update-service.sh @@ -27,12 +27,16 @@ ADDITIONAL_COMPOSE_ARGS=$(additionalComposeArgs) || exit $? ADDITIONAL_CACHE_ARGS=$(additionalComposeCacheArgs) || exit $? +ADDITIONAL_COMPOSE_EDQS_ARGS=$(additionalComposeEdqsArgs) || exit $? + COMPOSE_ARGS_PULL="\ - -f docker-compose.yml ${ADDITIONAL_CACHE_ARGS} ${ADDITIONAL_COMPOSE_ARGS} ${ADDITIONAL_COMPOSE_QUEUE_ARGS} \ + -f docker-compose.yml ${ADDITIONAL_CACHE_ARGS} ${ADDITIONAL_COMPOSE_ARGS} ${ADDITIONAL_COMPOSE_QUEUE_ARGS} + ${ADDITIONAL_COMPOSE_EDQS_ARGS} \ pull" COMPOSE_ARGS_BUILD="\ - -f docker-compose.yml ${ADDITIONAL_CACHE_ARGS} ${ADDITIONAL_COMPOSE_ARGS} ${ADDITIONAL_COMPOSE_QUEUE_ARGS} \ + -f docker-compose.yml ${ADDITIONAL_CACHE_ARGS} ${ADDITIONAL_COMPOSE_ARGS} ${ADDITIONAL_COMPOSE_QUEUE_ARGS} + ${ADDITIONAL_COMPOSE_EDQS_ARGS} \ up -d --no-deps --build" case $COMPOSE_VERSION in diff --git a/docker/docker-upgrade-tb.sh b/docker/docker-upgrade-tb.sh index 3be5fbc14b..eca5d34957 100755 --- a/docker/docker-upgrade-tb.sh +++ b/docker/docker-upgrade-tb.sh @@ -42,21 +42,26 @@ ADDITIONAL_COMPOSE_ARGS=$(additionalComposeArgs) || exit $? ADDITIONAL_CACHE_ARGS=$(additionalComposeCacheArgs) || exit $? +ADDITIONAL_COMPOSE_EDQS_ARGS=$(additionalComposeEdqsArgs) || exit $? + ADDITIONAL_STARTUP_SERVICES=$(additionalStartupServices) || exit $? checkFolders --create || exit $? COMPOSE_ARGS_PULL="\ - -f docker-compose.yml ${ADDITIONAL_CACHE_ARGS} ${ADDITIONAL_COMPOSE_ARGS} ${ADDITIONAL_COMPOSE_QUEUE_ARGS} \ + -f docker-compose.yml ${ADDITIONAL_CACHE_ARGS} ${ADDITIONAL_COMPOSE_ARGS} ${ADDITIONAL_COMPOSE_QUEUE_ARGS} + ${ADDITIONAL_COMPOSE_EDQS_ARGS} \ pull \ tb-core1" COMPOSE_ARGS_UP="\ - -f docker-compose.yml ${ADDITIONAL_CACHE_ARGS} ${ADDITIONAL_COMPOSE_ARGS} ${ADDITIONAL_COMPOSE_QUEUE_ARGS} \ + -f docker-compose.yml ${ADDITIONAL_CACHE_ARGS} ${ADDITIONAL_COMPOSE_ARGS} ${ADDITIONAL_COMPOSE_QUEUE_ARGS} + ${ADDITIONAL_COMPOSE_EDQS_ARGS} \ up -d ${ADDITIONAL_STARTUP_SERVICES}" COMPOSE_ARGS_RUN="\ - -f docker-compose.yml ${ADDITIONAL_CACHE_ARGS} ${ADDITIONAL_COMPOSE_ARGS} ${ADDITIONAL_COMPOSE_QUEUE_ARGS} \ + -f docker-compose.yml ${ADDITIONAL_CACHE_ARGS} ${ADDITIONAL_COMPOSE_ARGS} ${ADDITIONAL_COMPOSE_QUEUE_ARGS} + ${ADDITIONAL_COMPOSE_EDQS_ARGS} \ run --no-deps --rm -e UPGRADE_TB=true -e FROM_VERSION=${fromVersion} \ tb-core1" diff --git a/docker/edqs.env b/docker/edqs.env index 33e64d0c96..2c07d6e80d 100644 --- a/docker/edqs.env +++ b/docker/edqs.env @@ -1,6 +1,7 @@ ZOOKEEPER_ENABLED=true ZOOKEEPER_URL=zookeeper:2181 TB_KAFKA_SERVERS=kafka:9092 +HTTP_BIND_PORT=8080 METRICS_ENABLED=true METRICS_ENDPOINTS_EXPOSE=prometheus diff --git a/docker/edqs/conf/edqs.conf b/docker/edqs/conf/edqs.conf new file mode 100644 index 0000000000..8c6b5d1826 --- /dev/null +++ b/docker/edqs/conf/edqs.conf @@ -0,0 +1,22 @@ +# +# 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. +# + +export JAVA_OPTS="$JAVA_OPTS -Xlog:gc*,heap*,age*,safepoint=debug:file=/var/log/edqs/${TB_SERVICE_ID}-gc.log:time,uptime,level,tags:filecount=10,filesize=10M" +export JAVA_OPTS="$JAVA_OPTS -XX:+IgnoreUnrecognizedVMOptions -XX:+HeapDumpOnOutOfMemoryError" +export JAVA_OPTS="$JAVA_OPTS -XX:-UseBiasedLocking -XX:+UseTLAB -XX:+ResizeTLAB -XX:+PerfDisableSharedMem -XX:+UseCondCardMark" +export JAVA_OPTS="$JAVA_OPTS -XX:+UseG1GC -XX:MaxGCPauseMillis=500 -XX:+UseStringDeduplication -XX:+ParallelRefProcEnabled -XX:MaxTenuringThreshold=10" +export LOG_FILENAME=tb-edqs.out +export LOADER_PATH=/usr/share/edqs/conf diff --git a/docker/edqs/conf/logback.xml b/docker/edqs/conf/logback.xml new file mode 100644 index 0000000000..40481a8c35 --- /dev/null +++ b/docker/edqs/conf/logback.xml @@ -0,0 +1,52 @@ + + + + + + + /var/log/edqs/${TB_SERVICE_ID}/tb-edqs.log + + /var/log/edqs/tb-edqs.%d{yyyy-MM-dd}.%i.log + 100MB + 30 + 3GB + + + %d{ISO8601} [%thread] %-5level %logger{36} - %msg%n + + + + + + %d{ISO8601} [%thread] %-5level %logger{36} - %msg%n + + + + + + + + + + + + + diff --git a/docker/tb-node-edqs.env b/docker/tb-node-edqs.env new file mode 100644 index 0000000000..1ef523af50 --- /dev/null +++ b/docker/tb-node-edqs.env @@ -0,0 +1,4 @@ +# ThingsBoard server configuration with enabled EDQS synchronization + +TB_EDQS_SYNC_ENABLED=true +TB_EDQS_API_ENABLED=true \ No newline at end of file diff --git a/docker/tb-node.env b/docker/tb-node.env index d40ca66621..e07d5a2f23 100644 --- a/docker/tb-node.env +++ b/docker/tb-node.env @@ -4,10 +4,9 @@ ZOOKEEPER_ENABLED=true ZOOKEEPER_URL=zookeeper:2181 JS_EVALUATOR=remote TRANSPORT_TYPE=remote +TB_EDQS_MODE=remote HTTP_LOG_CONTROLLER_ERROR_STACK_TRACE=false METRICS_ENABLED=true METRICS_ENDPOINTS_EXPOSE=prometheus - -TB_EDQS_MODE=remote \ No newline at end of file diff --git a/msa/black-box-tests/src/test/java/org/thingsboard/server/msa/ContainerTestSuite.java b/msa/black-box-tests/src/test/java/org/thingsboard/server/msa/ContainerTestSuite.java index bc6c3611f5..5b57768b79 100644 --- a/msa/black-box-tests/src/test/java/org/thingsboard/server/msa/ContainerTestSuite.java +++ b/msa/black-box-tests/src/test/java/org/thingsboard/server/msa/ContainerTestSuite.java @@ -51,6 +51,7 @@ public class ContainerTestSuite { private static final String TB_CORE_LOG_REGEXP = ".*Starting polling for events.*"; private static final String TRANSPORTS_LOG_REGEXP = ".*Going to recalculate partitions.*"; private static final String TB_VC_LOG_REGEXP = TRANSPORTS_LOG_REGEXP; + private static final String TB_EDQS_LOG_REGEXP = ".*All partitions processed.*"; private static final String TB_JS_EXECUTOR_LOG_REGEXP = ".*template started.*"; private static final Duration CONTAINER_STARTUP_TIMEOUT = Duration.ofSeconds(400); @@ -115,6 +116,7 @@ public class ContainerTestSuite { List composeFiles = new ArrayList<>(Arrays.asList( new File(targetDir + "docker-compose.yml"), new File(targetDir + "docker-compose.edqs.yml"), + new File(targetDir + "docker-compose.edqs.volumes.yml"), new File(targetDir + "docker-compose.volumes.yml"), new File(targetDir + "docker-compose.mosquitto.yml"), new File(targetDir + (IS_HYBRID_MODE ? "docker-compose.hybrid.yml" : "docker-compose.postgres.yml")), @@ -163,11 +165,6 @@ public class ContainerTestSuite { composeFiles.add(new File(targetDir + "docker-compose.cassandra.volumes.yml")); } - // to trigger edqs synchronization - addToFile(targetDir, "tb-node.env", - Map.of("TB_EDQS_SYNC_ENABLED", "true", - "TB_EDQS_API_ENABLED", "true")); - testContainer = new DockerComposeContainerImpl<>(composeFiles) .withPull(false) .withLocalCompose(true) @@ -189,8 +186,8 @@ public class ContainerTestSuite { .waitingFor("tb-vc-executor1", Wait.forLogMessage(TB_VC_LOG_REGEXP, 1).withStartupTimeout(CONTAINER_STARTUP_TIMEOUT)) .waitingFor("tb-vc-executor2", Wait.forLogMessage(TB_VC_LOG_REGEXP, 1).withStartupTimeout(CONTAINER_STARTUP_TIMEOUT)) .waitingFor("tb-js-executor", Wait.forLogMessage(TB_JS_EXECUTOR_LOG_REGEXP, 1).withStartupTimeout(CONTAINER_STARTUP_TIMEOUT)) - .waitingFor("tb-edqs-1", Wait.forHttp("/api/edqs/ready").withStartupTimeout(CONTAINER_STARTUP_TIMEOUT)) - .waitingFor("tb-edqs-2", Wait.forHttp("/api/edqs/ready").withStartupTimeout(CONTAINER_STARTUP_TIMEOUT)); + .waitingFor("tb-edqs-1", Wait.forLogMessage(TB_EDQS_LOG_REGEXP, 1).withStartupTimeout(CONTAINER_STARTUP_TIMEOUT)) + .waitingFor("tb-edqs-2", Wait.forLogMessage(TB_EDQS_LOG_REGEXP, 1).withStartupTimeout(CONTAINER_STARTUP_TIMEOUT)); testContainer.start(); setActive(true); } catch (Exception e) { diff --git a/msa/black-box-tests/src/test/java/org/thingsboard/server/msa/ThingsBoardDbInstaller.java b/msa/black-box-tests/src/test/java/org/thingsboard/server/msa/ThingsBoardDbInstaller.java index 48664716f5..9ce9f82bca 100644 --- a/msa/black-box-tests/src/test/java/org/thingsboard/server/msa/ThingsBoardDbInstaller.java +++ b/msa/black-box-tests/src/test/java/org/thingsboard/server/msa/ThingsBoardDbInstaller.java @@ -48,6 +48,7 @@ public class ThingsBoardDbInstaller { private final static String TB_MQTT_TRANSPORT_LOG_VOLUME = "tb-mqtt-transport-log-test-volume"; private final static String TB_SNMP_TRANSPORT_LOG_VOLUME = "tb-snmp-transport-log-test-volume"; private final static String TB_VC_EXECUTOR_LOG_VOLUME = "tb-vc-executor-log-test-volume"; + private final static String TB_EDQS_LOG_VOLUME = "tb-edqs-log-test-volume"; private final static String JAVA_OPTS = "-Xmx512m"; private final DockerComposeExecutor dockerCompose; @@ -65,6 +66,7 @@ public class ThingsBoardDbInstaller { private final String tbMqttTransportLogVolume; private final String tbSnmpTransportLogVolume; private final String tbVcExecutorLogVolume; + private final String tbEdqsLogVolume; private final Map env; public ThingsBoardDbInstaller() { @@ -103,6 +105,7 @@ public class ThingsBoardDbInstaller { tbMqttTransportLogVolume = project + "_" + TB_MQTT_TRANSPORT_LOG_VOLUME; tbSnmpTransportLogVolume = project + "_" + TB_SNMP_TRANSPORT_LOG_VOLUME; tbVcExecutorLogVolume = project + "_" + TB_VC_EXECUTOR_LOG_VOLUME; + tbEdqsLogVolume = project + "_" + TB_EDQS_LOG_VOLUME; dockerCompose = new DockerComposeExecutor(composeFiles, project); @@ -119,6 +122,7 @@ public class ThingsBoardDbInstaller { env.put("TB_MQTT_TRANSPORT_LOG_VOLUME", tbMqttTransportLogVolume); env.put("TB_SNMP_TRANSPORT_LOG_VOLUME", tbSnmpTransportLogVolume); env.put("TB_VC_EXECUTOR_LOG_VOLUME", tbVcExecutorLogVolume); + env.put("TB_EDQS_LOG_VOLUME", tbEdqsLogVolume); if (IS_REDIS_CLUSTER) { for (int i = 0; i < 6; i++) { env.put("REDIS_CLUSTER_DATA_VOLUME_" + i, redisClusterDataVolume + '-' + i); @@ -189,6 +193,9 @@ public class ThingsBoardDbInstaller { dockerCompose.withCommand("volume create " + tbVcExecutorLogVolume); dockerCompose.invokeDocker(); + dockerCompose.withCommand("volume create " + tbEdqsLogVolume); + dockerCompose.invokeDocker(); + StringBuilder additionalServices = new StringBuilder(); if (IS_HYBRID_MODE) { additionalServices.append(" cassandra"); @@ -241,6 +248,7 @@ public class ThingsBoardDbInstaller { copyLogs(tbMqttTransportLogVolume, "./target/tb-mqtt-transport-logs/"); copyLogs(tbSnmpTransportLogVolume, "./target/tb-snmp-transport-logs/"); copyLogs(tbVcExecutorLogVolume, "./target/tb-vc-executor-logs/"); + copyLogs(tbEdqsLogVolume, "./target/tb-edqs-logs/"); StringJoiner rmVolumesCommand = new StringJoiner(" ") .add("volume rm -f") @@ -252,6 +260,7 @@ public class ThingsBoardDbInstaller { .add(tbMqttTransportLogVolume) .add(tbSnmpTransportLogVolume) .add(tbVcExecutorLogVolume) + .add(tbEdqsLogVolume) .add(resolveRedisComposeVolumeLog()); if (IS_HYBRID_MODE) { From aa0179303cbe94252777d98b8b7a1579ad49aeda Mon Sep 17 00:00:00 2001 From: ViacheslavKlimov Date: Thu, 27 Feb 2025 12:45:22 +0200 Subject: [PATCH 106/345] Subscribe to Zookeeper events only after application starts Sometimes ZK events came before the app start, causing repartition change events, while event listeners are not yet initialized by Spring --- .../server/queue/discovery/ZkDiscoveryService.java | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/common/queue/src/main/java/org/thingsboard/server/queue/discovery/ZkDiscoveryService.java b/common/queue/src/main/java/org/thingsboard/server/queue/discovery/ZkDiscoveryService.java index ad2a97de52..3ad6164c61 100644 --- a/common/queue/src/main/java/org/thingsboard/server/queue/discovery/ZkDiscoveryService.java +++ b/common/queue/src/main/java/org/thingsboard/server/queue/discovery/ZkDiscoveryService.java @@ -143,6 +143,7 @@ public class ZkDiscoveryService implements DiscoveryService, PathChildrenCacheLi } else { log.info("Received application ready event. Starting current ZK node."); } + subscribeToEvents(); if (client.getState() != CuratorFrameworkState.STARTED) { log.debug("Ignoring application ready event, ZK client is not started, ZK client state [{}]", client.getState()); return; @@ -212,6 +213,7 @@ public class ZkDiscoveryService implements DiscoveryService, PathChildrenCacheLi try { destroyZkClient(); initZkClient(); + subscribeToEvents(); publishCurrentServer(); } catch (Exception e) { log.error("Failed to reconnect to ZK: {}", e.getMessage(), e); @@ -227,7 +229,6 @@ public class ZkDiscoveryService implements DiscoveryService, PathChildrenCacheLi client.start(); client.blockUntilConnected(); cache = new PathChildrenCache(client, zkNodesDir, true); - cache.getListenable().addListener(this); cache.start(); stopped = false; log.info("ZK client connected"); @@ -239,6 +240,10 @@ public class ZkDiscoveryService implements DiscoveryService, PathChildrenCacheLi } } + private void subscribeToEvents() { + cache.getListenable().addListener(this); + } + private void unpublishCurrentServer() { try { if (nodePath != null) { From 46f5ce56a46f773984dced1f0a00a19fe48d1ede Mon Sep 17 00:00:00 2001 From: YevhenBondarenko Date: Thu, 27 Feb 2025 11:52:40 +0100 Subject: [PATCH 107/345] removed serializers from kafka node config, used default StringSerializer --- .../org/thingsboard/rule/engine/kafka/TbKafkaNode.java | 5 +++-- .../rule/engine/kafka/TbKafkaNodeConfiguration.java | 7 +++---- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/kafka/TbKafkaNode.java b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/kafka/TbKafkaNode.java index 6008305570..8f6a6dc617 100644 --- a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/kafka/TbKafkaNode.java +++ b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/kafka/TbKafkaNode.java @@ -26,6 +26,7 @@ import org.apache.kafka.common.config.SslConfigs; import org.apache.kafka.common.header.Headers; import org.apache.kafka.common.header.internals.RecordHeader; import org.apache.kafka.common.header.internals.RecordHeaders; +import org.apache.kafka.common.serialization.StringSerializer; import org.springframework.util.ReflectionUtils; import org.thingsboard.rule.engine.api.RuleNode; import org.thingsboard.rule.engine.api.TbContext; @@ -83,8 +84,8 @@ public class TbKafkaNode extends TbAbstractExternalNode { Properties properties = new Properties(); properties.put(ProducerConfig.CLIENT_ID_CONFIG, "producer-tb-kafka-node-" + ctx.getSelfId().getId().toString() + "-" + ctx.getServiceId()); properties.put(ProducerConfig.BOOTSTRAP_SERVERS_CONFIG, config.getBootstrapServers()); - properties.put(ProducerConfig.VALUE_SERIALIZER_CLASS_CONFIG, config.getValueSerializer()); - properties.put(ProducerConfig.KEY_SERIALIZER_CLASS_CONFIG, config.getKeySerializer()); + properties.put(ProducerConfig.VALUE_SERIALIZER_CLASS_CONFIG, StringSerializer.class); + properties.put(ProducerConfig.KEY_SERIALIZER_CLASS_CONFIG, StringSerializer.class); properties.put(ProducerConfig.ACKS_CONFIG, config.getAcks()); properties.put(ProducerConfig.RETRIES_CONFIG, config.getRetries()); properties.put(ProducerConfig.BATCH_SIZE_CONFIG, config.getBatchSize()); diff --git a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/kafka/TbKafkaNodeConfiguration.java b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/kafka/TbKafkaNodeConfiguration.java index 867dd2e54e..b3f1fe1473 100644 --- a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/kafka/TbKafkaNodeConfiguration.java +++ b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/kafka/TbKafkaNodeConfiguration.java @@ -15,6 +15,8 @@ */ package org.thingsboard.rule.engine.kafka; +import com.fasterxml.jackson.annotation.JsonIgnore; +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; import lombok.Data; import org.apache.kafka.common.serialization.StringSerializer; import org.thingsboard.rule.engine.api.NodeConfiguration; @@ -23,6 +25,7 @@ import java.util.Collections; import java.util.Map; @Data +@JsonIgnoreProperties(ignoreUnknown = true) public class TbKafkaNodeConfiguration implements NodeConfiguration { private String topicPattern; @@ -33,8 +36,6 @@ public class TbKafkaNodeConfiguration implements NodeConfiguration otherProperties; private boolean addMetadataKeyValuesAsKafkaHeaders; @@ -50,8 +51,6 @@ public class TbKafkaNodeConfiguration implements NodeConfiguration Date: Thu, 27 Feb 2025 13:01:21 +0200 Subject: [PATCH 108/345] Edge rule chain CRUD: fixed setting root rule chain --- .../processor/edge/EdgeEntityProcessor.java | 4 +++ .../rule/BaseRuleChainProcessor.java | 13 +++++--- .../rule/RuleChainEdgeProcessor.java | 33 +++++++++++-------- .../entitiy/EntityStateSourcingListener.java | 5 +++ .../server/edge/AbstractEdgeTest.java | 28 +++++++--------- .../server/edge/RuleChainEdgeTest.java | 1 + 6 files changed, 49 insertions(+), 35 deletions(-) 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 ddbe4810df..77fa31c028 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 @@ -49,8 +49,12 @@ public class EdgeEntityProcessor extends BaseEdgeProcessor { @Override public ListenableFuture processEntityNotification(TenantId tenantId, TransportProtos.EdgeNotificationMsgProto edgeNotificationMsg) { try { + EdgeId originatorEdgeId = safeGetEdgeId(edgeNotificationMsg.getOriginatorEdgeIdMSB(), edgeNotificationMsg.getOriginatorEdgeIdLSB()); EdgeEventActionType actionType = EdgeEventActionType.valueOf(edgeNotificationMsg.getAction()); EdgeId edgeId = new EdgeId(new UUID(edgeNotificationMsg.getEntityIdMSB(), edgeNotificationMsg.getEntityIdLSB())); + if (edgeId.equals(originatorEdgeId)) { + return Futures.immediateFuture(null); + } switch (actionType) { case ASSIGNED_TO_CUSTOMER: { CustomerId customerId = JacksonUtil.fromString(edgeNotificationMsg.getBody(), CustomerId.class); diff --git a/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/rule/BaseRuleChainProcessor.java b/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/rule/BaseRuleChainProcessor.java index 03904ce5ef..3231d7788a 100644 --- a/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/rule/BaseRuleChainProcessor.java +++ b/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/rule/BaseRuleChainProcessor.java @@ -39,24 +39,27 @@ public class BaseRuleChainProcessor extends BaseEdgeProcessor { protected Pair saveOrUpdateRuleChain(TenantId tenantId, RuleChainId ruleChainId, RuleChainUpdateMsg ruleChainUpdateMsg, RuleChainType ruleChainType) { boolean created = false; - RuleChain ruleChain = edgeCtx.getRuleChainService().findRuleChainById(tenantId, ruleChainId); - if (ruleChain == null) { + RuleChain ruleChainFromDb = edgeCtx.getRuleChainService().findRuleChainById(tenantId, ruleChainId); + if (ruleChainFromDb == null) { created = true; } - ruleChain = JacksonUtil.fromString(ruleChainUpdateMsg.getEntity(), RuleChain.class, true); + RuleChain ruleChain = JacksonUtil.fromString(ruleChainUpdateMsg.getEntity(), RuleChain.class, true); if (ruleChain == null) { throw new RuntimeException("[{" + tenantId + "}] ruleChainUpdateMsg {" + ruleChainUpdateMsg + "} cannot be converted to rule chain"); } boolean isRoot = ruleChain.isRoot(); - ruleChain.setRoot(false); + if (RuleChainType.CORE.equals(ruleChainType)) { + ruleChain.setRoot(false); + } else { + ruleChain.setRoot(ruleChainFromDb == null ? false : ruleChainFromDb.isRoot()); + } ruleChain.setType(ruleChainType); ruleChainValidator.validate(ruleChain, RuleChain::getTenantId); if (created) { ruleChain.setId(ruleChainId); } - edgeCtx.getRuleChainService().saveRuleChain(ruleChain); return Pair.of(created, isRoot); } 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 340fb09eab..06fb4c37a2 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 @@ -57,8 +57,7 @@ public class RuleChainEdgeProcessor extends BaseRuleChainProcessor { switch (ruleChainUpdateMsg.getMsgType()) { case ENTITY_CREATED_RPC_MESSAGE: case ENTITY_UPDATED_RPC_MESSAGE: - saveOrUpdateRuleChain(tenantId, ruleChainId, ruleChainUpdateMsg, edge); - return Futures.immediateFuture(null); + return saveOrUpdateRuleChain(tenantId, ruleChainId, ruleChainUpdateMsg, edge); case ENTITY_DELETED_RPC_MESSAGE: RuleChain ruleChainToDelete = edgeCtx.getRuleChainService().findRuleChainById(tenantId, ruleChainId); if (ruleChainToDelete != null) { @@ -81,19 +80,25 @@ public class RuleChainEdgeProcessor extends BaseRuleChainProcessor { } } - private void saveOrUpdateRuleChain(TenantId tenantId, RuleChainId ruleChainId, RuleChainUpdateMsg ruleChainUpdateMsg, Edge edge) { - Pair resultPair = super.saveOrUpdateRuleChain(tenantId, ruleChainId, ruleChainUpdateMsg, RuleChainType.EDGE); - Boolean created = resultPair.getFirst(); - if (created) { - createRelationFromEdge(tenantId, edge.getId(), ruleChainId); - pushRuleChainCreatedEventToRuleEngine(tenantId, edge, ruleChainId, ruleChainUpdateMsg.getEntity()); - edgeCtx.getRuleChainService().assignRuleChainToEdge(tenantId, ruleChainId, edge.getId()); - } - Boolean isRoot = resultPair.getSecond(); - if (isRoot) { - edge.setRootRuleChainId(ruleChainId); - edgeCtx.getEdgeService().saveEdge(edge); + private ListenableFuture saveOrUpdateRuleChain(TenantId tenantId, RuleChainId ruleChainId, RuleChainUpdateMsg ruleChainUpdateMsg, Edge edge) { + try { + Pair resultPair = super.saveOrUpdateRuleChain(tenantId, ruleChainId, ruleChainUpdateMsg, RuleChainType.EDGE); + Boolean created = resultPair.getFirst(); + if (created) { + createRelationFromEdge(tenantId, edge.getId(), ruleChainId); + pushRuleChainCreatedEventToRuleEngine(tenantId, edge, ruleChainId, ruleChainUpdateMsg.getEntity()); + edgeCtx.getRuleChainService().assignRuleChainToEdge(tenantId, ruleChainId, edge.getId()); + } + Boolean isRoot = resultPair.getSecond(); + if (isRoot) { + edge = edgeCtx.getEdgeService().findEdgeById(tenantId, edge.getId()); + edgeCtx.getEdgeService().setEdgeRootRuleChain(tenantId, edge, ruleChainId); + } + } catch (Exception e) { + log.error("Failed to save or update rule chain", e); + return Futures.immediateFailedFuture(e); } + return Futures.immediateFuture(null); } private void pushRuleChainCreatedEventToRuleEngine(TenantId tenantId, Edge edge, RuleChainId ruleChainId, String ruleChainAsString) { diff --git a/application/src/main/java/org/thingsboard/server/service/entitiy/EntityStateSourcingListener.java b/application/src/main/java/org/thingsboard/server/service/entitiy/EntityStateSourcingListener.java index 35aad36dff..e40251673d 100644 --- a/application/src/main/java/org/thingsboard/server/service/entitiy/EntityStateSourcingListener.java +++ b/application/src/main/java/org/thingsboard/server/service/entitiy/EntityStateSourcingListener.java @@ -50,6 +50,7 @@ import org.thingsboard.server.common.msg.TbMsgMetaData; import org.thingsboard.server.common.msg.edge.EdgeEventUpdateMsg; import org.thingsboard.server.common.msg.plugin.ComponentLifecycleMsg; import org.thingsboard.server.common.msg.rule.engine.DeviceCredentialsUpdateNotificationMsg; +import org.thingsboard.server.dao.edge.EdgeSynchronizationManager; import org.thingsboard.server.dao.eventsourcing.ActionEntityEvent; import org.thingsboard.server.dao.eventsourcing.DeleteEntityEvent; import org.thingsboard.server.dao.eventsourcing.SaveEntityEvent; @@ -64,6 +65,7 @@ public class EntityStateSourcingListener { private final TenantService tenantService; private final TbClusterService tbClusterService; + private final EdgeSynchronizationManager edgeSynchronizationManager; @PostConstruct public void init() { @@ -241,6 +243,9 @@ public class EntityStateSourcingListener { private void onEdgeEvent(TenantId tenantId, EntityId entityId, Object entity, ComponentLifecycleEvent lifecycleEvent) { if (entity instanceof Edge) { + if (entityId.equals(edgeSynchronizationManager.getEdgeId().get())) { + return; + } tbClusterService.onEdgeStateChangeEvent(new ComponentLifecycleMsg(tenantId, entityId, lifecycleEvent)); } else if (entity instanceof EdgeEvent edgeEvent) { tbClusterService.onEdgeEventUpdate(new EdgeEventUpdateMsg(tenantId, edgeEvent.getEdgeId())); diff --git a/application/src/test/java/org/thingsboard/server/edge/AbstractEdgeTest.java b/application/src/test/java/org/thingsboard/server/edge/AbstractEdgeTest.java index 44dac215e2..feac7adb04 100644 --- a/application/src/test/java/org/thingsboard/server/edge/AbstractEdgeTest.java +++ b/application/src/test/java/org/thingsboard/server/edge/AbstractEdgeTest.java @@ -140,7 +140,7 @@ abstract public class AbstractEdgeTest extends AbstractControllerTest { installation(); edgeImitator = new EdgeImitator("localhost", 7070, edge.getRoutingKey(), edge.getSecret()); - edgeImitator.expectMessageAmount(27); + edgeImitator.expectMessageAmount(25); edgeImitator.ignoreType(OAuth2ClientUpdateMsg.class); edgeImitator.ignoreType(OAuth2DomainUpdateMsg.class); edgeImitator.connect(); @@ -191,8 +191,10 @@ abstract public class AbstractEdgeTest extends AbstractControllerTest { } private RuleChainId getEdgeRootRuleChainId() throws Exception { - List edgeRuleChains = doGetTypedWithPageLink("/api/edge/" + edge.getUuidId() + "/ruleChains?", - new TypeReference>() {}, new PageLink(100)).getData(); + List edgeRuleChains = doGetTypedWithPageLink("/api/ruleChains?type={type}&", + new TypeReference>() {}, + new PageLink(100, 0, "Edge Root Rule Chain"), + "EDGE").getData(); for (RuleChain edgeRuleChain : edgeRuleChains) { if (edgeRuleChain.isRoot()) { return edgeRuleChain.getId(); @@ -243,8 +245,8 @@ abstract public class AbstractEdgeTest extends AbstractControllerTest { validateMsgsCnt(RuleChainUpdateMsg.class, 1); UUID ruleChainUUID = validateRuleChains(); - // 2 messages - 1 from rule chain fetcher and 1 from rule chain controller (it goes along with RuleChainUpdateMsg) - validateMsgsCnt(RuleChainMetadataUpdateMsg.class, 2); + // 1 from rule chain fetcher + validateMsgsCnt(RuleChainMetadataUpdateMsg.class, 1); validateRuleChainMetadataUpdates(ruleChainUUID); // 4 messages ('general', 'mail', 'connectivity', 'jwt') @@ -426,17 +428,11 @@ abstract public class AbstractEdgeTest extends AbstractControllerTest { } private void validateRuleChainMetadataUpdates(UUID expectedRuleChainUUID) { - List ruleChainMetadataUpdateMsgList = edgeImitator.findAllMessagesByType(RuleChainMetadataUpdateMsg.class); - Assert.assertEquals(2, ruleChainMetadataUpdateMsgList.size()); - // metadata create msg - RuleChainMetadataUpdateMsg ruleChainMetadataUpdateMsgCreated = ruleChainMetadataUpdateMsgList.get(0); - Assert.assertEquals(UpdateMsgType.ENTITY_CREATED_RPC_MESSAGE, ruleChainMetadataUpdateMsgCreated.getMsgType()); - RuleChainMetaData ruleChainMetaData = JacksonUtil.fromString(ruleChainMetadataUpdateMsgCreated.getEntity(), RuleChainMetaData.class, true); - Assert.assertEquals(expectedRuleChainUUID, ruleChainMetaData.getRuleChainId().getId()); - // metadata update msg - RuleChainMetadataUpdateMsg ruleChainMetadataUpdateMsgUpdated = ruleChainMetadataUpdateMsgList.get(1); - Assert.assertEquals(UpdateMsgType.ENTITY_UPDATED_RPC_MESSAGE, ruleChainMetadataUpdateMsgUpdated.getMsgType()); - ruleChainMetaData = JacksonUtil.fromString(ruleChainMetadataUpdateMsgUpdated.getEntity(), RuleChainMetaData.class, true); + Optional ruleChainMetadataUpdateMsgOpt = edgeImitator.findMessageByType(RuleChainMetadataUpdateMsg.class); + Assert.assertTrue(ruleChainMetadataUpdateMsgOpt.isPresent()); + RuleChainMetadataUpdateMsg ruleChainMetadataUpdateMsg = ruleChainMetadataUpdateMsgOpt.get(); + Assert.assertEquals(UpdateMsgType.ENTITY_CREATED_RPC_MESSAGE, ruleChainMetadataUpdateMsg.getMsgType()); + RuleChainMetaData ruleChainMetaData = JacksonUtil.fromString(ruleChainMetadataUpdateMsg.getEntity(), RuleChainMetaData.class, true); Assert.assertEquals(expectedRuleChainUUID, ruleChainMetaData.getRuleChainId().getId()); } diff --git a/application/src/test/java/org/thingsboard/server/edge/RuleChainEdgeTest.java b/application/src/test/java/org/thingsboard/server/edge/RuleChainEdgeTest.java index ed77c4b97a..7c029e0471 100644 --- a/application/src/test/java/org/thingsboard/server/edge/RuleChainEdgeTest.java +++ b/application/src/test/java/org/thingsboard/server/edge/RuleChainEdgeTest.java @@ -105,6 +105,7 @@ public class RuleChainEdgeTest extends AbstractEdgeTest { // create rule chain on edge RuleChain edgeRuleChain = new RuleChain(); + edgeRuleChain.setTenantId(tenantId); edgeRuleChain.setId(new RuleChainId(uuid)); edgeRuleChain.setName(ruleChainName); UplinkMsg.Builder uplinkMsgBuilder = UplinkMsg.newBuilder(); From b29b192efc78a3b2af9bed0679f111bf39be2c10 Mon Sep 17 00:00:00 2001 From: Volodymyr Babak Date: Thu, 27 Feb 2025 13:20:59 +0200 Subject: [PATCH 109/345] Edge Rule Chain CRUD: revert metadata validatino --- .../rule/BaseRuleChainProcessor.java | 8 +++++- .../server/dao/rule/RuleChainService.java | 2 -- .../server/dao/rule/BaseRuleChainService.java | 26 +++++-------------- 3 files changed, 13 insertions(+), 23 deletions(-) diff --git a/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/rule/BaseRuleChainProcessor.java b/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/rule/BaseRuleChainProcessor.java index 3231d7788a..5156956c3d 100644 --- a/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/rule/BaseRuleChainProcessor.java +++ b/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/rule/BaseRuleChainProcessor.java @@ -24,6 +24,7 @@ import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.common.data.rule.RuleChain; import org.thingsboard.server.common.data.rule.RuleChainMetaData; import org.thingsboard.server.common.data.rule.RuleChainType; +import org.thingsboard.server.common.data.rule.RuleNode; import org.thingsboard.server.dao.service.DataValidator; import org.thingsboard.server.gen.edge.v1.RuleChainMetadataUpdateMsg; import org.thingsboard.server.gen.edge.v1.RuleChainUpdateMsg; @@ -70,7 +71,12 @@ public class BaseRuleChainProcessor extends BaseEdgeProcessor { throw new RuntimeException("[{" + tenantId + "}] ruleChainMetadataUpdateMsg {" + ruleChainMetadataUpdateMsg + "} cannot be converted to rule chain metadata"); } if (!ruleChainMetadata.getNodes().isEmpty()) { - edgeCtx.getRuleChainService().saveRuleChainMetaData(tenantId, ruleChainMetadata, Function.identity(), true, false); + ruleChainMetadata.setVersion(null); + for (RuleNode ruleNode : ruleChainMetadata.getNodes()) { + ruleNode.setRuleChainId(null); + ruleNode.setId(null); + } + edgeCtx.getRuleChainService().saveRuleChainMetaData(tenantId, ruleChainMetadata, Function.identity(), true); } } } diff --git a/common/dao-api/src/main/java/org/thingsboard/server/dao/rule/RuleChainService.java b/common/dao-api/src/main/java/org/thingsboard/server/dao/rule/RuleChainService.java index 7558d49971..a2356ee149 100644 --- a/common/dao-api/src/main/java/org/thingsboard/server/dao/rule/RuleChainService.java +++ b/common/dao-api/src/main/java/org/thingsboard/server/dao/rule/RuleChainService.java @@ -54,8 +54,6 @@ public interface RuleChainService extends EntityDaoService { RuleChainUpdateResult saveRuleChainMetaData(TenantId tenantId, RuleChainMetaData ruleChainMetaData, Function ruleNodeUpdater, boolean publishSaveEvent); - RuleChainUpdateResult saveRuleChainMetaData(TenantId tenantId, RuleChainMetaData ruleChainMetaData, Function ruleNodeUpdater, boolean publishSaveEvent, boolean doValidate); - RuleChainMetaData loadRuleChainMetaData(TenantId tenantId, RuleChainId ruleChainId); RuleChain findRuleChainById(TenantId tenantId, RuleChainId ruleChainId); diff --git a/dao/src/main/java/org/thingsboard/server/dao/rule/BaseRuleChainService.java b/dao/src/main/java/org/thingsboard/server/dao/rule/BaseRuleChainService.java index 685a25a41e..b7325ed714 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/rule/BaseRuleChainService.java +++ b/dao/src/main/java/org/thingsboard/server/dao/rule/BaseRuleChainService.java @@ -174,25 +174,18 @@ public class BaseRuleChainService extends AbstractEntityService implements RuleC @Override @Transactional public RuleChainUpdateResult saveRuleChainMetaData(TenantId tenantId, RuleChainMetaData ruleChainMetaData, Function ruleNodeUpdater) { - return saveRuleChainMetaData(tenantId, ruleChainMetaData, ruleNodeUpdater, true, true); + return saveRuleChainMetaData(tenantId, ruleChainMetaData, ruleNodeUpdater, true); } @Transactional @Override public RuleChainUpdateResult saveRuleChainMetaData(TenantId tenantId, RuleChainMetaData ruleChainMetaData, Function ruleNodeUpdater, boolean publishSaveEvent) { - return saveRuleChainMetaData(tenantId, ruleChainMetaData, ruleNodeUpdater, publishSaveEvent, true); - } - - @Transactional - @Override - public RuleChainUpdateResult saveRuleChainMetaData(TenantId tenantId, RuleChainMetaData ruleChainMetaData, Function ruleNodeUpdater, - boolean publishSaveEvent, boolean doValidate) { Validator.validateId(ruleChainMetaData.getRuleChainId(), "Incorrect rule chain id."); RuleChain ruleChain = findRuleChainById(tenantId, ruleChainMetaData.getRuleChainId()); if (ruleChain == null) { return RuleChainUpdateResult.failed(); - } else if (doValidate && ruleChainMetaData.getVersion() != null && !ruleChainMetaData.getVersion().equals(ruleChain.getVersion())) { + } else if (ruleChainMetaData.getVersion() != null && !ruleChainMetaData.getVersion().equals(ruleChain.getVersion())) { throw new EntityVersionMismatchException(EntityType.RULE_CHAIN, null); } RuleChainDataValidator.validateMetaDataFieldsAndConnections(ruleChainMetaData); @@ -206,14 +199,9 @@ public class BaseRuleChainService extends AbstractEntityService implements RuleC if (nodes != null) { for (RuleNode node : nodes) { setSingletonMode(node); - if (doValidate) { - if (node.getId() != null) { - ruleNodeIndexMap.put(node.getId(), nodes.indexOf(node)); - } else { - toAddOrUpdate.add(node); - } - } else { + if (node.getId() != null) { ruleNodeIndexMap.put(node.getId(), nodes.indexOf(node)); + } else { toAddOrUpdate.add(node); } } @@ -226,10 +214,8 @@ public class BaseRuleChainService extends AbstractEntityService implements RuleC Integer index = ruleNodeIndexMap.get(existingNode.getId()); RuleNode newRuleNode = null; if (index != null) { - if (doValidate) { - newRuleNode = ruleChainMetaData.getNodes().get(index); - toAddOrUpdate.add(newRuleNode); - } + newRuleNode = ruleChainMetaData.getNodes().get(index); + toAddOrUpdate.add(newRuleNode); } else { updatedRuleNodes.add(new RuleNodeUpdateResult(existingNode, null)); toDelete.add(existingNode); From cc925a75c42e50d8978f9ade2c7487d6063be704 Mon Sep 17 00:00:00 2001 From: Volodymyr Babak Date: Thu, 27 Feb 2025 13:41:17 +0200 Subject: [PATCH 110/345] edge.proto - mark deprecated fields --- common/edge-api/src/main/proto/edge.proto | 25 +++++++++++++------ .../server/dao/rule/BaseRuleChainService.java | 3 +-- 2 files changed, 19 insertions(+), 9 deletions(-) diff --git a/common/edge-api/src/main/proto/edge.proto b/common/edge-api/src/main/proto/edge.proto index 4058260629..023ac00634 100644 --- a/common/edge-api/src/main/proto/edge.proto +++ b/common/edge-api/src/main/proto/edge.proto @@ -42,6 +42,8 @@ enum EdgeVersion { V_3_8_0 = 8; V_3_9_0 = 9; V_4_0_0 = 10; + + V_LATEST = 999; } /** @@ -303,8 +305,9 @@ message NotificationTemplateUpdateMsg { optional string entity = 4; } -// deprecated +// DEPRECATED. FOR REMOVAL message RuleChainMetadataRequestMsg { + option deprecated = true; int64 ruleChainIdMSB = 1; int64 ruleChainIdLSB = 2; } @@ -322,22 +325,30 @@ message RelationRequestMsg { string entityType = 3; } +// DEPRECATED. FOR REMOVAL message UserCredentialsRequestMsg { + option deprecated = true; int64 userIdMSB = 1; int64 userIdLSB = 2; } +// DEPRECATED. FOR REMOVAL message DeviceCredentialsRequestMsg { + option deprecated = true; int64 deviceIdMSB = 1; int64 deviceIdLSB = 2; } +// DEPRECATED. FOR REMOVAL message WidgetBundleTypesRequestMsg { + option deprecated = true; int64 widgetBundleIdMSB = 1; int64 widgetBundleIdLSB = 2; } +// DEPRECATED. FOR REMOVAL message EntityViewsRequestMsg { + option deprecated = true; int64 entityIdMSB = 1; int64 entityIdLSB = 2; string entityType = 3; @@ -395,14 +406,14 @@ message UplinkMsg { repeated DeviceCredentialsUpdateMsg deviceCredentialsUpdateMsg = 4; repeated AlarmUpdateMsg alarmUpdateMsg = 5; repeated RelationUpdateMsg relationUpdateMsg = 6; - repeated RuleChainMetadataRequestMsg ruleChainMetadataRequestMsg = 7; // deprecated + repeated RuleChainMetadataRequestMsg ruleChainMetadataRequestMsg = 7 [deprecated = true]; repeated AttributesRequestMsg attributesRequestMsg = 8; repeated RelationRequestMsg relationRequestMsg = 9; - repeated UserCredentialsRequestMsg userCredentialsRequestMsg = 10; - repeated DeviceCredentialsRequestMsg deviceCredentialsRequestMsg = 11; + repeated UserCredentialsRequestMsg userCredentialsRequestMsg = 10 [deprecated = true]; + repeated DeviceCredentialsRequestMsg deviceCredentialsRequestMsg = 11 [deprecated = true]; repeated DeviceRpcCallMsg deviceRpcCallMsg = 12; - repeated WidgetBundleTypesRequestMsg widgetBundleTypesRequestMsg = 14; - repeated EntityViewsRequestMsg entityViewsRequestMsg = 15; + repeated WidgetBundleTypesRequestMsg widgetBundleTypesRequestMsg = 14 [deprecated = true]; + repeated EntityViewsRequestMsg entityViewsRequestMsg = 15 [deprecated = true]; repeated AssetUpdateMsg assetUpdateMsg = 16; repeated DashboardUpdateMsg dashboardUpdateMsg = 17; repeated EntityViewUpdateMsg entityViewUpdateMsg = 18; @@ -430,7 +441,7 @@ message DownlinkMsg { int32 downlinkMsgId = 1; SyncCompletedMsg syncCompletedMsg = 2; repeated EntityDataProto entityData = 3; - repeated DeviceCredentialsRequestMsg deviceCredentialsRequestMsg = 4; + repeated DeviceCredentialsRequestMsg deviceCredentialsRequestMsg = 4 [deprecated = true]; repeated DeviceUpdateMsg deviceUpdateMsg = 5; repeated DeviceProfileUpdateMsg deviceProfileUpdateMsg = 6; repeated DeviceCredentialsUpdateMsg deviceCredentialsUpdateMsg = 7; diff --git a/dao/src/main/java/org/thingsboard/server/dao/rule/BaseRuleChainService.java b/dao/src/main/java/org/thingsboard/server/dao/rule/BaseRuleChainService.java index b7325ed714..b5fde88a0b 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/rule/BaseRuleChainService.java +++ b/dao/src/main/java/org/thingsboard/server/dao/rule/BaseRuleChainService.java @@ -179,8 +179,7 @@ public class BaseRuleChainService extends AbstractEntityService implements RuleC @Transactional @Override - public RuleChainUpdateResult saveRuleChainMetaData(TenantId tenantId, RuleChainMetaData ruleChainMetaData, Function ruleNodeUpdater, - boolean publishSaveEvent) { + public RuleChainUpdateResult saveRuleChainMetaData(TenantId tenantId, RuleChainMetaData ruleChainMetaData, Function ruleNodeUpdater, boolean publishSaveEvent) { Validator.validateId(ruleChainMetaData.getRuleChainId(), "Incorrect rule chain id."); RuleChain ruleChain = findRuleChainById(tenantId, ruleChainMetaData.getRuleChainId()); if (ruleChain == null) { From 07256796897cbeef008c4b2b258c78d8fee8f633 Mon Sep 17 00:00:00 2001 From: trikimiki <41070061+trikimiki@users.noreply.github.com> Date: Thu, 27 Feb 2025 14:59:40 +0200 Subject: [PATCH 111/345] remove checkFolders from other scripts reason - it breaks running TB for MacOS users; will update site documentation accordingly --- docker/docker-install-tb.sh | 2 -- docker/docker-start-services.sh | 2 -- docker/docker-upgrade-tb.sh | 2 -- 3 files changed, 6 deletions(-) diff --git a/docker/docker-install-tb.sh b/docker/docker-install-tb.sh index 1956e50eac..25f089afb4 100755 --- a/docker/docker-install-tb.sh +++ b/docker/docker-install-tb.sh @@ -51,8 +51,6 @@ ADDITIONAL_CACHE_ARGS=$(additionalComposeCacheArgs) || exit $? ADDITIONAL_STARTUP_SERVICES=$(additionalStartupServices) || exit $? -checkFolders --create || exit $? - if [ ! -z "${ADDITIONAL_STARTUP_SERVICES// }" ]; then COMPOSE_ARGS="\ diff --git a/docker/docker-start-services.sh b/docker/docker-start-services.sh index 3cdf10d00f..39dc57c1de 100755 --- a/docker/docker-start-services.sh +++ b/docker/docker-start-services.sh @@ -29,8 +29,6 @@ ADDITIONAL_CACHE_ARGS=$(additionalComposeCacheArgs) || exit $? ADDITIONAL_COMPOSE_MONITORING_ARGS=$(additionalComposeMonitoringArgs) || exit $? -checkFolders --create || exit $? - COMPOSE_ARGS="\ -f docker-compose.yml ${ADDITIONAL_CACHE_ARGS} ${ADDITIONAL_COMPOSE_ARGS} ${ADDITIONAL_COMPOSE_QUEUE_ARGS} ${ADDITIONAL_COMPOSE_MONITORING_ARGS} \ up -d" diff --git a/docker/docker-upgrade-tb.sh b/docker/docker-upgrade-tb.sh index 3be5fbc14b..5a34e29aa9 100755 --- a/docker/docker-upgrade-tb.sh +++ b/docker/docker-upgrade-tb.sh @@ -44,8 +44,6 @@ ADDITIONAL_CACHE_ARGS=$(additionalComposeCacheArgs) || exit $? ADDITIONAL_STARTUP_SERVICES=$(additionalStartupServices) || exit $? -checkFolders --create || exit $? - COMPOSE_ARGS_PULL="\ -f docker-compose.yml ${ADDITIONAL_CACHE_ARGS} ${ADDITIONAL_COMPOSE_ARGS} ${ADDITIONAL_COMPOSE_QUEUE_ARGS} \ pull \ From 16169dc7285ea2758bbecbfe0377772367f2923f Mon Sep 17 00:00:00 2001 From: ViacheslavKlimov Date: Thu, 27 Feb 2025 15:58:26 +0200 Subject: [PATCH 112/345] Improve ZkDiscoveryService shutdown --- .../server/queue/discovery/ZkDiscoveryService.java | 10 +++------- 1 file changed, 3 insertions(+), 7 deletions(-) diff --git a/common/queue/src/main/java/org/thingsboard/server/queue/discovery/ZkDiscoveryService.java b/common/queue/src/main/java/org/thingsboard/server/queue/discovery/ZkDiscoveryService.java index 3ad6164c61..f7a4d2abf6 100644 --- a/common/queue/src/main/java/org/thingsboard/server/queue/discovery/ZkDiscoveryService.java +++ b/common/queue/src/main/java/org/thingsboard/server/queue/discovery/ZkDiscoveryService.java @@ -251,25 +251,21 @@ public class ZkDiscoveryService implements DiscoveryService, PathChildrenCacheLi } } catch (Exception e) { log.error("Failed to delete ZK node {}", nodePath, e); - throw new RuntimeException(e); } } private void destroyZkClient() { stopped = true; - try { - unpublishCurrentServer(); - } catch (Exception e) { - } + unpublishCurrentServer(); CloseableUtils.closeQuietly(cache); CloseableUtils.closeQuietly(client); log.info("ZK client disconnected"); } @PreDestroy - public void destroy() { - destroyZkClient(); + private void destroy() { zkExecutorService.shutdownNow(); + destroyZkClient(); log.info("Stopped discovery service"); } From 58926d2af3dd062bd288474d2823e8ca9c63a4e8 Mon Sep 17 00:00:00 2001 From: Artem Dzhereleiko Date: Thu, 27 Feb 2025 16:02:14 +0200 Subject: [PATCH 113/345] Flow animation for hp scada connector --- .../bottom-right-elbow-connector-hp.svg | 234 ++++++- .../scada_symbols/bottom-tee-connector-hp.svg | 466 +++++++++++++- .../scada_symbols/cross-connector-hp.svg | 582 +++++++++++++++++- .../scada_symbols/horizontal-connector-hp.svg | 200 ++++-- .../left-bottom-elbow-connector-hp.svg | 232 ++++++- .../scada_symbols/left-tee-connector-hp.svg | 466 +++++++++++++- .../left-top-elbow-connector-hp.svg | 234 ++++++- .../long-horizontal-connector-hp.svg | 203 ++++-- .../long-vertical-connector-hp.svg | 200 ++++-- .../scada_symbols/right-tee-connector-hp.svg | 466 +++++++++++++- .../top-right-elbow-connector-hp.svg | 234 ++++++- .../scada_symbols/top-tee-connector-hp.svg | 466 +++++++++++++- .../scada_symbols/vertical-connector-hp.svg | 200 ++++-- .../assets/locale/locale.constant-en_US.json | 16 +- 14 files changed, 3745 insertions(+), 454 deletions(-) diff --git a/application/src/main/data/json/system/scada_symbols/bottom-right-elbow-connector-hp.svg b/application/src/main/data/json/system/scada_symbols/bottom-right-elbow-connector-hp.svg index 4e41fd77c6..37b3a199c6 100644 --- a/application/src/main/data/json/system/scada_symbols/bottom-right-elbow-connector-hp.svg +++ b/application/src/main/data/json/system/scada_symbols/bottom-right-elbow-connector-hp.svg @@ -3,6 +3,7 @@ "description": "Bottom right elbow connector", "widgetSizeX": 1, "widgetSizeY": 1, + "stateRenderFunction": "const {\n flowAnimation,\n animationDirection: flowDirection,\n flowAnimationSpeed\n} = ctx.values;\nconst {\n flowAnimationWidth: lineWidth,\n flowAnimationColor: lineColor,\n flowStyleDash: dashWidth,\n flowStyleGap: dashGap,\n flowDashCap: dashCap\n} = ctx.properties;\nconst line = ctx.tags.line[0].attr('d');\nconst animation = ctx.tags.animationGroup[0];\nconst offset = Date.now() % 1000;\nconst duration = 1 / flowAnimationSpeed;\n\nconst prevFlowAnimation = animation.remember('flowAnimation');\nconst prevFlowDirection = animation.remember('flowDirection');\nconst prevFlowDuration = animation.remember('flowDuration');\n\nif (flowAnimation && flowAnimation !== prevFlowAnimation) {\n animation.remember('flowAnimation', flowAnimation);\n animation.remember('flowDuration', duration);\n animation.remember('flowDirection', flowDirection);\n animateFlow(offset, flowDirection);\n} else if (flowAnimation && flowDirection !== prevFlowDirection) {\n animation.remember('flowDirection', flowDirection);\n animateFlow(offset, flowDirection);\n} else if (flowAnimation && duration !== prevFlowDuration) {\n animation.remember('flowDuration', duration);\n animation.findOne('animate').attr('dur', `${duration}s`) ;\n} else if (!flowAnimation && prevFlowAnimation) {\n animation.remember('flowAnimation', null);\n animation.clear();\n}\n\nfunction animateFlow(offset, flowDirection) {\n animation.clear();\n const dashArray = `${dashWidth}${dashGap ? ` ${dashGap}` : ''}`;\n const value = flowDirection ? `-${dashWidth + (dashGap || dashWidth)}` : `${dashWidth + (dashGap || dashWidth)}`;\n\n animation.add(``);\n}\n", "tags": [ { "tag": "line", @@ -10,23 +11,132 @@ "actions": null } ], - "behavior": [], + "behavior": [ + { + "id": "flowAnimation", + "name": "{i18n:scada.symbol.flow-animation}", + "hint": "{i18n:scada.symbol.flow-animation-hint}", + "group": null, + "type": "value", + "valueType": "BOOLEAN", + "trueLabel": "{i18n:scada.symbol.present}", + "falseLabel": "{i18n:scada.symbol.absent}", + "stateLabel": "{i18n:scada.symbol.flow-present}", + "defaultGetValueSettings": { + "action": "DO_NOTHING", + "defaultValue": false, + "executeRpc": { + "method": "getState", + "requestTimeout": 5000, + "requestPersistent": false, + "persistentPollingInterval": 1000 + }, + "getAttribute": { + "key": "state", + "scope": null + }, + "getTimeSeries": { + "key": "state" + }, + "getAlarmStatus": { + "severityList": null, + "typeList": null + }, + "dataToValue": { + "type": "NONE", + "compareToValue": true, + "dataToValueFunction": "/* Should return boolean value */\nreturn data;" + } + }, + "defaultSetValueSettings": null, + "defaultWidgetActionSettings": null + }, + { + "id": "animationDirection", + "name": "{i18n:scada.symbol.animation-direction}", + "hint": "{i18n:scada.symbol.animation-direction-hint}", + "group": null, + "type": "value", + "valueType": "BOOLEAN", + "trueLabel": "{i18n:scada.symbol.forward}", + "falseLabel": "{i18n:scada.symbol.reverse}", + "stateLabel": "{i18n:scada.symbol.forward}", + "defaultGetValueSettings": { + "action": "DO_NOTHING", + "defaultValue": true, + "executeRpc": { + "method": "getState", + "requestTimeout": 5000, + "requestPersistent": false, + "persistentPollingInterval": 1000 + }, + "getAttribute": { + "scope": null, + "key": "state" + }, + "getTimeSeries": { + "key": "state" + }, + "getAlarmStatus": { + "severityList": null, + "typeList": null + }, + "dataToValue": { + "type": "NONE", + "dataToValueFunction": "/* Should return boolean value */\nreturn data;", + "compareToValue": true + } + }, + "defaultSetValueSettings": null, + "defaultWidgetActionSettings": null + }, + { + "id": "flowAnimationSpeed", + "name": "{i18n:scada.symbol.flow-animation-speed}", + "hint": "{i18n:scada.symbol.flow-animation-speed-hint}", + "group": null, + "type": "value", + "valueType": "DOUBLE", + "trueLabel": null, + "falseLabel": null, + "stateLabel": null, + "defaultGetValueSettings": { + "action": "DO_NOTHING", + "defaultValue": 1, + "executeRpc": { + "method": "getState", + "requestTimeout": 5000, + "requestPersistent": false, + "persistentPollingInterval": 1000 + }, + "getAttribute": { + "scope": null, + "key": "state" + }, + "getTimeSeries": { + "key": "state" + }, + "getAlarmStatus": { + "severityList": null, + "typeList": null + }, + "dataToValue": { + "type": "NONE", + "dataToValueFunction": "/* Should return boolean value */\nreturn data;" + } + }, + "defaultSetValueSettings": null, + "defaultWidgetActionSettings": null + } + ], "properties": [ { "id": "mainLine", "name": "{i18n:scada.symbol.main-line}", "type": "switch", "default": true, - "required": null, - "subLabel": null, - "divider": null, - "fieldSuffix": null, - "disableOnProperty": null, - "rowClass": "", - "fieldClass": "", - "min": null, - "max": null, - "step": null + "disabled": false, + "visible": true }, { "id": "mainLineSize", @@ -37,12 +147,11 @@ "subLabel": "Main", "divider": true, "fieldSuffix": "px", - "disableOnProperty": null, - "rowClass": "", - "fieldClass": "", "min": 0, "max": 99, - "step": 1 + "step": 1, + "disabled": false, + "visible": true }, { "id": "secondaryLineSize", @@ -51,32 +160,95 @@ "default": 2, "required": true, "subLabel": "Secondary", - "divider": null, "fieldSuffix": "px", - "disableOnProperty": null, - "rowClass": "", - "fieldClass": "", "min": 0, "max": 99, - "step": 1 + "step": 1, + "disabled": false, + "visible": true }, { "id": "lineColor", "name": "{i18n:scada.symbol.line-color}", "type": "color", "default": "#1A1A1A", - "required": null, - "subLabel": null, - "divider": null, - "fieldSuffix": null, - "disableOnProperty": null, - "rowClass": "", - "fieldClass": "", - "min": null, - "max": null, - "step": null + "disabled": false, + "visible": true + }, + { + "id": "flowAnimationWidth", + "name": "{i18n:scada.symbol.flow}", + "group": "{i18n:scada.symbol.animation}", + "type": "number", + "default": 4, + "subLabel": "Width", + "fieldSuffix": "px", + "min": 1, + "step": 1, + "disabled": false, + "visible": true + }, + { + "id": "flowAnimationColor", + "name": "{i18n:scada.symbol.flow}", + "group": "{i18n:scada.symbol.animation}", + "type": "color", + "default": "#C8DFF7", + "disabled": false, + "visible": true + }, + { + "id": "flowStyleDash", + "name": "{i18n:scada.symbol.flow-style}", + "group": "{i18n:scada.symbol.animation}", + "type": "number", + "default": 10, + "required": true, + "subLabel": "{i18n:scada.symbol.dash}", + "divider": true, + "fieldSuffix": "px", + "min": 0, + "step": 1, + "disabled": false, + "visible": true + }, + { + "id": "flowStyleGap", + "name": "{i18n:scada.symbol.flow-style}", + "group": "{i18n:scada.symbol.animation}", + "type": "number", + "default": 10, + "subLabel": "{i18n:scada.symbol.gap}", + "fieldSuffix": "px", + "min": 1, + "step": 1, + "disabled": false, + "visible": true + }, + { + "id": "flowDashCap", + "name": "{i18n:scada.symbol.flow-dash-cap}", + "group": "{i18n:scada.symbol.animation}", + "type": "select", + "default": "butt", + "items": [ + { + "value": "butt", + "label": "{i18n:scada.symbol.dash-cap-butt}" + }, + { + "value": "round", + "label": "{i18n:scada.symbol.dash-cap-round}" + }, + { + "value": "square", + "label": "{i18n:scada.symbol.dash-cap-square}" + } + ], + "disabled": false, + "visible": true } ] }]]> - + \ No newline at end of file diff --git a/application/src/main/data/json/system/scada_symbols/bottom-tee-connector-hp.svg b/application/src/main/data/json/system/scada_symbols/bottom-tee-connector-hp.svg index 33942432cb..2feb95e9ff 100644 --- a/application/src/main/data/json/system/scada_symbols/bottom-tee-connector-hp.svg +++ b/application/src/main/data/json/system/scada_symbols/bottom-tee-connector-hp.svg @@ -3,6 +3,7 @@ "description": "Bottom tee connector", "widgetSizeX": 1, "widgetSizeY": 1, + "stateRenderFunction": "const {\n flowAnimationWidth: lineWidth,\n flowAnimationColor: lineColor,\n flowStyleDash: dashWidth,\n flowStyleGap: dashGap,\n flowDashCap: dashCap\n} = ctx.properties;\n\nconst leftLine = \"M0 100H100\";\nconst rightLine = \"M100 100H200\";\nconst bottomLine = \"M 100,200 V 103\";\n\nprepareFlowAnimation('left', leftLine);\nprepareFlowAnimation('right', rightLine);\nprepareFlowAnimation('bottom', bottomLine);\n\nfunction prepareFlowAnimation(prefix, line) {\n const flowAnimation = ctx.values[prefix + 'Flow'];\n const flowDirection = ctx.values[prefix + 'FlowDirection'];\n const flowAnimationSpeed = ctx.values[prefix + 'FlowAnimationSpeed'];\n\n const animation = ctx.tags[prefix + 'Line'][0];\n const offset = Date.now() % 1000;\n const duration = 1 / flowAnimationSpeed;\n \n const prevFlowAnimation = animation.remember('flowAnimation');\n const prevFlowDirection = animation.remember('flowDirection');\n const prevFlowDuration = animation.remember('flowDuration');\n \n if (flowAnimation && flowAnimation !== prevFlowAnimation) {\n animation.remember('flowAnimation', flowAnimation);\n animation.remember('flowDuration', duration);\n animation.remember('flowDirection', flowDirection);\n animateFlow(animation, offset, flowDirection, duration, line);\n } else if (flowAnimation && flowDirection !== prevFlowDirection) {\n animation.remember('flowDirection', flowDirection);\n animateFlow(animation, offset, flowDirection, duration, line);\n } else if (flowAnimation && duration !== prevFlowDuration) {\n animation.remember('flowDuration', duration);\n animation.findOne('animate').attr('dur', `${duration}s`) ;\n } else if (!flowAnimation && prevFlowAnimation) {\n animation.remember('flowAnimation', null);\n animation.clear();\n }\n}\n\nfunction animateFlow(group, offset, flowDirection, duration, line) {\n group.clear();\n const dashArray = `${dashWidth}${dashGap ? ` ${dashGap}` : ''}`;\n const value = flowDirection ? `${dashWidth + (dashGap || dashWidth)}` : `-${dashWidth + (dashGap || dashWidth)}`;\n\n group.add(``);\n}", "tags": [ { "tag": "line", @@ -15,23 +16,364 @@ "actions": null } ], - "behavior": [], + "behavior": [ + { + "id": "leftFlow", + "name": "{i18n:scada.symbol.flow-animation}", + "hint": "{i18n:scada.symbol.flow-animation-hint}", + "group": "{i18n:scada.symbol.left-connector}", + "type": "value", + "valueType": "BOOLEAN", + "trueLabel": "{i18n:scada.symbol.present}", + "falseLabel": "{i18n:scada.symbol.absent}", + "stateLabel": "{i18n:scada.symbol.fluid-present}", + "defaultGetValueSettings": { + "action": "DO_NOTHING", + "defaultValue": false, + "executeRpc": { + "method": "getState", + "requestTimeout": 5000, + "requestPersistent": false, + "persistentPollingInterval": 1000 + }, + "getAttribute": { + "key": "state", + "scope": null + }, + "getTimeSeries": { + "key": "state" + }, + "getAlarmStatus": { + "severityList": null, + "typeList": null + }, + "dataToValue": { + "type": "NONE", + "compareToValue": true, + "dataToValueFunction": "/* Should return boolean value */\nreturn data;" + } + }, + "defaultSetValueSettings": null, + "defaultWidgetActionSettings": null + }, + { + "id": "leftFlowDirection", + "name": "{i18n:scada.symbol.animation-direction}", + "hint": "{i18n:scada.symbol.animation-direction-hint}", + "group": "{i18n:scada.symbol.left-connector}", + "type": "value", + "valueType": "BOOLEAN", + "trueLabel": "{i18n:scada.symbol.forward}", + "falseLabel": "{i18n:scada.symbol.reverse}", + "stateLabel": "{i18n:scada.symbol.forward}", + "defaultGetValueSettings": { + "action": "DO_NOTHING", + "defaultValue": true, + "executeRpc": { + "method": "getState", + "requestTimeout": 5000, + "requestPersistent": false, + "persistentPollingInterval": 1000 + }, + "getAttribute": { + "scope": null, + "key": "state" + }, + "getTimeSeries": { + "key": "state" + }, + "getAlarmStatus": { + "severityList": null, + "typeList": null + }, + "dataToValue": { + "type": "NONE", + "dataToValueFunction": "/* Should return boolean value */\nreturn data;", + "compareToValue": true + } + }, + "defaultSetValueSettings": null, + "defaultWidgetActionSettings": null + }, + { + "id": "leftFlowAnimationSpeed", + "name": "{i18n:scada.symbol.flow-animation-speed}", + "hint": "{i18n:scada.symbol.flow-animation-speed-hint}", + "group": "{i18n:scada.symbol.left-connector}", + "type": "value", + "valueType": "DOUBLE", + "trueLabel": null, + "falseLabel": null, + "stateLabel": null, + "defaultGetValueSettings": { + "action": "DO_NOTHING", + "defaultValue": 1, + "executeRpc": { + "method": "getState", + "requestTimeout": 5000, + "requestPersistent": false, + "persistentPollingInterval": 1000 + }, + "getAttribute": { + "scope": null, + "key": "state" + }, + "getTimeSeries": { + "key": "state" + }, + "getAlarmStatus": { + "severityList": null, + "typeList": null + }, + "dataToValue": { + "type": "NONE", + "dataToValueFunction": "/* Should return boolean value */\nreturn data;" + } + }, + "defaultSetValueSettings": null, + "defaultWidgetActionSettings": null + }, + { + "id": "rightFlow", + "name": "{i18n:scada.symbol.flow-animation}", + "hint": "{i18n:scada.symbol.flow-animation-hint}", + "group": "{i18n:scada.symbol.right-connector}", + "type": "value", + "valueType": "BOOLEAN", + "trueLabel": "{i18n:scada.symbol.present}", + "falseLabel": "{i18n:scada.symbol.absent}", + "stateLabel": "{i18n:scada.symbol.flow-present}", + "defaultGetValueSettings": { + "action": "DO_NOTHING", + "defaultValue": false, + "executeRpc": { + "method": "getState", + "requestTimeout": 5000, + "requestPersistent": false, + "persistentPollingInterval": 1000 + }, + "getAttribute": { + "key": "state", + "scope": null + }, + "getTimeSeries": { + "key": "state" + }, + "getAlarmStatus": { + "severityList": null, + "typeList": null + }, + "dataToValue": { + "type": "NONE", + "compareToValue": true, + "dataToValueFunction": "/* Should return boolean value */\nreturn data;" + } + }, + "defaultSetValueSettings": null, + "defaultWidgetActionSettings": null + }, + { + "id": "rightFlowDirection", + "name": "{i18n:scada.symbol.flow-direction}", + "hint": "{i18n:scada.symbol.flow-direction-hint}", + "group": "{i18n:scada.symbol.right-connector}", + "type": "value", + "valueType": "BOOLEAN", + "trueLabel": "{i18n:scada.symbol.forward}", + "falseLabel": "{i18n:scada.symbol.reverse}", + "stateLabel": "{i18n:scada.symbol.forward}", + "defaultGetValueSettings": { + "action": "DO_NOTHING", + "defaultValue": true, + "executeRpc": { + "method": "getState", + "requestTimeout": 5000, + "requestPersistent": false, + "persistentPollingInterval": 1000 + }, + "getAttribute": { + "scope": null, + "key": "state" + }, + "getTimeSeries": { + "key": "state" + }, + "getAlarmStatus": { + "severityList": null, + "typeList": null + }, + "dataToValue": { + "type": "NONE", + "dataToValueFunction": "/* Should return boolean value */\nreturn data;", + "compareToValue": true + } + }, + "defaultSetValueSettings": null, + "defaultWidgetActionSettings": null + }, + { + "id": "rightFlowAnimationSpeed", + "name": "{i18n:scada.symbol.flow-animation-speed}", + "hint": "{i18n:scada.symbol.flow-animation-speed-hint}", + "group": "{i18n:scada.symbol.right-connector}", + "type": "value", + "valueType": "DOUBLE", + "trueLabel": null, + "falseLabel": null, + "stateLabel": null, + "defaultGetValueSettings": { + "action": "DO_NOTHING", + "defaultValue": 1, + "executeRpc": { + "method": "getState", + "requestTimeout": 5000, + "requestPersistent": false, + "persistentPollingInterval": 1000 + }, + "getAttribute": { + "scope": null, + "key": "state" + }, + "getTimeSeries": { + "key": "state" + }, + "getAlarmStatus": { + "severityList": null, + "typeList": null + }, + "dataToValue": { + "type": "NONE", + "dataToValueFunction": "/* Should return boolean value */\nreturn data;" + } + }, + "defaultSetValueSettings": null, + "defaultWidgetActionSettings": null + }, + { + "id": "bottomFlow", + "name": "{i18n:scada.symbol.flow-animation}", + "hint": "{i18n:scada.symbol.flow-animation-hint}", + "group": "{i18n:scada.symbol.bottom-connector}", + "type": "value", + "valueType": "BOOLEAN", + "trueLabel": "{i18n:scada.symbol.present}", + "falseLabel": "{i18n:scada.symbol.absent}", + "stateLabel": "{i18n:scada.symbol.flow-present}", + "defaultGetValueSettings": { + "action": "DO_NOTHING", + "defaultValue": false, + "executeRpc": { + "method": "getState", + "requestTimeout": 5000, + "requestPersistent": false, + "persistentPollingInterval": 1000 + }, + "getAttribute": { + "key": "state", + "scope": null + }, + "getTimeSeries": { + "key": "state" + }, + "getAlarmStatus": { + "severityList": null, + "typeList": null + }, + "dataToValue": { + "type": "NONE", + "compareToValue": true, + "dataToValueFunction": "/* Should return boolean value */\nreturn data;" + } + }, + "defaultSetValueSettings": null, + "defaultWidgetActionSettings": null + }, + { + "id": "bottomFlowDirection", + "name": "{i18n:scada.symbol.animation-direction}", + "hint": "{i18n:scada.symbol.animation-direction-hint}", + "group": "{i18n:scada.symbol.bottom-connector}", + "type": "value", + "valueType": "BOOLEAN", + "trueLabel": "{i18n:scada.symbol.forward}", + "falseLabel": "{i18n:scada.symbol.reverse}", + "stateLabel": "{i18n:scada.symbol.forward}", + "defaultGetValueSettings": { + "action": "DO_NOTHING", + "defaultValue": true, + "executeRpc": { + "method": "getState", + "requestTimeout": 5000, + "requestPersistent": false, + "persistentPollingInterval": 1000 + }, + "getAttribute": { + "scope": null, + "key": "state" + }, + "getTimeSeries": { + "key": "state" + }, + "getAlarmStatus": { + "severityList": null, + "typeList": null + }, + "dataToValue": { + "type": "NONE", + "dataToValueFunction": "/* Should return boolean value */\nreturn data;", + "compareToValue": true + } + }, + "defaultSetValueSettings": null, + "defaultWidgetActionSettings": null + }, + { + "id": "bottomFlowAnimationSpeed", + "name": "{i18n:scada.symbol.flow-animation-speed}", + "hint": "{i18n:scada.symbol.flow-animation-speed-hint}", + "group": "{i18n:scada.symbol.bottom-connector}", + "type": "value", + "valueType": "DOUBLE", + "trueLabel": null, + "falseLabel": null, + "stateLabel": null, + "defaultGetValueSettings": { + "action": "DO_NOTHING", + "defaultValue": 1, + "executeRpc": { + "method": "getState", + "requestTimeout": 5000, + "requestPersistent": false, + "persistentPollingInterval": 1000 + }, + "getAttribute": { + "scope": null, + "key": "state" + }, + "getTimeSeries": { + "key": "state" + }, + "getAlarmStatus": { + "severityList": null, + "typeList": null + }, + "dataToValue": { + "type": "NONE", + "dataToValueFunction": "/* Should return boolean value */\nreturn data;" + } + }, + "defaultSetValueSettings": null, + "defaultWidgetActionSettings": null + } + ], "properties": [ { "id": "mainLine", "name": "{i18n:scada.symbol.main-line}", "type": "switch", "default": true, - "required": null, - "subLabel": null, - "divider": null, - "fieldSuffix": null, - "disableOnProperty": null, - "rowClass": "", - "fieldClass": "", - "min": null, - "max": null, - "step": null + "disabled": false, + "visible": true }, { "id": "mainLineSize", @@ -42,12 +384,11 @@ "subLabel": "Main", "divider": true, "fieldSuffix": "px", - "disableOnProperty": null, - "rowClass": "", - "fieldClass": "", "min": 0, "max": 99, - "step": 1 + "step": 1, + "disabled": false, + "visible": true }, { "id": "secondaryLineSize", @@ -56,32 +397,95 @@ "default": 2, "required": true, "subLabel": "Secondary", - "divider": null, "fieldSuffix": "px", - "disableOnProperty": null, - "rowClass": "", - "fieldClass": "", "min": 0, "max": 99, - "step": 1 + "step": 1, + "disabled": false, + "visible": true }, { "id": "lineColor", "name": "{i18n:scada.symbol.line-color}", "type": "color", "default": "#1A1A1A", - "required": null, - "subLabel": null, - "divider": null, - "fieldSuffix": null, - "disableOnProperty": null, - "rowClass": "", - "fieldClass": "", - "min": null, - "max": null, - "step": null + "disabled": false, + "visible": true + }, + { + "id": "flowAnimationWidth", + "name": "{i18n:scada.symbol.flow}", + "group": "{i18n:scada.symbol.animation}", + "type": "number", + "default": 4, + "subLabel": "Width", + "fieldSuffix": "px", + "min": 1, + "step": 1, + "disabled": false, + "visible": true + }, + { + "id": "flowAnimationColor", + "name": "{i18n:scada.symbol.flow}", + "group": "{i18n:scada.symbol.animation}", + "type": "color", + "default": "#C8DFF7", + "disabled": false, + "visible": true + }, + { + "id": "flowStyleDash", + "name": "{i18n:scada.symbol.flow-style}", + "group": "{i18n:scada.symbol.animation}", + "type": "number", + "default": 10, + "required": true, + "subLabel": "{i18n:scada.symbol.dash}", + "divider": true, + "fieldSuffix": "px", + "min": 0, + "step": 1, + "disabled": false, + "visible": true + }, + { + "id": "flowStyleGap", + "name": "{i18n:scada.symbol.flow-style}", + "group": "{i18n:scada.symbol.animation}", + "type": "number", + "default": 10, + "subLabel": "{i18n:scada.symbol.gap}", + "fieldSuffix": "px", + "min": 1, + "step": 1, + "disabled": false, + "visible": true + }, + { + "id": "flowDashCap", + "name": "{i18n:scada.symbol.flow-dash-cap}", + "group": "{i18n:scada.symbol.animation}", + "type": "select", + "default": "butt", + "items": [ + { + "value": "butt", + "label": "{i18n:scada.symbol.dash-cap-butt}" + }, + { + "value": "round", + "label": "{i18n:scada.symbol.dash-cap-round}" + }, + { + "value": "square", + "label": "{i18n:scada.symbol.dash-cap-square}" + } + ], + "disabled": false, + "visible": true } ] }]]> - + \ No newline at end of file diff --git a/application/src/main/data/json/system/scada_symbols/cross-connector-hp.svg b/application/src/main/data/json/system/scada_symbols/cross-connector-hp.svg index df708d47d3..ab6798b48b 100644 --- a/application/src/main/data/json/system/scada_symbols/cross-connector-hp.svg +++ b/application/src/main/data/json/system/scada_symbols/cross-connector-hp.svg @@ -3,6 +3,7 @@ "description": "Cross connector", "widgetSizeX": 1, "widgetSizeY": 1, + "stateRenderFunction": "const {\n flowAnimationWidth: lineWidth,\n flowAnimationColor: lineColor,\n flowStyleDash: dashWidth,\n flowStyleGap: dashGap,\n flowDashCap: dashCap\n} = ctx.properties;\n\nconst leftLine = \"M0 100H100\";\nconst topLine = \"M100 97L100 0\";\nconst rightLine = \"M100 100H200\";\nconst bottomLine = \"M 100,200 V 103\";\n\nprepareFlowAnimation('left', leftLine);\nprepareFlowAnimation('top', topLine);\nprepareFlowAnimation('right', rightLine);\nprepareFlowAnimation('bottom', bottomLine);\n\nfunction prepareFlowAnimation(prefix, line) {\n const flowAnimation = ctx.values[prefix + 'Flow'];\n const flowDirection = ctx.values[prefix + 'FlowDirection'];\n const flowAnimationSpeed = ctx.values[prefix + 'FlowAnimationSpeed'];\n\n const animation = ctx.tags[prefix + 'Line'][0];\n const offset = Date.now() % 1000;\n const duration = 1 / flowAnimationSpeed;\n \n const prevFlowAnimation = animation.remember('flowAnimation');\n const prevFlowDirection = animation.remember('flowDirection');\n const prevFlowDuration = animation.remember('flowDuration');\n \n if (flowAnimation && flowAnimation !== prevFlowAnimation) {\n animation.remember('flowAnimation', flowAnimation);\n animation.remember('flowDuration', duration);\n animation.remember('flowDirection', flowDirection);\n animateFlow(animation, offset, flowDirection, duration, line);\n } else if (flowAnimation && flowDirection !== prevFlowDirection) {\n animation.remember('flowDirection', flowDirection);\n animateFlow(animation, offset, flowDirection, duration, line);\n } else if (flowAnimation && duration !== prevFlowDuration) {\n animation.remember('flowDuration', duration);\n animation.findOne('animate').attr('dur', `${duration}s`) ;\n } else if (!flowAnimation && prevFlowAnimation) {\n animation.remember('flowAnimation', null);\n animation.clear();\n }\n}\n\nfunction animateFlow(group, offset, flowDirection, duration, line) {\n group.clear();\n const dashArray = `${dashWidth}${dashGap ? ` ${dashGap}` : ''}`;\n const value = flowDirection ? `${dashWidth + (dashGap || dashWidth)}` : `-${dashWidth + (dashGap || dashWidth)}`;\n\n group.add(``);\n}", "tags": [ { "tag": "line", @@ -15,23 +16,480 @@ "actions": null } ], - "behavior": [], + "behavior": [ + { + "id": "leftFlow", + "name": "{i18n:scada.symbol.flow-animation}", + "hint": "{i18n:scada.symbol.flow-animation-hint}", + "group": "{i18n:scada.symbol.left-connector}", + "type": "value", + "valueType": "BOOLEAN", + "trueLabel": "{i18n:scada.symbol.present}", + "falseLabel": "{i18n:scada.symbol.absent}", + "stateLabel": "{i18n:scada.symbol.flow-present}", + "defaultGetValueSettings": { + "action": "DO_NOTHING", + "defaultValue": false, + "executeRpc": { + "method": "getState", + "requestTimeout": 5000, + "requestPersistent": false, + "persistentPollingInterval": 1000 + }, + "getAttribute": { + "key": "state", + "scope": null + }, + "getTimeSeries": { + "key": "state" + }, + "getAlarmStatus": { + "severityList": null, + "typeList": null + }, + "dataToValue": { + "type": "NONE", + "compareToValue": true, + "dataToValueFunction": "/* Should return boolean value */\nreturn data;" + } + }, + "defaultSetValueSettings": null, + "defaultWidgetActionSettings": null + }, + { + "id": "leftFlowDirection", + "name": "{i18n:scada.symbol.animation-direction}", + "hint": "{i18n:scada.symbol.animation-direction-hint}", + "group": "{i18n:scada.symbol.left-connector}", + "type": "value", + "valueType": "BOOLEAN", + "trueLabel": "{i18n:scada.symbol.forward}", + "falseLabel": "{i18n:scada.symbol.reverse}", + "stateLabel": "{i18n:scada.symbol.forward}", + "defaultGetValueSettings": { + "action": "DO_NOTHING", + "defaultValue": true, + "executeRpc": { + "method": "getState", + "requestTimeout": 5000, + "requestPersistent": false, + "persistentPollingInterval": 1000 + }, + "getAttribute": { + "scope": null, + "key": "state" + }, + "getTimeSeries": { + "key": "state" + }, + "getAlarmStatus": { + "severityList": null, + "typeList": null + }, + "dataToValue": { + "type": "NONE", + "dataToValueFunction": "/* Should return boolean value */\nreturn data;", + "compareToValue": true + } + }, + "defaultSetValueSettings": null, + "defaultWidgetActionSettings": null + }, + { + "id": "leftFlowAnimationSpeed", + "name": "{i18n:scada.symbol.flow-animation-speed}", + "hint": "{i18n:scada.symbol.flow-animation-speed-hint}", + "group": "{i18n:scada.symbol.left-connector}", + "type": "value", + "valueType": "DOUBLE", + "trueLabel": null, + "falseLabel": null, + "stateLabel": null, + "defaultGetValueSettings": { + "action": "DO_NOTHING", + "defaultValue": 1, + "executeRpc": { + "method": "getState", + "requestTimeout": 5000, + "requestPersistent": false, + "persistentPollingInterval": 1000 + }, + "getAttribute": { + "scope": null, + "key": "state" + }, + "getTimeSeries": { + "key": "state" + }, + "getAlarmStatus": { + "severityList": null, + "typeList": null + }, + "dataToValue": { + "type": "NONE", + "dataToValueFunction": "/* Should return boolean value */\nreturn data;" + } + }, + "defaultSetValueSettings": null, + "defaultWidgetActionSettings": null + }, + { + "id": "topFlow", + "name": "{i18n:scada.symbol.flow-animation}", + "hint": "{i18n:scada.symbol.flow-animation-hint}", + "group": "{i18n:scada.symbol.top-connector}", + "type": "value", + "valueType": "BOOLEAN", + "trueLabel": "{i18n:scada.symbol.present}", + "falseLabel": "{i18n:scada.symbol.absent}", + "stateLabel": "{i18n:scada.symbol.fluid-present}", + "defaultGetValueSettings": { + "action": "DO_NOTHING", + "defaultValue": false, + "executeRpc": { + "method": "getState", + "requestTimeout": 5000, + "requestPersistent": false, + "persistentPollingInterval": 1000 + }, + "getAttribute": { + "key": "state", + "scope": null + }, + "getTimeSeries": { + "key": "state" + }, + "getAlarmStatus": { + "severityList": null, + "typeList": null + }, + "dataToValue": { + "type": "NONE", + "compareToValue": true, + "dataToValueFunction": "/* Should return boolean value */\nreturn data;" + } + }, + "defaultSetValueSettings": null, + "defaultWidgetActionSettings": null + }, + { + "id": "topFlowDirection", + "name": "{i18n:scada.symbol.animation-direction}", + "hint": "{i18n:scada.symbol.animation-direction-hint}", + "group": "{i18n:scada.symbol.top-connector}", + "type": "value", + "valueType": "BOOLEAN", + "trueLabel": "{i18n:scada.symbol.forward}", + "falseLabel": "{i18n:scada.symbol.reverse}", + "stateLabel": "{i18n:scada.symbol.forward}", + "defaultGetValueSettings": { + "action": "DO_NOTHING", + "defaultValue": true, + "executeRpc": { + "method": "getState", + "requestTimeout": 5000, + "requestPersistent": false, + "persistentPollingInterval": 1000 + }, + "getAttribute": { + "scope": null, + "key": "state" + }, + "getTimeSeries": { + "key": "state" + }, + "getAlarmStatus": { + "severityList": null, + "typeList": null + }, + "dataToValue": { + "type": "NONE", + "dataToValueFunction": "/* Should return boolean value */\nreturn data;", + "compareToValue": true + } + }, + "defaultSetValueSettings": null, + "defaultWidgetActionSettings": null + }, + { + "id": "topFlowAnimationSpeed", + "name": "{i18n:scada.symbol.flow-animation-speed}", + "hint": "{i18n:scada.symbol.flow-animation-speed-hint}", + "group": "{i18n:scada.symbol.top-connector}", + "type": "value", + "valueType": "DOUBLE", + "trueLabel": null, + "falseLabel": null, + "stateLabel": null, + "defaultGetValueSettings": { + "action": "DO_NOTHING", + "defaultValue": 1, + "executeRpc": { + "method": "getState", + "requestTimeout": 5000, + "requestPersistent": false, + "persistentPollingInterval": 1000 + }, + "getAttribute": { + "scope": null, + "key": "state" + }, + "getTimeSeries": { + "key": "state" + }, + "getAlarmStatus": { + "severityList": null, + "typeList": null + }, + "dataToValue": { + "type": "NONE", + "dataToValueFunction": "/* Should return boolean value */\nreturn data;" + } + }, + "defaultSetValueSettings": null, + "defaultWidgetActionSettings": null + }, + { + "id": "rightFlow", + "name": "{i18n:scada.symbol.flow-animation}", + "hint": "{i18n:scada.symbol.flow-animation-hint}", + "group": "{i18n:scada.symbol.right-connector}", + "type": "value", + "valueType": "BOOLEAN", + "trueLabel": "{i18n:scada.symbol.present}", + "falseLabel": "{i18n:scada.symbol.absent}", + "stateLabel": "{i18n:scada.symbol.flow-present}", + "defaultGetValueSettings": { + "action": "DO_NOTHING", + "defaultValue": false, + "executeRpc": { + "method": "getState", + "requestTimeout": 5000, + "requestPersistent": false, + "persistentPollingInterval": 1000 + }, + "getAttribute": { + "key": "state", + "scope": null + }, + "getTimeSeries": { + "key": "state" + }, + "getAlarmStatus": { + "severityList": null, + "typeList": null + }, + "dataToValue": { + "type": "NONE", + "compareToValue": true, + "dataToValueFunction": "/* Should return boolean value */\nreturn data;" + } + }, + "defaultSetValueSettings": null, + "defaultWidgetActionSettings": null + }, + { + "id": "rightFlowDirection", + "name": "{i18n:scada.symbol.flow-direction}", + "hint": "{i18n:scada.symbol.flow-direction-hint}", + "group": "{i18n:scada.symbol.right-connector}", + "type": "value", + "valueType": "BOOLEAN", + "trueLabel": "{i18n:scada.symbol.forward}", + "falseLabel": "{i18n:scada.symbol.reverse}", + "stateLabel": "{i18n:scada.symbol.forward}", + "defaultGetValueSettings": { + "action": "DO_NOTHING", + "defaultValue": true, + "executeRpc": { + "method": "getState", + "requestTimeout": 5000, + "requestPersistent": false, + "persistentPollingInterval": 1000 + }, + "getAttribute": { + "scope": null, + "key": "state" + }, + "getTimeSeries": { + "key": "state" + }, + "getAlarmStatus": { + "severityList": null, + "typeList": null + }, + "dataToValue": { + "type": "NONE", + "dataToValueFunction": "/* Should return boolean value */\nreturn data;", + "compareToValue": true + } + }, + "defaultSetValueSettings": null, + "defaultWidgetActionSettings": null + }, + { + "id": "rightFlowAnimationSpeed", + "name": "{i18n:scada.symbol.flow-animation-speed}", + "hint": "{i18n:scada.symbol.flow-animation-speed-hint}", + "group": "{i18n:scada.symbol.right-connector}", + "type": "value", + "valueType": "DOUBLE", + "trueLabel": null, + "falseLabel": null, + "stateLabel": null, + "defaultGetValueSettings": { + "action": "DO_NOTHING", + "defaultValue": 1, + "executeRpc": { + "method": "getState", + "requestTimeout": 5000, + "requestPersistent": false, + "persistentPollingInterval": 1000 + }, + "getAttribute": { + "scope": null, + "key": "state" + }, + "getTimeSeries": { + "key": "state" + }, + "getAlarmStatus": { + "severityList": null, + "typeList": null + }, + "dataToValue": { + "type": "NONE", + "dataToValueFunction": "/* Should return boolean value */\nreturn data;" + } + }, + "defaultSetValueSettings": null, + "defaultWidgetActionSettings": null + }, + { + "id": "bottomFlow", + "name": "{i18n:scada.symbol.flow-animation}", + "hint": "{i18n:scada.symbol.flow-animation-hint}", + "group": "{i18n:scada.symbol.bottom-connector}", + "type": "value", + "valueType": "BOOLEAN", + "trueLabel": "{i18n:scada.symbol.present}", + "falseLabel": "{i18n:scada.symbol.absent}", + "stateLabel": "{i18n:scada.symbol.flow-present}", + "defaultGetValueSettings": { + "action": "DO_NOTHING", + "defaultValue": false, + "executeRpc": { + "method": "getState", + "requestTimeout": 5000, + "requestPersistent": false, + "persistentPollingInterval": 1000 + }, + "getAttribute": { + "key": "state", + "scope": null + }, + "getTimeSeries": { + "key": "state" + }, + "getAlarmStatus": { + "severityList": null, + "typeList": null + }, + "dataToValue": { + "type": "NONE", + "compareToValue": true, + "dataToValueFunction": "/* Should return boolean value */\nreturn data;" + } + }, + "defaultSetValueSettings": null, + "defaultWidgetActionSettings": null + }, + { + "id": "bottomFlowDirection", + "name": "{i18n:scada.symbol.animation-direction}", + "hint": "{i18n:scada.symbol.animation-direction-hint}", + "group": "{i18n:scada.symbol.bottom-connector}", + "type": "value", + "valueType": "BOOLEAN", + "trueLabel": "{i18n:scada.symbol.forward}", + "falseLabel": "{i18n:scada.symbol.reverse}", + "stateLabel": "{i18n:scada.symbol.forward}", + "defaultGetValueSettings": { + "action": "DO_NOTHING", + "defaultValue": true, + "executeRpc": { + "method": "getState", + "requestTimeout": 5000, + "requestPersistent": false, + "persistentPollingInterval": 1000 + }, + "getAttribute": { + "scope": null, + "key": "state" + }, + "getTimeSeries": { + "key": "state" + }, + "getAlarmStatus": { + "severityList": null, + "typeList": null + }, + "dataToValue": { + "type": "NONE", + "dataToValueFunction": "/* Should return boolean value */\nreturn data;", + "compareToValue": true + } + }, + "defaultSetValueSettings": null, + "defaultWidgetActionSettings": null + }, + { + "id": "bottomFlowAnimationSpeed", + "name": "{i18n:scada.symbol.flow-animation-speed}", + "hint": "{i18n:scada.symbol.flow-animation-speed-hint}", + "group": "{i18n:scada.symbol.bottom-connector}", + "type": "value", + "valueType": "DOUBLE", + "trueLabel": null, + "falseLabel": null, + "stateLabel": null, + "defaultGetValueSettings": { + "action": "DO_NOTHING", + "defaultValue": 1, + "executeRpc": { + "method": "getState", + "requestTimeout": 5000, + "requestPersistent": false, + "persistentPollingInterval": 1000 + }, + "getAttribute": { + "scope": null, + "key": "state" + }, + "getTimeSeries": { + "key": "state" + }, + "getAlarmStatus": { + "severityList": null, + "typeList": null + }, + "dataToValue": { + "type": "NONE", + "dataToValueFunction": "/* Should return boolean value */\nreturn data;" + } + }, + "defaultSetValueSettings": null, + "defaultWidgetActionSettings": null + } + ], "properties": [ { "id": "mainLine", "name": "{i18n:scada.symbol.main-line}", "type": "switch", "default": true, - "required": null, - "subLabel": null, - "divider": null, - "fieldSuffix": null, - "disableOnProperty": null, - "rowClass": "", - "fieldClass": "", - "min": null, - "max": null, - "step": null + "disabled": false, + "visible": true }, { "id": "mainLineSize", @@ -42,12 +500,11 @@ "subLabel": "Main", "divider": true, "fieldSuffix": "px", - "disableOnProperty": null, - "rowClass": "", - "fieldClass": "", "min": 0, "max": 99, - "step": 1 + "step": 1, + "disabled": false, + "visible": true }, { "id": "secondaryLineSize", @@ -56,32 +513,95 @@ "default": 2, "required": true, "subLabel": "Secondary", - "divider": null, "fieldSuffix": "px", - "disableOnProperty": null, - "rowClass": "", - "fieldClass": "", "min": 0, "max": 99, - "step": 1 + "step": 1, + "disabled": false, + "visible": true }, { "id": "lineColor", "name": "{i18n:scada.symbol.line-color}", "type": "color", "default": "#1A1A1A", - "required": null, - "subLabel": null, - "divider": null, - "fieldSuffix": null, - "disableOnProperty": null, - "rowClass": "", - "fieldClass": "", - "min": null, - "max": null, - "step": null + "disabled": false, + "visible": true + }, + { + "id": "flowAnimationWidth", + "name": "{i18n:scada.symbol.flow}", + "group": "{i18n:scada.symbol.animation}", + "type": "number", + "default": 4, + "subLabel": "Width", + "fieldSuffix": "px", + "min": 1, + "step": 1, + "disabled": false, + "visible": true + }, + { + "id": "flowAnimationColor", + "name": "{i18n:scada.symbol.flow}", + "group": "{i18n:scada.symbol.animation}", + "type": "color", + "default": "#C8DFF7", + "disabled": false, + "visible": true + }, + { + "id": "flowStyleDash", + "name": "{i18n:scada.symbol.flow-style}", + "group": "{i18n:scada.symbol.animation}", + "type": "number", + "default": 10, + "required": true, + "subLabel": "{i18n:scada.symbol.dash}", + "divider": true, + "fieldSuffix": "px", + "min": 0, + "step": 1, + "disabled": false, + "visible": true + }, + { + "id": "flowStyleGap", + "name": "{i18n:scada.symbol.flow-style}", + "group": "{i18n:scada.symbol.animation}", + "type": "number", + "default": 10, + "subLabel": "{i18n:scada.symbol.gap}", + "fieldSuffix": "px", + "min": 1, + "step": 1, + "disabled": false, + "visible": true + }, + { + "id": "flowDashCap", + "name": "{i18n:scada.symbol.flow-dash-cap}", + "group": "{i18n:scada.symbol.animation}", + "type": "select", + "default": "butt", + "items": [ + { + "value": "butt", + "label": "{i18n:scada.symbol.dash-cap-butt}" + }, + { + "value": "round", + "label": "{i18n:scada.symbol.dash-cap-round}" + }, + { + "value": "square", + "label": "{i18n:scada.symbol.dash-cap-square}" + } + ], + "disabled": false, + "visible": true } ] }]]> - + \ No newline at end of file diff --git a/application/src/main/data/json/system/scada_symbols/horizontal-connector-hp.svg b/application/src/main/data/json/system/scada_symbols/horizontal-connector-hp.svg index 33b6c0a222..74d048e884 100644 --- a/application/src/main/data/json/system/scada_symbols/horizontal-connector-hp.svg +++ b/application/src/main/data/json/system/scada_symbols/horizontal-connector-hp.svg @@ -3,6 +3,7 @@ "description": "Horizontal connector with an optional directional arrow to visually indicate flow.", "widgetSizeX": 1, "widgetSizeY": 1, + "stateRenderFunction": "const {\n flowAnimation,\n arrowDirection: flowDirection,\n flowAnimationSpeed\n} = ctx.values;\nconst {\n flowAnimationWidth: lineWidth,\n flowAnimationColor: lineColor,\n flowStyleDash: dashWidth,\n flowStyleGap: dashGap,\n flowDashCap: dashCap\n} = ctx.properties;\nconst line = ctx.tags.line[0].attr('d');\nconst animation = ctx.tags.animationGroup[0];\nconst offset = Date.now() % 1000;\nconst duration = 1 / flowAnimationSpeed;\n\nconst prevFlowAnimation = animation.remember('flowAnimation');\nconst prevFlowDirection = animation.remember('flowDirection');\nconst prevFlowDuration = animation.remember('flowDuration');\n\nif (flowAnimation && flowAnimation !== prevFlowAnimation) {\n animation.remember('flowAnimation', flowAnimation);\n animation.remember('flowDuration', duration);\n animation.remember('flowDirection', flowDirection);\n animateFlow(offset, flowDirection);\n} else if (flowAnimation && flowDirection !== prevFlowDirection) {\n animation.remember('flowDirection', flowDirection);\n animateFlow(offset, flowDirection);\n} else if (flowAnimation && duration !== prevFlowDuration) {\n animation.remember('flowDuration', duration);\n animation.findOne('animate').attr('dur', `${duration}s`) ;\n} else if (!flowAnimation && prevFlowAnimation) {\n animation.remember('flowAnimation', null);\n animation.clear();\n}\n\nfunction animateFlow(offset, flowDirection) {\n animation.clear();\n const dashArray = `${dashWidth}${dashGap ? ` ${dashGap}` : ''}`;\n const value = flowDirection ? `${dashWidth + (dashGap || dashWidth)}` : `-${dashWidth + (dashGap || dashWidth)}`;\n\n animation.add(``);\n}\n", "tags": [ { "tag": "arrow", @@ -85,6 +86,83 @@ }, "defaultSetValueSettings": null, "defaultWidgetActionSettings": null + }, + { + "id": "flowAnimation", + "name": "{i18n:scada.symbol.flow-animation}", + "hint": "{i18n:scada.symbol.flow-animation-hint}", + "group": null, + "type": "value", + "valueType": "BOOLEAN", + "trueLabel": "{i18n:scada.symbol.present}", + "falseLabel": "{i18n:scada.symbol.absent}", + "stateLabel": "{i18n:scada.symbol.flow-present}", + "defaultGetValueSettings": { + "action": "DO_NOTHING", + "defaultValue": false, + "executeRpc": { + "method": "getState", + "requestTimeout": 5000, + "requestPersistent": false, + "persistentPollingInterval": 1000 + }, + "getAttribute": { + "key": "state", + "scope": null + }, + "getTimeSeries": { + "key": "state" + }, + "getAlarmStatus": { + "severityList": null, + "typeList": null + }, + "dataToValue": { + "type": "NONE", + "compareToValue": true, + "dataToValueFunction": "/* Should return boolean value */\nreturn data;" + } + }, + "defaultSetValueSettings": null, + "defaultWidgetActionSettings": null + }, + { + "id": "flowAnimationSpeed", + "name": "{i18n:scada.symbol.flow-animation-speed}", + "hint": "{i18n:scada.symbol.flow-animation-speed-hint}", + "group": null, + "type": "value", + "valueType": "DOUBLE", + "trueLabel": null, + "falseLabel": null, + "stateLabel": null, + "defaultGetValueSettings": { + "action": "DO_NOTHING", + "defaultValue": 1, + "executeRpc": { + "method": "getState", + "requestTimeout": 5000, + "requestPersistent": false, + "persistentPollingInterval": 1000 + }, + "getAttribute": { + "scope": null, + "key": "state" + }, + "getTimeSeries": { + "key": "state" + }, + "getAlarmStatus": { + "severityList": null, + "typeList": null + }, + "dataToValue": { + "type": "NONE", + "dataToValueFunction": "/* Should return boolean value */\nreturn data;" + } + }, + "defaultSetValueSettings": null, + "defaultWidgetActionSettings": null } ], "properties": [ @@ -93,16 +171,8 @@ "name": "{i18n:scada.symbol.main-line}", "type": "switch", "default": true, - "required": null, - "subLabel": null, - "divider": null, - "fieldSuffix": null, - "disableOnProperty": null, - "rowClass": "", - "fieldClass": "", - "min": null, - "max": null, - "step": null + "disabled": false, + "visible": true }, { "id": "mainLineSize", @@ -113,12 +183,11 @@ "subLabel": "Main", "divider": true, "fieldSuffix": "px", - "disableOnProperty": null, - "rowClass": "", - "fieldClass": "", "min": 0, "max": 99, - "step": 1 + "step": 1, + "disabled": false, + "visible": true }, { "id": "secondaryLineSize", @@ -127,48 +196,93 @@ "default": 2, "required": true, "subLabel": "Secondary", - "divider": null, "fieldSuffix": "px", - "disableOnProperty": null, - "rowClass": "", - "fieldClass": "", "min": 0, "max": 99, - "step": 1 + "step": 1, + "disabled": false, + "visible": true }, { "id": "lineColor", "name": "{i18n:scada.symbol.line-color}", "type": "color", "default": "#1A1A1A", - "required": null, - "subLabel": null, - "divider": null, - "fieldSuffix": null, - "disableOnProperty": null, - "rowClass": "", - "fieldClass": "", - "min": null, - "max": null, - "step": null + "disabled": false, + "visible": true }, { - "id": "arrowColor", - "name": "{i18n:scada.symbol.arrow-color}", + "id": "flowAnimationWidth", + "name": "{i18n:scada.symbol.flow}", + "group": "{i18n:scada.symbol.animation}", + "type": "number", + "default": 4, + "subLabel": "Width", + "fieldSuffix": "px", + "min": 1, + "step": 1, + "disabled": false, + "visible": true + }, + { + "id": "flowAnimationColor", + "name": "{i18n:scada.symbol.flow}", + "group": "{i18n:scada.symbol.animation}", "type": "color", - "default": "#1A1A1A", - "required": null, - "subLabel": null, - "divider": null, - "fieldSuffix": null, - "disableOnProperty": null, - "rowClass": "", - "fieldClass": "", - "min": null, - "max": null, - "step": null + "default": "#C8DFF7" + }, + { + "id": "flowStyleDash", + "name": "{i18n:scada.symbol.flow-style}", + "group": "{i18n:scada.symbol.animation}", + "type": "number", + "default": 10, + "required": true, + "subLabel": "{i18n:scada.symbol.dash}", + "divider": true, + "fieldSuffix": "px", + "min": 0, + "step": 1, + "disabled": false, + "visible": true + }, + { + "id": "flowStyleGap", + "name": "{i18n:scada.symbol.flow-style}", + "group": "{i18n:scada.symbol.animation}", + "type": "number", + "default": 10, + "subLabel": "{i18n:scada.symbol.gap}", + "fieldSuffix": "px", + "min": 1, + "step": 1, + "disabled": false, + "visible": true + }, + { + "id": "flowDashCap", + "name": "{i18n:scada.symbol.flow-dash-cap}", + "group": "{i18n:scada.symbol.animation}", + "type": "select", + "default": "butt", + "items": [ + { + "value": "butt", + "label": "{i18n:scada.symbol.dash-cap-butt}" + }, + { + "value": "round", + "label": "{i18n:scada.symbol.dash-cap-round}" + }, + { + "value": "square", + "label": "{i18n:scada.symbol.dash-cap-square}" + } + ], + "disabled": false, + "visible": true } ] }]]> - + \ No newline at end of file diff --git a/application/src/main/data/json/system/scada_symbols/left-bottom-elbow-connector-hp.svg b/application/src/main/data/json/system/scada_symbols/left-bottom-elbow-connector-hp.svg index bfc97fe928..fd14834d12 100644 --- a/application/src/main/data/json/system/scada_symbols/left-bottom-elbow-connector-hp.svg +++ b/application/src/main/data/json/system/scada_symbols/left-bottom-elbow-connector-hp.svg @@ -3,6 +3,7 @@ "description": "Left bottom elbow connector", "widgetSizeX": 1, "widgetSizeY": 1, + "stateRenderFunction": "const {\n flowAnimation,\n animationDirection: flowDirection,\n flowAnimationSpeed\n} = ctx.values;\nconst {\n flowAnimationWidth: lineWidth,\n flowAnimationColor: lineColor,\n flowStyleDash: dashWidth,\n flowStyleGap: dashGap,\n flowDashCap: dashCap\n} = ctx.properties;\nconst line = ctx.tags.line[0].attr('d');\nconst animation = ctx.tags.animationGroup[0];\nconst offset = Date.now() % 1000;\nconst duration = 1 / flowAnimationSpeed;\n\nconst prevFlowAnimation = animation.remember('flowAnimation');\nconst prevFlowDirection = animation.remember('flowDirection');\nconst prevFlowDuration = animation.remember('flowDuration');\n\nif (flowAnimation && flowAnimation !== prevFlowAnimation) {\n animation.remember('flowAnimation', flowAnimation);\n animation.remember('flowDuration', duration);\n animation.remember('flowDirection', flowDirection);\n animateFlow(offset, flowDirection);\n} else if (flowAnimation && flowDirection !== prevFlowDirection) {\n animation.remember('flowDirection', flowDirection);\n animateFlow(offset, flowDirection);\n} else if (flowAnimation && duration !== prevFlowDuration) {\n animation.remember('flowDuration', duration);\n animation.findOne('animate').attr('dur', `${duration}s`) ;\n} else if (!flowAnimation && prevFlowAnimation) {\n animation.remember('flowAnimation', null);\n animation.clear();\n}\n\nfunction animateFlow(offset, flowDirection) {\n animation.clear();\n const dashArray = `${dashWidth}${dashGap ? ` ${dashGap}` : ''}`;\n const value = flowDirection ? `${dashWidth + (dashGap || dashWidth)}` : `-${dashWidth + (dashGap || dashWidth)}`;\n\n animation.add(``);\n}\n", "tags": [ { "tag": "line", @@ -10,23 +11,132 @@ "actions": null } ], - "behavior": [], + "behavior": [ + { + "id": "flowAnimation", + "name": "{i18n:scada.symbol.flow-animation}", + "hint": "{i18n:scada.symbol.flow-animation-hint}", + "group": null, + "type": "value", + "valueType": "BOOLEAN", + "trueLabel": "{i18n:scada.symbol.present}", + "falseLabel": "{i18n:scada.symbol.absent}", + "stateLabel": "{i18n:scada.symbol.flow-present}", + "defaultGetValueSettings": { + "action": "DO_NOTHING", + "defaultValue": false, + "executeRpc": { + "method": "getState", + "requestTimeout": 5000, + "requestPersistent": false, + "persistentPollingInterval": 1000 + }, + "getAttribute": { + "key": "state", + "scope": null + }, + "getTimeSeries": { + "key": "state" + }, + "getAlarmStatus": { + "severityList": null, + "typeList": null + }, + "dataToValue": { + "type": "NONE", + "compareToValue": true, + "dataToValueFunction": "/* Should return boolean value */\nreturn data;" + } + }, + "defaultSetValueSettings": null, + "defaultWidgetActionSettings": null + }, + { + "id": "animationDirection", + "name": "{i18n:scada.symbol.animation-direction}", + "hint": "{i18n:scada.symbol.animation-direction-hint}", + "group": null, + "type": "value", + "valueType": "BOOLEAN", + "trueLabel": "{i18n:scada.symbol.forward}", + "falseLabel": "{i18n:scada.symbol.reverse}", + "stateLabel": "{i18n:scada.symbol.forward}", + "defaultGetValueSettings": { + "action": "DO_NOTHING", + "defaultValue": true, + "executeRpc": { + "method": "getState", + "requestTimeout": 5000, + "requestPersistent": false, + "persistentPollingInterval": 1000 + }, + "getAttribute": { + "scope": null, + "key": "state" + }, + "getTimeSeries": { + "key": "state" + }, + "getAlarmStatus": { + "severityList": null, + "typeList": null + }, + "dataToValue": { + "type": "NONE", + "dataToValueFunction": "/* Should return boolean value */\nreturn data;", + "compareToValue": true + } + }, + "defaultSetValueSettings": null, + "defaultWidgetActionSettings": null + }, + { + "id": "flowAnimationSpeed", + "name": "{i18n:scada.symbol.flow-animation-speed}", + "hint": "{i18n:scada.symbol.flow-animation-speed-hint}", + "group": null, + "type": "value", + "valueType": "DOUBLE", + "trueLabel": null, + "falseLabel": null, + "stateLabel": null, + "defaultGetValueSettings": { + "action": "DO_NOTHING", + "defaultValue": 1, + "executeRpc": { + "method": "getState", + "requestTimeout": 5000, + "requestPersistent": false, + "persistentPollingInterval": 1000 + }, + "getAttribute": { + "scope": null, + "key": "state" + }, + "getTimeSeries": { + "key": "state" + }, + "getAlarmStatus": { + "severityList": null, + "typeList": null + }, + "dataToValue": { + "type": "NONE", + "dataToValueFunction": "/* Should return boolean value */\nreturn data;" + } + }, + "defaultSetValueSettings": null, + "defaultWidgetActionSettings": null + } + ], "properties": [ { "id": "mainLine", "name": "{i18n:scada.symbol.main-line}", "type": "switch", "default": true, - "required": null, - "subLabel": null, - "divider": null, - "fieldSuffix": null, - "disableOnProperty": null, - "rowClass": "", - "fieldClass": "", - "min": null, - "max": null, - "step": null + "disabled": false, + "visible": true }, { "id": "mainLineSize", @@ -37,12 +147,11 @@ "subLabel": "Main", "divider": true, "fieldSuffix": "px", - "disableOnProperty": null, - "rowClass": "", - "fieldClass": "", "min": 0, "max": 99, - "step": 1 + "step": 1, + "disabled": false, + "visible": true }, { "id": "secondaryLineSize", @@ -51,32 +160,93 @@ "default": 2, "required": true, "subLabel": "Secondary", - "divider": null, "fieldSuffix": "px", - "disableOnProperty": null, - "rowClass": "", - "fieldClass": "", "min": 0, "max": 99, - "step": 1 + "step": 1, + "disabled": false, + "visible": true }, { "id": "lineColor", "name": "{i18n:scada.symbol.line-color}", "type": "color", "default": "#1A1A1A", - "required": null, - "subLabel": null, - "divider": null, - "fieldSuffix": null, - "disableOnProperty": null, - "rowClass": "", - "fieldClass": "", - "min": null, - "max": null, - "step": null + "disabled": false, + "visible": true + }, + { + "id": "flowAnimationWidth", + "name": "{i18n:scada.symbol.flow}", + "group": "{i18n:scada.symbol.animation}", + "type": "number", + "default": 4, + "subLabel": "Width", + "fieldSuffix": "px", + "min": 1, + "step": 1, + "disabled": false, + "visible": true + }, + { + "id": "flowAnimationColor", + "name": "{i18n:scada.symbol.flow}", + "group": "{i18n:scada.symbol.animation}", + "type": "color", + "default": "#C8DFF7" + }, + { + "id": "flowStyleDash", + "name": "{i18n:scada.symbol.flow-style}", + "group": "{i18n:scada.symbol.animation}", + "type": "number", + "default": 10, + "required": true, + "subLabel": "{i18n:scada.symbol.dash}", + "divider": true, + "fieldSuffix": "px", + "min": 0, + "step": 1, + "disabled": false, + "visible": true + }, + { + "id": "flowStyleGap", + "name": "{i18n:scada.symbol.flow-style}", + "group": "{i18n:scada.symbol.animation}", + "type": "number", + "default": 10, + "subLabel": "{i18n:scada.symbol.gap}", + "fieldSuffix": "px", + "min": 1, + "step": 1, + "disabled": false, + "visible": true + }, + { + "id": "flowDashCap", + "name": "{i18n:scada.symbol.flow-dash-cap}", + "group": "{i18n:scada.symbol.animation}", + "type": "select", + "default": "butt", + "items": [ + { + "value": "butt", + "label": "{i18n:scada.symbol.dash-cap-butt}" + }, + { + "value": "round", + "label": "{i18n:scada.symbol.dash-cap-round}" + }, + { + "value": "square", + "label": "{i18n:scada.symbol.dash-cap-square}" + } + ], + "disabled": false, + "visible": true } ] }]]> - + \ No newline at end of file diff --git a/application/src/main/data/json/system/scada_symbols/left-tee-connector-hp.svg b/application/src/main/data/json/system/scada_symbols/left-tee-connector-hp.svg index eac3869a1a..af83a4abb3 100644 --- a/application/src/main/data/json/system/scada_symbols/left-tee-connector-hp.svg +++ b/application/src/main/data/json/system/scada_symbols/left-tee-connector-hp.svg @@ -3,6 +3,7 @@ "description": "Left tee connector", "widgetSizeX": 1, "widgetSizeY": 1, + "stateRenderFunction": "const {\n flowAnimationWidth: lineWidth,\n flowAnimationColor: lineColor,\n flowStyleDash: dashWidth,\n flowStyleGap: dashGap,\n flowDashCap: dashCap\n} = ctx.properties;\n\nconst leftLine = \"M0 100H97\";\nconst topLine = \"M100 100L100 0\";\nconst bottomLine = \"M 100,200 V 100\";\n\nprepareFlowAnimation('left', leftLine);\nprepareFlowAnimation('top', topLine);\nprepareFlowAnimation('bottom', bottomLine);\n\nfunction prepareFlowAnimation(prefix, line) {\n const flowAnimation = ctx.values[prefix + 'Flow'];\n const flowDirection = ctx.values[prefix + 'FlowDirection'];\n const flowAnimationSpeed = ctx.values[prefix + 'FlowAnimationSpeed'];\n\n const animation = ctx.tags[prefix + 'Line'][0];\n const offset = Date.now() % 1000;\n const duration = 1 / flowAnimationSpeed;\n \n const prevFlowAnimation = animation.remember('flowAnimation');\n const prevFlowDirection = animation.remember('flowDirection');\n const prevFlowDuration = animation.remember('flowDuration');\n \n if (flowAnimation && flowAnimation !== prevFlowAnimation) {\n animation.remember('flowAnimation', flowAnimation);\n animation.remember('flowDuration', duration);\n animation.remember('flowDirection', flowDirection);\n animateFlow(animation, offset, flowDirection, duration, line);\n } else if (flowAnimation && flowDirection !== prevFlowDirection) {\n animation.remember('flowDirection', flowDirection);\n animateFlow(animation, offset, flowDirection, duration, line);\n } else if (flowAnimation && duration !== prevFlowDuration) {\n animation.remember('flowDuration', duration);\n animation.findOne('animate').attr('dur', `${duration}s`) ;\n } else if (!flowAnimation && prevFlowAnimation) {\n animation.remember('flowAnimation', null);\n animation.clear();\n }\n}\n\nfunction animateFlow(group, offset, flowDirection, duration, line) {\n group.clear();\n const dashArray = `${dashWidth}${dashGap ? ` ${dashGap}` : ''}`;\n const value = flowDirection ? `${dashWidth + (dashGap || dashWidth)}` : `-${dashWidth + (dashGap || dashWidth)}`;\n\n group.add(``);\n}", "tags": [ { "tag": "line", @@ -15,23 +16,364 @@ "actions": null } ], - "behavior": [], + "behavior": [ + { + "id": "leftFlow", + "name": "{i18n:scada.symbol.flow-animation}", + "hint": "{i18n:scada.symbol.flow-animation-hint}", + "group": "{i18n:scada.symbol.left-connector}", + "type": "value", + "valueType": "BOOLEAN", + "trueLabel": "{i18n:scada.symbol.present}", + "falseLabel": "{i18n:scada.symbol.absent}", + "stateLabel": "{i18n:scada.symbol.flow-present}", + "defaultGetValueSettings": { + "action": "DO_NOTHING", + "defaultValue": false, + "executeRpc": { + "method": "getState", + "requestTimeout": 5000, + "requestPersistent": false, + "persistentPollingInterval": 1000 + }, + "getAttribute": { + "key": "state", + "scope": null + }, + "getTimeSeries": { + "key": "state" + }, + "getAlarmStatus": { + "severityList": null, + "typeList": null + }, + "dataToValue": { + "type": "NONE", + "compareToValue": true, + "dataToValueFunction": "/* Should return boolean value */\nreturn data;" + } + }, + "defaultSetValueSettings": null, + "defaultWidgetActionSettings": null + }, + { + "id": "leftFlowDirection", + "name": "{i18n:scada.symbol.animation-direction}", + "hint": "{i18n:scada.symbol.animation-direction-hint}", + "group": "{i18n:scada.symbol.left-connector}", + "type": "value", + "valueType": "BOOLEAN", + "trueLabel": "{i18n:scada.symbol.forward}", + "falseLabel": "{i18n:scada.symbol.reverse}", + "stateLabel": "{i18n:scada.symbol.forward}", + "defaultGetValueSettings": { + "action": "DO_NOTHING", + "defaultValue": true, + "executeRpc": { + "method": "getState", + "requestTimeout": 5000, + "requestPersistent": false, + "persistentPollingInterval": 1000 + }, + "getAttribute": { + "scope": null, + "key": "state" + }, + "getTimeSeries": { + "key": "state" + }, + "getAlarmStatus": { + "severityList": null, + "typeList": null + }, + "dataToValue": { + "type": "NONE", + "dataToValueFunction": "/* Should return boolean value */\nreturn data;", + "compareToValue": true + } + }, + "defaultSetValueSettings": null, + "defaultWidgetActionSettings": null + }, + { + "id": "leftFlowAnimationSpeed", + "name": "{i18n:scada.symbol.flow-animation-speed}", + "hint": "{i18n:scada.symbol.flow-animation-speed-hint}", + "group": "{i18n:scada.symbol.left-connector}", + "type": "value", + "valueType": "DOUBLE", + "trueLabel": null, + "falseLabel": null, + "stateLabel": null, + "defaultGetValueSettings": { + "action": "DO_NOTHING", + "defaultValue": 1, + "executeRpc": { + "method": "getState", + "requestTimeout": 5000, + "requestPersistent": false, + "persistentPollingInterval": 1000 + }, + "getAttribute": { + "scope": null, + "key": "state" + }, + "getTimeSeries": { + "key": "state" + }, + "getAlarmStatus": { + "severityList": null, + "typeList": null + }, + "dataToValue": { + "type": "NONE", + "dataToValueFunction": "/* Should return boolean value */\nreturn data;" + } + }, + "defaultSetValueSettings": null, + "defaultWidgetActionSettings": null + }, + { + "id": "topFlow", + "name": "{i18n:scada.symbol.flow-animation}", + "hint": "{i18n:scada.symbol.flow-animation-hint}", + "group": "{i18n:scada.symbol.top-connector}", + "type": "value", + "valueType": "BOOLEAN", + "trueLabel": "{i18n:scada.symbol.present}", + "falseLabel": "{i18n:scada.symbol.absent}", + "stateLabel": "{i18n:scada.symbol.fluid-present}", + "defaultGetValueSettings": { + "action": "DO_NOTHING", + "defaultValue": false, + "executeRpc": { + "method": "getState", + "requestTimeout": 5000, + "requestPersistent": false, + "persistentPollingInterval": 1000 + }, + "getAttribute": { + "key": "state", + "scope": null + }, + "getTimeSeries": { + "key": "state" + }, + "getAlarmStatus": { + "severityList": null, + "typeList": null + }, + "dataToValue": { + "type": "NONE", + "compareToValue": true, + "dataToValueFunction": "/* Should return boolean value */\nreturn data;" + } + }, + "defaultSetValueSettings": null, + "defaultWidgetActionSettings": null + }, + { + "id": "topFlowDirection", + "name": "{i18n:scada.symbol.animation-direction}", + "hint": "{i18n:scada.symbol.animation-direction-hint}", + "group": "{i18n:scada.symbol.top-connector}", + "type": "value", + "valueType": "BOOLEAN", + "trueLabel": "{i18n:scada.symbol.forward}", + "falseLabel": "{i18n:scada.symbol.reverse}", + "stateLabel": "{i18n:scada.symbol.forward}", + "defaultGetValueSettings": { + "action": "DO_NOTHING", + "defaultValue": true, + "executeRpc": { + "method": "getState", + "requestTimeout": 5000, + "requestPersistent": false, + "persistentPollingInterval": 1000 + }, + "getAttribute": { + "scope": null, + "key": "state" + }, + "getTimeSeries": { + "key": "state" + }, + "getAlarmStatus": { + "severityList": null, + "typeList": null + }, + "dataToValue": { + "type": "NONE", + "dataToValueFunction": "/* Should return boolean value */\nreturn data;", + "compareToValue": true + } + }, + "defaultSetValueSettings": null, + "defaultWidgetActionSettings": null + }, + { + "id": "topFlowAnimationSpeed", + "name": "{i18n:scada.symbol.flow-animation-speed}", + "hint": "{i18n:scada.symbol.flow-animation-speed-hint}", + "group": "{i18n:scada.symbol.top-connector}", + "type": "value", + "valueType": "DOUBLE", + "trueLabel": null, + "falseLabel": null, + "stateLabel": null, + "defaultGetValueSettings": { + "action": "DO_NOTHING", + "defaultValue": 1, + "executeRpc": { + "method": "getState", + "requestTimeout": 5000, + "requestPersistent": false, + "persistentPollingInterval": 1000 + }, + "getAttribute": { + "scope": null, + "key": "state" + }, + "getTimeSeries": { + "key": "state" + }, + "getAlarmStatus": { + "severityList": null, + "typeList": null + }, + "dataToValue": { + "type": "NONE", + "dataToValueFunction": "/* Should return boolean value */\nreturn data;" + } + }, + "defaultSetValueSettings": null, + "defaultWidgetActionSettings": null + }, + { + "id": "bottomFlow", + "name": "{i18n:scada.symbol.flow-animation}", + "hint": "{i18n:scada.symbol.flow-animation-hint}", + "group": "{i18n:scada.symbol.bottom-connector}", + "type": "value", + "valueType": "BOOLEAN", + "trueLabel": "{i18n:scada.symbol.present}", + "falseLabel": "{i18n:scada.symbol.absent}", + "stateLabel": "{i18n:scada.symbol.flow-present}", + "defaultGetValueSettings": { + "action": "DO_NOTHING", + "defaultValue": false, + "executeRpc": { + "method": "getState", + "requestTimeout": 5000, + "requestPersistent": false, + "persistentPollingInterval": 1000 + }, + "getAttribute": { + "key": "state", + "scope": null + }, + "getTimeSeries": { + "key": "state" + }, + "getAlarmStatus": { + "severityList": null, + "typeList": null + }, + "dataToValue": { + "type": "NONE", + "compareToValue": true, + "dataToValueFunction": "/* Should return boolean value */\nreturn data;" + } + }, + "defaultSetValueSettings": null, + "defaultWidgetActionSettings": null + }, + { + "id": "bottomFlowDirection", + "name": "{i18n:scada.symbol.flow-direction}", + "hint": "{i18n:scada.symbol.flow-direction-hint}", + "group": "{i18n:scada.symbol.bottom-connector}", + "type": "value", + "valueType": "BOOLEAN", + "trueLabel": "{i18n:scada.symbol.forward}", + "falseLabel": "{i18n:scada.symbol.reverse}", + "stateLabel": "{i18n:scada.symbol.forward}", + "defaultGetValueSettings": { + "action": "DO_NOTHING", + "defaultValue": true, + "executeRpc": { + "method": "getState", + "requestTimeout": 5000, + "requestPersistent": false, + "persistentPollingInterval": 1000 + }, + "getAttribute": { + "scope": null, + "key": "state" + }, + "getTimeSeries": { + "key": "state" + }, + "getAlarmStatus": { + "severityList": null, + "typeList": null + }, + "dataToValue": { + "type": "NONE", + "dataToValueFunction": "/* Should return boolean value */\nreturn data;", + "compareToValue": true + } + }, + "defaultSetValueSettings": null, + "defaultWidgetActionSettings": null + }, + { + "id": "bottomFlowAnimationSpeed", + "name": "{i18n:scada.symbol.flow-animation-speed}", + "hint": "{i18n:scada.symbol.flow-animation-speed-hint}", + "group": "{i18n:scada.symbol.bottom-connector}", + "type": "value", + "valueType": "DOUBLE", + "trueLabel": null, + "falseLabel": null, + "stateLabel": null, + "defaultGetValueSettings": { + "action": "DO_NOTHING", + "defaultValue": 1, + "executeRpc": { + "method": "getState", + "requestTimeout": 5000, + "requestPersistent": false, + "persistentPollingInterval": 1000 + }, + "getAttribute": { + "scope": null, + "key": "state" + }, + "getTimeSeries": { + "key": "state" + }, + "getAlarmStatus": { + "severityList": null, + "typeList": null + }, + "dataToValue": { + "type": "NONE", + "dataToValueFunction": "/* Should return boolean value */\nreturn data;" + } + }, + "defaultSetValueSettings": null, + "defaultWidgetActionSettings": null + } + ], "properties": [ { "id": "mainLine", "name": "{i18n:scada.symbol.main-line}", "type": "switch", "default": true, - "required": null, - "subLabel": null, - "divider": null, - "fieldSuffix": null, - "disableOnProperty": null, - "rowClass": "", - "fieldClass": "", - "min": null, - "max": null, - "step": null + "disabled": false, + "visible": true }, { "id": "mainLineSize", @@ -42,12 +384,11 @@ "subLabel": "Main", "divider": true, "fieldSuffix": "px", - "disableOnProperty": null, - "rowClass": "", - "fieldClass": "", "min": 0, "max": 99, - "step": 1 + "step": 1, + "disabled": false, + "visible": true }, { "id": "secondaryLineSize", @@ -56,32 +397,95 @@ "default": 2, "required": true, "subLabel": "Secondary", - "divider": null, "fieldSuffix": "px", - "disableOnProperty": null, - "rowClass": "", - "fieldClass": "", "min": 0, "max": 99, - "step": 1 + "step": 1, + "disabled": false, + "visible": true }, { "id": "lineColor", "name": "{i18n:scada.symbol.line-color}", "type": "color", "default": "#1A1A1A", - "required": null, - "subLabel": null, - "divider": null, - "fieldSuffix": null, - "disableOnProperty": null, - "rowClass": "", - "fieldClass": "", - "min": null, - "max": null, - "step": null + "disabled": false, + "visible": true + }, + { + "id": "flowAnimationWidth", + "name": "{i18n:scada.symbol.flow}", + "group": "{i18n:scada.symbol.animation}", + "type": "number", + "default": 4, + "subLabel": "Width", + "fieldSuffix": "px", + "min": 1, + "step": 1, + "disabled": false, + "visible": true + }, + { + "id": "flowAnimationColor", + "name": "{i18n:scada.symbol.flow}", + "group": "{i18n:scada.symbol.animation}", + "type": "color", + "default": "#C8DFF7", + "disabled": false, + "visible": true + }, + { + "id": "flowStyleDash", + "name": "{i18n:scada.symbol.flow-style}", + "group": "{i18n:scada.symbol.animation}", + "type": "number", + "default": 10, + "required": true, + "subLabel": "{i18n:scada.symbol.dash}", + "divider": true, + "fieldSuffix": "px", + "min": 0, + "step": 1, + "disabled": false, + "visible": true + }, + { + "id": "flowStyleGap", + "name": "{i18n:scada.symbol.flow-style}", + "group": "{i18n:scada.symbol.animation}", + "type": "number", + "default": 10, + "subLabel": "{i18n:scada.symbol.gap}", + "fieldSuffix": "px", + "min": 1, + "step": 1, + "disabled": false, + "visible": true + }, + { + "id": "flowDashCap", + "name": "{i18n:scada.symbol.flow-dash-cap}", + "group": "{i18n:scada.symbol.animation}", + "type": "select", + "default": "butt", + "items": [ + { + "value": "butt", + "label": "{i18n:scada.symbol.dash-cap-butt}" + }, + { + "value": "round", + "label": "{i18n:scada.symbol.dash-cap-round}" + }, + { + "value": "square", + "label": "{i18n:scada.symbol.dash-cap-square}" + } + ], + "disabled": false, + "visible": true } ] }]]> - + \ No newline at end of file diff --git a/application/src/main/data/json/system/scada_symbols/left-top-elbow-connector-hp.svg b/application/src/main/data/json/system/scada_symbols/left-top-elbow-connector-hp.svg index 6dfbc9e265..5b6d30ab65 100644 --- a/application/src/main/data/json/system/scada_symbols/left-top-elbow-connector-hp.svg +++ b/application/src/main/data/json/system/scada_symbols/left-top-elbow-connector-hp.svg @@ -3,6 +3,7 @@ "description": "Left top elbow connector", "widgetSizeX": 1, "widgetSizeY": 1, + "stateRenderFunction": "const {\n flowAnimation,\n animationDirection: flowDirection,\n flowAnimationSpeed\n} = ctx.values;\nconst {\n flowAnimationWidth: lineWidth,\n flowAnimationColor: lineColor,\n flowStyleDash: dashWidth,\n flowStyleGap: dashGap,\n flowDashCap: dashCap\n} = ctx.properties;\nconst line = ctx.tags.line[0].attr('d');\nconst animation = ctx.tags.animationGroup[0];\nconst offset = Date.now() % 1000;\nconst duration = 1 / flowAnimationSpeed;\n\nconst prevFlowAnimation = animation.remember('flowAnimation');\nconst prevFlowDirection = animation.remember('flowDirection');\nconst prevFlowDuration = animation.remember('flowDuration');\n\nif (flowAnimation && flowAnimation !== prevFlowAnimation) {\n animation.remember('flowAnimation', flowAnimation);\n animation.remember('flowDuration', duration);\n animation.remember('flowDirection', flowDirection);\n animateFlow(offset, flowDirection);\n} else if (flowAnimation && flowDirection !== prevFlowDirection) {\n animation.remember('flowDirection', flowDirection);\n animateFlow(offset, flowDirection);\n} else if (flowAnimation && duration !== prevFlowDuration) {\n animation.remember('flowDuration', duration);\n animation.findOne('animate').attr('dur', `${duration}s`) ;\n} else if (!flowAnimation && prevFlowAnimation) {\n animation.remember('flowAnimation', null);\n animation.clear();\n}\n\nfunction animateFlow(offset, flowDirection) {\n animation.clear();\n const dashArray = `${dashWidth}${dashGap ? ` ${dashGap}` : ''}`;\n const value = flowDirection ? `${dashWidth + (dashGap || dashWidth)}` : `-${dashWidth + (dashGap || dashWidth)}`;\n\n animation.add(``);\n}\n", "tags": [ { "tag": "line", @@ -10,23 +11,132 @@ "actions": null } ], - "behavior": [], + "behavior": [ + { + "id": "flowAnimation", + "name": "{i18n:scada.symbol.flow-animation}", + "hint": "{i18n:scada.symbol.flow-animation-hint}", + "group": null, + "type": "value", + "valueType": "BOOLEAN", + "trueLabel": "{i18n:scada.symbol.present}", + "falseLabel": "{i18n:scada.symbol.absent}", + "stateLabel": "{i18n:scada.symbol.flow-present}", + "defaultGetValueSettings": { + "action": "DO_NOTHING", + "defaultValue": false, + "executeRpc": { + "method": "getState", + "requestTimeout": 5000, + "requestPersistent": false, + "persistentPollingInterval": 1000 + }, + "getAttribute": { + "key": "state", + "scope": null + }, + "getTimeSeries": { + "key": "state" + }, + "getAlarmStatus": { + "severityList": null, + "typeList": null + }, + "dataToValue": { + "type": "NONE", + "compareToValue": true, + "dataToValueFunction": "/* Should return boolean value */\nreturn data;" + } + }, + "defaultSetValueSettings": null, + "defaultWidgetActionSettings": null + }, + { + "id": "animationDirection", + "name": "{i18n:scada.symbol.animation-direction}", + "hint": "{i18n:scada.symbol.animation-direction-hint}", + "group": null, + "type": "value", + "valueType": "BOOLEAN", + "trueLabel": "{i18n:scada.symbol.forward}", + "falseLabel": "{i18n:scada.symbol.reverse}", + "stateLabel": "{i18n:scada.symbol.forward}", + "defaultGetValueSettings": { + "action": "DO_NOTHING", + "defaultValue": true, + "executeRpc": { + "method": "getState", + "requestTimeout": 5000, + "requestPersistent": false, + "persistentPollingInterval": 1000 + }, + "getAttribute": { + "scope": null, + "key": "state" + }, + "getTimeSeries": { + "key": "state" + }, + "getAlarmStatus": { + "severityList": null, + "typeList": null + }, + "dataToValue": { + "type": "NONE", + "dataToValueFunction": "/* Should return boolean value */\nreturn data;", + "compareToValue": true + } + }, + "defaultSetValueSettings": null, + "defaultWidgetActionSettings": null + }, + { + "id": "flowAnimationSpeed", + "name": "{i18n:scada.symbol.flow-animation-speed}", + "hint": "{i18n:scada.symbol.flow-animation-speed-hint}", + "group": null, + "type": "value", + "valueType": "DOUBLE", + "trueLabel": null, + "falseLabel": null, + "stateLabel": null, + "defaultGetValueSettings": { + "action": "DO_NOTHING", + "defaultValue": 1, + "executeRpc": { + "method": "getState", + "requestTimeout": 5000, + "requestPersistent": false, + "persistentPollingInterval": 1000 + }, + "getAttribute": { + "scope": null, + "key": "state" + }, + "getTimeSeries": { + "key": "state" + }, + "getAlarmStatus": { + "severityList": null, + "typeList": null + }, + "dataToValue": { + "type": "NONE", + "dataToValueFunction": "/* Should return boolean value */\nreturn data;" + } + }, + "defaultSetValueSettings": null, + "defaultWidgetActionSettings": null + } + ], "properties": [ { "id": "mainLine", "name": "{i18n:scada.symbol.main-line}", "type": "switch", "default": true, - "required": null, - "subLabel": null, - "divider": null, - "fieldSuffix": null, - "disableOnProperty": null, - "rowClass": "", - "fieldClass": "", - "min": null, - "max": null, - "step": null + "disabled": false, + "visible": true }, { "id": "mainLineSize", @@ -37,12 +147,11 @@ "subLabel": "Main", "divider": true, "fieldSuffix": "px", - "disableOnProperty": null, - "rowClass": "", - "fieldClass": "", "min": 0, "max": 99, - "step": 1 + "step": 1, + "disabled": false, + "visible": true }, { "id": "secondaryLineSize", @@ -51,32 +160,95 @@ "default": 2, "required": true, "subLabel": "Secondary", - "divider": null, "fieldSuffix": "px", - "disableOnProperty": null, - "rowClass": "", - "fieldClass": "", "min": 0, "max": 99, - "step": 1 + "step": 1, + "disabled": false, + "visible": true }, { "id": "lineColor", "name": "{i18n:scada.symbol.line-color}", "type": "color", "default": "#1A1A1A", - "required": null, - "subLabel": null, - "divider": null, - "fieldSuffix": null, - "disableOnProperty": null, - "rowClass": "", - "fieldClass": "", - "min": null, - "max": null, - "step": null + "disabled": false, + "visible": true + }, + { + "id": "flowAnimationWidth", + "name": "{i18n:scada.symbol.flow}", + "group": "{i18n:scada.symbol.animation}", + "type": "number", + "default": 4, + "subLabel": "Width", + "fieldSuffix": "px", + "min": 1, + "step": 1, + "disabled": false, + "visible": true + }, + { + "id": "flowAnimationColor", + "name": "{i18n:scada.symbol.flow}", + "group": "{i18n:scada.symbol.animation}", + "type": "color", + "default": "#C8DFF7", + "disabled": false, + "visible": true + }, + { + "id": "flowStyleDash", + "name": "{i18n:scada.symbol.flow-style}", + "group": "{i18n:scada.symbol.animation}", + "type": "number", + "default": 10, + "required": true, + "subLabel": "{i18n:scada.symbol.dash}", + "divider": true, + "fieldSuffix": "px", + "min": 0, + "step": 1, + "disabled": false, + "visible": true + }, + { + "id": "flowStyleGap", + "name": "{i18n:scada.symbol.flow-style}", + "group": "{i18n:scada.symbol.animation}", + "type": "number", + "default": 10, + "subLabel": "{i18n:scada.symbol.gap}", + "fieldSuffix": "px", + "min": 1, + "step": 1, + "disabled": false, + "visible": true + }, + { + "id": "flowDashCap", + "name": "{i18n:scada.symbol.flow-dash-cap}", + "group": "{i18n:scada.symbol.animation}", + "type": "select", + "default": "butt", + "items": [ + { + "value": "butt", + "label": "{i18n:scada.symbol.dash-cap-butt}" + }, + { + "value": "round", + "label": "{i18n:scada.symbol.dash-cap-round}" + }, + { + "value": "square", + "label": "{i18n:scada.symbol.dash-cap-square}" + } + ], + "disabled": false, + "visible": true } ] }]]> - + \ No newline at end of file diff --git a/application/src/main/data/json/system/scada_symbols/long-horizontal-connector-hp.svg b/application/src/main/data/json/system/scada_symbols/long-horizontal-connector-hp.svg index 86bb07a520..3e65bd6b04 100644 --- a/application/src/main/data/json/system/scada_symbols/long-horizontal-connector-hp.svg +++ b/application/src/main/data/json/system/scada_symbols/long-horizontal-connector-hp.svg @@ -4,6 +4,7 @@ "description": "Long horizontal connector with an optional directional arrow to visually indicate flow.", "widgetSizeX": 2, "widgetSizeY": 1, + "stateRenderFunction": "const {\n flowAnimation,\n arrowDirection: flowDirection,\n flowAnimationSpeed\n} = ctx.values;\nconst {\n flowAnimationWidth: lineWidth,\n flowAnimationColor: lineColor,\n flowStyleDash: dashWidth,\n flowStyleGap: dashGap,\n flowDashCap: dashCap\n} = ctx.properties;\nconst line = ctx.tags.line[0].attr('d');\nconst animation = ctx.tags.animationGroup[0];\nconst offset = Date.now() % 1000;\nconst duration = 1 / flowAnimationSpeed;\n\nconst prevFlowAnimation = animation.remember('flowAnimation');\nconst prevFlowDirection = animation.remember('flowDirection');\nconst prevFlowDuration = animation.remember('flowDuration');\n\nif (flowAnimation && flowAnimation !== prevFlowAnimation) {\n animation.remember('flowAnimation', flowAnimation);\n animation.remember('flowDuration', duration);\n animation.remember('flowDirection', flowDirection);\n animateFlow(offset, flowDirection);\n} else if (flowAnimation && flowDirection !== prevFlowDirection) {\n animation.remember('flowDirection', flowDirection);\n animateFlow(offset, flowDirection);\n} else if (flowAnimation && duration !== prevFlowDuration) {\n animation.remember('flowDuration', duration);\n animation.findOne('animate').attr('dur', `${duration}s`) ;\n} else if (!flowAnimation && prevFlowAnimation) {\n animation.remember('flowAnimation', null);\n animation.clear();\n}\n\nfunction animateFlow(offset, flowDirection) {\n animation.clear();\n const dashArray = `${dashWidth}${dashGap ? ` ${dashGap}` : ''}`;\n const value = flowDirection ? `${dashWidth + (dashGap || dashWidth)}` : `-${dashWidth + (dashGap || dashWidth)}`;\n\n animation.add(``);\n}\n", "tags": [ { "tag": "arrow", @@ -86,6 +87,83 @@ }, "defaultSetValueSettings": null, "defaultWidgetActionSettings": null + }, + { + "id": "flowAnimation", + "name": "{i18n:scada.symbol.flow-animation}", + "hint": "{i18n:scada.symbol.flow-animation-hint}", + "group": null, + "type": "value", + "valueType": "BOOLEAN", + "trueLabel": "{i18n:scada.symbol.present}", + "falseLabel": "{i18n:scada.symbol.absent}", + "stateLabel": "{i18n:scada.symbol.flow-present}", + "defaultGetValueSettings": { + "action": "DO_NOTHING", + "defaultValue": false, + "executeRpc": { + "method": "getState", + "requestTimeout": 5000, + "requestPersistent": false, + "persistentPollingInterval": 1000 + }, + "getAttribute": { + "key": "state", + "scope": null + }, + "getTimeSeries": { + "key": "state" + }, + "getAlarmStatus": { + "severityList": null, + "typeList": null + }, + "dataToValue": { + "type": "NONE", + "compareToValue": true, + "dataToValueFunction": "/* Should return boolean value */\nreturn data;" + } + }, + "defaultSetValueSettings": null, + "defaultWidgetActionSettings": null + }, + { + "id": "flowAnimationSpeed", + "name": "{i18n:scada.symbol.flow-animation-speed}", + "hint": "{i18n:scada.symbol.flow-animation-speed-hint}", + "group": null, + "type": "value", + "valueType": "DOUBLE", + "trueLabel": null, + "falseLabel": null, + "stateLabel": null, + "defaultGetValueSettings": { + "action": "DO_NOTHING", + "defaultValue": 1, + "executeRpc": { + "method": "getState", + "requestTimeout": 5000, + "requestPersistent": false, + "persistentPollingInterval": 1000 + }, + "getAttribute": { + "scope": null, + "key": "state" + }, + "getTimeSeries": { + "key": "state" + }, + "getAlarmStatus": { + "severityList": null, + "typeList": null + }, + "dataToValue": { + "type": "NONE", + "dataToValueFunction": "/* Should return boolean value */\nreturn data;" + } + }, + "defaultSetValueSettings": null, + "defaultWidgetActionSettings": null } ], "properties": [ @@ -94,16 +172,8 @@ "name": "{i18n:scada.symbol.main-line}", "type": "switch", "default": true, - "required": null, - "subLabel": null, - "divider": null, - "fieldSuffix": null, - "disableOnProperty": null, - "rowClass": "", - "fieldClass": "", - "min": null, - "max": null, - "step": null + "disabled": false, + "visible": true }, { "id": "mainLineSize", @@ -114,12 +184,11 @@ "subLabel": "Main", "divider": true, "fieldSuffix": "px", - "disableOnProperty": null, - "rowClass": "", - "fieldClass": "", "min": 0, "max": 99, - "step": 1 + "step": 1, + "disabled": false, + "visible": true }, { "id": "secondaryLineSize", @@ -128,49 +197,93 @@ "default": 2, "required": true, "subLabel": "Secondary", - "divider": null, "fieldSuffix": "px", - "disableOnProperty": null, - "rowClass": "", - "fieldClass": "", "min": 0, "max": 99, - "step": 1 + "step": 1, + "disabled": false, + "visible": true }, { "id": "lineColor", "name": "{i18n:scada.symbol.line-color}", "type": "color", "default": "#1A1A1A", - "required": null, - "subLabel": null, - "divider": null, - "fieldSuffix": null, - "disableOnProperty": null, - "rowClass": "", - "fieldClass": "", - "min": null, - "max": null, - "step": null + "disabled": false, + "visible": true }, { - "id": "arrowColor", - "name": "{i18n:scada.symbol.arrow-color}", + "id": "flowAnimationWidth", + "name": "{i18n:scada.symbol.flow}", + "group": "{i18n:scada.symbol.animation}", + "type": "number", + "default": 4, + "subLabel": "Width", + "fieldSuffix": "px", + "min": 1, + "step": 1, + "disabled": false, + "visible": true + }, + { + "id": "flowAnimationColor", + "name": "{i18n:scada.symbol.flow}", + "group": "{i18n:scada.symbol.animation}", "type": "color", - "default": "#1A1A1A", - "required": null, - "subLabel": null, - "divider": null, - "fieldSuffix": null, - "disableOnProperty": null, - "rowClass": "", - "fieldClass": "", - "min": null, - "max": null, - "step": null + "default": "#C8DFF7" + }, + { + "id": "flowStyleDash", + "name": "{i18n:scada.symbol.flow-style}", + "group": "{i18n:scada.symbol.animation}", + "type": "number", + "default": 10, + "required": true, + "subLabel": "{i18n:scada.symbol.dash}", + "divider": true, + "fieldSuffix": "px", + "min": 0, + "step": 1, + "disabled": false, + "visible": true + }, + { + "id": "flowStyleGap", + "name": "{i18n:scada.symbol.flow-style}", + "group": "{i18n:scada.symbol.animation}", + "type": "number", + "default": 10, + "subLabel": "{i18n:scada.symbol.gap}", + "fieldSuffix": "px", + "min": 1, + "step": 1, + "disabled": false, + "visible": true + }, + { + "id": "flowDashCap", + "name": "{i18n:scada.symbol.flow-dash-cap}", + "group": "{i18n:scada.symbol.animation}", + "type": "select", + "default": "butt", + "items": [ + { + "value": "butt", + "label": "{i18n:scada.symbol.dash-cap-butt}" + }, + { + "value": "round", + "label": "{i18n:scada.symbol.dash-cap-round}" + }, + { + "value": "square", + "label": "{i18n:scada.symbol.dash-cap-square}" + } + ], + "disabled": false, + "visible": true } ] }]]> - - - + + \ No newline at end of file diff --git a/application/src/main/data/json/system/scada_symbols/long-vertical-connector-hp.svg b/application/src/main/data/json/system/scada_symbols/long-vertical-connector-hp.svg index d7a0aa8aa7..b5c9730842 100644 --- a/application/src/main/data/json/system/scada_symbols/long-vertical-connector-hp.svg +++ b/application/src/main/data/json/system/scada_symbols/long-vertical-connector-hp.svg @@ -3,6 +3,7 @@ "description": "Long vertical connector with an optional directional arrow to visually indicate flow.", "widgetSizeX": 1, "widgetSizeY": 2, + "stateRenderFunction": "const {\n flowAnimation,\n arrowDirection: flowDirection,\n flowAnimationSpeed\n} = ctx.values;\nconst {\n flowAnimationWidth: lineWidth,\n flowAnimationColor: lineColor,\n flowStyleDash: dashWidth,\n flowStyleGap: dashGap,\n flowDashCap: dashCap\n} = ctx.properties;\nconst line = ctx.tags.line[0].attr('d');\nconst animation = ctx.tags.animationGroup[0];\nconst offset = Date.now() % 1000;\nconst duration = 1 / flowAnimationSpeed;\n\nconst prevFlowAnimation = animation.remember('flowAnimation');\nconst prevFlowDirection = animation.remember('flowDirection');\nconst prevFlowDuration = animation.remember('flowDuration');\n\nif (flowAnimation && flowAnimation !== prevFlowAnimation) {\n animation.remember('flowAnimation', flowAnimation);\n animation.remember('flowDuration', duration);\n animation.remember('flowDirection', flowDirection);\n animateFlow(offset, flowDirection);\n} else if (flowAnimation && flowDirection !== prevFlowDirection) {\n animation.remember('flowDirection', flowDirection);\n animateFlow(offset, flowDirection);\n} else if (flowAnimation && duration !== prevFlowDuration) {\n animation.remember('flowDuration', duration);\n animation.findOne('animate').attr('dur', `${duration}s`) ;\n} else if (!flowAnimation && prevFlowAnimation) {\n animation.remember('flowAnimation', null);\n animation.clear();\n}\n\nfunction animateFlow(offset, flowDirection) {\n animation.clear();\n const dashArray = `${dashWidth}${dashGap ? ` ${dashGap}` : ''}`;\n const value = flowDirection ? `${dashWidth + (dashGap || dashWidth)}` : `-${dashWidth + (dashGap || dashWidth)}`;\n\n animation.add(``);\n}\n", "tags": [ { "tag": "arrow", @@ -85,6 +86,83 @@ }, "defaultSetValueSettings": null, "defaultWidgetActionSettings": null + }, + { + "id": "flowAnimation", + "name": "{i18n:scada.symbol.flow-animation}", + "hint": "{i18n:scada.symbol.flow-animation-hint}", + "group": null, + "type": "value", + "valueType": "BOOLEAN", + "trueLabel": "{i18n:scada.symbol.present}", + "falseLabel": "{i18n:scada.symbol.absent}", + "stateLabel": "{i18n:scada.symbol.flow-present}", + "defaultGetValueSettings": { + "action": "DO_NOTHING", + "defaultValue": false, + "executeRpc": { + "method": "getState", + "requestTimeout": 5000, + "requestPersistent": false, + "persistentPollingInterval": 1000 + }, + "getAttribute": { + "key": "state", + "scope": null + }, + "getTimeSeries": { + "key": "state" + }, + "getAlarmStatus": { + "severityList": null, + "typeList": null + }, + "dataToValue": { + "type": "NONE", + "compareToValue": true, + "dataToValueFunction": "/* Should return boolean value */\nreturn data;" + } + }, + "defaultSetValueSettings": null, + "defaultWidgetActionSettings": null + }, + { + "id": "flowAnimationSpeed", + "name": "{i18n:scada.symbol.flow-animation-speed}", + "hint": "{i18n:scada.symbol.flow-animation-speed-hint}", + "group": null, + "type": "value", + "valueType": "DOUBLE", + "trueLabel": null, + "falseLabel": null, + "stateLabel": null, + "defaultGetValueSettings": { + "action": "DO_NOTHING", + "defaultValue": 1, + "executeRpc": { + "method": "getState", + "requestTimeout": 5000, + "requestPersistent": false, + "persistentPollingInterval": 1000 + }, + "getAttribute": { + "scope": null, + "key": "state" + }, + "getTimeSeries": { + "key": "state" + }, + "getAlarmStatus": { + "severityList": null, + "typeList": null + }, + "dataToValue": { + "type": "NONE", + "dataToValueFunction": "/* Should return boolean value */\nreturn data;" + } + }, + "defaultSetValueSettings": null, + "defaultWidgetActionSettings": null } ], "properties": [ @@ -93,16 +171,8 @@ "name": "{i18n:scada.symbol.main-line}", "type": "switch", "default": true, - "required": null, - "subLabel": null, - "divider": null, - "fieldSuffix": null, - "disableOnProperty": null, - "rowClass": "", - "fieldClass": "", - "min": null, - "max": null, - "step": null + "disabled": false, + "visible": true }, { "id": "mainLineSize", @@ -113,12 +183,11 @@ "subLabel": "Main", "divider": true, "fieldSuffix": "px", - "disableOnProperty": null, - "rowClass": "", - "fieldClass": "", "min": 0, "max": 99, - "step": 1 + "step": 1, + "disabled": false, + "visible": true }, { "id": "secondaryLineSize", @@ -127,48 +196,93 @@ "default": 2, "required": true, "subLabel": "Secondary", - "divider": null, "fieldSuffix": "px", - "disableOnProperty": null, - "rowClass": "", - "fieldClass": "", "min": 0, "max": 99, - "step": 1 + "step": 1, + "disabled": false, + "visible": true }, { "id": "lineColor", "name": "{i18n:scada.symbol.line-color}", "type": "color", "default": "#1A1A1A", - "required": null, - "subLabel": null, - "divider": null, - "fieldSuffix": null, - "disableOnProperty": null, - "rowClass": "", - "fieldClass": "", - "min": null, - "max": null, - "step": null + "disabled": false, + "visible": true }, { - "id": "arrowColor", - "name": "{i18n:scada.symbol.arrow-color}", + "id": "flowAnimationWidth", + "name": "{i18n:scada.symbol.flow}", + "group": "{i18n:scada.symbol.animation}", + "type": "number", + "default": 4, + "subLabel": "Width", + "fieldSuffix": "px", + "min": 1, + "step": 1, + "disabled": false, + "visible": true + }, + { + "id": "flowAnimationColor", + "name": "{i18n:scada.symbol.flow}", + "group": "{i18n:scada.symbol.animation}", "type": "color", - "default": "#1A1A1A", - "required": null, - "subLabel": null, - "divider": null, - "fieldSuffix": null, - "disableOnProperty": null, - "rowClass": "", - "fieldClass": "", - "min": null, - "max": null, - "step": null + "default": "#C8DFF7" + }, + { + "id": "flowStyleDash", + "name": "{i18n:scada.symbol.flow-style}", + "group": "{i18n:scada.symbol.animation}", + "type": "number", + "default": 10, + "required": true, + "subLabel": "{i18n:scada.symbol.dash}", + "divider": true, + "fieldSuffix": "px", + "min": 0, + "step": 1, + "disabled": false, + "visible": true + }, + { + "id": "flowStyleGap", + "name": "{i18n:scada.symbol.flow-style}", + "group": "{i18n:scada.symbol.animation}", + "type": "number", + "default": 10, + "subLabel": "{i18n:scada.symbol.gap}", + "fieldSuffix": "px", + "min": 1, + "step": 1, + "disabled": false, + "visible": true + }, + { + "id": "flowDashCap", + "name": "{i18n:scada.symbol.flow-dash-cap}", + "group": "{i18n:scada.symbol.animation}", + "type": "select", + "default": "butt", + "items": [ + { + "value": "butt", + "label": "{i18n:scada.symbol.dash-cap-butt}" + }, + { + "value": "round", + "label": "{i18n:scada.symbol.dash-cap-round}" + }, + { + "value": "square", + "label": "{i18n:scada.symbol.dash-cap-square}" + } + ], + "disabled": false, + "visible": true } ] }]]> - + \ No newline at end of file diff --git a/application/src/main/data/json/system/scada_symbols/right-tee-connector-hp.svg b/application/src/main/data/json/system/scada_symbols/right-tee-connector-hp.svg index 17d8bf4e0c..62aecb065d 100644 --- a/application/src/main/data/json/system/scada_symbols/right-tee-connector-hp.svg +++ b/application/src/main/data/json/system/scada_symbols/right-tee-connector-hp.svg @@ -3,6 +3,7 @@ "description": "Right tee connector", "widgetSizeX": 1, "widgetSizeY": 1, + "stateRenderFunction": "const {\n flowAnimationWidth: lineWidth,\n flowAnimationColor: lineColor,\n flowStyleDash: dashWidth,\n flowStyleGap: dashGap,\n flowDashCap: dashCap\n} = ctx.properties;\n\nconst topLine = \"M100 100L100 0\";\nconst rightLine = \"M103 100H200\";\nconst bottomLine = \"M 100,200 V 100\";\n\nprepareFlowAnimation('top', topLine);\nprepareFlowAnimation('right', rightLine);\nprepareFlowAnimation('bottom', bottomLine);\n\nfunction prepareFlowAnimation(prefix, line) {\n const flowAnimation = ctx.values[prefix + 'Flow'];\n const flowDirection = ctx.values[prefix + 'FlowDirection'];\n const flowAnimationSpeed = ctx.values[prefix + 'FlowAnimationSpeed'];\n\n const animation = ctx.tags[prefix + 'Line'][0];\n const offset = Date.now() % 1000;\n const duration = 1 / flowAnimationSpeed;\n \n const prevFlowAnimation = animation.remember('flowAnimation');\n const prevFlowDirection = animation.remember('flowDirection');\n const prevFlowDuration = animation.remember('flowDuration');\n \n if (flowAnimation && flowAnimation !== prevFlowAnimation) {\n animation.remember('flowAnimation', flowAnimation);\n animation.remember('flowDuration', duration);\n animation.remember('flowDirection', flowDirection);\n animateFlow(animation, offset, flowDirection, duration, line);\n } else if (flowAnimation && flowDirection !== prevFlowDirection) {\n animation.remember('flowDirection', flowDirection);\n animateFlow(animation, offset, flowDirection, duration, line);\n } else if (flowAnimation && duration !== prevFlowDuration) {\n animation.remember('flowDuration', duration);\n animation.findOne('animate').attr('dur', `${duration}s`) ;\n } else if (!flowAnimation && prevFlowAnimation) {\n animation.remember('flowAnimation', null);\n animation.clear();\n }\n}\n\nfunction animateFlow(group, offset, flowDirection, duration, line) {\n group.clear();\n const dashArray = `${dashWidth}${dashGap ? ` ${dashGap}` : ''}`;\n const value = flowDirection ? `${dashWidth + (dashGap || dashWidth)}` : `-${dashWidth + (dashGap || dashWidth)}`;\n\n group.add(``);\n}", "tags": [ { "tag": "line", @@ -15,23 +16,364 @@ "actions": null } ], - "behavior": [], + "behavior": [ + { + "id": "topFlow", + "name": "{i18n:scada.symbol.flow-animation}", + "hint": "{i18n:scada.symbol.flow-animation-hint}", + "group": "{i18n:scada.symbol.top-connector}", + "type": "value", + "valueType": "BOOLEAN", + "trueLabel": "{i18n:scada.symbol.present}", + "falseLabel": "{i18n:scada.symbol.absent}", + "stateLabel": "{i18n:scada.symbol.fluid-present}", + "defaultGetValueSettings": { + "action": "DO_NOTHING", + "defaultValue": false, + "executeRpc": { + "method": "getState", + "requestTimeout": 5000, + "requestPersistent": false, + "persistentPollingInterval": 1000 + }, + "getAttribute": { + "key": "state", + "scope": null + }, + "getTimeSeries": { + "key": "state" + }, + "getAlarmStatus": { + "severityList": null, + "typeList": null + }, + "dataToValue": { + "type": "NONE", + "compareToValue": true, + "dataToValueFunction": "/* Should return boolean value */\nreturn data;" + } + }, + "defaultSetValueSettings": null, + "defaultWidgetActionSettings": null + }, + { + "id": "topFlowDirection", + "name": "{i18n:scada.symbol.animation-direction}", + "hint": "{i18n:scada.symbol.animation-direction-hint}", + "group": "{i18n:scada.symbol.top-connector}", + "type": "value", + "valueType": "BOOLEAN", + "trueLabel": "{i18n:scada.symbol.forward}", + "falseLabel": "{i18n:scada.symbol.reverse}", + "stateLabel": "{i18n:scada.symbol.forward}", + "defaultGetValueSettings": { + "action": "DO_NOTHING", + "defaultValue": true, + "executeRpc": { + "method": "getState", + "requestTimeout": 5000, + "requestPersistent": false, + "persistentPollingInterval": 1000 + }, + "getAttribute": { + "scope": null, + "key": "state" + }, + "getTimeSeries": { + "key": "state" + }, + "getAlarmStatus": { + "severityList": null, + "typeList": null + }, + "dataToValue": { + "type": "NONE", + "dataToValueFunction": "/* Should return boolean value */\nreturn data;", + "compareToValue": true + } + }, + "defaultSetValueSettings": null, + "defaultWidgetActionSettings": null + }, + { + "id": "topFlowAnimationSpeed", + "name": "{i18n:scada.symbol.flow-animation-speed}", + "hint": "{i18n:scada.symbol.flow-animation-speed-hint}", + "group": "{i18n:scada.symbol.top-connector}", + "type": "value", + "valueType": "DOUBLE", + "trueLabel": null, + "falseLabel": null, + "stateLabel": null, + "defaultGetValueSettings": { + "action": "DO_NOTHING", + "defaultValue": 1, + "executeRpc": { + "method": "getState", + "requestTimeout": 5000, + "requestPersistent": false, + "persistentPollingInterval": 1000 + }, + "getAttribute": { + "scope": null, + "key": "state" + }, + "getTimeSeries": { + "key": "state" + }, + "getAlarmStatus": { + "severityList": null, + "typeList": null + }, + "dataToValue": { + "type": "NONE", + "dataToValueFunction": "/* Should return boolean value */\nreturn data;" + } + }, + "defaultSetValueSettings": null, + "defaultWidgetActionSettings": null + }, + { + "id": "rightFlow", + "name": "{i18n:scada.symbol.flow-animation}", + "hint": "{i18n:scada.symbol.flow-animation-hint}", + "group": "{i18n:scada.symbol.right-connector}", + "type": "value", + "valueType": "BOOLEAN", + "trueLabel": "{i18n:scada.symbol.present}", + "falseLabel": "{i18n:scada.symbol.absent}", + "stateLabel": "{i18n:scada.symbol.flow-present}", + "defaultGetValueSettings": { + "action": "DO_NOTHING", + "defaultValue": false, + "executeRpc": { + "method": "getState", + "requestTimeout": 5000, + "requestPersistent": false, + "persistentPollingInterval": 1000 + }, + "getAttribute": { + "key": "state", + "scope": null + }, + "getTimeSeries": { + "key": "state" + }, + "getAlarmStatus": { + "severityList": null, + "typeList": null + }, + "dataToValue": { + "type": "NONE", + "compareToValue": true, + "dataToValueFunction": "/* Should return boolean value */\nreturn data;" + } + }, + "defaultSetValueSettings": null, + "defaultWidgetActionSettings": null + }, + { + "id": "rightFlowDirection", + "name": "{i18n:scada.symbol.flow-direction}", + "hint": "{i18n:scada.symbol.flow-direction-hint}", + "group": "{i18n:scada.symbol.right-connector}", + "type": "value", + "valueType": "BOOLEAN", + "trueLabel": "{i18n:scada.symbol.forward}", + "falseLabel": "{i18n:scada.symbol.reverse}", + "stateLabel": "{i18n:scada.symbol.forward}", + "defaultGetValueSettings": { + "action": "DO_NOTHING", + "defaultValue": true, + "executeRpc": { + "method": "getState", + "requestTimeout": 5000, + "requestPersistent": false, + "persistentPollingInterval": 1000 + }, + "getAttribute": { + "scope": null, + "key": "state" + }, + "getTimeSeries": { + "key": "state" + }, + "getAlarmStatus": { + "severityList": null, + "typeList": null + }, + "dataToValue": { + "type": "NONE", + "dataToValueFunction": "/* Should return boolean value */\nreturn data;", + "compareToValue": true + } + }, + "defaultSetValueSettings": null, + "defaultWidgetActionSettings": null + }, + { + "id": "rightFlowAnimationSpeed", + "name": "{i18n:scada.symbol.flow-animation-speed}", + "hint": "{i18n:scada.symbol.flow-animation-speed-hint}", + "group": "{i18n:scada.symbol.right-connector}", + "type": "value", + "valueType": "DOUBLE", + "trueLabel": null, + "falseLabel": null, + "stateLabel": null, + "defaultGetValueSettings": { + "action": "DO_NOTHING", + "defaultValue": 1, + "executeRpc": { + "method": "getState", + "requestTimeout": 5000, + "requestPersistent": false, + "persistentPollingInterval": 1000 + }, + "getAttribute": { + "scope": null, + "key": "state" + }, + "getTimeSeries": { + "key": "state" + }, + "getAlarmStatus": { + "severityList": null, + "typeList": null + }, + "dataToValue": { + "type": "NONE", + "dataToValueFunction": "/* Should return boolean value */\nreturn data;" + } + }, + "defaultSetValueSettings": null, + "defaultWidgetActionSettings": null + }, + { + "id": "bottomFlow", + "name": "{i18n:scada.symbol.flow-animation}", + "hint": "{i18n:scada.symbol.flow-animation-hint}", + "group": "{i18n:scada.symbol.bottom-connector}", + "type": "value", + "valueType": "BOOLEAN", + "trueLabel": "{i18n:scada.symbol.present}", + "falseLabel": "{i18n:scada.symbol.absent}", + "stateLabel": "{i18n:scada.symbol.flow-present}", + "defaultGetValueSettings": { + "action": "DO_NOTHING", + "defaultValue": false, + "executeRpc": { + "method": "getState", + "requestTimeout": 5000, + "requestPersistent": false, + "persistentPollingInterval": 1000 + }, + "getAttribute": { + "key": "state", + "scope": null + }, + "getTimeSeries": { + "key": "state" + }, + "getAlarmStatus": { + "severityList": null, + "typeList": null + }, + "dataToValue": { + "type": "NONE", + "compareToValue": true, + "dataToValueFunction": "/* Should return boolean value */\nreturn data;" + } + }, + "defaultSetValueSettings": null, + "defaultWidgetActionSettings": null + }, + { + "id": "bottomFlowDirection", + "name": "{i18n:scada.symbol.animation-direction}", + "hint": "{i18n:scada.symbol.animation-direction-hint}", + "group": "{i18n:scada.symbol.bottom-connector}", + "type": "value", + "valueType": "BOOLEAN", + "trueLabel": "{i18n:scada.symbol.forward}", + "falseLabel": "{i18n:scada.symbol.reverse}", + "stateLabel": "{i18n:scada.symbol.forward}", + "defaultGetValueSettings": { + "action": "DO_NOTHING", + "defaultValue": true, + "executeRpc": { + "method": "getState", + "requestTimeout": 5000, + "requestPersistent": false, + "persistentPollingInterval": 1000 + }, + "getAttribute": { + "scope": null, + "key": "state" + }, + "getTimeSeries": { + "key": "state" + }, + "getAlarmStatus": { + "severityList": null, + "typeList": null + }, + "dataToValue": { + "type": "NONE", + "dataToValueFunction": "/* Should return boolean value */\nreturn data;", + "compareToValue": true + } + }, + "defaultSetValueSettings": null, + "defaultWidgetActionSettings": null + }, + { + "id": "bottomFlowAnimationSpeed", + "name": "{i18n:scada.symbol.flow-animation-speed}", + "hint": "{i18n:scada.symbol.flow-animation-speed-hint}", + "group": "{i18n:scada.symbol.bottom-connector}", + "type": "value", + "valueType": "DOUBLE", + "trueLabel": null, + "falseLabel": null, + "stateLabel": null, + "defaultGetValueSettings": { + "action": "DO_NOTHING", + "defaultValue": 1, + "executeRpc": { + "method": "getState", + "requestTimeout": 5000, + "requestPersistent": false, + "persistentPollingInterval": 1000 + }, + "getAttribute": { + "scope": null, + "key": "state" + }, + "getTimeSeries": { + "key": "state" + }, + "getAlarmStatus": { + "severityList": null, + "typeList": null + }, + "dataToValue": { + "type": "NONE", + "dataToValueFunction": "/* Should return boolean value */\nreturn data;" + } + }, + "defaultSetValueSettings": null, + "defaultWidgetActionSettings": null + } + ], "properties": [ { "id": "mainLine", "name": "{i18n:scada.symbol.main-line}", "type": "switch", "default": true, - "required": null, - "subLabel": null, - "divider": null, - "fieldSuffix": null, - "disableOnProperty": null, - "rowClass": "", - "fieldClass": "", - "min": null, - "max": null, - "step": null + "disabled": false, + "visible": true }, { "id": "mainLineSize", @@ -42,12 +384,11 @@ "subLabel": "Main", "divider": true, "fieldSuffix": "px", - "disableOnProperty": null, - "rowClass": "", - "fieldClass": "", "min": 0, "max": 99, - "step": 1 + "step": 1, + "disabled": false, + "visible": true }, { "id": "secondaryLineSize", @@ -56,32 +397,95 @@ "default": 2, "required": true, "subLabel": "Secondary", - "divider": null, "fieldSuffix": "px", - "disableOnProperty": null, - "rowClass": "", - "fieldClass": "", "min": 0, "max": 99, - "step": 1 + "step": 1, + "disabled": false, + "visible": true }, { "id": "lineColor", "name": "{i18n:scada.symbol.line-color}", "type": "color", "default": "#1A1A1A", - "required": null, - "subLabel": null, - "divider": null, - "fieldSuffix": null, - "disableOnProperty": null, - "rowClass": "", - "fieldClass": "", - "min": null, - "max": null, - "step": null + "disabled": false, + "visible": true + }, + { + "id": "flowAnimationWidth", + "name": "{i18n:scada.symbol.flow}", + "group": "{i18n:scada.symbol.animation}", + "type": "number", + "default": 4, + "subLabel": "Width", + "fieldSuffix": "px", + "min": 1, + "step": 1, + "disabled": false, + "visible": true + }, + { + "id": "flowAnimationColor", + "name": "{i18n:scada.symbol.flow}", + "group": "{i18n:scada.symbol.animation}", + "type": "color", + "default": "#C8DFF7", + "disabled": false, + "visible": true + }, + { + "id": "flowStyleDash", + "name": "{i18n:scada.symbol.flow-style}", + "group": "{i18n:scada.symbol.animation}", + "type": "number", + "default": 10, + "required": true, + "subLabel": "{i18n:scada.symbol.dash}", + "divider": true, + "fieldSuffix": "px", + "min": 0, + "step": 1, + "disabled": false, + "visible": true + }, + { + "id": "flowStyleGap", + "name": "{i18n:scada.symbol.flow-style}", + "group": "{i18n:scada.symbol.animation}", + "type": "number", + "default": 10, + "subLabel": "{i18n:scada.symbol.gap}", + "fieldSuffix": "px", + "min": 1, + "step": 1, + "disabled": false, + "visible": true + }, + { + "id": "flowDashCap", + "name": "{i18n:scada.symbol.flow-dash-cap}", + "group": "{i18n:scada.symbol.animation}", + "type": "select", + "default": "butt", + "items": [ + { + "value": "butt", + "label": "{i18n:scada.symbol.dash-cap-butt}" + }, + { + "value": "round", + "label": "{i18n:scada.symbol.dash-cap-round}" + }, + { + "value": "square", + "label": "{i18n:scada.symbol.dash-cap-square}" + } + ], + "disabled": false, + "visible": true } ] }]]> - + \ No newline at end of file diff --git a/application/src/main/data/json/system/scada_symbols/top-right-elbow-connector-hp.svg b/application/src/main/data/json/system/scada_symbols/top-right-elbow-connector-hp.svg index f2d579c551..8ec4e3cc65 100644 --- a/application/src/main/data/json/system/scada_symbols/top-right-elbow-connector-hp.svg +++ b/application/src/main/data/json/system/scada_symbols/top-right-elbow-connector-hp.svg @@ -3,6 +3,7 @@ "description": "Top right elbow connector", "widgetSizeX": 1, "widgetSizeY": 1, + "stateRenderFunction": "const {\n flowAnimation,\n animationDirection: flowDirection,\n flowAnimationSpeed\n} = ctx.values;\nconst {\n flowAnimationWidth: lineWidth,\n flowAnimationColor: lineColor,\n flowStyleDash: dashWidth,\n flowStyleGap: dashGap,\n flowDashCap: dashCap\n} = ctx.properties;\nconst line = ctx.tags.line[0].attr('d');\nconst animation = ctx.tags.animationGroup[0];\nconst offset = Date.now() % 1000;\nconst duration = 1 / flowAnimationSpeed;\n\nconst prevFlowAnimation = animation.remember('flowAnimation');\nconst prevFlowDirection = animation.remember('flowDirection');\nconst prevFlowDuration = animation.remember('flowDuration');\n\nif (flowAnimation && flowAnimation !== prevFlowAnimation) {\n animation.remember('flowAnimation', flowAnimation);\n animation.remember('flowDuration', duration);\n animation.remember('flowDirection', flowDirection);\n animateFlow(offset, flowDirection);\n} else if (flowAnimation && flowDirection !== prevFlowDirection) {\n animation.remember('flowDirection', flowDirection);\n animateFlow(offset, flowDirection);\n} else if (flowAnimation && duration !== prevFlowDuration) {\n animation.remember('flowDuration', duration);\n animation.findOne('animate').attr('dur', `${duration}s`) ;\n} else if (!flowAnimation && prevFlowAnimation) {\n animation.remember('flowAnimation', null);\n animation.clear();\n}\n\nfunction animateFlow(offset, flowDirection) {\n animation.clear();\n const dashArray = `${dashWidth}${dashGap ? ` ${dashGap}` : ''}`;\n const value = flowDirection ? `-${dashWidth + (dashGap || dashWidth)}` : `${dashWidth + (dashGap || dashWidth)}`;\n\n animation.add(``);\n}\n", "tags": [ { "tag": "line", @@ -10,23 +11,132 @@ "actions": null } ], - "behavior": [], + "behavior": [ + { + "id": "flowAnimation", + "name": "{i18n:scada.symbol.flow-animation}", + "hint": "{i18n:scada.symbol.flow-animation-hint}", + "group": null, + "type": "value", + "valueType": "BOOLEAN", + "trueLabel": "{i18n:scada.symbol.present}", + "falseLabel": "{i18n:scada.symbol.absent}", + "stateLabel": "{i18n:scada.symbol.flow-present}", + "defaultGetValueSettings": { + "action": "DO_NOTHING", + "defaultValue": false, + "executeRpc": { + "method": "getState", + "requestTimeout": 5000, + "requestPersistent": false, + "persistentPollingInterval": 1000 + }, + "getAttribute": { + "key": "state", + "scope": null + }, + "getTimeSeries": { + "key": "state" + }, + "getAlarmStatus": { + "severityList": null, + "typeList": null + }, + "dataToValue": { + "type": "NONE", + "compareToValue": true, + "dataToValueFunction": "/* Should return boolean value */\nreturn data;" + } + }, + "defaultSetValueSettings": null, + "defaultWidgetActionSettings": null + }, + { + "id": "animationDirection", + "name": "{i18n:scada.symbol.animation-direction}", + "hint": "{i18n:scada.symbol.animation-direction-hint}", + "group": null, + "type": "value", + "valueType": "BOOLEAN", + "trueLabel": "{i18n:scada.symbol.forward}", + "falseLabel": "{i18n:scada.symbol.reverse}", + "stateLabel": "{i18n:scada.symbol.forward}", + "defaultGetValueSettings": { + "action": "DO_NOTHING", + "defaultValue": true, + "executeRpc": { + "method": "getState", + "requestTimeout": 5000, + "requestPersistent": false, + "persistentPollingInterval": 1000 + }, + "getAttribute": { + "scope": null, + "key": "state" + }, + "getTimeSeries": { + "key": "state" + }, + "getAlarmStatus": { + "severityList": null, + "typeList": null + }, + "dataToValue": { + "type": "NONE", + "dataToValueFunction": "/* Should return boolean value */\nreturn data;", + "compareToValue": true + } + }, + "defaultSetValueSettings": null, + "defaultWidgetActionSettings": null + }, + { + "id": "flowAnimationSpeed", + "name": "{i18n:scada.symbol.flow-animation-speed}", + "hint": "{i18n:scada.symbol.flow-animation-speed-hint}", + "group": null, + "type": "value", + "valueType": "DOUBLE", + "trueLabel": null, + "falseLabel": null, + "stateLabel": null, + "defaultGetValueSettings": { + "action": "DO_NOTHING", + "defaultValue": 1, + "executeRpc": { + "method": "getState", + "requestTimeout": 5000, + "requestPersistent": false, + "persistentPollingInterval": 1000 + }, + "getAttribute": { + "scope": null, + "key": "state" + }, + "getTimeSeries": { + "key": "state" + }, + "getAlarmStatus": { + "severityList": null, + "typeList": null + }, + "dataToValue": { + "type": "NONE", + "dataToValueFunction": "/* Should return boolean value */\nreturn data;" + } + }, + "defaultSetValueSettings": null, + "defaultWidgetActionSettings": null + } + ], "properties": [ { "id": "mainLine", "name": "{i18n:scada.symbol.main-line}", "type": "switch", "default": true, - "required": null, - "subLabel": null, - "divider": null, - "fieldSuffix": null, - "disableOnProperty": null, - "rowClass": "", - "fieldClass": "", - "min": null, - "max": null, - "step": null + "disabled": false, + "visible": true }, { "id": "mainLineSize", @@ -37,12 +147,11 @@ "subLabel": "Main", "divider": true, "fieldSuffix": "px", - "disableOnProperty": null, - "rowClass": "", - "fieldClass": "", "min": 0, "max": 99, - "step": 1 + "step": 1, + "disabled": false, + "visible": true }, { "id": "secondaryLineSize", @@ -51,32 +160,95 @@ "default": 2, "required": true, "subLabel": "Secondary", - "divider": null, "fieldSuffix": "px", - "disableOnProperty": null, - "rowClass": "", - "fieldClass": "", "min": 0, "max": 99, - "step": 1 + "step": 1, + "disabled": false, + "visible": true }, { "id": "lineColor", "name": "{i18n:scada.symbol.line-color}", "type": "color", "default": "#1A1A1A", - "required": null, - "subLabel": null, - "divider": null, - "fieldSuffix": null, - "disableOnProperty": null, - "rowClass": "", - "fieldClass": "", - "min": null, - "max": null, - "step": null + "disabled": false, + "visible": true + }, + { + "id": "flowAnimationWidth", + "name": "{i18n:scada.symbol.flow}", + "group": "{i18n:scada.symbol.animation}", + "type": "number", + "default": 4, + "subLabel": "Width", + "fieldSuffix": "px", + "min": 1, + "step": 1, + "disabled": false, + "visible": true + }, + { + "id": "flowAnimationColor", + "name": "{i18n:scada.symbol.flow}", + "group": "{i18n:scada.symbol.animation}", + "type": "color", + "default": "#C8DFF7", + "disabled": false, + "visible": true + }, + { + "id": "flowStyleDash", + "name": "{i18n:scada.symbol.flow-style}", + "group": "{i18n:scada.symbol.animation}", + "type": "number", + "default": 10, + "required": true, + "subLabel": "{i18n:scada.symbol.dash}", + "divider": true, + "fieldSuffix": "px", + "min": 0, + "step": 1, + "disabled": false, + "visible": true + }, + { + "id": "flowStyleGap", + "name": "{i18n:scada.symbol.flow-style}", + "group": "{i18n:scada.symbol.animation}", + "type": "number", + "default": 10, + "subLabel": "{i18n:scada.symbol.gap}", + "fieldSuffix": "px", + "min": 1, + "step": 1, + "disabled": false, + "visible": true + }, + { + "id": "flowDashCap", + "name": "{i18n:scada.symbol.flow-dash-cap}", + "group": "{i18n:scada.symbol.animation}", + "type": "select", + "default": "butt", + "items": [ + { + "value": "butt", + "label": "{i18n:scada.symbol.dash-cap-butt}" + }, + { + "value": "round", + "label": "{i18n:scada.symbol.dash-cap-round}" + }, + { + "value": "square", + "label": "{i18n:scada.symbol.dash-cap-square}" + } + ], + "disabled": false, + "visible": true } ] }]]> - + \ No newline at end of file diff --git a/application/src/main/data/json/system/scada_symbols/top-tee-connector-hp.svg b/application/src/main/data/json/system/scada_symbols/top-tee-connector-hp.svg index 004096aaa7..e4561a8347 100644 --- a/application/src/main/data/json/system/scada_symbols/top-tee-connector-hp.svg +++ b/application/src/main/data/json/system/scada_symbols/top-tee-connector-hp.svg @@ -3,6 +3,7 @@ "description": "Top tee connector", "widgetSizeX": 1, "widgetSizeY": 1, + "stateRenderFunction": "const {\n flowAnimationWidth: lineWidth,\n flowAnimationColor: lineColor,\n flowStyleDash: dashWidth,\n flowStyleGap: dashGap,\n flowDashCap: dashCap\n} = ctx.properties;\n\nconst leftLine = \"M0 100H100\";\nconst topLine = \"M100 97L100 0\";\nconst rightLine = \"M100 100H200\";\n\nprepareFlowAnimation('left', leftLine);\nprepareFlowAnimation('top', topLine);\nprepareFlowAnimation('right', rightLine);\n\nfunction prepareFlowAnimation(prefix, line) {\n const flowAnimation = ctx.values[prefix + 'Flow'];\n const flowDirection = ctx.values[prefix + 'FlowDirection'];\n const flowAnimationSpeed = ctx.values[prefix + 'FlowAnimationSpeed'];\n\n const animation = ctx.tags[prefix + 'Line'][0];\n const offset = Date.now() % 1000;\n const duration = 1 / flowAnimationSpeed;\n \n const prevFlowAnimation = animation.remember('flowAnimation');\n const prevFlowDirection = animation.remember('flowDirection');\n const prevFlowDuration = animation.remember('flowDuration');\n \n if (flowAnimation && flowAnimation !== prevFlowAnimation) {\n animation.remember('flowAnimation', flowAnimation);\n animation.remember('flowDuration', duration);\n animation.remember('flowDirection', flowDirection);\n animateFlow(animation, offset, flowDirection, duration, line);\n } else if (flowAnimation && flowDirection !== prevFlowDirection) {\n animation.remember('flowDirection', flowDirection);\n animateFlow(animation, offset, flowDirection, duration, line);\n } else if (flowAnimation && duration !== prevFlowDuration) {\n animation.remember('flowDuration', duration);\n animation.findOne('animate').attr('dur', `${duration}s`) ;\n } else if (!flowAnimation && prevFlowAnimation) {\n animation.remember('flowAnimation', null);\n animation.clear();\n }\n}\n\nfunction animateFlow(group, offset, flowDirection, duration, line) {\n group.clear();\n const dashArray = `${dashWidth}${dashGap ? ` ${dashGap}` : ''}`;\n const value = flowDirection ? `${dashWidth + (dashGap || dashWidth)}` : `-${dashWidth + (dashGap || dashWidth)}`;\n\n group.add(``);\n}", "tags": [ { "tag": "line", @@ -15,23 +16,364 @@ "actions": null } ], - "behavior": [], + "behavior": [ + { + "id": "leftFlow", + "name": "{i18n:scada.symbol.flow-animation}", + "hint": "{i18n:scada.symbol.flow-animation-hint}", + "group": "{i18n:scada.symbol.left-connector}", + "type": "value", + "valueType": "BOOLEAN", + "trueLabel": "{i18n:scada.symbol.present}", + "falseLabel": "{i18n:scada.symbol.absent}", + "stateLabel": "{i18n:scada.symbol.flow-present}", + "defaultGetValueSettings": { + "action": "DO_NOTHING", + "defaultValue": false, + "executeRpc": { + "method": "getState", + "requestTimeout": 5000, + "requestPersistent": false, + "persistentPollingInterval": 1000 + }, + "getAttribute": { + "key": "state", + "scope": null + }, + "getTimeSeries": { + "key": "state" + }, + "getAlarmStatus": { + "severityList": null, + "typeList": null + }, + "dataToValue": { + "type": "NONE", + "compareToValue": true, + "dataToValueFunction": "/* Should return boolean value */\nreturn data;" + } + }, + "defaultSetValueSettings": null, + "defaultWidgetActionSettings": null + }, + { + "id": "leftFlowDirection", + "name": "{i18n:scada.symbol.animation-direction}", + "hint": "{i18n:scada.symbol.animation-direction-hint}", + "group": "{i18n:scada.symbol.left-connector}", + "type": "value", + "valueType": "BOOLEAN", + "trueLabel": "{i18n:scada.symbol.forward}", + "falseLabel": "{i18n:scada.symbol.reverse}", + "stateLabel": "{i18n:scada.symbol.forward}", + "defaultGetValueSettings": { + "action": "DO_NOTHING", + "defaultValue": true, + "executeRpc": { + "method": "getState", + "requestTimeout": 5000, + "requestPersistent": false, + "persistentPollingInterval": 1000 + }, + "getAttribute": { + "scope": null, + "key": "state" + }, + "getTimeSeries": { + "key": "state" + }, + "getAlarmStatus": { + "severityList": null, + "typeList": null + }, + "dataToValue": { + "type": "NONE", + "dataToValueFunction": "/* Should return boolean value */\nreturn data;", + "compareToValue": true + } + }, + "defaultSetValueSettings": null, + "defaultWidgetActionSettings": null + }, + { + "id": "leftFlowAnimationSpeed", + "name": "{i18n:scada.symbol.flow-animation-speed}", + "hint": "{i18n:scada.symbol.flow-animation-speed-hint}", + "group": "{i18n:scada.symbol.left-connector}", + "type": "value", + "valueType": "DOUBLE", + "trueLabel": null, + "falseLabel": null, + "stateLabel": null, + "defaultGetValueSettings": { + "action": "DO_NOTHING", + "defaultValue": 1, + "executeRpc": { + "method": "getState", + "requestTimeout": 5000, + "requestPersistent": false, + "persistentPollingInterval": 1000 + }, + "getAttribute": { + "scope": null, + "key": "state" + }, + "getTimeSeries": { + "key": "state" + }, + "getAlarmStatus": { + "severityList": null, + "typeList": null + }, + "dataToValue": { + "type": "NONE", + "dataToValueFunction": "/* Should return boolean value */\nreturn data;" + } + }, + "defaultSetValueSettings": null, + "defaultWidgetActionSettings": null + }, + { + "id": "topFlow", + "name": "{i18n:scada.symbol.flow-animation}", + "hint": "{i18n:scada.symbol.flow-animation-hint}", + "group": "{i18n:scada.symbol.top-connector}", + "type": "value", + "valueType": "BOOLEAN", + "trueLabel": "{i18n:scada.symbol.present}", + "falseLabel": "{i18n:scada.symbol.absent}", + "stateLabel": "{i18n:scada.symbol.fluid-present}", + "defaultGetValueSettings": { + "action": "DO_NOTHING", + "defaultValue": false, + "executeRpc": { + "method": "getState", + "requestTimeout": 5000, + "requestPersistent": false, + "persistentPollingInterval": 1000 + }, + "getAttribute": { + "key": "state", + "scope": null + }, + "getTimeSeries": { + "key": "state" + }, + "getAlarmStatus": { + "severityList": null, + "typeList": null + }, + "dataToValue": { + "type": "NONE", + "compareToValue": true, + "dataToValueFunction": "/* Should return boolean value */\nreturn data;" + } + }, + "defaultSetValueSettings": null, + "defaultWidgetActionSettings": null + }, + { + "id": "topFlowDirection", + "name": "{i18n:scada.symbol.animation-direction}", + "hint": "{i18n:scada.symbol.animation-direction-hint}", + "group": "{i18n:scada.symbol.top-connector}", + "type": "value", + "valueType": "BOOLEAN", + "trueLabel": "{i18n:scada.symbol.forward}", + "falseLabel": "{i18n:scada.symbol.reverse}", + "stateLabel": "{i18n:scada.symbol.forward}", + "defaultGetValueSettings": { + "action": "DO_NOTHING", + "defaultValue": true, + "executeRpc": { + "method": "getState", + "requestTimeout": 5000, + "requestPersistent": false, + "persistentPollingInterval": 1000 + }, + "getAttribute": { + "scope": null, + "key": "state" + }, + "getTimeSeries": { + "key": "state" + }, + "getAlarmStatus": { + "severityList": null, + "typeList": null + }, + "dataToValue": { + "type": "NONE", + "dataToValueFunction": "/* Should return boolean value */\nreturn data;", + "compareToValue": true + } + }, + "defaultSetValueSettings": null, + "defaultWidgetActionSettings": null + }, + { + "id": "topFlowAnimationSpeed", + "name": "{i18n:scada.symbol.flow-animation-speed}", + "hint": "{i18n:scada.symbol.flow-animation-speed-hint}", + "group": "{i18n:scada.symbol.top-connector}", + "type": "value", + "valueType": "DOUBLE", + "trueLabel": null, + "falseLabel": null, + "stateLabel": null, + "defaultGetValueSettings": { + "action": "DO_NOTHING", + "defaultValue": 1, + "executeRpc": { + "method": "getState", + "requestTimeout": 5000, + "requestPersistent": false, + "persistentPollingInterval": 1000 + }, + "getAttribute": { + "scope": null, + "key": "state" + }, + "getTimeSeries": { + "key": "state" + }, + "getAlarmStatus": { + "severityList": null, + "typeList": null + }, + "dataToValue": { + "type": "NONE", + "dataToValueFunction": "/* Should return boolean value */\nreturn data;" + } + }, + "defaultSetValueSettings": null, + "defaultWidgetActionSettings": null + }, + { + "id": "rightFlow", + "name": "{i18n:scada.symbol.flow-animation}", + "hint": "{i18n:scada.symbol.flow-animation-hint}", + "group": "{i18n:scada.symbol.right-connector}", + "type": "value", + "valueType": "BOOLEAN", + "trueLabel": "{i18n:scada.symbol.present}", + "falseLabel": "{i18n:scada.symbol.absent}", + "stateLabel": "{i18n:scada.symbol.flow-present}", + "defaultGetValueSettings": { + "action": "DO_NOTHING", + "defaultValue": false, + "executeRpc": { + "method": "getState", + "requestTimeout": 5000, + "requestPersistent": false, + "persistentPollingInterval": 1000 + }, + "getAttribute": { + "key": "state", + "scope": null + }, + "getTimeSeries": { + "key": "state" + }, + "getAlarmStatus": { + "severityList": null, + "typeList": null + }, + "dataToValue": { + "type": "NONE", + "compareToValue": true, + "dataToValueFunction": "/* Should return boolean value */\nreturn data;" + } + }, + "defaultSetValueSettings": null, + "defaultWidgetActionSettings": null + }, + { + "id": "rightFlowDirection", + "name": "{i18n:scada.symbol.flow-direction}", + "hint": "{i18n:scada.symbol.flow-direction-hint}", + "group": "{i18n:scada.symbol.right-connector}", + "type": "value", + "valueType": "BOOLEAN", + "trueLabel": "{i18n:scada.symbol.forward}", + "falseLabel": "{i18n:scada.symbol.reverse}", + "stateLabel": "{i18n:scada.symbol.forward}", + "defaultGetValueSettings": { + "action": "DO_NOTHING", + "defaultValue": true, + "executeRpc": { + "method": "getState", + "requestTimeout": 5000, + "requestPersistent": false, + "persistentPollingInterval": 1000 + }, + "getAttribute": { + "scope": null, + "key": "state" + }, + "getTimeSeries": { + "key": "state" + }, + "getAlarmStatus": { + "severityList": null, + "typeList": null + }, + "dataToValue": { + "type": "NONE", + "dataToValueFunction": "/* Should return boolean value */\nreturn data;", + "compareToValue": true + } + }, + "defaultSetValueSettings": null, + "defaultWidgetActionSettings": null + }, + { + "id": "rightFlowAnimationSpeed", + "name": "{i18n:scada.symbol.flow-animation-speed}", + "hint": "{i18n:scada.symbol.flow-animation-speed-hint}", + "group": "{i18n:scada.symbol.right-connector}", + "type": "value", + "valueType": "DOUBLE", + "trueLabel": null, + "falseLabel": null, + "stateLabel": null, + "defaultGetValueSettings": { + "action": "DO_NOTHING", + "defaultValue": 1, + "executeRpc": { + "method": "getState", + "requestTimeout": 5000, + "requestPersistent": false, + "persistentPollingInterval": 1000 + }, + "getAttribute": { + "scope": null, + "key": "state" + }, + "getTimeSeries": { + "key": "state" + }, + "getAlarmStatus": { + "severityList": null, + "typeList": null + }, + "dataToValue": { + "type": "NONE", + "dataToValueFunction": "/* Should return boolean value */\nreturn data;" + } + }, + "defaultSetValueSettings": null, + "defaultWidgetActionSettings": null + } + ], "properties": [ { "id": "mainLine", "name": "{i18n:scada.symbol.main-line}", "type": "switch", "default": true, - "required": null, - "subLabel": null, - "divider": null, - "fieldSuffix": null, - "disableOnProperty": null, - "rowClass": "", - "fieldClass": "", - "min": null, - "max": null, - "step": null + "disabled": false, + "visible": true }, { "id": "mainLineSize", @@ -42,12 +384,11 @@ "subLabel": "Main", "divider": true, "fieldSuffix": "px", - "disableOnProperty": null, - "rowClass": "", - "fieldClass": "", "min": 0, "max": 99, - "step": 1 + "step": 1, + "disabled": false, + "visible": true }, { "id": "secondaryLineSize", @@ -56,32 +397,95 @@ "default": 2, "required": true, "subLabel": "Secondary", - "divider": null, "fieldSuffix": "px", - "disableOnProperty": null, - "rowClass": "", - "fieldClass": "", "min": 0, "max": 99, - "step": 1 + "step": 1, + "disabled": false, + "visible": true }, { "id": "lineColor", "name": "{i18n:scada.symbol.line-color}", "type": "color", "default": "#1A1A1A", - "required": null, - "subLabel": null, - "divider": null, - "fieldSuffix": null, - "disableOnProperty": null, - "rowClass": "", - "fieldClass": "", - "min": null, - "max": null, - "step": null + "disabled": false, + "visible": true + }, + { + "id": "flowAnimationWidth", + "name": "{i18n:scada.symbol.flow}", + "group": "{i18n:scada.symbol.animation}", + "type": "number", + "default": 4, + "subLabel": "Width", + "fieldSuffix": "px", + "min": 1, + "step": 1, + "disabled": false, + "visible": true + }, + { + "id": "flowAnimationColor", + "name": "{i18n:scada.symbol.flow}", + "group": "{i18n:scada.symbol.animation}", + "type": "color", + "default": "#C8DFF7", + "disabled": false, + "visible": true + }, + { + "id": "flowStyleDash", + "name": "{i18n:scada.symbol.flow-style}", + "group": "{i18n:scada.symbol.animation}", + "type": "number", + "default": 10, + "required": true, + "subLabel": "{i18n:scada.symbol.dash}", + "divider": true, + "fieldSuffix": "px", + "min": 0, + "step": 1, + "disabled": false, + "visible": true + }, + { + "id": "flowStyleGap", + "name": "{i18n:scada.symbol.flow-style}", + "group": "{i18n:scada.symbol.animation}", + "type": "number", + "default": 10, + "subLabel": "{i18n:scada.symbol.gap}", + "fieldSuffix": "px", + "min": 1, + "step": 1, + "disabled": false, + "visible": true + }, + { + "id": "flowDashCap", + "name": "{i18n:scada.symbol.flow-dash-cap}", + "group": "{i18n:scada.symbol.animation}", + "type": "select", + "default": "butt", + "items": [ + { + "value": "butt", + "label": "{i18n:scada.symbol.dash-cap-butt}" + }, + { + "value": "round", + "label": "{i18n:scada.symbol.dash-cap-round}" + }, + { + "value": "square", + "label": "{i18n:scada.symbol.dash-cap-square}" + } + ], + "disabled": false, + "visible": true } ] }]]> - + \ No newline at end of file diff --git a/application/src/main/data/json/system/scada_symbols/vertical-connector-hp.svg b/application/src/main/data/json/system/scada_symbols/vertical-connector-hp.svg index 6ce9336fee..cfaf6793ea 100644 --- a/application/src/main/data/json/system/scada_symbols/vertical-connector-hp.svg +++ b/application/src/main/data/json/system/scada_symbols/vertical-connector-hp.svg @@ -3,6 +3,7 @@ "description": "Vertical connector with an optional directional arrow to visually indicate flow.", "widgetSizeX": 1, "widgetSizeY": 1, + "stateRenderFunction": "const {\n flowAnimation,\n arrowDirection: flowDirection,\n flowAnimationSpeed\n} = ctx.values;\nconst {\n flowAnimationWidth: lineWidth,\n flowAnimationColor: lineColor,\n flowStyleDash: dashWidth,\n flowStyleGap: dashGap,\n flowDashCap: dashCap\n} = ctx.properties;\nconst line = ctx.tags.line[0].attr('d');\nconst animation = ctx.tags.animationGroup[0];\nconst offset = Date.now() % 1000;\nconst duration = 1 / flowAnimationSpeed;\n\nconst prevFlowAnimation = animation.remember('flowAnimation');\nconst prevFlowDirection = animation.remember('flowDirection');\nconst prevFlowDuration = animation.remember('flowDuration');\n\nif (flowAnimation && flowAnimation !== prevFlowAnimation) {\n animation.remember('flowAnimation', flowAnimation);\n animation.remember('flowDuration', duration);\n animation.remember('flowDirection', flowDirection);\n animateFlow(offset, flowDirection);\n} else if (flowAnimation && flowDirection !== prevFlowDirection) {\n animation.remember('flowDirection', flowDirection);\n animateFlow(offset, flowDirection);\n} else if (flowAnimation && duration !== prevFlowDuration) {\n animation.remember('flowDuration', duration);\n animation.findOne('animate').attr('dur', `${duration}s`) ;\n} else if (!flowAnimation && prevFlowAnimation) {\n animation.remember('flowAnimation', null);\n animation.clear();\n}\n\nfunction animateFlow(offset, flowDirection) {\n animation.clear();\n const dashArray = `${dashWidth}${dashGap ? ` ${dashGap}` : ''}`;\n const value = flowDirection ? `${dashWidth + (dashGap || dashWidth)}` : `-${dashWidth + (dashGap || dashWidth)}`;\n\n animation.add(``);\n}\n", "tags": [ { "tag": "arrow", @@ -85,6 +86,83 @@ }, "defaultSetValueSettings": null, "defaultWidgetActionSettings": null + }, + { + "id": "flowAnimation", + "name": "{i18n:scada.symbol.flow-animation}", + "hint": "{i18n:scada.symbol.flow-animation-hint}", + "group": null, + "type": "value", + "valueType": "BOOLEAN", + "trueLabel": "{i18n:scada.symbol.present}", + "falseLabel": "{i18n:scada.symbol.absent}", + "stateLabel": "{i18n:scada.symbol.flow-present}", + "defaultGetValueSettings": { + "action": "DO_NOTHING", + "defaultValue": false, + "executeRpc": { + "method": "getState", + "requestTimeout": 5000, + "requestPersistent": false, + "persistentPollingInterval": 1000 + }, + "getAttribute": { + "key": "state", + "scope": null + }, + "getTimeSeries": { + "key": "state" + }, + "getAlarmStatus": { + "severityList": null, + "typeList": null + }, + "dataToValue": { + "type": "NONE", + "compareToValue": true, + "dataToValueFunction": "/* Should return boolean value */\nreturn data;" + } + }, + "defaultSetValueSettings": null, + "defaultWidgetActionSettings": null + }, + { + "id": "flowAnimationSpeed", + "name": "{i18n:scada.symbol.flow-animation-speed}", + "hint": "{i18n:scada.symbol.flow-animation-speed-hint}", + "group": null, + "type": "value", + "valueType": "DOUBLE", + "trueLabel": null, + "falseLabel": null, + "stateLabel": null, + "defaultGetValueSettings": { + "action": "DO_NOTHING", + "defaultValue": 1, + "executeRpc": { + "method": "getState", + "requestTimeout": 5000, + "requestPersistent": false, + "persistentPollingInterval": 1000 + }, + "getAttribute": { + "scope": null, + "key": "state" + }, + "getTimeSeries": { + "key": "state" + }, + "getAlarmStatus": { + "severityList": null, + "typeList": null + }, + "dataToValue": { + "type": "NONE", + "dataToValueFunction": "/* Should return boolean value */\nreturn data;" + } + }, + "defaultSetValueSettings": null, + "defaultWidgetActionSettings": null } ], "properties": [ @@ -93,16 +171,8 @@ "name": "{i18n:scada.symbol.main-line}", "type": "switch", "default": true, - "required": null, - "subLabel": null, - "divider": null, - "fieldSuffix": null, - "disableOnProperty": null, - "rowClass": "", - "fieldClass": "", - "min": null, - "max": null, - "step": null + "disabled": false, + "visible": true }, { "id": "mainLineSize", @@ -113,12 +183,11 @@ "subLabel": "Main", "divider": true, "fieldSuffix": "px", - "disableOnProperty": null, - "rowClass": "", - "fieldClass": "", "min": 0, "max": 99, - "step": 1 + "step": 1, + "disabled": false, + "visible": true }, { "id": "secondaryLineSize", @@ -127,48 +196,93 @@ "default": 2, "required": true, "subLabel": "Secondary", - "divider": null, "fieldSuffix": "px", - "disableOnProperty": null, - "rowClass": "", - "fieldClass": "", "min": 0, "max": 99, - "step": 1 + "step": 1, + "disabled": false, + "visible": true }, { "id": "lineColor", "name": "{i18n:scada.symbol.line-color}", "type": "color", "default": "#1A1A1A", - "required": null, - "subLabel": null, - "divider": null, - "fieldSuffix": null, - "disableOnProperty": null, - "rowClass": "", - "fieldClass": "", - "min": null, - "max": null, - "step": null + "disabled": false, + "visible": true }, { - "id": "arrowColor", - "name": "{i18n:scada.symbol.arrow-color}", + "id": "flowAnimationWidth", + "name": "{i18n:scada.symbol.flow}", + "group": "{i18n:scada.symbol.animation}", + "type": "number", + "default": 4, + "subLabel": "Width", + "fieldSuffix": "px", + "min": 1, + "step": 1, + "disabled": false, + "visible": true + }, + { + "id": "flowAnimationColor", + "name": "{i18n:scada.symbol.flow}", + "group": "{i18n:scada.symbol.animation}", "type": "color", - "default": "#1A1A1A", - "required": null, - "subLabel": null, - "divider": null, - "fieldSuffix": null, - "disableOnProperty": null, - "rowClass": "", - "fieldClass": "", - "min": null, - "max": null, - "step": null + "default": "#C8DFF7" + }, + { + "id": "flowStyleDash", + "name": "{i18n:scada.symbol.flow-style}", + "group": "{i18n:scada.symbol.animation}", + "type": "number", + "default": 10, + "required": true, + "subLabel": "{i18n:scada.symbol.dash}", + "divider": true, + "fieldSuffix": "px", + "min": 0, + "step": 1, + "disabled": false, + "visible": true + }, + { + "id": "flowStyleGap", + "name": "{i18n:scada.symbol.flow-style}", + "group": "{i18n:scada.symbol.animation}", + "type": "number", + "default": 10, + "subLabel": "{i18n:scada.symbol.gap}", + "fieldSuffix": "px", + "min": 1, + "step": 1, + "disabled": false, + "visible": true + }, + { + "id": "flowDashCap", + "name": "{i18n:scada.symbol.flow-dash-cap}", + "group": "{i18n:scada.symbol.animation}", + "type": "select", + "default": "butt", + "items": [ + { + "value": "butt", + "label": "{i18n:scada.symbol.dash-cap-butt}" + }, + { + "value": "round", + "label": "{i18n:scada.symbol.dash-cap-round}" + }, + { + "value": "square", + "label": "{i18n:scada.symbol.dash-cap-square}" + } + ], + "disabled": false, + "visible": true } ] }]]> - + \ No newline at end of file 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 18801bda2c..72c2cf80ae 100644 --- a/ui-ngx/src/assets/locale/locale.constant-en_US.json +++ b/ui-ngx/src/assets/locale/locale.constant-en_US.json @@ -3260,6 +3260,8 @@ "left-bottom-connector": "Left bottom connector", "top-left-connector": "Top left connector", "top-right-connector": "Top right connector", + "top-connector": "Top connector", + "bottom-connector": "Bottom connector", "running-color": "Running color", "stopped-color": "Stopped color", "stopped": "Stopped", @@ -3332,8 +3334,20 @@ "arrow-presence": "Arrow presence", "arrow-presence-hint": "Indicates whether arrow is present in connector.", "arrow-present": "Arrow present", - "arrow-direction": "Arrow direction", + "arrow-direction": "Arrow/Animation direction", "arrow-direction-hint": "Indicates flow direction.", + "animation-direction": "Flow animation direction", + "animation-direction-hint": "Indicates animation flow direction.", + "flow-animation": "Flow animation", + "flow-animation-hint": "Indicates whether animation is present in connector.", + "flow": "Flow", + "flow-style": "Flow style", + "flow-dash-cap": "Flow dash cap", + "dash-cap-butt": "Butt", + "dash-cap-round": "Round", + "dash-cap-square": "Square", + "dash": "Dash", + "gap": "Gap", "main-line": "Main line", "line": "Line", "line-color": "Line color", From 0115ac231b19613e8133a26c9c6cf1a614fb6458 Mon Sep 17 00:00:00 2001 From: ViacheslavKlimov Date: Thu, 27 Feb 2025 16:04:56 +0200 Subject: [PATCH 114/345] Fix license headers --- .../common/data/queue/ProcessingStrategyType.java | 15 --------------- .../server/edqs/repo/AssetTypeFilterTest.java | 15 --------------- 2 files changed, 30 deletions(-) diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/queue/ProcessingStrategyType.java b/common/data/src/main/java/org/thingsboard/server/common/data/queue/ProcessingStrategyType.java index 5b8c86d7d1..ca63d34a84 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/queue/ProcessingStrategyType.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/queue/ProcessingStrategyType.java @@ -13,21 +13,6 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -/** - * Copyright © 2016-2020 The Thingsboard Authors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ package org.thingsboard.server.common.data.queue; public enum ProcessingStrategyType { diff --git a/edqs/src/test/java/org/thingsboard/server/edqs/repo/AssetTypeFilterTest.java b/edqs/src/test/java/org/thingsboard/server/edqs/repo/AssetTypeFilterTest.java index 1420dfac0f..0961a7c12c 100644 --- a/edqs/src/test/java/org/thingsboard/server/edqs/repo/AssetTypeFilterTest.java +++ b/edqs/src/test/java/org/thingsboard/server/edqs/repo/AssetTypeFilterTest.java @@ -13,21 +13,6 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -/** - * Copyright © 2016-2024 The Thingsboard Authors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ package org.thingsboard.server.edqs.repo; import org.junit.After; From f772bab21843b6935ff44ac99e7d96a74760d4c1 Mon Sep 17 00:00:00 2001 From: trikimiki <41070061+trikimiki@users.noreply.github.com> Date: Thu, 27 Feb 2025 16:16:05 +0200 Subject: [PATCH 115/345] bump zookeeper version --- docker/docker-compose.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docker/docker-compose.yml b/docker/docker-compose.yml index 69b7722459..1cee5ad5ad 100644 --- a/docker/docker-compose.yml +++ b/docker/docker-compose.yml @@ -20,7 +20,7 @@ version: '3.0' services: zookeeper: restart: always - image: "zookeeper:3.8.0" + image: "zookeeper:3.8.1" ports: - "2181" environment: From f11ea0ba3d09718c124aef77874809f338647eff Mon Sep 17 00:00:00 2001 From: Tarnavskiy Date: Thu, 27 Feb 2025 16:41:21 +0200 Subject: [PATCH 116/345] Code formatting --- .../settings/alarm/alarms-table-widget-settings.component.ts | 3 +-- .../control/persistent-table-widget-settings.component.html | 2 +- 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/alarm/alarms-table-widget-settings.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/settings/alarm/alarms-table-widget-settings.component.ts index 7a7b97dc18..51452fb9b9 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/settings/alarm/alarms-table-widget-settings.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/alarm/alarms-table-widget-settings.component.ts @@ -97,8 +97,7 @@ export class AlarmsTableWidgetSettingsComponent extends WidgetSettingsComponent } protected validatorTriggers(): string[] { - return ['useRowStyleFunction', 'displayPagination', 'pageStepCount', - 'pageStepIncrement']; + return ['useRowStyleFunction', 'displayPagination', 'pageStepCount', 'pageStepIncrement']; } protected updateValidators(emitEvent: boolean, trigger: string) { diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/control/persistent-table-widget-settings.component.html b/ui-ngx/src/app/modules/home/components/widget/lib/settings/control/persistent-table-widget-settings.component.html index e69ef101fb..addf275f4f 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/settings/control/persistent-table-widget-settings.component.html +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/control/persistent-table-widget-settings.component.html @@ -45,7 +45,7 @@
widgets.table.pagination
- + {{ 'widgets.table.display-pagination' | translate }}
From 02857b70bcdd2149392ca6ed1717c2847be896f9 Mon Sep 17 00:00:00 2001 From: dashevchenko Date: Thu, 27 Feb 2025 17:31:46 +0200 Subject: [PATCH 117/345] fixed flaky test, code refactoring --- .../subscription/TbAlarmDataSubCtx.java | 3 +- .../entitiy/EdqsEntityServiceTest.java | 7 +++ .../service/entitiy/EntityServiceTest.java | 56 ++++++++----------- .../server/dao/entity/BaseEntityService.java | 2 +- .../server/msa/TestRestClient.java | 15 ++++- .../msa/edqs/EdqsEntityDataQueryTest.java | 21 +++++-- 6 files changed, 59 insertions(+), 45 deletions(-) diff --git a/application/src/main/java/org/thingsboard/server/service/subscription/TbAlarmDataSubCtx.java b/application/src/main/java/org/thingsboard/server/service/subscription/TbAlarmDataSubCtx.java index c471c84b11..f6b4067543 100644 --- a/application/src/main/java/org/thingsboard/server/service/subscription/TbAlarmDataSubCtx.java +++ b/application/src/main/java/org/thingsboard/server/service/subscription/TbAlarmDataSubCtx.java @@ -39,6 +39,7 @@ import org.thingsboard.server.dao.alarm.AlarmService; import org.thingsboard.server.dao.attributes.AttributesService; import org.thingsboard.server.dao.entity.EntityService; import org.thingsboard.server.dao.model.ModelConstants; +import org.thingsboard.server.dao.sql.query.EntityKeyMapping; import org.thingsboard.server.service.ws.WebSocketService; import org.thingsboard.server.service.ws.WebSocketSessionRef; import org.thingsboard.server.service.ws.telemetry.cmd.v2.AlarmDataUpdate; @@ -359,7 +360,7 @@ public class TbAlarmDataSubCtx extends TbAbstractDataSubCtx { EntityDataSortOrder sortOrder = query.getPageLink().getSortOrder(); EntityDataSortOrder entitiesSortOrder; if (sortOrder == null || sortOrder.getKey().getType().equals(EntityKeyType.ALARM_FIELD)) { - entitiesSortOrder = new EntityDataSortOrder(new EntityKey(EntityKeyType.ENTITY_FIELD, "createdTime")); + entitiesSortOrder = new EntityDataSortOrder(new EntityKey(EntityKeyType.ENTITY_FIELD, EntityKeyMapping.CREATED_TIME)); } else { entitiesSortOrder = sortOrder; } diff --git a/application/src/test/java/org/thingsboard/server/service/entitiy/EdqsEntityServiceTest.java b/application/src/test/java/org/thingsboard/server/service/entitiy/EdqsEntityServiceTest.java index b9cd7dbac2..f4cdf3038b 100644 --- a/application/src/test/java/org/thingsboard/server/service/entitiy/EdqsEntityServiceTest.java +++ b/application/src/test/java/org/thingsboard/server/service/entitiy/EdqsEntityServiceTest.java @@ -29,6 +29,7 @@ import org.thingsboard.server.common.data.page.PageData; import org.thingsboard.server.common.data.query.EntityCountQuery; import org.thingsboard.server.common.data.query.EntityData; import org.thingsboard.server.common.data.query.EntityDataQuery; +import org.thingsboard.server.common.data.query.EntityKeyType; import org.thingsboard.server.common.data.query.RelationsQueryFilter; import org.thingsboard.server.common.data.relation.EntitySearchDirection; import org.thingsboard.server.common.data.relation.RelationEntityTypeFilter; @@ -106,6 +107,12 @@ public class EdqsEntityServiceTest extends EntityServiceTest { result -> result.getTotalElements() == expectedResultSize); } + @Override + protected List findByQueryAndCheckTelemetry(EntityDataQuery query, EntityKeyType entityKeyType, String key, List expectedTelemetries) { + return await().atMost(15, TimeUnit.SECONDS).until(() -> findEntitiesTelemetry(query, entityKeyType, key, expectedTelemetries), + loadedTelemetry -> loadedTelemetry.containsAll(expectedTelemetries)); + } + @Override protected long countByQueryAndCheck(EntityCountQuery countQuery, int expectedResult) { return countByQueryAndCheck(new CustomerId(CustomerId.NULL_UUID), countQuery, expectedResult); 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 0297281b68..76d3ac1659 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 @@ -116,7 +116,6 @@ import java.util.stream.Collectors; import java.util.stream.Stream; import static org.assertj.core.api.Assertions.assertThat; -import static org.junit.Assert.assertEquals; import static org.thingsboard.server.common.data.query.EntityKeyType.ATTRIBUTE; import static org.thingsboard.server.common.data.query.EntityKeyType.ENTITY_FIELD; @@ -1571,47 +1570,19 @@ public class EntityServiceTest extends AbstractControllerTest { for (EntityKeyType currentAttributeKeyType : attributesEntityTypes) { List latestValues = Collections.singletonList(new EntityKey(currentAttributeKeyType, "temperature")); EntityDataQuery query = new EntityDataQuery(filter, pageLink, entityFields, latestValues, null); - PageData data = findByQueryAndCheck(query, 67); - List loadedEntities = new ArrayList<>(data.getData()); - while (data.hasNext()) { - query = query.next(); - data = findByQuery(query); - loadedEntities.addAll(data.getData()); - } - Assert.assertEquals(67, loadedEntities.size()); - List loadedTemperatures = new ArrayList<>(); - for (Device device : devices) { - loadedTemperatures.add(loadedEntities.stream().filter(entityData -> entityData.getEntityId().equals(device.getId())).findFirst().orElse(null) - .getLatest().get(currentAttributeKeyType).get("temperature").getValue()); - } - List deviceTemperatures = temperatures.stream().map(aLong -> Long.toString(aLong)).collect(Collectors.toList()); - assertThat(loadedTemperatures).containsExactlyInAnyOrderElementsOf(deviceTemperatures); + List deviceTemperatures = temperatures.stream().map(aLong -> Long.toString(aLong)).toList(); + findByQueryAndCheckTelemetry(query, currentAttributeKeyType, "temperature", deviceTemperatures); pageLink = new EntityDataPageLink(10, 0, null, sortOrder); KeyFilter highTemperatureFilter = createNumericKeyFilter("temperature", currentAttributeKeyType, NumericFilterPredicate.NumericOperation.GREATER, 45); List keyFiltersHighTemperature = Collections.singletonList(highTemperatureFilter); query = new EntityDataQuery(filter, pageLink, entityFields, latestValues, keyFiltersHighTemperature); - data = findByQueryAndCheck(query, highTemperatures.size()); - - loadedEntities = new ArrayList<>(data.getData()); - while (data.hasNext()) { - query = query.next(); - data = findByQuery(query); - loadedEntities.addAll(data.getData()); - } - Assert.assertEquals(highTemperatures.size(), loadedEntities.size()); - - List loadedHighTemperatures = loadedEntities.stream().map(entityData -> - entityData.getLatest().get(currentAttributeKeyType).get("temperature").getValue()).collect(Collectors.toList()); - List deviceHighTemperatures = highTemperatures.stream().map(aLong -> Long.toString(aLong)).collect(Collectors.toList()); - - assertThat(loadedHighTemperatures).containsExactlyInAnyOrderElementsOf(deviceHighTemperatures); + findByQueryAndCheckTelemetry(query, currentAttributeKeyType, "temperature", highTemperatures.stream().map(Object::toString).toList()); } deviceService.deleteDevicesByTenantId(tenantId); } - @Test public void testBuildNumericPredicateQueryOperations() throws ExecutionException, InterruptedException { @@ -2519,7 +2490,7 @@ public class EntityServiceTest extends AbstractControllerTest { findByQueryAndCheck(new CustomerId(EntityId.NULL_UUID), query, 0); } - private PageData findByQuery(EntityDataQuery query) { + protected PageData findByQuery(EntityDataQuery query) { return findByQuery(new CustomerId(CustomerId.NULL_UUID), query); } @@ -2527,7 +2498,7 @@ public class EntityServiceTest extends AbstractControllerTest { return entityService.findEntityDataByQuery(tenantId, customerId, query); } - private PageData findByQueryAndCheck(EntityDataQuery query, long expectedResultSize) { + protected PageData findByQueryAndCheck(EntityDataQuery query, long expectedResultSize) { return findByQueryAndCheck(new CustomerId(CustomerId.NULL_UUID), query, expectedResultSize); } @@ -2537,6 +2508,23 @@ public class EntityServiceTest extends AbstractControllerTest { return result; } + protected List findByQueryAndCheckTelemetry(EntityDataQuery query, EntityKeyType entityKeyType, String key, List expectedTelemetry) { + List entitiesTelemetry = findEntitiesTelemetry(query, entityKeyType, key, expectedTelemetry); + assertThat(entitiesTelemetry).containsExactlyInAnyOrderElementsOf(expectedTelemetry); + return entitiesTelemetry; + } + + protected List findEntitiesTelemetry(EntityDataQuery query, EntityKeyType entityKeyType, String key, List expectedTelemetries) { + PageData data = findByQueryAndCheck(query, expectedTelemetries.size()); + List loadedEntities = new ArrayList<>(data.getData()); + while (data.hasNext()) { + query = query.next(); + data = findByQuery(query); + loadedEntities.addAll(data.getData()); + } + return loadedEntities.stream().map(entityData -> entityData.getLatest().get(entityKeyType).get(key).getValue()).toList(); + } + protected long countByQuery(CustomerId customerId, EntityCountQuery query) { return entityService.countEntitiesByQuery(tenantId, customerId, query); } diff --git a/dao/src/main/java/org/thingsboard/server/dao/entity/BaseEntityService.java b/dao/src/main/java/org/thingsboard/server/dao/entity/BaseEntityService.java index bbe21341a4..3e398f9746 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/entity/BaseEntityService.java +++ b/dao/src/main/java/org/thingsboard/server/dao/entity/BaseEntityService.java @@ -112,7 +112,7 @@ public class BaseEntityService extends AbstractEntityService implements EntitySe validateId(customerId, id -> INCORRECT_CUSTOMER_ID + id); validateEntityDataQuery(query); - if (edqsApiService.isEnabled() && validForEdqs(query) && !tenantId.isSysTenantId()) { + if (edqsApiService.isEnabled() && validForEdqs(query)) { EdqsRequest request = EdqsRequest.builder() .entityDataQuery(query) .build(); diff --git a/msa/black-box-tests/src/test/java/org/thingsboard/server/msa/TestRestClient.java b/msa/black-box-tests/src/test/java/org/thingsboard/server/msa/TestRestClient.java index ed4b54f2fc..d74438a428 100644 --- a/msa/black-box-tests/src/test/java/org/thingsboard/server/msa/TestRestClient.java +++ b/msa/black-box-tests/src/test/java/org/thingsboard/server/msa/TestRestClient.java @@ -237,6 +237,15 @@ public class TestRestClient { .as(JsonNode.class); } + public JsonNode getLatestTelemetry(EntityId entityId) { + return given().spec(requestSpec) + .get("/api/plugins/telemetry/" + entityId.getEntityType().name() + "/" + entityId.getId() + "/values/timeseries") + .then() + .statusCode(HTTP_OK) + .extract() + .as(JsonNode.class); + } + public JsonPath postProvisionRequest(String provisionRequest) { return given().spec(requestSpec) .body(provisionRequest) @@ -498,13 +507,13 @@ public class TestRestClient { public UserId createUserAndLogin(User user, String password) { UserId userId = postUser(user).getId(); - getAndSetUserToken(userId.getId().toString()); + getAndSetUserToken(userId); return userId; } - public void getAndSetUserToken(String id) { + public void getAndSetUserToken(UserId id) { ObjectNode tokenInfo = given().spec(requestSpec) - .get("/api/user/" + id + "/token") + .get("/api/user/" + id.getId().toString() + "/token") .then() .extract() .as(ObjectNode.class); diff --git a/msa/black-box-tests/src/test/java/org/thingsboard/server/msa/edqs/EdqsEntityDataQueryTest.java b/msa/black-box-tests/src/test/java/org/thingsboard/server/msa/edqs/EdqsEntityDataQueryTest.java index 6c0a507431..8e77b315e6 100644 --- a/msa/black-box-tests/src/test/java/org/thingsboard/server/msa/edqs/EdqsEntityDataQueryTest.java +++ b/msa/black-box-tests/src/test/java/org/thingsboard/server/msa/edqs/EdqsEntityDataQueryTest.java @@ -37,6 +37,7 @@ import org.thingsboard.server.common.data.query.EntityDataSortOrder; import org.thingsboard.server.common.data.query.EntityKey; import org.thingsboard.server.common.data.query.EntityKeyType; import org.thingsboard.server.common.data.query.EntityTypeFilter; +import org.thingsboard.server.common.data.query.TsValue; import org.thingsboard.server.msa.AbstractContainerTest; import org.thingsboard.server.msa.DisableUIListeners; import org.thingsboard.server.msa.ui.utils.EntityPrototypes; @@ -44,6 +45,7 @@ import org.thingsboard.server.msa.ui.utils.EntityPrototypes; import java.util.ArrayList; import java.util.Collections; import java.util.List; +import java.util.Map; import java.util.concurrent.TimeUnit; import static org.assertj.core.api.Assertions.assertThat; @@ -111,16 +113,16 @@ public class EdqsEntityDataQueryTest extends AbstractContainerTest { EntityCountQuery query = new EntityCountQuery(allDeviceFilter); await("Waiting for total device count") .atMost(30, TimeUnit.SECONDS) - .until(() -> testRestClient.postCountDataQuery(query).compareTo(97L * 2) >= 0); + .until(() -> testRestClient.postCountDataQuery(query).equals(97L * 2)); - testRestClient.getAndSetUserToken(tenantAdminId.getId().toString()); + testRestClient.getAndSetUserToken(tenantAdminId); await("Waiting for total device count") .atMost(30, TimeUnit.SECONDS) .until(() -> testRestClient.postCountDataQuery(query).equals(97L)); testRestClient.resetToken(); testRestClient.login("sysadmin@thingsboard.org", "sysadmin"); - testRestClient.getAndSetUserToken(tenant2AdminId.getId().toString()); + testRestClient.getAndSetUserToken(tenant2AdminId); await("Waiting for total device count") .atMost(30, TimeUnit.SECONDS) .until(() -> testRestClient.postCountDataQuery(query).equals(97L)); @@ -129,17 +131,17 @@ public class EdqsEntityDataQueryTest extends AbstractContainerTest { @Test public void testRetrieveTenantDevicesByDeviceTypeFilter() { // login tenant admin - testRestClient.getAndSetUserToken(tenantAdminId.getId().toString()); + testRestClient.getAndSetUserToken(tenantAdminId); checkUserDevices(tenantDevices); // login customer user - testRestClient.getAndSetUserToken(customerUserId.getId().toString()); + testRestClient.getAndSetUserToken(customerUserId); checkUserDevices(tenantDevices.subList(0, 12)); // login other tenant admin testRestClient.resetToken(); testRestClient.login("sysadmin@thingsboard.org", "sysadmin"); - testRestClient.getAndSetUserToken(tenant2AdminId.getId().toString()); + testRestClient.getAndSetUserToken(tenant2AdminId); checkUserDevices(tenant2Devices); } @@ -168,6 +170,13 @@ public class EdqsEntityDataQueryTest extends AbstractContainerTest { assertThat(retrievedDevices).hasSize(10); List retrievedDeviceNames = retrievedDevices.stream().map(entityData -> entityData.getLatest().get(EntityKeyType.ENTITY_FIELD).get("name").getValue()).toList(); assertThat(retrievedDeviceNames).containsExactlyInAnyOrderElementsOf(devices.stream().map(Device::getName).toList().subList(0, 10)); + + //check temperature + for (int i = 0; i < 10; i++) { + Map> latest = retrievedDevices.get(i).getLatest(); + String name = latest.get(EntityKeyType.ENTITY_FIELD).get("name").getValue(); + //assertThat(latest.get(EntityKeyType.TIME_SERIES).get("temperature").getValue()).isEqualTo(name.substring(name.length() - 1)); + } } private String createDevices(String deviceType, List tenantDevices, int deviceCount) throws InterruptedException { From 45d289d0a3a7c4e5e677b93d2aa2c2bd0b5dd761 Mon Sep 17 00:00:00 2001 From: dashevchenko Date: Thu, 27 Feb 2025 18:22:52 +0200 Subject: [PATCH 118/345] deleted TENANT_PROFILE from edqs types --- .../org/thingsboard/server/common/data/ObjectType.java | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/ObjectType.java b/common/data/src/main/java/org/thingsboard/server/common/data/ObjectType.java index e25ef57a44..51c631fe4f 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/ObjectType.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/ObjectType.java @@ -60,15 +60,14 @@ public enum ObjectType { LATEST_TS_KV; public static final Set edqsTenantTypes = EnumSet.of( - TENANT, TENANT_PROFILE, CUSTOMER, DEVICE_PROFILE, DEVICE, ASSET_PROFILE, ASSET, EDGE, ENTITY_VIEW, USER, DASHBOARD, + TENANT, CUSTOMER, DEVICE_PROFILE, DEVICE, ASSET_PROFILE, ASSET, EDGE, ENTITY_VIEW, USER, DASHBOARD, RULE_CHAIN, WIDGET_TYPE, WIDGETS_BUNDLE, API_USAGE_STATE, QUEUE_STATS ); public static final Set edqsTypes = EnumSet.copyOf(edqsTenantTypes); - public static final Set edqsSystemTypes = EnumSet.of(TENANT, TENANT_PROFILE, USER, DASHBOARD, + public static final Set edqsSystemTypes = EnumSet.of(TENANT, USER, DASHBOARD, API_USAGE_STATE, ATTRIBUTE_KV, LATEST_TS_KV); public static final Set unversionedTypes = EnumSet.of( - QUEUE_STATS, // created once, never updated - TENANT_PROFILE // only for total count calculation + QUEUE_STATS // created once, never updated ); static { From 73a654d7840c2290877a3eaf7f765f4d3efb45f6 Mon Sep 17 00:00:00 2001 From: Igor Kulikov Date: Thu, 27 Feb 2025 18:43:19 +0200 Subject: [PATCH 119/345] UI: Trip map settings. --- .../lib/maps/data-layer/trips-data-layer.ts | 4 +- .../widget/lib/maps/models/map.models.ts | 35 +++- .../lib/maps/models/marker-shape.models.ts | 71 +++++++- .../panels/map-timeline-panel.component.html | 11 ++ .../panels/map-timeline-panel.component.scss | 8 + .../panels/map-timeline-panel.component.ts | 36 +++- ...data-layer-pattern-settings.component.html | 2 +- .../data-layer-pattern-settings.component.ts | 3 + .../map/map-action-button-row.component.html | 2 +- .../map/map-action-button-row.component.ts | 2 +- ...map-action-buttons-settings.component.html | 2 +- .../map-action-buttons-settings.component.ts | 2 +- .../map/map-data-layer-dialog.component.html | 172 +++++++++++++++--- .../map/map-data-layer-dialog.component.ts | 124 +++++++++++-- .../map/map-data-layer-row.component.html | 12 +- .../map/map-data-layer-row.component.ts | 30 ++- .../common/map/map-data-layers.component.html | 4 +- .../common/map/map-data-layers.component.ts | 4 + .../common/map/map-settings.component.html | 9 + .../map/map-settings.component.models.ts | 4 +- .../common/map/map-settings.component.ts | 12 +- .../map/marker-shape-settings.component.ts | 10 +- .../common/map/marker-shapes.component.ts | 13 +- .../map/trip-timeline-settings.component.html | 81 +++++++++ .../map/trip-timeline-settings.component.ts | 172 ++++++++++++++++++ .../common/widget-settings-common.module.ts | 4 + .../map/map-widget-settings.component.html | 1 + .../map/map-widget-settings.component.ts | 12 +- .../assets/locale/locale.constant-en_US.json | 31 +++- ui-ngx/src/assets/markers/tripShape1.svg | 4 + ui-ngx/src/assets/markers/tripShape10.svg | 10 + ui-ngx/src/assets/markers/tripShape2.svg | 15 ++ ui-ngx/src/assets/markers/tripShape3.svg | 9 + ui-ngx/src/assets/markers/tripShape4.svg | 9 + ui-ngx/src/assets/markers/tripShape5.svg | 10 + ui-ngx/src/assets/markers/tripShape6.svg | 10 + ui-ngx/src/assets/markers/tripShape7.svg | 11 ++ ui-ngx/src/assets/markers/tripShape8.svg | 9 + ui-ngx/src/assets/markers/tripShape9.svg | 10 + 39 files changed, 882 insertions(+), 88 deletions(-) create mode 100644 ui-ngx/src/app/modules/home/components/widget/lib/settings/common/map/trip-timeline-settings.component.html create mode 100644 ui-ngx/src/app/modules/home/components/widget/lib/settings/common/map/trip-timeline-settings.component.ts create mode 100644 ui-ngx/src/assets/markers/tripShape1.svg create mode 100644 ui-ngx/src/assets/markers/tripShape10.svg create mode 100644 ui-ngx/src/assets/markers/tripShape2.svg create mode 100644 ui-ngx/src/assets/markers/tripShape3.svg create mode 100644 ui-ngx/src/assets/markers/tripShape4.svg create mode 100644 ui-ngx/src/assets/markers/tripShape5.svg create mode 100644 ui-ngx/src/assets/markers/tripShape6.svg create mode 100644 ui-ngx/src/assets/markers/tripShape7.svg create mode 100644 ui-ngx/src/assets/markers/tripShape8.svg create mode 100644 ui-ngx/src/assets/markers/tripShape9.svg diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/maps/data-layer/trips-data-layer.ts b/ui-ngx/src/app/modules/home/components/widget/lib/maps/data-layer/trips-data-layer.ts index 354fa4eb0f..63824365ca 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/maps/data-layer/trips-data-layer.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/maps/data-layer/trips-data-layer.ts @@ -331,7 +331,7 @@ export class TbTripsDataLayer extends TbMapDataLayer d[0].$datasource.mapDataIds.includes(this.mapDataId)).map( + tripsData.filter(d => !!d.length && d[0].$datasource.mapDataIds.includes(this.mapDataId)).map( item => this.clearIncorrectFirsLastDatapoint(item)).filter(arr => arr.length); this.latestTripsData = tripsLatestData.filter(d => d.$datasource.mapDataIds.includes(this.mapDataId)); this.rawTripsData.forEach((dataSource) => { @@ -396,7 +396,7 @@ export class TbTripsDataLayer extends TbMapDataLayer { - return createColorMarkerIconElement(this.getCtx().$injector.get(MatIconRegistry), this.getCtx().$injector.get(DomSanitizer), icon, color).pipe( + return createColorMarkerIconElement(this.getCtx().$injector.get(MatIconRegistry), this.getCtx().$injector.get(DomSanitizer), icon, color, true).pipe( map((element) => { element.style.transform = `rotate(${rotationAngle}deg)`; return { diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/maps/models/map.models.ts b/ui-ngx/src/app/modules/home/components/widget/lib/maps/models/map.models.ts index 2361440828..f93eab6dbc 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/maps/models/map.models.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/maps/models/map.models.ts @@ -157,7 +157,7 @@ export const defaultBaseDataLayerSettings = (mapType: MapType): Partial { if (!dataLayer.dsType || ![DatasourceType.function, DatasourceType.device, DatasourceType.entity].includes(dataLayer.dsType)) { @@ -307,13 +307,13 @@ const defaultMarkerYPosFunction = 'var value = prevValue || 0.3;\n' + '}\n' + 'return value;'; -export const defaultMarkersDataLayerSettings = (mapType: MapType, functionsOnly = false): MarkersDataLayerSettings => mergeDeep({ +const defaultMarkersDataSourceSettings = (mapType: MapType, timeSeries = false, functionsOnly = false): Partial => ({ dsType: functionsOnly ? DatasourceType.function : DatasourceType.entity, dsLabel: functionsOnly ? 'First point' : '', xKey: { name: functionsOnly ? 'f(x)' : (MapType.geoMap === mapType ? 'latitude' : 'xPos'), label: MapType.geoMap === mapType ? 'latitude' : 'xPos', - type: functionsOnly ? DataKeyType.function : DataKeyType.attribute, + type: functionsOnly ? DataKeyType.function : (timeSeries ? DataKeyType.timeseries : DataKeyType.attribute), funcBody: functionsOnly ? (MapType.geoMap === mapType ? defaultMarkerLatitudeFunction : defaultMarkerXPosFunction) : undefined, settings: {}, color: materialColors[0].value @@ -321,12 +321,16 @@ export const defaultMarkersDataLayerSettings = (mapType: MapType, functionsOnly yKey: { name: functionsOnly ? 'f(x)' : (MapType.geoMap === mapType ? 'longitude' : 'yPos'), label: MapType.geoMap === mapType ? 'longitude' : 'yPos', - type: functionsOnly ? DataKeyType.function : DataKeyType.attribute, + type: functionsOnly ? DataKeyType.function : (timeSeries ? DataKeyType.timeseries : DataKeyType.attribute), funcBody: functionsOnly ? (MapType.geoMap === mapType ? defaultMarkerLongitudeFunction : defaultMarkerYPosFunction) : undefined, settings: {}, color: materialColors[0].value } -} as MarkersDataLayerSettings, defaultBaseMarkersDataLayerSettings(mapType) as MarkersDataLayerSettings); +}); + +export const defaultMarkersDataLayerSettings = (mapType: MapType, functionsOnly = false): MarkersDataLayerSettings => mergeDeep( + defaultMarkersDataSourceSettings(mapType, false, functionsOnly) as MarkersDataLayerSettings, + defaultBaseMarkersDataLayerSettings(mapType) as MarkersDataLayerSettings); export const defaultBaseMarkersDataLayerSettings = (mapType: MapType): Partial => mergeDeep({ markerType: MarkerType.shape, @@ -378,8 +382,8 @@ export const pathDecoratorSymbols = Object.keys(PathDecoratorSymbol) as PathDeco export const pathDecoratorSymbolTranslationMap = new Map( [ - [PathDecoratorSymbol.arrowHead, 'widgets.maps.data-layer.decorator-symbol-arrow-head'], - [PathDecoratorSymbol.dash, 'widgets.maps.data-layer.decorator-symbol-dash'] + [PathDecoratorSymbol.arrowHead, 'widgets.maps.data-layer.path.decorator-symbol-arrow-head'], + [PathDecoratorSymbol.dash, 'widgets.maps.data-layer.path.decorator-symbol-dash'] ] ); @@ -402,6 +406,10 @@ export interface TripsDataLayerSettings extends MarkersDataLayerSettings { pointTooltip?: DataLayerTooltipSettings; } +export const defaultTripsDataLayerSettings = (mapType: MapType, functionsOnly = false): TripsDataLayerSettings => mergeDeep( + defaultMarkersDataSourceSettings(mapType, true, functionsOnly) as TripsDataLayerSettings, + defaultBaseTripsDataLayerSettings(mapType) as TripsDataLayerSettings); + export const defaultBaseTripsDataLayerSettings = (mapType: MapType): Partial => mergeDeep( defaultBaseMarkersDataLayerSettings(mapType), { @@ -412,6 +420,15 @@ export const defaultBaseTripsDataLayerSettings = (mapType: MapType): Partial { switch (dataLayerType) { + case 'trips': + return defaultTripsDataLayerSettings(mapType, functionsOnly); case 'markers': return defaultMarkersDataLayerSettings(mapType, functionsOnly); case 'polygons': @@ -523,6 +542,8 @@ export const defaultMapDataLayerSettings = (mapType: MapType, dataLayerType: Map export const defaultBaseMapDataLayerSettings = (mapType: MapType, dataLayerType: MapDataLayerType): T => { switch (dataLayerType) { + case 'trips': + return defaultBaseTripsDataLayerSettings(mapType) as T; case 'markers': return defaultBaseMarkersDataLayerSettings(mapType) as T; case 'polygons': diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/maps/models/marker-shape.models.ts b/ui-ngx/src/app/modules/home/components/widget/lib/maps/models/marker-shape.models.ts index 7abf0e19cd..1948ff6fe7 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/maps/models/marker-shape.models.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/maps/models/marker-shape.models.ts @@ -32,7 +32,17 @@ export enum MarkerShape { markerShape7 = 'markerShape7', markerShape8 = 'markerShape8', markerShape9 = 'markerShape9', - markerShape10 = 'markerShape10' + markerShape10 = 'markerShape10', + tripMarkerShape1 = 'tripMarkerShape1', + tripMarkerShape2 = 'tripMarkerShape2', + tripMarkerShape3 = 'tripMarkerShape3', + tripMarkerShape4 = 'tripMarkerShape4', + tripMarkerShape5 = 'tripMarkerShape5', + tripMarkerShape6 = 'tripMarkerShape6', + tripMarkerShape7 = 'tripMarkerShape7', + tripMarkerShape8 = 'tripMarkerShape8', + tripMarkerShape9 = 'tripMarkerShape9', + tripMarkerShape10 = 'tripMarkerShape10' } export const markerShapeMap = new Map( @@ -46,21 +56,63 @@ export const markerShapeMap = new Map( [MarkerShape.markerShape7, '/assets/markers/shape7.svg'], [MarkerShape.markerShape8, '/assets/markers/shape8.svg'], [MarkerShape.markerShape9, '/assets/markers/shape9.svg'], - [MarkerShape.markerShape10, '/assets/markers/shape10.svg'] + [MarkerShape.markerShape10, '/assets/markers/shape10.svg'], + [MarkerShape.tripMarkerShape1, '/assets/markers/tripShape1.svg'], + [MarkerShape.tripMarkerShape2, '/assets/markers/tripShape2.svg'], + [MarkerShape.tripMarkerShape3, '/assets/markers/tripShape3.svg'], + [MarkerShape.tripMarkerShape4, '/assets/markers/tripShape4.svg'], + [MarkerShape.tripMarkerShape5, '/assets/markers/tripShape5.svg'], + [MarkerShape.tripMarkerShape6, '/assets/markers/tripShape6.svg'], + [MarkerShape.tripMarkerShape7, '/assets/markers/tripShape7.svg'], + [MarkerShape.tripMarkerShape8, '/assets/markers/tripShape8.svg'], + [MarkerShape.tripMarkerShape9, '/assets/markers/tripShape9.svg'], + [MarkerShape.tripMarkerShape10, '/assets/markers/tripShape10.svg'] ] ); +export const markerShapes = [ + MarkerShape.markerShape1, + MarkerShape.markerShape2, + MarkerShape.markerShape3, + MarkerShape.markerShape4, + MarkerShape.markerShape5, + MarkerShape.markerShape6, + MarkerShape.markerShape7, + MarkerShape.markerShape8, + MarkerShape.markerShape9, + MarkerShape.markerShape10 +]; + +export const tripMarkerShapes = [ + MarkerShape.tripMarkerShape1, + MarkerShape.tripMarkerShape2, + MarkerShape.tripMarkerShape3, + MarkerShape.tripMarkerShape4, + MarkerShape.tripMarkerShape5, + MarkerShape.tripMarkerShape6, + MarkerShape.tripMarkerShape7, + MarkerShape.tripMarkerShape8, + MarkerShape.tripMarkerShape9, + MarkerShape.tripMarkerShape10 +]; + const createColorMarkerShape = (iconRegistry: MatIconRegistry, domSanitizer: DomSanitizer, shape: MarkerShape, color: tinycolor.Instance): Observable => { const markerAssetUrl = markerShapeMap.get(shape); const safeUrl = domSanitizer.bypassSecurityTrustResourceUrl(markerAssetUrl); return iconRegistry.getSvgIconFromUrl(safeUrl).pipe( map((svgElement) => { const colorElements = Array.from(svgElement.getElementsByClassName('marker-color')); + if (svgElement.classList.contains('marker-color')) { + colorElements.push(svgElement); + } colorElements.forEach(el => { el.setAttribute('fill', '#'+color.toHex()); el.setAttribute('fill-opacity', `${color.getAlpha()}`); }); const strokeElements = Array.from(svgElement.getElementsByClassName('marker-stroke')); + if (svgElement.classList.contains('marker-stroke')) { + strokeElements.push(svgElement); + } strokeElements.forEach(el => { el.setAttribute('stroke', '#'+color.toHex()); el.setAttribute('stroke-opacity', `${color.getAlpha()}`); @@ -80,10 +132,10 @@ export const createColorMarkerShapeURI = (iconRegistry: MatIconRegistry, domSani ); } -const createIconElement = (iconRegistry: MatIconRegistry, icon: string, size: number, color: tinycolor.Instance): Observable => { +const createIconElement = (iconRegistry: MatIconRegistry, icon: string, size: number, color: tinycolor.Instance, trip = false): Observable => { const isSvg = isSvgIcon(icon); const iconAlpha = color.getAlpha(); - const iconColor = tinycolor.mix(color.clone().setAlpha(1), tinycolor('rgba(0,0,0,0.38)')); + const iconColor = trip ? color : tinycolor.mix(color.clone().setAlpha(1), tinycolor('rgba(0,0,0,0.38)')); if (isSvg) { const [namespace, iconName] = splitIconName(icon); return iconRegistry @@ -122,14 +174,19 @@ const createIconElement = (iconRegistry: MatIconRegistry, icon: string, size: nu } } -export const createColorMarkerIconElement = (iconRegistry: MatIconRegistry, domSanitizer: DomSanitizer, icon: string, color: tinycolor.Instance): Observable => { - return createColorMarkerShape(iconRegistry, domSanitizer, MarkerShape.markerShape6, color).pipe( +const markerIconShape = MarkerShape.markerShape6; +const tripMarkerIconShape = MarkerShape.tripMarkerShape2; + +export const createColorMarkerIconElement = (iconRegistry: MatIconRegistry, domSanitizer: DomSanitizer, icon: string, color: tinycolor.Instance, + trip = false): Observable => { + return createColorMarkerShape(iconRegistry, domSanitizer, trip ? tripMarkerIconShape : markerIconShape, color).pipe( switchMap((svgElement) => { - return createIconElement(iconRegistry, icon, 12, color).pipe( + return createIconElement(iconRegistry, icon, trip ? 24 : 12, trip ? tinycolor('#fff') : color, trip).pipe( map((iconElement) => { let elements = svgElement.getElementsByClassName('marker-icon-container'); if (iconElement && elements.length) { const iconContainer = new G(elements[0] as SVGGElement); + iconContainer.clear(); iconContainer.add(iconElement); const box = iconElement.bbox(); iconElement.translate(-box.cx, -box.cy); diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/maps/panels/map-timeline-panel.component.html b/ui-ngx/src/app/modules/home/components/widget/lib/maps/panels/map-timeline-panel.component.html index 543f07a3d7..57bf909613 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/maps/panels/map-timeline-panel.component.html +++ b/ui-ngx/src/app/modules/home/components/widget/lib/maps/panels/map-timeline-panel.component.html @@ -23,4 +23,15 @@ [step]="settings.timeStep"> +
+
+
+
+
2
+
+ + {{speedValue}}x + +
+
diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/maps/panels/map-timeline-panel.component.scss b/ui-ngx/src/app/modules/home/components/widget/lib/maps/panels/map-timeline-panel.component.scss index 9e38394deb..7795e05523 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/maps/panels/map-timeline-panel.component.scss +++ b/ui-ngx/src/app/modules/home/components/widget/lib/maps/panels/map-timeline-panel.component.scss @@ -14,6 +14,14 @@ * limitations under the License. */ .tb-map-timeline-panel { + padding-top: 4px; + padding-left: 16px; + padding-right: 16px; display: flex; flex-direction: column; + .tb-timeline-controls { + display: flex; + flex-direction: row; + align-items: center; + } } diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/maps/panels/map-timeline-panel.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/maps/panels/map-timeline-panel.component.ts index 33cac7c012..1cb76aceea 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/maps/panels/map-timeline-panel.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/maps/panels/map-timeline-panel.component.ts @@ -15,16 +15,18 @@ /// import { + ChangeDetectorRef, Component, ElementRef, - EventEmitter, - Input, + EventEmitter, Injector, + Input, OnChanges, OnDestroy, OnInit, Output, ViewEncapsulation } from '@angular/core'; import { TripTimelineSettings } from '@home/components/widget/lib/maps/models/map.models'; +import { DateFormatProcessor } from '@shared/models/widget-settings.models'; @Component({ selector: 'tb-map-timeline-panel', @@ -32,7 +34,7 @@ import { TripTimelineSettings } from '@home/components/widget/lib/maps/models/ma styleUrls: ['./map-timeline-panel.component.scss'], encapsulation: ViewEncapsulation.None }) -export class MapTimelinePanelComponent implements OnInit, OnDestroy { +export class MapTimelinePanelComponent implements OnInit, OnChanges, OnDestroy { @Input() settings: TripTimelineSettings; @@ -51,18 +53,42 @@ export class MapTimelinePanelComponent implements OnInit, OnDestroy { currentTime = 0; - constructor(public element: ElementRef) { + timestampFormat: DateFormatProcessor; + + speed: number; + + constructor(public element: ElementRef, + private cd: ChangeDetectorRef, + private injector: Injector) { } ngOnInit() { + if (this.settings.showTimestamp) { + this.timestampFormat = DateFormatProcessor.fromSettings(this.injector, this.settings.timestampFormat); + this.timestampFormat.update(this.currentTime); + } + this.speed = this.settings.speedOptions[0]; + } + + ngOnChanges() { + this.currentTime = this.min === Infinity ? 0 : this.min; + if (this.settings.showTimestamp) { + this.timestampFormat.update(this.currentTime); + this.cd.markForCheck(); + } } ngOnDestroy() { } public onTimeChange() { - //this.updateValueText(); + if (this.settings.showTimestamp) { + this.timestampFormat.update(this.currentTime); + this.cd.markForCheck(); + } this.timeChanged.next(this.currentTime); } + public speedUpdated() {} + } diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/map/data-layer-pattern-settings.component.html b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/map/data-layer-pattern-settings.component.html index 71dbecab18..4286bf1c22 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/map/data-layer-pattern-settings.component.html +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/map/data-layer-pattern-settings.component.html @@ -23,7 +23,7 @@
- {{ (patternType === 'label' ? 'widgets.maps.data-layer.label' : 'widgets.maps.data-layer.tooltip') | translate }} + {{ patternTitle ? patternTitle : ((patternType === 'label' ? 'widgets.maps.data-layer.label' : 'widgets.maps.data-layer.tooltip') | translate) }} {{ 'widgets.maps.data-layer.pattern-type-pattern' | translate }} diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/map/data-layer-pattern-settings.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/map/data-layer-pattern-settings.component.ts index 9a8835fa7b..e13e7eec1c 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/map/data-layer-pattern-settings.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/map/data-layer-pattern-settings.component.ts @@ -71,6 +71,9 @@ export class DataLayerPatternSettingsComponent implements OnInit, ControlValueAc @Input() patternType: 'label' | 'tooltip' = 'label'; + @Input() + patternTitle: string; + @Input() @coerceBoolean() hasTooltipOffset = false; diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/map/map-action-button-row.component.html b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/map/map-action-button-row.component.html index 70cc12dd27..8bd57487b0 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/map/map-action-button-row.component.html +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/map/map-action-button-row.component.html @@ -1,6 +1,6 @@ + + +
+ + + + + {{ 'widgets.maps.timeline.control-panel' | translate }} + + + + +
+
widgets.maps.timeline.time-step
+ + + ms + +
+
+
widgets.maps.timeline.speed-options
+ + + + {{ speed }} + + + +
+
+ + {{ 'widgets.maps.timeline.timestamp' | translate }} + + +
+
+ + + + + {{ 'widgets.maps.timeline.snap-to-real-location' | translate }} + + + + + + + + +
+
+
+
+
diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/map/trip-timeline-settings.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/map/trip-timeline-settings.component.ts new file mode 100644 index 0000000000..fd9afb8740 --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/map/trip-timeline-settings.component.ts @@ -0,0 +1,172 @@ +/// +/// 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, forwardRef, Input, OnInit } from '@angular/core'; +import { + ControlValueAccessor, + NG_VALIDATORS, + NG_VALUE_ACCESSOR, + UntypedFormBuilder, + UntypedFormControl, + UntypedFormGroup, + Validator, + Validators +} from '@angular/forms'; +import { merge } from 'rxjs'; +import { WidgetService } from '@core/http/widget.service'; +import { takeUntilDestroyed } from '@angular/core/rxjs-interop'; +import { + DataLayerPatternSettings, + DataLayerPatternType, + DataLayerTooltipSettings, + dataLayerTooltipTriggers, + dataLayerTooltipTriggerTranslationMap, + pathDecoratorSymbols, pathDecoratorSymbolTranslationMap, + TripTimelineSettings +} from '@home/components/widget/lib/maps/models/map.models'; +import { coerceBoolean } from '@shared/decorators/coercion'; +import { MapSettingsContext } from '@home/components/widget/lib/settings/common/map/map-settings.component.models'; + +@Component({ + selector: 'tb-trip-timeline-settings', + templateUrl: './trip-timeline-settings.component.html', + styleUrls: ['./../../widget-settings.scss'], + providers: [ + { + provide: NG_VALUE_ACCESSOR, + useExisting: forwardRef(() => TripTimelineSettingsComponent), + multi: true + }, + { + provide: NG_VALIDATORS, + useExisting: forwardRef(() => TripTimelineSettingsComponent), + multi: true + } + ] +}) +export class TripTimelineSettingsComponent implements OnInit, ControlValueAccessor, Validator { + + settingsExpanded = false; + + functionScopeVariables = this.widgetService.getWidgetScopeVariables(); + + @Input() + disabled: boolean; + + private modelValue: TripTimelineSettings; + + private propagateChange = null; + + public tripTimelineSettingsFormGroup: UntypedFormGroup; + + constructor(private fb: UntypedFormBuilder, + private widgetService: WidgetService, + private destroyRef: DestroyRef) { + } + + ngOnInit(): void { + + this.tripTimelineSettingsFormGroup = this.fb.group({ + showTimelineControl: [null], + timeStep: [null, [Validators.required, Validators.min(1)]], + speedOptions: [null, [Validators.required]], + showTimestamp: [null], + timestampFormat: [null, [Validators.required]], + snapToRealLocation: [null], + locationSnapFilter: [null, [Validators.required]] + }); + + this.tripTimelineSettingsFormGroup.valueChanges.pipe( + takeUntilDestroyed(this.destroyRef) + ).subscribe(() => { + this.updateModel(); + }); + merge(this.tripTimelineSettingsFormGroup.get('showTimelineControl').valueChanges, + this.tripTimelineSettingsFormGroup.get('showTimestamp').valueChanges, + this.tripTimelineSettingsFormGroup.get('snapToRealLocation').valueChanges + ).pipe( + takeUntilDestroyed(this.destroyRef) + ).subscribe(() => { + this.updateValidators(); + }); + } + + registerOnChange(fn: any): void { + this.propagateChange = fn; + } + + registerOnTouched(_fn: any): void { + } + + setDisabledState(isDisabled: boolean): void { + this.disabled = isDisabled; + if (isDisabled) { + this.tripTimelineSettingsFormGroup.disable({emitEvent: false}); + } else { + this.tripTimelineSettingsFormGroup.enable({emitEvent: false}); + this.updateValidators(); + } + } + + writeValue(value: TripTimelineSettings): void { + this.modelValue = value; + this.tripTimelineSettingsFormGroup.patchValue( + value, {emitEvent: false} + ); + this.updateValidators(); + this.settingsExpanded = this.tripTimelineSettingsFormGroup.get('showTimelineControl').value; + this.tripTimelineSettingsFormGroup.get('showTimelineControl').valueChanges.pipe( + takeUntilDestroyed(this.destroyRef) + ).subscribe((show) => { + this.settingsExpanded = show; + }); + } + + public validate(c: UntypedFormControl) { + const valid = this.tripTimelineSettingsFormGroup.valid; + return valid ? null : { + tripTimelineSettings: { + valid: false, + }, + }; + } + + private updateValidators() { + const showTimelineControl: boolean = this.tripTimelineSettingsFormGroup.get('showTimelineControl').value; + const showTimestamp: boolean = this.tripTimelineSettingsFormGroup.get('showTimestamp').value; + const snapToRealLocation: boolean = this.tripTimelineSettingsFormGroup.get('snapToRealLocation').value; + if (showTimelineControl) { + this.tripTimelineSettingsFormGroup.enable({emitEvent: false}); + if (!showTimestamp) { + this.tripTimelineSettingsFormGroup.get('timestampFormat').disable({emitEvent: false}); + } + if (!snapToRealLocation) { + this.tripTimelineSettingsFormGroup.get('locationSnapFilter').disable({emitEvent: false}); + } + } else { + this.tripTimelineSettingsFormGroup.disable({emitEvent: false}); + this.tripTimelineSettingsFormGroup.get('showTimelineControl').enable({emitEvent: false}); + } + } + + private updateModel() { + this.modelValue = this.tripTimelineSettingsFormGroup.getRawValue(); + this.propagateChange(this.modelValue); + } + + protected readonly pathDecoratorSymbols = pathDecoratorSymbols; + protected readonly pathDecoratorSymbolTranslationMap = pathDecoratorSymbolTranslationMap; +} 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 536623e86b..ef7dca9a11 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 @@ -242,6 +242,9 @@ import { import { MapActionButtonRowComponent } from '@home/components/widget/lib/settings/common/map/map-action-button-row.component'; +import { + TripTimelineSettingsComponent +} from '@home/components/widget/lib/settings/common/map/trip-timeline-settings.component'; @NgModule({ declarations: [ @@ -332,6 +335,7 @@ import { MapDataLayersComponent, MapDataSourceRowComponent, MapDataSourcesComponent, + TripTimelineSettingsComponent, MapSettingsComponent, EntityAliasSelectComponent, FilterSelectComponent, diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/map/map-widget-settings.component.html b/ui-ngx/src/app/modules/home/components/widget/lib/settings/map/map-widget-settings.component.html index 870c1f6deb..f58e1d65ab 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/settings/map/map-widget-settings.component.html +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/map/map-widget-settings.component.html @@ -17,6 +17,7 @@ --> , private fb: UntypedFormBuilder) { super(store); @@ -40,6 +43,13 @@ export class MapWidgetSettingsComponent extends WidgetSettingsComponent { return this.mapWidgetSettingsForm; } + protected onWidgetConfigSet(widgetConfig: WidgetConfigComponentData) { + const params = widgetConfig.typeParameters as any; + if (isDefinedAndNotNull(params.trip)) { + this.trip = params.trip === true; + } + } + protected defaultSettings(): WidgetSettings { return mergeDeepIgnoreArray({} as MapWidgetSettings, mapWidgetDefaultSettings); } 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 cc6acce2b2..39156e001b 100644 --- a/ui-ngx/src/assets/locale/locale.constant-en_US.json +++ b/ui-ngx/src/assets/locale/locale.constant-en_US.json @@ -7706,6 +7706,14 @@ "zoom-double-click": "Double click", "zoom-control-buttons": "Control buttons" }, + "timeline": { + "control-panel": "Timeline control panel", + "time-step": "Time step", + "speed-options": "Speed options", + "timestamp": "Timestamp", + "snap-to-real-location": "Snap to real location", + "location-snap-filter-function": "Location snap filter function" + }, "map-action": { "map-action-buttons": "Map action buttons", "label": "Label", @@ -7779,6 +7787,7 @@ }, "overlays": { "overlays": "Overlays", + "trips": "Trips", "markers": "Markers", "polygons": "Polygons", "circles": "Circles" @@ -7827,7 +7836,14 @@ "action-remove": "Remove", "edit-instruments": "Instruments", "enable-snapping": "Enable snapping to other vertices for precision drawing", + "trip": { + "no-trips": "No trips configured", + "add-trip": "Add trip", + "trip-configuration": "Trip configuration", + "remove-trip": "Remove trip" + }, "marker": { + "marker": "Marker", "latitude-key": "Latitude key", "longitude-key": "Longitude key", "x-pos-key": "X position key", @@ -7857,6 +7873,8 @@ "marker-offset": "Marker offset", "offset-horizontal": "Horizontal", "offset-vertical": "Vertical", + "rotate-marker": "Rotate marker", + "offset-angle": "Offset angle", "position-conversion": "Position conversion", "position-conversion-function": "Position conversion function, should return x,y coordinates as double from 0 to 1 each", "clustering": { @@ -7880,8 +7898,19 @@ "place-marker-hint-with-entity": "Click to place '{{entityName}}' entity" }, "path": { + "path": "Path", + "path-decorator": "Path decorator", + "decorator-symbol": "Decorator symbol", "decorator-symbol-arrow-head": "Arrow", - "decorator-symbol-dash": "Dash" + "decorator-symbol-dash": "Dash", + "decorator-arrangement": "Decorator arrangement", + "decorator-offset": "Start", + "decorator-end-offset": "End", + "decorator-repeat": "Repeat" + }, + "points": { + "points": "Points", + "point-tooltip": "Point tooltip" }, "polygon": { "polygon-key": "Polygon key", diff --git a/ui-ngx/src/assets/markers/tripShape1.svg b/ui-ngx/src/assets/markers/tripShape1.svg new file mode 100644 index 0000000000..da98975b52 --- /dev/null +++ b/ui-ngx/src/assets/markers/tripShape1.svg @@ -0,0 +1,4 @@ + + + + diff --git a/ui-ngx/src/assets/markers/tripShape10.svg b/ui-ngx/src/assets/markers/tripShape10.svg new file mode 100644 index 0000000000..507d3725de --- /dev/null +++ b/ui-ngx/src/assets/markers/tripShape10.svg @@ -0,0 +1,10 @@ + + + + + + + + + + diff --git a/ui-ngx/src/assets/markers/tripShape2.svg b/ui-ngx/src/assets/markers/tripShape2.svg new file mode 100644 index 0000000000..0dfc4e22a1 --- /dev/null +++ b/ui-ngx/src/assets/markers/tripShape2.svg @@ -0,0 +1,15 @@ + + + + + + + + + + + + + + diff --git a/ui-ngx/src/assets/markers/tripShape3.svg b/ui-ngx/src/assets/markers/tripShape3.svg new file mode 100644 index 0000000000..e49d699ad2 --- /dev/null +++ b/ui-ngx/src/assets/markers/tripShape3.svg @@ -0,0 +1,9 @@ + + + + + + + + + diff --git a/ui-ngx/src/assets/markers/tripShape4.svg b/ui-ngx/src/assets/markers/tripShape4.svg new file mode 100644 index 0000000000..f4b48da113 --- /dev/null +++ b/ui-ngx/src/assets/markers/tripShape4.svg @@ -0,0 +1,9 @@ + + + + + + + + + diff --git a/ui-ngx/src/assets/markers/tripShape5.svg b/ui-ngx/src/assets/markers/tripShape5.svg new file mode 100644 index 0000000000..a88d26035d --- /dev/null +++ b/ui-ngx/src/assets/markers/tripShape5.svg @@ -0,0 +1,10 @@ + + + + + + + + + + diff --git a/ui-ngx/src/assets/markers/tripShape6.svg b/ui-ngx/src/assets/markers/tripShape6.svg new file mode 100644 index 0000000000..23105baba5 --- /dev/null +++ b/ui-ngx/src/assets/markers/tripShape6.svg @@ -0,0 +1,10 @@ + + + + + + + + + + diff --git a/ui-ngx/src/assets/markers/tripShape7.svg b/ui-ngx/src/assets/markers/tripShape7.svg new file mode 100644 index 0000000000..342c9db364 --- /dev/null +++ b/ui-ngx/src/assets/markers/tripShape7.svg @@ -0,0 +1,11 @@ + + + + + + + + + + + diff --git a/ui-ngx/src/assets/markers/tripShape8.svg b/ui-ngx/src/assets/markers/tripShape8.svg new file mode 100644 index 0000000000..f8edbe4dc0 --- /dev/null +++ b/ui-ngx/src/assets/markers/tripShape8.svg @@ -0,0 +1,9 @@ + + + + + + + + + diff --git a/ui-ngx/src/assets/markers/tripShape9.svg b/ui-ngx/src/assets/markers/tripShape9.svg new file mode 100644 index 0000000000..9494a976f0 --- /dev/null +++ b/ui-ngx/src/assets/markers/tripShape9.svg @@ -0,0 +1,10 @@ + + + + + + + + + + From aef85444b64420750ba6b1561d030272b91e8f16 Mon Sep 17 00:00:00 2001 From: Artem Dzhereleiko Date: Fri, 28 Feb 2025 10:03:23 +0200 Subject: [PATCH 120/345] UI: Remove key and value serializer form kafka node --- .../rule-node/external/kafka-config.component.html | 14 -------------- .../rule-node/external/kafka-config.component.ts | 2 -- .../src/assets/locale/locale.constant-en_US.json | 4 ---- 3 files changed, 20 deletions(-) diff --git a/ui-ngx/src/app/modules/home/components/rule-node/external/kafka-config.component.html b/ui-ngx/src/app/modules/home/components/rule-node/external/kafka-config.component.html index 3aa743247f..1132c9982c 100644 --- a/ui-ngx/src/app/modules/home/components/rule-node/external/kafka-config.component.html +++ b/ui-ngx/src/app/modules/home/components/rule-node/external/kafka-config.component.html @@ -73,20 +73,6 @@ - - rule-node-config.key-serializer - - - {{ 'rule-node-config.key-serializer-required' | translate }} - - - - rule-node-config.value-serializer - - - {{ 'rule-node-config.value-serializer-required' | translate }} - - Date: Fri, 28 Feb 2025 11:36:42 +0200 Subject: [PATCH 121/345] Save time series strategies: ensure Device State Service is notified about inactivity timeout updates --- .../server/actors/ActorSystemContext.java | 4 +- .../actors/ruleChain/DefaultTbContext.java | 4 +- .../queue/DefaultTbCoreConsumerService.java | 18 ++ .../service/queue/TbCoreConsumerStats.java | 8 + .../state/DefaultDeviceStateManager.java | 180 ++++++++++++++++ .../DefaultRuleEngineDeviceStateManager.java | 196 ------------------ .../DefaultSubscriptionManagerService.java | 3 - .../DefaultTelemetrySubscriptionService.java | 36 +++- ...ava => DefaultDeviceStateManagerTest.java} | 36 ++-- ...faultTelemetrySubscriptionServiceTest.java | 5 +- common/proto/src/main/proto/queue.proto | 9 + .../thingsboard/common/util/DonAsynchron.java | 6 + ...teManager.java => DeviceStateManager.java} | 4 +- .../rule/engine/api/TbContext.java | 2 +- .../rule/engine/action/TbDeviceStateNode.java | 4 +- .../engine/action/TbDeviceStateNodeTest.java | 14 +- 16 files changed, 295 insertions(+), 234 deletions(-) create mode 100644 application/src/main/java/org/thingsboard/server/service/state/DefaultDeviceStateManager.java delete mode 100644 application/src/main/java/org/thingsboard/server/service/state/DefaultRuleEngineDeviceStateManager.java rename application/src/test/java/org/thingsboard/server/service/state/{DefaultRuleEngineDeviceStateManagerTest.java => DefaultDeviceStateManagerTest.java} (81%) rename rule-engine/rule-engine-api/src/main/java/org/thingsboard/rule/engine/api/{RuleEngineDeviceStateManager.java => DeviceStateManager.java} (88%) diff --git a/application/src/main/java/org/thingsboard/server/actors/ActorSystemContext.java b/application/src/main/java/org/thingsboard/server/actors/ActorSystemContext.java index eb76473386..1cec80dffc 100644 --- a/application/src/main/java/org/thingsboard/server/actors/ActorSystemContext.java +++ b/application/src/main/java/org/thingsboard/server/actors/ActorSystemContext.java @@ -33,7 +33,7 @@ import org.springframework.stereotype.Component; import org.thingsboard.common.util.JacksonUtil; import org.thingsboard.rule.engine.api.MailService; import org.thingsboard.rule.engine.api.NotificationCenter; -import org.thingsboard.rule.engine.api.RuleEngineDeviceStateManager; +import org.thingsboard.rule.engine.api.DeviceStateManager; import org.thingsboard.rule.engine.api.SmsService; import org.thingsboard.rule.engine.api.notification.SlackService; import org.thingsboard.rule.engine.api.sms.SmsSenderFactory; @@ -206,7 +206,7 @@ public class ActorSystemContext { @Autowired(required = false) @Getter - private RuleEngineDeviceStateManager deviceStateManager; + private DeviceStateManager deviceStateManager; @Autowired @Getter diff --git a/application/src/main/java/org/thingsboard/server/actors/ruleChain/DefaultTbContext.java b/application/src/main/java/org/thingsboard/server/actors/ruleChain/DefaultTbContext.java index a1e8434b6e..64afa3eca0 100644 --- a/application/src/main/java/org/thingsboard/server/actors/ruleChain/DefaultTbContext.java +++ b/application/src/main/java/org/thingsboard/server/actors/ruleChain/DefaultTbContext.java @@ -29,7 +29,7 @@ import org.thingsboard.rule.engine.api.RuleEngineAlarmService; import org.thingsboard.rule.engine.api.RuleEngineApiUsageStateService; import org.thingsboard.rule.engine.api.RuleEngineAssetProfileCache; import org.thingsboard.rule.engine.api.RuleEngineDeviceProfileCache; -import org.thingsboard.rule.engine.api.RuleEngineDeviceStateManager; +import org.thingsboard.rule.engine.api.DeviceStateManager; import org.thingsboard.rule.engine.api.RuleEngineRpcService; import org.thingsboard.rule.engine.api.RuleEngineTelemetryService; import org.thingsboard.rule.engine.api.ScriptEngine; @@ -724,7 +724,7 @@ public class DefaultTbContext implements TbContext { } @Override - public RuleEngineDeviceStateManager getDeviceStateManager() { + public DeviceStateManager getDeviceStateManager() { return mainCtx.getDeviceStateManager(); } diff --git a/application/src/main/java/org/thingsboard/server/service/queue/DefaultTbCoreConsumerService.java b/application/src/main/java/org/thingsboard/server/service/queue/DefaultTbCoreConsumerService.java index 586902d542..f9f4e86773 100644 --- a/application/src/main/java/org/thingsboard/server/service/queue/DefaultTbCoreConsumerService.java +++ b/application/src/main/java/org/thingsboard/server/service/queue/DefaultTbCoreConsumerService.java @@ -289,6 +289,9 @@ public class DefaultTbCoreConsumerService extends AbstractConsumerService future = deviceActivityEventsExecutor.submit(() -> stateService.onDeviceInactivityTimeoutUpdate(tenantId, deviceId, deviceInactivityTimeoutUpdateMsg.getInactivityTimeout())); + DonAsynchron.withCallback(future, + __ -> callback.onSuccess(), + t -> { + log.warn("[{}] Failed to process device inactivity timeout update message for device [{}]", tenantId.getId(), deviceId.getId(), t); + callback.onFailure(t); + }); + } + private void forwardToNotificationSchedulerService(TransportProtos.NotificationSchedulerServiceMsg msg, TbCallback callback) { TenantId tenantId = toTenantId(msg.getTenantIdMSB(), msg.getTenantIdLSB()); NotificationRequestId notificationRequestId = new NotificationRequestId(new UUID(msg.getRequestIdMSB(), msg.getRequestIdLSB())); diff --git a/application/src/main/java/org/thingsboard/server/service/queue/TbCoreConsumerStats.java b/application/src/main/java/org/thingsboard/server/service/queue/TbCoreConsumerStats.java index 728f18bfff..46a42284b4 100644 --- a/application/src/main/java/org/thingsboard/server/service/queue/TbCoreConsumerStats.java +++ b/application/src/main/java/org/thingsboard/server/service/queue/TbCoreConsumerStats.java @@ -40,6 +40,7 @@ public class TbCoreConsumerStats { public static final String DEVICE_ACTIVITIES = "deviceActivity"; public static final String DEVICE_DISCONNECTS = "deviceDisconnect"; public static final String DEVICE_INACTIVITIES = "deviceInactivity"; + public static final String DEVICE_INACTIVITY_TIMEOUT_UPDATES = "deviceInactivityTimeoutUpdate"; public static final String TO_CORE_NF_OTHER = "coreNfOther"; // normally, there is no messages when codebase is fine public static final String TO_CORE_NF_COMPONENT_LIFECYCLE = "coreNfCompLfcl"; @@ -65,6 +66,7 @@ public class TbCoreConsumerStats { private final StatsCounter deviceActivitiesCounter; private final StatsCounter deviceDisconnectsCounter; private final StatsCounter deviceInactivitiesCounter; + private final StatsCounter deviceInactivityTimeoutUpdatesCounter; private final StatsCounter toCoreNfOtherCounter; private final StatsCounter toCoreNfComponentLifecycleCounter; @@ -95,6 +97,7 @@ public class TbCoreConsumerStats { this.deviceActivitiesCounter = register(statsFactory.createStatsCounter(statsKey, DEVICE_ACTIVITIES)); this.deviceDisconnectsCounter = register(statsFactory.createStatsCounter(statsKey, DEVICE_DISCONNECTS)); this.deviceInactivitiesCounter = register(statsFactory.createStatsCounter(statsKey, DEVICE_INACTIVITIES)); + this.deviceInactivityTimeoutUpdatesCounter = register(statsFactory.createStatsCounter(statsKey, DEVICE_INACTIVITY_TIMEOUT_UPDATES)); // Core notification counters this.toCoreNfOtherCounter = register(statsFactory.createStatsCounter(statsKey, TO_CORE_NF_OTHER)); @@ -163,6 +166,11 @@ public class TbCoreConsumerStats { deviceInactivitiesCounter.increment(); } + public void log(TransportProtos.DeviceInactivityTimeoutUpdateProto msg) { + totalCounter.increment(); + deviceInactivityTimeoutUpdatesCounter.increment(); + } + public void log(TransportProtos.SubscriptionMgrMsgProto msg) { totalCounter.increment(); subscriptionMsgCounter.increment(); diff --git a/application/src/main/java/org/thingsboard/server/service/state/DefaultDeviceStateManager.java b/application/src/main/java/org/thingsboard/server/service/state/DefaultDeviceStateManager.java new file mode 100644 index 0000000000..26750e887e --- /dev/null +++ b/application/src/main/java/org/thingsboard/server/service/state/DefaultDeviceStateManager.java @@ -0,0 +1,180 @@ +/** + * 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.state; + +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Service; +import org.thingsboard.rule.engine.api.DeviceStateManager; +import org.thingsboard.server.cluster.TbClusterService; +import org.thingsboard.server.common.data.id.DeviceId; +import org.thingsboard.server.common.data.id.TenantId; +import org.thingsboard.server.common.msg.queue.ServiceType; +import org.thingsboard.server.common.msg.queue.TbCallback; +import org.thingsboard.server.common.msg.queue.TopicPartitionInfo; +import org.thingsboard.server.gen.transport.TransportProtos; +import org.thingsboard.server.queue.common.SimpleTbQueueCallback; +import org.thingsboard.server.queue.discovery.PartitionService; +import org.thingsboard.server.queue.discovery.TbServiceInfoProvider; + +import java.util.Optional; +import java.util.function.Consumer; +import java.util.function.Supplier; + +@Slf4j +@Service +@RequiredArgsConstructor +public class DefaultDeviceStateManager implements DeviceStateManager { + + private final TbServiceInfoProvider serviceInfoProvider; + private final PartitionService partitionService; + + private final Optional deviceStateService; + private final TbClusterService clusterService; + + @Override + public void onDeviceConnect(TenantId tenantId, DeviceId deviceId, long connectTime, TbCallback callback) { + forwardToDeviceStateService(tenantId, deviceId, + deviceStateService -> { + log.debug("[{}][{}] Forwarding device connect event to local service. Connect time: [{}].", tenantId.getId(), deviceId.getId(), connectTime); + deviceStateService.onDeviceConnect(tenantId, deviceId, connectTime); + }, + () -> { + log.debug("[{}][{}] Sending device connect message to core. Connect time: [{}].", tenantId.getId(), deviceId.getId(), connectTime); + var deviceConnectMsg = TransportProtos.DeviceConnectProto.newBuilder() + .setTenantIdMSB(tenantId.getId().getMostSignificantBits()) + .setTenantIdLSB(tenantId.getId().getLeastSignificantBits()) + .setDeviceIdMSB(deviceId.getId().getMostSignificantBits()) + .setDeviceIdLSB(deviceId.getId().getLeastSignificantBits()) + .setLastConnectTime(connectTime) + .build(); + return TransportProtos.ToCoreMsg.newBuilder() + .setDeviceConnectMsg(deviceConnectMsg) + .build(); + }, callback); + } + + @Override + public void onDeviceActivity(TenantId tenantId, DeviceId deviceId, long activityTime, TbCallback callback) { + forwardToDeviceStateService(tenantId, deviceId, + deviceStateService -> { + log.debug("[{}][{}] Forwarding device activity event to local service. Activity time: [{}].", tenantId.getId(), deviceId.getId(), activityTime); + deviceStateService.onDeviceActivity(tenantId, deviceId, activityTime); + }, + () -> { + log.debug("[{}][{}] Sending device activity message to core. Activity time: [{}].", tenantId.getId(), deviceId.getId(), activityTime); + var deviceActivityMsg = TransportProtos.DeviceActivityProto.newBuilder() + .setTenantIdMSB(tenantId.getId().getMostSignificantBits()) + .setTenantIdLSB(tenantId.getId().getLeastSignificantBits()) + .setDeviceIdMSB(deviceId.getId().getMostSignificantBits()) + .setDeviceIdLSB(deviceId.getId().getLeastSignificantBits()) + .setLastActivityTime(activityTime) + .build(); + return TransportProtos.ToCoreMsg.newBuilder() + .setDeviceActivityMsg(deviceActivityMsg) + .build(); + }, callback); + } + + @Override + public void onDeviceDisconnect(TenantId tenantId, DeviceId deviceId, long disconnectTime, TbCallback callback) { + forwardToDeviceStateService(tenantId, deviceId, + deviceStateService -> { + log.debug("[{}][{}] Forwarding device disconnect event to local service. Disconnect time: [{}].", tenantId.getId(), deviceId.getId(), disconnectTime); + deviceStateService.onDeviceDisconnect(tenantId, deviceId, disconnectTime); + }, + () -> { + log.debug("[{}][{}] Sending device disconnect message to core. Disconnect time: [{}].", tenantId.getId(), deviceId.getId(), disconnectTime); + var deviceDisconnectMsg = TransportProtos.DeviceDisconnectProto.newBuilder() + .setTenantIdMSB(tenantId.getId().getMostSignificantBits()) + .setTenantIdLSB(tenantId.getId().getLeastSignificantBits()) + .setDeviceIdMSB(deviceId.getId().getMostSignificantBits()) + .setDeviceIdLSB(deviceId.getId().getLeastSignificantBits()) + .setLastDisconnectTime(disconnectTime) + .build(); + return TransportProtos.ToCoreMsg.newBuilder() + .setDeviceDisconnectMsg(deviceDisconnectMsg) + .build(); + }, callback); + } + + @Override + public void onDeviceInactivity(TenantId tenantId, DeviceId deviceId, long inactivityTime, TbCallback callback) { + forwardToDeviceStateService(tenantId, deviceId, + deviceStateService -> { + log.debug("[{}][{}] Forwarding device inactivity event to local service. Inactivity time: [{}].", tenantId.getId(), deviceId.getId(), inactivityTime); + deviceStateService.onDeviceInactivity(tenantId, deviceId, inactivityTime); + }, + () -> { + log.debug("[{}][{}] Sending device inactivity message to core. Inactivity time: [{}].", tenantId.getId(), deviceId.getId(), inactivityTime); + var deviceInactivityMsg = TransportProtos.DeviceInactivityProto.newBuilder() + .setTenantIdMSB(tenantId.getId().getMostSignificantBits()) + .setTenantIdLSB(tenantId.getId().getLeastSignificantBits()) + .setDeviceIdMSB(deviceId.getId().getMostSignificantBits()) + .setDeviceIdLSB(deviceId.getId().getLeastSignificantBits()) + .setLastInactivityTime(inactivityTime) + .build(); + return TransportProtos.ToCoreMsg.newBuilder() + .setDeviceInactivityMsg(deviceInactivityMsg) + .build(); + }, callback); + } + + @Override + public void onDeviceInactivityTimeoutUpdate(TenantId tenantId, DeviceId deviceId, long inactivityTimeout, TbCallback callback) { + forwardToDeviceStateService(tenantId, deviceId, + deviceStateService -> { + log.debug("[{}][{}] Forwarding device inactivity timeout update to local service. Updated inactivity timeout: [{}].", tenantId.getId(), deviceId.getId(), inactivityTimeout); + deviceStateService.onDeviceInactivityTimeoutUpdate(tenantId, deviceId, inactivityTimeout); + }, + () -> { + log.debug("[{}][{}] Sending device inactivity timeout update message to core. Updated inactivity timeout: [{}].", tenantId.getId(), deviceId.getId(), inactivityTimeout); + var deviceInactivityTimeoutUpdateMsg = TransportProtos.DeviceInactivityTimeoutUpdateProto.newBuilder() + .setTenantIdMSB(tenantId.getId().getMostSignificantBits()) + .setTenantIdLSB(tenantId.getId().getLeastSignificantBits()) + .setDeviceIdMSB(deviceId.getId().getMostSignificantBits()) + .setDeviceIdLSB(deviceId.getId().getLeastSignificantBits()) + .setInactivityTimeout(inactivityTimeout) + .build(); + return TransportProtos.ToCoreMsg.newBuilder() + .setDeviceInactivityTimeoutUpdateMsg(deviceInactivityTimeoutUpdateMsg) + .build(); + }, callback); + } + + private void forwardToDeviceStateService( + TenantId tenantId, DeviceId deviceId, + Consumer toDeviceStateService, + Supplier toCore, + TbCallback callback + ) { + TopicPartitionInfo tpi = partitionService.resolve(ServiceType.TB_CORE, tenantId, deviceId); + if (serviceInfoProvider.isService(ServiceType.TB_CORE) && tpi.isMyPartition() && deviceStateService.isPresent()) { + try { + toDeviceStateService.accept(deviceStateService.get()); + } catch (Exception e) { + log.error("[{}][{}] Failed to process device connectivity event.", tenantId.getId(), deviceId.getId(), e); + callback.onFailure(e); + return; + } + callback.onSuccess(); + } else { + TransportProtos.ToCoreMsg toCoreMsg = toCore.get(); + clusterService.pushMsgToCore(tpi, deviceId.getId(), toCoreMsg, new SimpleTbQueueCallback(__ -> callback.onSuccess(), callback::onFailure)); + } + } + +} diff --git a/application/src/main/java/org/thingsboard/server/service/state/DefaultRuleEngineDeviceStateManager.java b/application/src/main/java/org/thingsboard/server/service/state/DefaultRuleEngineDeviceStateManager.java deleted file mode 100644 index b064c1e8c6..0000000000 --- a/application/src/main/java/org/thingsboard/server/service/state/DefaultRuleEngineDeviceStateManager.java +++ /dev/null @@ -1,196 +0,0 @@ -/** - * 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.state; - -import lombok.Getter; -import lombok.extern.slf4j.Slf4j; -import org.springframework.stereotype.Service; -import org.thingsboard.rule.engine.api.RuleEngineDeviceStateManager; -import org.thingsboard.server.cluster.TbClusterService; -import org.thingsboard.server.common.data.id.DeviceId; -import org.thingsboard.server.common.data.id.TenantId; -import org.thingsboard.server.common.msg.queue.ServiceType; -import org.thingsboard.server.common.msg.queue.TbCallback; -import org.thingsboard.server.common.msg.queue.TopicPartitionInfo; -import org.thingsboard.server.gen.transport.TransportProtos; -import org.thingsboard.server.queue.common.SimpleTbQueueCallback; -import org.thingsboard.server.queue.discovery.PartitionService; -import org.thingsboard.server.queue.discovery.TbServiceInfoProvider; -import org.thingsboard.server.queue.util.TbRuleEngineComponent; - -import java.util.Optional; -import java.util.UUID; - -@Slf4j -@Service -@TbRuleEngineComponent -public class DefaultRuleEngineDeviceStateManager implements RuleEngineDeviceStateManager { - - private final TbServiceInfoProvider serviceInfoProvider; - private final PartitionService partitionService; - - private final Optional deviceStateService; - private final TbClusterService clusterService; - - public DefaultRuleEngineDeviceStateManager( - TbServiceInfoProvider serviceInfoProvider, PartitionService partitionService, - Optional deviceStateServiceOptional, TbClusterService clusterService - ) { - this.serviceInfoProvider = serviceInfoProvider; - this.partitionService = partitionService; - this.deviceStateService = deviceStateServiceOptional; - this.clusterService = clusterService; - } - - @Getter - private abstract static class ConnectivityEventInfo { - - private final TenantId tenantId; - private final DeviceId deviceId; - private final long eventTime; - - private ConnectivityEventInfo(TenantId tenantId, DeviceId deviceId, long eventTime) { - this.tenantId = tenantId; - this.deviceId = deviceId; - this.eventTime = eventTime; - } - - abstract void forwardToLocalService(); - - abstract TransportProtos.ToCoreMsg toQueueMsg(); - - } - - @Override - public void onDeviceConnect(TenantId tenantId, DeviceId deviceId, long connectTime, TbCallback callback) { - routeEvent(new ConnectivityEventInfo(tenantId, deviceId, connectTime) { - @Override - void forwardToLocalService() { - deviceStateService.ifPresent(service -> service.onDeviceConnect(tenantId, deviceId, connectTime)); - } - - @Override - TransportProtos.ToCoreMsg toQueueMsg() { - var deviceConnectMsg = TransportProtos.DeviceConnectProto.newBuilder() - .setTenantIdMSB(tenantId.getId().getMostSignificantBits()) - .setTenantIdLSB(tenantId.getId().getLeastSignificantBits()) - .setDeviceIdMSB(deviceId.getId().getMostSignificantBits()) - .setDeviceIdLSB(deviceId.getId().getLeastSignificantBits()) - .setLastConnectTime(connectTime) - .build(); - return TransportProtos.ToCoreMsg.newBuilder() - .setDeviceConnectMsg(deviceConnectMsg) - .build(); - } - }, callback); - } - - @Override - public void onDeviceActivity(TenantId tenantId, DeviceId deviceId, long activityTime, TbCallback callback) { - routeEvent(new ConnectivityEventInfo(tenantId, deviceId, activityTime) { - @Override - void forwardToLocalService() { - deviceStateService.ifPresent(service -> service.onDeviceActivity(tenantId, deviceId, activityTime)); - } - - @Override - TransportProtos.ToCoreMsg toQueueMsg() { - var deviceActivityMsg = TransportProtos.DeviceActivityProto.newBuilder() - .setTenantIdMSB(tenantId.getId().getMostSignificantBits()) - .setTenantIdLSB(tenantId.getId().getLeastSignificantBits()) - .setDeviceIdMSB(deviceId.getId().getMostSignificantBits()) - .setDeviceIdLSB(deviceId.getId().getLeastSignificantBits()) - .setLastActivityTime(activityTime) - .build(); - return TransportProtos.ToCoreMsg.newBuilder() - .setDeviceActivityMsg(deviceActivityMsg) - .build(); - } - }, callback); - } - - @Override - public void onDeviceDisconnect(TenantId tenantId, DeviceId deviceId, long disconnectTime, TbCallback callback) { - routeEvent(new ConnectivityEventInfo(tenantId, deviceId, disconnectTime) { - @Override - void forwardToLocalService() { - deviceStateService.ifPresent(service -> service.onDeviceDisconnect(tenantId, deviceId, disconnectTime)); - } - - @Override - TransportProtos.ToCoreMsg toQueueMsg() { - var deviceDisconnectMsg = TransportProtos.DeviceDisconnectProto.newBuilder() - .setTenantIdMSB(tenantId.getId().getMostSignificantBits()) - .setTenantIdLSB(tenantId.getId().getLeastSignificantBits()) - .setDeviceIdMSB(deviceId.getId().getMostSignificantBits()) - .setDeviceIdLSB(deviceId.getId().getLeastSignificantBits()) - .setLastDisconnectTime(disconnectTime) - .build(); - return TransportProtos.ToCoreMsg.newBuilder() - .setDeviceDisconnectMsg(deviceDisconnectMsg) - .build(); - } - }, callback); - } - - @Override - public void onDeviceInactivity(TenantId tenantId, DeviceId deviceId, long inactivityTime, TbCallback callback) { - routeEvent(new ConnectivityEventInfo(tenantId, deviceId, inactivityTime) { - @Override - void forwardToLocalService() { - deviceStateService.ifPresent(service -> service.onDeviceInactivity(tenantId, deviceId, inactivityTime)); - } - - @Override - TransportProtos.ToCoreMsg toQueueMsg() { - var deviceInactivityMsg = TransportProtos.DeviceInactivityProto.newBuilder() - .setTenantIdMSB(tenantId.getId().getMostSignificantBits()) - .setTenantIdLSB(tenantId.getId().getLeastSignificantBits()) - .setDeviceIdMSB(deviceId.getId().getMostSignificantBits()) - .setDeviceIdLSB(deviceId.getId().getLeastSignificantBits()) - .setLastInactivityTime(inactivityTime) - .build(); - return TransportProtos.ToCoreMsg.newBuilder() - .setDeviceInactivityMsg(deviceInactivityMsg) - .build(); - } - }, callback); - } - - private void routeEvent(ConnectivityEventInfo eventInfo, TbCallback callback) { - var tenantId = eventInfo.getTenantId(); - var deviceId = eventInfo.getDeviceId(); - long eventTime = eventInfo.getEventTime(); - - TopicPartitionInfo tpi = partitionService.resolve(ServiceType.TB_CORE, tenantId, deviceId); - if (serviceInfoProvider.isService(ServiceType.TB_CORE) && tpi.isMyPartition() && deviceStateService.isPresent()) { - log.debug("[{}][{}] Forwarding device connectivity event to local service. Event time: [{}].", tenantId.getId(), deviceId.getId(), eventTime); - try { - eventInfo.forwardToLocalService(); - } catch (Exception e) { - log.error("[{}][{}] Failed to process device connectivity event. Event time: [{}].", tenantId.getId(), deviceId.getId(), eventTime, e); - callback.onFailure(e); - return; - } - callback.onSuccess(); - } else { - TransportProtos.ToCoreMsg msg = eventInfo.toQueueMsg(); - log.debug("[{}][{}] Sending device connectivity message to core. Event time: [{}].", tenantId.getId(), deviceId.getId(), eventTime); - clusterService.pushMsgToCore(tpi, UUID.randomUUID(), msg, new SimpleTbQueueCallback(__ -> callback.onSuccess(), callback::onFailure)); - } - } - -} diff --git a/application/src/main/java/org/thingsboard/server/service/subscription/DefaultSubscriptionManagerService.java b/application/src/main/java/org/thingsboard/server/service/subscription/DefaultSubscriptionManagerService.java index 23adacfc2e..6d52d86012 100644 --- a/application/src/main/java/org/thingsboard/server/service/subscription/DefaultSubscriptionManagerService.java +++ b/application/src/main/java/org/thingsboard/server/service/subscription/DefaultSubscriptionManagerService.java @@ -161,9 +161,6 @@ public class DefaultSubscriptionManagerService extends TbApplicationEventListene @Override public void onTimeSeriesUpdate(TenantId tenantId, EntityId entityId, List ts, TbCallback callback) { onTimeSeriesUpdate(entityId, ts); - if (entityId.getEntityType() == EntityType.DEVICE) { - updateDeviceInactivityTimeout(tenantId, entityId, ts); - } callback.onSuccess(); } diff --git a/application/src/main/java/org/thingsboard/server/service/telemetry/DefaultTelemetrySubscriptionService.java b/application/src/main/java/org/thingsboard/server/service/telemetry/DefaultTelemetrySubscriptionService.java index 568d80787d..54e87348bb 100644 --- a/application/src/main/java/org/thingsboard/server/service/telemetry/DefaultTelemetrySubscriptionService.java +++ b/application/src/main/java/org/thingsboard/server/service/telemetry/DefaultTelemetrySubscriptionService.java @@ -31,6 +31,7 @@ import org.thingsboard.common.util.DonAsynchron; import org.thingsboard.common.util.ThingsBoardThreadFactory; import org.thingsboard.rule.engine.api.AttributesDeleteRequest; import org.thingsboard.rule.engine.api.AttributesSaveRequest; +import org.thingsboard.rule.engine.api.DeviceStateManager; import org.thingsboard.rule.engine.api.RuleEngineTelemetryService; import org.thingsboard.rule.engine.api.TimeseriesDeleteRequest; import org.thingsboard.rule.engine.api.TimeseriesSaveRequest; @@ -38,9 +39,11 @@ import org.thingsboard.server.common.data.ApiUsageRecordKey; import org.thingsboard.server.common.data.EntityType; import org.thingsboard.server.common.data.EntityView; import org.thingsboard.server.common.data.id.CustomerId; +import org.thingsboard.server.common.data.id.DeviceId; import org.thingsboard.server.common.data.id.EntityId; import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.common.data.kv.AttributeKvEntry; +import org.thingsboard.server.common.data.kv.KvEntry; import org.thingsboard.server.common.data.kv.TsKvEntry; import org.thingsboard.server.common.data.kv.TsKvLatestRemovingResult; import org.thingsboard.server.common.msg.queue.TbCallback; @@ -50,6 +53,7 @@ import org.thingsboard.server.dao.timeseries.TimeseriesService; import org.thingsboard.server.dao.util.KvUtils; import org.thingsboard.server.service.apiusage.TbApiUsageStateService; import org.thingsboard.server.service.entitiy.entityview.TbEntityViewService; +import org.thingsboard.server.service.state.DefaultDeviceStateService; import org.thingsboard.server.service.subscription.TbSubscriptionUtils; import java.util.ArrayList; @@ -75,6 +79,7 @@ public class DefaultTelemetrySubscriptionService extends AbstractSubscriptionSer private final TbEntityViewService tbEntityViewService; private final TbApiUsageReportClient apiUsageClient; private final TbApiUsageStateService apiUsageStateService; + private final DeviceStateManager deviceStateManager; private ExecutorService tsCallBackExecutor; @@ -85,12 +90,14 @@ public class DefaultTelemetrySubscriptionService extends AbstractSubscriptionSer TimeseriesService tsService, @Lazy TbEntityViewService tbEntityViewService, TbApiUsageReportClient apiUsageClient, - TbApiUsageStateService apiUsageStateService) { + TbApiUsageStateService apiUsageStateService, + DeviceStateManager deviceStateManager) { this.attrService = attrService; this.tsService = tsService; this.tbEntityViewService = tbEntityViewService; this.apiUsageClient = apiUsageClient; this.apiUsageStateService = apiUsageStateService; + this.deviceStateManager = deviceStateManager; } @PostConstruct @@ -146,6 +153,14 @@ public class DefaultTelemetrySubscriptionService extends AbstractSubscriptionSer saveFuture = Futures.immediateFuture(0); } + if (entityId.getEntityType() == EntityType.DEVICE && request.getStrategy().saveLatest()) { + findNewInactivityTimeout(request.getEntries()).ifPresent(newInactivityTimeout -> + addMainCallback(saveFuture, __ -> deviceStateManager.onDeviceInactivityTimeoutUpdate( + tenantId, new DeviceId(entityId.getId()), newInactivityTimeout, TbCallback.EMPTY) + ) + ); + } + addMainCallback(saveFuture, request.getCallback()); if (strategy.sendWsUpdate()) { addWsCallback(saveFuture, success -> onTimeSeriesUpdate(tenantId, entityId, request.getEntries())); @@ -156,6 +171,21 @@ public class DefaultTelemetrySubscriptionService extends AbstractSubscriptionSer return saveFuture; } + private static Optional findNewInactivityTimeout(List entries) { + return entries.stream() + .filter(entry -> Objects.equals(DefaultDeviceStateService.INACTIVITY_TIMEOUT, entry.getKey())) + .findFirst() + .map(DefaultTelemetrySubscriptionService::parseAsLong); + } + + private static long parseAsLong(KvEntry kve) { + try { + return Long.parseLong(kve.getValueAsString()); + } catch (NumberFormatException e) { + return 0L; + } + } + @Override public void saveAttributes(AttributesSaveRequest request) { checkInternalEntity(request.getEntityId()); @@ -312,6 +342,10 @@ public class DefaultTelemetrySubscriptionService extends AbstractSubscriptionSer addMainCallback(saveFuture, result -> callback.onSuccess(null), callback::onFailure); } + private void addMainCallback(ListenableFuture saveFuture, Consumer onSuccess) { + addMainCallback(saveFuture, onSuccess, null); + } + private void addMainCallback(ListenableFuture saveFuture, Consumer onSuccess, Consumer onFailure) { DonAsynchron.withCallback(saveFuture, onSuccess, onFailure, tsCallBackExecutor); } diff --git a/application/src/test/java/org/thingsboard/server/service/state/DefaultRuleEngineDeviceStateManagerTest.java b/application/src/test/java/org/thingsboard/server/service/state/DefaultDeviceStateManagerTest.java similarity index 81% rename from application/src/test/java/org/thingsboard/server/service/state/DefaultRuleEngineDeviceStateManagerTest.java rename to application/src/test/java/org/thingsboard/server/service/state/DefaultDeviceStateManagerTest.java index 28ea2d270a..8d2a747d2d 100644 --- a/application/src/test/java/org/thingsboard/server/service/state/DefaultRuleEngineDeviceStateManagerTest.java +++ b/application/src/test/java/org/thingsboard/server/service/state/DefaultDeviceStateManagerTest.java @@ -52,7 +52,7 @@ import static org.mockito.Mockito.doThrow; import static org.mockito.Mockito.never; @ExtendWith(MockitoExtension.class) -public class DefaultRuleEngineDeviceStateManagerTest { +public class DefaultDeviceStateManagerTest { @Mock private DeviceStateService deviceStateServiceMock; @@ -71,7 +71,7 @@ public class DefaultRuleEngineDeviceStateManagerTest { @Captor private ArgumentCaptor queueCallbackCaptor; - private DefaultRuleEngineDeviceStateManager deviceStateManager; + private DefaultDeviceStateManager deviceStateManager; private static final TenantId TENANT_ID = TenantId.fromUUID(UUID.fromString("57ab2e6c-bc4c-11ee-a506-0242ac120002")); private static final DeviceId DEVICE_ID = DeviceId.fromString("74a9053e-bc4c-11ee-a506-0242ac120002"); @@ -82,7 +82,7 @@ public class DefaultRuleEngineDeviceStateManagerTest { @BeforeEach public void setup() { - deviceStateManager = new DefaultRuleEngineDeviceStateManager(serviceInfoProviderMock, partitionServiceMock, Optional.of(deviceStateServiceMock), clusterServiceMock); + deviceStateManager = new DefaultDeviceStateManager(serviceInfoProviderMock, partitionServiceMock, Optional.of(deviceStateServiceMock), clusterServiceMock); } @ParameterizedTest @@ -90,7 +90,7 @@ public class DefaultRuleEngineDeviceStateManagerTest { "when onDeviceX() is called, then should route event to local service and call onSuccess() callback.") @MethodSource public void givenRoutedToLocalAndProcessingSuccess_whenOnDeviceAction_thenShouldCallLocalServiceAndSuccessCallback( - BiConsumer onDeviceAction, Consumer actionVerification + BiConsumer onDeviceAction, Consumer actionVerification ) { // GIVEN given(serviceInfoProviderMock.isService(ServiceType.TB_CORE)).willReturn(true); @@ -109,19 +109,19 @@ public class DefaultRuleEngineDeviceStateManagerTest { private static Stream givenRoutedToLocalAndProcessingSuccess_whenOnDeviceAction_thenShouldCallLocalServiceAndSuccessCallback() { return Stream.of( Arguments.of( - (BiConsumer) (deviceStateManager, tbCallbackMock) -> deviceStateManager.onDeviceConnect(TENANT_ID, DEVICE_ID, EVENT_TS, tbCallbackMock), + (BiConsumer) (deviceStateManager, tbCallbackMock) -> deviceStateManager.onDeviceConnect(TENANT_ID, DEVICE_ID, EVENT_TS, tbCallbackMock), (Consumer) deviceStateServiceMock -> then(deviceStateServiceMock).should().onDeviceConnect(TENANT_ID, DEVICE_ID, EVENT_TS) ), Arguments.of( - (BiConsumer) (deviceStateManager, tbCallbackMock) -> deviceStateManager.onDeviceActivity(TENANT_ID, DEVICE_ID, EVENT_TS, tbCallbackMock), + (BiConsumer) (deviceStateManager, tbCallbackMock) -> deviceStateManager.onDeviceActivity(TENANT_ID, DEVICE_ID, EVENT_TS, tbCallbackMock), (Consumer) deviceStateServiceMock -> then(deviceStateServiceMock).should().onDeviceActivity(TENANT_ID, DEVICE_ID, EVENT_TS) ), Arguments.of( - (BiConsumer) (deviceStateManager, tbCallbackMock) -> deviceStateManager.onDeviceDisconnect(TENANT_ID, DEVICE_ID, EVENT_TS, tbCallbackMock), + (BiConsumer) (deviceStateManager, tbCallbackMock) -> deviceStateManager.onDeviceDisconnect(TENANT_ID, DEVICE_ID, EVENT_TS, tbCallbackMock), (Consumer) deviceStateServiceMock -> then(deviceStateServiceMock).should().onDeviceDisconnect(TENANT_ID, DEVICE_ID, EVENT_TS) ), Arguments.of( - (BiConsumer) (deviceStateManager, tbCallbackMock) -> deviceStateManager.onDeviceInactivity(TENANT_ID, DEVICE_ID, EVENT_TS, tbCallbackMock), + (BiConsumer) (deviceStateManager, tbCallbackMock) -> deviceStateManager.onDeviceInactivity(TENANT_ID, DEVICE_ID, EVENT_TS, tbCallbackMock), (Consumer) deviceStateServiceMock -> then(deviceStateServiceMock).should().onDeviceInactivity(TENANT_ID, DEVICE_ID, EVENT_TS) ) ); @@ -132,7 +132,7 @@ public class DefaultRuleEngineDeviceStateManagerTest { "when onDeviceX() is called, then should route event to local service and call onFailure() callback.") @MethodSource public void givenRoutedToLocalAndProcessingFailure_whenOnDeviceAction_thenShouldCallLocalServiceAndFailureCallback( - Consumer exceptionThrowSetup, BiConsumer onDeviceAction, Consumer actionVerification + Consumer exceptionThrowSetup, BiConsumer onDeviceAction, Consumer actionVerification ) { // GIVEN given(serviceInfoProviderMock.isService(ServiceType.TB_CORE)).willReturn(true); @@ -155,22 +155,22 @@ public class DefaultRuleEngineDeviceStateManagerTest { return Stream.of( Arguments.of( (Consumer) deviceStateServiceMock -> doThrow(RUNTIME_EXCEPTION).when(deviceStateServiceMock).onDeviceConnect(TENANT_ID, DEVICE_ID, EVENT_TS), - (BiConsumer) (deviceStateManager, tbCallbackMock) -> deviceStateManager.onDeviceConnect(TENANT_ID, DEVICE_ID, EVENT_TS, tbCallbackMock), + (BiConsumer) (deviceStateManager, tbCallbackMock) -> deviceStateManager.onDeviceConnect(TENANT_ID, DEVICE_ID, EVENT_TS, tbCallbackMock), (Consumer) deviceStateServiceMock -> then(deviceStateServiceMock).should().onDeviceConnect(TENANT_ID, DEVICE_ID, EVENT_TS) ), Arguments.of( (Consumer) deviceStateServiceMock -> doThrow(RUNTIME_EXCEPTION).when(deviceStateServiceMock).onDeviceActivity(TENANT_ID, DEVICE_ID, EVENT_TS), - (BiConsumer) (deviceStateManager, tbCallbackMock) -> deviceStateManager.onDeviceActivity(TENANT_ID, DEVICE_ID, EVENT_TS, tbCallbackMock), + (BiConsumer) (deviceStateManager, tbCallbackMock) -> deviceStateManager.onDeviceActivity(TENANT_ID, DEVICE_ID, EVENT_TS, tbCallbackMock), (Consumer) deviceStateServiceMock -> then(deviceStateServiceMock).should().onDeviceActivity(TENANT_ID, DEVICE_ID, EVENT_TS) ), Arguments.of( (Consumer) deviceStateServiceMock -> doThrow(RUNTIME_EXCEPTION).when(deviceStateServiceMock).onDeviceDisconnect(TENANT_ID, DEVICE_ID, EVENT_TS), - (BiConsumer) (deviceStateManager, tbCallbackMock) -> deviceStateManager.onDeviceDisconnect(TENANT_ID, DEVICE_ID, EVENT_TS, tbCallbackMock), + (BiConsumer) (deviceStateManager, tbCallbackMock) -> deviceStateManager.onDeviceDisconnect(TENANT_ID, DEVICE_ID, EVENT_TS, tbCallbackMock), (Consumer) deviceStateServiceMock -> then(deviceStateServiceMock).should().onDeviceDisconnect(TENANT_ID, DEVICE_ID, EVENT_TS) ), Arguments.of( (Consumer) deviceStateServiceMock -> doThrow(RUNTIME_EXCEPTION).when(deviceStateServiceMock).onDeviceInactivity(TENANT_ID, DEVICE_ID, EVENT_TS), - (BiConsumer) (deviceStateManager, tbCallbackMock) -> deviceStateManager.onDeviceInactivity(TENANT_ID, DEVICE_ID, EVENT_TS, tbCallbackMock), + (BiConsumer) (deviceStateManager, tbCallbackMock) -> deviceStateManager.onDeviceInactivity(TENANT_ID, DEVICE_ID, EVENT_TS, tbCallbackMock), (Consumer) deviceStateServiceMock -> then(deviceStateServiceMock).should().onDeviceInactivity(TENANT_ID, DEVICE_ID, EVENT_TS) ) ); @@ -181,7 +181,7 @@ public class DefaultRuleEngineDeviceStateManagerTest { "when onDeviceX() is called, then should send correct queue message to external service with correct callback object.") @MethodSource public void givenRoutedToExternal_whenOnDeviceAction_thenShouldSendQueueMsgToExternalServiceWithCorrectCallback( - BiConsumer onDeviceAction, BiConsumer> actionVerification + BiConsumer onDeviceAction, BiConsumer> actionVerification ) { // WHEN ReflectionTestUtils.setField(deviceStateManager, "deviceStateService", Optional.empty()); @@ -203,7 +203,7 @@ public class DefaultRuleEngineDeviceStateManagerTest { private static Stream givenRoutedToExternal_whenOnDeviceAction_thenShouldSendQueueMsgToExternalServiceWithCorrectCallback() { return Stream.of( Arguments.of( - (BiConsumer) (deviceStateManager, tbCallbackMock) -> deviceStateManager.onDeviceConnect(TENANT_ID, DEVICE_ID, EVENT_TS, tbCallbackMock), + (BiConsumer) (deviceStateManager, tbCallbackMock) -> deviceStateManager.onDeviceConnect(TENANT_ID, DEVICE_ID, EVENT_TS, tbCallbackMock), (BiConsumer>) (clusterServiceMock, queueCallbackCaptor) -> { var deviceConnectMsg = TransportProtos.DeviceConnectProto.newBuilder() .setTenantIdMSB(TENANT_ID.getId().getMostSignificantBits()) @@ -219,7 +219,7 @@ public class DefaultRuleEngineDeviceStateManagerTest { } ), Arguments.of( - (BiConsumer) (deviceStateManager, tbCallbackMock) -> deviceStateManager.onDeviceActivity(TENANT_ID, DEVICE_ID, EVENT_TS, tbCallbackMock), + (BiConsumer) (deviceStateManager, tbCallbackMock) -> deviceStateManager.onDeviceActivity(TENANT_ID, DEVICE_ID, EVENT_TS, tbCallbackMock), (BiConsumer>) (clusterServiceMock, queueCallbackCaptor) -> { var deviceActivityMsg = TransportProtos.DeviceActivityProto.newBuilder() .setTenantIdMSB(TENANT_ID.getId().getMostSignificantBits()) @@ -235,7 +235,7 @@ public class DefaultRuleEngineDeviceStateManagerTest { } ), Arguments.of( - (BiConsumer) (deviceStateManager, tbCallbackMock) -> deviceStateManager.onDeviceDisconnect(TENANT_ID, DEVICE_ID, EVENT_TS, tbCallbackMock), + (BiConsumer) (deviceStateManager, tbCallbackMock) -> deviceStateManager.onDeviceDisconnect(TENANT_ID, DEVICE_ID, EVENT_TS, tbCallbackMock), (BiConsumer>) (clusterServiceMock, queueCallbackCaptor) -> { var deviceDisconnectMsg = TransportProtos.DeviceDisconnectProto.newBuilder() .setTenantIdMSB(TENANT_ID.getId().getMostSignificantBits()) @@ -251,7 +251,7 @@ public class DefaultRuleEngineDeviceStateManagerTest { } ), Arguments.of( - (BiConsumer) (deviceStateManager, tbCallbackMock) -> deviceStateManager.onDeviceInactivity(TENANT_ID, DEVICE_ID, EVENT_TS, tbCallbackMock), + (BiConsumer) (deviceStateManager, tbCallbackMock) -> deviceStateManager.onDeviceInactivity(TENANT_ID, DEVICE_ID, EVENT_TS, tbCallbackMock), (BiConsumer>) (clusterServiceMock, queueCallbackCaptor) -> { var deviceInactivityMsg = TransportProtos.DeviceInactivityProto.newBuilder() .setTenantIdMSB(TENANT_ID.getId().getMostSignificantBits()) diff --git a/application/src/test/java/org/thingsboard/server/service/telemetry/DefaultTelemetrySubscriptionServiceTest.java b/application/src/test/java/org/thingsboard/server/service/telemetry/DefaultTelemetrySubscriptionServiceTest.java index dbd61b638e..c04b92e6e4 100644 --- a/application/src/test/java/org/thingsboard/server/service/telemetry/DefaultTelemetrySubscriptionServiceTest.java +++ b/application/src/test/java/org/thingsboard/server/service/telemetry/DefaultTelemetrySubscriptionServiceTest.java @@ -29,6 +29,7 @@ import org.junit.jupiter.params.provider.MethodSource; import org.mockito.Mock; import org.mockito.junit.jupiter.MockitoExtension; import org.springframework.test.util.ReflectionTestUtils; +import org.thingsboard.rule.engine.api.DeviceStateManager; import org.thingsboard.rule.engine.api.TimeseriesSaveRequest; import org.thingsboard.server.cluster.TbClusterService; import org.thingsboard.server.common.data.ApiUsageRecordKey; @@ -125,12 +126,14 @@ class DefaultTelemetrySubscriptionServiceTest { TbApiUsageReportClient apiUsageClient; @Mock TbApiUsageStateService apiUsageStateService; + @Mock + DeviceStateManager deviceStateManager; DefaultTelemetrySubscriptionService telemetryService; @BeforeEach void setup() { - telemetryService = new DefaultTelemetrySubscriptionService(attrService, tsService, tbEntityViewService, apiUsageClient, apiUsageStateService); + telemetryService = new DefaultTelemetrySubscriptionService(attrService, tsService, tbEntityViewService, apiUsageClient, apiUsageStateService, deviceStateManager); ReflectionTestUtils.setField(telemetryService, "clusterService", clusterService); ReflectionTestUtils.setField(telemetryService, "partitionService", partitionService); ReflectionTestUtils.setField(telemetryService, "subscriptionManagerService", Optional.of(subscriptionManagerService)); diff --git a/common/proto/src/main/proto/queue.proto b/common/proto/src/main/proto/queue.proto index 1ecd4e6a3a..8227624af5 100644 --- a/common/proto/src/main/proto/queue.proto +++ b/common/proto/src/main/proto/queue.proto @@ -772,6 +772,14 @@ message DeviceInactivityProto { int64 lastInactivityTime = 5; } +message DeviceInactivityTimeoutUpdateProto { + int64 tenantIdMSB = 1; + int64 tenantIdLSB = 2; + int64 deviceIdMSB = 3; + int64 deviceIdLSB = 4; + int64 inactivityTimeout = 5; +} + //Used to report session state to tb-Service and persist this state in the cache on the tb-Service level. message SubscriptionInfoProto { int64 lastActivityTime = 1; @@ -1515,6 +1523,7 @@ message ToCoreMsg { DeviceConnectProto deviceConnectMsg = 50; DeviceDisconnectProto deviceDisconnectMsg = 51; DeviceInactivityProto deviceInactivityMsg = 52; + DeviceInactivityTimeoutUpdateProto deviceInactivityTimeoutUpdateMsg = 53; } /* High priority messages with low latency are handled by ThingsBoard Core Service separately */ diff --git a/common/util/src/main/java/org/thingsboard/common/util/DonAsynchron.java b/common/util/src/main/java/org/thingsboard/common/util/DonAsynchron.java index 008158246e..0f1a56cb17 100644 --- a/common/util/src/main/java/org/thingsboard/common/util/DonAsynchron.java +++ b/common/util/src/main/java/org/thingsboard/common/util/DonAsynchron.java @@ -36,6 +36,9 @@ public class DonAsynchron { FutureCallback callback = new FutureCallback() { @Override public void onSuccess(T result) { + if (onSuccess == null) { + return; + } try { onSuccess.accept(result); } catch (Throwable th) { @@ -45,6 +48,9 @@ public class DonAsynchron { @Override public void onFailure(Throwable t) { + if (onFailure == null) { + return; + } onFailure.accept(t); } }; diff --git a/rule-engine/rule-engine-api/src/main/java/org/thingsboard/rule/engine/api/RuleEngineDeviceStateManager.java b/rule-engine/rule-engine-api/src/main/java/org/thingsboard/rule/engine/api/DeviceStateManager.java similarity index 88% rename from rule-engine/rule-engine-api/src/main/java/org/thingsboard/rule/engine/api/RuleEngineDeviceStateManager.java rename to rule-engine/rule-engine-api/src/main/java/org/thingsboard/rule/engine/api/DeviceStateManager.java index fb3e282c9a..887f7ecaa2 100644 --- a/rule-engine/rule-engine-api/src/main/java/org/thingsboard/rule/engine/api/RuleEngineDeviceStateManager.java +++ b/rule-engine/rule-engine-api/src/main/java/org/thingsboard/rule/engine/api/DeviceStateManager.java @@ -19,7 +19,7 @@ import org.thingsboard.server.common.data.id.DeviceId; import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.common.msg.queue.TbCallback; -public interface RuleEngineDeviceStateManager { +public interface DeviceStateManager { void onDeviceConnect(TenantId tenantId, DeviceId deviceId, long connectTime, TbCallback callback); @@ -29,4 +29,6 @@ public interface RuleEngineDeviceStateManager { void onDeviceInactivity(TenantId tenantId, DeviceId deviceId, long inactivityTime, TbCallback callback); + void onDeviceInactivityTimeoutUpdate(TenantId tenantId, DeviceId deviceId, long inactivityTimeout, TbCallback callback); + } diff --git a/rule-engine/rule-engine-api/src/main/java/org/thingsboard/rule/engine/api/TbContext.java b/rule-engine/rule-engine-api/src/main/java/org/thingsboard/rule/engine/api/TbContext.java index 46b63e0595..e4c07c4ddb 100644 --- a/rule-engine/rule-engine-api/src/main/java/org/thingsboard/rule/engine/api/TbContext.java +++ b/rule-engine/rule-engine-api/src/main/java/org/thingsboard/rule/engine/api/TbContext.java @@ -279,7 +279,7 @@ public interface TbContext { DeviceCredentialsService getDeviceCredentialsService(); - RuleEngineDeviceStateManager getDeviceStateManager(); + DeviceStateManager getDeviceStateManager(); String getDeviceStateNodeRateLimitConfig(); diff --git a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/action/TbDeviceStateNode.java b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/action/TbDeviceStateNode.java index 3aed05d372..c9a50cf88d 100644 --- a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/action/TbDeviceStateNode.java +++ b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/action/TbDeviceStateNode.java @@ -17,7 +17,7 @@ package org.thingsboard.rule.engine.action; import lombok.extern.slf4j.Slf4j; import org.springframework.util.ConcurrentReferenceHashMap; -import org.thingsboard.rule.engine.api.RuleEngineDeviceStateManager; +import org.thingsboard.rule.engine.api.DeviceStateManager; import org.thingsboard.rule.engine.api.RuleNode; import org.thingsboard.rule.engine.api.TbContext; import org.thingsboard.rule.engine.api.TbNode; @@ -119,7 +119,7 @@ public class TbDeviceStateNode implements TbNode { TenantId tenantId = ctx.getTenantId(); long eventTs = msg.getMetaDataTs(); - RuleEngineDeviceStateManager deviceStateManager = ctx.getDeviceStateManager(); + DeviceStateManager deviceStateManager = ctx.getDeviceStateManager(); TbCallback callback = getMsgEnqueuedCallback(ctx, msg); switch (event) { diff --git a/rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/action/TbDeviceStateNodeTest.java b/rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/action/TbDeviceStateNodeTest.java index b31dffdc01..2c3c61150e 100644 --- a/rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/action/TbDeviceStateNodeTest.java +++ b/rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/action/TbDeviceStateNodeTest.java @@ -29,7 +29,7 @@ import org.mockito.junit.jupiter.MockitoExtension; import org.springframework.test.util.ReflectionTestUtils; import org.springframework.util.ConcurrentReferenceHashMap; import org.thingsboard.common.util.JacksonUtil; -import org.thingsboard.rule.engine.api.RuleEngineDeviceStateManager; +import org.thingsboard.rule.engine.api.DeviceStateManager; import org.thingsboard.rule.engine.api.TbContext; import org.thingsboard.rule.engine.api.TbNodeConfiguration; import org.thingsboard.rule.engine.api.TbNodeException; @@ -66,7 +66,7 @@ public class TbDeviceStateNodeTest { @Mock private TbContext ctxMock; @Mock - private RuleEngineDeviceStateManager deviceStateManagerMock; + private DeviceStateManager deviceStateManagerMock; @Captor private ArgumentCaptor callbackCaptor; private TbDeviceStateNode node; @@ -263,7 +263,7 @@ public class TbDeviceStateNodeTest { @ParameterizedTest @MethodSource - public void givenSupportedEventAndDeviceOriginator_whenOnMsg_thenCorrectEventIsSentWithCorrectCallback(TbMsgType supportedEventType, BiConsumer> actionVerification) { + public void givenSupportedEventAndDeviceOriginator_whenOnMsg_thenCorrectEventIsSentWithCorrectCallback(TbMsgType supportedEventType, BiConsumer> actionVerification) { // GIVEN given(ctxMock.getTenantId()).willReturn(TENANT_ID); given(ctxMock.getDeviceStateNodeRateLimitConfig()).willReturn("1:1"); @@ -297,10 +297,10 @@ public class TbDeviceStateNodeTest { private static Stream givenSupportedEventAndDeviceOriginator_whenOnMsg_thenCorrectEventIsSentWithCorrectCallback() { return Stream.of( - Arguments.of(TbMsgType.CONNECT_EVENT, (BiConsumer>) (deviceStateManagerMock, callbackCaptor) -> then(deviceStateManagerMock).should().onDeviceConnect(eq(TENANT_ID), eq(DEVICE_ID), eq(METADATA_TS), callbackCaptor.capture())), - Arguments.of(TbMsgType.ACTIVITY_EVENT, (BiConsumer>) (deviceStateManagerMock, callbackCaptor) -> then(deviceStateManagerMock).should().onDeviceActivity(eq(TENANT_ID), eq(DEVICE_ID), eq(METADATA_TS), callbackCaptor.capture())), - Arguments.of(TbMsgType.DISCONNECT_EVENT, (BiConsumer>) (deviceStateManagerMock, callbackCaptor) -> then(deviceStateManagerMock).should().onDeviceDisconnect(eq(TENANT_ID), eq(DEVICE_ID), eq(METADATA_TS), callbackCaptor.capture())), - Arguments.of(TbMsgType.INACTIVITY_EVENT, (BiConsumer>) (deviceStateManagerMock, callbackCaptor) -> then(deviceStateManagerMock).should().onDeviceInactivity(eq(TENANT_ID), eq(DEVICE_ID), eq(METADATA_TS), callbackCaptor.capture())) + Arguments.of(TbMsgType.CONNECT_EVENT, (BiConsumer>) (deviceStateManagerMock, callbackCaptor) -> then(deviceStateManagerMock).should().onDeviceConnect(eq(TENANT_ID), eq(DEVICE_ID), eq(METADATA_TS), callbackCaptor.capture())), + Arguments.of(TbMsgType.ACTIVITY_EVENT, (BiConsumer>) (deviceStateManagerMock, callbackCaptor) -> then(deviceStateManagerMock).should().onDeviceActivity(eq(TENANT_ID), eq(DEVICE_ID), eq(METADATA_TS), callbackCaptor.capture())), + Arguments.of(TbMsgType.DISCONNECT_EVENT, (BiConsumer>) (deviceStateManagerMock, callbackCaptor) -> then(deviceStateManagerMock).should().onDeviceDisconnect(eq(TENANT_ID), eq(DEVICE_ID), eq(METADATA_TS), callbackCaptor.capture())), + Arguments.of(TbMsgType.INACTIVITY_EVENT, (BiConsumer>) (deviceStateManagerMock, callbackCaptor) -> then(deviceStateManagerMock).should().onDeviceInactivity(eq(TENANT_ID), eq(DEVICE_ID), eq(METADATA_TS), callbackCaptor.capture())) ); } From 343df41a264c305bb73811c2f8fa87c8ccd3a2e7 Mon Sep 17 00:00:00 2001 From: Tarnavskiy Date: Fri, 28 Feb 2025 11:39:43 +0200 Subject: [PATCH 122/345] PROD-5684: Setting to disable sorting in Entity-group tables and Table-widgets --- .../widget/lib/alarm/alarms-table-widget.component.html | 2 +- .../widget/lib/alarm/alarms-table-widget.component.ts | 5 +---- .../widget/lib/entity/entities-table-widget.component.ts | 3 ++- .../settings/alarm/alarms-table-key-settings.component.html | 5 +++++ .../settings/alarm/alarms-table-key-settings.component.ts | 4 +++- .../cards/timeseries-table-key-settings.component.html | 5 +++++ .../cards/timeseries-table-key-settings.component.ts | 4 +++- .../timeseries-table-latest-key-settings.component.html | 5 +++++ .../cards/timeseries-table-latest-key-settings.component.ts | 4 +++- .../entity/entities-table-key-settings.component.html | 5 +++++ .../settings/entity/entities-table-key-settings.component.ts | 4 +++- .../home/components/widget/lib/table-widget.models.ts | 3 ++- .../widget/lib/timeseries-table-widget.component.ts | 4 ++-- ui-ngx/src/assets/locale/locale.constant-en_US.json | 3 ++- 14 files changed, 42 insertions(+), 14 deletions(-) diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/alarm/alarms-table-widget.component.html b/ui-ngx/src/app/modules/home/components/widget/lib/alarm/alarms-table-widget.component.html index 19a0a95abf..acd4fc43be 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/alarm/alarms-table-widget.component.html +++ b/ui-ngx/src/app/modules/home/components/widget/lib/alarm/alarms-table-widget.component.html @@ -78,7 +78,7 @@ - + {{ column.title }}
+
+ + {{ 'widgets.table.disable-sorting' | translate }} + +
diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/alarm/alarms-table-key-settings.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/settings/alarm/alarms-table-key-settings.component.ts index 8984e3889f..cccc826ff4 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/settings/alarm/alarms-table-key-settings.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/alarm/alarms-table-key-settings.component.ts @@ -47,7 +47,8 @@ export class AlarmsTableKeySettingsComponent extends WidgetSettingsComponent { useCellContentFunction: false, cellContentFunction: '', defaultColumnVisibility: 'visible', - columnSelectionToDisplay: 'enabled' + columnSelectionToDisplay: 'enabled', + disableSorting: false }; } @@ -61,6 +62,7 @@ export class AlarmsTableKeySettingsComponent extends WidgetSettingsComponent { cellContentFunction: [settings.cellContentFunction, [Validators.required]], defaultColumnVisibility: [settings.defaultColumnVisibility, []], columnSelectionToDisplay: [settings.columnSelectionToDisplay, []], + disableSorting: [settings.disableSorting, []] }); } diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/cards/timeseries-table-key-settings.component.html b/ui-ngx/src/app/modules/home/components/widget/lib/settings/cards/timeseries-table-key-settings.component.html index 1dcd4f1f3f..1905888bb6 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/settings/cards/timeseries-table-key-settings.component.html +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/cards/timeseries-table-key-settings.component.html @@ -47,6 +47,11 @@
+
+ + {{ 'widgets.table.disable-sorting' | translate }} + +
diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/cards/timeseries-table-key-settings.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/settings/cards/timeseries-table-key-settings.component.ts index 1bc28baf53..ff314d9d2d 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/settings/cards/timeseries-table-key-settings.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/cards/timeseries-table-key-settings.component.ts @@ -45,7 +45,8 @@ export class TimeseriesTableKeySettingsComponent extends WidgetSettingsComponent useCellContentFunction: false, cellContentFunction: '', defaultColumnVisibility: 'visible', - columnSelectionToDisplay: 'enabled' + columnSelectionToDisplay: 'enabled', + disableSorting: false }; } @@ -57,6 +58,7 @@ export class TimeseriesTableKeySettingsComponent extends WidgetSettingsComponent cellContentFunction: [settings.cellContentFunction, [Validators.required]], defaultColumnVisibility: [settings.defaultColumnVisibility, []], columnSelectionToDisplay: [settings.columnSelectionToDisplay, []], + disableSorting: [settings.disableSorting, []] }); } diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/cards/timeseries-table-latest-key-settings.component.html b/ui-ngx/src/app/modules/home/components/widget/lib/settings/cards/timeseries-table-latest-key-settings.component.html index 54b2a370e6..684258bea7 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/settings/cards/timeseries-table-latest-key-settings.component.html +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/cards/timeseries-table-latest-key-settings.component.html @@ -56,6 +56,11 @@
+
+ + {{ 'widgets.table.disable-sorting' | translate }} + +
diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/cards/timeseries-table-latest-key-settings.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/settings/cards/timeseries-table-latest-key-settings.component.ts index a0ee7e7c70..2e2f414f2c 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/settings/cards/timeseries-table-latest-key-settings.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/cards/timeseries-table-latest-key-settings.component.ts @@ -46,7 +46,8 @@ export class TimeseriesTableLatestKeySettingsComponent extends WidgetSettingsCom useCellContentFunction: false, cellContentFunction: '', defaultColumnVisibility: 'visible', - columnSelectionToDisplay: 'enabled' + columnSelectionToDisplay: 'enabled', + disableSorting: false }; } @@ -60,6 +61,7 @@ export class TimeseriesTableLatestKeySettingsComponent extends WidgetSettingsCom cellContentFunction: [settings.cellContentFunction, [Validators.required]], defaultColumnVisibility: [settings.defaultColumnVisibility, []], columnSelectionToDisplay: [settings.columnSelectionToDisplay, []], + disableSorting: [settings.disableSorting, []] }); } diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/entity/entities-table-key-settings.component.html b/ui-ngx/src/app/modules/home/components/widget/lib/settings/entity/entities-table-key-settings.component.html index 76aa5fd2f4..14383b9608 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/settings/entity/entities-table-key-settings.component.html +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/entity/entities-table-key-settings.component.html @@ -59,6 +59,11 @@
+
+ + {{ 'widgets.table.disable-sorting' | translate }} + +
diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/entity/entities-table-key-settings.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/settings/entity/entities-table-key-settings.component.ts index 8572a9e348..89393ade34 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/settings/entity/entities-table-key-settings.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/entity/entities-table-key-settings.component.ts @@ -47,7 +47,8 @@ export class EntitiesTableKeySettingsComponent extends WidgetSettingsComponent { useCellContentFunction: false, cellContentFunction: '', defaultColumnVisibility: 'visible', - columnSelectionToDisplay: 'enabled' + columnSelectionToDisplay: 'enabled', + disableSorting: false }; } @@ -61,6 +62,7 @@ export class EntitiesTableKeySettingsComponent extends WidgetSettingsComponent { cellContentFunction: [settings.cellContentFunction, [Validators.required]], defaultColumnVisibility: [settings.defaultColumnVisibility, []], columnSelectionToDisplay: [settings.columnSelectionToDisplay, []], + disableSorting: [settings.disableSorting, []] }); } diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/table-widget.models.ts b/ui-ngx/src/app/modules/home/components/widget/lib/table-widget.models.ts index 47ae0b8cc8..15a94f5ecc 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/table-widget.models.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/table-widget.models.ts @@ -61,6 +61,7 @@ export interface TableWidgetDataKeySettings { cellContentFunction?: TbFunction; defaultColumnVisibility?: ColumnVisibilityOptions; columnSelectionToDisplay?: ColumnSelectionOptions; + disableSorting?: boolean; } export type ShowCellButtonActionFunction = (ctx: WidgetContext, data: EntityData | AlarmDataInfo | FormattedData) => boolean; @@ -145,7 +146,7 @@ export function entityDataSortOrderFromString(strSortOrder: string, columns: Ent if (!column) { column = findColumnByName(property, columns); } - if (column && column.entityKey) { + if (column && column.entityKey && column.sortable) { return {key: column.entityKey, direction}; } return null; diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/timeseries-table-widget.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/timeseries-table-widget.component.ts index 0fda3b9afe..623aed9c34 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/timeseries-table-widget.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/timeseries-table-widget.component.ts @@ -518,8 +518,8 @@ export class TimeseriesTableWidgetComponent extends PageComponent implements OnI const latestDataKeys = datasource.latestDataKeys; let header: TimeseriesHeader[] = []; dataKeys.forEach((dataKey, index) => { - const sortable = !dataKey.usePostProcessing; const keySettings: TableWidgetDataKeySettings = dataKey.settings; + const sortable = !keySettings.disableSorting && !dataKey.usePostProcessing; const styleInfo = getCellStyleInfo(this.ctx, keySettings, 'value, rowData, ctx'); const contentFunctionInfo = getCellContentFunctionInfo(this.ctx, keySettings, 'value, rowData, ctx'); const columnDefaultVisibility = getColumnDefaultVisibility(keySettings, this.ctx); @@ -544,8 +544,8 @@ export class TimeseriesTableWidgetComponent extends PageComponent implements OnI if (latestDataKeys) { latestDataKeys.forEach((dataKey, latestIndex) => { const index = dataKeys.length + latestIndex; - const sortable = !dataKey.usePostProcessing; const keySettings: TimeseriesWidgetLatestDataKeySettings = dataKey.settings; + const sortable = !keySettings.disableSorting && !dataKey.usePostProcessing; const styleInfo = getCellStyleInfo(this.ctx, keySettings, 'value, rowData, ctx'); const contentFunctionInfo = getCellContentFunctionInfo(this.ctx, keySettings, 'value, rowData, ctx'); const columnDefaultVisibility = getColumnDefaultVisibility(keySettings, this.ctx); 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 18801bda2c..62a9b6883f 100644 --- a/ui-ngx/src/assets/locale/locale.constant-en_US.json +++ b/ui-ngx/src/assets/locale/locale.constant-en_US.json @@ -8164,7 +8164,8 @@ "timeseries-column-error": "At least one time series column should be specified", "alarm-column-error": "At least one alarm column should be specified", "table-tabs": "Table tabs", - "show-cell-actions-menu-mobile": "Show cell actions dropdown menu in mobile mode" + "show-cell-actions-menu-mobile": "Show cell actions dropdown menu in mobile mode", + "disable-sorting": "Disable sorting" }, "latest-chart": { "total": "Total", From ffa9aec2b1fca4eb17f7d84b1068f94506cb8618 Mon Sep 17 00:00:00 2001 From: Volodymyr Babak Date: Fri, 28 Feb 2025 11:49:00 +0200 Subject: [PATCH 123/345] BaseRuleChainProcessor - use saveRuleChain method without validation --- .../service/edge/rpc/processor/rule/BaseRuleChainProcessor.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/rule/BaseRuleChainProcessor.java b/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/rule/BaseRuleChainProcessor.java index 5156956c3d..00b9c732aa 100644 --- a/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/rule/BaseRuleChainProcessor.java +++ b/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/rule/BaseRuleChainProcessor.java @@ -61,7 +61,7 @@ public class BaseRuleChainProcessor extends BaseEdgeProcessor { if (created) { ruleChain.setId(ruleChainId); } - edgeCtx.getRuleChainService().saveRuleChain(ruleChain); + edgeCtx.getRuleChainService().saveRuleChain(ruleChain, true, false); return Pair.of(created, isRoot); } From 506ec363eb935d323f99959e712f7ea2535bfb86 Mon Sep 17 00:00:00 2001 From: ViacheslavKlimov Date: Fri, 28 Feb 2025 12:45:01 +0200 Subject: [PATCH 124/345] EDQS: optimize serialization, fix api usage fields --- .../data/edqs/fields/ApiUsageStateFields.java | 5 +++-- .../common/data/edqs/fields/EntityFields.java | 2 ++ .../common/data/edqs/fields/FieldsUtil.java | 1 + .../server/edqs/repo/TenantRepo.java | 4 +++- .../server/edqs/util/EdqsConverter.java | 17 ++++++++++++++--- .../usagerecord/ApiUsageStateRepository.java | 3 ++- 6 files changed, 25 insertions(+), 7 deletions(-) diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/edqs/fields/ApiUsageStateFields.java b/common/data/src/main/java/org/thingsboard/server/common/data/edqs/fields/ApiUsageStateFields.java index 3524fdbcbf..d10f375bc1 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/edqs/fields/ApiUsageStateFields.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/edqs/fields/ApiUsageStateFields.java @@ -43,8 +43,9 @@ public class ApiUsageStateFields extends AbstractEntityFields { public ApiUsageStateFields(UUID id, long createdTime, UUID tenantId, UUID entityId, String entityType, ApiUsageStateValue transportState, ApiUsageStateValue dbStorageState, ApiUsageStateValue reExecState, ApiUsageStateValue jsExecState, ApiUsageStateValue tbelExecState, - ApiUsageStateValue emailExecState, ApiUsageStateValue smsExecState, ApiUsageStateValue alarmExecState) { - super(id, createdTime, tenantId); + ApiUsageStateValue emailExecState, ApiUsageStateValue smsExecState, ApiUsageStateValue alarmExecState, + Long version) { + super(id, createdTime, tenantId, null, null, version); this.entityId = (entityType != null && entityId != null) ? EntityIdFactory.getByTypeAndUuid(entityType, entityId) : null; this.transportState = transportState; this.dbStorageState = dbStorageState; diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/edqs/fields/EntityFields.java b/common/data/src/main/java/org/thingsboard/server/common/data/edqs/fields/EntityFields.java index 1bbf8a519a..532c4a92ac 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/edqs/fields/EntityFields.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/edqs/fields/EntityFields.java @@ -15,6 +15,7 @@ */ package org.thingsboard.server.common.data.edqs.fields; +import com.fasterxml.jackson.annotation.JsonInclude; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.thingsboard.server.common.data.id.EntityId; @@ -23,6 +24,7 @@ import java.util.Collections; import java.util.List; import java.util.UUID; +@JsonInclude(JsonInclude.Include.NON_NULL) public interface EntityFields { Logger log = LoggerFactory.getLogger(EntityFields.class); diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/edqs/fields/FieldsUtil.java b/common/data/src/main/java/org/thingsboard/server/common/data/edqs/fields/FieldsUtil.java index 72c45066f1..ce9873b5d6 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/edqs/fields/FieldsUtil.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/edqs/fields/FieldsUtil.java @@ -284,6 +284,7 @@ public class FieldsUtil { .emailExecState(entity.getEmailExecState()) .smsExecState(entity.getSmsExecState()) .alarmExecState(entity.getAlarmExecState()) + .version(entity.getVersion()) .build(); } diff --git a/common/edqs/src/main/java/org/thingsboard/server/edqs/repo/TenantRepo.java b/common/edqs/src/main/java/org/thingsboard/server/edqs/repo/TenantRepo.java index 5020ed3476..0c55bc50dd 100644 --- a/common/edqs/src/main/java/org/thingsboard/server/edqs/repo/TenantRepo.java +++ b/common/edqs/src/main/java/org/thingsboard/server/edqs/repo/TenantRepo.java @@ -219,7 +219,9 @@ public class TenantRepo { EntityType entityType = entity.getType(); EntityData removed = getEntityMap(entityType).remove(entityId); if (removed != null) { - getEntitySet(entityType).remove(removed); + if (removed.getFields() != null) { + getEntitySet(entityType).remove(removed); + } edqsStatsService.ifPresent(statService -> statService.reportEvent(tenantId, ObjectType.fromEntityType(entityType), EdqsEventType.DELETED)); UUID customerId = removed.getCustomerId(); if (customerId != null) { diff --git a/common/edqs/src/main/java/org/thingsboard/server/edqs/util/EdqsConverter.java b/common/edqs/src/main/java/org/thingsboard/server/edqs/util/EdqsConverter.java index 781ae2ed3d..5b4cd7ac4a 100644 --- a/common/edqs/src/main/java/org/thingsboard/server/edqs/util/EdqsConverter.java +++ b/common/edqs/src/main/java/org/thingsboard/server/edqs/util/EdqsConverter.java @@ -15,12 +15,15 @@ */ package org.thingsboard.server.edqs.util; +import com.fasterxml.jackson.annotation.JsonAutoDetect; +import com.fasterxml.jackson.annotation.PropertyAccessor; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.json.JsonMapper; import com.google.protobuf.ByteString; import lombok.RequiredArgsConstructor; import lombok.SneakyThrows; import lombok.extern.slf4j.Slf4j; import org.springframework.stereotype.Service; -import org.thingsboard.common.util.JacksonUtil; import org.thingsboard.server.common.data.AttributeScope; import org.thingsboard.server.common.data.EntityType; import org.thingsboard.server.common.data.ObjectType; @@ -217,16 +220,24 @@ public class EdqsConverter { @RequiredArgsConstructor private static class JsonConverter implements Converter { + private static final ObjectMapper mapper = JsonMapper.builder() + .visibility(PropertyAccessor.FIELD, JsonAutoDetect.Visibility.ANY) + .visibility(PropertyAccessor.GETTER, JsonAutoDetect.Visibility.NONE) + .visibility(PropertyAccessor.IS_GETTER, JsonAutoDetect.Visibility.NONE) + .build(); + private final Class type; + @SneakyThrows @Override public byte[] serialize(ObjectType objectType, T value) { - return JacksonUtil.writeValueAsBytes(value); + return mapper.writeValueAsBytes(value); } + @SneakyThrows @Override public T deserialize(ObjectType objectType, byte[] bytes) { - return JacksonUtil.fromBytes(bytes, this.type); + return mapper.readValue(bytes, this.type); } } diff --git a/dao/src/main/java/org/thingsboard/server/dao/sql/usagerecord/ApiUsageStateRepository.java b/dao/src/main/java/org/thingsboard/server/dao/sql/usagerecord/ApiUsageStateRepository.java index b9afe8ea4c..98e62fc110 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/sql/usagerecord/ApiUsageStateRepository.java +++ b/dao/src/main/java/org/thingsboard/server/dao/sql/usagerecord/ApiUsageStateRepository.java @@ -54,6 +54,7 @@ public interface ApiUsageStateRepository extends JpaRepository :id ORDER BY a.id") + "a.emailExecState, a.smsExecState, a.alarmExecState, a.version) FROM ApiUsageStateEntity a WHERE a.id > :id ORDER BY a.id") List findNextBatch(@Param("id") UUID id, Limit limit); + } From 0e819850c83a2c5bc27d894cd312a61efc02545a Mon Sep 17 00:00:00 2001 From: nick Date: Fri, 28 Feb 2025 15:11:21 +0200 Subject: [PATCH 125/345] lwm2m: fix bug monitoring --- .../transport/TransportHealthChecker.java | 2 +- .../main/resources/lwm2m/device_profile.json | 6 +- .../resources/lwm2m/models/test-model.xml | 324 +++++++++++++++++- .../src/main/resources/lwm2m/resource.json | 2 +- pom.xml | 2 + 5 files changed, 312 insertions(+), 24 deletions(-) diff --git a/monitoring/src/main/java/org/thingsboard/monitoring/service/transport/TransportHealthChecker.java b/monitoring/src/main/java/org/thingsboard/monitoring/service/transport/TransportHealthChecker.java index 1b8a704fad..f5e6cb5469 100644 --- a/monitoring/src/main/java/org/thingsboard/monitoring/service/transport/TransportHealthChecker.java +++ b/monitoring/src/main/java/org/thingsboard/monitoring/service/transport/TransportHealthChecker.java @@ -145,7 +145,7 @@ public abstract class TransportHealthChecker { TbResource newResource = ResourceUtils.getResource("lwm2m/resource.json", TbResource.class); diff --git a/monitoring/src/main/resources/lwm2m/device_profile.json b/monitoring/src/main/resources/lwm2m/device_profile.json index c75fe9ce7a..d23e02825d 100644 --- a/monitoring/src/main/resources/lwm2m/device_profile.json +++ b/monitoring/src/main/resources/lwm2m/device_profile.json @@ -12,14 +12,14 @@ "transportConfiguration": { "observeAttr": { "observe": [ - "/3_1.0/0/0" + "/3_1.1/0/0" ], "attribute": [], "telemetry": [ - "/3_1.0/0/0" + "/3_1.1/0/0" ], "keyName": { - "/3_1.0/0/0": "testData" + "/3_1.1/0/0": "testData" }, "attributeLwm2m": {} }, diff --git a/monitoring/src/main/resources/lwm2m/models/test-model.xml b/monitoring/src/main/resources/lwm2m/models/test-model.xml index 2d25f4698e..02b084127f 100644 --- a/monitoring/src/main/resources/lwm2m/models/test-model.xml +++ b/monitoring/src/main/resources/lwm2m/models/test-model.xml @@ -1,45 +1,331 @@ + - + + LwM2M Monitoring - - + 3 - urn:oma:lwm2m:oma:3 + urn:oma:lwm2m:oma:3:1.1 1.1 - 1.0 + 1.1 Single Mandatory - Test data + Manufacturer + R + Single + Optional + String + + + + + + Model Number R Single Optional String - + - + + Serial Number + R + Single + Optional + String + + + + + + Firmware Version + R + Single + Optional + String + + + + + + Reboot + E + Single + Mandatory + + + + + + + Factory Reset + E + Single + Optional + + + + + + + Available Power Sources + R + Multiple + Optional + Integer + 0..7 + + + + + Power Source Voltage + R + Multiple + Optional + Integer + + + + + + Power Source Current + R + Multiple + Optional + Integer + + + + + + Battery Level + R + Single + Optional + Integer + 0..100 + /100 + + + + Memory Free + R + Single + Optional + Integer + + + + + + Error Code + R + Multiple + Mandatory + Integer + 0..32 + + + + + Reset Error Code + E + Single + Optional + + + + + + + Current Time + RW + Single + Optional + Time + + + + + + UTC Offset + RW + Single + Optional + String + + + + + + Timezone + RW + Single + Optional + String + + + + + + Supported Binding and Modes + R + Single + Mandatory + String + + + + + Device Type + R + Single + Optional + String + + + + + Hardware Version + R + Single + Optional + String + + + + + Software Version + R + Single + Optional + String + + + + + Battery Status + R + Single + Optional + Integer + 0..6 + + + + Memory Total + R + Single + Optional + Integer + + + + + ExtDevInfo + R + Multiple + Optional + Objlnk + + + + diff --git a/monitoring/src/main/resources/lwm2m/resource.json b/monitoring/src/main/resources/lwm2m/resource.json index dcc0182bdd..bbef8da7ec 100644 --- a/monitoring/src/main/resources/lwm2m/resource.json +++ b/monitoring/src/main/resources/lwm2m/resource.json @@ -2,5 +2,5 @@ "title": "", "resourceType": "LWM2M_MODEL", "fileName": "test-model.xml", - "data": "PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0iVVRGLTgiPz4KPCEtLQoKICAgIENvcHlyaWdodCDCqSAyMDE2LTIwMjQgVGhlIFRoaW5nc2JvYXJkIEF1dGhvcnMKCiAgICBMaWNlbnNlZCB1bmRlciB0aGUgQXBhY2hlIExpY2Vuc2UsIFZlcnNpb24gMi4wICh0aGUgIkxpY2Vuc2UiKTsKICAgIHlvdSBtYXkgbm90IHVzZSB0aGlzIGZpbGUgZXhjZXB0IGluIGNvbXBsaWFuY2Ugd2l0aCB0aGUgTGljZW5zZS4KICAgIFlvdSBtYXkgb2J0YWluIGEgY29weSBvZiB0aGUgTGljZW5zZSBhdAoKICAgICAgICBodHRwOi8vd3d3LmFwYWNoZS5vcmcvbGljZW5zZXMvTElDRU5TRS0yLjAKCiAgICBVbmxlc3MgcmVxdWlyZWQgYnkgYXBwbGljYWJsZSBsYXcgb3IgYWdyZWVkIHRvIGluIHdyaXRpbmcsIHNvZnR3YXJlCiAgICBkaXN0cmlidXRlZCB1bmRlciB0aGUgTGljZW5zZSBpcyBkaXN0cmlidXRlZCBvbiBhbiAiQVMgSVMiIEJBU0lTLAogICAgV0lUSE9VVCBXQVJSQU5USUVTIE9SIENPTkRJVElPTlMgT0YgQU5ZIEtJTkQsIGVpdGhlciBleHByZXNzIG9yIGltcGxpZWQuCiAgICBTZWUgdGhlIExpY2Vuc2UgZm9yIHRoZSBzcGVjaWZpYyBsYW5ndWFnZSBnb3Zlcm5pbmcgcGVybWlzc2lvbnMgYW5kCiAgICBsaW1pdGF0aW9ucyB1bmRlciB0aGUgTGljZW5zZS4KCi0tPgo8TFdNMk0geG1sbnM6eHNpPSJodHRwOi8vd3d3LnczLm9yZy8yMDAxL1hNTFNjaGVtYS1pbnN0YW5jZSIKICAgICAgIHhzaTpub05hbWVzcGFjZVNjaGVtYUxvY2F0aW9uPSJodHRwOi8vd3d3Lm9wZW5tb2JpbGVhbGxpYW5jZS5vcmcvdGVjaC9wcm9maWxlcy9MV00yTS12MV8xLnhzZCI+CiAgICA8T2JqZWN0IE9iamVjdFR5cGU9Ik1PRGVmaW5pdGlvbiI+CiAgICAgICAgPE5hbWU+THdNMk0gTW9uaXRvcmluZzwvTmFtZT4KICAgICAgICA8RGVzY3JpcHRpb24xPgogICAgICAgICAgICA8IVtDREFUQVtdXT48L0Rlc2NyaXB0aW9uMT4KICAgICAgICA8T2JqZWN0SUQ+MzwvT2JqZWN0SUQ+CiAgICAgICAgPE9iamVjdFVSTj51cm46b21hOmx3bTJtOm9tYTozPC9PYmplY3RVUk4+CiAgICAgICAgPExXTTJNVmVyc2lvbj4xLjE8L0xXTTJNVmVyc2lvbj4KICAgICAgICA8T2JqZWN0VmVyc2lvbj4xLjA8L09iamVjdFZlcnNpb24+CiAgICAgICAgPE11bHRpcGxlSW5zdGFuY2VzPlNpbmdsZTwvTXVsdGlwbGVJbnN0YW5jZXM+CiAgICAgICAgPE1hbmRhdG9yeT5NYW5kYXRvcnk8L01hbmRhdG9yeT4KICAgICAgICA8UmVzb3VyY2VzPgogICAgICAgICAgICA8SXRlbSBJRD0iMCI+CiAgICAgICAgICAgICAgICA8TmFtZT5UZXN0IGRhdGE8L05hbWU+CiAgICAgICAgICAgICAgICA8T3BlcmF0aW9ucz5SPC9PcGVyYXRpb25zPgogICAgICAgICAgICAgICAgPE11bHRpcGxlSW5zdGFuY2VzPlNpbmdsZTwvTXVsdGlwbGVJbnN0YW5jZXM+CiAgICAgICAgICAgICAgICA8TWFuZGF0b3J5Pk9wdGlvbmFsPC9NYW5kYXRvcnk+CiAgICAgICAgICAgICAgICA8VHlwZT5TdHJpbmc8L1R5cGU+CiAgICAgICAgICAgICAgICA8UmFuZ2VFbnVtZXJhdGlvbj48L1JhbmdlRW51bWVyYXRpb24+CiAgICAgICAgICAgICAgICA8VW5pdHM+PC9Vbml0cz4KICAgICAgICAgICAgICAgIDxEZXNjcmlwdGlvbj48IVtDREFUQVtUZXN0IGRhdGFdXT48L0Rlc2NyaXB0aW9uPgogICAgICAgICAgICA8L0l0ZW0+CiAgICAgICAgPC9SZXNvdXJjZXM+CiAgICAgICAgPERlc2NyaXB0aW9uMj48L0Rlc2NyaXB0aW9uMj4KICAgIDwvT2JqZWN0Pgo8L0xXTTJNPgo=" + "data": "PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0iVVRGLTgiPz4KCjwhLS0KRklMRSBJTkZPUk1BVElPTgoKT01BIFBlcm1hbmVudCBEb2N1bWVudAogICBGaWxlOiBPTUEtU1VQLVhNTF8zLVYxXzItMjAyMDExMTAtQS54bWwKICAgUGF0aDogaHR0cDovL3d3dy5vcGVubW9iaWxlYWxsaWFuY2Uub3JnL3JlbGVhc2UvT2JqTHdNMk1fRGV2aWNlLwoKT01OQSBMd00yTSBSZWdpc3RyeQogICBQYXRoOiBodHRwczovL2dpdGh1Yi5jb20vT3Blbk1vYmlsZUFsbGlhbmNlL2x3bTJtLXJlZ2lzdHJ5CiAgIE5hbWU6IDMueG1sCgpOT1JNQVRJVkUgSU5GT1JNQVRJT04KCiAgSW5mb3JtYXRpb24gYWJvdXQgdGhpcyBmaWxlIGNhbiBiZSBmb3VuZCBpbiB0aGUgbGF0ZXN0IHJldmlzaW9uIG9mCgogICAgT01BLVRTLUxpZ2h0d2VpZ2h0TTJNX0NvcmUtVjFfMgoKICBUaGlzIGlzIGF2YWlsYWJsZSBhdCBodHRwOi8vd3d3Lm9wZW5tb2JpbGVhbGxpYW5jZS5vcmcvcmVsZWFzZS9MaWdodHdlaWdodE0yTS8KCiAgU2VuZCBjb21tZW50cyB0byBodHRwczovL2dpdGh1Yi5jb20vT3Blbk1vYmlsZUFsbGlhbmNlL09NQV9Md00yTV9mb3JfRGV2ZWxvcGVycy9pc3N1ZXMKCkxFR0FMIERJU0NMQUlNRVIKCiAgQ29weXJpZ2h0IDIwMjAgT3BlbiBNb2JpbGUgQWxsaWFuY2UuCgogIFJlZGlzdHJpYnV0aW9uIGFuZCB1c2UgaW4gc291cmNlIGFuZCBiaW5hcnkgZm9ybXMsIHdpdGggb3Igd2l0aG91dAogIG1vZGlmaWNhdGlvbiwgYXJlIHBlcm1pdHRlZCBwcm92aWRlZCB0aGF0IHRoZSBmb2xsb3dpbmcgY29uZGl0aW9ucwogIGFyZSBtZXQ6CgogIDEuIFJlZGlzdHJpYnV0aW9ucyBvZiBzb3VyY2UgY29kZSBtdXN0IHJldGFpbiB0aGUgYWJvdmUgY29weXJpZ2h0CiAgbm90aWNlLCB0aGlzIGxpc3Qgb2YgY29uZGl0aW9ucyBhbmQgdGhlIGZvbGxvd2luZyBkaXNjbGFpbWVyLgogIDIuIFJlZGlzdHJpYnV0aW9ucyBpbiBiaW5hcnkgZm9ybSBtdXN0IHJlcHJvZHVjZSB0aGUgYWJvdmUgY29weXJpZ2h0CiAgbm90aWNlLCB0aGlzIGxpc3Qgb2YgY29uZGl0aW9ucyBhbmQgdGhlIGZvbGxvd2luZyBkaXNjbGFpbWVyIGluIHRoZQogIGRvY3VtZW50YXRpb24gYW5kL29yIG90aGVyIG1hdGVyaWFscyBwcm92aWRlZCB3aXRoIHRoZSBkaXN0cmlidXRpb24uCiAgMy4gTmVpdGhlciB0aGUgbmFtZSBvZiB0aGUgY29weXJpZ2h0IGhvbGRlciBub3IgdGhlIG5hbWVzIG9mIGl0cwogIGNvbnRyaWJ1dG9ycyBtYXkgYmUgdXNlZCB0byBlbmRvcnNlIG9yIHByb21vdGUgcHJvZHVjdHMgZGVyaXZlZAogIGZyb20gdGhpcyBzb2Z0d2FyZSB3aXRob3V0IHNwZWNpZmljIHByaW9yIHdyaXR0ZW4gcGVybWlzc2lvbi4KCiAgVEhJUyBTT0ZUV0FSRSBJUyBQUk9WSURFRCBCWSBUSEUgQ09QWVJJR0hUIEhPTERFUlMgQU5EIENPTlRSSUJVVE9SUwogICJBUyBJUyIgQU5EIEFOWSBFWFBSRVNTIE9SIElNUExJRUQgV0FSUkFOVElFUywgSU5DTFVESU5HLCBCVVQgTk9UCiAgTElNSVRFRCBUTywgVEhFIElNUExJRUQgV0FSUkFOVElFUyBPRiBNRVJDSEFOVEFCSUxJVFkgQU5EIEZJVE5FU1MKICBGT1IgQSBQQVJUSUNVTEFSIFBVUlBPU0UgQVJFIERJU0NMQUlNRUQuIElOIE5PIEVWRU5UIFNIQUxMIFRIRQogIENPUFlSSUdIVCBIT0xERVIgT1IgQ09OVFJJQlVUT1JTIEJFIExJQUJMRSBGT1IgQU5ZIERJUkVDVCwgSU5ESVJFQ1QsCiAgSU5DSURFTlRBTCwgU1BFQ0lBTCwgRVhFTVBMQVJZLCBPUiBDT05TRVFVRU5USUFMIERBTUFHRVMgKElOQ0xVRElORywKICBCVVQgTk9UIExJTUlURUQgVE8sIFBST0NVUkVNRU5UIE9GIFNVQlNUSVRVVEUgR09PRFMgT1IgU0VSVklDRVM7CiAgTE9TUyBPRiBVU0UsIERBVEEsIE9SIFBST0ZJVFM7IE9SIEJVU0lORVNTIElOVEVSUlVQVElPTikgSE9XRVZFUgogIENBVVNFRCBBTkQgT04gQU5ZIFRIRU9SWSBPRiBMSUFCSUxJVFksIFdIRVRIRVIgSU4gQ09OVFJBQ1QsIFNUUklDVAogIExJQUJJTElUWSwgT1IgVE9SVCAoSU5DTFVESU5HIE5FR0xJR0VOQ0UgT1IgT1RIRVJXSVNFKSBBUklTSU5HIElOCiAgQU5ZIFdBWSBPVVQgT0YgVEhFIFVTRSBPRiBUSElTIFNPRlRXQVJFLCBFVkVOIElGIEFEVklTRUQgT0YgVEhFCiAgUE9TU0lCSUxJVFkgT0YgU1VDSCBEQU1BR0UuCgogIFRoZSBhYm92ZSBsaWNlbnNlIGlzIHVzZWQgYXMgYSBsaWNlbnNlIHVuZGVyIGNvcHlyaWdodCBvbmx5LiAgUGxlYXNlCiAgcmVmZXJlbmNlIHRoZSBPTUEgSVBSIFBvbGljeSBmb3IgcGF0ZW50IGxpY2Vuc2luZyB0ZXJtczoKICBodHRwczovL3d3dy5vbWFzcGVjd29ya3Mub3JnL2Fib3V0L2ludGVsbGVjdHVhbC1wcm9wZXJ0eS1yaWdodHMvCgotLT4KCjxMV00yTSB4bWxuczp4c2k9Imh0dHA6Ly93d3cudzMub3JnLzIwMDEvWE1MU2NoZW1hLWluc3RhbmNlIiB4c2k6bm9OYW1lc3BhY2VTY2hlbWFMb2NhdGlvbj0iaHR0cDovL3d3dy5vcGVubW9iaWxlYWxsaWFuY2Uub3JnL3RlY2gvcHJvZmlsZXMvTFdNMk0tdjFfMS54c2QiPgogICAgPE9iamVjdCBPYmplY3RUeXBlPSJNT0RlZmluaXRpb24iPgogICAgICAgIDxOYW1lPkx3TTJNIE1vbml0b3Jpbmc8L05hbWU+CiAgICAgICAgPERlc2NyaXB0aW9uMT48IVtDREFUQVtUaGlzIEx3TTJNIE9iamVjdCBwcm92aWRlcyBhIHJhbmdlIG9mIGRldmljZSByZWxhdGVkIGluZm9ybWF0aW9uIHdoaWNoIGNhbiBiZSBxdWVyaWVkIGJ5IHRoZSBMd00yTSBTZXJ2ZXIsIGFuZCBhIGRldmljZSByZWJvb3QgYW5kIGZhY3RvcnkgcmVzZXQgZnVuY3Rpb24uXV0+PC9EZXNjcmlwdGlvbjE+CiAgICAgICAgPE9iamVjdElEPjM8L09iamVjdElEPgogICAgICAgIDxPYmplY3RVUk4+dXJuOm9tYTpsd20ybTpvbWE6MzoxLjE8L09iamVjdFVSTj4KICAgICAgICA8TFdNMk1WZXJzaW9uPjEuMTwvTFdNMk1WZXJzaW9uPgogICAgICAgIDxPYmplY3RWZXJzaW9uPjEuMTwvT2JqZWN0VmVyc2lvbj4KICAgICAgICA8TXVsdGlwbGVJbnN0YW5jZXM+U2luZ2xlPC9NdWx0aXBsZUluc3RhbmNlcz4KICAgICAgICA8TWFuZGF0b3J5Pk1hbmRhdG9yeTwvTWFuZGF0b3J5PgogICAgICAgIDxSZXNvdXJjZXM+CiAgICAgICAgICAgIDxJdGVtIElEPSIwIj4KICAgICAgICAgICAgICAgIDxOYW1lPk1hbnVmYWN0dXJlcjwvTmFtZT4KICAgICAgICAgICAgICAgIDxPcGVyYXRpb25zPlI8L09wZXJhdGlvbnM+CiAgICAgICAgICAgICAgICA8TXVsdGlwbGVJbnN0YW5jZXM+U2luZ2xlPC9NdWx0aXBsZUluc3RhbmNlcz4KICAgICAgICAgICAgICAgIDxNYW5kYXRvcnk+T3B0aW9uYWw8L01hbmRhdG9yeT4KICAgICAgICAgICAgICAgIDxUeXBlPlN0cmluZzwvVHlwZT4KICAgICAgICAgICAgICAgIDxSYW5nZUVudW1lcmF0aW9uPjwvUmFuZ2VFbnVtZXJhdGlvbj4KICAgICAgICAgICAgICAgIDxVbml0cz48L1VuaXRzPgogICAgICAgICAgICAgICAgPERlc2NyaXB0aW9uPjwhW0NEQVRBW0h1bWFuIHJlYWRhYmxlIG1hbnVmYWN0dXJlciBuYW1lXV0+PC9EZXNjcmlwdGlvbj4KICAgICAgICAgICAgPC9JdGVtPgogICAgICAgICAgICA8SXRlbSBJRD0iMSI+CiAgICAgICAgICAgICAgICA8TmFtZT5Nb2RlbCBOdW1iZXI8L05hbWU+CiAgICAgICAgICAgICAgICA8T3BlcmF0aW9ucz5SPC9PcGVyYXRpb25zPgogICAgICAgICAgICAgICAgPE11bHRpcGxlSW5zdGFuY2VzPlNpbmdsZTwvTXVsdGlwbGVJbnN0YW5jZXM+CiAgICAgICAgICAgICAgICA8TWFuZGF0b3J5Pk9wdGlvbmFsPC9NYW5kYXRvcnk+CiAgICAgICAgICAgICAgICA8VHlwZT5TdHJpbmc8L1R5cGU+CiAgICAgICAgICAgICAgICA8UmFuZ2VFbnVtZXJhdGlvbj48L1JhbmdlRW51bWVyYXRpb24+CiAgICAgICAgICAgICAgICA8VW5pdHM+PC9Vbml0cz4KICAgICAgICAgICAgICAgIDxEZXNjcmlwdGlvbj48IVtDREFUQVtBIG1vZGVsIGlkZW50aWZpZXIgKG1hbnVmYWN0dXJlciBzcGVjaWZpZWQgc3RyaW5nKV1dPjwvRGVzY3JpcHRpb24+CiAgICAgICAgICAgIDwvSXRlbT4KICAgICAgICAgICAgPEl0ZW0gSUQ9IjIiPgogICAgICAgICAgICAgICAgPE5hbWU+U2VyaWFsIE51bWJlcjwvTmFtZT4KICAgICAgICAgICAgICAgIDxPcGVyYXRpb25zPlI8L09wZXJhdGlvbnM+CiAgICAgICAgICAgICAgICA8TXVsdGlwbGVJbnN0YW5jZXM+U2luZ2xlPC9NdWx0aXBsZUluc3RhbmNlcz4KICAgICAgICAgICAgICAgIDxNYW5kYXRvcnk+T3B0aW9uYWw8L01hbmRhdG9yeT4KICAgICAgICAgICAgICAgIDxUeXBlPlN0cmluZzwvVHlwZT4KICAgICAgICAgICAgICAgIDxSYW5nZUVudW1lcmF0aW9uPjwvUmFuZ2VFbnVtZXJhdGlvbj4KICAgICAgICAgICAgICAgIDxVbml0cz48L1VuaXRzPgogICAgICAgICAgICAgICAgPERlc2NyaXB0aW9uPjwhW0NEQVRBW1NlcmlhbCBOdW1iZXJdXT48L0Rlc2NyaXB0aW9uPgogICAgICAgICAgICA8L0l0ZW0+CiAgICAgICAgICAgIDxJdGVtIElEPSIzIj4KICAgICAgICAgICAgICAgIDxOYW1lPkZpcm13YXJlIFZlcnNpb248L05hbWU+CiAgICAgICAgICAgICAgICA8T3BlcmF0aW9ucz5SPC9PcGVyYXRpb25zPgogICAgICAgICAgICAgICAgPE11bHRpcGxlSW5zdGFuY2VzPlNpbmdsZTwvTXVsdGlwbGVJbnN0YW5jZXM+CiAgICAgICAgICAgICAgICA8TWFuZGF0b3J5Pk9wdGlvbmFsPC9NYW5kYXRvcnk+CiAgICAgICAgICAgICAgICA8VHlwZT5TdHJpbmc8L1R5cGU+CiAgICAgICAgICAgICAgICA8UmFuZ2VFbnVtZXJhdGlvbj48L1JhbmdlRW51bWVyYXRpb24+CiAgICAgICAgICAgICAgICA8VW5pdHM+PC9Vbml0cz4KICAgICAgICAgICAgICAgIDxEZXNjcmlwdGlvbj48IVtDREFUQVtDdXJyZW50IGZpcm13YXJlIHZlcnNpb24gb2YgdGhlIERldmljZS5UaGUgRmlybXdhcmUgTWFuYWdlbWVudCBmdW5jdGlvbiBjb3VsZCByZWx5IG9uIHRoaXMgcmVzb3VyY2UuXV0+PC9EZXNjcmlwdGlvbj4KICAgICAgICAgICAgPC9JdGVtPgogICAgICAgICAgICA8SXRlbSBJRD0iNCI+CiAgICAgICAgICAgICAgICA8TmFtZT5SZWJvb3Q8L05hbWU+CiAgICAgICAgICAgICAgICA8T3BlcmF0aW9ucz5FPC9PcGVyYXRpb25zPgogICAgICAgICAgICAgICAgPE11bHRpcGxlSW5zdGFuY2VzPlNpbmdsZTwvTXVsdGlwbGVJbnN0YW5jZXM+CiAgICAgICAgICAgICAgICA8TWFuZGF0b3J5Pk1hbmRhdG9yeTwvTWFuZGF0b3J5PgogICAgICAgICAgICAgICAgPFR5cGU+PC9UeXBlPgogICAgICAgICAgICAgICAgPFJhbmdlRW51bWVyYXRpb24+PC9SYW5nZUVudW1lcmF0aW9uPgogICAgICAgICAgICAgICAgPFVuaXRzPjwvVW5pdHM+CiAgICAgICAgICAgICAgICA8RGVzY3JpcHRpb24+PCFbQ0RBVEFbUmVib290IHRoZSBMd00yTSBEZXZpY2UgdG8gcmVzdG9yZSB0aGUgRGV2aWNlIGZyb20gdW5leHBlY3RlZCBmaXJtd2FyZSBmYWlsdXJlLl1dPjwvRGVzY3JpcHRpb24+CiAgICAgICAgICAgIDwvSXRlbT4KICAgICAgICAgICAgPEl0ZW0gSUQ9IjUiPgogICAgICAgICAgICAgICAgPE5hbWU+RmFjdG9yeSBSZXNldDwvTmFtZT4KICAgICAgICAgICAgICAgIDxPcGVyYXRpb25zPkU8L09wZXJhdGlvbnM+CiAgICAgICAgICAgICAgICA8TXVsdGlwbGVJbnN0YW5jZXM+U2luZ2xlPC9NdWx0aXBsZUluc3RhbmNlcz4KICAgICAgICAgICAgICAgIDxNYW5kYXRvcnk+T3B0aW9uYWw8L01hbmRhdG9yeT4KICAgICAgICAgICAgICAgIDxUeXBlPjwvVHlwZT4KICAgICAgICAgICAgICAgIDxSYW5nZUVudW1lcmF0aW9uPjwvUmFuZ2VFbnVtZXJhdGlvbj4KICAgICAgICAgICAgICAgIDxVbml0cz48L1VuaXRzPgogICAgICAgICAgICAgICAgPERlc2NyaXB0aW9uPjwhW0NEQVRBW1BlcmZvcm0gZmFjdG9yeSByZXNldCBvZiB0aGUgTHdNMk0gRGV2aWNlIHRvIG1ha2UgdGhlIEx3TTJNIERldmljZSB0byBnbyB0aHJvdWdoIGluaXRpYWwgZGVwbG95bWVudCBzZXF1ZW5jZSB3aGVyZSBwcm92aXNpb25pbmcgYW5kIGJvb3RzdHJhcCBzZXF1ZW5jZSBpcyBwZXJmb3JtZWQuIFRoaXMgcmVxdWlyZXMgY2xpZW50IGVuc3VyaW5nIHBvc3QgZmFjdG9yeSByZXNldCB0byBoYXZlIG1pbmltYWwgaW5mb3JtYXRpb24gdG8gYWxsb3cgaXQgdG8gY2Fycnkgb3V0IG9uZSBvZiB0aGUgYm9vdHN0cmFwIG1ldGhvZHMgc3BlY2lmaWVkIGluIHNlY3Rpb24gNS4yLjMuCldoZW4gdGhpcyBSZXNvdXJjZSBpcyBleGVjdXRlZCwgIkRlLXJlZ2lzdGVyIiBvcGVyYXRpb24gTUFZIGJlIHNlbnQgdG8gdGhlIEx3TTJNIFNlcnZlcihzKSBiZWZvcmUgZmFjdG9yeSByZXNldCBvZiB0aGUgTHdNMk0gRGV2aWNlLl1dPjwvRGVzY3JpcHRpb24+CiAgICAgICAgICAgIDwvSXRlbT4KICAgICAgICAgICAgPEl0ZW0gSUQ9IjYiPgogICAgICAgICAgICAgICAgPE5hbWU+QXZhaWxhYmxlIFBvd2VyIFNvdXJjZXM8L05hbWU+CiAgICAgICAgICAgICAgICA8T3BlcmF0aW9ucz5SPC9PcGVyYXRpb25zPgogICAgICAgICAgICAgICAgPE11bHRpcGxlSW5zdGFuY2VzPk11bHRpcGxlPC9NdWx0aXBsZUluc3RhbmNlcz4KICAgICAgICAgICAgICAgIDxNYW5kYXRvcnk+T3B0aW9uYWw8L01hbmRhdG9yeT4KICAgICAgICAgICAgICAgIDxUeXBlPkludGVnZXI8L1R5cGU+CiAgICAgICAgICAgICAgICA8UmFuZ2VFbnVtZXJhdGlvbj4wLi43PC9SYW5nZUVudW1lcmF0aW9uPgogICAgICAgICAgICAgICAgPFVuaXRzPjwvVW5pdHM+CiAgICAgICAgICAgICAgICA8RGVzY3JpcHRpb24+PCFbQ0RBVEFbMDogREMgcG93ZXIKMTogSW50ZXJuYWwgQmF0dGVyeQoyOiBFeHRlcm5hbCBCYXR0ZXJ5CjM6IEZ1ZWwgQ2VsbAo0OiBQb3dlciBvdmVyIEV0aGVybmV0CjU6IFVTQgo2OiBBQyAoTWFpbnMpIHBvd2VyCjc6IFNvbGFyClRoZSBzYW1lIFJlc291cmNlIEluc3RhbmNlIElEIE1VU1QgYmUgdXNlZCB0byBhc3NvY2lhdGUgYSBnaXZlbiBQb3dlciBTb3VyY2UgKFJlc291cmNlIElEOjYpIHdpdGggaXRzIFByZXNlbnQgVm9sdGFnZSAoUmVzb3VyY2UgSUQ6NykgYW5kIGl0cyBQcmVzZW50IEN1cnJlbnQgKFJlc291cmNlIElEOjgpXV0+PC9EZXNjcmlwdGlvbj4KICAgICAgICAgICAgPC9JdGVtPgogICAgICAgICAgICA8SXRlbSBJRD0iNyI+CiAgICAgICAgICAgICAgICA8TmFtZT5Qb3dlciBTb3VyY2UgVm9sdGFnZTwvTmFtZT4KICAgICAgICAgICAgICAgIDxPcGVyYXRpb25zPlI8L09wZXJhdGlvbnM+CiAgICAgICAgICAgICAgICA8TXVsdGlwbGVJbnN0YW5jZXM+TXVsdGlwbGU8L011bHRpcGxlSW5zdGFuY2VzPgogICAgICAgICAgICAgICAgPE1hbmRhdG9yeT5PcHRpb25hbDwvTWFuZGF0b3J5PgogICAgICAgICAgICAgICAgPFR5cGU+SW50ZWdlcjwvVHlwZT4KICAgICAgICAgICAgICAgIDxSYW5nZUVudW1lcmF0aW9uPjwvUmFuZ2VFbnVtZXJhdGlvbj4KICAgICAgICAgICAgICAgIDxVbml0cz48L1VuaXRzPgogICAgICAgICAgICAgICAgPERlc2NyaXB0aW9uPjwhW0NEQVRBW1ByZXNlbnQgdm9sdGFnZSBmb3IgZWFjaCBBdmFpbGFibGUgUG93ZXIgU291cmNlcyBSZXNvdXJjZSBJbnN0YW5jZS4gVGhlIHVuaXQgdXNlZCBmb3IgdGhpcyByZXNvdXJjZSBpcyBpbiBtVi5dXT48L0Rlc2NyaXB0aW9uPgogICAgICAgICAgICA8L0l0ZW0+CiAgICAgICAgICAgIDxJdGVtIElEPSI4Ij4KICAgICAgICAgICAgICAgIDxOYW1lPlBvd2VyIFNvdXJjZSBDdXJyZW50PC9OYW1lPgogICAgICAgICAgICAgICAgPE9wZXJhdGlvbnM+UjwvT3BlcmF0aW9ucz4KICAgICAgICAgICAgICAgIDxNdWx0aXBsZUluc3RhbmNlcz5NdWx0aXBsZTwvTXVsdGlwbGVJbnN0YW5jZXM+CiAgICAgICAgICAgICAgICA8TWFuZGF0b3J5Pk9wdGlvbmFsPC9NYW5kYXRvcnk+CiAgICAgICAgICAgICAgICA8VHlwZT5JbnRlZ2VyPC9UeXBlPgogICAgICAgICAgICAgICAgPFJhbmdlRW51bWVyYXRpb24+PC9SYW5nZUVudW1lcmF0aW9uPgogICAgICAgICAgICAgICAgPFVuaXRzPjwvVW5pdHM+CiAgICAgICAgICAgICAgICA8RGVzY3JpcHRpb24+PCFbQ0RBVEFbUHJlc2VudCBjdXJyZW50IGZvciBlYWNoIEF2YWlsYWJsZSBQb3dlciBTb3VyY2UuIFRoZSB1bml0IHVzZWQgZm9yIHRoaXMgcmVzb3VyY2UgaXMgaW4gbUEuXV0+PC9EZXNjcmlwdGlvbj4KICAgICAgICAgICAgPC9JdGVtPgogICAgICAgICAgICA8SXRlbSBJRD0iOSI+CiAgICAgICAgICAgICAgICA8TmFtZT5CYXR0ZXJ5IExldmVsPC9OYW1lPgogICAgICAgICAgICAgICAgPE9wZXJhdGlvbnM+UjwvT3BlcmF0aW9ucz4KICAgICAgICAgICAgICAgIDxNdWx0aXBsZUluc3RhbmNlcz5TaW5nbGU8L011bHRpcGxlSW5zdGFuY2VzPgogICAgICAgICAgICAgICAgPE1hbmRhdG9yeT5PcHRpb25hbDwvTWFuZGF0b3J5PgogICAgICAgICAgICAgICAgPFR5cGU+SW50ZWdlcjwvVHlwZT4KICAgICAgICAgICAgICAgIDxSYW5nZUVudW1lcmF0aW9uPjAuLjEwMDwvUmFuZ2VFbnVtZXJhdGlvbj4KICAgICAgICAgICAgICAgIDxVbml0cz4vMTAwPC9Vbml0cz4KICAgICAgICAgICAgICAgIDxEZXNjcmlwdGlvbj48IVtDREFUQVtDb250YWlucyB0aGUgY3VycmVudCBiYXR0ZXJ5IGxldmVsIGFzIGEgcGVyY2VudGFnZSAod2l0aCBhIHJhbmdlIGZyb20gMCB0byAxMDApLiBUaGlzIHZhbHVlIGlzIG9ubHkgdmFsaWQgZm9yIHRoZSBEZXZpY2UgaW50ZXJuYWwgQmF0dGVyeSBpZiBwcmVzZW50IChvbmUgQXZhaWxhYmxlIFBvd2VyIFNvdXJjZXMgUmVzb3VyY2UgSW5zdGFuY2UgaXMgMSkuXV0+PC9EZXNjcmlwdGlvbj4KICAgICAgICAgICAgPC9JdGVtPgogICAgICAgICAgICA8SXRlbSBJRD0iMTAiPgogICAgICAgICAgICAgICAgPE5hbWU+TWVtb3J5IEZyZWU8L05hbWU+CiAgICAgICAgICAgICAgICA8T3BlcmF0aW9ucz5SPC9PcGVyYXRpb25zPgogICAgICAgICAgICAgICAgPE11bHRpcGxlSW5zdGFuY2VzPlNpbmdsZTwvTXVsdGlwbGVJbnN0YW5jZXM+CiAgICAgICAgICAgICAgICA8TWFuZGF0b3J5Pk9wdGlvbmFsPC9NYW5kYXRvcnk+CiAgICAgICAgICAgICAgICA8VHlwZT5JbnRlZ2VyPC9UeXBlPgogICAgICAgICAgICAgICAgPFJhbmdlRW51bWVyYXRpb24+PC9SYW5nZUVudW1lcmF0aW9uPgogICAgICAgICAgICAgICAgPFVuaXRzPjwvVW5pdHM+CiAgICAgICAgICAgICAgICA8RGVzY3JpcHRpb24+PCFbQ0RBVEFbRXN0aW1hdGVkIGN1cnJlbnQgYXZhaWxhYmxlIGFtb3VudCBvZiBzdG9yYWdlIHNwYWNlIHdoaWNoIGNhbiBzdG9yZSBkYXRhIGFuZCBzb2Z0d2FyZSBpbiB0aGUgTHdNMk0gRGV2aWNlIChleHByZXNzZWQgaW4ga2lsb2J5dGVzKS4gTm90ZTogMSBraWxvYnl0ZSBjb3JyZXNwb25kcyB0byAxMDAwIGJ5dGVzLl1dPjwvRGVzY3JpcHRpb24+CiAgICAgICAgICAgIDwvSXRlbT4KICAgICAgICAgICAgPEl0ZW0gSUQ9IjExIj4KICAgICAgICAgICAgICAgIDxOYW1lPkVycm9yIENvZGU8L05hbWU+CiAgICAgICAgICAgICAgICA8T3BlcmF0aW9ucz5SPC9PcGVyYXRpb25zPgogICAgICAgICAgICAgICAgPE11bHRpcGxlSW5zdGFuY2VzPk11bHRpcGxlPC9NdWx0aXBsZUluc3RhbmNlcz4KICAgICAgICAgICAgICAgIDxNYW5kYXRvcnk+TWFuZGF0b3J5PC9NYW5kYXRvcnk+CiAgICAgICAgICAgICAgICA8VHlwZT5JbnRlZ2VyPC9UeXBlPgogICAgICAgICAgICAgICAgPFJhbmdlRW51bWVyYXRpb24+MC4uMzI8L1JhbmdlRW51bWVyYXRpb24+CiAgICAgICAgICAgICAgICA8VW5pdHM+PC9Vbml0cz4KICAgICAgICAgICAgICAgIDxEZXNjcmlwdGlvbj48IVtDREFUQVswPU5vIGVycm9yCjE9TG93IGJhdHRlcnkgcG93ZXIKMj1FeHRlcm5hbCBwb3dlciBzdXBwbHkgb2ZmCjM9R1BTIG1vZHVsZSBmYWlsdXJlCjQ9TG93IHJlY2VpdmVkIHNpZ25hbCBzdHJlbmd0aAo1PU91dCBvZiBtZW1vcnkKNj1TTVMgZmFpbHVyZQo3PUlQIGNvbm5lY3Rpdml0eSBmYWlsdXJlCjg9UGVyaXBoZXJhbCBtYWxmdW5jdGlvbgo5Li4xNT1SZXNlcnZlZCBmb3IgZnV0dXJlIHVzZQoxNi4uMzI9RGV2aWNlIHNwZWNpZmljIGVycm9yIGNvZGVzCgpXaGVuIHRoZSBzaW5nbGUgRGV2aWNlIE9iamVjdCBJbnN0YW5jZSBpcyBpbml0aWF0ZWQsIHRoZXJlIGlzIG9ubHkgb25lIGVycm9yIGNvZGUgUmVzb3VyY2UgSW5zdGFuY2Ugd2hvc2UgdmFsdWUgaXMgZXF1YWwgdG8gMCB0aGF0IG1lYW5zIG5vIGVycm9yLiBXaGVuIHRoZSBmaXJzdCBlcnJvciBoYXBwZW5zLCB0aGUgTHdNMk0gQ2xpZW50IGNoYW5nZXMgZXJyb3IgY29kZSBSZXNvdXJjZSBJbnN0YW5jZSB0byBhbnkgbm9uLXplcm8gdmFsdWUgdG8gaW5kaWNhdGUgdGhlIGVycm9yIHR5cGUuIFdoZW4gYW55IG90aGVyIGVycm9yIGhhcHBlbnMsIGEgbmV3IGVycm9yIGNvZGUgUmVzb3VyY2UgSW5zdGFuY2UgaXMgY3JlYXRlZC4gV2hlbiBhbiBlcnJvciBhc3NvY2lhdGVkIHdpdGggYSBSZXNvdXJjZSBJbnN0YW5jZSBpcyBubyBsb25nZXIgcHJlc2VudCwgdGhhdCBSZXNvdXJjZSBJbnN0YW5jZSBpcyBkZWxldGVkLiBXaGVuIHRoZSBzaW5nbGUgZXhpc3RpbmcgZXJyb3IgaXMgbm8gbG9uZ2VyIHByZXNlbnQsIHRoZSBMd00yTSBDbGllbnQgcmV0dXJucyB0byB0aGUgb3JpZ2luYWwgbm8gZXJyb3Igc3RhdGUgd2hlcmUgSW5zdGFuY2UgMCBoYXMgdmFsdWUgMC4KVGhpcyBlcnJvciBjb2RlIFJlc291cmNlIE1BWSBiZSBvYnNlcnZlZCBieSB0aGUgTHdNMk0gU2VydmVyLiBIb3cgdG8gZGVhbCB3aXRoIEx3TTJNIENsaWVudOKAmXMgZXJyb3IgcmVwb3J0IGRlcGVuZHMgb24gdGhlIHBvbGljeSBvZiB0aGUgTHdNMk0gU2VydmVyLiBFcnJvciBjb2RlcyBpbiBiZXR3ZWVuIDE2IGFuZCAzMiBhcmUgc3BlY2lmaWMgdG8gdGhlIERldmljZSBhbmQgbWF5IGhhdmUgZGlmZmVyZW50IG1lYW5pbmdzIGFtb25nIGltcGxlbWVudGF0aW9ucy5dXT48L0Rlc2NyaXB0aW9uPgogICAgICAgICAgICA8L0l0ZW0+CiAgICAgICAgICAgIDxJdGVtIElEPSIxMiI+CiAgICAgICAgICAgICAgICA8TmFtZT5SZXNldCBFcnJvciBDb2RlPC9OYW1lPgogICAgICAgICAgICAgICAgPE9wZXJhdGlvbnM+RTwvT3BlcmF0aW9ucz4KICAgICAgICAgICAgICAgIDxNdWx0aXBsZUluc3RhbmNlcz5TaW5nbGU8L011bHRpcGxlSW5zdGFuY2VzPgogICAgICAgICAgICAgICAgPE1hbmRhdG9yeT5PcHRpb25hbDwvTWFuZGF0b3J5PgogICAgICAgICAgICAgICAgPFR5cGU+PC9UeXBlPgogICAgICAgICAgICAgICAgPFJhbmdlRW51bWVyYXRpb24+PC9SYW5nZUVudW1lcmF0aW9uPgogICAgICAgICAgICAgICAgPFVuaXRzPjwvVW5pdHM+CiAgICAgICAgICAgICAgICA8RGVzY3JpcHRpb24+PCFbQ0RBVEFbRGVsZXRlIGFsbCBlcnJvciBjb2RlIFJlc291cmNlIEluc3RhbmNlcyBhbmQgY3JlYXRlIG9ubHkgb25lIHplcm8tdmFsdWUgZXJyb3IgY29kZSB0aGF0IGltcGxpZXMgbm8gZXJyb3IsIHRoZW4gcmUtZXZhbHVhdGUgYWxsIGVycm9yIGNvbmRpdGlvbnMgYW5kIHVwZGF0ZSBhbmQgY3JlYXRlIFJlc291cmNlcyBJbnN0YW5jZXMgdG8gY2FwdHVyZSBhbGwgY3VycmVudCBlcnJvciBjb25kaXRpb25zLl1dPjwvRGVzY3JpcHRpb24+CiAgICAgICAgICAgIDwvSXRlbT4KICAgICAgICAgICAgPEl0ZW0gSUQ9IjEzIj4KICAgICAgICAgICAgICAgIDxOYW1lPkN1cnJlbnQgVGltZTwvTmFtZT4KICAgICAgICAgICAgICAgIDxPcGVyYXRpb25zPlJXPC9PcGVyYXRpb25zPgogICAgICAgICAgICAgICAgPE11bHRpcGxlSW5zdGFuY2VzPlNpbmdsZTwvTXVsdGlwbGVJbnN0YW5jZXM+CiAgICAgICAgICAgICAgICA8TWFuZGF0b3J5Pk9wdGlvbmFsPC9NYW5kYXRvcnk+CiAgICAgICAgICAgICAgICA8VHlwZT5UaW1lPC9UeXBlPgogICAgICAgICAgICAgICAgPFJhbmdlRW51bWVyYXRpb24+PC9SYW5nZUVudW1lcmF0aW9uPgogICAgICAgICAgICAgICAgPFVuaXRzPjwvVW5pdHM+CiAgICAgICAgICAgICAgICA8RGVzY3JpcHRpb24+PCFbQ0RBVEFbQ3VycmVudCBVTklYIHRpbWUgb2YgdGhlIEx3TTJNIENsaWVudC4KVGhlIEx3TTJNIENsaWVudCBzaG91bGQgYmUgcmVzcG9uc2libGUgdG8gaW5jcmVhc2UgdGhpcyB0aW1lIHZhbHVlIGFzIGV2ZXJ5IHNlY29uZCBlbGFwc2VzLgpUaGUgTHdNMk0gU2VydmVyIGlzIGFibGUgdG8gd3JpdGUgdGhpcyBSZXNvdXJjZSB0byBtYWtlIHRoZSBMd00yTSBDbGllbnQgc3luY2hyb25pemVkIHdpdGggdGhlIEx3TTJNIFNlcnZlci5dXT48L0Rlc2NyaXB0aW9uPgogICAgICAgICAgICA8L0l0ZW0+CiAgICAgICAgICAgIDxJdGVtIElEPSIxNCI+CiAgICAgICAgICAgICAgICA8TmFtZT5VVEMgT2Zmc2V0PC9OYW1lPgogICAgICAgICAgICAgICAgPE9wZXJhdGlvbnM+Ulc8L09wZXJhdGlvbnM+CiAgICAgICAgICAgICAgICA8TXVsdGlwbGVJbnN0YW5jZXM+U2luZ2xlPC9NdWx0aXBsZUluc3RhbmNlcz4KICAgICAgICAgICAgICAgIDxNYW5kYXRvcnk+T3B0aW9uYWw8L01hbmRhdG9yeT4KICAgICAgICAgICAgICAgIDxUeXBlPlN0cmluZzwvVHlwZT4KICAgICAgICAgICAgICAgIDxSYW5nZUVudW1lcmF0aW9uPjwvUmFuZ2VFbnVtZXJhdGlvbj4KICAgICAgICAgICAgICAgIDxVbml0cz48L1VuaXRzPgogICAgICAgICAgICAgICAgPERlc2NyaXB0aW9uPjwhW0NEQVRBW0luZGljYXRlcyB0aGUgVVRDIG9mZnNldCBjdXJyZW50bHkgaW4gZWZmZWN0IGZvciB0aGlzIEx3TTJNIERldmljZS4gVVRDK1ggW0lTTyA4NjAxXS5dXT48L0Rlc2NyaXB0aW9uPgogICAgICAgICAgICA8L0l0ZW0+CiAgICAgICAgICAgIDxJdGVtIElEPSIxNSI+CiAgICAgICAgICAgICAgICA8TmFtZT5UaW1lem9uZTwvTmFtZT4KICAgICAgICAgICAgICAgIDxPcGVyYXRpb25zPlJXPC9PcGVyYXRpb25zPgogICAgICAgICAgICAgICAgPE11bHRpcGxlSW5zdGFuY2VzPlNpbmdsZTwvTXVsdGlwbGVJbnN0YW5jZXM+CiAgICAgICAgICAgICAgICA8TWFuZGF0b3J5Pk9wdGlvbmFsPC9NYW5kYXRvcnk+CiAgICAgICAgICAgICAgICA8VHlwZT5TdHJpbmc8L1R5cGU+CiAgICAgICAgICAgICAgICA8UmFuZ2VFbnVtZXJhdGlvbj48L1JhbmdlRW51bWVyYXRpb24+CiAgICAgICAgICAgICAgICA8VW5pdHM+PC9Vbml0cz4KICAgICAgICAgICAgICAgIDxEZXNjcmlwdGlvbj48IVtDREFUQVtJbmRpY2F0ZXMgaW4gd2hpY2ggdGltZSB6b25lIHRoZSBMd00yTSBEZXZpY2UgaXMgbG9jYXRlZCwgaW4gSUFOQSBUaW1lem9uZSAoVFopIGRhdGFiYXNlIGZvcm1hdC5dXT48L0Rlc2NyaXB0aW9uPgogICAgICAgICAgICA8L0l0ZW0+CiAgICAgICAgICAgIDxJdGVtIElEPSIxNiI+CiAgICAgICAgICAgICAgICA8TmFtZT5TdXBwb3J0ZWQgQmluZGluZyBhbmQgTW9kZXM8L05hbWU+CiAgICAgICAgICAgICAgICA8T3BlcmF0aW9ucz5SPC9PcGVyYXRpb25zPgogICAgICAgICAgICAgICAgPE11bHRpcGxlSW5zdGFuY2VzPlNpbmdsZTwvTXVsdGlwbGVJbnN0YW5jZXM+CiAgICAgICAgICAgICAgICA8TWFuZGF0b3J5Pk1hbmRhdG9yeTwvTWFuZGF0b3J5PgogICAgICAgICAgICAgICAgPFR5cGU+U3RyaW5nPC9UeXBlPgogICAgICAgICAgICAgICAgPFJhbmdlRW51bWVyYXRpb24+PC9SYW5nZUVudW1lcmF0aW9uPgogICAgICAgICAgICAgICAgPFVuaXRzPjwvVW5pdHM+CiAgICAgICAgICAgICAgICA8RGVzY3JpcHRpb24+PCFbQ0RBVEFbSW5kaWNhdGVzIHdoaWNoIGJpbmRpbmdzIGFuZCBtb2RlcyBhcmUgc3VwcG9ydGVkIGluIHRoZSBMd00yTSBDbGllbnQuIFRoZSBwb3NzaWJsZSB2YWx1ZXMgYXJlIHRob3NlIGxpc3RlZCBpbiB0aGUgTHdNMk0gQ29yZSBTcGVjaWZpY2F0aW9uLl1dPjwvRGVzY3JpcHRpb24+CiAgICAgICAgICAgIDwvSXRlbT4KICAgICAgICAgICAgPEl0ZW0gSUQ9IjE3Ij48TmFtZT5EZXZpY2UgVHlwZTwvTmFtZT4KICAgICAgICAgICAgICAgIDxPcGVyYXRpb25zPlI8L09wZXJhdGlvbnM+CiAgICAgICAgICAgICAgICA8TXVsdGlwbGVJbnN0YW5jZXM+U2luZ2xlPC9NdWx0aXBsZUluc3RhbmNlcz4KICAgICAgICAgICAgICAgIDxNYW5kYXRvcnk+T3B0aW9uYWw8L01hbmRhdG9yeT4KICAgICAgICAgICAgICAgIDxUeXBlPlN0cmluZzwvVHlwZT4KICAgICAgICAgICAgICAgIDxSYW5nZUVudW1lcmF0aW9uPjwvUmFuZ2VFbnVtZXJhdGlvbj4KICAgICAgICAgICAgICAgIDxVbml0cz48L1VuaXRzPgogICAgICAgICAgICAgICAgPERlc2NyaXB0aW9uPjwhW0NEQVRBW1R5cGUgb2YgdGhlIGRldmljZSAobWFudWZhY3R1cmVyIHNwZWNpZmllZCBzdHJpbmc6IGUuZy4gc21hcnQgbWV0ZXJzIC8gZGV2IENsYXNzIC8gLi4uKV1dPjwvRGVzY3JpcHRpb24+CiAgICAgICAgICAgIDwvSXRlbT4KICAgICAgICAgICAgPEl0ZW0gSUQ9IjE4Ij48TmFtZT5IYXJkd2FyZSBWZXJzaW9uPC9OYW1lPgogICAgICAgICAgICAgICAgPE9wZXJhdGlvbnM+UjwvT3BlcmF0aW9ucz4KICAgICAgICAgICAgICAgIDxNdWx0aXBsZUluc3RhbmNlcz5TaW5nbGU8L011bHRpcGxlSW5zdGFuY2VzPgogICAgICAgICAgICAgICAgPE1hbmRhdG9yeT5PcHRpb25hbDwvTWFuZGF0b3J5PgogICAgICAgICAgICAgICAgPFR5cGU+U3RyaW5nPC9UeXBlPgogICAgICAgICAgICAgICAgPFJhbmdlRW51bWVyYXRpb24+PC9SYW5nZUVudW1lcmF0aW9uPgogICAgICAgICAgICAgICAgPFVuaXRzPjwvVW5pdHM+CiAgICAgICAgICAgICAgICA8RGVzY3JpcHRpb24+PCFbQ0RBVEFbQ3VycmVudCBoYXJkd2FyZSB2ZXJzaW9uIG9mIHRoZSBkZXZpY2VdXT48L0Rlc2NyaXB0aW9uPgogICAgICAgICAgICA8L0l0ZW0+CiAgICAgICAgICAgIDxJdGVtIElEPSIxOSI+PE5hbWU+U29mdHdhcmUgVmVyc2lvbjwvTmFtZT4KICAgICAgICAgICAgICAgIDxPcGVyYXRpb25zPlI8L09wZXJhdGlvbnM+CiAgICAgICAgICAgICAgICA8TXVsdGlwbGVJbnN0YW5jZXM+U2luZ2xlPC9NdWx0aXBsZUluc3RhbmNlcz4KICAgICAgICAgICAgICAgIDxNYW5kYXRvcnk+T3B0aW9uYWw8L01hbmRhdG9yeT4KICAgICAgICAgICAgICAgIDxUeXBlPlN0cmluZzwvVHlwZT4KICAgICAgICAgICAgICAgIDxSYW5nZUVudW1lcmF0aW9uPjwvUmFuZ2VFbnVtZXJhdGlvbj4KICAgICAgICAgICAgICAgIDxVbml0cz48L1VuaXRzPgogICAgICAgICAgICAgICAgPERlc2NyaXB0aW9uPjwhW0NEQVRBW0N1cnJlbnQgc29mdHdhcmUgdmVyc2lvbiBvZiB0aGUgZGV2aWNlIChtYW51ZmFjdHVyZXIgc3BlY2lmaWVkIHN0cmluZykuIE9uIGVsYWJvcmF0ZWQgTHdNMk0gZGV2aWNlLCBTVyBjb3VsZCBiZSBzcGxpdCBpbiAyIHBhcnRzOiBhIGZpcm13YXJlIG9uZSBhbmQgYSBoaWdoZXIgbGV2ZWwgc29mdHdhcmUgb24gdG9wLgpCb3RoIHBpZWNlcyBvZiBTb2Z0d2FyZSBhcmUgdG9nZXRoZXIgbWFuYWdlZCBieSBMd00yTSBGaXJtd2FyZSBVcGRhdGUgT2JqZWN0IChPYmplY3QgSUQgNSldXT48L0Rlc2NyaXB0aW9uPgogICAgICAgICAgICA8L0l0ZW0+CiAgICAgICAgICAgIDxJdGVtIElEPSIyMCI+PE5hbWU+QmF0dGVyeSBTdGF0dXM8L05hbWU+CiAgICAgICAgICAgICAgICA8T3BlcmF0aW9ucz5SPC9PcGVyYXRpb25zPgogICAgICAgICAgICAgICAgPE11bHRpcGxlSW5zdGFuY2VzPlNpbmdsZTwvTXVsdGlwbGVJbnN0YW5jZXM+CiAgICAgICAgICAgICAgICA8TWFuZGF0b3J5Pk9wdGlvbmFsPC9NYW5kYXRvcnk+CiAgICAgICAgICAgICAgICA8VHlwZT5JbnRlZ2VyPC9UeXBlPgogICAgICAgICAgICAgICAgPFJhbmdlRW51bWVyYXRpb24+MC4uNjwvUmFuZ2VFbnVtZXJhdGlvbj4KICAgICAgICAgICAgICAgIDxVbml0cz48L1VuaXRzPgogICAgICAgICAgICAgICAgPERlc2NyaXB0aW9uPjwhW0NEQVRBW1RoaXMgdmFsdWUgaXMgb25seSB2YWxpZCBmb3IgdGhlIERldmljZSBJbnRlcm5hbCBCYXR0ZXJ5IGlmIHByZXNlbnQgKG9uZSBBdmFpbGFibGUgUG93ZXIgU291cmNlcyBSZXNvdXJjZSBJbnN0YW5jZSB2YWx1ZSBpcyAxKS4KQmF0dGVyeQpTdGF0dXMJTWVhbmluZwlEZXNjcmlwdGlvbgowCU5vcm1hbAlUaGUgYmF0dGVyeSBpcyBvcGVyYXRpbmcgbm9ybWFsbHkgYW5kIG5vdCBvbiBwb3dlci4KMQlDaGFyZ2luZwlUaGUgYmF0dGVyeSBpcyBjdXJyZW50bHkgY2hhcmdpbmcuCjIJQ2hhcmdlIENvbXBsZXRlCVRoZSBiYXR0ZXJ5IGlzIGZ1bGx5IGNoYXJnZWQgYW5kIHN0aWxsIG9uIHBvd2VyLgozCURhbWFnZWQJVGhlIGJhdHRlcnkgaGFzIHNvbWUgcHJvYmxlbS4KNAlMb3cgQmF0dGVyeQlUaGUgYmF0dGVyeSBpcyBsb3cgb24gY2hhcmdlLgo1CU5vdCBJbnN0YWxsZWQJVGhlIGJhdHRlcnkgaXMgbm90IGluc3RhbGxlZC4KNglVbmtub3duCVRoZSBiYXR0ZXJ5IGluZm9ybWF0aW9uIGlzIG5vdCBhdmFpbGFibGUuXV0+PC9EZXNjcmlwdGlvbj4KICAgICAgICAgICAgPC9JdGVtPgogICAgICAgICAgICA8SXRlbSBJRD0iMjEiPjxOYW1lPk1lbW9yeSBUb3RhbDwvTmFtZT4KICAgICAgICAgICAgICAgIDxPcGVyYXRpb25zPlI8L09wZXJhdGlvbnM+CiAgICAgICAgICAgICAgICA8TXVsdGlwbGVJbnN0YW5jZXM+U2luZ2xlPC9NdWx0aXBsZUluc3RhbmNlcz4KICAgICAgICAgICAgICAgIDxNYW5kYXRvcnk+T3B0aW9uYWw8L01hbmRhdG9yeT4KICAgICAgICAgICAgICAgIDxUeXBlPkludGVnZXI8L1R5cGU+CiAgICAgICAgICAgICAgICA8UmFuZ2VFbnVtZXJhdGlvbj48L1JhbmdlRW51bWVyYXRpb24+CiAgICAgICAgICAgICAgICA8VW5pdHM+PC9Vbml0cz4KICAgICAgICAgICAgICAgIDxEZXNjcmlwdGlvbj48IVtDREFUQVtUb3RhbCBhbW91bnQgb2Ygc3RvcmFnZSBzcGFjZSB3aGljaCBjYW4gc3RvcmUgZGF0YSBhbmQgc29mdHdhcmUgaW4gdGhlIEx3TTJNIERldmljZSAoZXhwcmVzc2VkIGluIGtpbG9ieXRlcykuICBOb3RlOiAxIGtpbG9ieXRlIGNvcnJlc3BvbmRzIHRvIDEwMDAgYnl0ZXMuXV0+PC9EZXNjcmlwdGlvbj4KICAgICAgICAgICAgPC9JdGVtPgogICAgICAgICAgICA8SXRlbSBJRD0iMjIiPjxOYW1lPkV4dERldkluZm88L05hbWU+CiAgICAgICAgICAgICAgICA8T3BlcmF0aW9ucz5SPC9PcGVyYXRpb25zPgogICAgICAgICAgICAgICAgPE11bHRpcGxlSW5zdGFuY2VzPk11bHRpcGxlPC9NdWx0aXBsZUluc3RhbmNlcz4KICAgICAgICAgICAgICAgIDxNYW5kYXRvcnk+T3B0aW9uYWw8L01hbmRhdG9yeT4KICAgICAgICAgICAgICAgIDxUeXBlPk9iamxuazwvVHlwZT4KICAgICAgICAgICAgICAgIDxSYW5nZUVudW1lcmF0aW9uPjwvUmFuZ2VFbnVtZXJhdGlvbj4KICAgICAgICAgICAgICAgIDxVbml0cz48L1VuaXRzPgogICAgICAgICAgICAgICAgPERlc2NyaXB0aW9uPjwhW0NEQVRBW1JlZmVyZW5jZSB0byBleHRlcm5hbCAiRGV2aWNlIiBvYmplY3QgaW5zdGFuY2UgY29udGFpbmluZyBpbmZvcm1hdGlvbi4gRm9yIGV4YW1wbGUsIHN1Y2ggYW4gZXh0ZXJuYWwgZGV2aWNlIGNhbiBiZSBhIEhvc3QgRGV2aWNlLCB3aGljaCBpcyBhIGRldmljZSBpbnRvIHdoaWNoIHRoZSBEZXZpY2UgY29udGFpbmluZyB0aGUgTHdNMk0gY2xpZW50IGlzIGVtYmVkZGVkLiBUaGlzIFJlc291cmNlIG1heSBiZSB1c2VkIHRvIHJldHJpZXZlIGluZm9ybWF0aW9uIGFib3V0IHRoZSBIb3N0IERldmljZS5dXT48L0Rlc2NyaXB0aW9uPgogICAgICAgICAgICA8L0l0ZW0+PC9SZXNvdXJjZXM+CiAgICAgICAgPERlc2NyaXB0aW9uMj48L0Rlc2NyaXB0aW9uMj4KICAgIDwvT2JqZWN0Pgo8L0xXTTJNPgo=" } \ No newline at end of file diff --git a/pom.xml b/pom.xml index b851b8fc10..736c49e80f 100755 --- a/pom.xml +++ b/pom.xml @@ -848,6 +848,8 @@ .run/** **/NetworkReceive.java **/lwm2m-registry/** + **/test/resources/lwm2m/** + **/resources/lwm2m/models/** src/main/data/resources/** From d25af150749816a53459433451c14d148ec2e8ca Mon Sep 17 00:00:00 2001 From: dashevchenko Date: Fri, 28 Feb 2025 15:15:00 +0200 Subject: [PATCH 126/345] fixed test --- .../thingsboard/server/msa/edqs/EdqsEntityDataQueryTest.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/msa/black-box-tests/src/test/java/org/thingsboard/server/msa/edqs/EdqsEntityDataQueryTest.java b/msa/black-box-tests/src/test/java/org/thingsboard/server/msa/edqs/EdqsEntityDataQueryTest.java index 8e77b315e6..1e3f3b1a60 100644 --- a/msa/black-box-tests/src/test/java/org/thingsboard/server/msa/edqs/EdqsEntityDataQueryTest.java +++ b/msa/black-box-tests/src/test/java/org/thingsboard/server/msa/edqs/EdqsEntityDataQueryTest.java @@ -113,7 +113,7 @@ public class EdqsEntityDataQueryTest extends AbstractContainerTest { EntityCountQuery query = new EntityCountQuery(allDeviceFilter); await("Waiting for total device count") .atMost(30, TimeUnit.SECONDS) - .until(() -> testRestClient.postCountDataQuery(query).equals(97L * 2)); + .until(() -> testRestClient.postCountDataQuery(query).compareTo(97L * 2) >= 0); testRestClient.getAndSetUserToken(tenantAdminId); await("Waiting for total device count") From f42e0e49b91f9587609e77f80ce0613286627627 Mon Sep 17 00:00:00 2001 From: Artem Dzhereleiko Date: Fri, 28 Feb 2025 15:16:13 +0200 Subject: [PATCH 127/345] UI: Processing settings for save attribute node --- ...dvanced-persistence-setting.component.html | 12 ++- .../advanced-persistence-setting.component.ts | 52 +++++++++-- .../action/attributes-config.component.html | 41 +++++++++ .../action/attributes-config.component.ts | 91 ++++++++++++++++++- .../action/attributes-config.model.ts | 59 ++++++++++++ .../action/timeseries-config.component.html | 1 + .../assets/locale/locale.constant-en_US.json | 16 ++++ 7 files changed, 257 insertions(+), 15 deletions(-) create mode 100644 ui-ngx/src/app/modules/home/components/rule-node/action/attributes-config.model.ts diff --git a/ui-ngx/src/app/modules/home/components/rule-node/action/advanced-persistence-setting.component.html b/ui-ngx/src/app/modules/home/components/rule-node/action/advanced-persistence-setting.component.html index 094f6dbc2a..f951a9f826 100644 --- a/ui-ngx/src/app/modules/home/components/rule-node/action/advanced-persistence-setting.component.html +++ b/ui-ngx/src/app/modules/home/components/rule-node/action/advanced-persistence-setting.component.html @@ -18,18 +18,22 @@
- - + - diff --git a/ui-ngx/src/app/modules/home/components/rule-node/action/advanced-persistence-setting.component.ts b/ui-ngx/src/app/modules/home/components/rule-node/action/advanced-persistence-setting.component.ts index 01314ec4f3..4609a7d010 100644 --- a/ui-ngx/src/app/modules/home/components/rule-node/action/advanced-persistence-setting.component.ts +++ b/ui-ngx/src/app/modules/home/components/rule-node/action/advanced-persistence-setting.component.ts @@ -19,12 +19,15 @@ import { FormBuilder, NG_VALIDATORS, NG_VALUE_ACCESSOR, + UntypedFormGroup, ValidationErrors, Validator } from '@angular/forms'; -import { Component, forwardRef } from '@angular/core'; +import { Component, DestroyRef, forwardRef, Input, OnInit } from '@angular/core'; import { takeUntilDestroyed } from '@angular/core/rxjs-interop'; import { AdvancedProcessingStrategy } from '@home/components/rule-node/action/timeseries-config.models'; +import { coerceBoolean } from '@shared/decorators/coercion'; +import { AttributeAdvancedProcessingStrategy } from '@home/components/rule-node/action/attributes-config.model'; @Component({ selector: 'tb-advanced-persistence-settings', @@ -39,19 +42,48 @@ import { AdvancedProcessingStrategy } from '@home/components/rule-node/action/ti multi: true }] }) -export class AdvancedPersistenceSettingComponent implements ControlValueAccessor, Validator { +export class AdvancedPersistenceSettingComponent implements OnInit, ControlValueAccessor, Validator { - persistenceForm = this.fb.group({ - timeseries: [null], - latest: [null], - webSockets: [null] - }); + @Input() + @coerceBoolean() + timeseries = false; + + @Input() + @coerceBoolean() + attribute = false; + + @Input() + @coerceBoolean() + latest = false; + + @Input() + @coerceBoolean() + webSockets = false; + + persistenceForm: UntypedFormGroup; private propagateChange: (value: any) => void = () => {}; - constructor(private fb: FormBuilder) { + constructor(private fb: FormBuilder, + private destroyRef: DestroyRef) { + } + + ngOnInit() { + this.persistenceForm = this.fb.group({}); + if (this.timeseries) { + this.persistenceForm.addControl('timeseries', this.fb.control(null, [])); + } + if (this.attribute) { + this.persistenceForm.addControl('attribute', this.fb.control(null, [])); + } + if (this.attribute) { + this.persistenceForm.addControl('latest', this.fb.control(null, [])); + } + if (this.attribute) { + this.persistenceForm.addControl('webSockets', this.fb.control(null, [])); + } this.persistenceForm.valueChanges.pipe( - takeUntilDestroyed() + takeUntilDestroyed(this.destroyRef) ).subscribe(value => this.propagateChange(value)); } @@ -76,7 +108,7 @@ export class AdvancedPersistenceSettingComponent implements ControlValueAccessor }; } - writeValue(value: AdvancedProcessingStrategy) { + writeValue(value: AdvancedProcessingStrategy | AttributeAdvancedProcessingStrategy) { this.persistenceForm.patchValue(value, {emitEvent: false}); } } diff --git a/ui-ngx/src/app/modules/home/components/rule-node/action/attributes-config.component.html b/ui-ngx/src/app/modules/home/components/rule-node/action/attributes-config.component.html index 03ea4fccdd..69feec1732 100644 --- a/ui-ngx/src/app/modules/home/components/rule-node/action/attributes-config.component.html +++ b/ui-ngx/src/app/modules/home/components/rule-node/action/attributes-config.component.html @@ -16,6 +16,47 @@ -->
+
+
+
+ rule-node-config.save-attribute.processing-settings +
+ + {{ 'rule-node-config.basic-mode' | translate}} + {{ 'rule-node-config.advanced-mode' | translate }} + +
+ @if(!attributesConfigForm.get('processingSettings.isAdvanced').value) { + + rule-node-config.save-attribute.strategy + + @for (strategy of persistenceStrategies; track strategy) { + {{ PersistenceTypeTranslationMap.get(strategy) | translate }} + } + + + + @if(attributesConfigForm.get('processingSettings.type').value === PersistenceType.DEDUPLICATE) { + + + } + } @else { + + } +
diff --git a/ui-ngx/src/app/modules/home/components/rule-node/action/attributes-config.component.ts b/ui-ngx/src/app/modules/home/components/rule-node/action/attributes-config.component.ts index 5a404c8de4..7257565608 100644 --- a/ui-ngx/src/app/modules/home/components/rule-node/action/attributes-config.component.ts +++ b/ui-ngx/src/app/modules/home/components/rule-node/action/attributes-config.component.ts @@ -15,10 +15,22 @@ /// import { Component } from '@angular/core'; -import { UntypedFormBuilder, UntypedFormGroup, Validators } from '@angular/forms'; +import { FormGroup, UntypedFormBuilder, UntypedFormGroup, Validators } from '@angular/forms'; import { RuleNodeConfiguration, RuleNodeConfigurationComponent } from '@shared/models/rule-node.models'; import { AttributeScope, telemetryTypeTranslations } from '@app/shared/models/telemetry/telemetry.models'; import { takeUntilDestroyed } from '@angular/core/rxjs-interop'; +import { + maxDeduplicateTimeSecs, + ProcessingSettings, + ProcessingSettingsForm, + ProcessingType, + ProcessingTypeTranslationMap +} from '@home/components/rule-node/action/timeseries-config.models'; +import { + AttributeNodeConfiguration, + AttributeNodeConfigurationForm, + defaultAttributeAdvancedPersistenceStrategy +} from '@home/components/rule-node/action/attributes-config.model'; @Component({ selector: 'tb-action-node-attributes-config', @@ -31,6 +43,12 @@ export class AttributesConfigComponent extends RuleNodeConfigurationComponent { attributeScopes = Object.keys(AttributeScope); telemetryTypeTranslationsMap = telemetryTypeTranslations; + PersistenceType = ProcessingType; + persistenceStrategies = [ProcessingType.ON_EVERY_MESSAGE, ProcessingType.DEDUPLICATE, ProcessingType.WEBSOCKETS_ONLY]; + PersistenceTypeTranslationMap = ProcessingTypeTranslationMap; + + maxDeduplicateTime = maxDeduplicateTimeSecs; + attributesConfigForm: UntypedFormGroup; constructor(private fb: UntypedFormBuilder) { @@ -41,8 +59,64 @@ export class AttributesConfigComponent extends RuleNodeConfigurationComponent { return this.attributesConfigForm; } + protected validatorTriggers(): string[] { + return ['processingSettings.isAdvanced', 'processingSettings.type']; + } + + protected prepareInputConfig(config: AttributeNodeConfiguration): AttributeNodeConfigurationForm { + let processingSettings: ProcessingSettingsForm; + if (config?.processingSettings) { + const isAdvanced = config?.processingSettings?.type === ProcessingType.ADVANCED; + processingSettings = { + type: isAdvanced ? ProcessingType.ON_EVERY_MESSAGE : config.processingSettings.type, + isAdvanced: isAdvanced, + deduplicationIntervalSecs: config.processingSettings?.deduplicationIntervalSecs ?? 60, + advanced: isAdvanced ? config.processingSettings : defaultAttributeAdvancedPersistenceStrategy + } + } else { + processingSettings = { + type: ProcessingType.ON_EVERY_MESSAGE, + isAdvanced: false, + deduplicationIntervalSecs: 60, + advanced: defaultAttributeAdvancedPersistenceStrategy + }; + } + return { + ...config, + processingSettings: processingSettings + } + } + + protected prepareOutputConfig(config: AttributeNodeConfigurationForm): AttributeNodeConfiguration { + let processingSettings: ProcessingSettings; + if (config.processingSettings.isAdvanced) { + processingSettings = { + ...config.processingSettings.advanced, + type: ProcessingType.ADVANCED + }; + } else { + processingSettings = { + type: config.processingSettings.type, + deduplicationIntervalSecs: config.processingSettings?.deduplicationIntervalSecs + }; + } + return { + ...config, + processingSettings + }; + } + protected onConfigurationSet(configuration: RuleNodeConfiguration) { this.attributesConfigForm = this.fb.group({ + processingSettings: this.fb.group({ + isAdvanced: [configuration?.processingSettings?.isAdvanced ?? false], + type: [configuration?.processingSettings?.type ?? ProcessingType.ON_EVERY_MESSAGE], + deduplicationIntervalSecs: [ + {value: configuration?.processingSettings?.deduplicationIntervalSecs ?? 60, disabled: true}, + [Validators.required, Validators.max(maxDeduplicateTimeSecs)] + ], + advanced: [{value: null, disabled: true}] + }), scope: [configuration ? configuration.scope : null, [Validators.required]], notifyDevice: [configuration ? configuration.notifyDevice : true, []], sendAttributesUpdatedNotification: [configuration ? configuration.sendAttributesUpdatedNotification : false, []], @@ -62,4 +136,19 @@ export class AttributesConfigComponent extends RuleNodeConfigurationComponent { }); } + protected updateValidators(emitEvent: boolean, _trigger?: string) { + const processingForm = this.attributesConfigForm.get('processingSettings') as FormGroup; + const isAdvanced: boolean = processingForm.get('isAdvanced').value; + const type: ProcessingType = processingForm.get('type').value; + if (!isAdvanced && type === ProcessingType.DEDUPLICATE) { + processingForm.get('deduplicationIntervalSecs').enable({emitEvent}); + } else { + processingForm.get('deduplicationIntervalSecs').disable({emitEvent}); + } + if (isAdvanced) { + processingForm.get('advanced').enable({emitEvent}); + } else { + processingForm.get('advanced').disable({emitEvent}); + } + } } diff --git a/ui-ngx/src/app/modules/home/components/rule-node/action/attributes-config.model.ts b/ui-ngx/src/app/modules/home/components/rule-node/action/attributes-config.model.ts new file mode 100644 index 0000000000..fb53a1ceaa --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/rule-node/action/attributes-config.model.ts @@ -0,0 +1,59 @@ +/// +/// 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 { DAY, SECOND } from '@shared/models/time/time.models'; +import { AttributeScope } from '@shared/models/telemetry/telemetry.models'; +import { BasicProcessingSettings, ProcessingType } from '@home/components/rule-node/action/timeseries-config.models'; + +export interface AttributeNodeConfiguration { + processingSettings: AttributeProcessingSettings; + scope: AttributeScope; + notifyDevice: boolean; + sendAttributesUpdatedNotification: boolean; + updateAttributesOnlyOnValueChange: boolean; +} + +export interface AttributeNodeConfigurationForm extends Omit { + processingSettings: AttributeProcessingSettingsForm +} + +export type AttributeProcessingSettings = BasicProcessingSettings & Partial & Partial; + +export type AttributeProcessingSettingsForm = Omit & { + isAdvanced: boolean; + advanced?: Partial; + type: ProcessingType; +}; + +export interface AttributeDeduplicateProcessingStrategy extends BasicProcessingSettings{ + deduplicationIntervalSecs: number; +} + +export interface AttributeAdvancedProcessingStrategy extends BasicProcessingSettings{ + attribute: AttributeAdvancedProcessingConfig; + webSockets: AttributeAdvancedProcessingConfig; +} + +export type AttributeAdvancedProcessingConfig = WithOptional; + +export const defaultAdvancedProcessingConfig: AttributeAdvancedProcessingConfig = { + type: ProcessingType.ON_EVERY_MESSAGE +} + +export const defaultAttributeAdvancedPersistenceStrategy: Omit = { + attribute: defaultAdvancedProcessingConfig, + webSockets: defaultAdvancedProcessingConfig, +} diff --git a/ui-ngx/src/app/modules/home/components/rule-node/action/timeseries-config.component.html b/ui-ngx/src/app/modules/home/components/rule-node/action/timeseries-config.component.html index 6e3e9fb6d1..250e66541b 100644 --- a/ui-ngx/src/app/modules/home/components/rule-node/action/timeseries-config.component.html +++ b/ui-ngx/src/app/modules/home/components/rule-node/action/timeseries-config.component.html @@ -53,6 +53,7 @@ }
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 18801bda2c..291cae525f 100644 --- a/ui-ngx/src/assets/locale/locale.constant-en_US.json +++ b/ui-ngx/src/assets/locale/locale.constant-en_US.json @@ -5155,6 +5155,22 @@ "latest": "Latest values", "web-sockets": "WebSockets" }, + "save-attribute": { + "processing-settings": "Processing settings", + "processing-settings-hint": "Define how incoming messages are processed. In Basic mode, select a preconfigured processing strategy or enable only WebSocket updates. Advanced mode allows you to select individual processing strategies for each action.", + "advanced-settings-hint": "Be cautious when configuring processing strategies. Certain combinations can lead to unexpected behavior.", + "strategy": "Strategy", + "deduplication-interval": "Deduplication interval", + "deduplication-interval-required": "Deduplication interval is required", + "deduplication-interval-min-max-range": "Deduplication interval should be at least 1 second and at most 1 day", + "strategy-type": { + "every-message": "On every message", + "skip": "Skip", + "deduplicate": "Deduplicate", + "web-sockets-only": "WebSockets only" + }, + "attribute": "Attribute" + }, "key-val": { "key": "Key", "value": "Value", From b90522cc9d61837330fb9d76edb3edf382eaafeb Mon Sep 17 00:00:00 2001 From: Dmytro Skarzhynets Date: Fri, 28 Feb 2025 15:56:21 +0200 Subject: [PATCH 128/345] Save time series strategies: tests for inactivity timeout update notification --- .../DefaultTelemetrySubscriptionService.java | 2 +- .../DefaultTbCoreConsumerServiceTest.java | 93 +++++++++++ .../state/DefaultDeviceStateManagerTest.java | 25 +++ ...faultTelemetrySubscriptionServiceTest.java | 158 ++++++++++++++++++ 4 files changed, 277 insertions(+), 1 deletion(-) diff --git a/application/src/main/java/org/thingsboard/server/service/telemetry/DefaultTelemetrySubscriptionService.java b/application/src/main/java/org/thingsboard/server/service/telemetry/DefaultTelemetrySubscriptionService.java index 54e87348bb..b643413bfa 100644 --- a/application/src/main/java/org/thingsboard/server/service/telemetry/DefaultTelemetrySubscriptionService.java +++ b/application/src/main/java/org/thingsboard/server/service/telemetry/DefaultTelemetrySubscriptionService.java @@ -153,7 +153,7 @@ public class DefaultTelemetrySubscriptionService extends AbstractSubscriptionSer saveFuture = Futures.immediateFuture(0); } - if (entityId.getEntityType() == EntityType.DEVICE && request.getStrategy().saveLatest()) { + if (entityId.getEntityType() == EntityType.DEVICE && request.getStrategy().saveLatest() /* Device State Service reads from the latest values when initializing */) { findNewInactivityTimeout(request.getEntries()).ifPresent(newInactivityTimeout -> addMainCallback(saveFuture, __ -> deviceStateManager.onDeviceInactivityTimeoutUpdate( tenantId, new DeviceId(entityId.getId()), newInactivityTimeout, TbCallback.EMPTY) diff --git a/application/src/test/java/org/thingsboard/server/service/queue/DefaultTbCoreConsumerServiceTest.java b/application/src/test/java/org/thingsboard/server/service/queue/DefaultTbCoreConsumerServiceTest.java index fe222237c6..26832f6176 100644 --- a/application/src/test/java/org/thingsboard/server/service/queue/DefaultTbCoreConsumerServiceTest.java +++ b/application/src/test/java/org/thingsboard/server/service/queue/DefaultTbCoreConsumerServiceTest.java @@ -532,6 +532,98 @@ public class DefaultTbCoreConsumerServiceTest { then(statsMock).should(never()).log(inactivityMsg); } + @Test + public void givenProcessingSuccess_whenForwardingInactivityTimeoutUpdateMsgToStateService_thenOnSuccessCallbackIsCalled() { + // GIVEN + var inactivityTimeoutUpdateMsg = TransportProtos.DeviceInactivityTimeoutUpdateProto.newBuilder() + .setTenantIdMSB(tenantId.getId().getMostSignificantBits()) + .setTenantIdLSB(tenantId.getId().getLeastSignificantBits()) + .setDeviceIdMSB(deviceId.getId().getMostSignificantBits()) + .setDeviceIdLSB(deviceId.getId().getLeastSignificantBits()) + .setInactivityTimeout(time) + .build(); + + doCallRealMethod().when(defaultTbCoreConsumerServiceMock).forwardToStateService(inactivityTimeoutUpdateMsg, tbCallbackMock); + + // WHEN + defaultTbCoreConsumerServiceMock.forwardToStateService(inactivityTimeoutUpdateMsg, tbCallbackMock); + + // THEN + then(stateServiceMock).should().onDeviceInactivityTimeoutUpdate(tenantId, deviceId, time); + then(tbCallbackMock).should().onSuccess(); + then(tbCallbackMock).should(never()).onFailure(any()); + } + + @Test + public void givenProcessingFailure_whenForwardingInactivityTimeoutUpdateMsgToStateService_thenOnFailureCallbackIsCalled() { + // GIVEN + var inactivityTimeoutUpdateMsg = TransportProtos.DeviceInactivityTimeoutUpdateProto.newBuilder() + .setTenantIdMSB(tenantId.getId().getMostSignificantBits()) + .setTenantIdLSB(tenantId.getId().getLeastSignificantBits()) + .setDeviceIdMSB(deviceId.getId().getMostSignificantBits()) + .setDeviceIdLSB(deviceId.getId().getLeastSignificantBits()) + .setInactivityTimeout(time) + .build(); + + doCallRealMethod().when(defaultTbCoreConsumerServiceMock).forwardToStateService(inactivityTimeoutUpdateMsg, tbCallbackMock); + + var runtimeException = new RuntimeException("Something bad happened!"); + doThrow(runtimeException).when(stateServiceMock).onDeviceInactivityTimeoutUpdate(tenantId, deviceId, time); + + // WHEN + defaultTbCoreConsumerServiceMock.forwardToStateService(inactivityTimeoutUpdateMsg, tbCallbackMock); + + // THEN + then(tbCallbackMock).should(never()).onSuccess(); + then(tbCallbackMock).should().onFailure(runtimeException); + } + + @Test + public void givenStatsEnabled_whenForwardingInactivityTimeoutUpdateMsgToStateService_thenStatsAreRecorded() { + // GIVEN + ReflectionTestUtils.setField(defaultTbCoreConsumerServiceMock, "stats", statsMock); + ReflectionTestUtils.setField(defaultTbCoreConsumerServiceMock, "statsEnabled", true); + + var inactivityTimeoutUpdateMsg = TransportProtos.DeviceInactivityTimeoutUpdateProto.newBuilder() + .setTenantIdMSB(tenantId.getId().getMostSignificantBits()) + .setTenantIdLSB(tenantId.getId().getLeastSignificantBits()) + .setDeviceIdMSB(deviceId.getId().getMostSignificantBits()) + .setDeviceIdLSB(deviceId.getId().getLeastSignificantBits()) + .setInactivityTimeout(time) + .build(); + + doCallRealMethod().when(defaultTbCoreConsumerServiceMock).forwardToStateService(inactivityTimeoutUpdateMsg, tbCallbackMock); + + // WHEN + defaultTbCoreConsumerServiceMock.forwardToStateService(inactivityTimeoutUpdateMsg, tbCallbackMock); + + // THEN + then(statsMock).should().log(inactivityTimeoutUpdateMsg); + } + + @Test + public void givenStatsDisabled_whenForwardingInactivityTimeoutUpdateMsgToStateService_thenStatsAreNotRecorded() { + // GIVEN + ReflectionTestUtils.setField(defaultTbCoreConsumerServiceMock, "stats", statsMock); + ReflectionTestUtils.setField(defaultTbCoreConsumerServiceMock, "statsEnabled", false); + + var inactivityTimeoutUpdateMsg = TransportProtos.DeviceInactivityTimeoutUpdateProto.newBuilder() + .setTenantIdMSB(tenantId.getId().getMostSignificantBits()) + .setTenantIdLSB(tenantId.getId().getLeastSignificantBits()) + .setDeviceIdMSB(deviceId.getId().getMostSignificantBits()) + .setDeviceIdLSB(deviceId.getId().getLeastSignificantBits()) + .setInactivityTimeout(time) + .build(); + + doCallRealMethod().when(defaultTbCoreConsumerServiceMock).forwardToStateService(inactivityTimeoutUpdateMsg, tbCallbackMock); + + // WHEN + defaultTbCoreConsumerServiceMock.forwardToStateService(inactivityTimeoutUpdateMsg, tbCallbackMock); + + // THEN + then(statsMock).should(never()).log(inactivityTimeoutUpdateMsg); + } + @Test public void givenRestApiCallResponseMsgProto_whenForwardToRuleEngineCallService_thenCallOnQueueMsg() { // GIVEN @@ -545,4 +637,5 @@ public class DefaultTbCoreConsumerServiceTest { // THEN then(ruleEngineCallServiceMock).should().onQueueMsg(restApiCallResponseMsgProto, tbCallbackMock); } + } diff --git a/application/src/test/java/org/thingsboard/server/service/state/DefaultDeviceStateManagerTest.java b/application/src/test/java/org/thingsboard/server/service/state/DefaultDeviceStateManagerTest.java index 8d2a747d2d..6cf0ab9053 100644 --- a/application/src/test/java/org/thingsboard/server/service/state/DefaultDeviceStateManagerTest.java +++ b/application/src/test/java/org/thingsboard/server/service/state/DefaultDeviceStateManagerTest.java @@ -123,6 +123,10 @@ public class DefaultDeviceStateManagerTest { Arguments.of( (BiConsumer) (deviceStateManager, tbCallbackMock) -> deviceStateManager.onDeviceInactivity(TENANT_ID, DEVICE_ID, EVENT_TS, tbCallbackMock), (Consumer) deviceStateServiceMock -> then(deviceStateServiceMock).should().onDeviceInactivity(TENANT_ID, DEVICE_ID, EVENT_TS) + ), + Arguments.of( + (BiConsumer) (deviceStateManager, tbCallbackMock) -> deviceStateManager.onDeviceInactivityTimeoutUpdate(TENANT_ID, DEVICE_ID, EVENT_TS, tbCallbackMock), + (Consumer) deviceStateServiceMock -> then(deviceStateServiceMock).should().onDeviceInactivityTimeoutUpdate(TENANT_ID, DEVICE_ID, EVENT_TS) ) ); } @@ -172,6 +176,11 @@ public class DefaultDeviceStateManagerTest { (Consumer) deviceStateServiceMock -> doThrow(RUNTIME_EXCEPTION).when(deviceStateServiceMock).onDeviceInactivity(TENANT_ID, DEVICE_ID, EVENT_TS), (BiConsumer) (deviceStateManager, tbCallbackMock) -> deviceStateManager.onDeviceInactivity(TENANT_ID, DEVICE_ID, EVENT_TS, tbCallbackMock), (Consumer) deviceStateServiceMock -> then(deviceStateServiceMock).should().onDeviceInactivity(TENANT_ID, DEVICE_ID, EVENT_TS) + ), + Arguments.of( + (Consumer) deviceStateServiceMock -> doThrow(RUNTIME_EXCEPTION).when(deviceStateServiceMock).onDeviceInactivityTimeoutUpdate(TENANT_ID, DEVICE_ID, EVENT_TS), + (BiConsumer) (deviceStateManager, tbCallbackMock) -> deviceStateManager.onDeviceInactivityTimeoutUpdate(TENANT_ID, DEVICE_ID, EVENT_TS, tbCallbackMock), + (Consumer) deviceStateServiceMock -> then(deviceStateServiceMock).should().onDeviceInactivityTimeoutUpdate(TENANT_ID, DEVICE_ID, EVENT_TS) ) ); } @@ -265,6 +274,22 @@ public class DefaultDeviceStateManagerTest { .build(); then(clusterServiceMock).should().pushMsgToCore(eq(EXTERNAL_TPI), any(UUID.class), eq(toCoreMsg), queueCallbackCaptor.capture()); } + ), + Arguments.of( + (BiConsumer) (deviceStateManager, tbCallbackMock) -> deviceStateManager.onDeviceInactivityTimeoutUpdate(TENANT_ID, DEVICE_ID, EVENT_TS, tbCallbackMock), + (BiConsumer>) (clusterServiceMock, queueCallbackCaptor) -> { + var deviceInactivityTimeoutUpdateMsg = TransportProtos.DeviceInactivityTimeoutUpdateProto.newBuilder() + .setTenantIdMSB(TENANT_ID.getId().getMostSignificantBits()) + .setTenantIdLSB(TENANT_ID.getId().getLeastSignificantBits()) + .setDeviceIdMSB(DEVICE_ID.getId().getMostSignificantBits()) + .setDeviceIdLSB(DEVICE_ID.getId().getLeastSignificantBits()) + .setInactivityTimeout(EVENT_TS) + .build(); + var toCoreMsg = TransportProtos.ToCoreMsg.newBuilder() + .setDeviceInactivityTimeoutUpdateMsg(deviceInactivityTimeoutUpdateMsg) + .build(); + then(clusterServiceMock).should().pushMsgToCore(eq(EXTERNAL_TPI), any(UUID.class), eq(toCoreMsg), queueCallbackCaptor.capture()); + } ) ); } diff --git a/application/src/test/java/org/thingsboard/server/service/telemetry/DefaultTelemetrySubscriptionServiceTest.java b/application/src/test/java/org/thingsboard/server/service/telemetry/DefaultTelemetrySubscriptionServiceTest.java index c04b92e6e4..5537cfbb4b 100644 --- a/application/src/test/java/org/thingsboard/server/service/telemetry/DefaultTelemetrySubscriptionServiceTest.java +++ b/application/src/test/java/org/thingsboard/server/service/telemetry/DefaultTelemetrySubscriptionServiceTest.java @@ -25,6 +25,7 @@ import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; import org.junit.jupiter.params.ParameterizedTest; import org.junit.jupiter.params.provider.Arguments; +import org.junit.jupiter.params.provider.EnumSource; import org.junit.jupiter.params.provider.MethodSource; import org.mockito.Mock; import org.mockito.junit.jupiter.MockitoExtension; @@ -35,15 +36,19 @@ import org.thingsboard.server.cluster.TbClusterService; import org.thingsboard.server.common.data.ApiUsageRecordKey; import org.thingsboard.server.common.data.ApiUsageState; import org.thingsboard.server.common.data.ApiUsageStateValue; +import org.thingsboard.server.common.data.EntityType; import org.thingsboard.server.common.data.EntityView; +import org.thingsboard.server.common.data.id.ApiUsageStateId; import org.thingsboard.server.common.data.id.CustomerId; import org.thingsboard.server.common.data.id.DeviceId; import org.thingsboard.server.common.data.id.EntityId; +import org.thingsboard.server.common.data.id.EntityIdFactory; import org.thingsboard.server.common.data.id.EntityViewId; import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.common.data.kv.BasicTsKvEntry; import org.thingsboard.server.common.data.kv.DoubleDataEntry; import org.thingsboard.server.common.data.kv.KvEntry; +import org.thingsboard.server.common.data.kv.LongDataEntry; import org.thingsboard.server.common.data.kv.TsKvEntry; import org.thingsboard.server.common.data.objects.AttributesEntityView; import org.thingsboard.server.common.data.objects.TelemetryEntityView; @@ -72,11 +77,16 @@ import java.util.concurrent.ExecutorService; import java.util.stream.LongStream; import java.util.stream.Stream; +import static com.google.common.util.concurrent.Futures.immediateFailedFuture; import static com.google.common.util.concurrent.Futures.immediateFuture; import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyLong; import static org.mockito.BDDMockito.given; import static org.mockito.BDDMockito.then; import static org.mockito.Mockito.lenient; +import static org.mockito.Mockito.never; @ExtendWith(MockitoExtension.class) class DefaultTelemetrySubscriptionServiceTest { @@ -368,6 +378,154 @@ class DefaultTelemetrySubscriptionServiceTest { ); } + @Test + void shouldThrowErrorWhenTryingToSaveTimeseriesForApiUsageState() { + // GIVEN + var request = TimeseriesSaveRequest.builder() + .tenantId(tenantId) + .customerId(customerId) + .entityId(new ApiUsageStateId(UUID.randomUUID())) + .entries(sampleTelemetry) + .strategy(TimeseriesSaveRequest.Strategy.SAVE_ALL) + .callback(emptyCallback) + .build(); + + // WHEN + assertThatThrownBy(() -> telemetryService.saveTimeseries(request)) + .isInstanceOf(RuntimeException.class) + .hasMessage("Can't update API Usage State!"); + + // THEN + then(tsService).shouldHaveNoInteractions(); + then(deviceStateManager).shouldHaveNoInteractions(); + } + + @Test + void shouldNotifyDeviceStateManagerWhenDeviceInactivityTimeoutTimeseriesWasSavedToLatest() { + // GIVEN + var deviceId = DeviceId.fromString("cc51e450-53e1-11ee-883e-e56b48fd2088"); + var inactivityTimeout = new BasicTsKvEntry(123L, new LongDataEntry("inactivityTimeout", 5000L)); + + var request = TimeseriesSaveRequest.builder() + .tenantId(tenantId) + .customerId(customerId) + .entityId(deviceId) + .entry(inactivityTimeout) + .strategy(new TimeseriesSaveRequest.Strategy(false, true, false)) + .callback(emptyCallback) + .build(); + + given(tsService.saveLatest(tenantId, deviceId, List.of(inactivityTimeout))).willReturn(immediateFuture(listOfNNumbers(1))); + + // WHEN + telemetryService.saveTimeseries(request); + + // THEN + then(deviceStateManager).should().onDeviceInactivityTimeoutUpdate(tenantId, deviceId, 5000L, TbCallback.EMPTY); + } + + @ParameterizedTest + @EnumSource( + value = EntityType.class, + names = {"DEVICE", "API_USAGE_STATE"}, // API usage state excluded due to coverage in another test + mode = EnumSource.Mode.EXCLUDE + ) + void shouldNotNotifyDeviceStateManagerWhenInactivityTimeoutTimeseriesWasUpdatedButEntityTypeIsNotDevice(EntityType entityType) { + // GIVEN + var nonDeviceId = EntityIdFactory.getByTypeAndUuid(entityType, "cc51e450-53e1-11ee-883e-e56b48fd2088"); + var inactivityTimeout = new BasicTsKvEntry(123L, new LongDataEntry("inactivityTimeout", 5000L)); + + var request = TimeseriesSaveRequest.builder() + .tenantId(tenantId) + .customerId(customerId) + .entityId(nonDeviceId) + .entry(inactivityTimeout) + .strategy(new TimeseriesSaveRequest.Strategy(false, true, false)) + .callback(emptyCallback) + .build(); + + given(tsService.saveLatest(tenantId, nonDeviceId, List.of(inactivityTimeout))).willReturn(immediateFuture(listOfNNumbers(1))); + lenient().when(tbEntityViewService.findEntityViewsByTenantIdAndEntityIdAsync(tenantId, nonDeviceId)).thenReturn(immediateFuture(Collections.emptyList())); + + // WHEN + telemetryService.saveTimeseries(request); + + // THEN + then(deviceStateManager).should(never()).onDeviceInactivityTimeoutUpdate(any(), any(), anyLong(), any()); + } + + @Test + void shouldNotNotifyDeviceStateManagerWhenDeviceInactivityTimeoutTimeseriesWasNotSavedToLatest() { + // GIVEN + var deviceId = DeviceId.fromString("cc51e450-53e1-11ee-883e-e56b48fd2088"); + var inactivityTimeout = new BasicTsKvEntry(123L, new LongDataEntry("inactivityTimeout", 5000L)); + + var request = TimeseriesSaveRequest.builder() + .tenantId(tenantId) + .customerId(customerId) + .entityId(deviceId) + .entry(inactivityTimeout) + .strategy(new TimeseriesSaveRequest.Strategy(true, false, true)) + .callback(emptyCallback) + .build(); + + given(tsService.saveWithoutLatest(tenantId, deviceId, List.of(inactivityTimeout), 0L)).willReturn(immediateFuture(1)); + + // WHEN + telemetryService.saveTimeseries(request); + + // THEN + then(deviceStateManager).should(never()).onDeviceInactivityTimeoutUpdate(any(), any(), anyLong(), any()); + } + + @Test + void shouldNotNotifyDeviceStateManagerWhenInactivityTimeoutTimeseriesWasNotUpdated() { + // GIVEN + var deviceId = DeviceId.fromString("cc51e450-53e1-11ee-883e-e56b48fd2088"); + var notInactivityTimeout = new BasicTsKvEntry(123L, new LongDataEntry("notInactivityTimeout", 5000L)); + + var request = TimeseriesSaveRequest.builder() + .tenantId(tenantId) + .customerId(customerId) + .entityId(deviceId) + .entry(notInactivityTimeout) + .strategy(new TimeseriesSaveRequest.Strategy(false, true, false)) + .callback(emptyCallback) + .build(); + + given(tsService.saveLatest(tenantId, deviceId, List.of(notInactivityTimeout))).willReturn(immediateFuture(listOfNNumbers(1))); + + // WHEN + telemetryService.saveTimeseries(request); + + // THEN + then(deviceStateManager).should(never()).onDeviceInactivityTimeoutUpdate(any(), any(), anyLong(), any()); + } + + @Test + void shouldNotNotifyDeviceStateManagerWhenDeviceInactivityTimeoutTimeseriesSaveFailed() { + // GIVEN + var deviceId = DeviceId.fromString("cc51e450-53e1-11ee-883e-e56b48fd2088"); + var inactivityTimeout = new BasicTsKvEntry(123L, new LongDataEntry("inactivityTimeout", 5000L)); + + var request = TimeseriesSaveRequest.builder() + .tenantId(tenantId) + .customerId(customerId) + .entityId(deviceId) + .entry(inactivityTimeout) + .strategy(new TimeseriesSaveRequest.Strategy(false, true, false)) + .callback(emptyCallback) + .build(); + + given(tsService.saveLatest(tenantId, deviceId, List.of(inactivityTimeout))).willReturn(immediateFailedFuture(new RuntimeException("failed to save"))); + + // WHEN + telemetryService.saveTimeseries(request); + + // THEN + then(deviceStateManager).should(never()).onDeviceInactivityTimeoutUpdate(any(), any(), anyLong(), any()); + } + // used to emulate sequence numbers returned by save latest API private static List listOfNNumbers(int N) { return LongStream.range(0, N).boxed().toList(); From 56ba30a4e4f3432023b32f92652fa93f3d7a25ca Mon Sep 17 00:00:00 2001 From: Volodymyr Babak Date: Fri, 28 Feb 2025 16:19:35 +0200 Subject: [PATCH 129/345] Use CLIENT_SCOPE by default for Push to cloud node --- .../main/data/json/edge/rule_chains/edge_root_rule_chain.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/application/src/main/data/json/edge/rule_chains/edge_root_rule_chain.json b/application/src/main/data/json/edge/rule_chains/edge_root_rule_chain.json index 6701b59e0e..ef0bf2698f 100644 --- a/application/src/main/data/json/edge/rule_chains/edge_root_rule_chain.json +++ b/application/src/main/data/json/edge/rule_chains/edge_root_rule_chain.json @@ -119,7 +119,7 @@ "type": "org.thingsboard.rule.engine.edge.TbMsgPushToCloudNode", "name": "Push to cloud", "configuration": { - "scope": "SERVER_SCOPE" + "scope": "CLIENT_SCOPE" }, "externalId": null }, From 5372f22b8a314e84823d39cb4a19a762f206cf59 Mon Sep 17 00:00:00 2001 From: mpetrov Date: Fri, 28 Feb 2025 17:08:24 +0200 Subject: [PATCH 130/345] Added latest debug event call in Calculated Field Dialog on Test function --- .../core/http/calculated-fields.service.ts | 5 +++++ .../calculated-field-dialog.component.ts | 22 ++++++++++++++----- 2 files changed, 22 insertions(+), 5 deletions(-) diff --git a/ui-ngx/src/app/core/http/calculated-fields.service.ts b/ui-ngx/src/app/core/http/calculated-fields.service.ts index fe5b0f7b52..66c0cb609e 100644 --- a/ui-ngx/src/app/core/http/calculated-fields.service.ts +++ b/ui-ngx/src/app/core/http/calculated-fields.service.ts @@ -23,6 +23,7 @@ import { CalculatedField, CalculatedFieldTestScriptInputParams } from '@shared/m import { PageLink } from '@shared/models/page/page-link'; import { EntityId } from '@shared/models/id/entity-id'; import { EntityTestScriptResult } from '@shared/models/entity.models'; +import { CalculatedFieldEventBody } from '@shared/models/event.models'; @Injectable({ providedIn: 'root' @@ -53,4 +54,8 @@ export class CalculatedFieldsService { public testScript(inputParams: CalculatedFieldTestScriptInputParams, config?: RequestConfig): Observable { return this.http.post('/api/calculatedField/testScript', inputParams, defaultHttpOptionsFromConfig(config)); } + + public getLatestCalculatedFieldDebugEvent(id: string, config?: RequestConfig): Observable { + return this.http.get(`/api/calculatedField/${id}/debug`, defaultHttpOptionsFromConfig(config)); + } } diff --git a/ui-ngx/src/app/modules/home/components/calculated-fields/components/dialog/calculated-field-dialog.component.ts b/ui-ngx/src/app/modules/home/components/calculated-fields/components/dialog/calculated-field-dialog.component.ts index c3e3d52a72..65654efd70 100644 --- a/ui-ngx/src/app/modules/home/components/calculated-fields/components/dialog/calculated-field-dialog.component.ts +++ b/ui-ngx/src/app/modules/home/components/calculated-fields/components/dialog/calculated-field-dialog.component.ts @@ -35,7 +35,7 @@ import { import { noLeadTrailSpacesRegex } from '@shared/models/regex.constants'; import { AttributeScope } from '@shared/models/telemetry/telemetry.models'; import { EntityType } from '@shared/models/entity-type.models'; -import { map, startWith } from 'rxjs/operators'; +import { map, startWith, switchMap } from 'rxjs/operators'; import { takeUntilDestroyed } from '@angular/core/rxjs-interop'; import { ScriptLanguage } from '@shared/models/rule-node.models'; import { CalculatedFieldsService } from '@core/http/calculated-fields.service'; @@ -136,10 +136,22 @@ export class CalculatedFieldDialogComponent extends DialogComponent { - this.configFormGroup.get('expressionSCRIPT').setValue(expression); - this.configFormGroup.get('expressionSCRIPT').markAsDirty(); - }); + const calculatedFieldId = this.data.value?.id?.id; + + (calculatedFieldId + ? this.calculatedFieldsService.getLatestCalculatedFieldDebugEvent(calculatedFieldId) + .pipe( + switchMap(event => { + const args = event?.arguments ? JSON.parse(event.arguments) : null; + return this.data.getTestScriptDialogFn(this.fromGroupValue, args, false); + }), + takeUntilDestroyed(this.destroyRef) + ) + : this.data.getTestScriptDialogFn(this.fromGroupValue, null, false)) + .subscribe(expression => { + this.configFormGroup.get('expressionSCRIPT').setValue(expression); + this.configFormGroup.get('expressionSCRIPT').markAsDirty(); + }); } private applyDialogData(): void { From 99beae211577c39944c136e411ab15d9e8b6f0a2 Mon Sep 17 00:00:00 2001 From: mpetrov Date: Fri, 28 Feb 2025 17:17:55 +0200 Subject: [PATCH 131/345] refactoring --- .../calculated-field-dialog.component.ts | 19 ++++++++++++------- 1 file changed, 12 insertions(+), 7 deletions(-) diff --git a/ui-ngx/src/app/modules/home/components/calculated-fields/components/dialog/calculated-field-dialog.component.ts b/ui-ngx/src/app/modules/home/components/calculated-fields/components/dialog/calculated-field-dialog.component.ts index 65654efd70..68abd646e3 100644 --- a/ui-ngx/src/app/modules/home/components/calculated-fields/components/dialog/calculated-field-dialog.component.ts +++ b/ui-ngx/src/app/modules/home/components/calculated-fields/components/dialog/calculated-field-dialog.component.ts @@ -39,6 +39,7 @@ import { map, startWith, switchMap } from 'rxjs/operators'; import { takeUntilDestroyed } from '@angular/core/rxjs-interop'; import { ScriptLanguage } from '@shared/models/rule-node.models'; import { CalculatedFieldsService } from '@core/http/calculated-fields.service'; +import { Observable } from 'rxjs'; @Component({ selector: 'tb-calculated-field-dialog', @@ -137,9 +138,10 @@ export class CalculatedFieldDialogComponent extends DialogComponent; - (calculatedFieldId - ? this.calculatedFieldsService.getLatestCalculatedFieldDebugEvent(calculatedFieldId) + if (calculatedFieldId) { + testScriptDialogResult$ = this.calculatedFieldsService.getLatestCalculatedFieldDebugEvent(calculatedFieldId) .pipe( switchMap(event => { const args = event?.arguments ? JSON.parse(event.arguments) : null; @@ -147,11 +149,14 @@ export class CalculatedFieldDialogComponent extends DialogComponent { - this.configFormGroup.get('expressionSCRIPT').setValue(expression); - this.configFormGroup.get('expressionSCRIPT').markAsDirty(); - }); + } else { + testScriptDialogResult$ = this.data.getTestScriptDialogFn(this.fromGroupValue, null, false); + } + + testScriptDialogResult$.subscribe(expression => { + this.configFormGroup.get('expressionSCRIPT').setValue(expression); + this.configFormGroup.get('expressionSCRIPT').markAsDirty(); + }); } private applyDialogData(): void { From faff0ea8c2ca937deeac1b2b33c73c472abe1ef5 Mon Sep 17 00:00:00 2001 From: ViacheslavKlimov Date: Fri, 28 Feb 2025 17:22:53 +0200 Subject: [PATCH 132/345] Refactor TbRocksDb usage --- .../server/service/cf/CfRocksDb.java | 11 ++- .../thingsboard/server/utils/TbRocksDb.java | 71 ------------------- .../server/edqs/util/EdqsRocksDb.java | 5 +- .../server/edqs/util/TbRocksDb.java | 31 +++++--- 4 files changed, 33 insertions(+), 85 deletions(-) delete mode 100644 application/src/main/java/org/thingsboard/server/utils/TbRocksDb.java diff --git a/application/src/main/java/org/thingsboard/server/service/cf/CfRocksDb.java b/application/src/main/java/org/thingsboard/server/service/cf/CfRocksDb.java index cb6d3f0d6b..f95227bc24 100644 --- a/application/src/main/java/org/thingsboard/server/service/cf/CfRocksDb.java +++ b/application/src/main/java/org/thingsboard/server/service/cf/CfRocksDb.java @@ -15,22 +15,29 @@ */ package org.thingsboard.server.service.cf; +import jakarta.annotation.PostConstruct; import jakarta.annotation.PreDestroy; import org.rocksdb.Options; import org.rocksdb.WriteOptions; import org.springframework.beans.factory.annotation.Value; import org.springframework.boot.autoconfigure.condition.ConditionalOnExpression; import org.springframework.stereotype.Component; -import org.thingsboard.server.utils.TbRocksDb; +import org.thingsboard.server.edqs.util.TbRocksDb; @Component @ConditionalOnExpression("'${queue.type:null}'=='in-memory'") public class CfRocksDb extends TbRocksDb { - public CfRocksDb(@Value("${queue.calculated_fields.rocks_db_path:${user.home}/.rocksdb/cf_states}") String path) throws Exception { + public CfRocksDb(@Value("${queue.calculated_fields.rocks_db_path:${user.home}/.rocksdb/cf_states}") String path) { super(path, new Options().setCreateIfMissing(true), new WriteOptions().setSync(true)); } + @PostConstruct + @Override + public void init() { + super.init(); + } + @PreDestroy @Override public void close() { diff --git a/application/src/main/java/org/thingsboard/server/utils/TbRocksDb.java b/application/src/main/java/org/thingsboard/server/utils/TbRocksDb.java deleted file mode 100644 index 5c4fe185e5..0000000000 --- a/application/src/main/java/org/thingsboard/server/utils/TbRocksDb.java +++ /dev/null @@ -1,71 +0,0 @@ -/** - * 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.utils; - -import lombok.SneakyThrows; -import org.rocksdb.Options; -import org.rocksdb.RocksDB; -import org.rocksdb.RocksIterator; -import org.rocksdb.WriteOptions; - -import java.nio.charset.StandardCharsets; -import java.nio.file.Files; -import java.nio.file.Path; -import java.util.function.BiConsumer; - -public class TbRocksDb { - - protected final String path; - private final WriteOptions writeOptions; - protected final RocksDB db; - - static { - RocksDB.loadLibrary(); - } - - public TbRocksDb(String path, Options dbOptions, WriteOptions writeOptions) throws Exception { - this.path = path; - this.writeOptions = writeOptions; - Files.createDirectories(Path.of(path).getParent()); - this.db = RocksDB.open(dbOptions, path); - } - - @SneakyThrows - public void put(String key, byte[] value) { - db.put(writeOptions, key.getBytes(StandardCharsets.UTF_8), value); - } - - public void forEach(BiConsumer processor) { - try (RocksIterator iterator = db.newIterator()) { - for (iterator.seekToFirst(); iterator.isValid(); iterator.next()) { - String key = new String(iterator.key(), StandardCharsets.UTF_8); - processor.accept(key, iterator.value()); - } - } - } - - @SneakyThrows - public void delete(String key) { - db.delete(writeOptions, key.getBytes(StandardCharsets.UTF_8)); - } - - public void close() { - if (db != null) { - db.close(); - } - } - -} \ No newline at end of file diff --git a/common/edqs/src/main/java/org/thingsboard/server/edqs/util/EdqsRocksDb.java b/common/edqs/src/main/java/org/thingsboard/server/edqs/util/EdqsRocksDb.java index ae1436e6ca..4a991432c7 100644 --- a/common/edqs/src/main/java/org/thingsboard/server/edqs/util/EdqsRocksDb.java +++ b/common/edqs/src/main/java/org/thingsboard/server/edqs/util/EdqsRocksDb.java @@ -19,6 +19,7 @@ import jakarta.annotation.PostConstruct; import jakarta.annotation.PreDestroy; import lombok.Getter; import org.rocksdb.Options; +import org.rocksdb.WriteOptions; import org.springframework.beans.factory.annotation.Value; import org.springframework.stereotype.Component; import org.thingsboard.server.queue.edqs.InMemoryEdqsComponent; @@ -34,16 +35,18 @@ public class EdqsRocksDb extends TbRocksDb { private boolean isNew; public EdqsRocksDb(@Value("${queue.edqs.local.rocksdb_path:${user.home}/.rocksdb/edqs}") String path) { - super(path, new Options().setCreateIfMissing(true)); + super(path, new Options().setCreateIfMissing(true), new WriteOptions()); } @PostConstruct + @Override public void init() { isNew = !Files.exists(Path.of(path)); super.init(); } @PreDestroy + @Override public void close() { super.close(); } diff --git a/common/edqs/src/main/java/org/thingsboard/server/edqs/util/TbRocksDb.java b/common/edqs/src/main/java/org/thingsboard/server/edqs/util/TbRocksDb.java index cd5ba0c9d9..23f2fa2c9e 100644 --- a/common/edqs/src/main/java/org/thingsboard/server/edqs/util/TbRocksDb.java +++ b/common/edqs/src/main/java/org/thingsboard/server/edqs/util/TbRocksDb.java @@ -15,35 +15,43 @@ */ package org.thingsboard.server.edqs.util; -import lombok.RequiredArgsConstructor; import lombok.SneakyThrows; import org.rocksdb.Options; import org.rocksdb.RocksDB; -import org.rocksdb.RocksDBException; import org.rocksdb.RocksIterator; +import org.rocksdb.WriteOptions; import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.nio.file.Path; import java.util.function.BiConsumer; -@RequiredArgsConstructor public class TbRocksDb { protected final String path; - private final Options options; - - private RocksDB db; + private final Options dbOptions; + private final WriteOptions writeOptions; + protected RocksDB db; static { RocksDB.loadLibrary(); } + public TbRocksDb(String path, Options dbOptions, WriteOptions writeOptions) { + this.path = path; + this.dbOptions = dbOptions; + this.writeOptions = writeOptions; + } + @SneakyThrows public void init() { - db = RocksDB.open(options, path); + Files.createDirectories(Path.of(path).getParent()); + db = RocksDB.open(dbOptions, path); } - public void put(String key, byte[] value) throws RocksDBException { - db.put(key.getBytes(StandardCharsets.UTF_8), value); + @SneakyThrows + public void put(String key, byte[] value) { + db.put(writeOptions, key.getBytes(StandardCharsets.UTF_8), value); } public void forEach(BiConsumer processor) { @@ -55,8 +63,9 @@ public class TbRocksDb { } } - public void delete(String key) throws RocksDBException { - db.delete(key.getBytes(StandardCharsets.UTF_8)); + @SneakyThrows + public void delete(String key) { + db.delete(writeOptions, key.getBytes(StandardCharsets.UTF_8)); } public void close() { From dba18323e2029b9c4802959da1095865d2ef9858 Mon Sep 17 00:00:00 2001 From: mpetrov Date: Fri, 28 Feb 2025 17:32:54 +0200 Subject: [PATCH 133/345] Fixed transport messages maximum number hints --- .../default-tenant-profile-configuration.component.html | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/ui-ngx/src/app/modules/home/components/profile/tenant/default-tenant-profile-configuration.component.html b/ui-ngx/src/app/modules/home/components/profile/tenant/default-tenant-profile-configuration.component.html index 64d5039914..aded3e2f78 100644 --- a/ui-ngx/src/app/modules/home/components/profile/tenant/default-tenant-profile-configuration.component.html +++ b/ui-ngx/src/app/modules/home/components/profile/tenant/default-tenant-profile-configuration.component.html @@ -155,10 +155,10 @@ formControlName="maxTransportMessages" type="number"> - {{ 'tenant-profile.max-transport-messages-range' | translate}} + {{ 'tenant-profile.max-transport-messages-required' | translate}} - {{ 'tenant-profile.max-transport-messages-required' | translate}} + {{ 'tenant-profile.max-transport-messages-range' | translate}} From 877362def0172e1e27c305bfec0a2e1453e8cb89 Mon Sep 17 00:00:00 2001 From: Dmytro Skarzhynets Date: Fri, 28 Feb 2025 17:33:53 +0200 Subject: [PATCH 134/345] Save time series strategies: after-merge fixes --- .../telemetry/DefaultTelemetrySubscriptionServiceTest.java | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/application/src/test/java/org/thingsboard/server/service/telemetry/DefaultTelemetrySubscriptionServiceTest.java b/application/src/test/java/org/thingsboard/server/service/telemetry/DefaultTelemetrySubscriptionServiceTest.java index 5756ecd125..b1c84184d2 100644 --- a/application/src/test/java/org/thingsboard/server/service/telemetry/DefaultTelemetrySubscriptionServiceTest.java +++ b/application/src/test/java/org/thingsboard/server/service/telemetry/DefaultTelemetrySubscriptionServiceTest.java @@ -389,7 +389,6 @@ class DefaultTelemetrySubscriptionServiceTest { ); } - // used to emulate versions returned by save latest API @Test void shouldThrowErrorWhenTryingToSaveTimeseriesForApiUsageState() { // GIVEN @@ -477,7 +476,7 @@ class DefaultTelemetrySubscriptionServiceTest { .strategy(new TimeseriesSaveRequest.Strategy(true, false, true, true)) .build(); - given(tsService.saveWithoutLatest(tenantId, deviceId, List.of(inactivityTimeout), 0L)).willReturn(immediateFuture(TimeseriesSaveResult.of(1, listOfNNumbers(1)))); + given(tsService.saveWithoutLatest(tenantId, deviceId, List.of(inactivityTimeout), 0L)).willReturn(immediateFuture(TimeseriesSaveResult.of(1, null))); // WHEN telemetryService.saveTimeseries(request); From 6f21c950197a81349596bf64c9e3419ded4c7ad0 Mon Sep 17 00:00:00 2001 From: dashevchenko Date: Fri, 28 Feb 2025 17:44:42 +0200 Subject: [PATCH 135/345] fixed docker-compose.edqs.yml --- docker/docker-compose.edqs.yml | 10 ++++++++-- docker/tb-core-edqs.env | 5 +++++ docker/tb-node.env | 1 - docker/{tb-node-edqs.env => tb-rule-engine-edqs.env} | 1 - 4 files changed, 13 insertions(+), 4 deletions(-) create mode 100644 docker/tb-core-edqs.env rename docker/{tb-node-edqs.env => tb-rule-engine-edqs.env} (80%) diff --git a/docker/docker-compose.edqs.yml b/docker/docker-compose.edqs.yml index 2f0e836240..67e9c987e3 100644 --- a/docker/docker-compose.edqs.yml +++ b/docker/docker-compose.edqs.yml @@ -19,10 +19,16 @@ version: '3.0' services: tb-core1: env_file: - - tb-node-edqs.env + - tb-core-edqs.env tb-core2: env_file: - - tb-node-edqs.env + - tb-core-edqs.env + tb-rule-engine1: + env_file: + - tb-rule-engine-edqs.env + tb-rule-engine2: + env_file: + - tb-rule-engine-edqs.env tb-edqs-1: restart: always image: "${DOCKER_REPO}/${EDQS_DOCKER_NAME}:${TB_VERSION}" diff --git a/docker/tb-core-edqs.env b/docker/tb-core-edqs.env new file mode 100644 index 0000000000..1417b780ec --- /dev/null +++ b/docker/tb-core-edqs.env @@ -0,0 +1,5 @@ +# ThingsBoard server configuration with enabled EDQS synchronization + +TB_EDQS_MODE=remote +TB_EDQS_SYNC_ENABLED=true +TB_EDQS_API_ENABLED=true \ No newline at end of file diff --git a/docker/tb-node.env b/docker/tb-node.env index e07d5a2f23..85a60eb51a 100644 --- a/docker/tb-node.env +++ b/docker/tb-node.env @@ -4,7 +4,6 @@ ZOOKEEPER_ENABLED=true ZOOKEEPER_URL=zookeeper:2181 JS_EVALUATOR=remote TRANSPORT_TYPE=remote -TB_EDQS_MODE=remote HTTP_LOG_CONTROLLER_ERROR_STACK_TRACE=false diff --git a/docker/tb-node-edqs.env b/docker/tb-rule-engine-edqs.env similarity index 80% rename from docker/tb-node-edqs.env rename to docker/tb-rule-engine-edqs.env index 1ef523af50..82395ddcfe 100644 --- a/docker/tb-node-edqs.env +++ b/docker/tb-rule-engine-edqs.env @@ -1,4 +1,3 @@ # ThingsBoard server configuration with enabled EDQS synchronization TB_EDQS_SYNC_ENABLED=true -TB_EDQS_API_ENABLED=true \ No newline at end of file From 8ad3afa4ba1f7b255cd8e550863e75df51011c2e Mon Sep 17 00:00:00 2001 From: dashevchenko Date: Fri, 28 Feb 2025 17:46:11 +0200 Subject: [PATCH 136/345] unignored test check --- .../thingsboard/server/msa/edqs/EdqsEntityDataQueryTest.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/msa/black-box-tests/src/test/java/org/thingsboard/server/msa/edqs/EdqsEntityDataQueryTest.java b/msa/black-box-tests/src/test/java/org/thingsboard/server/msa/edqs/EdqsEntityDataQueryTest.java index 1e3f3b1a60..53d8a72e7f 100644 --- a/msa/black-box-tests/src/test/java/org/thingsboard/server/msa/edqs/EdqsEntityDataQueryTest.java +++ b/msa/black-box-tests/src/test/java/org/thingsboard/server/msa/edqs/EdqsEntityDataQueryTest.java @@ -175,7 +175,7 @@ public class EdqsEntityDataQueryTest extends AbstractContainerTest { for (int i = 0; i < 10; i++) { Map> latest = retrievedDevices.get(i).getLatest(); String name = latest.get(EntityKeyType.ENTITY_FIELD).get("name").getValue(); - //assertThat(latest.get(EntityKeyType.TIME_SERIES).get("temperature").getValue()).isEqualTo(name.substring(name.length() - 1)); + assertThat(latest.get(EntityKeyType.TIME_SERIES).get("temperature").getValue()).isEqualTo(name.substring(name.length() - 1)); } } From ac52098207e4330fa44225f07bc08614d019bf9f Mon Sep 17 00:00:00 2001 From: Andrii Shvaika Date: Fri, 28 Feb 2025 17:46:25 +0200 Subject: [PATCH 137/345] CF: merge function draft --- .../api/tbel/DefaultTbelInvokeService.java | 1 + .../script/api/tbel/TbTimeWindow.java | 2 + .../thingsboard/script/api/tbel/TbUtils.java | 51 ++++++++++++++-- .../api/tbel/TbelCfTsMultiDoubleVal.java | 32 ++++++++++ .../script/api/tbel/TbelCfTsRollingArg.java | 5 -- .../script/api/tbel/TbelCfTsRollingData.java | 61 +++++++++++++++++++ 6 files changed, 143 insertions(+), 9 deletions(-) create mode 100644 common/script/script-api/src/main/java/org/thingsboard/script/api/tbel/TbelCfTsMultiDoubleVal.java create mode 100644 common/script/script-api/src/main/java/org/thingsboard/script/api/tbel/TbelCfTsRollingData.java diff --git a/common/script/script-api/src/main/java/org/thingsboard/script/api/tbel/DefaultTbelInvokeService.java b/common/script/script-api/src/main/java/org/thingsboard/script/api/tbel/DefaultTbelInvokeService.java index f626746a64..962d384a46 100644 --- a/common/script/script-api/src/main/java/org/thingsboard/script/api/tbel/DefaultTbelInvokeService.java +++ b/common/script/script-api/src/main/java/org/thingsboard/script/api/tbel/DefaultTbelInvokeService.java @@ -136,6 +136,7 @@ public class DefaultTbelInvokeService extends AbstractScriptInvokeService implem parserConfig.registerDataType("TbelCfSingleValueArg", TbelCfSingleValueArg.class, TbelCfSingleValueArg::memorySize); parserConfig.registerDataType("TbelCfTsRollingArg", TbelCfTsRollingArg.class, TbelCfTsRollingArg::memorySize); parserConfig.registerDataType("TbelCfTsDoubleVal", TbelCfTsDoubleVal.class, TbelCfTsDoubleVal::memorySize); + parserConfig.registerDataType("TbelCfTsRollingData", TbelCfTsRollingData.class, TbelCfTsRollingData::memorySize); parserConfig.registerDataType("TbTimeWindow", TbTimeWindow.class, TbTimeWindow::memorySize); TbUtils.register(parserConfig); executor = MoreExecutors.listeningDecorator(ThingsBoardExecutors.newWorkStealingPool(threadPoolSize, "tbel-executor")); diff --git a/common/script/script-api/src/main/java/org/thingsboard/script/api/tbel/TbTimeWindow.java b/common/script/script-api/src/main/java/org/thingsboard/script/api/tbel/TbTimeWindow.java index ce61965317..5048611838 100644 --- a/common/script/script-api/src/main/java/org/thingsboard/script/api/tbel/TbTimeWindow.java +++ b/common/script/script-api/src/main/java/org/thingsboard/script/api/tbel/TbTimeWindow.java @@ -17,9 +17,11 @@ package org.thingsboard.script.api.tbel; import lombok.AllArgsConstructor; import lombok.Data; +import lombok.NoArgsConstructor; @Data @AllArgsConstructor +@NoArgsConstructor public class TbTimeWindow implements TbelCfObject { public static final long OBJ_SIZE = 32L; diff --git a/common/script/script-api/src/main/java/org/thingsboard/script/api/tbel/TbUtils.java b/common/script/script-api/src/main/java/org/thingsboard/script/api/tbel/TbUtils.java index 9a57240d23..b108bbe33e 100644 --- a/common/script/script-api/src/main/java/org/thingsboard/script/api/tbel/TbUtils.java +++ b/common/script/script-api/src/main/java/org/thingsboard/script/api/tbel/TbUtils.java @@ -1,12 +1,12 @@ /** * 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 - * + *

+ * 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. @@ -44,6 +44,7 @@ import java.util.LinkedHashMap; import java.util.List; import java.util.Map; import java.util.Set; +import java.util.TreeSet; import java.util.regex.Matcher; import static java.lang.Character.MAX_RADIX; @@ -367,6 +368,8 @@ public class TbUtils { byte[].class, int.class))); parserConfig.addImport("parseBinaryArrayToInt", new MethodStub(TbUtils.class.getMethod("parseBinaryArrayToInt", byte[].class, int.class, int.class))); + parserConfig.addImport("merge", new MethodStub(TbUtils.class.getMethod("mergeCfTsRollingArgs", + TbelCfTsRollingArg.class, TbelCfTsRollingArg.class))); } public static String btoa(String input) { @@ -1506,5 +1509,45 @@ public class TbUtils { } return hex; } + + public static TbelCfTsRollingData mergeCfTsRollingArgs(TbelCfTsRollingArg a, TbelCfTsRollingArg b) { + return mergeCfTsRollingArgs(Arrays.asList(a, b), null); + } + + public static TbelCfTsRollingData mergeCfTsRollingArgs(List args, Map settings) { + TreeSet allTimestamps = new TreeSet<>(); + long startTs = Long.MAX_VALUE; + long endTs = Long.MIN_VALUE; + for (TbelCfTsRollingArg arg : args) { + for (TbelCfTsDoubleVal val : arg.getValues()) { + allTimestamps.add(val.getTs()); + } + startTs = Math.min(startTs, arg.getTimeWindow().getStartTs()); + endTs = Math.max(endTs, arg.getTimeWindow().getEndTs()); + } + + List data = new ArrayList<>(); + + int[] lastIndex = new int[args.size()]; + double[] result = new double[args.size()]; + Arrays.fill(result, Double.NaN); + + var tw = new TbTimeWindow(startTs, endTs, allTimestamps.size()); + + for (long ts : allTimestamps) { + for (int i = 0; i < args.size(); i++) { + var arg = args.get(i); + var values = arg.getValues(); + while (lastIndex[i] < values.size() && values.get(lastIndex[i]).getTs() <= ts) { + result[i] = values.get(lastIndex[i]).getValue(); + lastIndex[i]++; + } + } + data.add(new TbelCfTsMultiDoubleVal(ts, Arrays.copyOf(result, result.length))); + } + + return new TbelCfTsRollingData(tw, data); + } + } diff --git a/common/script/script-api/src/main/java/org/thingsboard/script/api/tbel/TbelCfTsMultiDoubleVal.java b/common/script/script-api/src/main/java/org/thingsboard/script/api/tbel/TbelCfTsMultiDoubleVal.java new file mode 100644 index 0000000000..33e4a65bb8 --- /dev/null +++ b/common/script/script-api/src/main/java/org/thingsboard/script/api/tbel/TbelCfTsMultiDoubleVal.java @@ -0,0 +1,32 @@ +/** + * 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.script.api.tbel; + +import lombok.Data; + +@Data +public class TbelCfTsMultiDoubleVal implements TbelCfObject { + + public static final long OBJ_SIZE = 32L; // Approximate calculation; + + private final long ts; + private final double[] values; + + @Override + public long memorySize() { + return OBJ_SIZE + values.length * 8L; + } +} diff --git a/common/script/script-api/src/main/java/org/thingsboard/script/api/tbel/TbelCfTsRollingArg.java b/common/script/script-api/src/main/java/org/thingsboard/script/api/tbel/TbelCfTsRollingArg.java index 807d498a16..74c9d77454 100644 --- a/common/script/script-api/src/main/java/org/thingsboard/script/api/tbel/TbelCfTsRollingArg.java +++ b/common/script/script-api/src/main/java/org/thingsboard/script/api/tbel/TbelCfTsRollingArg.java @@ -266,11 +266,6 @@ public class TbelCfTsRollingArg implements TbelCfArg, Iterable action) { - values.forEach(action); - } - @Override public String getType() { return "TS_ROLLING"; diff --git a/common/script/script-api/src/main/java/org/thingsboard/script/api/tbel/TbelCfTsRollingData.java b/common/script/script-api/src/main/java/org/thingsboard/script/api/tbel/TbelCfTsRollingData.java new file mode 100644 index 0000000000..646e826915 --- /dev/null +++ b/common/script/script-api/src/main/java/org/thingsboard/script/api/tbel/TbelCfTsRollingData.java @@ -0,0 +1,61 @@ +/** + * 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.script.api.tbel; + +import com.fasterxml.jackson.annotation.JsonIgnore; +import lombok.Getter; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.Iterator; +import java.util.List; +import java.util.function.Consumer; + +import static org.thingsboard.script.api.tbel.TbelCfTsDoubleVal.OBJ_SIZE; + +public class TbelCfTsRollingData implements TbelCfObject, Iterable { + + @Getter + private final TbTimeWindow timeWindow; + @Getter + private final List values; + + public TbelCfTsRollingData(TbTimeWindow timeWindow, List values) { + this.timeWindow = timeWindow; + this.values = Collections.unmodifiableList(values); + } + + @Override + public long memorySize() { + return 12 + values.size() * OBJ_SIZE; + } + + @JsonIgnore + public List getValue() { + return values; + } + + @JsonIgnore + public int getSize() { + return values.size(); + } + + @Override + public Iterator iterator() { + return values.iterator(); + } + +} From 2b51451896f74aad4854bd199b3abd41dc4d0fa3 Mon Sep 17 00:00:00 2001 From: Andrii Landiak Date: Fri, 28 Feb 2025 18:17:20 +0200 Subject: [PATCH 138/345] Change cf strategy for vc --- .../DefaultEntitiesExportImportService.java | 13 +- .../sync/ie/EntitiesExportImportService.java | 1 - .../ie/exporting/impl/AssetExportService.java | 17 ++- .../impl/AssetProfileExportService.java | 17 ++- .../BaseCalculatedFieldsExportService.java | 51 ++++++++ .../impl/CalculatedFieldExportService.java | 49 ------- .../exporting/impl/DeviceExportService.java | 11 +- .../impl/DeviceProfileExportService.java | 17 ++- .../ie/importing/impl/AssetImportService.java | 18 ++- .../impl/AssetProfileImportService.java | 18 ++- .../BaseCalculatedFieldsImportService.java | 123 ++++++++++++++++++ .../impl/CalculatedFieldImportService.java | 85 ------------ .../importing/impl/DeviceImportService.java | 46 +++++-- .../impl/DeviceProfileImportService.java | 18 ++- .../DefaultEntitiesVersionControlService.java | 10 +- .../sync/vc/data/EntitiesExportCtx.java | 2 + .../sync/vc/data/EntitiesImportCtx.java | 12 +- .../sync/vc/data/EntityTypeExportCtx.java | 1 + .../sync/ie/ExportImportServiceSqlTest.java | 50 ++++--- .../service/sync/vc/VersionControlTest.java | 11 +- .../common/data/sync/ie/AssetExportData.java | 28 ++++ .../data/sync/ie/AssetProfileExportData.java | 28 ++++ .../sync/ie/CalculatedFieldExportData.java | 56 ++++++++ .../common/data/sync/ie/DeviceExportData.java | 3 +- .../data/sync/ie/DeviceProfileExportData.java | 28 ++++ .../common/data/sync/ie/EntityExportData.java | 8 ++ .../data/sync/ie/EntityExportSettings.java | 3 + .../data/sync/ie/EntityImportSettings.java | 3 + .../request/create/VersionCreateConfig.java | 5 + .../vc/request/load/VersionLoadConfig.java | 1 + .../dao/cf/BaseCalculatedFieldService.java | 1 - .../model/sql/CalculatedFieldLinkEntity.java | 11 +- 32 files changed, 524 insertions(+), 221 deletions(-) create mode 100644 application/src/main/java/org/thingsboard/server/service/sync/ie/exporting/impl/BaseCalculatedFieldsExportService.java delete mode 100644 application/src/main/java/org/thingsboard/server/service/sync/ie/exporting/impl/CalculatedFieldExportService.java create mode 100644 application/src/main/java/org/thingsboard/server/service/sync/ie/importing/impl/BaseCalculatedFieldsImportService.java delete mode 100644 application/src/main/java/org/thingsboard/server/service/sync/ie/importing/impl/CalculatedFieldImportService.java create mode 100644 common/data/src/main/java/org/thingsboard/server/common/data/sync/ie/AssetExportData.java create mode 100644 common/data/src/main/java/org/thingsboard/server/common/data/sync/ie/AssetProfileExportData.java create mode 100644 common/data/src/main/java/org/thingsboard/server/common/data/sync/ie/CalculatedFieldExportData.java create mode 100644 common/data/src/main/java/org/thingsboard/server/common/data/sync/ie/DeviceProfileExportData.java 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 7d0f5ebf48..94b0ddc07e 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,6 +31,7 @@ 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; @@ -61,6 +62,7 @@ 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; @@ -69,11 +71,9 @@ public class DefaultEntitiesExportImportService implements EntitiesExportImportS EntityType.DASHBOARD, EntityType.ASSET_PROFILE, EntityType.ASSET, EntityType.DEVICE_PROFILE, EntityType.DEVICE, EntityType.ENTITY_VIEW, EntityType.WIDGET_TYPE, EntityType.WIDGETS_BUNDLE, - EntityType.NOTIFICATION_TEMPLATE, EntityType.NOTIFICATION_TARGET, EntityType.NOTIFICATION_RULE, - EntityType.CALCULATED_FIELD + EntityType.NOTIFICATION_TEMPLATE, EntityType.NOTIFICATION_TARGET, EntityType.NOTIFICATION_RULE ); - @Override public , I extends EntityId> EntityExportData exportEntity(EntitiesExportCtx ctx, I entityId) throws ThingsboardException { if (!rateLimitService.checkRateLimit(LimitedApi.ENTITY_EXPORT, ctx.getTenantId())) { @@ -128,15 +128,18 @@ public class DefaultEntitiesExportImportService implements EntitiesExportImportS logEntityActionService.logEntityRelationAction(ctx.getTenantId(), null, relation, ctx.getUser(), ActionType.RELATION_ADD_OR_UPDATE, null, relation); } - } + ctx.getCalculatedFields().forEach((calculatedField, created) -> { + var savedCalculatedField = calculatedFieldService.save(calculatedField); + logEntityActionService.logEntityAction(ctx.getTenantId(), savedCalculatedField.getId(), savedCalculatedField, created ? ActionType.ADDED : ActionType.UPDATED, ctx.getUser()); + }); + } @Override public Comparator getEntityTypeComparatorForImport() { return Comparator.comparing(SUPPORTED_ENTITY_TYPES::indexOf); } - @SuppressWarnings("unchecked") private , D extends EntityExportData> EntityExportService getExportService(EntityType entityType) { EntityExportService exportService = exportServices.get(entityType); diff --git a/application/src/main/java/org/thingsboard/server/service/sync/ie/EntitiesExportImportService.java b/application/src/main/java/org/thingsboard/server/service/sync/ie/EntitiesExportImportService.java index 2b7e7d3593..b77447df8d 100644 --- a/application/src/main/java/org/thingsboard/server/service/sync/ie/EntitiesExportImportService.java +++ b/application/src/main/java/org/thingsboard/server/service/sync/ie/EntitiesExportImportService.java @@ -32,7 +32,6 @@ public interface EntitiesExportImportService { , I extends EntityId> EntityImportResult importEntity(EntitiesImportCtx ctx, EntityExportData exportData) throws ThingsboardException; - void saveReferencesAndRelations(EntitiesImportCtx ctx) throws ThingsboardException; Comparator getEntityTypeComparatorForImport(); diff --git a/application/src/main/java/org/thingsboard/server/service/sync/ie/exporting/impl/AssetExportService.java b/application/src/main/java/org/thingsboard/server/service/sync/ie/exporting/impl/AssetExportService.java index 56d5bb0934..6f56fa5145 100644 --- a/application/src/main/java/org/thingsboard/server/service/sync/ie/exporting/impl/AssetExportService.java +++ b/application/src/main/java/org/thingsboard/server/service/sync/ie/exporting/impl/AssetExportService.java @@ -19,7 +19,8 @@ import org.springframework.stereotype.Service; import org.thingsboard.server.common.data.EntityType; import org.thingsboard.server.common.data.asset.Asset; import org.thingsboard.server.common.data.id.AssetId; -import org.thingsboard.server.common.data.sync.ie.EntityExportData; +import org.thingsboard.server.common.data.sync.ie.AssetExportData; +import org.thingsboard.server.dao.cf.CalculatedFieldService; import org.thingsboard.server.queue.util.TbCoreComponent; import org.thingsboard.server.service.sync.vc.data.EntitiesExportCtx; @@ -27,12 +28,22 @@ import java.util.Set; @Service @TbCoreComponent -public class AssetExportService extends BaseEntityExportService> { +public class AssetExportService extends BaseCalculatedFieldsExportService { + + protected AssetExportService(CalculatedFieldService calculatedFieldService) { + super(calculatedFieldService); + } @Override - protected void setRelatedEntities(EntitiesExportCtx ctx, Asset asset, EntityExportData exportData) { + protected void setRelatedEntities(EntitiesExportCtx ctx, Asset asset, AssetExportData exportData) { asset.setCustomerId(getExternalIdOrElseInternal(ctx, asset.getCustomerId())); asset.setAssetProfileId(getExternalIdOrElseInternal(ctx, asset.getAssetProfileId())); + setCalculatedFields(ctx, asset, exportData); + } + + @Override + protected AssetExportData newExportData() { + return new AssetExportData(); } @Override diff --git a/application/src/main/java/org/thingsboard/server/service/sync/ie/exporting/impl/AssetProfileExportService.java b/application/src/main/java/org/thingsboard/server/service/sync/ie/exporting/impl/AssetProfileExportService.java index b670daf06c..2314db92f5 100644 --- a/application/src/main/java/org/thingsboard/server/service/sync/ie/exporting/impl/AssetProfileExportService.java +++ b/application/src/main/java/org/thingsboard/server/service/sync/ie/exporting/impl/AssetProfileExportService.java @@ -19,7 +19,8 @@ import org.springframework.stereotype.Service; import org.thingsboard.server.common.data.EntityType; import org.thingsboard.server.common.data.asset.AssetProfile; import org.thingsboard.server.common.data.id.AssetProfileId; -import org.thingsboard.server.common.data.sync.ie.EntityExportData; +import org.thingsboard.server.common.data.sync.ie.AssetProfileExportData; +import org.thingsboard.server.dao.cf.CalculatedFieldService; import org.thingsboard.server.queue.util.TbCoreComponent; import org.thingsboard.server.service.sync.vc.data.EntitiesExportCtx; @@ -27,13 +28,23 @@ import java.util.Set; @Service @TbCoreComponent -public class AssetProfileExportService extends BaseEntityExportService> { +public class AssetProfileExportService extends BaseCalculatedFieldsExportService { + + protected AssetProfileExportService(CalculatedFieldService calculatedFieldService) { + super(calculatedFieldService); + } @Override - protected void setRelatedEntities(EntitiesExportCtx ctx, AssetProfile assetProfile, EntityExportData exportData) { + protected void setRelatedEntities(EntitiesExportCtx ctx, AssetProfile assetProfile, AssetProfileExportData exportData) { assetProfile.setDefaultDashboardId(getExternalIdOrElseInternal(ctx, assetProfile.getDefaultDashboardId())); assetProfile.setDefaultRuleChainId(getExternalIdOrElseInternal(ctx, assetProfile.getDefaultRuleChainId())); assetProfile.setDefaultEdgeRuleChainId(getExternalIdOrElseInternal(ctx, assetProfile.getDefaultEdgeRuleChainId())); + setCalculatedFields(ctx, assetProfile, exportData); + } + + @Override + protected AssetProfileExportData newExportData() { + return new AssetProfileExportData(); } @Override diff --git a/application/src/main/java/org/thingsboard/server/service/sync/ie/exporting/impl/BaseCalculatedFieldsExportService.java b/application/src/main/java/org/thingsboard/server/service/sync/ie/exporting/impl/BaseCalculatedFieldsExportService.java new file mode 100644 index 0000000000..e28f8aaa79 --- /dev/null +++ b/application/src/main/java/org/thingsboard/server/service/sync/ie/exporting/impl/BaseCalculatedFieldsExportService.java @@ -0,0 +1,51 @@ +/** + * 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.sync.ie.exporting.impl; + +import org.thingsboard.server.common.data.ExportableEntity; +import org.thingsboard.server.common.data.HasTenantId; +import org.thingsboard.server.common.data.cf.CalculatedField; +import org.thingsboard.server.common.data.id.EntityId; +import org.thingsboard.server.common.data.sync.ie.CalculatedFieldExportData; +import org.thingsboard.server.dao.cf.CalculatedFieldService; +import org.thingsboard.server.service.sync.vc.data.EntitiesExportCtx; + +import java.util.List; + +public abstract class BaseCalculatedFieldsExportService & HasTenantId, D extends CalculatedFieldExportData> extends BaseEntityExportService { + + protected final CalculatedFieldService calculatedFieldService; + + protected BaseCalculatedFieldsExportService(CalculatedFieldService calculatedFieldService) { + this.calculatedFieldService = calculatedFieldService; + } + + protected void setCalculatedFields(EntitiesExportCtx ctx, E entity, D exportData) { + if (ctx.getSettings().isExportCalculatedFields()) { + List calculatedFields = calculatedFieldService.findCalculatedFieldsByEntityId(ctx.getTenantId(), entity.getId()); + calculatedFields.forEach(calculatedField -> { + calculatedField.getConfiguration().getArguments().values().forEach(argument -> { + if (argument.getRefEntityId() != null) { + EntityId externalId = getExternalIdOrElseInternal(ctx, argument.getRefEntityId()); + argument.setRefEntityId(externalId); + } + }); + }); + exportData.setCalculatedFields(calculatedFields); + } + } + +} diff --git a/application/src/main/java/org/thingsboard/server/service/sync/ie/exporting/impl/CalculatedFieldExportService.java b/application/src/main/java/org/thingsboard/server/service/sync/ie/exporting/impl/CalculatedFieldExportService.java deleted file mode 100644 index b42f3c6647..0000000000 --- a/application/src/main/java/org/thingsboard/server/service/sync/ie/exporting/impl/CalculatedFieldExportService.java +++ /dev/null @@ -1,49 +0,0 @@ -/** - * 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.sync.ie.exporting.impl; - -import org.springframework.stereotype.Service; -import org.thingsboard.server.common.data.EntityType; -import org.thingsboard.server.common.data.cf.CalculatedField; -import org.thingsboard.server.common.data.id.CalculatedFieldId; -import org.thingsboard.server.common.data.id.EntityId; -import org.thingsboard.server.common.data.sync.ie.EntityExportData; -import org.thingsboard.server.queue.util.TbCoreComponent; -import org.thingsboard.server.service.sync.vc.data.EntitiesExportCtx; - -import java.util.Set; - -@Service -@TbCoreComponent -public class CalculatedFieldExportService extends BaseEntityExportService> { - - @Override - protected void setRelatedEntities(EntitiesExportCtx ctx, CalculatedField calculatedField, EntityExportData exportData) { - calculatedField.setEntityId(getExternalIdOrElseInternal(ctx, calculatedField.getEntityId())); - calculatedField.getConfiguration().getArguments().values().forEach(argument -> { - if (argument.getRefEntityId() != null) { - EntityId internalEntityId = getExternalIdOrElseInternal(ctx, argument.getRefEntityId()); - argument.setRefEntityId(internalEntityId); - } - }); - } - - @Override - public Set getSupportedEntityTypes() { - return Set.of(EntityType.CALCULATED_FIELD); - } - -} diff --git a/application/src/main/java/org/thingsboard/server/service/sync/ie/exporting/impl/DeviceExportService.java b/application/src/main/java/org/thingsboard/server/service/sync/ie/exporting/impl/DeviceExportService.java index 7d5f7ee57e..f1b5ade487 100644 --- a/application/src/main/java/org/thingsboard/server/service/sync/ie/exporting/impl/DeviceExportService.java +++ b/application/src/main/java/org/thingsboard/server/service/sync/ie/exporting/impl/DeviceExportService.java @@ -15,12 +15,12 @@ */ package org.thingsboard.server.service.sync.ie.exporting.impl; -import lombok.RequiredArgsConstructor; import org.springframework.stereotype.Service; import org.thingsboard.server.common.data.Device; import org.thingsboard.server.common.data.EntityType; import org.thingsboard.server.common.data.id.DeviceId; import org.thingsboard.server.common.data.sync.ie.DeviceExportData; +import org.thingsboard.server.dao.cf.CalculatedFieldService; import org.thingsboard.server.dao.device.DeviceCredentialsService; import org.thingsboard.server.queue.util.TbCoreComponent; import org.thingsboard.server.service.sync.vc.data.EntitiesExportCtx; @@ -29,11 +29,15 @@ import java.util.Set; @Service @TbCoreComponent -@RequiredArgsConstructor -public class DeviceExportService extends BaseEntityExportService { +public class DeviceExportService extends BaseCalculatedFieldsExportService { private final DeviceCredentialsService deviceCredentialsService; + public DeviceExportService(CalculatedFieldService calculatedFieldService, DeviceCredentialsService deviceCredentialsService) { + super(calculatedFieldService); + this.deviceCredentialsService = deviceCredentialsService; + } + @Override protected void setRelatedEntities(EntitiesExportCtx ctx, Device device, DeviceExportData exportData) { device.setCustomerId(getExternalIdOrElseInternal(ctx, device.getCustomerId())); @@ -44,6 +48,7 @@ public class DeviceExportService extends BaseEntityExportService> { +public class DeviceProfileExportService extends BaseCalculatedFieldsExportService { + + protected DeviceProfileExportService(CalculatedFieldService calculatedFieldService) { + super(calculatedFieldService); + } @Override - protected void setRelatedEntities(EntitiesExportCtx ctx, DeviceProfile deviceProfile, EntityExportData exportData) { + protected void setRelatedEntities(EntitiesExportCtx ctx, DeviceProfile deviceProfile, DeviceProfileExportData exportData) { deviceProfile.setDefaultDashboardId(getExternalIdOrElseInternal(ctx, deviceProfile.getDefaultDashboardId())); deviceProfile.setDefaultRuleChainId(getExternalIdOrElseInternal(ctx, deviceProfile.getDefaultRuleChainId())); deviceProfile.setDefaultEdgeRuleChainId(getExternalIdOrElseInternal(ctx, deviceProfile.getDefaultEdgeRuleChainId())); + setCalculatedFields(ctx, deviceProfile, exportData); + } + + @Override + protected DeviceProfileExportData newExportData() { + return new DeviceProfileExportData(); } @Override diff --git a/application/src/main/java/org/thingsboard/server/service/sync/ie/importing/impl/AssetImportService.java b/application/src/main/java/org/thingsboard/server/service/sync/ie/importing/impl/AssetImportService.java index 00ce23b2e8..0b0649072e 100644 --- a/application/src/main/java/org/thingsboard/server/service/sync/ie/importing/impl/AssetImportService.java +++ b/application/src/main/java/org/thingsboard/server/service/sync/ie/importing/impl/AssetImportService.java @@ -15,24 +15,28 @@ */ package org.thingsboard.server.service.sync.ie.importing.impl; -import lombok.RequiredArgsConstructor; import org.springframework.stereotype.Service; import org.thingsboard.server.common.data.EntityType; import org.thingsboard.server.common.data.asset.Asset; import org.thingsboard.server.common.data.id.AssetId; import org.thingsboard.server.common.data.id.TenantId; -import org.thingsboard.server.common.data.sync.ie.EntityExportData; +import org.thingsboard.server.common.data.sync.ie.AssetExportData; import org.thingsboard.server.dao.asset.AssetService; +import org.thingsboard.server.dao.cf.CalculatedFieldService; import org.thingsboard.server.queue.util.TbCoreComponent; import org.thingsboard.server.service.sync.vc.data.EntitiesImportCtx; @Service @TbCoreComponent -@RequiredArgsConstructor -public class AssetImportService extends BaseEntityImportService> { +public class AssetImportService extends BaseCalculatedFieldsImportService { private final AssetService assetService; + public AssetImportService(CalculatedFieldService calculatedFieldService, AssetService assetService) { + super(calculatedFieldService); + this.assetService = assetService; + } + @Override protected void setOwner(TenantId tenantId, Asset asset, IdProvider idProvider) { asset.setTenantId(tenantId); @@ -40,14 +44,14 @@ public class AssetImportService extends BaseEntityImportService exportData, IdProvider idProvider) { + protected Asset prepare(EntitiesImportCtx ctx, Asset asset, Asset old, AssetExportData exportData, IdProvider idProvider) { asset.setAssetProfileId(idProvider.getInternalId(asset.getAssetProfileId())); return asset; } @Override - protected Asset saveOrUpdate(EntitiesImportCtx ctx, Asset asset, EntityExportData exportData, IdProvider idProvider) { - return assetService.saveAsset(asset); + protected Asset saveOrUpdate(EntitiesImportCtx ctx, Asset asset, AssetExportData exportData, IdProvider idProvider) { + return saveOrUpdateEntity(ctx, asset, exportData, idProvider, assetService::saveAsset); } @Override diff --git a/application/src/main/java/org/thingsboard/server/service/sync/ie/importing/impl/AssetProfileImportService.java b/application/src/main/java/org/thingsboard/server/service/sync/ie/importing/impl/AssetProfileImportService.java index 97c54957bf..6797c92c81 100644 --- a/application/src/main/java/org/thingsboard/server/service/sync/ie/importing/impl/AssetProfileImportService.java +++ b/application/src/main/java/org/thingsboard/server/service/sync/ie/importing/impl/AssetProfileImportService.java @@ -15,7 +15,6 @@ */ package org.thingsboard.server.service.sync.ie.importing.impl; -import lombok.RequiredArgsConstructor; import org.springframework.stereotype.Service; import org.thingsboard.server.common.data.EntityType; import org.thingsboard.server.common.data.User; @@ -23,25 +22,30 @@ import org.thingsboard.server.common.data.asset.AssetProfile; import org.thingsboard.server.common.data.audit.ActionType; import org.thingsboard.server.common.data.id.AssetProfileId; import org.thingsboard.server.common.data.id.TenantId; -import org.thingsboard.server.common.data.sync.ie.EntityExportData; +import org.thingsboard.server.common.data.sync.ie.AssetProfileExportData; import org.thingsboard.server.dao.asset.AssetProfileService; +import org.thingsboard.server.dao.cf.CalculatedFieldService; import org.thingsboard.server.queue.util.TbCoreComponent; import org.thingsboard.server.service.sync.vc.data.EntitiesImportCtx; @Service @TbCoreComponent -@RequiredArgsConstructor -public class AssetProfileImportService extends BaseEntityImportService> { +public class AssetProfileImportService extends BaseCalculatedFieldsImportService { private final AssetProfileService assetProfileService; + public AssetProfileImportService(CalculatedFieldService calculatedFieldService, AssetProfileService assetProfileService) { + super(calculatedFieldService); + this.assetProfileService = assetProfileService; + } + @Override protected void setOwner(TenantId tenantId, AssetProfile assetProfile, IdProvider idProvider) { assetProfile.setTenantId(tenantId); } @Override - protected AssetProfile prepare(EntitiesImportCtx ctx, AssetProfile assetProfile, AssetProfile old, EntityExportData exportData, IdProvider idProvider) { + protected AssetProfile prepare(EntitiesImportCtx ctx, AssetProfile assetProfile, AssetProfile old, AssetProfileExportData exportData, IdProvider idProvider) { assetProfile.setDefaultRuleChainId(idProvider.getInternalId(assetProfile.getDefaultRuleChainId())); assetProfile.setDefaultDashboardId(idProvider.getInternalId(assetProfile.getDefaultDashboardId())); assetProfile.setDefaultEdgeRuleChainId(idProvider.getInternalId(assetProfile.getDefaultEdgeRuleChainId())); @@ -49,8 +53,8 @@ public class AssetProfileImportService extends BaseEntityImportService exportData, IdProvider idProvider) { - return assetProfileService.saveAssetProfile(assetProfile); + protected AssetProfile saveOrUpdate(EntitiesImportCtx ctx, AssetProfile assetProfile, AssetProfileExportData exportData, IdProvider idProvider) { + return saveOrUpdateEntity(ctx, assetProfile, exportData, idProvider, assetProfileService::saveAssetProfile); } @Override diff --git a/application/src/main/java/org/thingsboard/server/service/sync/ie/importing/impl/BaseCalculatedFieldsImportService.java b/application/src/main/java/org/thingsboard/server/service/sync/ie/importing/impl/BaseCalculatedFieldsImportService.java new file mode 100644 index 0000000000..ffa51938f0 --- /dev/null +++ b/application/src/main/java/org/thingsboard/server/service/sync/ie/importing/impl/BaseCalculatedFieldsImportService.java @@ -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. + */ +package org.thingsboard.server.service.sync.ie.importing.impl; + +import org.thingsboard.server.common.data.ExportableEntity; +import org.thingsboard.server.common.data.HasTenantId; +import org.thingsboard.server.common.data.cf.CalculatedField; +import org.thingsboard.server.common.data.id.CalculatedFieldId; +import org.thingsboard.server.common.data.id.EntityId; +import org.thingsboard.server.common.data.sync.ie.CalculatedFieldExportData; +import org.thingsboard.server.dao.cf.CalculatedFieldService; +import org.thingsboard.server.service.sync.vc.data.EntitiesImportCtx; + +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import java.util.function.Function; +import java.util.stream.Collectors; + +public abstract class BaseCalculatedFieldsImportService & HasTenantId, D extends CalculatedFieldExportData> extends BaseEntityImportService { + + private final CalculatedFieldService calculatedFieldService; + + protected BaseCalculatedFieldsImportService(CalculatedFieldService calculatedFieldService) { + this.calculatedFieldService = calculatedFieldService; + } + + protected E saveOrUpdateEntity(EntitiesImportCtx ctx, E entity, D exportData, IdProvider idProvider, Function saveFunction) { + E savedEntity = saveFunction.apply(entity); + + if (ctx.isFinalImportAttempt() || ctx.getCurrentImportResult().isUpdatedAllExternalIds()) { + saveCalculatedFields(ctx, savedEntity, exportData, idProvider); + } + return savedEntity; + } + + protected void saveCalculatedFields(EntitiesImportCtx ctx, E savedEntity, D exportData, IdProvider idProvider) { + if (exportData.getCalculatedFields() == null || !ctx.isSaveCalculatedFields()) { + return; + } + + exportData.getCalculatedFields().forEach(calculatedField -> { + calculatedField.setTenantId(savedEntity.getTenantId()); + calculatedField.setExternalId(calculatedField.getId()); + calculatedField.setId(idProvider.getInternalId(calculatedField.getId(), false)); + calculatedField.setEntityId(savedEntity.getId()); + + calculatedField.getConfiguration().getArguments().values().forEach(argument -> { + if (argument.getRefEntityId() != null) { + argument.setRefEntityId(idProvider.getInternalId(argument.getRefEntityId(), false)); + } + }); + + calculatedFieldService.save(calculatedField); + }); + } + + @Override + protected boolean updateRelatedEntitiesIfUnmodified(EntitiesImportCtx ctx, E prepared, D exportData, IdProvider idProvider) { + boolean updated = super.updateRelatedEntitiesIfUnmodified(ctx, prepared, exportData, idProvider); + updated |= updateCalculatedFields(ctx, prepared, exportData, idProvider); + return updated; + } + + private boolean updateCalculatedFields(EntitiesImportCtx ctx, E prepared, D exportData, IdProvider idProvider) { + var calculatedFields = exportData.getCalculatedFields(); + if (calculatedFields == null || !ctx.isSaveCalculatedFields()) { + return false; + } + Map calculatedFieldMap = calculatedFields.stream() + .peek(newField -> { + newField.setTenantId(ctx.getTenantId()); + newField.setExternalId(newField.getId()); + newField.setId(idProvider.getInternalId(newField.getId(), false)); + newField.setEntityId(prepared.getId()); + newField.getConfiguration().getArguments().values().forEach(argument -> { + argument.setRefEntityId(idProvider.getInternalId(argument.getRefEntityId(), false)); + }); + }) + .collect(Collectors.toMap(CalculatedField::getId, field -> field)); + + List existingFields = calculatedFieldService.findCalculatedFieldsByEntityId(ctx.getTenantId(), prepared.getId()); + boolean updated = false; + + Map result = new LinkedHashMap<>(); + for (CalculatedField existingField : existingFields) { + if (calculatedFieldMap.containsKey(existingField.getId())) { + CalculatedField newField = calculatedFieldMap.get(existingField.getId()); + if (!newField.equals(existingField)) { + result.put(newField, false); + } + calculatedFieldMap.remove(existingField.getId()); + } else { + updated = true; + calculatedFieldService.deleteCalculatedField(ctx.getTenantId(), existingField.getId()); + } + } + + for (CalculatedField newField : calculatedFieldMap.values()) { + result.put(newField, true); + } + + if (!result.isEmpty()) { + updated = true; + ctx.addCalculatedFields(result); + } + return updated; + } + +} diff --git a/application/src/main/java/org/thingsboard/server/service/sync/ie/importing/impl/CalculatedFieldImportService.java b/application/src/main/java/org/thingsboard/server/service/sync/ie/importing/impl/CalculatedFieldImportService.java deleted file mode 100644 index a723f28b1b..0000000000 --- a/application/src/main/java/org/thingsboard/server/service/sync/ie/importing/impl/CalculatedFieldImportService.java +++ /dev/null @@ -1,85 +0,0 @@ -/** - * 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.sync.ie.importing.impl; - -import lombok.RequiredArgsConstructor; -import org.springframework.stereotype.Service; -import org.thingsboard.server.common.data.EntityType; -import org.thingsboard.server.common.data.User; -import org.thingsboard.server.common.data.audit.ActionType; -import org.thingsboard.server.common.data.cf.CalculatedField; -import org.thingsboard.server.common.data.exception.ThingsboardException; -import org.thingsboard.server.common.data.id.CalculatedFieldId; -import org.thingsboard.server.common.data.id.EntityId; -import org.thingsboard.server.common.data.id.TenantId; -import org.thingsboard.server.common.data.sync.ie.EntityExportData; -import org.thingsboard.server.dao.cf.CalculatedFieldService; -import org.thingsboard.server.dao.service.ConstraintValidator; -import org.thingsboard.server.queue.util.TbCoreComponent; -import org.thingsboard.server.service.sync.vc.data.EntitiesImportCtx; - -@Service -@TbCoreComponent -@RequiredArgsConstructor -public class CalculatedFieldImportService extends BaseEntityImportService> { - - private final CalculatedFieldService calculatedFieldService; - - @Override - protected void setOwner(TenantId tenantId, CalculatedField calculatedField, IdProvider idProvider) { - calculatedField.setTenantId(tenantId); - } - - @Override - protected CalculatedField prepare(EntitiesImportCtx ctx, CalculatedField calculatedField, CalculatedField oldEntity, EntityExportData exportData, IdProvider idProvider) { - calculatedField.setEntityId(idProvider.getInternalId(calculatedField.getEntityId())); - calculatedField.getConfiguration().getArguments().values().forEach(argument -> { - if (argument.getRefEntityId() != null) { - EntityId internalEntityId = idProvider.getInternalId(argument.getRefEntityId()); - argument.setRefEntityId(internalEntityId); - } - }); - return calculatedField; - } - - @Override - protected CalculatedField saveOrUpdate(EntitiesImportCtx ctx, CalculatedField calculatedField, EntityExportData exportData, IdProvider idProvider) { - ConstraintValidator.validateFields(calculatedField); - return calculatedFieldService.save(calculatedField); - } - - @Override - protected CalculatedField deepCopy(CalculatedField calculatedField) { - return new CalculatedField(calculatedField); - } - - @Override - protected void onEntitySaved(User user, CalculatedField savedEntity, CalculatedField oldEntity) throws ThingsboardException { - entityActionService.logEntityAction(user, savedEntity.getId(), savedEntity, null, - oldEntity == null ? ActionType.ADDED : ActionType.UPDATED, null); - } - - @Override - protected void cleanupForComparison(CalculatedField e) { - super.cleanupForComparison(e); - } - - @Override - public EntityType getEntityType() { - return EntityType.CALCULATED_FIELD; - } - -} diff --git a/application/src/main/java/org/thingsboard/server/service/sync/ie/importing/impl/DeviceImportService.java b/application/src/main/java/org/thingsboard/server/service/sync/ie/importing/impl/DeviceImportService.java index 61b9839cbd..c8e99cdbc1 100644 --- a/application/src/main/java/org/thingsboard/server/service/sync/ie/importing/impl/DeviceImportService.java +++ b/application/src/main/java/org/thingsboard/server/service/sync/ie/importing/impl/DeviceImportService.java @@ -15,13 +15,13 @@ */ package org.thingsboard.server.service.sync.ie.importing.impl; -import lombok.RequiredArgsConstructor; import org.springframework.stereotype.Service; import org.thingsboard.server.common.data.Device; import org.thingsboard.server.common.data.EntityType; import org.thingsboard.server.common.data.id.DeviceId; import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.common.data.sync.ie.DeviceExportData; +import org.thingsboard.server.dao.cf.CalculatedFieldService; import org.thingsboard.server.dao.device.DeviceCredentialsService; import org.thingsboard.server.dao.device.DeviceService; import org.thingsboard.server.queue.util.TbCoreComponent; @@ -29,12 +29,17 @@ import org.thingsboard.server.service.sync.vc.data.EntitiesImportCtx; @Service @TbCoreComponent -@RequiredArgsConstructor -public class DeviceImportService extends BaseEntityImportService { +public class DeviceImportService extends BaseCalculatedFieldsImportService { private final DeviceService deviceService; private final DeviceCredentialsService credentialsService; + public DeviceImportService(CalculatedFieldService calculatedFieldService, DeviceService deviceService, DeviceCredentialsService credentialsService) { + super(calculatedFieldService); + this.deviceService = deviceService; + this.credentialsService = credentialsService; + } + @Override protected void setOwner(TenantId tenantId, Device device, IdProvider idProvider) { device.setTenantId(tenantId); @@ -64,29 +69,42 @@ public class DeviceImportService extends BaseEntityImportService> { +public class DeviceProfileImportService extends BaseCalculatedFieldsImportService { private final DeviceProfileService deviceProfileService; + public DeviceProfileImportService(CalculatedFieldService calculatedFieldService, DeviceProfileService deviceProfileService) { + super(calculatedFieldService); + this.deviceProfileService = deviceProfileService; + } + @Override protected void setOwner(TenantId tenantId, DeviceProfile deviceProfile, IdProvider idProvider) { deviceProfile.setTenantId(tenantId); } @Override - protected DeviceProfile prepare(EntitiesImportCtx ctx, DeviceProfile deviceProfile, DeviceProfile old, EntityExportData exportData, IdProvider idProvider) { + protected DeviceProfile prepare(EntitiesImportCtx ctx, DeviceProfile deviceProfile, DeviceProfile old, DeviceProfileExportData exportData, IdProvider idProvider) { deviceProfile.setDefaultRuleChainId(idProvider.getInternalId(deviceProfile.getDefaultRuleChainId())); deviceProfile.setDefaultEdgeRuleChainId(idProvider.getInternalId(deviceProfile.getDefaultEdgeRuleChainId())); deviceProfile.setDefaultDashboardId(idProvider.getInternalId(deviceProfile.getDefaultDashboardId())); @@ -51,8 +55,8 @@ public class DeviceProfileImportService extends BaseEntityImportService exportData, IdProvider idProvider) { - return deviceProfileService.saveDeviceProfile(deviceProfile); + protected DeviceProfile saveOrUpdate(EntitiesImportCtx ctx, DeviceProfile deviceProfile, DeviceProfileExportData exportData, IdProvider idProvider) { + return saveOrUpdateEntity(ctx, deviceProfile, exportData, idProvider, deviceProfileService::saveDeviceProfile); } @Override diff --git a/application/src/main/java/org/thingsboard/server/service/sync/vc/DefaultEntitiesVersionControlService.java b/application/src/main/java/org/thingsboard/server/service/sync/vc/DefaultEntitiesVersionControlService.java index eed18abe52..89b51ce84a 100644 --- a/application/src/main/java/org/thingsboard/server/service/sync/vc/DefaultEntitiesVersionControlService.java +++ b/application/src/main/java/org/thingsboard/server/service/sync/vc/DefaultEntitiesVersionControlService.java @@ -94,7 +94,6 @@ import java.util.Set; import java.util.UUID; import java.util.concurrent.ExecutionException; import java.util.function.Function; -import java.util.stream.Collectors; import static com.google.common.util.concurrent.Futures.transform; import static org.thingsboard.server.common.data.sync.vc.VcUtils.checkBranchName; @@ -304,6 +303,7 @@ public class DefaultEntitiesVersionControlService implements EntitiesVersionCont .updateRelations(config.isLoadRelations()) .saveAttributes(config.isLoadAttributes()) .saveCredentials(config.isLoadCredentials()) + .saveCalculatedFields(config.isLoadCalculatedFields()) .findExistingByName(false) .build()); ctx.setFinalImportAttempt(true); @@ -327,7 +327,7 @@ public class DefaultEntitiesVersionControlService implements EntitiesVersionCont var sw = TbStopWatch.create("before"); List entityTypes = request.getEntityTypes().keySet().stream() - .sorted(exportImportService.getEntityTypeComparatorForImport()).collect(Collectors.toList()); + .sorted(exportImportService.getEntityTypeComparatorForImport()).toList(); for (EntityType entityType : entityTypes) { log.debug("[{}] Loading {} entities", ctx.getTenantId(), entityType); sw.startNew("Entities " + entityType.name()); @@ -362,6 +362,7 @@ public class DefaultEntitiesVersionControlService implements EntitiesVersionCont .updateRelations(config.isLoadRelations()) .saveAttributes(config.isLoadAttributes()) .saveCredentials(config.isLoadCredentials()) + .saveCalculatedFields(config.isLoadCalculatedFields()) .findExistingByName(config.isFindExistingEntityByName()) .build(); } @@ -471,7 +472,7 @@ public class DefaultEntitiesVersionControlService implements EntitiesVersionCont } @Override - public ListenableFuture compareEntityDataToVersion(User user, EntityId entityId, String versionId) throws Exception { + public ListenableFuture compareEntityDataToVersion(User user, EntityId entityId, String versionId) { HasId entity = exportableEntitiesService.findEntityByTenantIdAndId(user.getTenantId(), entityId); if (!(entity instanceof ExportableEntity)) throw new IllegalArgumentException("Unsupported entity type"); @@ -484,6 +485,7 @@ public class DefaultEntitiesVersionControlService implements EntitiesVersionCont .exportRelations(otherVersion.hasRelations()) .exportAttributes(otherVersion.hasAttributes()) .exportCredentials(otherVersion.hasCredentials()) + .exportCalculatedFields(otherVersion.hasCalculatedFields()) .build()); EntityExportData currentVersion; try { @@ -503,7 +505,7 @@ public class DefaultEntitiesVersionControlService implements EntitiesVersionCont @Override - public ListenableFuture> listBranches(TenantId tenantId) throws Exception { + public ListenableFuture> listBranches(TenantId tenantId) { return gitServiceQueue.listBranches(tenantId); } diff --git a/application/src/main/java/org/thingsboard/server/service/sync/vc/data/EntitiesExportCtx.java b/application/src/main/java/org/thingsboard/server/service/sync/vc/data/EntitiesExportCtx.java index 669dad347a..b3007c6738 100644 --- a/application/src/main/java/org/thingsboard/server/service/sync/vc/data/EntitiesExportCtx.java +++ b/application/src/main/java/org/thingsboard/server/service/sync/vc/data/EntitiesExportCtx.java @@ -69,6 +69,7 @@ public abstract class EntitiesExportCtx { .exportRelations(config.isSaveRelations()) .exportAttributes(config.isSaveAttributes()) .exportCredentials(config.isSaveCredentials()) + .exportCalculatedFields(config.isSaveCalculatedFields()) .build(); } @@ -85,4 +86,5 @@ public abstract class EntitiesExportCtx { log.debug("[{}][{}] Local cache put: {}", internalId.getEntityType(), internalId.getId(), externalId); externalIdMap.put(internalId, externalId != null ? externalId : internalId); } + } diff --git a/application/src/main/java/org/thingsboard/server/service/sync/vc/data/EntitiesImportCtx.java b/application/src/main/java/org/thingsboard/server/service/sync/vc/data/EntitiesImportCtx.java index ab836fa13d..c1a8dad161 100644 --- a/application/src/main/java/org/thingsboard/server/service/sync/vc/data/EntitiesImportCtx.java +++ b/application/src/main/java/org/thingsboard/server/service/sync/vc/data/EntitiesImportCtx.java @@ -19,6 +19,7 @@ import lombok.Data; import lombok.extern.slf4j.Slf4j; import org.thingsboard.server.common.data.EntityType; import org.thingsboard.server.common.data.User; +import org.thingsboard.server.common.data.cf.CalculatedField; import org.thingsboard.server.common.data.id.EntityId; import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.common.data.relation.EntityRelation; @@ -31,6 +32,7 @@ import java.util.ArrayList; import java.util.Collection; import java.util.HashMap; import java.util.HashSet; +import java.util.LinkedHashMap; import java.util.LinkedHashSet; import java.util.List; import java.util.Map; @@ -54,6 +56,7 @@ public class EntitiesImportCtx { private final Set notFoundIds = new HashSet<>(); private final Set relations = new LinkedHashSet<>(); + private final Map calculatedFields = new LinkedHashMap<>(); private boolean finalImportAttempt = false; private EntityImportSettings settings; @@ -91,6 +94,10 @@ public class EntitiesImportCtx { return getSettings().isSaveCredentials(); } + public boolean isSaveCalculatedFields() { + return getSettings().isSaveCalculatedFields(); + } + public EntityId getInternalId(EntityId externalId) { var result = externalToInternalIdMap.get(externalId); log.debug("[{}][{}] Local cache {} for id", externalId.getEntityType(), externalId.getId(), result != null ? "hit" : "miss"); @@ -120,6 +127,10 @@ public class EntitiesImportCtx { relations.addAll(values); } + public void addCalculatedFields(Map calculatedFieldMap) { + calculatedFields.putAll(calculatedFieldMap); + } + public void addReferenceCallback(EntityId externalId, ThrowingRunnable tr) { if (tr != null) { referenceCallbacks.put(externalId, tr); @@ -140,5 +151,4 @@ public class EntitiesImportCtx { return notFoundIds.contains(externalId); } - } diff --git a/application/src/main/java/org/thingsboard/server/service/sync/vc/data/EntityTypeExportCtx.java b/application/src/main/java/org/thingsboard/server/service/sync/vc/data/EntityTypeExportCtx.java index e0a5b40281..55c8a763b0 100644 --- a/application/src/main/java/org/thingsboard/server/service/sync/vc/data/EntityTypeExportCtx.java +++ b/application/src/main/java/org/thingsboard/server/service/sync/vc/data/EntityTypeExportCtx.java @@ -39,6 +39,7 @@ public class EntityTypeExportCtx extends EntitiesExportCtx .exportRelations(config.isSaveRelations()) .exportAttributes(config.isSaveAttributes()) .exportCredentials(config.isSaveCredentials()) + .exportCalculatedFields(config.isSaveCalculatedFields()) .build(); this.overwrite = ObjectUtils.defaultIfNull(config.getSyncStrategy(), defaultSyncStrategy) == SyncStrategy.OVERWRITE; } diff --git a/application/src/test/java/org/thingsboard/server/service/sync/ie/ExportImportServiceSqlTest.java b/application/src/test/java/org/thingsboard/server/service/sync/ie/ExportImportServiceSqlTest.java index 8870194685..9184a3c034 100644 --- a/application/src/test/java/org/thingsboard/server/service/sync/ie/ExportImportServiceSqlTest.java +++ b/application/src/test/java/org/thingsboard/server/service/sync/ie/ExportImportServiceSqlTest.java @@ -80,6 +80,7 @@ import org.thingsboard.server.common.data.rule.RuleChainMetaData; import org.thingsboard.server.common.data.rule.RuleChainType; import org.thingsboard.server.common.data.rule.RuleNode; import org.thingsboard.server.common.data.security.Authority; +import org.thingsboard.server.common.data.sync.ie.DeviceExportData; import org.thingsboard.server.common.data.sync.ie.EntityExportData; import org.thingsboard.server.common.data.sync.ie.EntityExportSettings; import org.thingsboard.server.common.data.sync.ie.EntityImportResult; @@ -207,11 +208,12 @@ public class ExportImportServiceSqlTest extends AbstractControllerTest { CalculatedField calculatedField = createCalculatedField(tenantId1, device.getId(), asset.getId()); Map entitiesExportData = Stream.of(customer.getId(), asset.getId(), device.getId(), - ruleChain.getId(), dashboard.getId(), assetProfile.getId(), deviceProfile.getId(), calculatedField.getId()) + ruleChain.getId(), dashboard.getId(), assetProfile.getId(), deviceProfile.getId()) .map(entityId -> { try { return exportEntity(tenantAdmin1, entityId, EntityExportSettings.builder() .exportCredentials(false) + .exportCalculatedFields(true) .build()); } catch (Exception e) { throw new RuntimeException(e); @@ -281,23 +283,28 @@ public class ExportImportServiceSqlTest extends AbstractControllerTest { importEntity(tenantAdmin2, getAndClone(entitiesExportData, EntityType.DEVICE)); verify(tbClusterService, Mockito.never()).onDeviceUpdated(eq(importedDevice), eq(importedDevice)); + // calculated field of imported device: + List calculatedFields = calculatedFieldService.findCalculatedFieldsByEntityId(tenantId2, importedDevice.getId()); + assertThat(calculatedFields.size()).isOne(); + var importedCalculatedField = calculatedFields.get(0); + assertThat(importedCalculatedField.getName()).isEqualTo(calculatedField.getName()); + assertThat(importedCalculatedField.getExternalId()).isEqualTo(calculatedField.getId()); + EntityExportData updatedDeviceEntity = getAndClone(entitiesExportData, EntityType.DEVICE); updatedDeviceEntity.getEntity().setLabel("t" + updatedDeviceEntity.getEntity().getLabel()); Device updatedDevice = importEntity(tenantAdmin2, updatedDeviceEntity).getSavedEntity(); verify(tbClusterService).onDeviceUpdated(eq(updatedDevice), eq(importedDevice)); - CalculatedField importedCalculatedField = (CalculatedField) importEntity(tenantAdmin2, getAndClone(entitiesExportData, EntityType.CALCULATED_FIELD)).getSavedEntity(); - verify(entityActionService).logEntityAction(any(), eq(importedCalculatedField.getId()), eq(importedCalculatedField), - any(), eq(ActionType.ADDED), isNull()); - importEntity(tenantAdmin2, getAndClone(entitiesExportData, EntityType.CALCULATED_FIELD)); - verify(entityActionService, Mockito.never()).logEntityAction(any(), eq(importedCalculatedField.getId()), eq(importedCalculatedField), - any(), eq(ActionType.UPDATED), isNull()); + // update calculated field: + DeviceExportData deviceExportData = (DeviceExportData) getAndClone(entitiesExportData, EntityType.DEVICE); + deviceExportData.setCalculatedFields(deviceExportData.getCalculatedFields().stream().peek(field -> field.setName("t_" + field.getName())).toList()); + importEntity(tenantAdmin2, deviceExportData).getSavedEntity(); - EntityExportData updatedCalculatedFieldEntity = getAndClone(entitiesExportData, EntityType.CALCULATED_FIELD); - updatedCalculatedFieldEntity.getEntity().setName("t" + updatedCalculatedFieldEntity.getEntity().getName()); - CalculatedField updatedCalculatedField = importEntity(tenantAdmin2, updatedCalculatedFieldEntity).getSavedEntity(); - verify(entityActionService).logEntityAction(any(), eq(updatedCalculatedField.getId()), eq(updatedCalculatedField), - any(), eq(ActionType.UPDATED), isNull()); + calculatedFields = calculatedFieldService.findCalculatedFieldsByEntityId(tenantId2, importedDevice.getId()); + assertThat(calculatedFields.size()).isOne(); + importedCalculatedField = calculatedFields.get(0); + assertThat(importedCalculatedField.getExternalId()).isEqualTo(calculatedField.getId()); + assertThat(importedCalculatedField.getName()).startsWith("t_"); } @Test @@ -320,10 +327,11 @@ public class ExportImportServiceSqlTest extends AbstractControllerTest { Map ids = new HashMap<>(); for (EntityId entityId : List.of(customer.getId(), ruleChain.getId(), dashboard.getId(), assetProfile.getId(), asset.getId(), - deviceProfile.getId(), device.getId(), entityView.getId(), ruleChain.getId(), dashboard.getId(), calculatedField.getId())) { + deviceProfile.getId(), device.getId(), entityView.getId(), ruleChain.getId(), dashboard.getId())) { EntityExportData exportData = exportEntity(getSecurityUser(tenantAdmin1), entityId); EntityImportResult importResult = importEntity(getSecurityUser(tenantAdmin2), exportData, EntityImportSettings.builder() .saveCredentials(false) + .saveCalculatedFields(true) .build()); ids.put(entityId, (EntityId) importResult.getSavedEntity().getId()); } @@ -353,19 +361,20 @@ public class ExportImportServiceSqlTest extends AbstractControllerTest { assertThat(exportedDeviceProfile.getDefaultRuleChainId()).isEqualTo(ruleChain.getId()); assertThat(exportedDeviceProfile.getDefaultDashboardId()).isEqualTo(dashboard.getId()); - Device exportedDevice = (Device) exportEntity(tenantAdmin2, (DeviceId) ids.get(device.getId())).getEntity(); + EntityExportData entityExportData = exportEntity(tenantAdmin2, (DeviceId) ids.get(device.getId())); + Device exportedDevice = entityExportData.getEntity(); assertThat(exportedDevice.getCustomerId()).isEqualTo(customer.getId()); assertThat(exportedDevice.getDeviceProfileId()).isEqualTo(deviceProfile.getId()); + List calculatedFields = ((DeviceExportData) entityExportData).getCalculatedFields(); + assertThat(calculatedFields.size()).isOne(); + CalculatedField field = calculatedFields.get(0); + assertThat(field.getName()).isEqualTo(calculatedField.getName()); + EntityView exportedEntityView = (EntityView) exportEntity(tenantAdmin2, (EntityViewId) ids.get(entityView.getId())).getEntity(); assertThat(exportedEntityView.getCustomerId()).isEqualTo(customer.getId()); assertThat(exportedEntityView.getEntityId()).isEqualTo(device.getId()); - CalculatedField exportedCalculatedField = (CalculatedField) exportEntity(tenantAdmin2, (CalculatedFieldId) ids.get(calculatedField.getId())).getEntity(); - assertThat(exportedCalculatedField.getName()).isEqualTo(calculatedField.getName()); - assertThat(exportedCalculatedField.getEntityId()).isEqualTo(device.getId()); - assertThat(exportedCalculatedField.getConfiguration().getReferencedEntities()).isEqualTo(calculatedField.getConfiguration().getReferencedEntities()); - deviceProfile.setDefaultDashboardId(null); deviceProfileService.saveDeviceProfile(deviceProfile); DeviceProfile importedDeviceProfile = deviceProfileService.findDeviceProfileById(tenantId2, (DeviceProfileId) ids.get(deviceProfile.getId())); @@ -373,7 +382,6 @@ public class ExportImportServiceSqlTest extends AbstractControllerTest { deviceProfileService.saveDeviceProfile(importedDeviceProfile); } - protected Device createDevice(TenantId tenantId, CustomerId customerId, DeviceProfileId deviceProfileId, String name) { Device device = new Device(); device.setTenantId(tenantId); @@ -618,6 +626,7 @@ public class ExportImportServiceSqlTest extends AbstractControllerTest { protected , I extends EntityId> EntityExportData exportEntity(User user, I entityId) throws Exception { return exportEntity(user, entityId, EntityExportSettings.builder() .exportCredentials(true) + .exportCalculatedFields(true) .build()); } @@ -628,6 +637,7 @@ public class ExportImportServiceSqlTest extends AbstractControllerTest { protected , I extends EntityId> EntityImportResult importEntity(User user, EntityExportData exportData) throws Exception { return importEntity(user, exportData, EntityImportSettings.builder() .saveCredentials(true) + .saveCalculatedFields(true) .build()); } 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 28eaf06da7..453fea676f 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 @@ -576,12 +576,12 @@ public class VersionControlTest extends AbstractControllerTest { Asset asset = createAsset(null, null, "Asset 1"); Device device = createDevice(null, null, "Device 1", "test1"); CalculatedField calculatedField = createCalculatedField("CalculatedField1", device.getId(), asset.getId()); - String versionId = createVersion("calculated fields of asset and device", EntityType.ASSET, EntityType.DEVICE, EntityType.DEVICE_PROFILE, EntityType.ASSET_PROFILE, EntityType.CALCULATED_FIELD); + String versionId = createVersion("calculated fields of asset and device", EntityType.ASSET, EntityType.DEVICE, EntityType.DEVICE_PROFILE, EntityType.ASSET_PROFILE); loginTenant2(); loadVersion(versionId, config -> { config.setLoadCredentials(false); - }, EntityType.ASSET, EntityType.DEVICE, EntityType.DEVICE_PROFILE, EntityType.ASSET_PROFILE, EntityType.CALCULATED_FIELD); + }, EntityType.ASSET, EntityType.DEVICE, EntityType.DEVICE_PROFILE, EntityType.ASSET_PROFILE); Asset importedAsset = findAsset(asset.getName()); Device importedDevice = findDevice(device.getName()); @@ -602,9 +602,9 @@ public class VersionControlTest extends AbstractControllerTest { public void testVcWithCalculatedFields_sameTenant() throws Exception { Asset asset = createAsset(null, null, "Asset 1"); CalculatedField calculatedField = createCalculatedField("CalculatedField", asset.getId(), asset.getId()); - String versionId = createVersion("asset and field", EntityType.ASSET, EntityType.CALCULATED_FIELD); + String versionId = createVersion("asset and field", EntityType.ASSET); - loadVersion(versionId, EntityType.ASSET, EntityType.CALCULATED_FIELD); + loadVersion(versionId, EntityType.ASSET); CalculatedField importedCalculatedField = findCalculatedFieldByEntityId(asset.getId()); checkImportedEntity(tenantId1, calculatedField, tenantId1, importedCalculatedField); assertThat(importedCalculatedField.getName()).isEqualTo(calculatedField.getName()); @@ -676,10 +676,10 @@ public class VersionControlTest extends AbstractControllerTest { request.setEntityTypes(Arrays.stream(entityTypes).collect(Collectors.toMap(t -> t, entityType -> { EntityTypeVersionCreateConfig config = new EntityTypeVersionCreateConfig(); config.setAllEntities(true); - config.setSaveRelations(true); config.setSaveAttributes(true); config.setSaveCredentials(true); + config.setSaveCalculatedFields(true); return config; }))); @@ -745,6 +745,7 @@ public class VersionControlTest extends AbstractControllerTest { config.setLoadAttributes(true); config.setLoadRelations(true); config.setLoadCredentials(true); + config.setLoadCalculatedFields(true); config.setRemoveOtherEntities(false); config.setFindExistingEntityByName(true); configModifier.accept(config); diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/sync/ie/AssetExportData.java b/common/data/src/main/java/org/thingsboard/server/common/data/sync/ie/AssetExportData.java new file mode 100644 index 0000000000..4bb836d5d9 --- /dev/null +++ b/common/data/src/main/java/org/thingsboard/server/common/data/sync/ie/AssetExportData.java @@ -0,0 +1,28 @@ +/** + * 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.common.data.sync.ie; + +import lombok.Data; +import lombok.EqualsAndHashCode; +import lombok.ToString; +import org.thingsboard.server.common.data.asset.Asset; + +@EqualsAndHashCode(callSuper = true) +@ToString(callSuper = true) +@Data +public class AssetExportData extends CalculatedFieldExportData { + +} diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/sync/ie/AssetProfileExportData.java b/common/data/src/main/java/org/thingsboard/server/common/data/sync/ie/AssetProfileExportData.java new file mode 100644 index 0000000000..42d9523828 --- /dev/null +++ b/common/data/src/main/java/org/thingsboard/server/common/data/sync/ie/AssetProfileExportData.java @@ -0,0 +1,28 @@ +/** + * 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.common.data.sync.ie; + +import lombok.Data; +import lombok.EqualsAndHashCode; +import lombok.ToString; +import org.thingsboard.server.common.data.asset.AssetProfile; + +@EqualsAndHashCode(callSuper = true) +@ToString(callSuper = true) +@Data +public class AssetProfileExportData extends CalculatedFieldExportData { + +} diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/sync/ie/CalculatedFieldExportData.java b/common/data/src/main/java/org/thingsboard/server/common/data/sync/ie/CalculatedFieldExportData.java new file mode 100644 index 0000000000..7741f88822 --- /dev/null +++ b/common/data/src/main/java/org/thingsboard/server/common/data/sync/ie/CalculatedFieldExportData.java @@ -0,0 +1,56 @@ +/** + * 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.common.data.sync.ie; + +import com.fasterxml.jackson.annotation.JsonIgnore; +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonProperty; +import lombok.Data; +import lombok.EqualsAndHashCode; +import lombok.ToString; +import org.thingsboard.server.common.data.ExportableEntity; +import org.thingsboard.server.common.data.cf.CalculatedField; + +import java.util.Comparator; +import java.util.List; + +@Data +@EqualsAndHashCode(callSuper = true) +@ToString(callSuper = true) +public class CalculatedFieldExportData> extends EntityExportData { + + public static final Comparator calculatedFieldsComparator = Comparator.comparing(CalculatedField::getName); + + @JsonProperty(index = 102) + @JsonIgnoreProperties({"entityId", "createdTime", "version"}) + private List calculatedFields; + + @JsonIgnore + @Override + public boolean hasCalculatedFields() { + return calculatedFields != null; + } + + @Override + public CalculatedFieldExportData sort() { + super.sort(); + if (calculatedFields != null && !calculatedFields.isEmpty()) { + calculatedFields.sort(calculatedFieldsComparator); + } + return this; + } + +} diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/sync/ie/DeviceExportData.java b/common/data/src/main/java/org/thingsboard/server/common/data/sync/ie/DeviceExportData.java index bfa82caa2b..a391067c74 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/sync/ie/DeviceExportData.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/sync/ie/DeviceExportData.java @@ -27,7 +27,7 @@ import org.thingsboard.server.common.data.security.DeviceCredentials; @EqualsAndHashCode(callSuper = true) @ToString(callSuper = true) @Data -public class DeviceExportData extends EntityExportData { +public class DeviceExportData extends CalculatedFieldExportData { @JsonProperty(index = 3) @JsonIgnoreProperties({"id", "deviceId", "createdTime", "version"}) @@ -38,4 +38,5 @@ public class DeviceExportData extends EntityExportData { public boolean hasCredentials() { return credentials != null; } + } diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/sync/ie/DeviceProfileExportData.java b/common/data/src/main/java/org/thingsboard/server/common/data/sync/ie/DeviceProfileExportData.java new file mode 100644 index 0000000000..b6b4d9bdcf --- /dev/null +++ b/common/data/src/main/java/org/thingsboard/server/common/data/sync/ie/DeviceProfileExportData.java @@ -0,0 +1,28 @@ +/** + * 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.common.data.sync.ie; + +import lombok.Data; +import lombok.EqualsAndHashCode; +import lombok.ToString; +import org.thingsboard.server.common.data.DeviceProfile; + +@EqualsAndHashCode(callSuper = true) +@ToString(callSuper = true) +@Data +public class DeviceProfileExportData extends CalculatedFieldExportData { + +} diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/sync/ie/EntityExportData.java b/common/data/src/main/java/org/thingsboard/server/common/data/sync/ie/EntityExportData.java index 7bc9a63c29..5b7d95db1a 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/sync/ie/EntityExportData.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/sync/ie/EntityExportData.java @@ -37,7 +37,10 @@ import java.util.Map; @JsonIgnoreProperties(ignoreUnknown = true) @JsonTypeInfo(use = JsonTypeInfo.Id.NAME, property = "entityType", include = As.EXISTING_PROPERTY, visible = true, defaultImpl = EntityExportData.class) @JsonSubTypes({ + @Type(name = "DEVICE_PROFILE", value = DeviceProfileExportData.class), + @Type(name = "ASSET_PROFILE", value = AssetProfileExportData.class), @Type(name = "DEVICE", value = DeviceExportData.class), + @Type(name = "ASSET", value = AssetExportData.class), @Type(name = "RULE_CHAIN", value = RuleChainExportData.class), @Type(name = "WIDGET_TYPE", value = WidgetTypeExportData.class), @Type(name = "WIDGETS_BUNDLE", value = WidgetsBundleExportData.class) @@ -96,4 +99,9 @@ public class EntityExportData> { return relations != null; } + @JsonIgnore + public boolean hasCalculatedFields() { + return false; + } + } diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/sync/ie/EntityExportSettings.java b/common/data/src/main/java/org/thingsboard/server/common/data/sync/ie/EntityExportSettings.java index 46dc6860ee..078ca29e28 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/sync/ie/EntityExportSettings.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/sync/ie/EntityExportSettings.java @@ -25,7 +25,10 @@ import lombok.NoArgsConstructor; @NoArgsConstructor @Builder public class EntityExportSettings { + private boolean exportRelations; private boolean exportAttributes; private boolean exportCredentials; + private boolean exportCalculatedFields; + } diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/sync/ie/EntityImportSettings.java b/common/data/src/main/java/org/thingsboard/server/common/data/sync/ie/EntityImportSettings.java index 2abdb55b65..d49801537c 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/sync/ie/EntityImportSettings.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/sync/ie/EntityImportSettings.java @@ -25,8 +25,11 @@ import lombok.NoArgsConstructor; @NoArgsConstructor @Builder public class EntityImportSettings { + private boolean findExistingByName; private boolean updateRelations; private boolean saveAttributes; private boolean saveCredentials; + private boolean saveCalculatedFields; + } diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/sync/vc/request/create/VersionCreateConfig.java b/common/data/src/main/java/org/thingsboard/server/common/data/sync/vc/request/create/VersionCreateConfig.java index 5a02b18364..6d1ac2283b 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/sync/vc/request/create/VersionCreateConfig.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/sync/vc/request/create/VersionCreateConfig.java @@ -17,13 +17,18 @@ package org.thingsboard.server.common.data.sync.vc.request.create; import lombok.Data; +import java.io.Serial; import java.io.Serializable; @Data public class VersionCreateConfig implements Serializable { + + @Serial private static final long serialVersionUID = 1223723167716612772L; private boolean saveRelations; private boolean saveAttributes; private boolean saveCredentials; + private boolean saveCalculatedFields; + } diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/sync/vc/request/load/VersionLoadConfig.java b/common/data/src/main/java/org/thingsboard/server/common/data/sync/vc/request/load/VersionLoadConfig.java index 0ae263dada..7f3ce89372 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/sync/vc/request/load/VersionLoadConfig.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/sync/vc/request/load/VersionLoadConfig.java @@ -23,5 +23,6 @@ public class VersionLoadConfig { private boolean loadRelations; private boolean loadAttributes; private boolean loadCredentials; + private boolean loadCalculatedFields; } diff --git a/dao/src/main/java/org/thingsboard/server/dao/cf/BaseCalculatedFieldService.java b/dao/src/main/java/org/thingsboard/server/dao/cf/BaseCalculatedFieldService.java index 416bff6ea3..dceef73aba 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/cf/BaseCalculatedFieldService.java +++ b/dao/src/main/java/org/thingsboard/server/dao/cf/BaseCalculatedFieldService.java @@ -15,7 +15,6 @@ */ package org.thingsboard.server.dao.cf; -import com.google.common.util.concurrent.ListenableFuture; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.springframework.stereotype.Service; diff --git a/dao/src/main/java/org/thingsboard/server/dao/model/sql/CalculatedFieldLinkEntity.java b/dao/src/main/java/org/thingsboard/server/dao/model/sql/CalculatedFieldLinkEntity.java index 2668a1a0d8..3a529b7d93 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/model/sql/CalculatedFieldLinkEntity.java +++ b/dao/src/main/java/org/thingsboard/server/dao/model/sql/CalculatedFieldLinkEntity.java @@ -15,9 +15,7 @@ */ package org.thingsboard.server.dao.model.sql; -import com.fasterxml.jackson.databind.JsonNode; import jakarta.persistence.Column; -import jakarta.persistence.Convert; import jakarta.persistence.Entity; import jakarta.persistence.Table; import lombok.Data; @@ -60,6 +58,15 @@ public class CalculatedFieldLinkEntity extends BaseSqlEntity Date: Fri, 28 Feb 2025 18:38:22 +0200 Subject: [PATCH 139/345] tbel: new ver 1.2.6 add test for docs: Unmodifiable --- .../service/script/TbelInvokeDocsIoTest.java | 68 ++++++++++++++++++- pom.xml | 2 +- 2 files changed, 66 insertions(+), 4 deletions(-) diff --git a/application/src/test/java/org/thingsboard/server/service/script/TbelInvokeDocsIoTest.java b/application/src/test/java/org/thingsboard/server/service/script/TbelInvokeDocsIoTest.java index 8c8b82928a..896b7b2261 100644 --- a/application/src/test/java/org/thingsboard/server/service/script/TbelInvokeDocsIoTest.java +++ b/application/src/test/java/org/thingsboard/server/service/script/TbelInvokeDocsIoTest.java @@ -21,6 +21,7 @@ import org.thingsboard.script.api.tbel.TbDate; import java.nio.charset.StandardCharsets; import java.util.ArrayList; +import java.util.Arrays; import java.util.Base64; import java.util.Collections; import java.util.Comparator; @@ -660,14 +661,13 @@ class TbelInvokeDocsIoTest extends AbstractTbelInvokeTest { """; decoderStr = """ var list = msg.list; - var listAdd = ["thigsboard", 4, 67]; return { list: list.clone(), length: list.length(), memorySize: list.memorySize(), indOf1: list.indexOf("B", 1), indOf2: list.indexOf(2, 2), - sStr: list.validateClazzInArrayIsOnlyString() + sStr: list.validateClazzInArrayIsOnlyNumber() } """; ArrayList list = new ArrayList<>(List.of(67, 2, 2, 2)); @@ -677,7 +677,7 @@ class TbelInvokeDocsIoTest extends AbstractTbelInvokeTest { expected.put("memorySize", 32L); expected.put("indOf1", -1); expected.put("indOf2", 2); - expected.put("sStr", false); + expected.put("sStr", true); Object actual = invokeScript(evalScript(decoderStr), msgStr); assertEquals(expected, actual); } @@ -2148,6 +2148,68 @@ class TbelInvokeDocsIoTest extends AbstractTbelInvokeTest { assertEquals(expected, actual); } + @Test + public void toUnmodifiableExecutionArrayList_Test() throws ExecutionException, InterruptedException { + msgStr = "{}"; + decoderStr = String.format(""" + var original = []; + original.add(0x35); + var unmodifiable = original.toUnmodifiable(); + msg.result = unmodifiable; + return {msg: msg}; + """); + LinkedHashMap expected = new LinkedHashMap<>(); + List expectedList = Arrays.asList(0x35); + LinkedHashMap expectedResult = new LinkedHashMap<>(); + expectedResult.put("result", expectedList); + expected.put("msg", expectedResult); + Object actual = invokeScript(evalScript(decoderStr), msgStr); + assertEquals(expected, actual); + + decoderStr = String.format(""" + var original = []; + original.add(0x67); + var unmodifiable = original.toUnmodifiable(); + unmodifiable.add(0x35); + msg.result = unmodifiable; + return {msg: msg}; + """); + assertThatThrownBy(() -> { + invokeScript(evalScript(decoderStr), msgStr); + }).hasMessageContaining("Error: unmodifiable.add(0x35): List is unmodifiable"); + } + + + @Test + public void toUnmodifiableExecutionHashMap_Test() throws ExecutionException, InterruptedException { + msgStr = "{}"; + decoderStr = String.format(""" + var original = {}; + original.putIfAbsent("entry1", 73); + var unmodifiable = original.toUnmodifiable(); + msg.result = unmodifiable; + return {msg: msg}; + """); + LinkedHashMap expected = new LinkedHashMap<>(); + LinkedHashMap expectedMap = new LinkedHashMap<>(Map.of("entry1", 73)); + LinkedHashMap expectedResult = new LinkedHashMap<>(); + expectedResult.put("result", expectedMap); + expected.put("msg", expectedResult); + Object actual = invokeScript(evalScript(decoderStr), msgStr); + assertEquals(expected, actual); + + decoderStr = String.format(""" + var original = {}; + original.humidity = 73; + var unmodifiable = original.toUnmodifiable(); + unmodifiable.put("temperature1", 96); + msg.result = unmodifiable; + return {msg: msg}; + """); + assertThatThrownBy(() -> { + invokeScript(evalScript(decoderStr), msgStr); + }).hasMessageContaining("Error: unmodifiable.put(\"temperature1\", 96): Map is unmodifiable"); + } private List splice(List oldList, int start, int deleteCount, Object... values) { start = initStartIndex(oldList, start); diff --git a/pom.xml b/pom.xml index 0f26f4f05c..5067582bfd 100755 --- a/pom.xml +++ b/pom.xml @@ -83,7 +83,7 @@ 3.9.2 3.25.5 1.63.0 - 1.2.5 + 1.2.6 1.18.32 1.2.5 1.2.5 From cd3a7c4c4ea1e1ff863a7006d24a6184285535d9 Mon Sep 17 00:00:00 2001 From: Igor Kulikov Date: Fri, 28 Feb 2025 19:31:22 +0200 Subject: [PATCH 140/345] UI: Trip Map - Implement timeline control. Implement trip item. --- .../lib/maps/data-layer/data-layer-utils.ts | 96 +++++ .../lib/maps/data-layer/map-data-layer.ts | 67 +--- .../lib/maps/data-layer/trips-data-layer.ts | 347 +++++++++++++++++- .../home/components/widget/lib/maps/map.ts | 106 +++++- .../widget/lib/maps/models/map.models.ts | 52 ++- .../panels/map-timeline-panel.component.html | 74 +++- .../panels/map-timeline-panel.component.scss | 29 +- .../panels/map-timeline-panel.component.ts | 174 ++++++++- .../assets/locale/locale.constant-en_US.json | 3 +- 9 files changed, 819 insertions(+), 129 deletions(-) create mode 100644 ui-ngx/src/app/modules/home/components/widget/lib/maps/data-layer/data-layer-utils.ts 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 new file mode 100644 index 0000000000..f5872844c9 --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/widget/lib/maps/data-layer/data-layer-utils.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 { + DataLayerTooltipSettings, + DataLayerTooltipTrigger, processTooltipTemplate, + TbMapDatasource +} from '@home/components/widget/lib/maps/models/map.models'; +import { TbMap } from '@home/components/widget/lib/maps/map'; +import { FormattedData } from '@shared/models/widget.models'; +import L from 'leaflet'; +import { DataLayerPatternProcessor } from '@home/components/widget/lib/maps/data-layer/map-data-layer'; + +export const createTooltip = (map: TbMap, + layer: L.Layer, + settings: DataLayerTooltipSettings, + data: FormattedData, + canOpen: () => boolean): L.Popup => { + const tooltip = L.popup(); + layer.bindPopup(tooltip, {autoClose: settings.autoclose, closeOnClick: false}); + layer.off('click'); + if (settings.trigger === DataLayerTooltipTrigger.click) { + layer.on('click', () => { + if (tooltip.isOpen()) { + layer.closePopup(); + } else if (canOpen()) { + layer.openPopup(); + } + }); + } else if (settings.trigger === DataLayerTooltipTrigger.hover) { + layer.on('mouseover', () => { + if (canOpen()) { + layer.openPopup(); + } + }); + layer.on('mousemove', (e) => { + tooltip.setLatLng(e.latlng); + }); + layer.on('mouseout', () => { + layer.closePopup(); + }); + } + layer.on('popupopen', () => { + bindTooltipActions(map, tooltip, settings, data); + (layer as any)._popup._closeButton.addEventListener('click', (event: Event) => { + event.preventDefault(); + }); + }); + return tooltip; +} + +export const updateTooltip = (map: TbMap, + tooltip: L.Popup, + settings: DataLayerTooltipSettings, + processor: DataLayerPatternProcessor, + data: FormattedData, + dsData: FormattedData[]): void => { + let tooltipTemplate = processor.processPattern(data, dsData); + tooltipTemplate = processTooltipTemplate(tooltipTemplate); + tooltip.setContent(tooltipTemplate); + if (tooltip.isOpen() && tooltip.getElement()) { + bindTooltipActions(map, tooltip, settings, data); + } +} + +const bindTooltipActions = (map: TbMap, tooltip: L.Popup, settings: DataLayerTooltipSettings, data: FormattedData): void => { + const actions = tooltip.getElement().getElementsByClassName('tb-custom-action'); + Array.from(actions).forEach( + (element: HTMLElement) => { + const actionName = element.getAttribute('data-action-name'); + if (settings?.tagActions) { + const action = settings.tagActions.find(action => action.name === actionName); + if (action) { + element.onclick = ($event) => + { + map.dataItemClick($event, action, data.$datasource); + return false; + }; + } + } + } + ); +} diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/maps/data-layer/map-data-layer.ts b/ui-ngx/src/app/modules/home/components/widget/lib/maps/data-layer/map-data-layer.ts index 8916931d2b..1c4e58a320 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/maps/data-layer/map-data-layer.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/maps/data-layer/map-data-layer.ts @@ -44,6 +44,7 @@ import { CompiledTbFunction } from '@shared/models/js-function.models'; import { map } from 'rxjs/operators'; import { WidgetContext } from '@home/models/widget-component.models'; import { CustomTranslatePipe } from '@shared/pipe/custom-translate.pipe'; +import { createTooltip, updateTooltip } from './data-layer-utils'; export abstract class TbDataLayerItem = TbMapDataLayer, L extends L.Layer = L.Layer> { @@ -60,8 +61,12 @@ export abstract class TbDataLayerItem { + return !this.isEditing(); + }); + updateTooltip(this.dataLayer.getMap(), this.tooltip, + this.settings.tooltip, this.dataLayer.dataLayerTooltipProcessor, data, dsData); } this.bindEvents(); try { @@ -220,12 +225,8 @@ export abstract class TbDataLayerItem, dsData: FormattedData[]) { if (this.settings.tooltip.show) { - let tooltipTemplate = this.dataLayer.dataLayerTooltipProcessor.processPattern(data, dsData); - tooltipTemplate = processTooltipTemplate(tooltipTemplate); - this.tooltip.setContent(tooltipTemplate); - if (this.tooltip.isOpen() && this.tooltip.getElement()) { - this.bindTooltipActions(); - } + updateTooltip(this.dataLayer.getMap(), this.tooltip, + this.settings.tooltip, this.dataLayer.dataLayerTooltipProcessor, data, dsData); } } @@ -253,56 +254,6 @@ export abstract class TbDataLayerItem; - private createTooltip() { - this.tooltip = L.popup(); - this.layer.bindPopup(this.tooltip, {autoClose: this.settings.tooltip.autoclose, closeOnClick: false}); - this.layer.off('click'); - if (this.settings.tooltip.trigger === DataLayerTooltipTrigger.click) { - this.layer.on('click', () => { - if (this.tooltip.isOpen()) { - this.layer.closePopup(); - } else if (!this.isEditing()) { - this.layer.openPopup(); - } - }); - } else if (this.settings.tooltip.trigger === DataLayerTooltipTrigger.hover) { - this.layer.on('mouseover', () => { - if (!this.isEditing()) { - this.layer.openPopup(); - } - }); - this.layer.on('mousemove', (e) => { - this.tooltip.setLatLng(e.latlng); - }); - this.layer.on('mouseout', () => { - this.layer.closePopup(); - }); - } - this.layer.on('popupopen', () => { - this.bindTooltipActions(); - (this.layer as any)._popup._closeButton.addEventListener('click', (event: Event) => { - event.preventDefault(); - }); - }); - } - - private bindTooltipActions() { - const actions = this.tooltip.getElement().getElementsByClassName('tb-custom-action'); - Array.from(actions).forEach( - (element: HTMLElement) => { - const actionName = element.getAttribute('data-action-name'); - if (this.settings.tooltip?.tagActions) { - const action = this.settings.tooltip.tagActions.find(action => action.name === actionName); - if (action) { - element.onclick = ($event) => - { - this.dataLayer.getMap().dataItemClick($event, action, this.data.$datasource); - return false; - }; - } - } - }); - } } export enum MapDataLayerType { diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/maps/data-layer/trips-data-layer.ts b/ui-ngx/src/app/modules/home/components/widget/lib/maps/data-layer/trips-data-layer.ts index 63824365ca..a7b0d74d4d 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/maps/data-layer/trips-data-layer.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/maps/data-layer/trips-data-layer.ts @@ -22,17 +22,25 @@ import { UnplacedMapDataItem } from '@home/components/widget/lib/maps/data-layer/map-data-layer'; import { - BaseMarkerShapeSettings, ClusterMarkerColorFunction, - DataLayerColorType, defaultBaseMarkersDataLayerSettings, defaultBaseTripsDataLayerSettings, + BaseMarkerShapeSettings, + calculateInterpolationRatio, calculateLastPoints, + ClusterMarkerColorFunction, + DataLayerColorType, + defaultBaseMarkersDataLayerSettings, + defaultBaseTripsDataLayerSettings, + findRotationAngle, + interpolateLineSegment, isValidLatLng, loadImageWithAspect, - MapStringFunction, MapType, + MapStringFunction, + MapType, MarkerIconInfo, MarkerIconSettings, MarkerImageFunction, MarkerImageInfo, MarkerImageSettings, MarkerImageType, - MarkerPositionFunction, MarkersDataLayerSettings, + MarkerPositionFunction, + MarkersDataLayerSettings, MarkerShapeSettings, MarkerType, TbMapDatasource, @@ -49,12 +57,244 @@ import tinycolor from 'tinycolor2'; import { MatIconRegistry } from '@angular/material/icon'; import { DomSanitizer } from '@angular/platform-browser'; import { catchError, map, switchMap } from 'rxjs/operators'; -import L from 'leaflet'; +import L, { PolylineDecorator } from 'leaflet'; import { CompiledTbFunction } from '@shared/models/js-function.models'; -import { isDefined, isDefinedAndNotNull, parseTbFunction, safeExecuteTbFunction } from '@core/utils'; +import { + deepClone, + isDefined, + isDefinedAndNotNull, + isEmptyStr, + isUndefined, + parseTbFunction, + safeExecuteTbFunction +} from '@core/utils'; import { ImagePipe } from '@shared/pipe/image.pipe'; import { TbMap } from '@home/components/widget/lib/maps/map'; import { DataKeyType } from '@shared/models/telemetry/telemetry.models'; +import moment from 'moment/moment'; +import { TbImageMap } from '@home/components/widget/lib/maps/image-map'; +import { createTooltip, updateTooltip } from '@home/components/widget/lib/maps/data-layer/data-layer-utils'; + +type TripRouteData = {[time: number]: FormattedData}; + +class TbTripDataItem { + + private tripRouteData: TripRouteData; + + private layer: L.FeatureGroup; + + private marker: L.Marker; + private markerTooltip: L.Popup; + private labelOffset: L.PointTuple; + + private polyline: L.Polyline; + private polylineDecorator: PolylineDecorator; + private points: L.FeatureGroup; + + private currentTime: number; + private currentPositionData: FormattedData; + + constructor(private rawRouteData: FormattedData[], + private latestData: FormattedData, + private settings: TripsDataLayerSettings, + private dataLayer: TbTripsDataLayer) { + this.tripRouteData = this.prepareTripRouteData(); + this.create(); + } + + private create() { + this.updateCurrentPosition(); + this.layer = L.featureGroup(); + let pointData = this.currentPositionData; + if (this.latestData) { + pointData = {...pointData, ...this.latestData}; + } + this.createMarker(pointData); + try { + this.dataLayer.getDataLayerContainer().addLayer(this.layer); + } catch (e) { + console.warn(e); + } + } + + public update(rawRouteData: FormattedData[]) { + this.rawRouteData = rawRouteData; + this.tripRouteData = this.prepareTripRouteData(); + this.updateCurrentPosition(true); + let pointData = this.currentPositionData; + if (this.latestData) { + pointData = {...pointData, ...this.latestData}; + } + this.updateMarker(pointData); + } + + public updateLatestData(latestData: FormattedData) { + this.latestData = latestData; + this.updateAppearance(); + } + + public updateAppearance() { + let data = this.currentPositionData; + if (this.latestData) { + data = {...data, ...this.latestData}; + } + const dsData = this.dataLayer.getMap().getData(); + if (this.settings.tooltip.show) { + updateTooltip(this.dataLayer.getMap(), this.markerTooltip, + this.settings.tooltip, this.dataLayer.dataLayerTooltipProcessor, data, dsData); + } + this.updateMarkerIcon(data, dsData); + } + + public updateCurrentTime() { + this.updateCurrentPosition(); + let pointData = this.currentPositionData; + if (this.latestData) { + pointData = {...pointData, ...this.latestData}; + } + this.updateMarker(pointData); + } + + public remove() { + this.dataLayer.getDataLayerContainer().removeLayer(this.layer); + this.layer.off(); + } + + public getLayer(): L.Layer { + return this.layer; + } + + private createMarker(data: FormattedData) { + const dsData = this.dataLayer.getMap().getData(); + const location = this.dataLayer.extractLocation(data, dsData); + this.marker = L.marker(location, { + tbMarkerData: data, + snapIgnore: true + }); + this.marker.addTo(this.layer); + this.updateMarkerIcon(data, dsData); + if (this.settings.tooltip?.show) { + this.markerTooltip = createTooltip(this.dataLayer.getMap(), + this.marker, this.settings.tooltip, data, () => true); + updateTooltip(this.dataLayer.getMap(), this.markerTooltip, + this.settings.tooltip, this.dataLayer.dataLayerTooltipProcessor, data, dsData); + } + } + + private updateMarker(data: FormattedData) { + const dsData = this.dataLayer.getMap().getData(); + this.marker.options.tbMarkerData = data; + this.updateMarkerLocation(data, dsData); + if (this.settings.tooltip.show) { + updateTooltip(this.dataLayer.getMap(), this.markerTooltip, + this.settings.tooltip, this.dataLayer.dataLayerTooltipProcessor, data, dsData); + } + this.updateMarkerIcon(data, dsData); + } + + private updateMarkerLocation(data: FormattedData, dsData: FormattedData[]) { + const location = this.dataLayer.extractLocation(data, dsData); + if (!this.marker.getLatLng().equals(location)) { + this.marker.setLatLng(location); + } + } + + private updateMarkerIcon(data: FormattedData, dsData: FormattedData[]) { + this.dataLayer.tripMarkerIconProcessor.createMarkerIcon(data, dsData).subscribe( + (iconInfo) => { + const options = deepClone(iconInfo.icon.options); + this.marker.setIcon(iconInfo.icon); + const anchor = options.iconAnchor; + if (anchor && Array.isArray(anchor)) { + this.labelOffset = [iconInfo.size[0] / 2 - anchor[0], 10 - anchor[1]]; + } else { + this.labelOffset = [0, -iconInfo.size[1] * this.dataLayer.markerOffset[1] + 10]; + } + this.updateMarkerLabel(data, dsData); + } + ); + } + + private updateMarkerLabel(data: FormattedData, dsData: FormattedData[]) { + if (this.settings.label.show) { + this.marker.unbindTooltip(); + const label = this.dataLayer.dataLayerLabelProcessor.processPattern(data, dsData); + const labelColor = this.dataLayer.getCtx().widgetConfig.color; + const content: L.Content = `

${label}
`; + this.marker.bindTooltip(content, { className: 'tb-marker-label', permanent: true, direction: 'top', offset: this.labelOffset }); + } + } + + private prepareTripRouteData(): TripRouteData { + const result: TripRouteData = {}; + const minTime = this.dataLayer.getMap().getMinTime(); + const maxTime = this.dataLayer.getMap().getMaxTime(); + const timeStep = this.dataLayer.getMap().getTimeStep(); + const timeline = this.dataLayer.getMap().hasTimeline(); + for (const data of this.rawRouteData) { + const currentTime = data.time; + const normalizeTime = timeline ? (minTime + + Math.ceil((currentTime - minTime) / timeStep) * timeStep) : currentTime; + result[normalizeTime] = { + ...data, + minTime: minTime !== Infinity ? moment(minTime).format('YYYY-MM-DD HH:mm:ss') : '', + maxTime: maxTime !== -Infinity ? moment(maxTime).format('YYYY-MM-DD HH:mm:ss') : '', + rotationAngle: this.settings.rotateMarker ? this.settings.offsetAngle : 0 + }; + } + if (timeline) { + const xKey = this.settings.xKey.label; + const yKey = this.settings.yKey.label; + const timeStamp = Object.keys(result); + for (let i = 0; i < timeStamp.length - 1; i++) { + if (isUndefined(result[timeStamp[i + 1]][xKey]) || isUndefined(result[timeStamp[i + 1]][yKey])) { + for (let j = i + 2; j < timeStamp.length - 1; j++) { + if (isDefined(result[timeStamp[j]][xKey]) || isDefined(result[timeStamp[j]][yKey])) { + const ratio = calculateInterpolationRatio(Number(timeStamp[i]), Number(timeStamp[j]), Number(timeStamp[i + 1])); + result[timeStamp[i + 1]] = { + ...interpolateLineSegment(result[timeStamp[i]], result[timeStamp[j]], xKey, yKey, ratio), + ...result[timeStamp[i + 1]], + }; + break; + } + } + } + if (this.settings.rotateMarker) { + result[timeStamp[i]].rotationAngle += findRotationAngle(result[timeStamp[i]], result[timeStamp[i + 1]], xKey, yKey); + } + } + } + return result; + } + + private updateCurrentPosition(force = false) { + if (this.currentTime !== this.dataLayer.getMap().getCurrentTime() || force) { + this.currentTime = this.dataLayer.getMap().getCurrentTime(); + let currentPosition = this.tripRouteData[this.currentTime]; + if (!currentPosition) { + const timePoints = Object.keys(this.tripRouteData).map(item => parseInt(item, 10)); + for (let i = 1; i < timePoints.length; i++) { + if (timePoints[i - 1] < this.currentTime && timePoints[i] > this.currentTime) { + const beforePosition = this.tripRouteData[timePoints[i - 1]]; + const afterPosition = this.tripRouteData[timePoints[i]]; + const ratio = calculateInterpolationRatio(timePoints[i - 1], timePoints[i], this.currentTime); + currentPosition = { + ...beforePosition, + time: this.currentTime, + ...interpolateLineSegment(beforePosition, afterPosition, this.settings.xKey.label, this.settings.yKey.label, ratio) + }; + break; + } + } + } + if (!currentPosition) { + currentPosition = calculateLastPoints(this.tripRouteData, this.currentTime); + } + this.currentPositionData = currentPosition; + } + } + +} abstract class TripMarkerIconProcessor { @@ -205,7 +445,7 @@ class ImageTripMarkerIconProcessor extends TripMarkerIconProcessor
`, className: 'tb-marker-div-icon', @@ -241,6 +481,8 @@ export class TbTripsDataLayer extends TbMapDataLayer(); + private positionFunction: CompiledTbFunction; private rawTripsData: FormattedData[][]; @@ -338,16 +580,66 @@ export class TbTripsDataLayer extends TbMapDataLayer { + const entityId = rawTripData[0].entityId; + let tripItem = this.tripItems.get(entityId); + if (tripItem) { + tripItem.update(rawTripData); + } else { + const latestData = this.latestTripsData.find(d => d.entityId === entityId); + tripItem = new TbTripDataItem(rawTripData, latestData, this.settings, this); + this.tripItems.set(entityId, tripItem); + } + toDelete.delete(entityId); + }); + toDelete.forEach((key) => { + this.removeItem(key); + }); + } + + public removeItem(key: string): void { + const item = this.tripItems.get(key); + if (item) { + item.remove(); + this.tripItems.delete(key); + } } public updateTripsLatestData(tripsLatestData: FormattedData[]) { this.latestTripsData = tripsLatestData.filter(d => d.$datasource.mapDataIds.includes(this.mapDataId)); + console.log(`Update trips latest data`); + this.tripItems.forEach((item, entityId) => { + const latestData = this.latestTripsData.find(d => d.entityId === entityId); + item.updateLatestData(latestData); + }); + } + + public updateCurrentTime() { + const currentTime = this.map.getCurrentTime(); + console.log(`Update trips current time: current(${currentTime})`); + this.tripItems.forEach(item => { + item.updateCurrentTime(); + }); + } + + public updateAppearance() { + console.log(`Update trips appearance`); + this.tripItems.forEach(item => { + item.updateAppearance(); + }); + } + + public calculateAnchors(): number[] { + return []; } private clearIncorrectFirsLastDatapoint(dataSource: FormattedData[]): FormattedData[] { @@ -413,4 +705,39 @@ export class TbTripsDataLayer extends TbMapDataLayer): {x: number; y: number} { + if (data) { + const xKeyVal = data[this.settings.xKey.label]; + const yKeyVal = data[this.settings.yKey.label]; + switch (this.mapType()) { + case MapType.geoMap: + if (!isValidLatLng(xKeyVal, yKeyVal)) { + return null; + } + break; + case MapType.image: + if (!isDefinedAndNotNull(xKeyVal) || isEmptyStr(xKeyVal) || isNaN(xKeyVal) || !isDefinedAndNotNull(yKeyVal) || isEmptyStr(yKeyVal) || isNaN(yKeyVal)) { + return null; + } + break; + } + return {x: xKeyVal, y: yKeyVal}; + } else { + return null; + } + } + + public extractLocation(data: FormattedData, dsData: FormattedData[]): L.LatLng { + let locationData = this.extractLocationData(data); + if (locationData) { + if (this.map.type() === MapType.image && this.positionFunction) { + const imageMap = this.map as TbImageMap; + locationData = this.positionFunction.execute(locationData.x, locationData.y, data, dsData, imageMap.getAspect()) || {x: 0, y: 0}; + } + return this.map.locationDataToLatLng(locationData); + } else { + return null; + } + } + } 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 7e884c473d..11af4338e9 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 @@ -33,7 +33,7 @@ import { formattedDataArrayFromDatasourceData, formattedDataFormDatasourceData, isDefined, - isDefinedAndNotNull, + isDefinedAndNotNull, isUndefined, mergeDeepIgnoreArray } from '@core/utils'; import { DeepPartial } from '@shared/models/common'; @@ -90,6 +90,12 @@ export abstract class TbMap { protected tripDataLayers: TbTripsDataLayer[]; protected dsData: FormattedData[]; + protected timeline = false; + protected minTime: number; + protected maxTime: number; + protected timeStep: number; + protected currentTime: number; + protected selectedDataItem: TbDataLayerItem; protected mapLayoutElement: HTMLElement; @@ -108,9 +114,9 @@ export abstract class TbMap { protected timeLineComponentRef: ComponentRef; protected timeLineComponent: MapTimelinePanelComponent; - protected addMarkerDataLayers: TbMapDataLayer[]; - protected addPolygonDataLayers: TbMapDataLayer[]; - protected addCircleDataLayers: TbMapDataLayer[]; + protected addMarkerDataLayers: TbMapDataLayer[]; + protected addPolygonDataLayers: TbMapDataLayer[]; + protected addCircleDataLayers: TbMapDataLayer[]; private readonly mapResize$: ResizeObserver; @@ -135,11 +141,14 @@ export abstract class TbMap { $(containerElement).append(mapLayoutElement); if (this.settings.tripTimeline?.showTimelineControl) { + this.timeline = true; + this.timeStep = this.settings.tripTimeline.timeStep; this.timeLineComponentRef = this.ctx.widgetContentContainer.createComponent(MapTimelinePanelComponent); this.timeLineComponent = this.timeLineComponentRef.instance; this.timeLineComponent.settings = this.settings.tripTimeline; this.timeLineComponent.timeChanged.subscribe((time) => { - console.log(`Time updated: ${time}`); + this.currentTime = time; + this.updateTripsTime(); }); const parentElement = this.timeLineComponentRef.instance.element.nativeElement; const content = parentElement.firstChild; @@ -740,7 +749,7 @@ export abstract class TbMap { undefined, undefined, el => el.datasource.entityId + el.datasource.mapDataIds[0]); this.dataLayers.forEach(dl => dl.updateData(this.dsData)); this.updateTripsAppearance(); - this.updateTripsTimeline(); + this.updateTripsAnchors(); this.updateBounds(); this.updateAddButtonsStates(); } @@ -757,8 +766,23 @@ export abstract class TbMap { minTime = Math.min(minMax.minTime, minTime); maxTime = Math.max(minMax.maxTime, maxTime); } - this.tripDataLayers.forEach(dl => dl.updateTrips(minTime, maxTime)); - this.updateTripsTimeline(minTime, maxTime); + const prevMinTime = this.minTime; + const prevMaxTime = this.maxTime; + this.minTime = minTime; + this.maxTime = maxTime; + if (this.timeline) { + this.timeLineComponent.min = this.minTime; + this.timeLineComponent.max = this.maxTime; + const currentTime = this.calculateCurrentTime(prevMinTime, prevMaxTime); + if (currentTime !== this.currentTime) { + this.currentTime = currentTime; + this.timeLineComponent.currentTime = currentTime; + } + } else { + this.currentTime = this.maxTime; + } + this.tripDataLayers.forEach(dl => dl.updateTrips()); + this.updateTripsAnchors(); this.updateBounds(); } @@ -766,22 +790,43 @@ export abstract class TbMap { const tripsLatestData = formattedDataFormDatasourceData(subscription.latestData, undefined, undefined, el => el.datasource.entityId + el.datasource.mapDataIds[0]); this.tripDataLayers.forEach(dl => dl.updateTripsLatestData(tripsLatestData)); - this.updateTripsAppearance(); - this.updateTripsTimeline(); + this.updateTripsAnchors(); } - private updateTripsAppearance() {} + private updateTripsAppearance() { + this.tripDataLayers.forEach(dl => dl.updateAppearance()); + } + private updateTripsTime() { + this.tripDataLayers.forEach(dl => dl.updateCurrentTime()); + } - private updateTripsTimeline(minTime?: number, maxTime?: number) { - if (this.settings.tripTimeline?.showTimelineControl) { - if (isDefinedAndNotNull(minTime) && isDefinedAndNotNull(maxTime)) { - this.timeLineComponent.min = minTime; - this.timeLineComponent.max = maxTime; - } + private updateTripsAnchors() { + if (this.timeline) { if (this.settings.tripTimeline.snapToRealLocation) { // Recalculate anchors only for enabled layers + let anchors: number[] = []; + const enableTrips = this.tripDataLayers.filter(dl => dl.isEnabled()); + for (const tripsDataLayer of enableTrips) { + const tripsAnchors = tripsDataLayer.calculateAnchors(); + anchors = [...new Set([...anchors, ...tripsAnchors])]; + } + anchors.sort((a, b) => a - b); + this.timeLineComponent.anchors = anchors; + } + } + } + + private calculateCurrentTime(minTime: number, maxTime: number): number { + if (minTime !== this.minTime || maxTime !== this.maxTime) { + if (this.minTime >= this.currentTime || isUndefined(this.currentTime)) { + return this.minTime; + } else if (this.maxTime <= this.currentTime) { + return this.maxTime; + } else { + return this.minTime + Math.ceil((this.currentTime - this.minTime) / this.settings.tripTimeline.timeStep) * this.settings.tripTimeline.timeStep; } } + return this.currentTime; } private resize() { @@ -793,6 +838,9 @@ export abstract class TbMap { private updateBounds() { const enabledDataLayers = this.dataLayers.filter(dl => dl.isEnabled()); const dataLayersBounds = enabledDataLayers.map(dl => dl.getBounds()).filter(b => b.isValid()); + const enabledTripsDataLayers = this.tripDataLayers.filter(dl => dl.isEnabled()); + const tripsDataLayersBounds = enabledTripsDataLayers.map(dl => dl.getBounds()).filter(b => b.isValid()); + dataLayersBounds.push(...tripsDataLayersBounds); let bounds: L.LatLngBounds; if (dataLayersBounds.length) { bounds = new L.LatLngBounds(null, null); @@ -902,7 +950,7 @@ export abstract class TbMap { public enabledDataLayersUpdated() { this.updateAddButtonsStates(); - this.updateTripsTimeline(); + this.updateTripsAnchors(); } public dataItemClick($event: Event, action: WidgetAction, entityInfo: TbMapDatasource) { @@ -993,6 +1041,28 @@ export abstract class TbMap { } } + // Timeline methods + + public hasTimeline(): boolean { + return this.timeline; + } + + public getMinTime(): number { + return this.minTime; + } + + public getMaxTime(): number { + return this.maxTime; + } + + public getTimeStep(): number { + return this.timeStep; + } + + public getCurrentTime(): number { + return this.currentTime; + } + public destroy() { if (this.mapResize$) { this.mapResize$.disconnect(); diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/maps/models/map.models.ts b/ui-ngx/src/app/modules/home/components/widget/lib/maps/models/map.models.ts index f93eab6dbc..79fb8a7c04 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/maps/models/map.models.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/maps/models/map.models.ts @@ -23,7 +23,7 @@ import { WidgetActionType } from '@shared/models/widget.models'; import { DataKeyType } from '@shared/models/telemetry/telemetry.models'; -import { guid, hashCode, isDefinedAndNotNull, isNotEmptyStr, isString, mergeDeep } from '@core/utils'; +import { guid, hashCode, isDefinedAndNotNull, isNotEmptyStr, isString, isUndefined, mergeDeep } from '@core/utils'; import { AbstractControl, ValidationErrors, ValidatorFn } from '@angular/forms'; import { materialColors } from '@shared/models/material.models'; import L from 'leaflet'; @@ -414,6 +414,7 @@ export const defaultBaseTripsDataLayerSettings = (mapType: MapType): Partial${entityName}

Latitude: ${latitude:7}
Longitude: ${longitude:7}
End Time: ${maxTime}
Start Time: ${minTime}' : '${entityName}

X Pos: ${xPos:2}
Y Pos: ${yPos:2}
End Time: ${maxTime}
Start Time: ${minTime}', @@ -1274,7 +1275,7 @@ export const processTooltipTemplate = (template: string): string => { return template; } -export function calculateNewPointCoordinate(coordinate: number, imageSize: number): number { +export const calculateNewPointCoordinate = (coordinate: number, imageSize: number): number => { let pointCoordinate = coordinate / imageSize; if (pointCoordinate < 0) { pointCoordinate = 0; @@ -1284,7 +1285,7 @@ export function calculateNewPointCoordinate(coordinate: number, imageSize: numbe return pointCoordinate; } -export function checkLngLat(point: L.LatLng, southWest: L.LatLng, northEast: L.LatLng, offset = 0): L.LatLng { +export const checkLngLat = (point: L.LatLng, southWest: L.LatLng, northEast: L.LatLng, offset = 0): L.LatLng => { const maxLngMap = northEast.lng - offset; const minLngMap = southWest.lng + offset; const maxLatMap = northEast.lat - offset; @@ -1301,3 +1302,48 @@ export function checkLngLat(point: L.LatLng, southWest: L.LatLng, northEast: L.L } return point; } + +export type TripRouteData = {[time: number]: FormattedData}; + +export const calculateInterpolationRatio = (firsMoment: number, secondMoment: number, intermediateMoment: number): number => { + return (intermediateMoment - firsMoment) / (secondMoment - firsMoment); +} + +export const interpolateLineSegment = ( + pointA: FormattedData, + pointB: FormattedData, + xKey: string, + yKey: string, + ratio: number +): { [key: string]: number } => { + return { + [xKey]: (pointA[xKey] + (pointB[xKey] - pointA[xKey]) * ratio), + [yKey]: (pointA[yKey] + (pointB[yKey] - pointA[yKey]) * ratio) + }; +} + +export const findRotationAngle = (startPoint: FormattedData, endPoint: FormattedData, xKey: string, yKey: string): number => { + if (isUndefined(startPoint) || isUndefined(endPoint)) { + return 0; + } + let angle = -Math.atan2(endPoint[xKey] - startPoint[xKey], endPoint[yKey] - startPoint[yKey]); + angle = angle * 180 / Math.PI; + return parseInt(angle.toFixed(2), 10); +} + +export const calculateLastPoints = (routeData: TripRouteData, time: number): FormattedData => { + const timeArr = Object.keys(routeData); + let index = timeArr.findIndex((dtime) => { + return Number(dtime) >= time; + }); + + if (index !== -1) { + if (Number(timeArr[index]) !== time && index !== 0) { + index--; + } + } else { + index = timeArr.length - 1; + } + + return routeData[timeArr[index]]; +} diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/maps/panels/map-timeline-panel.component.html b/ui-ngx/src/app/modules/home/components/widget/lib/maps/panels/map-timeline-panel.component.html index 57bf909613..7a725e48eb 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/maps/panels/map-timeline-panel.component.html +++ b/ui-ngx/src/app/modules/home/components/widget/lib/maps/panels/map-timeline-panel.component.html @@ -16,22 +16,62 @@ -->
- - - -
-
-
+ @if(hasData) { + + + +
+
+
+
+
+ + + @if(playing) { + + } @else { + + } + + +
+
+ + {{speed}}x + {{speedValue}}x + +
-
2
-
- - {{speedValue}}x - -
-
+ } @else { +
widgets.maps.timeline.no-trips-data-available
+ }
diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/maps/panels/map-timeline-panel.component.scss b/ui-ngx/src/app/modules/home/components/widget/lib/maps/panels/map-timeline-panel.component.scss index 7795e05523..3a022f65f3 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/maps/panels/map-timeline-panel.component.scss +++ b/ui-ngx/src/app/modules/home/components/widget/lib/maps/panels/map-timeline-panel.component.scss @@ -14,14 +14,37 @@ * limitations under the License. */ .tb-map-timeline-panel { - padding-top: 4px; - padding-left: 16px; - padding-right: 16px; + height: 100px; + padding: 4px 16px 8px; display: flex; flex-direction: column; .tb-timeline-controls { display: flex; flex-direction: row; align-items: center; + .tb-map-timeline-timestamp { + font-size: 14px; + font-weight: 400; + color: rgba(0,0,0,0.76); + } + .tb-timeline-buttons { + .mat-mdc-mini-fab.mat-primary { + --mdc-fab-small-container-color: rgba(0,0,0,0); + --mat-fab-small-foreground-color: rgba(0,0,0,0.54); + --mdc-fab-small-container-elevation-shadow: 4px 4px 4px 0px rgba(0, 0, 0, 0.15); + &:before { + opacity: 0.1; + } + } + .tb-timeline-button-32 { + width: 32px; + height: 32px; + } + } + } + .tb-map-timeline-no-data { + font-size: 18px; + font-weight: 400; + color: rgba(0,0,0,0.76); } } diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/maps/panels/map-timeline-panel.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/maps/panels/map-timeline-panel.component.ts index 1cb76aceea..7b2b428c5c 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/maps/panels/map-timeline-panel.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/maps/panels/map-timeline-panel.component.ts @@ -16,17 +16,20 @@ import { ChangeDetectorRef, - Component, + Component, DestroyRef, ElementRef, - EventEmitter, Injector, - Input, OnChanges, - OnDestroy, + EventEmitter, + Injector, + Input, OnInit, Output, ViewEncapsulation } from '@angular/core'; import { TripTimelineSettings } from '@home/components/widget/lib/maps/models/map.models'; import { DateFormatProcessor } from '@shared/models/widget-settings.models'; +import { interval, Observable, Subscription } from 'rxjs'; +import { filter } from 'rxjs/operators'; +import { takeUntilDestroyed } from '@angular/core/rxjs-interop'; @Component({ selector: 'tb-map-timeline-panel', @@ -34,7 +37,7 @@ import { DateFormatProcessor } from '@shared/models/widget-settings.models'; styleUrls: ['./map-timeline-panel.component.scss'], encapsulation: ViewEncapsulation.None }) -export class MapTimelinePanelComponent implements OnInit, OnChanges, OnDestroy { +export class MapTimelinePanelComponent implements OnInit { @Input() settings: TripTimelineSettings; @@ -43,22 +46,70 @@ export class MapTimelinePanelComponent implements OnInit, OnChanges, OnDestroy { disabled = false; @Input() - min = 0; + set min(value: number) { + if (this.minValue !== value) { + this.minValue = value; + this.maxTimeIndex = Math.ceil((this.maxValue - this.minValue) / this.settings.timeStep); + this.cd.markForCheck(); + } + } + + get min(): number { + return this.minValue; + } + + @Input() + set max(value: number) { + if (this.maxValue !== value) { + this.maxValue = value; + this.maxTimeIndex = Math.ceil((this.maxValue - this.minValue) / this.settings.timeStep); + this.cd.markForCheck(); + } + } + + get max(): number { + return this.maxValue; + } @Input() - max = 10000; + set currentTime(time: number) { + if (this.currentTimeValue !== time) { + this.currentTimeValue = time; + this.updateTimestampDisplayValue(); + this.cd.markForCheck(); + } + } + + get currentTime(): number { + return this.currentTimeValue; + } + + get hasData(): boolean { + return !!this.currentTimeValue && this.currentTimeValue !== Infinity; + } + + @Input() + anchors: number[] = []; @Output() timeChanged = new EventEmitter(); - currentTime = 0; - timestampFormat: DateFormatProcessor; + minTimeIndex = 0; + maxTimeIndex = 0; + index = 0; + playing = false; + interval: Subscription; speed: number; + private minValue: number; + private maxValue: number; + private currentTimeValue: number = null; + constructor(public element: ElementRef, private cd: ChangeDetectorRef, + private destroyRef: DestroyRef, private injector: Injector) { } @@ -70,25 +121,110 @@ export class MapTimelinePanelComponent implements OnInit, OnChanges, OnDestroy { this.speed = this.settings.speedOptions[0]; } - ngOnChanges() { - this.currentTime = this.min === Infinity ? 0 : this.min; - if (this.settings.showTimestamp) { - this.timestampFormat.update(this.currentTime); - this.cd.markForCheck(); + public onIndexChange(index: number) { + this.index = index; + this.updateCurrentTime(); + } + + public play() { + this.playing = true; + if (!this.interval) { + this.interval = interval(1000 / this.speed) + .pipe( + takeUntilDestroyed(this.destroyRef), + filter(() => this.playing) + ).subscribe( + { + next: () => { + if (this.index < this.maxTimeIndex) { + this.index++; + this.updateCurrentTime(); + } else { + this.playing = false; + this.cd.markForCheck(); + this.interval.unsubscribe(); + this.interval = null; + } + }, + error: (err) => { + console.error(err); + } + } + ); } } - ngOnDestroy() { + public pause() { + this.playing = false; + this.updateCurrentTime(); } - public onTimeChange() { - if (this.settings.showTimestamp) { + public fastRewind() { + this.index = this.minTimeIndex; + this.pause(); + } + + public fastForward() { + this.index = this.maxTimeIndex; + this.pause(); + } + + public moveNext() { + if (this.index < this.maxTimeIndex) { + if (this.settings.snapToRealLocation) { + const anchorIndex = this.findIndex(this.currentTime, this.anchors) + 1; + this.index = Math.floor((this.anchors[anchorIndex] - this.minValue) / this.settings.timeStep); + } else { + this.index++; + } + } + this.pause(); + } + + public movePrev() { + if (this.index > this.minTimeIndex) { + if (this.settings.snapToRealLocation) { + const anchorIndex = this.findIndex(this.currentTime, this.anchors) - 1; + this.index = Math.floor((this.anchors[anchorIndex] - this.minValue) / this.settings.timeStep); + } else { + this.index--; + } + } + this.pause(); + } + + public speedUpdated() { + if (this.interval) { + this.interval.unsubscribe(); + this.interval = null; + } + if (this.playing) { + this.play(); + } + } + + private updateCurrentTime() { + const newTime = this.minValue + this.index * this.settings.timeStep; + if (this.currentTime !== newTime) { + this.currentTime = newTime; + this.timeChanged.emit(this.currentTime); + this.updateTimestampDisplayValue(); + } + } + + private updateTimestampDisplayValue() { + if (this.settings.showTimestamp && this.hasData) { this.timestampFormat.update(this.currentTime); this.cd.markForCheck(); } - this.timeChanged.next(this.currentTime); } - public speedUpdated() {} + private findIndex(value: number, array: number[]): number { + let i = 0; + while (array[i] < value) { + i++; + } + return i; + } } 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 39156e001b..c0876889f3 100644 --- a/ui-ngx/src/assets/locale/locale.constant-en_US.json +++ b/ui-ngx/src/assets/locale/locale.constant-en_US.json @@ -7712,7 +7712,8 @@ "speed-options": "Speed options", "timestamp": "Timestamp", "snap-to-real-location": "Snap to real location", - "location-snap-filter-function": "Location snap filter function" + "location-snap-filter-function": "Location snap filter function", + "no-trips-data-available": "No trips data available" }, "map-action": { "map-action-buttons": "Map action buttons", From d0a51cdaeb37c9c27769962c455f19ad7db54e95 Mon Sep 17 00:00:00 2001 From: nick Date: Fri, 28 Feb 2025 19:50:37 +0200 Subject: [PATCH 141/345] tbel: tbDate docs New Function test --- .../service/script/TbelInvokeDocsIoTest.java | 88 +++++++++++++++++++ 1 file changed, 88 insertions(+) diff --git a/application/src/test/java/org/thingsboard/server/service/script/TbelInvokeDocsIoTest.java b/application/src/test/java/org/thingsboard/server/service/script/TbelInvokeDocsIoTest.java index 896b7b2261..2d043b72ae 100644 --- a/application/src/test/java/org/thingsboard/server/service/script/TbelInvokeDocsIoTest.java +++ b/application/src/test/java/org/thingsboard/server/service/script/TbelInvokeDocsIoTest.java @@ -2211,6 +2211,94 @@ class TbelInvokeDocsIoTest extends AbstractTbelInvokeTest { }).hasMessageContaining("Error: unmodifiable.put(\"temperature1\", 96): Map is unmodifiable"); } + @Test + public void tbDateFunction_Test() throws ExecutionException, InterruptedException { + msgStr = "{}"; + decoderStr = String.format(""" + var d = new Date(2024, 1, 1, 10, 0, 0, 0); // TZ => "UTC" + var dIsoY1 = d.toISOString(); // return 2024-01-01T08:00:00Z + d.addYears(1); + var dIsoY2 = d.toISOString(); // return 2025-01-01T08:00:00Z + d.addYears(-2); + var dIsoY3 = d.toISOString(); // return 2023-01-01T08:00:00Z + d.addMonths(2); + var dIsoM1 = d.toISOString(); // return 2023-03-01T08:00:00Z + d.addMonths(10); + var dIsoM2 = d.toISOString(); // return 2024-01-01T08:00:00Z + d.addMonths(-13); + var dIsoM3 = d.toISOString(); // return 2022-12-01T08:00:00Z + d.addWeeks(4); + var dIsoW1 = d.toISOString(); // return 2022-12-29T08:00:00Z + d.addWeeks(-5); + var dIsoW2 = d.toISOString(); // return 2022-11-24T08:00:00Z + d.addDays(6); + var dIsoD1 = d.toISOString(); // return 2022-11-30T08:00:00Z + d.addDays(45); + var dIsoD2 = d.toISOString(); // return 2023-01-14T08:00:00Z + d.addDays(-50); + var dIsoD3 = d.toISOString(); // return 2022-11-25T08:00:00Z + d.addHours(23); + var dIsoH1 = d.toISOString(); // return 2022-11-26T07:00:00Z + d.addHours(-47); + var dIsoH2 = d.toISOString(); // return 2022-11-24T08:00:00Z + d.addMinutes(59); + var dIsoMin1 = d.toISOString(); // return 2022-11-24T08:59:00Z + d.addMinutes(-60); + var dIsoMin2 = d.toISOString(); // return 2022-11-24T07:59:00Z + d.addSeconds(59); + var dIsoS1 = d.toISOString(); // return 2022-11-24T07:59:59Z + d.addSeconds(-60); + var dIsoS2 = d.toISOString(); // return 2022-11-24T07:58:59Z + d.addNanos(999999); + var dIsoN1 = d.toISOString(); // return 2022-11-24T07:58:59.000999999Z + d.addNanos(-1000000); + var dIsoN2 = d.toISOString(); // return 2022-11-24T07:58:58.999999999Z + return { + "dIsoY1": dIsoY1, + "dIsoY2": dIsoY2, + "dIsoY3": dIsoY3, + "dIsoM1": dIsoM1, + "dIsoM2": dIsoM2, + "dIsoM3": dIsoM3, + "dIsoW1": dIsoW1, + "dIsoW2": dIsoW2, + "dIsoD1": dIsoD1, + "dIsoD2": dIsoD2, + "dIsoD3": dIsoD3, + "dIsoH1": dIsoH1, + "dIsoH2": dIsoH2, + "dIsoMin1": dIsoMin1, + "dIsoMin2": dIsoMin2, + "dIsoS1": dIsoS1, + "dIsoS2": dIsoS2, + "dIsoN1": dIsoN1, + "dIsoN2": dIsoN2 + } + """); + LinkedHashMap expected = new LinkedHashMap<>(); + expected.put("dIsoY1", "2024-01-01T08:00:00Z"); + expected.put("dIsoY2", "2025-01-01T08:00:00Z"); + expected.put("dIsoY3", "2023-01-01T08:00:00Z"); + expected.put("dIsoM1", "2023-03-01T08:00:00Z"); + expected.put("dIsoM2", "2024-01-01T08:00:00Z"); + expected.put("dIsoM3", "2022-12-01T08:00:00Z"); + expected.put("dIsoW1", "2022-12-29T08:00:00Z"); + expected.put("dIsoW2", "2022-11-24T08:00:00Z"); + expected.put("dIsoD1", "2022-11-30T08:00:00Z"); + expected.put("dIsoD2", "2023-01-14T08:00:00Z"); + expected.put("dIsoD3", "2022-11-25T08:00:00Z"); + expected.put("dIsoH1", "2022-11-26T07:00:00Z"); + expected.put("dIsoH2", "2022-11-24T08:00:00Z"); + expected.put("dIsoMin1", "2022-11-24T08:59:00Z"); + expected.put("dIsoMin2", "2022-11-24T07:59:00Z"); + expected.put("dIsoS1", "2022-11-24T07:59:59Z"); + expected.put("dIsoS2", "2022-11-24T07:58:59Z"); + expected.put("dIsoN1", "2022-11-24T07:58:59.000999999Z"); + expected.put("dIsoN2", "2022-11-24T07:58:58.999999999Z"); + Object actual = invokeScript(evalScript(decoderStr), msgStr); + assertEquals(expected, actual); + } + private List splice(List oldList, int start, int deleteCount, Object... values) { start = initStartIndex(oldList, start); deleteCount = deleteCount < 0 ? 0 : Math.min(deleteCount, (oldList.size() - start)); From dcc88a619e5837964f46d98d6bc67a82f075e9bd Mon Sep 17 00:00:00 2001 From: Oleksandra Matviienko Date: Sun, 2 Mar 2025 19:24:14 +0100 Subject: [PATCH 142/345] Tests for scheduleStatsPersistTick and destroy methods in ComponentActor were written. Signed-off-by: Oleksandra Matviienko --- .../server/actors/service/ComponentActor.java | 2 +- .../actors/service/ComponentActorTest.java | 74 +++++++++++++++++++ 2 files changed, 75 insertions(+), 1 deletion(-) create mode 100644 application/src/test/java/org/thingsboard/server/actors/service/ComponentActorTest.java diff --git a/application/src/main/java/org/thingsboard/server/actors/service/ComponentActor.java b/application/src/main/java/org/thingsboard/server/actors/service/ComponentActor.java index bf50273814..8203528e41 100644 --- a/application/src/main/java/org/thingsboard/server/actors/service/ComponentActor.java +++ b/application/src/main/java/org/thingsboard/server/actors/service/ComponentActor.java @@ -77,7 +77,7 @@ public abstract class ComponentActor statsScheduledFuture = Mockito.mock(ScheduledFuture.class); + ActorSystemContext systemContext = Mockito.mock(ActorSystemContext.class); + ReflectionTestUtils.setField(componentActor, "systemContext", systemContext); + ComponentMsgProcessor processor = Mockito.mock(ComponentMsgProcessor.class); + componentActor.processor = processor; + BDDMockito.willReturn(statsScheduledFuture).given(processor).scheduleStatsPersistTick(any(), anyLong()); + BDDMockito.willCallRealMethod().given(componentActor).scheduleStatsPersistTick(); + + componentActor.scheduleStatsPersistTick(); + + Assertions.assertNotNull(componentActor.statsScheduledFuture); + } + + @Test + void destroyTest() { + ScheduledFuture statsScheduledFuture = Mockito.mock(ScheduledFuture.class); + componentActor.statsScheduledFuture = statsScheduledFuture; + Assertions.assertNotNull(componentActor.statsScheduledFuture); + Throwable cause = new Throwable(); + EntityId id = Mockito.mock(EntityId.class); + ReflectionTestUtils.setField(componentActor, "id", id); + BDDMockito.willCallRealMethod().given(componentActor).destroy(any(), any()); + + componentActor.destroy(TbActorStopReason.STOPPED, cause); + + Mockito.verify(statsScheduledFuture).cancel(false); + Assertions.assertNull(componentActor.statsScheduledFuture); + } + +} From 8ebf5125af9d17ea5bf7373a5b6a1fa600d9e1a6 Mon Sep 17 00:00:00 2001 From: Vladyslav Prykhodko Date: Sun, 2 Mar 2025 22:27:42 +0200 Subject: [PATCH 143/345] UI: Fix process header widget action - place map item --- .../widget/lib/maps/map-widget.component.ts | 2 +- .../home/components/widget/lib/maps/map.ts | 74 ++++++++-------- .../widget/lib/maps/models/map.models.ts | 5 +- .../components/widget/widget.component.ts | 87 +++++++++++-------- 4 files changed, 93 insertions(+), 75 deletions(-) diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/maps/map-widget.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/maps/map-widget.component.ts index a462a01356..a020bb1527 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/maps/map-widget.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/maps/map-widget.component.ts @@ -64,7 +64,7 @@ export class MapWidgetComponent implements OnInit, OnDestroy { overlayStyle: ComponentStyle = {}; padding: string; - private map: TbMap; + map: TbMap; constructor(public widgetComponent: WidgetComponent, private imagePipe: ImagePipe, 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 11af4338e9..121277e7da 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 @@ -33,7 +33,8 @@ import { formattedDataArrayFromDatasourceData, formattedDataFormDatasourceData, isDefined, - isDefinedAndNotNull, isUndefined, + isDefinedAndNotNull, + isUndefined, mergeDeepIgnoreArray } from '@core/utils'; import { DeepPartial } from '@shared/models/common'; @@ -48,7 +49,7 @@ import { UnplacedMapDataItem, } from '@home/components/widget/lib/maps/data-layer/map-data-layer'; import { IWidgetSubscription, WidgetSubscriptionOptions } from '@core/api/widget-api.models'; -import { FormattedData, MapItemType, WidgetAction, WidgetActionType, widgetType } from '@shared/models/widget.models'; +import { FormattedData, MapItemType, WidgetAction, 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'; @@ -545,66 +546,67 @@ export abstract class TbMap { title: customTranslate.transform(actionButton.label) }; const toolbarButton = this.customActionsToolbar.toolbarButton(actionButtonConfig); - if (actionButton.action.type !== WidgetActionType.placeMapItem) { - toolbarButton.onClick((e) => this.ctx.actionsApi.handleWidgetAction(e, actionButton.action)); - } else { - switch (actionButton.action.mapItemType) { - case MapItemType.marker: - toolbarButton.onClick((e, button) => this.createMarker(e, {button, action: actionButton.action})); - break; - case MapItemType.polygon: - toolbarButton.onClick((e, button) => this.createPolygon(e, {button, action: actionButton.action})); - break; - case MapItemType.rectangle: - toolbarButton.onClick((e, button) => this.createRectangle(e, {button, action: actionButton.action})); - break; - case MapItemType.circle: - toolbarButton.onClick((e, button) => this.createCircle(e, {button, action: actionButton.action})); - break; - } - } + toolbarButton.onClick((e, button) => this.ctx.actionsApi.handleWidgetAction(e, actionButton.action, null, null, {button})); }); } } - private createMarker(e: MouseEvent, actionData: CustomActionData) { - this.createItem(e, actionData, () => this.prepareDrawMode('Marker', { + public placeMapItem(actionData: CustomActionData): void { + switch (actionData.action.mapItemType) { + case MapItemType.marker: + this.createMarker(actionData); + break; + case MapItemType.polygon: + this.createPolygon(actionData); + break; + case MapItemType.rectangle: + this.createRectangle(actionData); + break; + case MapItemType.circle: + this.createCircle(actionData); + break; + } + } + + private createMarker(actionData: CustomActionData) { + this.createItem(actionData, () => this.prepareDrawMode('Marker', { placeMarker: this.ctx.translate.instant('widgets.maps.data-layer.marker.place-marker-hint') })); } - private createRectangle(e: MouseEvent, actionData: CustomActionData): void { - this.createItem(e, actionData, () => this.prepareDrawMode('Rectangle', { + private createRectangle(actionData: CustomActionData): 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') })); } - private createPolygon(e: MouseEvent, actionData: CustomActionData): void { - this.createItem(e, actionData, () => this.prepareDrawMode('Polygon', { + private createPolygon(actionData: CustomActionData): 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') })); } - private createCircle(e: MouseEvent, actionData: CustomActionData): void { - this.createItem(e, actionData, () => this.prepareDrawMode('Circle', { + private createCircle(actionData: CustomActionData): 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') })); } - private createItem(e: MouseEvent, actionData: CustomActionData, prepareDrawMode: () => void) { + private createItem(actionData: CustomActionData, prepareDrawMode: () => void) { if (this.isPlacingItem) { return; } - this.updatePlaceItemState(actionData.button); + this.updatePlaceItemState(actionData.button, true); this.map.once('pm:create', (e) => { - this.ctx.actionsApi.handleWidgetAction(e as any, actionData.action, null, null, { + actionData.afterPlaceItemCallback(e as any, actionData.action, null, null, { coordinates: convertLayerToCoordinates(actionData.action.mapItemType, e.layer), - layer: e.layer + layer: e.layer, + button: actionData.button }); // @ts-ignore @@ -673,7 +675,7 @@ export abstract class TbMap { L.DomUtil.addClass(this.map.pm.Draw[shape]._hintMarker.getTooltip()._container, 'tb-place-item-label'); } - private updatePlaceItemState(addButton?: L.TB.ToolbarButton): void { + private updatePlaceItemState(addButton?: L.TB.ToolbarButton, disabled = false): void { if (addButton) { this.deselectItem(false, true); addButton.setActive(true); @@ -681,7 +683,7 @@ export abstract class TbMap { this.currentAddButton.setActive(false); } this.currentAddButton = addButton; - this.updateAddButtonsStates(); + this.updateAddButtonsStates(disabled); } private createdControlButtonTooltip(root: HTMLElement, side: TooltipPositioningSide) { @@ -856,8 +858,8 @@ export abstract class TbMap { } } - private updateAddButtonsStates() { - if (this.currentAddButton) { + private updateAddButtonsStates(disabled = false) { + if (this.currentAddButton || disabled) { if (this.addMarkerButton && this.addMarkerButton !== this.currentAddButton) { this.addMarkerButton.setDisabled(true); } diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/maps/models/map.models.ts b/ui-ngx/src/app/modules/home/components/widget/lib/maps/models/map.models.ts index 79fb8a7c04..9863253ff8 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/maps/models/map.models.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/maps/models/map.models.ts @@ -34,6 +34,7 @@ import { ImagePipe } from '@shared/pipe/image.pipe'; import { MarkerShape } from '@home/components/widget/lib/maps/models/marker-shape.models'; import { UnplacedMapDataItem } from '@home/components/widget/lib/maps/data-layer/map-data-layer'; import { DateFormatSettings, simpleDateFormat } from '@shared/models/widget-settings.models'; +import { EntityId } from '@shared/models/id/entity-id'; export enum MapType { geoMap = 'geoMap', @@ -1033,8 +1034,10 @@ export interface MarkerIconInfo { } export interface CustomActionData { - button: L.TB.TopToolbarButton; + button?: L.TB.TopToolbarButton; action: WidgetAction; + afterPlaceItemCallback: ($event: Event, descriptor: WidgetAction, entityId?: EntityId, entityName?: string, + additionalParams?: any, entityLabel?: string) => void } export type MapStringFunction = (data: FormattedData, diff --git a/ui-ngx/src/app/modules/home/components/widget/widget.component.ts b/ui-ngx/src/app/modules/home/components/widget/widget.component.ts index 8a9b6e194c..2ac0a6a0e7 100644 --- a/ui-ngx/src/app/modules/home/components/widget/widget.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/widget.component.ts @@ -125,6 +125,7 @@ import { IModulesMap } from '@modules/common/modules-map.models'; import { DashboardUtilsService } from '@core/services/dashboard-utils.service'; import { CompiledTbFunction, compileTbFunction, isNotEmptyTbFunction } from '@shared/models/js-function.models'; import { HttpClient } from '@angular/common/http'; +import type { MapWidgetComponent } from '@home/components/widget/lib/maps/map-widget.component'; @Component({ selector: 'tb-widget', @@ -1150,44 +1151,17 @@ export class WidgetComponent extends PageComponent implements OnInit, OnChanges, } break; case WidgetActionType.placeMapItem: - case WidgetActionType.customPretty: - const customPrettyFunction = descriptor.customFunction; - const customHtml = descriptor.customHtml; - const customCss = descriptor.customCss; - const customResources = descriptor.customResources; - const actionNamespace = `custom-action-pretty-${guid()}`; - let htmlTemplate = ''; - if (isDefined(customHtml) && customHtml.length > 0) { - htmlTemplate = customHtml; + const mapWidget: MapWidgetComponent = this.widgetContext.$scope.mapWidget + if (mapWidget) { + mapWidget.map.placeMapItem({ + action: descriptor, + afterPlaceItemCallback: this.executeCustomPrettyAction.bind(this), + button: additionalParams?.button + }); } - this.loadCustomActionResources(actionNamespace, customCss, customResources, descriptor).subscribe({ - next: () => { - if (isNotEmptyTbFunction(customPrettyFunction)) { - compileTbFunction(this.http, customPrettyFunction, '$event', 'widgetContext', 'entityId', - 'entityName', 'htmlTemplate', 'additionalParams', 'entityLabel').subscribe( - { - next: (compiled) => { - try { - if (!additionalParams) { - additionalParams = {}; - } - this.widgetContext.customDialog.setAdditionalImports(descriptor.customImports); - compiled.execute($event, this.widgetContext, entityId, entityName, htmlTemplate, additionalParams, entityLabel); - } catch (e) { - console.error(e); - } - }, - error: (err) => { - console.error(err); - } - } - ) - } - }, - error: (errorMessages: string[]) => { - this.processResourcesLoadErrors(errorMessages); - } - }); + break; + case WidgetActionType.customPretty: + this.executeCustomPrettyAction($event, descriptor, entityId, entityName, additionalParams, entityLabel); break; case WidgetActionType.mobileAction: const mobileAction = descriptor.mobileAction; @@ -1559,6 +1533,45 @@ export class WidgetComponent extends PageComponent implements OnInit, OnChanges, this.handleWidgetAction($event, action, entityId, entityName, null, entityLabel); } + private executeCustomPrettyAction($event: Event, descriptor: WidgetAction, entityId?: EntityId, + entityName?: string, additionalParams?: any, entityLabel?: string) { + const customPrettyFunction = descriptor.customFunction; + const customHtml = descriptor.customHtml; + const customCss = descriptor.customCss; + const customResources = descriptor.customResources; + const actionNamespace = `custom-action-pretty-${guid()}`; + let htmlTemplate = ''; + if (isDefined(customHtml) && customHtml.length > 0) { + htmlTemplate = customHtml; + } + this.loadCustomActionResources(actionNamespace, customCss, customResources, descriptor).subscribe({ + next: () => { + if (isNotEmptyTbFunction(customPrettyFunction)) { + compileTbFunction(this.http, customPrettyFunction, '$event', 'widgetContext', 'entityId', + 'entityName', 'htmlTemplate', 'additionalParams', 'entityLabel').subscribe({ + next: (compiled) => { + try { + if (!additionalParams) { + additionalParams = {}; + } + this.widgetContext.customDialog.setAdditionalImports(descriptor.customImports); + compiled.execute($event, this.widgetContext, entityId, entityName, htmlTemplate, additionalParams, entityLabel); + } catch (e) { + console.error(e); + } + }, + error: (err) => { + console.error(err); + } + }); + } + }, + error: (errorMessages: string[]) => { + this.processResourcesLoadErrors(errorMessages); + } + }); + } + private loadCustomActionResources(actionNamespace: string, customCss: string, customResources: Array, actionDescriptor: WidgetAction): Observable { const resourceTasks: Observable[] = []; From bd43b7ae6183907667b6f3e42a97f0d93bbca837 Mon Sep 17 00:00:00 2001 From: IrynaMatveieva Date: Mon, 3 Mar 2025 11:04:10 +0200 Subject: [PATCH 144/345] added expression function description --- .../en_US/calculated-field/expression_fn.md | 188 +++++++++++++++++- 1 file changed, 187 insertions(+), 1 deletion(-) diff --git a/ui-ngx/src/assets/help/en_US/calculated-field/expression_fn.md b/ui-ngx/src/assets/help/en_US/calculated-field/expression_fn.md index f8173dc528..eb06cda0cb 100644 --- a/ui-ngx/src/assets/help/en_US/calculated-field/expression_fn.md +++ b/ui-ngx/src/assets/help/en_US/calculated-field/expression_fn.md @@ -1 +1,187 @@ - +#### Calculated field TBEL script function + +The **calculate()** function is user-defined and receives arguments configured by the user to generate new telemetry data. +It allows you to perform custom calculations using [TBEL{:target="_blank"}](${siteBaseUrl}/docs${docPlatformPrefix}/user-guide/tbel/). + +##### Function signature + +```javascript + function calculate(arg1, arg2, ...): object | object[] +``` + +The function automatically receives user-defined arguments **(arg1, arg2, ...)** and should return a JSON object. + +##### Example: Air Density Calculation + +```javascript +function calculate(altitude, temperature) { + var avgTemperature = temperature.mean(); // Get average temperature + var temperatureK = (avgTemperature - 32) * (5 / 9) + 273.15; // Convert Fahrenheit to Kelvin + + // Estimate air pressure based on altitude + var pressure = 101325 * Math.pow((1 - 2.25577e-5 * altitude.value), 5.25588); + + // Air density formula + var airDensity = pressure / (287.05 * temperatureK); + + return { + "airDensity": airDensity + }; +} +``` + +##### Function arguments + +The function receives user-configured arguments, which can be of two types: + +* single value arguments - represent the latest telemetry data or attribute. + ```json + { + "altitude": { + "ts": 1740644636669, + "value": 1034 + } + } + ``` + + Use dot notation (`.`) to access argument properties. + + ```javascript + var altitudeTimestamp = altitude.ts; + var altitudeValue = altitude.value; + ``` + +* time series rolling arguments - contain historical data within a defined time window. + ```json + { + "temperature": { + "timeWindow": { + "startTs": 1740643762896, + "endTs": 1740644662896, + "limit": 5000 + }, + "values": [ + { "ts": 1740644355935, "value": 72.32 }, + { "ts": 1740644365935, "value": 72.86 }, + { "ts": 1740644375935, "value": 73.58 }, + { "ts": 1740644385935, "value": "NaN" } + ] + } + } + ``` + + Use dot notation (`.`) to access argument properties. + + ```javascript + var startOfInterval = temperature.timeWindow.startTs; + var firstTimestamp = temperature.values[0].ts; + var firstValue = temperature.values[0].value; + ``` + +**Built-in methods for rolling arguments** + +Time series rolling arguments provide built-in functions for calculations. +These functions accept an optional 'ignoreNaN' boolean parameter, which controls how NaN values are handled. +Each method has two function signatures: + +* **Without parameters:** `method()` → called **without parameters** and defaults to `ignoreNaN = true`, meaning NaN values are ignored. +* **With an explicit parameter:** `method(boolean ignoreNaN)` → called with a boolean `ignoreNaN` parameter: + * `true` → ignores NaN values (default behavior). + * `false` → includes NaN values in calculations. + +| Method | Default Behavior (`ignoreNaN = true`) | Alternative (`ignoreNaN = false`) | +|-----------|--------------------------------------------------|---------------------------------------------| +| `max()` | Returns the highest value, ignoring NaN values. | Returns NaN if any NaN values exist. | +| `min()` | Returns the lowest value, ignoring NaN values. | Returns NaN if any NaN values exist. | +| `mean()` | Computes the average value, ignoring NaN values. | Returns NaN if any NaN values exist. | +| `std()` | Calculates the standard deviation, ignoring NaN. | Returns NaN if any NaN values exist. | +| `median()` | Returns the median value, ignoring NaN values. | Returns NaN if any NaN values exist. | +| `count()` | Counts only valid (non-NaN) values. | Counts all values, including NaN. | +| `last()` | Returns the most recent non-NaN value. | Returns the last value, even if it is NaN. | +| `first()` | Returns the oldest non-NaN value. | Returns the first value, even if it is NaN. | +| `sum()` | Computes the total sum, ignoring NaN values. | Returns NaN if any NaN values exist. | + +##### The following calculations are executed over the provided above arguments: + +**Example usage: default (`ignoreNaN = true`)** + +```javascript +var avgTemp = temperature.mean(); +var tempMax = temperature.max(); +var valueCount = temperature.count(); +``` + +**Output:** + +```json +{ + "avgTemp": 72.92, + "tempMax": 73.58, + "valueCount": 3 +} +``` + +**Example usage: explicit (`ignoreNaN = false`)** + +```javascript +var avgTemp = temperature.mean(false); // Returns NaN if any NaN values exist +var tempMax = temperature.max(false); // Returns NaN if any NaN values exist +var valueCount = temperature.count(false); // Counts all values, including NaN +``` + +**Output:** + +```json +{ + "avgTemp": "NaN", + "tempMax": "NaN", + "valueCount": 4 +} +``` + +##### Function return format: + +The script should return a JSON object formatted according to the [ThingsBoard Telemetry Upload API](${siteBaseUrl}/docs${docPlatformPrefix}/user-guide/telemetry/#time-series-data-upload-api/). +The return value must match one of the supported telemetry upload formats. +**Example Formats**: + +Single key-value format: + +```json +{ + "airDensity": 1.06 +} +``` + +Key-value format with a timestamp: + +```json +{ + "ts": 1740644636669, + "values": { + "airDensity": 1.06 + } +} +``` + +Array of telemetry entries: + +```json +[ + { + "ts": 1740644636669, + "values": { + "airDensity": 1.06 + } + }, + { + "ts": 1740644662896, + "values": [ + { "ts": 1740644355935, "value": 72.32 }, + { "ts": 1740644365935, "value": 72.86 }, + { "ts": 1740644375935, "value": 73.58 }, + { "ts": 1740644385935, "value": "NaN" } + ] + } +] +``` From b29cc4fc413a481ea6ce49b867a90d6aadfe8a51 Mon Sep 17 00:00:00 2001 From: Andrii Landiak Date: Mon, 3 Mar 2025 11:20:04 +0200 Subject: [PATCH 145/345] Fix tupo --- .../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 3e3051ee21..5992083d85 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}") + @Value("${queue.calculated_fields.notifications-topic:calculated_field.notifications}") private String tbCalculatedFieldNotificationsTopic; private final ConcurrentMap tbCoreNotificationTopics = new ConcurrentHashMap<>(); From bfef1a3ea3c7068c426ad3c69002c296532849d5 Mon Sep 17 00:00:00 2001 From: Andrii Landiak Date: Mon, 3 Mar 2025 11:21:50 +0200 Subject: [PATCH 146/345] Fix notifications_topic for cf --- application/src/main/resources/thingsboard.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/application/src/main/resources/thingsboard.yml b/application/src/main/resources/thingsboard.yml index 730dfec128..ec9925bf06 100644 --- a/application/src/main/resources/thingsboard.yml +++ b/application/src/main/resources/thingsboard.yml @@ -1759,7 +1759,7 @@ queue: # Topic name for Calculated Field (CF) compacted states state_topic: "${TB_QUEUE_CF_STATE_TOPIC:tb_cf_state}" # For high-priority notifications that require minimum latency and processing time - notifications_topic: "${TB_QUEUE_CF_NOTIFICATIONS_TOPIC:calculated_field}" + notifications_topic: "${TB_QUEUE_CF_NOTIFICATIONS_TOPIC:calculated_field.notifications}" # Interval in milliseconds to poll messages by CF (Rule Engine) microservices poll_interval: "${TB_QUEUE_CF_POLL_INTERVAL_MS:25}" # Amount of partitions used by CF microservices From ad42442553cdf71f22dd8e33055077e481e01ae5 Mon Sep 17 00:00:00 2001 From: mpetrov Date: Mon, 3 Mar 2025 11:36:47 +0200 Subject: [PATCH 147/345] Added UI for calculated fields version control --- .../vc/auto-commit-settings.component.html | 3 +++ .../vc/auto-commit-settings.component.ts | 14 +++++++++++--- .../vc/entity-types-version-create.component.html | 3 +++ .../vc/entity-types-version-create.component.ts | 7 ++++++- .../vc/entity-types-version-load.component.html | 3 +++ .../vc/entity-types-version-load.component.ts | 7 ++++++- .../vc/entity-version-create.component.html | 3 +++ .../vc/entity-version-create.component.ts | 6 +++++- .../vc/entity-version-restore.component.html | 3 +++ .../vc/entity-version-restore.component.ts | 6 ++++-- ui-ngx/src/app/shared/models/vc.models.ts | 7 +++++++ .../src/assets/locale/locale.constant-en_US.json | 2 ++ 12 files changed, 56 insertions(+), 8 deletions(-) diff --git a/ui-ngx/src/app/modules/home/components/vc/auto-commit-settings.component.html b/ui-ngx/src/app/modules/home/components/vc/auto-commit-settings.component.html index 053d0f5006..d80892664d 100644 --- a/ui-ngx/src/app/modules/home/components/vc/auto-commit-settings.component.html +++ b/ui-ngx/src/app/modules/home/components/vc/auto-commit-settings.component.html @@ -84,6 +84,9 @@ {{ 'version-control.export-relations' | translate }} + + {{ 'version-control.export-calculated-fields' | translate }} +
diff --git a/ui-ngx/src/app/modules/home/components/vc/auto-commit-settings.component.ts b/ui-ngx/src/app/modules/home/components/vc/auto-commit-settings.component.ts index e0a88a3e09..d55515a959 100644 --- a/ui-ngx/src/app/modules/home/components/vc/auto-commit-settings.component.ts +++ b/ui-ngx/src/app/modules/home/components/vc/auto-commit-settings.component.ts @@ -25,7 +25,11 @@ import { TranslateService } from '@ngx-translate/core'; import { DialogService } from '@core/services/dialog.service'; import { catchError, map, mergeMap } from 'rxjs/operators'; import { Observable, of } from 'rxjs'; -import { EntityTypeVersionCreateConfig, exportableEntityTypes } from '@shared/models/vc.models'; +import { + EntityTypeVersionCreateConfig, + exportableEntityTypes, + typesWithCalculatedFields +} from '@shared/models/vc.models'; import { EntityType, entityTypeTranslations } from '@shared/models/entity-type.models'; import { DomSanitizer, SafeHtml } from '@angular/platform-browser'; @@ -43,6 +47,8 @@ export class AutoCommitSettingsComponent extends PageComponent implements OnInit isReadOnly: Observable; + readonly typesWithCalculatedFields = typesWithCalculatedFields; + constructor(protected store: Store, private adminService: AdminService, private dialogService: DialogService, @@ -104,7 +110,8 @@ export class AutoCommitSettingsComponent extends PageComponent implements OnInit branch: null, saveAttributes: true, saveRelations: false, - saveCredentials: true + saveCredentials: true, + saveCalculatedFields: true, }; const allowed = this.allowedEntityTypes(); let entityType: EntityType = null; @@ -206,7 +213,8 @@ export class AutoCommitSettingsComponent extends PageComponent implements OnInit branch: [config.branch, []], saveRelations: [config.saveRelations, []], saveAttributes: [config.saveAttributes, []], - saveCredentials: [config.saveCredentials, []] + saveCredentials: [config.saveCredentials, []], + saveCalculatedFields: [config.saveCalculatedFields, []] }) } ); diff --git a/ui-ngx/src/app/modules/home/components/vc/entity-types-version-create.component.html b/ui-ngx/src/app/modules/home/components/vc/entity-types-version-create.component.html index b9a9a6c01c..8fdf22f6cd 100644 --- a/ui-ngx/src/app/modules/home/components/vc/entity-types-version-create.component.html +++ b/ui-ngx/src/app/modules/home/components/vc/entity-types-version-create.component.html @@ -72,6 +72,9 @@ {{ 'version-control.export-relations' | translate }} + + {{ 'version-control.export-calculated-fields' | translate }} +
diff --git a/ui-ngx/src/app/modules/home/components/vc/entity-types-version-create.component.ts b/ui-ngx/src/app/modules/home/components/vc/entity-types-version-create.component.ts index 8b16905587..8618e405b0 100644 --- a/ui-ngx/src/app/modules/home/components/vc/entity-types-version-create.component.ts +++ b/ui-ngx/src/app/modules/home/components/vc/entity-types-version-create.component.ts @@ -33,7 +33,8 @@ import { EntityTypeVersionCreateConfig, exportableEntityTypes, SyncStrategy, - syncStrategyTranslationMap + syncStrategyTranslationMap, + typesWithCalculatedFields } from '@shared/models/vc.models'; import { Store } from '@ngrx/store'; import { AppState } from '@core/core.state'; @@ -79,6 +80,8 @@ export class EntityTypesVersionCreateComponent extends PageComponent implements loading = true; + readonly typesWithCalculatedFields = typesWithCalculatedFields; + constructor(protected store: Store, private translate: TranslateService, private fb: UntypedFormBuilder, @@ -150,6 +153,7 @@ export class EntityTypesVersionCreateComponent extends PageComponent implements saveRelations: [config.saveRelations, []], saveAttributes: [config.saveAttributes, []], saveCredentials: [config.saveCredentials, []], + saveCalculatedFields: [config.saveCalculatedFields, []], allEntities: [config.allEntities, []], entityIds: [config.entityIds, [Validators.required]] }) @@ -202,6 +206,7 @@ export class EntityTypesVersionCreateComponent extends PageComponent implements saveAttributes: true, saveRelations: true, saveCredentials: true, + saveCalculatedFields: true, allEntities: true, entityIds: [] }; diff --git a/ui-ngx/src/app/modules/home/components/vc/entity-types-version-load.component.html b/ui-ngx/src/app/modules/home/components/vc/entity-types-version-load.component.html index 4736bb1ee2..b877d19b63 100644 --- a/ui-ngx/src/app/modules/home/components/vc/entity-types-version-load.component.html +++ b/ui-ngx/src/app/modules/home/components/vc/entity-types-version-load.component.html @@ -72,6 +72,9 @@ {{ 'version-control.load-relations' | translate }} + + {{ 'version-control.load-calculated-fields' | translate }} + diff --git a/ui-ngx/src/app/modules/home/components/vc/entity-types-version-load.component.ts b/ui-ngx/src/app/modules/home/components/vc/entity-types-version-load.component.ts index 23f09ae977..f06abaf3a8 100644 --- a/ui-ngx/src/app/modules/home/components/vc/entity-types-version-load.component.ts +++ b/ui-ngx/src/app/modules/home/components/vc/entity-types-version-load.component.ts @@ -31,7 +31,8 @@ import { PageComponent } from '@shared/components/page.component'; import { entityTypesWithoutRelatedData, EntityTypeVersionLoadConfig, - exportableEntityTypes + exportableEntityTypes, + typesWithCalculatedFields } from '@shared/models/vc.models'; import { Store } from '@ngrx/store'; import { AppState } from '@core/core.state'; @@ -75,6 +76,8 @@ export class EntityTypesVersionLoadComponent extends PageComponent implements On loading = true; + readonly typesWithCalculatedFields = typesWithCalculatedFields; + constructor(protected store: Store, private translate: TranslateService, private popoverService: TbPopoverService, @@ -145,6 +148,7 @@ export class EntityTypesVersionLoadComponent extends PageComponent implements On loadRelations: [config.loadRelations, []], loadAttributes: [config.loadAttributes, []], loadCredentials: [config.loadCredentials, []], + loadCalculatedFields: [config.loadCalculatedFields, []], removeOtherEntities: [config.removeOtherEntities, []], findExistingEntityByName: [config.findExistingEntityByName, []] }) @@ -180,6 +184,7 @@ export class EntityTypesVersionLoadComponent extends PageComponent implements On loadAttributes: true, loadRelations: true, loadCredentials: true, + loadCalculatedFields: true, removeOtherEntities: false, findExistingEntityByName: true }; diff --git a/ui-ngx/src/app/modules/home/components/vc/entity-version-create.component.html b/ui-ngx/src/app/modules/home/components/vc/entity-version-create.component.html index 10c2dc5e40..da4c31a1b8 100644 --- a/ui-ngx/src/app/modules/home/components/vc/entity-version-create.component.html +++ b/ui-ngx/src/app/modules/home/components/vc/entity-version-create.component.html @@ -47,6 +47,9 @@ {{ 'version-control.export-relations' | translate }} + + {{ 'version-control.export-calculated-fields' | translate }} + diff --git a/ui-ngx/src/app/modules/home/components/vc/entity-version-create.component.ts b/ui-ngx/src/app/modules/home/components/vc/entity-version-create.component.ts index 48b2a2a174..0c4b696ad1 100644 --- a/ui-ngx/src/app/modules/home/components/vc/entity-version-create.component.ts +++ b/ui-ngx/src/app/modules/home/components/vc/entity-version-create.component.ts @@ -20,6 +20,7 @@ import { UntypedFormBuilder, UntypedFormGroup, Validators } from '@angular/forms import { entityTypesWithoutRelatedData, SingleEntityVersionCreateRequest, + typesWithCalculatedFields, VersionCreateRequestType, VersionCreationResult } from '@shared/models/vc.models'; @@ -71,6 +72,8 @@ export class EntityVersionCreateComponent extends PageComponent implements OnIni private versionCreateResultSubscription: Subscription; + readonly typesWithCalculatedFields = typesWithCalculatedFields; + constructor(protected store: Store, private entitiesVersionControlService: EntitiesVersionControlService, private cd: ChangeDetectorRef, @@ -115,7 +118,8 @@ export class EntityVersionCreateComponent extends PageComponent implements OnIni ? this.createVersionFormGroup.get('saveRelations').value : false, saveAttributes: !entityTypesWithoutRelatedData.has(this.entityId.entityType) ? this.createVersionFormGroup.get('saveAttributes').value : false, - saveCredentials: this.entityId.entityType === EntityType.DEVICE ? this.createVersionFormGroup.get('saveCredentials').value : false + saveCredentials: this.entityId.entityType === EntityType.DEVICE ? this.createVersionFormGroup.get('saveCredentials').value : false, + saveCalculatedFields: typesWithCalculatedFields.has(this.entityId.entityType) ? this.createVersionFormGroup.get('saveCalculatedFields').value : false, }, type: VersionCreateRequestType.SINGLE_ENTITY }; diff --git a/ui-ngx/src/app/modules/home/components/vc/entity-version-restore.component.html b/ui-ngx/src/app/modules/home/components/vc/entity-version-restore.component.html index 4bc61757c4..1183223850 100644 --- a/ui-ngx/src/app/modules/home/components/vc/entity-version-restore.component.html +++ b/ui-ngx/src/app/modules/home/components/vc/entity-version-restore.component.html @@ -36,6 +36,9 @@ {{ 'version-control.load-relations' | translate }} + + {{ 'version-control.load-calculated-fields' | translate }} + diff --git a/ui-ngx/src/app/modules/home/components/vc/entity-version-restore.component.ts b/ui-ngx/src/app/modules/home/components/vc/entity-version-restore.component.ts index df2e8c19ff..4456eb7ce7 100644 --- a/ui-ngx/src/app/modules/home/components/vc/entity-version-restore.component.ts +++ b/ui-ngx/src/app/modules/home/components/vc/entity-version-restore.component.ts @@ -79,7 +79,8 @@ export class EntityVersionRestoreComponent extends PageComponent implements OnIn this.restoreFormGroup = this.fb.group({ loadAttributes: [true, []], loadRelations: [true, []], - loadCredentials: [true, []] + loadCredentials: [true, []], + loadCalculatedFields: [true, []] }); this.entitiesVersionControlService.getEntityDataInfo(this.externalEntityId, this.versionId).subscribe((data) => { this.entityDataInfo = data; @@ -110,7 +111,8 @@ export class EntityVersionRestoreComponent extends PageComponent implements OnIn config: { loadRelations: this.entityDataInfo.hasRelations ? this.restoreFormGroup.get('loadRelations').value : false, loadAttributes: this.entityDataInfo.hasAttributes ? this.restoreFormGroup.get('loadAttributes').value : false, - loadCredentials: this.entityDataInfo.hasCredentials ? this.restoreFormGroup.get('loadCredentials').value : false + loadCredentials: this.entityDataInfo.hasCredentials ? this.restoreFormGroup.get('loadCredentials').value : false, + loadCalculatedFields: this.entityDataInfo.hasCalculatedFields ? this.restoreFormGroup.get('loadCalculatedFields').value : false }, type: VersionLoadRequestType.SINGLE_ENTITY }; diff --git a/ui-ngx/src/app/shared/models/vc.models.ts b/ui-ngx/src/app/shared/models/vc.models.ts index 6cc8de02eb..3795518ffc 100644 --- a/ui-ngx/src/app/shared/models/vc.models.ts +++ b/ui-ngx/src/app/shared/models/vc.models.ts @@ -49,6 +49,7 @@ export interface VersionCreateConfig { saveRelations: boolean; saveAttributes: boolean; saveCredentials: boolean; + saveCalculatedFields: boolean; } export enum VersionCreateRequestType { @@ -106,6 +107,7 @@ export function createDefaultEntityTypesVersionCreate(): {[entityType: string]: syncStrategy: null, saveAttributes: !entityTypesWithoutRelatedData.has(entityType), saveRelations: !entityTypesWithoutRelatedData.has(entityType), + saveCalculatedFields: typesWithCalculatedFields.has(entityType), saveCredentials: true, allEntities: true, entityIds: [] @@ -118,6 +120,7 @@ export interface VersionLoadConfig { loadRelations: boolean; loadAttributes: boolean; loadCredentials: boolean; + loadCalculatedFields: boolean; } export enum VersionLoadRequestType { @@ -154,6 +157,7 @@ export function createDefaultEntityTypesVersionLoad(): {[entityType: string]: En loadAttributes: !entityTypesWithoutRelatedData.has(entityType), loadRelations: !entityTypesWithoutRelatedData.has(entityType), loadCredentials: true, + loadCalculatedFields: typesWithCalculatedFields.has(entityType), removeOtherEntities: false, findExistingEntityByName: true }; @@ -254,4 +258,7 @@ export interface EntityDataInfo { hasRelations: boolean; hasAttributes: boolean; hasCredentials: boolean; + hasCalculatedFields: boolean; } + +export const typesWithCalculatedFields = new Set([EntityType.DEVICE, EntityType.ASSET, EntityType.ASSET_PROFILE, EntityType.DEVICE_PROFILE]); 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 02c604d8ae..6db2fa9ff1 100644 --- a/ui-ngx/src/assets/locale/locale.constant-en_US.json +++ b/ui-ngx/src/assets/locale/locale.constant-en_US.json @@ -6320,6 +6320,7 @@ "export-relations": "Export relations", "export-attributes": "Export attributes", "export-credentials": "Export credentials", + "export-calculated-fields": "Export calculated fields", "entity-versions": "Entity versions", "versions": "Versions", "created-time": "Created time", @@ -6336,6 +6337,7 @@ "load-relations": "Load relations", "load-attributes": "Load attributes", "load-credentials": "Load credentials", + "load-calculated-fields": "Load calculated fields", "compare-with-current": "Compare with current", "diff-entity-with-version": "Diff with entity version '{{versionName}}'", "previous-difference": "Previous Difference", From ed6262ba9f4d48aeae1c82c0c9fad0a9fd1f9482 Mon Sep 17 00:00:00 2001 From: Dmytro Skarzhynets Date: Mon, 3 Mar 2025 12:04:51 +0200 Subject: [PATCH 148/345] Save attributes strategies: fix UI bugs --- .../action/advanced-processing-setting.component.html | 4 ++-- .../action/advanced-processing-setting.component.ts | 10 +++++----- .../rule-node/action/attributes-config.component.html | 2 +- .../rule-node/action/attributes-config.model.ts | 4 ++-- 4 files changed, 10 insertions(+), 10 deletions(-) diff --git a/ui-ngx/src/app/modules/home/components/rule-node/action/advanced-processing-setting.component.html b/ui-ngx/src/app/modules/home/components/rule-node/action/advanced-processing-setting.component.html index 02d8f7be8f..02965fe535 100644 --- a/ui-ngx/src/app/modules/home/components/rule-node/action/advanced-processing-setting.component.html +++ b/ui-ngx/src/app/modules/home/components/rule-node/action/advanced-processing-setting.component.html @@ -25,8 +25,8 @@ formControlName="timeseries" title="{{ 'rule-node-config.save-time-series.time-series' | translate }}" > - } diff --git a/ui-ngx/src/app/modules/home/components/rule-node/action/attributes-config.model.ts b/ui-ngx/src/app/modules/home/components/rule-node/action/attributes-config.model.ts index 441fe9d816..1f24b69116 100644 --- a/ui-ngx/src/app/modules/home/components/rule-node/action/attributes-config.model.ts +++ b/ui-ngx/src/app/modules/home/components/rule-node/action/attributes-config.model.ts @@ -42,7 +42,7 @@ export interface AttributeDeduplicateProcessingStrategy extends BasicProcessingS } export interface AttributeAdvancedProcessingStrategy extends BasicProcessingSettings { - attribute: AttributeAdvancedProcessingConfig; + attributes: AttributeAdvancedProcessingConfig; webSockets: AttributeAdvancedProcessingConfig; calculatedFields: AttributeAdvancedProcessingConfig; } @@ -54,7 +54,7 @@ export const defaultAdvancedProcessingConfig: AttributeAdvancedProcessingConfig } export const defaultAttributeAdvancedProcessingStrategy: Omit = { - attribute: defaultAdvancedProcessingConfig, + attributes: defaultAdvancedProcessingConfig, webSockets: defaultAdvancedProcessingConfig, calculatedFields: defaultAdvancedProcessingConfig, } From 2b7deda3d16bad64ea9754ed64e29a3d179a9c58 Mon Sep 17 00:00:00 2001 From: Dmytro Skarzhynets Date: Mon, 3 Mar 2025 12:26:20 +0200 Subject: [PATCH 149/345] Save attributes strategies: add advanced mode warning text; minor title correction --- ...advanced-processing-setting.component.html | 4 +-- .../rulenode/save_attributes_node_advanced.md | 32 +++++++++++++++++++ .../assets/locale/locale.constant-en_US.json | 2 +- 3 files changed, 35 insertions(+), 3 deletions(-) create mode 100644 ui-ngx/src/assets/help/en_US/rulenode/save_attributes_node_advanced.md diff --git a/ui-ngx/src/app/modules/home/components/rule-node/action/advanced-processing-setting.component.html b/ui-ngx/src/app/modules/home/components/rule-node/action/advanced-processing-setting.component.html index 02965fe535..edce8b12a9 100644 --- a/ui-ngx/src/app/modules/home/components/rule-node/action/advanced-processing-setting.component.html +++ b/ui-ngx/src/app/modules/home/components/rule-node/action/advanced-processing-setting.component.html @@ -18,7 +18,7 @@
Date: Mon, 3 Mar 2025 12:50:34 +0200 Subject: [PATCH 150/345] UI: Fix device profile extra scroll in lwm2m model table. --- .../lwm2m/lwm2m-observe-attr-telemetry-instances.component.scss | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ui-ngx/src/app/modules/home/components/profile/device/lwm2m/lwm2m-observe-attr-telemetry-instances.component.scss b/ui-ngx/src/app/modules/home/components/profile/device/lwm2m/lwm2m-observe-attr-telemetry-instances.component.scss index 910c6625db..d86864d1f3 100644 --- a/ui-ngx/src/app/modules/home/components/profile/device/lwm2m/lwm2m-observe-attr-telemetry-instances.component.scss +++ b/ui-ngx/src/app/modules/home/components/profile/device/lwm2m/lwm2m-observe-attr-telemetry-instances.component.scss @@ -15,7 +15,7 @@ */ :host{ .tb-panel { - min-height: 42px; + min-height: 48px; .tb-panel-title { font-weight: 500; From cf8e3aa4cd860e4184382f2321e550db1bc37d6b Mon Sep 17 00:00:00 2001 From: nick Date: Mon, 3 Mar 2025 13:02:29 +0200 Subject: [PATCH 151/345] tbel: Maps docs 'foreach(element : map --- .../service/script/TbelInvokeDocsIoTest.java | 25 +++++++++++++++++++ 1 file changed, 25 insertions(+) diff --git a/application/src/test/java/org/thingsboard/server/service/script/TbelInvokeDocsIoTest.java b/application/src/test/java/org/thingsboard/server/service/script/TbelInvokeDocsIoTest.java index 2d043b72ae..8b6c739d31 100644 --- a/application/src/test/java/org/thingsboard/server/service/script/TbelInvokeDocsIoTest.java +++ b/application/src/test/java/org/thingsboard/server/service/script/TbelInvokeDocsIoTest.java @@ -147,6 +147,31 @@ class TbelInvokeDocsIoTest extends AbstractTbelInvokeTest { assertEquals(expected, actual); } + @Test + public void mapsImplicitIterationWithoutEntrySet() throws ExecutionException, InterruptedException { + msgStr = msgMapStr; + decoderStr = """ + foreach(element : msg){ + if(element.getKey() == null){ + return raiseError("Bad getKey"); + } + if(element.key == null){ + return raiseError("Bad key"); + } + if(element.getValue() == null){ + return raiseError("Bad getValue"); + } + if(element.value == null){ + return raiseError("Bad value"); + } + } + return msg; + """; + LinkedHashMap expected = expectedMap; + Object actual = invokeScript(evalScript(decoderStr), msgStr); + assertEquals(expected, actual); + } + @Test public void mapsGetInfoSize_Test() throws ExecutionException, InterruptedException { msgStr = msgMapStr; From da4e231ea3857f25d78cce47cc34f5c13a572d50 Mon Sep 17 00:00:00 2001 From: Dmytro Skarzhynets Date: Mon, 3 Mar 2025 14:47:51 +0200 Subject: [PATCH 152/345] Save attributes strategies: ensure shared attributes subscription update is sent even if WS updates are disabled --- .../DefaultSubscriptionManagerService.java | 4 - .../DefaultTelemetrySubscriptionService.java | 17 ++ ...faultTelemetrySubscriptionServiceTest.java | 205 ++++++++++++++++++ .../thingsboard/common/util/DonAsynchron.java | 6 + 4 files changed, 228 insertions(+), 4 deletions(-) diff --git a/application/src/main/java/org/thingsboard/server/service/subscription/DefaultSubscriptionManagerService.java b/application/src/main/java/org/thingsboard/server/service/subscription/DefaultSubscriptionManagerService.java index 23adacfc2e..d5a5318798 100644 --- a/application/src/main/java/org/thingsboard/server/service/subscription/DefaultSubscriptionManagerService.java +++ b/application/src/main/java/org/thingsboard/server/service/subscription/DefaultSubscriptionManagerService.java @@ -217,10 +217,6 @@ public class DefaultSubscriptionManagerService extends TbApplicationEventListene if (entityId.getEntityType() == EntityType.DEVICE) { if (TbAttributeSubscriptionScope.SERVER_SCOPE.name().equalsIgnoreCase(scope)) { updateDeviceInactivityTimeout(tenantId, entityId, attributes); - } else if (TbAttributeSubscriptionScope.SHARED_SCOPE.name().equalsIgnoreCase(scope) && notifyDevice) { - clusterService.pushMsgToCore(DeviceAttributesEventNotificationMsg.onUpdate(tenantId, - new DeviceId(entityId.getId()), DataConstants.SHARED_SCOPE, new ArrayList<>(attributes)) - , null); } } callback.onSuccess(); diff --git a/application/src/main/java/org/thingsboard/server/service/telemetry/DefaultTelemetrySubscriptionService.java b/application/src/main/java/org/thingsboard/server/service/telemetry/DefaultTelemetrySubscriptionService.java index d60d1621b7..3af2c04643 100644 --- a/application/src/main/java/org/thingsboard/server/service/telemetry/DefaultTelemetrySubscriptionService.java +++ b/application/src/main/java/org/thingsboard/server/service/telemetry/DefaultTelemetrySubscriptionService.java @@ -35,9 +35,11 @@ import org.thingsboard.rule.engine.api.RuleEngineTelemetryService; import org.thingsboard.rule.engine.api.TimeseriesDeleteRequest; import org.thingsboard.rule.engine.api.TimeseriesSaveRequest; import org.thingsboard.server.common.data.ApiUsageRecordKey; +import org.thingsboard.server.common.data.DataConstants; import org.thingsboard.server.common.data.EntityType; import org.thingsboard.server.common.data.EntityView; import org.thingsboard.server.common.data.id.CustomerId; +import org.thingsboard.server.common.data.id.DeviceId; import org.thingsboard.server.common.data.id.EntityId; import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.common.data.kv.AttributeKvEntry; @@ -45,6 +47,7 @@ import org.thingsboard.server.common.data.kv.TimeseriesSaveResult; import org.thingsboard.server.common.data.kv.TsKvEntry; import org.thingsboard.server.common.data.kv.TsKvLatestRemovingResult; import org.thingsboard.server.common.msg.queue.TbCallback; +import org.thingsboard.server.common.msg.rule.engine.DeviceAttributesEventNotificationMsg; import org.thingsboard.server.common.stats.TbApiUsageReportClient; import org.thingsboard.server.dao.attributes.AttributesService; import org.thingsboard.server.dao.timeseries.TimeseriesService; @@ -52,6 +55,7 @@ import org.thingsboard.server.dao.util.KvUtils; import org.thingsboard.server.service.apiusage.TbApiUsageStateService; import org.thingsboard.server.service.cf.CalculatedFieldQueueService; import org.thingsboard.server.service.entitiy.entityview.TbEntityViewService; +import org.thingsboard.server.service.subscription.TbAttributeSubscriptionScope; import org.thingsboard.server.service.subscription.TbSubscriptionUtils; import java.util.ArrayList; @@ -197,6 +201,15 @@ public class DefaultTelemetrySubscriptionService extends AbstractSubscriptionSer } }, t -> request.getCallback().onFailure(t)); + if (strategy.saveAttributes() + && entityId.getEntityType() == EntityType.DEVICE + && TbAttributeSubscriptionScope.SHARED_SCOPE.name().equalsIgnoreCase(request.getScope().name()) + && request.isNotifyDevice()) { + addMainCallback(resultFuture, success -> clusterService.pushMsgToCore( + DeviceAttributesEventNotificationMsg.onUpdate(tenantId, new DeviceId(entityId.getId()), DataConstants.SHARED_SCOPE, request.getEntries()), null + )); + } + if (strategy.sendWsUpdate()) { addWsCallback(resultFuture, success -> onAttributesUpdate(tenantId, entityId, request.getScope().name(), request.getEntries(), request.isNotifyDevice())); } @@ -342,6 +355,10 @@ public class DefaultTelemetrySubscriptionService extends AbstractSubscriptionSer addMainCallback(saveFuture, result -> callback.onSuccess(null), callback::onFailure); } + private void addMainCallback(ListenableFuture saveFuture, Consumer onSuccess) { + DonAsynchron.withCallback(saveFuture, onSuccess, null, tsCallBackExecutor); + } + private void addMainCallback(ListenableFuture saveFuture, Consumer onSuccess, Consumer onFailure) { DonAsynchron.withCallback(saveFuture, onSuccess, onFailure, tsCallBackExecutor); } diff --git a/application/src/test/java/org/thingsboard/server/service/telemetry/DefaultTelemetrySubscriptionServiceTest.java b/application/src/test/java/org/thingsboard/server/service/telemetry/DefaultTelemetrySubscriptionServiceTest.java index c96b96b2e0..8bdcbc1349 100644 --- a/application/src/test/java/org/thingsboard/server/service/telemetry/DefaultTelemetrySubscriptionServiceTest.java +++ b/application/src/test/java/org/thingsboard/server/service/telemetry/DefaultTelemetrySubscriptionServiceTest.java @@ -24,6 +24,7 @@ import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; import org.junit.jupiter.params.ParameterizedTest; import org.junit.jupiter.params.provider.Arguments; +import org.junit.jupiter.params.provider.EnumSource; import org.junit.jupiter.params.provider.MethodSource; import org.mockito.Mock; import org.mockito.junit.jupiter.MockitoExtension; @@ -35,15 +36,21 @@ import org.thingsboard.server.common.data.ApiUsageRecordKey; import org.thingsboard.server.common.data.ApiUsageState; import org.thingsboard.server.common.data.ApiUsageStateValue; import org.thingsboard.server.common.data.AttributeScope; +import org.thingsboard.server.common.data.EntityType; import org.thingsboard.server.common.data.EntityView; +import org.thingsboard.server.common.data.id.ApiUsageStateId; import org.thingsboard.server.common.data.id.CustomerId; import org.thingsboard.server.common.data.id.DeviceId; import org.thingsboard.server.common.data.id.EntityId; +import org.thingsboard.server.common.data.id.EntityIdFactory; import org.thingsboard.server.common.data.id.EntityViewId; import org.thingsboard.server.common.data.id.TenantId; +import org.thingsboard.server.common.data.kv.AttributeKvEntry; +import org.thingsboard.server.common.data.kv.BaseAttributeKvEntry; import org.thingsboard.server.common.data.kv.BasicTsKvEntry; import org.thingsboard.server.common.data.kv.DoubleDataEntry; import org.thingsboard.server.common.data.kv.KvEntry; +import org.thingsboard.server.common.data.kv.StringDataEntry; import org.thingsboard.server.common.data.kv.TimeseriesSaveResult; import org.thingsboard.server.common.data.kv.TsKvEntry; import org.thingsboard.server.common.data.objects.AttributesEntityView; @@ -51,6 +58,7 @@ import org.thingsboard.server.common.data.objects.TelemetryEntityView; import org.thingsboard.server.common.msg.queue.ServiceType; import org.thingsboard.server.common.msg.queue.TbCallback; import org.thingsboard.server.common.msg.queue.TopicPartitionInfo; +import org.thingsboard.server.common.msg.rule.engine.DeviceAttributesEventNotificationMsg; import org.thingsboard.server.common.stats.TbApiUsageReportClient; import org.thingsboard.server.dao.attributes.AttributesService; import org.thingsboard.server.dao.timeseries.TimeseriesService; @@ -74,13 +82,17 @@ import java.util.concurrent.ExecutorService; import java.util.stream.LongStream; import java.util.stream.Stream; +import static com.google.common.util.concurrent.Futures.immediateFailedFuture; import static com.google.common.util.concurrent.Futures.immediateFuture; import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.ArgumentMatchers.isNull; import static org.mockito.BDDMockito.given; import static org.mockito.BDDMockito.then; import static org.mockito.Mockito.lenient; +import static org.mockito.Mockito.never; @ExtendWith(MockitoExtension.class) class DefaultTelemetrySubscriptionServiceTest { @@ -428,6 +440,199 @@ class DefaultTelemetrySubscriptionServiceTest { ); } + @Test + void shouldThrowErrorWhenTryingToSaveAttributesForApiUsageState() { + // GIVEN + var request = AttributesSaveRequest.builder() + .tenantId(tenantId) + .entityId(new ApiUsageStateId(UUID.randomUUID())) + .scope(AttributeScope.SHARED_SCOPE) + .entry(new DoubleDataEntry("temperature", 65.2)) + .notifyDevice(true) + .strategy(new AttributesSaveRequest.Strategy(true, false, false)) + .build(); + + // WHEN + assertThatThrownBy(() -> telemetryService.saveAttributes(request)) + .isInstanceOf(RuntimeException.class) + .hasMessage("Can't update API Usage State!"); + + // THEN + then(attrService).shouldHaveNoInteractions(); + } + + @Test + void shouldSendAttributesUpdateNotificationWhenDeviceSharedAttributesAreSavedAndNotifyDeviceIsTrue() { + // GIVEN + var deviceId = DeviceId.fromString("cc51e450-53e1-11ee-883e-e56b48fd2088"); + List entries = List.of( + new BaseAttributeKvEntry(123L, new DoubleDataEntry("shared1", 65.2)), + new BaseAttributeKvEntry(456L, new StringDataEntry("shared2", "test")) + ); + + var request = AttributesSaveRequest.builder() + .tenantId(tenantId) + .entityId(deviceId) + .scope(AttributeScope.SHARED_SCOPE) + .entries(entries) + .notifyDevice(true) + .strategy(new AttributesSaveRequest.Strategy(true, false, false)) + .build(); + + given(attrService.save(tenantId, deviceId, request.getScope(), entries)).willReturn(immediateFuture(listOfNNumbers(entries.size()))); + + // WHEN + telemetryService.saveAttributes(request); + + // THEN + var expectedAttributesUpdateMsg = DeviceAttributesEventNotificationMsg.onUpdate(tenantId, deviceId, "SHARED_SCOPE", entries); + + then(clusterService).should().pushMsgToCore(eq(expectedAttributesUpdateMsg), isNull()); + } + + @ParameterizedTest + @EnumSource( + value = EntityType.class, + names = {"DEVICE", "API_USAGE_STATE"}, // API usage state excluded due to coverage in another test + mode = EnumSource.Mode.EXCLUDE + ) + void shouldNotSendAttributesUpdateNotificationWhenEntityIsNotDevice(EntityType entityType) { + // GIVEN + var nonDeviceId = EntityIdFactory.getByTypeAndUuid(entityType, "cc51e450-53e1-11ee-883e-e56b48fd2088"); + List entries = List.of( + new BaseAttributeKvEntry(123L, new DoubleDataEntry("shared1", 65.2)), + new BaseAttributeKvEntry(456L, new StringDataEntry("shared2", "test")) + ); + + var request = AttributesSaveRequest.builder() + .tenantId(tenantId) + .entityId(nonDeviceId) + .scope(AttributeScope.SHARED_SCOPE) + .entries(entries) + .notifyDevice(true) + .strategy(new AttributesSaveRequest.Strategy(true, false, false)) + .build(); + + given(attrService.save(tenantId, nonDeviceId, request.getScope(), entries)).willReturn(immediateFuture(listOfNNumbers(entries.size()))); + + // WHEN + telemetryService.saveAttributes(request); + + // THEN + then(clusterService).should(never()).pushMsgToCore(any(), any()); + } + + @ParameterizedTest + @EnumSource( + value = AttributeScope.class, + names = "SHARED_SCOPE", + mode = EnumSource.Mode.EXCLUDE + ) + void shouldNotSendAttributesUpdateNotificationWhenAttributesAreNotShared(AttributeScope notSharedScope) { + // GIVEN + var deviceId = DeviceId.fromString("cc51e450-53e1-11ee-883e-e56b48fd2088"); + List entries = List.of( + new BaseAttributeKvEntry(123L, new DoubleDataEntry("shared1", 65.2)), + new BaseAttributeKvEntry(456L, new StringDataEntry("shared2", "test")) + ); + + var request = AttributesSaveRequest.builder() + .tenantId(tenantId) + .entityId(deviceId) + .scope(notSharedScope) + .entries(entries) + .notifyDevice(true) + .strategy(new AttributesSaveRequest.Strategy(true, false, false)) + .build(); + + given(attrService.save(tenantId, deviceId, request.getScope(), entries)).willReturn(immediateFuture(listOfNNumbers(entries.size()))); + + // WHEN + telemetryService.saveAttributes(request); + + // THEN + then(clusterService).should(never()).pushMsgToCore(any(), any()); + } + + @Test + void shouldNotSendAttributesUpdateNotificationWhenNotifyDeviceIsFalse() { + // GIVEN + var deviceId = DeviceId.fromString("cc51e450-53e1-11ee-883e-e56b48fd2088"); + List entries = List.of( + new BaseAttributeKvEntry(123L, new DoubleDataEntry("shared1", 65.2)), + new BaseAttributeKvEntry(456L, new StringDataEntry("shared2", "test")) + ); + + var request = AttributesSaveRequest.builder() + .tenantId(tenantId) + .entityId(deviceId) + .scope(AttributeScope.SHARED_SCOPE) + .entries(entries) + .notifyDevice(false) + .strategy(new AttributesSaveRequest.Strategy(true, false, false)) + .build(); + + given(attrService.save(tenantId, deviceId, request.getScope(), entries)).willReturn(immediateFuture(listOfNNumbers(entries.size()))); + + // WHEN + telemetryService.saveAttributes(request); + + // THEN + then(clusterService).should(never()).pushMsgToCore(any(), any()); + } + + @Test + void shouldNotSendAttributesUpdateNotificationWhenAttributesSaveWasSkipped() { + // GIVEN + var deviceId = DeviceId.fromString("cc51e450-53e1-11ee-883e-e56b48fd2088"); + List entries = List.of( + new BaseAttributeKvEntry(123L, new DoubleDataEntry("shared1", 65.2)), + new BaseAttributeKvEntry(456L, new StringDataEntry("shared2", "test")) + ); + + var request = AttributesSaveRequest.builder() + .tenantId(tenantId) + .entityId(deviceId) + .scope(AttributeScope.SHARED_SCOPE) + .entries(entries) + .notifyDevice(true) + .strategy(new AttributesSaveRequest.Strategy(false, false, false)) + .build(); + + // WHEN + telemetryService.saveAttributes(request); + + // THEN + then(clusterService).should(never()).pushMsgToCore(any(), any()); + } + + @Test + void shouldNotSendAttributesUpdateNotificationWhenAttributesSaveFailed() { + // GIVEN + var deviceId = DeviceId.fromString("cc51e450-53e1-11ee-883e-e56b48fd2088"); + List entries = List.of( + new BaseAttributeKvEntry(123L, new DoubleDataEntry("shared1", 65.2)), + new BaseAttributeKvEntry(456L, new StringDataEntry("shared2", "test")) + ); + + var request = AttributesSaveRequest.builder() + .tenantId(tenantId) + .entityId(deviceId) + .scope(AttributeScope.SHARED_SCOPE) + .entries(entries) + .notifyDevice(true) + .strategy(new AttributesSaveRequest.Strategy(true, false, false)) + .build(); + + given(attrService.save(tenantId, deviceId, request.getScope(), entries)).willReturn(immediateFailedFuture(new RuntimeException("failed to save"))); + + // WHEN + telemetryService.saveAttributes(request); + + // THEN + then(clusterService).should(never()).pushMsgToCore(any(), any()); + } + // used to emulate versions returned by save APIs private static List listOfNNumbers(int N) { return LongStream.range(0, N).boxed().toList(); diff --git a/common/util/src/main/java/org/thingsboard/common/util/DonAsynchron.java b/common/util/src/main/java/org/thingsboard/common/util/DonAsynchron.java index 008158246e..0f1a56cb17 100644 --- a/common/util/src/main/java/org/thingsboard/common/util/DonAsynchron.java +++ b/common/util/src/main/java/org/thingsboard/common/util/DonAsynchron.java @@ -36,6 +36,9 @@ public class DonAsynchron { FutureCallback callback = new FutureCallback() { @Override public void onSuccess(T result) { + if (onSuccess == null) { + return; + } try { onSuccess.accept(result); } catch (Throwable th) { @@ -45,6 +48,9 @@ public class DonAsynchron { @Override public void onFailure(Throwable t) { + if (onFailure == null) { + return; + } onFailure.accept(t); } }; From 9f274aa197923fc844a0b0e742cbf7da509fc08c Mon Sep 17 00:00:00 2001 From: dashevchenko Date: Mon, 3 Mar 2025 14:54:00 +0200 Subject: [PATCH 153/345] set EDQS_ENABLED to false by default --- docker/.env | 1 + docker/compose-utils.sh | 7 ++++++- 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/docker/.env b/docker/.env index f6d9ebc0f8..71722247df 100644 --- a/docker/.env +++ b/docker/.env @@ -15,6 +15,7 @@ LWM2M_TRANSPORT_DOCKER_NAME=tb-lwm2m-transport SNMP_TRANSPORT_DOCKER_NAME=tb-snmp-transport TB_VC_EXECUTOR_DOCKER_NAME=tb-vc-executor EDQS_DOCKER_NAME=tb-edqs +EDQS_ENABLED=false TB_VERSION=latest diff --git a/docker/compose-utils.sh b/docker/compose-utils.sh index bd04657339..5767026b11 100755 --- a/docker/compose-utils.sh +++ b/docker/compose-utils.sh @@ -151,7 +151,6 @@ function permissionList() { 799 799 tb-transports/coap/log 799 799 tb-vc-executor/log 999 999 tb-node/postgres - 799 799 edqs/log " source .env @@ -162,6 +161,12 @@ function permissionList() { " fi + if [ "$EDQS_ENABLED" = true ]; then + PERMISSION_LIST="$PERMISSION_LIST + 799 799 edqs/log + " + fi + CACHE="${CACHE:-redis}" case $CACHE in redis) From bab367c50f26fa5c4b7ee971ffdd7cbad7ab930f Mon Sep 17 00:00:00 2001 From: dashevchenko Date: Mon, 3 Mar 2025 15:24:23 +0200 Subject: [PATCH 154/345] delete redundant env variable --- .../java/org/thingsboard/server/msa/ThingsBoardDbInstaller.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/msa/black-box-tests/src/test/java/org/thingsboard/server/msa/ThingsBoardDbInstaller.java b/msa/black-box-tests/src/test/java/org/thingsboard/server/msa/ThingsBoardDbInstaller.java index 9ce9f82bca..9c1a90a4f1 100644 --- a/msa/black-box-tests/src/test/java/org/thingsboard/server/msa/ThingsBoardDbInstaller.java +++ b/msa/black-box-tests/src/test/java/org/thingsboard/server/msa/ThingsBoardDbInstaller.java @@ -227,7 +227,7 @@ public class ThingsBoardDbInstaller { dockerCompose.withCommand("up -d postgres" + additionalServices); dockerCompose.invokeCompose(); - dockerCompose.withCommand("run --no-deps --rm -e INSTALL_TB=true -e LOAD_DEMO=true -e TB_EDQS_SYNC_ENABLED=false " + + dockerCompose.withCommand("run --no-deps --rm -e INSTALL_TB=true -e LOAD_DEMO=true " + "tb-core1"); dockerCompose.invokeCompose(); From 3d3a8ffc057d98820f0b612265fb8ca5223bed13 Mon Sep 17 00:00:00 2001 From: mpetrov Date: Mon, 3 Mar 2025 15:55:39 +0200 Subject: [PATCH 155/345] Added debug settings service --- .../calculated-fields-table-config.ts | 61 ++++++----------- .../calculated-fields-table.component.ts | 11 ++- ...ntity-debug-settings-button.component.html | 2 +- .../entity-debug-settings-button.component.ts | 46 +++++-------- .../debug/entity-debug-settings.service.ts | 68 +++++++++++++++++++ .../home/components/home-components.module.ts | 4 +- ui-ngx/src/app/shared/models/entity.models.ts | 11 +++ ui-ngx/src/app/shared/shared.module.ts | 2 + 8 files changed, 123 insertions(+), 82 deletions(-) create mode 100644 ui-ngx/src/app/modules/home/components/entity/debug/entity-debug-settings.service.ts diff --git a/ui-ngx/src/app/modules/home/components/calculated-fields/calculated-fields-table-config.ts b/ui-ngx/src/app/modules/home/components/calculated-fields/calculated-fields-table-config.ts index 6932860be4..9cb04ee04a 100644 --- a/ui-ngx/src/app/modules/home/components/calculated-fields/calculated-fields-table-config.ts +++ b/ui-ngx/src/app/modules/home/components/calculated-fields/calculated-fields-table-config.ts @@ -29,10 +29,7 @@ import { AppState } from '@core/core.state'; import { getCurrentAuthState, getCurrentAuthUser } from '@core/auth/auth.selectors'; import { DestroyRef, Renderer2 } from '@angular/core'; import { EntityDebugSettings } from '@shared/models/entity.models'; -import { DurationLeftPipe } from '@shared/pipe/duration-left.pipe'; import { takeUntilDestroyed } from '@angular/core/rxjs-interop'; -import { TbPopoverService } from '@shared/components/popover.service'; -import { EntityDebugSettingsPanelComponent } from '@home/components/entity/debug/entity-debug-settings-panel.component'; import { CalculatedFieldsService } from '@core/http/calculated-fields.service'; import { catchError, filter, switchMap, tap } from 'rxjs/operators'; import { @@ -53,6 +50,7 @@ import { } from './components/public-api'; import { ImportExportService } from '@shared/import-export/import-export.service'; import { isObject } from '@core/utils'; +import { EntityDebugSettingsService } from '@home/components/entity/debug/entity-debug-settings.service'; export class CalculatedFieldsTableConfig extends EntityTableConfig { @@ -71,12 +69,11 @@ export class CalculatedFieldsTableConfig extends EntityTableConfig, - private durationLeft: DurationLeftPipe, - private popoverService: TbPopoverService, private destroyRef: DestroyRef, private renderer: Renderer2, public entityName: string, - private importExportService: ImportExportService + private importExportService: ImportExportService, + private entityDebugSettingsService: EntityDebugSettingsService, ) { super(); this.tableTitle = this.translate.instant('entity.type-calculated-fields'); @@ -131,10 +128,10 @@ export class CalculatedFieldsTableConfig extends EntityTableConfig this.getDebugConfigLabel(entity?.debugSettings), + nameFunction: entity => this.entityDebugSettingsService.getDebugConfigLabel(entity?.debugSettings), icon: 'mdi:bug', isEnabled: () => true, - iconFunction: ({ debugSettings }) => this.isDebugActive(debugSettings?.allEnabledUntil) || debugSettings?.failuresEnabled ? 'mdi:bug' : 'mdi:bug-outline', + iconFunction: ({ debugSettings }) => this.entityDebugSettingsService.isDebugActive(debugSettings?.allEnabledUntil) || debugSettings?.failuresEnabled ? 'mdi:bug' : 'mdi:bug-outline', onAction: ($event, entity) => this.onOpenDebugConfig($event, entity), }, { @@ -160,26 +157,20 @@ export class CalculatedFieldsTableConfig extends EntityTableConfig { - this.onDebugConfigChanged(id.id, settings); - debugStrategyPopover.hide(); - }); - } + + this.entityDebugSettingsService.viewContainerRef = viewContainerRef; + this.entityDebugSettingsService.renderer = this.renderer; + + this.entityDebugSettingsService.openDebugStrategyPanel({ + debugSettings, + debugConfig: { + debugLimitsConfiguration: this.calculatedFieldsDebugPerTenantLimitsConfiguration, + maxDebugModeDuration: this.maxDebugModeDuration, + entityLabel: this.translate.instant('debug-settings.calculated-field'), + additionalActionConfig, + }, + onSettingsAppliedFn: settings => this.onDebugConfigChanged(id.id, settings) + }, $event.target as Element); } private editCalculatedField(calculatedField: CalculatedField, isDirty = false): void { @@ -239,20 +230,6 @@ export class CalculatedFieldsTableConfig extends EntityTableConfig this.updateData()); } - private getDebugConfigLabel(debugSettings: EntityDebugSettings): string { - const isDebugActive = this.isDebugActive(debugSettings?.allEnabledUntil); - - if (!isDebugActive) { - return debugSettings?.failuresEnabled ? this.translate.instant('debug-settings.failures') : this.translate.instant('common.disabled'); - } else { - return this.durationLeft.transform(debugSettings?.allEnabledUntil); - } - } - - private isDebugActive(allEnabledUntil: number): boolean { - return allEnabledUntil > new Date().getTime(); - } - private onDebugConfigChanged(id: string, debugSettings: EntityDebugSettings): void { this.calculatedFieldsService.getCalculatedFieldById(id).pipe( switchMap(field => this.calculatedFieldsService.saveCalculatedField({ ...field, debugSettings })), diff --git a/ui-ngx/src/app/modules/home/components/calculated-fields/calculated-fields-table.component.ts b/ui-ngx/src/app/modules/home/components/calculated-fields/calculated-fields-table.component.ts index be6fb11480..82d96b9557 100644 --- a/ui-ngx/src/app/modules/home/components/calculated-fields/calculated-fields-table.component.ts +++ b/ui-ngx/src/app/modules/home/components/calculated-fields/calculated-fields-table.component.ts @@ -31,10 +31,9 @@ import { MatDialog } from '@angular/material/dialog'; import { Store } from '@ngrx/store'; import { AppState } from '@core/core.state'; import { CalculatedFieldsTableConfig } from '@home/components/calculated-fields/calculated-fields-table-config'; -import { DurationLeftPipe } from '@shared/pipe/duration-left.pipe'; -import { TbPopoverService } from '@shared/components/popover.service'; import { CalculatedFieldsService } from '@core/http/calculated-fields.service'; import { ImportExportService } from '@shared/import-export/import-export.service'; +import { EntityDebugSettingsService } from '@home/components/entity/debug/entity-debug-settings.service'; @Component({ selector: 'tb-calculated-fields-table', @@ -56,11 +55,10 @@ export class CalculatedFieldsTableComponent { private translate: TranslateService, private dialog: MatDialog, private store: Store, - private durationLeft: DurationLeftPipe, - private popoverService: TbPopoverService, private cd: ChangeDetectorRef, private renderer: Renderer2, private importExportService: ImportExportService, + private entityDebugSettingsService: EntityDebugSettingsService, private destroyRef: DestroyRef) { effect(() => { @@ -71,12 +69,11 @@ export class CalculatedFieldsTableComponent { this.dialog, this.entityId(), this.store, - this.durationLeft, - this.popoverService, this.destroyRef, this.renderer, this.entityName(), - this.importExportService + this.importExportService, + this.entityDebugSettingsService, ); this.cd.markForCheck(); } diff --git a/ui-ngx/src/app/modules/home/components/entity/debug/entity-debug-settings-button.component.html b/ui-ngx/src/app/modules/home/components/entity/debug/entity-debug-settings-button.component.html index a6033df8cb..4243044fc8 100644 --- a/ui-ngx/src/app/modules/home/components/entity/debug/entity-debug-settings-button.component.html +++ b/ui-ngx/src/app/modules/home/components/entity/debug/entity-debug-settings-button.component.html @@ -21,7 +21,7 @@ #matButton [class.active]="((isDebugAllActive$ | async) || failuresEnabled) && !disabled" [disabled]="disabled" - (click)="openDebugStrategyPanel($event, matButton)"> + (click)="onOpenDebugStrategyPanel($event, matButton)"> bug_report @if (isDebugAllActive$ | async) { {{ (allEnabled$ | async) === false ? (allEnabledUntil | durationLeft) : (maxDebugModeDuration | milliSecondsToTimeString: true : true) }} diff --git a/ui-ngx/src/app/modules/home/components/entity/debug/entity-debug-settings-button.component.ts b/ui-ngx/src/app/modules/home/components/entity/debug/entity-debug-settings-button.component.ts index 852da5f7bf..99a08494fc 100644 --- a/ui-ngx/src/app/modules/home/components/entity/debug/entity-debug-settings-button.component.ts +++ b/ui-ngx/src/app/modules/home/components/entity/debug/entity-debug-settings-button.component.ts @@ -20,15 +20,11 @@ import { Component, forwardRef, Input, - Renderer2, - ViewContainerRef } from '@angular/core'; import { CommonModule } from '@angular/common'; import { SharedModule } from '@shared/shared.module'; import { DurationLeftPipe } from '@shared/pipe/duration-left.pipe'; -import { TbPopoverService } from '@shared/components/popover.service'; import { MatButton } from '@angular/material/button'; -import { EntityDebugSettingsPanelComponent } from './entity-debug-settings-panel.component'; import { takeUntilDestroyed } from '@angular/core/rxjs-interop'; import { BehaviorSubject, of, shareReplay, timer } from 'rxjs'; import { SECOND, MINUTE } from '@shared/models/time/time.models'; @@ -38,6 +34,7 @@ import { getCurrentAuthState } from '@core/auth/auth.selectors'; import { AppState } from '@core/core.state'; import { Store } from '@ngrx/store'; import { ControlValueAccessor, FormBuilder, NG_VALUE_ACCESSOR } from '@angular/forms'; +import { EntityDebugSettingsService } from '@home/components/entity/debug/entity-debug-settings.service'; @Component({ selector: 'tb-entity-debug-settings-button', @@ -54,6 +51,7 @@ import { ControlValueAccessor, FormBuilder, NG_VALUE_ACCESSOR } from '@angular/f useExisting: forwardRef(() => EntityDebugSettingsButtonComponent), multi: true }, + EntityDebugSettingsService ], changeDetection: ChangeDetectionStrategy.OnPush }) @@ -92,11 +90,9 @@ export class EntityDebugSettingsButtonComponent implements ControlValueAccessor private propagateChange: (settings: EntityDebugSettings) => void = () => {}; - constructor(private popoverService: TbPopoverService, - private renderer: Renderer2, - private store: Store, - private viewContainerRef: ViewContainerRef, + constructor(private store: Store, private fb: FormBuilder, + private entityDebugSettingsService: EntityDebugSettingsService, private cd : ChangeDetectorRef, ) { this.debugSettingsFormGroup.valueChanges.pipe( @@ -118,33 +114,23 @@ export class EntityDebugSettingsButtonComponent implements ControlValueAccessor return this.debugSettingsFormGroup.get('allEnabledUntil').value; } - openDebugStrategyPanel($event: Event, matButton: MatButton): void { + onOpenDebugStrategyPanel($event: Event, matButton: MatButton): void { if ($event) { $event.stopPropagation(); } - const trigger = matButton._elementRef.nativeElement; - const debugSettings = this.debugSettingsFormGroup.value; - - if (this.popoverService.hasPopover(trigger)) { - this.popoverService.hidePopover(trigger); - } else { - const debugStrategyPopover = this.popoverService.displayPopover(trigger, this.renderer, - this.viewContainerRef, EntityDebugSettingsPanelComponent, 'bottom', true, null, - { - ...debugSettings, - maxDebugModeDuration: this.maxDebugModeDuration, - debugLimitsConfiguration: this.debugLimitsConfiguration, - entityLabel: this.entityLabel, - additionalActionConfig: this.additionalActionConfig, - }, - {}, - {}, {}, true); - debugStrategyPopover.tbComponentRef.instance.onSettingsApplied.subscribe((settings: EntityDebugSettings) => { + this.entityDebugSettingsService.openDebugStrategyPanel({ + debugSettings: this.debugSettingsFormGroup.value, + debugConfig: { + maxDebugModeDuration: this.maxDebugModeDuration, + debugLimitsConfiguration: this.debugLimitsConfiguration, + entityLabel: this.entityLabel, + additionalActionConfig: this.additionalActionConfig, + }, + onSettingsAppliedFn: settings => { this.debugSettingsFormGroup.patchValue(settings); this.cd.markForCheck(); - debugStrategyPopover.hide(); - }); - } + } + }, matButton._elementRef.nativeElement); } registerOnChange(fn: (settings: EntityDebugSettings) => void): void { diff --git a/ui-ngx/src/app/modules/home/components/entity/debug/entity-debug-settings.service.ts b/ui-ngx/src/app/modules/home/components/entity/debug/entity-debug-settings.service.ts new file mode 100644 index 0000000000..c53bc5b777 --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/entity/debug/entity-debug-settings.service.ts @@ -0,0 +1,68 @@ +/// +/// 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 { Injectable, Optional, Renderer2, ViewContainerRef } from '@angular/core'; +import { EntityDebugSettingsPanelComponent } from '@home/components/entity/debug/entity-debug-settings-panel.component'; +import { EntityDebugSettingPanelConfig, EntityDebugSettings } from '@shared/models/entity.models'; +import { TbPopoverService } from '@shared/components/popover.service'; +import { TranslateService } from '@ngx-translate/core'; +import { DurationLeftPipe } from '@shared/pipe/duration-left.pipe'; + +@Injectable() +export class EntityDebugSettingsService { + + constructor( + private popoverService: TbPopoverService, + @Optional() public renderer: Renderer2, + @Optional() public viewContainerRef: ViewContainerRef, + private translate: TranslateService, + private durationLeft: DurationLeftPipe, + ) {} + + openDebugStrategyPanel(panelConfig: EntityDebugSettingPanelConfig, trigger: Element): void { + if (this.popoverService.hasPopover(trigger)) { + this.popoverService.hidePopover(trigger); + } else { + const debugStrategyPopover = this.popoverService.displayPopover(trigger, this.renderer, + this.viewContainerRef, EntityDebugSettingsPanelComponent, 'bottom', true, null, + { + ...panelConfig.debugSettings, + ...panelConfig.debugConfig, + }, + {}, + {}, {}, true); + debugStrategyPopover.tbComponentRef.instance.onSettingsApplied.subscribe(settings => { + panelConfig.onSettingsAppliedFn(settings); + debugStrategyPopover.hide(); + }); + } + } + + + getDebugConfigLabel(debugSettings: EntityDebugSettings): string { + const isDebugActive = this.isDebugActive(debugSettings?.allEnabledUntil); + + if (!isDebugActive) { + return debugSettings?.failuresEnabled ? this.translate.instant('debug-settings.failures') : this.translate.instant('common.disabled'); + } else { + return this.durationLeft.transform(debugSettings?.allEnabledUntil); + } + } + + isDebugActive(allEnabledUntil: number): boolean { + return allEnabledUntil > new Date().getTime(); + } +} diff --git a/ui-ngx/src/app/modules/home/components/home-components.module.ts b/ui-ngx/src/app/modules/home/components/home-components.module.ts index e680232129..76998669fd 100644 --- a/ui-ngx/src/app/modules/home/components/home-components.module.ts +++ b/ui-ngx/src/app/modules/home/components/home-components.module.ts @@ -184,7 +184,6 @@ import { import { EntityChipsComponent } from '@home/components/entity/entity-chips.component'; import { DashboardViewComponent } from '@home/components/dashboard-view/dashboard-view.component'; import { CalculatedFieldsTableComponent } from '@home/components/calculated-fields/calculated-fields-table.component'; -import { DurationLeftPipe } from '@shared/pipe/duration-left.pipe'; import { CalculatedFieldDialogComponent } from '@home/components/calculated-fields/components/dialog/calculated-field-dialog.component'; import { EntityDebugSettingsButtonComponent @@ -204,6 +203,7 @@ import { import { CalculatedFieldTestArgumentsComponent } from '@home/components/calculated-fields/components/test-arguments/calculated-field-test-arguments.component'; +import { EntityDebugSettingsService } from '@home/components/entity/debug/entity-debug-settings.service'; @NgModule({ declarations: @@ -504,7 +504,7 @@ import { providers: [ WidgetComponentService, CustomDialogService, - DurationLeftPipe, + EntityDebugSettingsService, {provide: EMBED_DASHBOARD_DIALOG_TOKEN, useValue: EmbedDashboardDialogComponent}, {provide: COMPLEX_FILTER_PREDICATE_DIALOG_COMPONENT_TOKEN, useValue: ComplexFilterPredicateDialogComponent}, {provide: DASHBOARD_PAGE_COMPONENT_TOKEN, useValue: DashboardPageComponent}, diff --git a/ui-ngx/src/app/shared/models/entity.models.ts b/ui-ngx/src/app/shared/models/entity.models.ts index 6fe8cd040e..87536fdc54 100644 --- a/ui-ngx/src/app/shared/models/entity.models.ts +++ b/ui-ngx/src/app/shared/models/entity.models.ts @@ -203,6 +203,17 @@ export interface EntityDebugSettings { allEnabledUntil?: number; } +export interface EntityDebugSettingPanelConfig { + debugSettings: EntityDebugSettings; + debugConfig: { + maxDebugModeDuration: number; + debugLimitsConfiguration: string; + entityLabel?: string; + additionalActionConfig?: AdditionalDebugActionConfig; + } + onSettingsAppliedFn: (settings: EntityDebugSettings) => void; +} + export interface EntityTestScriptResult { output: string; error: string; diff --git a/ui-ngx/src/app/shared/shared.module.ts b/ui-ngx/src/app/shared/shared.module.ts index 7eb622029f..8eec19379e 100644 --- a/ui-ngx/src/app/shared/shared.module.ts +++ b/ui-ngx/src/app/shared/shared.module.ts @@ -225,6 +225,7 @@ import { GroupingIntervalOptionsComponent } from '@shared/components/time/aggreg import { JsFuncModulesComponent } from '@shared/components/js-func-modules.component'; import { JsFuncModuleRowComponent } from '@shared/components/js-func-module-row.component'; import { EntityKeyAutocompleteComponent } from '@shared/components/entity/entity-key-autocomplete.component'; +import { DurationLeftPipe } from '@shared/pipe/duration-left.pipe'; export function MarkedOptionsFactory(markedOptionsService: MarkedOptionsService) { return markedOptionsService; @@ -244,6 +245,7 @@ export function MarkedOptionsFactory(markedOptionsService: MarkedOptionsService) ShortNumberPipe, ImagePipe, CustomTranslatePipe, + DurationLeftPipe, { provide: FlowInjectionToken, useValue: Flow From 9d4be3a9b38eed204052eaa29c7617265e84f606 Mon Sep 17 00:00:00 2001 From: mpetrov Date: Mon, 3 Mar 2025 16:44:34 +0200 Subject: [PATCH 156/345] optimization --- .../calculated-fields/calculated-fields-table-config.ts | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/ui-ngx/src/app/modules/home/components/calculated-fields/calculated-fields-table-config.ts b/ui-ngx/src/app/modules/home/components/calculated-fields/calculated-fields-table-config.ts index 9cb04ee04a..ddf52642f0 100644 --- a/ui-ngx/src/app/modules/home/components/calculated-fields/calculated-fields-table-config.ts +++ b/ui-ngx/src/app/modules/home/components/calculated-fields/calculated-fields-table-config.ts @@ -153,13 +153,15 @@ export class CalculatedFieldsTableConfig extends EntityTableConfig this.openDebugEventsDialog(calculatedField) }; - const { viewContainerRef } = this.getTable(); if ($event) { $event.stopPropagation(); } - this.entityDebugSettingsService.viewContainerRef = viewContainerRef; - this.entityDebugSettingsService.renderer = this.renderer; + const { viewContainerRef, renderer } = this.entityDebugSettingsService; + if (!viewContainerRef || !renderer) { + this.entityDebugSettingsService.viewContainerRef = this.getTable().viewContainerRef; + this.entityDebugSettingsService.renderer = this.renderer; + } this.entityDebugSettingsService.openDebugStrategyPanel({ debugSettings, From c1a70dcbaa6c5e5cf4cad330e26024066f7d5f34 Mon Sep 17 00:00:00 2001 From: mpetrov Date: Mon, 3 Mar 2025 16:55:35 +0200 Subject: [PATCH 157/345] optimization --- .../calculated-fields/calculated-fields-table.component.ts | 1 + .../src/app/modules/home/components/home-components.module.ts | 2 -- 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/ui-ngx/src/app/modules/home/components/calculated-fields/calculated-fields-table.component.ts b/ui-ngx/src/app/modules/home/components/calculated-fields/calculated-fields-table.component.ts index 82d96b9557..c1dd0bbd5a 100644 --- a/ui-ngx/src/app/modules/home/components/calculated-fields/calculated-fields-table.component.ts +++ b/ui-ngx/src/app/modules/home/components/calculated-fields/calculated-fields-table.component.ts @@ -40,6 +40,7 @@ import { EntityDebugSettingsService } from '@home/components/entity/debug/entity templateUrl: './calculated-fields-table.component.html', styleUrls: ['./calculated-fields-table.component.scss'], changeDetection: ChangeDetectionStrategy.OnPush, + providers: [EntityDebugSettingsService] }) export class CalculatedFieldsTableComponent { diff --git a/ui-ngx/src/app/modules/home/components/home-components.module.ts b/ui-ngx/src/app/modules/home/components/home-components.module.ts index 76998669fd..ac0296e2a5 100644 --- a/ui-ngx/src/app/modules/home/components/home-components.module.ts +++ b/ui-ngx/src/app/modules/home/components/home-components.module.ts @@ -203,7 +203,6 @@ import { import { CalculatedFieldTestArgumentsComponent } from '@home/components/calculated-fields/components/test-arguments/calculated-field-test-arguments.component'; -import { EntityDebugSettingsService } from '@home/components/entity/debug/entity-debug-settings.service'; @NgModule({ declarations: @@ -504,7 +503,6 @@ import { EntityDebugSettingsService } from '@home/components/entity/debug/entity providers: [ WidgetComponentService, CustomDialogService, - EntityDebugSettingsService, {provide: EMBED_DASHBOARD_DIALOG_TOKEN, useValue: EmbedDashboardDialogComponent}, {provide: COMPLEX_FILTER_PREDICATE_DIALOG_COMPONENT_TOKEN, useValue: ComplexFilterPredicateDialogComponent}, {provide: DASHBOARD_PAGE_COMPONENT_TOKEN, useValue: DashboardPageComponent}, From f467fc009e2d2e29484799bec0941378188c407c Mon Sep 17 00:00:00 2001 From: Andrii Landiak Date: Mon, 3 Mar 2025 16:58:26 +0200 Subject: [PATCH 158/345] Refactor cf vc --- .../DefaultEntitiesExportImportService.java | 7 +- .../ie/exporting/impl/AssetExportService.java | 18 +-- .../impl/AssetProfileExportService.java | 17 +-- .../BaseCalculatedFieldsExportService.java | 51 -------- .../impl/DefaultEntityExportService.java | 21 +++ .../exporting/impl/DeviceExportService.java | 11 +- .../impl/DeviceProfileExportService.java | 17 +-- .../ie/importing/impl/AssetImportService.java | 22 ++-- .../impl/AssetProfileImportService.java | 22 ++-- .../BaseCalculatedFieldsImportService.java | 123 ------------------ .../impl/BaseEntityImportService.java | 57 +++++++- .../importing/impl/DeviceImportService.java | 40 ++---- .../impl/DeviceProfileImportService.java | 22 ++-- .../sync/vc/data/EntitiesImportCtx.java | 6 +- .../sync/ie/ExportImportServiceSqlTest.java | 2 - .../service/sync/vc/VersionControlTest.java | 6 +- .../common/data/cf/CalculatedField.java | 37 +++--- .../common/data/id/CalculatedFieldId.java | 3 + .../server/common/data/sync/JsonTbEntity.java | 4 +- .../common/data/sync/ie/AssetExportData.java | 28 ---- .../data/sync/ie/AssetProfileExportData.java | 28 ---- .../sync/ie/CalculatedFieldExportData.java | 56 -------- .../common/data/sync/ie/DeviceExportData.java | 2 +- .../data/sync/ie/DeviceProfileExportData.java | 28 ---- .../common/data/sync/ie/EntityExportData.java | 14 +- .../server/dao/cf/CalculatedFieldDao.java | 3 +- .../server/dao/model/ModelConstants.java | 1 - .../dao/model/sql/CalculatedFieldEntity.java | 10 -- .../dao/sql/cf/CalculatedFieldRepository.java | 11 +- ...efaultNativeCalculatedFieldRepository.java | 1 - .../dao/sql/cf/JpaCalculatedFieldDao.java | 20 --- .../main/resources/sql/schema-entities.sql | 4 +- .../service/CalculatedFieldServiceTest.java | 14 -- 33 files changed, 181 insertions(+), 525 deletions(-) delete mode 100644 application/src/main/java/org/thingsboard/server/service/sync/ie/exporting/impl/BaseCalculatedFieldsExportService.java delete mode 100644 application/src/main/java/org/thingsboard/server/service/sync/ie/importing/impl/BaseCalculatedFieldsImportService.java delete mode 100644 common/data/src/main/java/org/thingsboard/server/common/data/sync/ie/AssetExportData.java delete mode 100644 common/data/src/main/java/org/thingsboard/server/common/data/sync/ie/AssetProfileExportData.java delete mode 100644 common/data/src/main/java/org/thingsboard/server/common/data/sync/ie/CalculatedFieldExportData.java delete mode 100644 common/data/src/main/java/org/thingsboard/server/common/data/sync/ie/DeviceProfileExportData.java 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 94b0ddc07e..d45f32c918 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 @@ -23,6 +23,7 @@ import org.thingsboard.server.cache.limits.RateLimitService; import org.thingsboard.server.common.data.EntityType; import org.thingsboard.server.common.data.ExportableEntity; import org.thingsboard.server.common.data.audit.ActionType; +import org.thingsboard.server.common.data.cf.CalculatedField; import org.thingsboard.server.common.data.exception.ThingsboardErrorCode; import org.thingsboard.server.common.data.exception.ThingsboardException; import org.thingsboard.server.common.data.id.EntityId; @@ -129,10 +130,10 @@ public class DefaultEntitiesExportImportService implements EntitiesExportImportS relation, ctx.getUser(), ActionType.RELATION_ADD_OR_UPDATE, null, relation); } - ctx.getCalculatedFields().forEach((calculatedField, created) -> { + for (CalculatedField calculatedField : ctx.getCalculatedFields()) { var savedCalculatedField = calculatedFieldService.save(calculatedField); - logEntityActionService.logEntityAction(ctx.getTenantId(), savedCalculatedField.getId(), savedCalculatedField, created ? ActionType.ADDED : ActionType.UPDATED, ctx.getUser()); - }); + logEntityActionService.logEntityAction(ctx.getTenantId(), savedCalculatedField.getId(), savedCalculatedField, ActionType.ADDED, ctx.getUser()); + } } @Override diff --git a/application/src/main/java/org/thingsboard/server/service/sync/ie/exporting/impl/AssetExportService.java b/application/src/main/java/org/thingsboard/server/service/sync/ie/exporting/impl/AssetExportService.java index 6f56fa5145..fbda92b6be 100644 --- a/application/src/main/java/org/thingsboard/server/service/sync/ie/exporting/impl/AssetExportService.java +++ b/application/src/main/java/org/thingsboard/server/service/sync/ie/exporting/impl/AssetExportService.java @@ -15,12 +15,12 @@ */ package org.thingsboard.server.service.sync.ie.exporting.impl; +import lombok.RequiredArgsConstructor; import org.springframework.stereotype.Service; import org.thingsboard.server.common.data.EntityType; import org.thingsboard.server.common.data.asset.Asset; import org.thingsboard.server.common.data.id.AssetId; -import org.thingsboard.server.common.data.sync.ie.AssetExportData; -import org.thingsboard.server.dao.cf.CalculatedFieldService; +import org.thingsboard.server.common.data.sync.ie.EntityExportData; import org.thingsboard.server.queue.util.TbCoreComponent; import org.thingsboard.server.service.sync.vc.data.EntitiesExportCtx; @@ -28,22 +28,18 @@ import java.util.Set; @Service @TbCoreComponent -public class AssetExportService extends BaseCalculatedFieldsExportService { - - protected AssetExportService(CalculatedFieldService calculatedFieldService) { - super(calculatedFieldService); - } +@RequiredArgsConstructor +public class AssetExportService extends BaseEntityExportService> { @Override - protected void setRelatedEntities(EntitiesExportCtx ctx, Asset asset, AssetExportData exportData) { + protected void setRelatedEntities(EntitiesExportCtx ctx, Asset asset, EntityExportData exportData) { asset.setCustomerId(getExternalIdOrElseInternal(ctx, asset.getCustomerId())); asset.setAssetProfileId(getExternalIdOrElseInternal(ctx, asset.getAssetProfileId())); - setCalculatedFields(ctx, asset, exportData); } @Override - protected AssetExportData newExportData() { - return new AssetExportData(); + protected EntityExportData newExportData() { + return new EntityExportData<>(); } @Override diff --git a/application/src/main/java/org/thingsboard/server/service/sync/ie/exporting/impl/AssetProfileExportService.java b/application/src/main/java/org/thingsboard/server/service/sync/ie/exporting/impl/AssetProfileExportService.java index 2314db92f5..b670daf06c 100644 --- a/application/src/main/java/org/thingsboard/server/service/sync/ie/exporting/impl/AssetProfileExportService.java +++ b/application/src/main/java/org/thingsboard/server/service/sync/ie/exporting/impl/AssetProfileExportService.java @@ -19,8 +19,7 @@ import org.springframework.stereotype.Service; import org.thingsboard.server.common.data.EntityType; import org.thingsboard.server.common.data.asset.AssetProfile; import org.thingsboard.server.common.data.id.AssetProfileId; -import org.thingsboard.server.common.data.sync.ie.AssetProfileExportData; -import org.thingsboard.server.dao.cf.CalculatedFieldService; +import org.thingsboard.server.common.data.sync.ie.EntityExportData; import org.thingsboard.server.queue.util.TbCoreComponent; import org.thingsboard.server.service.sync.vc.data.EntitiesExportCtx; @@ -28,23 +27,13 @@ import java.util.Set; @Service @TbCoreComponent -public class AssetProfileExportService extends BaseCalculatedFieldsExportService { - - protected AssetProfileExportService(CalculatedFieldService calculatedFieldService) { - super(calculatedFieldService); - } +public class AssetProfileExportService extends BaseEntityExportService> { @Override - protected void setRelatedEntities(EntitiesExportCtx ctx, AssetProfile assetProfile, AssetProfileExportData exportData) { + protected void setRelatedEntities(EntitiesExportCtx ctx, AssetProfile assetProfile, EntityExportData exportData) { assetProfile.setDefaultDashboardId(getExternalIdOrElseInternal(ctx, assetProfile.getDefaultDashboardId())); assetProfile.setDefaultRuleChainId(getExternalIdOrElseInternal(ctx, assetProfile.getDefaultRuleChainId())); assetProfile.setDefaultEdgeRuleChainId(getExternalIdOrElseInternal(ctx, assetProfile.getDefaultEdgeRuleChainId())); - setCalculatedFields(ctx, assetProfile, exportData); - } - - @Override - protected AssetProfileExportData newExportData() { - return new AssetProfileExportData(); } @Override diff --git a/application/src/main/java/org/thingsboard/server/service/sync/ie/exporting/impl/BaseCalculatedFieldsExportService.java b/application/src/main/java/org/thingsboard/server/service/sync/ie/exporting/impl/BaseCalculatedFieldsExportService.java deleted file mode 100644 index e28f8aaa79..0000000000 --- a/application/src/main/java/org/thingsboard/server/service/sync/ie/exporting/impl/BaseCalculatedFieldsExportService.java +++ /dev/null @@ -1,51 +0,0 @@ -/** - * 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.sync.ie.exporting.impl; - -import org.thingsboard.server.common.data.ExportableEntity; -import org.thingsboard.server.common.data.HasTenantId; -import org.thingsboard.server.common.data.cf.CalculatedField; -import org.thingsboard.server.common.data.id.EntityId; -import org.thingsboard.server.common.data.sync.ie.CalculatedFieldExportData; -import org.thingsboard.server.dao.cf.CalculatedFieldService; -import org.thingsboard.server.service.sync.vc.data.EntitiesExportCtx; - -import java.util.List; - -public abstract class BaseCalculatedFieldsExportService & HasTenantId, D extends CalculatedFieldExportData> extends BaseEntityExportService { - - protected final CalculatedFieldService calculatedFieldService; - - protected BaseCalculatedFieldsExportService(CalculatedFieldService calculatedFieldService) { - this.calculatedFieldService = calculatedFieldService; - } - - protected void setCalculatedFields(EntitiesExportCtx ctx, E entity, D exportData) { - if (ctx.getSettings().isExportCalculatedFields()) { - List calculatedFields = calculatedFieldService.findCalculatedFieldsByEntityId(ctx.getTenantId(), entity.getId()); - calculatedFields.forEach(calculatedField -> { - calculatedField.getConfiguration().getArguments().values().forEach(argument -> { - if (argument.getRefEntityId() != null) { - EntityId externalId = getExternalIdOrElseInternal(ctx, argument.getRefEntityId()); - argument.setRefEntityId(externalId); - } - }); - }); - exportData.setCalculatedFields(calculatedFields); - } - } - -} diff --git a/application/src/main/java/org/thingsboard/server/service/sync/ie/exporting/impl/DefaultEntityExportService.java b/application/src/main/java/org/thingsboard/server/service/sync/ie/exporting/impl/DefaultEntityExportService.java index b0bea68290..5c2fcc7dc6 100644 --- a/application/src/main/java/org/thingsboard/server/service/sync/ie/exporting/impl/DefaultEntityExportService.java +++ b/application/src/main/java/org/thingsboard/server/service/sync/ie/exporting/impl/DefaultEntityExportService.java @@ -23,6 +23,7 @@ import org.thingsboard.server.common.data.AttributeScope; import org.thingsboard.server.common.data.EntityType; import org.thingsboard.server.common.data.ExportableEntity; import org.thingsboard.server.common.data.HasVersion; +import org.thingsboard.server.common.data.cf.CalculatedField; import org.thingsboard.server.common.data.exception.ThingsboardException; import org.thingsboard.server.common.data.id.EntityId; import org.thingsboard.server.common.data.id.EntityIdFactory; @@ -31,6 +32,7 @@ import org.thingsboard.server.common.data.relation.RelationTypeGroup; import org.thingsboard.server.common.data.sync.ie.AttributeExportData; import org.thingsboard.server.common.data.sync.ie.EntityExportData; import org.thingsboard.server.dao.attributes.AttributesService; +import org.thingsboard.server.dao.cf.CalculatedFieldService; import org.thingsboard.server.dao.relation.RelationDao; import org.thingsboard.server.queue.util.TbCoreComponent; import org.thingsboard.server.service.sync.ie.exporting.EntityExportService; @@ -59,6 +61,8 @@ public class DefaultEntityExportService ctx, I entityId) throws ThingsboardException { @@ -98,6 +102,10 @@ public class DefaultEntityExportService> attributes = exportAttributes(ctx, entity); exportData.setAttributes(attributes); } + if (ctx.getSettings().isExportCalculatedFields()) { + List calculatedFields = exportCalculatedFields(ctx, entity.getId()); + exportData.setCalculatedFields(calculatedFields); + } } private List exportRelations(EntitiesExportCtx ctx, E entity) throws ThingsboardException { @@ -141,6 +149,19 @@ public class DefaultEntityExportService exportCalculatedFields(EntitiesExportCtx ctx, EntityId entityId) { + List calculatedFields = calculatedFieldService.findCalculatedFieldsByEntityId(ctx.getTenantId(), entityId); + calculatedFields.forEach(calculatedField -> { + calculatedField.setEntityId(getExternalIdOrElseInternal(ctx, entityId)); + calculatedField.getConfiguration().getArguments().values().forEach(argument -> { + if (argument.getRefEntityId() != null) { + argument.setRefEntityId(getExternalIdOrElseInternal(ctx, argument.getRefEntityId())); + } + }); + }); + return calculatedFields; + } + protected ID getExternalIdOrElseInternal(EntitiesExportCtx ctx, ID internalId) { if (internalId == null || internalId.isNullUid()) return internalId; var result = ctx.getExternalId(internalId); diff --git a/application/src/main/java/org/thingsboard/server/service/sync/ie/exporting/impl/DeviceExportService.java b/application/src/main/java/org/thingsboard/server/service/sync/ie/exporting/impl/DeviceExportService.java index f1b5ade487..7d5f7ee57e 100644 --- a/application/src/main/java/org/thingsboard/server/service/sync/ie/exporting/impl/DeviceExportService.java +++ b/application/src/main/java/org/thingsboard/server/service/sync/ie/exporting/impl/DeviceExportService.java @@ -15,12 +15,12 @@ */ package org.thingsboard.server.service.sync.ie.exporting.impl; +import lombok.RequiredArgsConstructor; import org.springframework.stereotype.Service; import org.thingsboard.server.common.data.Device; import org.thingsboard.server.common.data.EntityType; import org.thingsboard.server.common.data.id.DeviceId; import org.thingsboard.server.common.data.sync.ie.DeviceExportData; -import org.thingsboard.server.dao.cf.CalculatedFieldService; import org.thingsboard.server.dao.device.DeviceCredentialsService; import org.thingsboard.server.queue.util.TbCoreComponent; import org.thingsboard.server.service.sync.vc.data.EntitiesExportCtx; @@ -29,15 +29,11 @@ import java.util.Set; @Service @TbCoreComponent -public class DeviceExportService extends BaseCalculatedFieldsExportService { +@RequiredArgsConstructor +public class DeviceExportService extends BaseEntityExportService { private final DeviceCredentialsService deviceCredentialsService; - public DeviceExportService(CalculatedFieldService calculatedFieldService, DeviceCredentialsService deviceCredentialsService) { - super(calculatedFieldService); - this.deviceCredentialsService = deviceCredentialsService; - } - @Override protected void setRelatedEntities(EntitiesExportCtx ctx, Device device, DeviceExportData exportData) { device.setCustomerId(getExternalIdOrElseInternal(ctx, device.getCustomerId())); @@ -48,7 +44,6 @@ public class DeviceExportService extends BaseCalculatedFieldsExportService { - - protected DeviceProfileExportService(CalculatedFieldService calculatedFieldService) { - super(calculatedFieldService); - } +public class DeviceProfileExportService extends BaseEntityExportService> { @Override - protected void setRelatedEntities(EntitiesExportCtx ctx, DeviceProfile deviceProfile, DeviceProfileExportData exportData) { + protected void setRelatedEntities(EntitiesExportCtx ctx, DeviceProfile deviceProfile, EntityExportData exportData) { deviceProfile.setDefaultDashboardId(getExternalIdOrElseInternal(ctx, deviceProfile.getDefaultDashboardId())); deviceProfile.setDefaultRuleChainId(getExternalIdOrElseInternal(ctx, deviceProfile.getDefaultRuleChainId())); deviceProfile.setDefaultEdgeRuleChainId(getExternalIdOrElseInternal(ctx, deviceProfile.getDefaultEdgeRuleChainId())); - setCalculatedFields(ctx, deviceProfile, exportData); - } - - @Override - protected DeviceProfileExportData newExportData() { - return new DeviceProfileExportData(); } @Override diff --git a/application/src/main/java/org/thingsboard/server/service/sync/ie/importing/impl/AssetImportService.java b/application/src/main/java/org/thingsboard/server/service/sync/ie/importing/impl/AssetImportService.java index 0b0649072e..7cd4c3aca1 100644 --- a/application/src/main/java/org/thingsboard/server/service/sync/ie/importing/impl/AssetImportService.java +++ b/application/src/main/java/org/thingsboard/server/service/sync/ie/importing/impl/AssetImportService.java @@ -15,28 +15,24 @@ */ package org.thingsboard.server.service.sync.ie.importing.impl; +import lombok.RequiredArgsConstructor; import org.springframework.stereotype.Service; import org.thingsboard.server.common.data.EntityType; import org.thingsboard.server.common.data.asset.Asset; import org.thingsboard.server.common.data.id.AssetId; import org.thingsboard.server.common.data.id.TenantId; -import org.thingsboard.server.common.data.sync.ie.AssetExportData; +import org.thingsboard.server.common.data.sync.ie.EntityExportData; import org.thingsboard.server.dao.asset.AssetService; -import org.thingsboard.server.dao.cf.CalculatedFieldService; import org.thingsboard.server.queue.util.TbCoreComponent; import org.thingsboard.server.service.sync.vc.data.EntitiesImportCtx; @Service @TbCoreComponent -public class AssetImportService extends BaseCalculatedFieldsImportService { +@RequiredArgsConstructor +public class AssetImportService extends BaseEntityImportService> { private final AssetService assetService; - public AssetImportService(CalculatedFieldService calculatedFieldService, AssetService assetService) { - super(calculatedFieldService); - this.assetService = assetService; - } - @Override protected void setOwner(TenantId tenantId, Asset asset, IdProvider idProvider) { asset.setTenantId(tenantId); @@ -44,14 +40,18 @@ public class AssetImportService extends BaseCalculatedFieldsImportService exportData, IdProvider idProvider) { asset.setAssetProfileId(idProvider.getInternalId(asset.getAssetProfileId())); return asset; } @Override - protected Asset saveOrUpdate(EntitiesImportCtx ctx, Asset asset, AssetExportData exportData, IdProvider idProvider) { - return saveOrUpdateEntity(ctx, asset, exportData, idProvider, assetService::saveAsset); + protected Asset saveOrUpdate(EntitiesImportCtx ctx, Asset asset, EntityExportData exportData, IdProvider idProvider) { + Asset savedAsset = assetService.saveAsset(asset); + if (ctx.isFinalImportAttempt() || ctx.getCurrentImportResult().isUpdatedAllExternalIds()) { + importCalculatedFields(ctx, savedAsset, exportData, idProvider); + } + return savedAsset; } @Override diff --git a/application/src/main/java/org/thingsboard/server/service/sync/ie/importing/impl/AssetProfileImportService.java b/application/src/main/java/org/thingsboard/server/service/sync/ie/importing/impl/AssetProfileImportService.java index 6797c92c81..32a0090a4a 100644 --- a/application/src/main/java/org/thingsboard/server/service/sync/ie/importing/impl/AssetProfileImportService.java +++ b/application/src/main/java/org/thingsboard/server/service/sync/ie/importing/impl/AssetProfileImportService.java @@ -15,6 +15,7 @@ */ package org.thingsboard.server.service.sync.ie.importing.impl; +import lombok.RequiredArgsConstructor; import org.springframework.stereotype.Service; import org.thingsboard.server.common.data.EntityType; import org.thingsboard.server.common.data.User; @@ -22,30 +23,25 @@ import org.thingsboard.server.common.data.asset.AssetProfile; import org.thingsboard.server.common.data.audit.ActionType; import org.thingsboard.server.common.data.id.AssetProfileId; import org.thingsboard.server.common.data.id.TenantId; -import org.thingsboard.server.common.data.sync.ie.AssetProfileExportData; +import org.thingsboard.server.common.data.sync.ie.EntityExportData; import org.thingsboard.server.dao.asset.AssetProfileService; -import org.thingsboard.server.dao.cf.CalculatedFieldService; import org.thingsboard.server.queue.util.TbCoreComponent; import org.thingsboard.server.service.sync.vc.data.EntitiesImportCtx; @Service @TbCoreComponent -public class AssetProfileImportService extends BaseCalculatedFieldsImportService { +@RequiredArgsConstructor +public class AssetProfileImportService extends BaseEntityImportService> { private final AssetProfileService assetProfileService; - public AssetProfileImportService(CalculatedFieldService calculatedFieldService, AssetProfileService assetProfileService) { - super(calculatedFieldService); - this.assetProfileService = assetProfileService; - } - @Override protected void setOwner(TenantId tenantId, AssetProfile assetProfile, IdProvider idProvider) { assetProfile.setTenantId(tenantId); } @Override - protected AssetProfile prepare(EntitiesImportCtx ctx, AssetProfile assetProfile, AssetProfile old, AssetProfileExportData exportData, IdProvider idProvider) { + protected AssetProfile prepare(EntitiesImportCtx ctx, AssetProfile assetProfile, AssetProfile old, EntityExportData exportData, IdProvider idProvider) { assetProfile.setDefaultRuleChainId(idProvider.getInternalId(assetProfile.getDefaultRuleChainId())); assetProfile.setDefaultDashboardId(idProvider.getInternalId(assetProfile.getDefaultDashboardId())); assetProfile.setDefaultEdgeRuleChainId(idProvider.getInternalId(assetProfile.getDefaultEdgeRuleChainId())); @@ -53,8 +49,12 @@ public class AssetProfileImportService extends BaseCalculatedFieldsImportService } @Override - protected AssetProfile saveOrUpdate(EntitiesImportCtx ctx, AssetProfile assetProfile, AssetProfileExportData exportData, IdProvider idProvider) { - return saveOrUpdateEntity(ctx, assetProfile, exportData, idProvider, assetProfileService::saveAssetProfile); + protected AssetProfile saveOrUpdate(EntitiesImportCtx ctx, AssetProfile assetProfile, EntityExportData exportData, IdProvider idProvider) { + AssetProfile saved = assetProfileService.saveAssetProfile(assetProfile); + if (ctx.isFinalImportAttempt() || ctx.getCurrentImportResult().isUpdatedAllExternalIds()) { + importCalculatedFields(ctx, saved, exportData, idProvider); + } + return saved; } @Override diff --git a/application/src/main/java/org/thingsboard/server/service/sync/ie/importing/impl/BaseCalculatedFieldsImportService.java b/application/src/main/java/org/thingsboard/server/service/sync/ie/importing/impl/BaseCalculatedFieldsImportService.java deleted file mode 100644 index ffa51938f0..0000000000 --- a/application/src/main/java/org/thingsboard/server/service/sync/ie/importing/impl/BaseCalculatedFieldsImportService.java +++ /dev/null @@ -1,123 +0,0 @@ -/** - * 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.sync.ie.importing.impl; - -import org.thingsboard.server.common.data.ExportableEntity; -import org.thingsboard.server.common.data.HasTenantId; -import org.thingsboard.server.common.data.cf.CalculatedField; -import org.thingsboard.server.common.data.id.CalculatedFieldId; -import org.thingsboard.server.common.data.id.EntityId; -import org.thingsboard.server.common.data.sync.ie.CalculatedFieldExportData; -import org.thingsboard.server.dao.cf.CalculatedFieldService; -import org.thingsboard.server.service.sync.vc.data.EntitiesImportCtx; - -import java.util.LinkedHashMap; -import java.util.List; -import java.util.Map; -import java.util.function.Function; -import java.util.stream.Collectors; - -public abstract class BaseCalculatedFieldsImportService & HasTenantId, D extends CalculatedFieldExportData> extends BaseEntityImportService { - - private final CalculatedFieldService calculatedFieldService; - - protected BaseCalculatedFieldsImportService(CalculatedFieldService calculatedFieldService) { - this.calculatedFieldService = calculatedFieldService; - } - - protected E saveOrUpdateEntity(EntitiesImportCtx ctx, E entity, D exportData, IdProvider idProvider, Function saveFunction) { - E savedEntity = saveFunction.apply(entity); - - if (ctx.isFinalImportAttempt() || ctx.getCurrentImportResult().isUpdatedAllExternalIds()) { - saveCalculatedFields(ctx, savedEntity, exportData, idProvider); - } - return savedEntity; - } - - protected void saveCalculatedFields(EntitiesImportCtx ctx, E savedEntity, D exportData, IdProvider idProvider) { - if (exportData.getCalculatedFields() == null || !ctx.isSaveCalculatedFields()) { - return; - } - - exportData.getCalculatedFields().forEach(calculatedField -> { - calculatedField.setTenantId(savedEntity.getTenantId()); - calculatedField.setExternalId(calculatedField.getId()); - calculatedField.setId(idProvider.getInternalId(calculatedField.getId(), false)); - calculatedField.setEntityId(savedEntity.getId()); - - calculatedField.getConfiguration().getArguments().values().forEach(argument -> { - if (argument.getRefEntityId() != null) { - argument.setRefEntityId(idProvider.getInternalId(argument.getRefEntityId(), false)); - } - }); - - calculatedFieldService.save(calculatedField); - }); - } - - @Override - protected boolean updateRelatedEntitiesIfUnmodified(EntitiesImportCtx ctx, E prepared, D exportData, IdProvider idProvider) { - boolean updated = super.updateRelatedEntitiesIfUnmodified(ctx, prepared, exportData, idProvider); - updated |= updateCalculatedFields(ctx, prepared, exportData, idProvider); - return updated; - } - - private boolean updateCalculatedFields(EntitiesImportCtx ctx, E prepared, D exportData, IdProvider idProvider) { - var calculatedFields = exportData.getCalculatedFields(); - if (calculatedFields == null || !ctx.isSaveCalculatedFields()) { - return false; - } - Map calculatedFieldMap = calculatedFields.stream() - .peek(newField -> { - newField.setTenantId(ctx.getTenantId()); - newField.setExternalId(newField.getId()); - newField.setId(idProvider.getInternalId(newField.getId(), false)); - newField.setEntityId(prepared.getId()); - newField.getConfiguration().getArguments().values().forEach(argument -> { - argument.setRefEntityId(idProvider.getInternalId(argument.getRefEntityId(), false)); - }); - }) - .collect(Collectors.toMap(CalculatedField::getId, field -> field)); - - List existingFields = calculatedFieldService.findCalculatedFieldsByEntityId(ctx.getTenantId(), prepared.getId()); - boolean updated = false; - - Map result = new LinkedHashMap<>(); - for (CalculatedField existingField : existingFields) { - if (calculatedFieldMap.containsKey(existingField.getId())) { - CalculatedField newField = calculatedFieldMap.get(existingField.getId()); - if (!newField.equals(existingField)) { - result.put(newField, false); - } - calculatedFieldMap.remove(existingField.getId()); - } else { - updated = true; - calculatedFieldService.deleteCalculatedField(ctx.getTenantId(), existingField.getId()); - } - } - - for (CalculatedField newField : calculatedFieldMap.values()) { - result.put(newField, true); - } - - if (!result.isEmpty()) { - updated = true; - ctx.addCalculatedFields(result); - } - return updated; - } - -} diff --git a/application/src/main/java/org/thingsboard/server/service/sync/ie/importing/impl/BaseEntityImportService.java b/application/src/main/java/org/thingsboard/server/service/sync/ie/importing/impl/BaseEntityImportService.java index 7044b235a0..65f2b4f032 100644 --- a/application/src/main/java/org/thingsboard/server/service/sync/ie/importing/impl/BaseEntityImportService.java +++ b/application/src/main/java/org/thingsboard/server/service/sync/ie/importing/impl/BaseEntityImportService.java @@ -32,6 +32,7 @@ import org.thingsboard.server.common.data.HasDefaultOption; import org.thingsboard.server.common.data.HasVersion; import org.thingsboard.server.common.data.User; import org.thingsboard.server.common.data.audit.ActionType; +import org.thingsboard.server.common.data.cf.CalculatedField; import org.thingsboard.server.common.data.exception.ThingsboardException; import org.thingsboard.server.common.data.id.EntityId; import org.thingsboard.server.common.data.id.EntityIdFactory; @@ -50,6 +51,7 @@ import org.thingsboard.server.common.data.relation.RelationTypeGroup; import org.thingsboard.server.common.data.sync.ie.AttributeExportData; import org.thingsboard.server.common.data.sync.ie.EntityExportData; import org.thingsboard.server.common.data.sync.ie.EntityImportResult; +import org.thingsboard.server.dao.cf.CalculatedFieldService; import org.thingsboard.server.dao.relation.RelationDao; import org.thingsboard.server.dao.relation.RelationService; import org.thingsboard.server.service.action.EntityActionService; @@ -78,6 +80,8 @@ public abstract class BaseEntityImportService existing = calculatedFieldService.findCalculatedFieldsByEntityId(ctx.getTenantId(), savedEntity.getId()); + List fieldsToSave = exportData.getCalculatedFields().stream() + .peek(calculatedField -> { + calculatedField.setTenantId(ctx.getTenantId()); + calculatedField.setEntityId(savedEntity.getId()); + calculatedField.getConfiguration().getArguments().values().forEach(argument -> { + if (argument.getRefEntityId() != null) { + argument.setRefEntityId(idProvider.getInternalId(argument.getRefEntityId(), false)); + } + }); + }).toList(); + + for (CalculatedField existingField : existing) { + boolean found = fieldsToSave.stream().anyMatch(importedField -> compareCalculatedFields(existingField, importedField)); + if (!found) { + calculatedFieldService.deleteCalculatedField(ctx.getTenantId(), existingField.getId()); + updated = true; + } + } + + for (CalculatedField calculatedField : fieldsToSave) { + boolean found = existing.stream().anyMatch(existingField -> compareCalculatedFields(existingField, calculatedField)); + if (!found) { + calculatedFieldService.save(calculatedField); + updated = true; + } + } + return updated; + } + + private boolean compareCalculatedFields(CalculatedField existingField, CalculatedField newField) { + CalculatedField oldCopy = new CalculatedField(existingField); + CalculatedField newCopy = new CalculatedField(newField); + oldCopy.setId(null); + newCopy.setId(null); + oldCopy.setVersion(null); + newCopy.setVersion(null); + oldCopy.setCreatedTime(0); + newCopy.setCreatedTime(0); + return oldCopy.equals(newCopy); + } + protected void onEntitySaved(User user, E savedEntity, E oldEntity) throws ThingsboardException { logEntityActionService.logEntityAction(user.getTenantId(), savedEntity.getId(), savedEntity, null, oldEntity == null ? ActionType.ADDED : ActionType.UPDATED, user); } - @SuppressWarnings("unchecked") protected E findExistingEntity(EntitiesImportCtx ctx, E entity, IdProvider idProvider) { return (E) Optional.ofNullable(entitiesService.findEntityByTenantIdAndExternalId(ctx.getTenantId(), entity.getId())) @@ -313,10 +364,10 @@ public abstract class BaseEntityImportService new MissingEntityException(externalId)); } - @SuppressWarnings("unchecked") @RequiredArgsConstructor protected class IdProvider { + private final EntitiesImportCtx ctx; private final EntityImportResult importResult; diff --git a/application/src/main/java/org/thingsboard/server/service/sync/ie/importing/impl/DeviceImportService.java b/application/src/main/java/org/thingsboard/server/service/sync/ie/importing/impl/DeviceImportService.java index c8e99cdbc1..92bdb1e294 100644 --- a/application/src/main/java/org/thingsboard/server/service/sync/ie/importing/impl/DeviceImportService.java +++ b/application/src/main/java/org/thingsboard/server/service/sync/ie/importing/impl/DeviceImportService.java @@ -15,13 +15,13 @@ */ package org.thingsboard.server.service.sync.ie.importing.impl; +import lombok.RequiredArgsConstructor; import org.springframework.stereotype.Service; import org.thingsboard.server.common.data.Device; import org.thingsboard.server.common.data.EntityType; import org.thingsboard.server.common.data.id.DeviceId; import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.common.data.sync.ie.DeviceExportData; -import org.thingsboard.server.dao.cf.CalculatedFieldService; import org.thingsboard.server.dao.device.DeviceCredentialsService; import org.thingsboard.server.dao.device.DeviceService; import org.thingsboard.server.queue.util.TbCoreComponent; @@ -29,17 +29,12 @@ import org.thingsboard.server.service.sync.vc.data.EntitiesImportCtx; @Service @TbCoreComponent -public class DeviceImportService extends BaseCalculatedFieldsImportService { +@RequiredArgsConstructor +public class DeviceImportService extends BaseEntityImportService { private final DeviceService deviceService; private final DeviceCredentialsService credentialsService; - public DeviceImportService(CalculatedFieldService calculatedFieldService, DeviceService deviceService, DeviceCredentialsService credentialsService) { - super(calculatedFieldService); - this.deviceService = deviceService; - this.credentialsService = credentialsService; - } - @Override protected void setOwner(TenantId tenantId, Device device, IdProvider idProvider) { device.setTenantId(tenantId); @@ -78,7 +73,7 @@ public class DeviceImportService extends BaseCalculatedFieldsImportService { +@RequiredArgsConstructor +public class DeviceProfileImportService extends BaseEntityImportService> { private final DeviceProfileService deviceProfileService; - public DeviceProfileImportService(CalculatedFieldService calculatedFieldService, DeviceProfileService deviceProfileService) { - super(calculatedFieldService); - this.deviceProfileService = deviceProfileService; - } - @Override protected void setOwner(TenantId tenantId, DeviceProfile deviceProfile, IdProvider idProvider) { deviceProfile.setTenantId(tenantId); } @Override - protected DeviceProfile prepare(EntitiesImportCtx ctx, DeviceProfile deviceProfile, DeviceProfile old, DeviceProfileExportData exportData, IdProvider idProvider) { + protected DeviceProfile prepare(EntitiesImportCtx ctx, DeviceProfile deviceProfile, DeviceProfile old, EntityExportData exportData, IdProvider idProvider) { deviceProfile.setDefaultRuleChainId(idProvider.getInternalId(deviceProfile.getDefaultRuleChainId())); deviceProfile.setDefaultEdgeRuleChainId(idProvider.getInternalId(deviceProfile.getDefaultEdgeRuleChainId())); deviceProfile.setDefaultDashboardId(idProvider.getInternalId(deviceProfile.getDefaultDashboardId())); @@ -55,8 +51,12 @@ public class DeviceProfileImportService extends BaseCalculatedFieldsImportServic } @Override - protected DeviceProfile saveOrUpdate(EntitiesImportCtx ctx, DeviceProfile deviceProfile, DeviceProfileExportData exportData, IdProvider idProvider) { - return saveOrUpdateEntity(ctx, deviceProfile, exportData, idProvider, deviceProfileService::saveDeviceProfile); + protected DeviceProfile saveOrUpdate(EntitiesImportCtx ctx, DeviceProfile deviceProfile, EntityExportData exportData, IdProvider idProvider) { + DeviceProfile saved = deviceProfileService.saveDeviceProfile(deviceProfile); + if (ctx.isFinalImportAttempt() || ctx.getCurrentImportResult().isUpdatedAllExternalIds()) { + importCalculatedFields(ctx, saved, exportData, idProvider); + } + return saved; } @Override diff --git a/application/src/main/java/org/thingsboard/server/service/sync/vc/data/EntitiesImportCtx.java b/application/src/main/java/org/thingsboard/server/service/sync/vc/data/EntitiesImportCtx.java index c1a8dad161..726dd12102 100644 --- a/application/src/main/java/org/thingsboard/server/service/sync/vc/data/EntitiesImportCtx.java +++ b/application/src/main/java/org/thingsboard/server/service/sync/vc/data/EntitiesImportCtx.java @@ -56,7 +56,7 @@ public class EntitiesImportCtx { private final Set notFoundIds = new HashSet<>(); private final Set relations = new LinkedHashSet<>(); - private final Map calculatedFields = new LinkedHashMap<>(); + private final Set calculatedFields = new LinkedHashSet<>(); private boolean finalImportAttempt = false; private EntityImportSettings settings; @@ -127,8 +127,8 @@ public class EntitiesImportCtx { relations.addAll(values); } - public void addCalculatedFields(Map calculatedFieldMap) { - calculatedFields.putAll(calculatedFieldMap); + public void addCalculatedFields(Collection values) { + calculatedFields.addAll(values); } public void addReferenceCallback(EntityId externalId, ThrowingRunnable tr) { diff --git a/application/src/test/java/org/thingsboard/server/service/sync/ie/ExportImportServiceSqlTest.java b/application/src/test/java/org/thingsboard/server/service/sync/ie/ExportImportServiceSqlTest.java index 9184a3c034..a75fbba095 100644 --- a/application/src/test/java/org/thingsboard/server/service/sync/ie/ExportImportServiceSqlTest.java +++ b/application/src/test/java/org/thingsboard/server/service/sync/ie/ExportImportServiceSqlTest.java @@ -288,7 +288,6 @@ public class ExportImportServiceSqlTest extends AbstractControllerTest { assertThat(calculatedFields.size()).isOne(); var importedCalculatedField = calculatedFields.get(0); assertThat(importedCalculatedField.getName()).isEqualTo(calculatedField.getName()); - assertThat(importedCalculatedField.getExternalId()).isEqualTo(calculatedField.getId()); EntityExportData updatedDeviceEntity = getAndClone(entitiesExportData, EntityType.DEVICE); updatedDeviceEntity.getEntity().setLabel("t" + updatedDeviceEntity.getEntity().getLabel()); @@ -303,7 +302,6 @@ public class ExportImportServiceSqlTest extends AbstractControllerTest { calculatedFields = calculatedFieldService.findCalculatedFieldsByEntityId(tenantId2, importedDevice.getId()); assertThat(calculatedFields.size()).isOne(); importedCalculatedField = calculatedFields.get(0); - assertThat(importedCalculatedField.getExternalId()).isEqualTo(calculatedField.getId()); assertThat(importedCalculatedField.getName()).startsWith("t_"); } 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 453fea676f..d90ca60333 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 @@ -575,7 +575,7 @@ public class VersionControlTest extends AbstractControllerTest { public void testVcWithCalculatedFields_betweenTenants() throws Exception { Asset asset = createAsset(null, null, "Asset 1"); Device device = createDevice(null, null, "Device 1", "test1"); - CalculatedField calculatedField = createCalculatedField("CalculatedField1", device.getId(), asset.getId()); + createCalculatedField("CalculatedField1", device.getId(), asset.getId()); String versionId = createVersion("calculated fields of asset and device", EntityType.ASSET, EntityType.DEVICE, EntityType.DEVICE_PROFILE, EntityType.ASSET_PROFILE); loginTenant2(); @@ -588,13 +588,13 @@ public class VersionControlTest extends AbstractControllerTest { CalculatedField importedCalculatedField = findCalculatedFieldByEntityId(importedDevice.getId()); checkImportedEntity(tenantId1, device, tenantId2, importedDevice); checkImportedEntity(tenantId1, asset, tenantId2, importedAsset); - checkImportedEntity(tenantId1, calculatedField, tenantId2, importedCalculatedField); List importedCalculatedFields = findCalculatedFieldsByEntityId(importedDevice.getId()); assertThat(importedCalculatedFields).size().isOne(); assertThat(importedCalculatedFields.get(0)).satisfies(importedField -> { assertThat(importedField.getName()).isEqualTo(importedCalculatedField.getName()); assertThat(importedField.getType()).isEqualTo(importedCalculatedField.getType()); + assertThat(importedField.getId()).isNotEqualTo(importedCalculatedField.getId()); }); } @@ -606,7 +606,7 @@ public class VersionControlTest extends AbstractControllerTest { loadVersion(versionId, EntityType.ASSET); CalculatedField importedCalculatedField = findCalculatedFieldByEntityId(asset.getId()); - checkImportedEntity(tenantId1, calculatedField, tenantId1, importedCalculatedField); + assertThat(importedCalculatedField.getId()).isEqualTo(calculatedField.getId()); assertThat(importedCalculatedField.getName()).isEqualTo(calculatedField.getName()); assertThat(importedCalculatedField.getConfiguration()).isEqualTo(calculatedField.getConfiguration()); assertThat(importedCalculatedField.getType()).isEqualTo(calculatedField.getType()); 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 b45d7c6921..b86f30ca78 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 @@ -23,7 +23,6 @@ import lombok.EqualsAndHashCode; import lombok.Getter; import lombok.Setter; import org.thingsboard.server.common.data.BaseData; -import org.thingsboard.server.common.data.ExportableEntity; import org.thingsboard.server.common.data.HasDebugSettings; import org.thingsboard.server.common.data.HasName; import org.thingsboard.server.common.data.HasTenantId; @@ -42,7 +41,7 @@ import java.io.Serial; @Schema @Data @EqualsAndHashCode(callSuper = true) -public class CalculatedField extends BaseData implements HasName, HasTenantId, HasVersion, ExportableEntity, HasDebugSettings { +public class CalculatedField extends BaseData implements HasName, HasTenantId, HasVersion, HasDebugSettings { @Serial private static final long serialVersionUID = 4491966747773381420L; @@ -69,9 +68,6 @@ public class CalculatedField extends BaseData implements HasN @Getter @Setter private Long version; - @Getter - @Setter - private CalculatedFieldId externalId; public CalculatedField() { super(); @@ -81,21 +77,7 @@ public class CalculatedField extends BaseData implements HasN super(id); } - public CalculatedField(CalculatedField other) { - super(other); - this.tenantId = other.tenantId; - this.entityId = other.entityId; - this.type = other.type; - this.name = other.name; - this.configurationVersion = other.configurationVersion; - this.configuration = other.configuration; - this.version = other.version; - this.externalId = other.externalId; - this.debugMode = other.debugMode; - this.debugSettings = other.debugSettings; - } - - public CalculatedField(TenantId tenantId, EntityId entityId, CalculatedFieldType type, String name, int configurationVersion, CalculatedFieldConfiguration configuration, Long version, CalculatedFieldId externalId) { + public CalculatedField(TenantId tenantId, EntityId entityId, CalculatedFieldType type, String name, int configurationVersion, CalculatedFieldConfiguration configuration, Long version) { this.tenantId = tenantId; this.entityId = entityId; this.type = type; @@ -103,7 +85,19 @@ public class CalculatedField extends BaseData implements HasN this.configurationVersion = configurationVersion; this.configuration = configuration; this.version = version; - this.externalId = externalId; + } + + public CalculatedField(CalculatedField calculatedField) { + super(calculatedField); + this.tenantId = calculatedField.tenantId; + this.entityId = calculatedField.entityId; + this.type = calculatedField.type; + this.name = calculatedField.name; + this.debugMode = calculatedField.debugMode; + this.debugSettings = calculatedField.debugSettings; + this.configurationVersion = calculatedField.configurationVersion; + this.configuration = calculatedField.configuration; + this.version = calculatedField.version; } @Schema(description = "JSON object with the Calculated Field Id. Referencing non-existing Calculated Field Id will cause error.") @@ -129,7 +123,6 @@ public class CalculatedField extends BaseData implements HasN .append(", configurationVersion=").append(configurationVersion) .append(", configuration=").append(configuration) .append(", version=").append(version) - .append(", externalId=").append(externalId) .append(", createdTime=").append(createdTime) .append(", id=").append(id).append(']') .toString(); diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/id/CalculatedFieldId.java b/common/data/src/main/java/org/thingsboard/server/common/data/id/CalculatedFieldId.java index 0a83f1a19f..e17a066d88 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/id/CalculatedFieldId.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/id/CalculatedFieldId.java @@ -20,11 +20,13 @@ import com.fasterxml.jackson.annotation.JsonProperty; import io.swagger.v3.oas.annotations.media.Schema; import org.thingsboard.server.common.data.EntityType; +import java.io.Serial; import java.util.UUID; @Schema public class CalculatedFieldId extends UUIDBased implements EntityId { + @Serial private static final long serialVersionUID = 1L; @JsonCreator @@ -41,4 +43,5 @@ public class CalculatedFieldId extends UUIDBased implements EntityId { public EntityType getEntityType() { return EntityType.CALCULATED_FIELD; } + } diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/sync/JsonTbEntity.java b/common/data/src/main/java/org/thingsboard/server/common/data/sync/JsonTbEntity.java index 5dfe6ac8b2..c763daeb7f 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/sync/JsonTbEntity.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/sync/JsonTbEntity.java @@ -28,7 +28,6 @@ import org.thingsboard.server.common.data.EntityView; import org.thingsboard.server.common.data.TbResource; import org.thingsboard.server.common.data.asset.Asset; import org.thingsboard.server.common.data.asset.AssetProfile; -import org.thingsboard.server.common.data.cf.CalculatedField; import org.thingsboard.server.common.data.notification.rule.NotificationRule; import org.thingsboard.server.common.data.notification.targets.NotificationTarget; import org.thingsboard.server.common.data.notification.template.NotificationTemplate; @@ -59,8 +58,7 @@ import java.lang.annotation.Target; @Type(name = "NOTIFICATION_TEMPLATE", value = NotificationTemplate.class), @Type(name = "NOTIFICATION_TARGET", value = NotificationTarget.class), @Type(name = "NOTIFICATION_RULE", value = NotificationRule.class), - @Type(name = "TB_RESOURCE", value = TbResource.class), - @Type(name = "CALCULATED_FIELD", value = CalculatedField.class) + @Type(name = "TB_RESOURCE", value = TbResource.class) }) @JsonIgnoreProperties(value = {"tenantId", "createdTime", "version"}, ignoreUnknown = true) public @interface JsonTbEntity { diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/sync/ie/AssetExportData.java b/common/data/src/main/java/org/thingsboard/server/common/data/sync/ie/AssetExportData.java deleted file mode 100644 index 4bb836d5d9..0000000000 --- a/common/data/src/main/java/org/thingsboard/server/common/data/sync/ie/AssetExportData.java +++ /dev/null @@ -1,28 +0,0 @@ -/** - * 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.common.data.sync.ie; - -import lombok.Data; -import lombok.EqualsAndHashCode; -import lombok.ToString; -import org.thingsboard.server.common.data.asset.Asset; - -@EqualsAndHashCode(callSuper = true) -@ToString(callSuper = true) -@Data -public class AssetExportData extends CalculatedFieldExportData { - -} diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/sync/ie/AssetProfileExportData.java b/common/data/src/main/java/org/thingsboard/server/common/data/sync/ie/AssetProfileExportData.java deleted file mode 100644 index 42d9523828..0000000000 --- a/common/data/src/main/java/org/thingsboard/server/common/data/sync/ie/AssetProfileExportData.java +++ /dev/null @@ -1,28 +0,0 @@ -/** - * 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.common.data.sync.ie; - -import lombok.Data; -import lombok.EqualsAndHashCode; -import lombok.ToString; -import org.thingsboard.server.common.data.asset.AssetProfile; - -@EqualsAndHashCode(callSuper = true) -@ToString(callSuper = true) -@Data -public class AssetProfileExportData extends CalculatedFieldExportData { - -} diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/sync/ie/CalculatedFieldExportData.java b/common/data/src/main/java/org/thingsboard/server/common/data/sync/ie/CalculatedFieldExportData.java deleted file mode 100644 index 7741f88822..0000000000 --- a/common/data/src/main/java/org/thingsboard/server/common/data/sync/ie/CalculatedFieldExportData.java +++ /dev/null @@ -1,56 +0,0 @@ -/** - * 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.common.data.sync.ie; - -import com.fasterxml.jackson.annotation.JsonIgnore; -import com.fasterxml.jackson.annotation.JsonIgnoreProperties; -import com.fasterxml.jackson.annotation.JsonProperty; -import lombok.Data; -import lombok.EqualsAndHashCode; -import lombok.ToString; -import org.thingsboard.server.common.data.ExportableEntity; -import org.thingsboard.server.common.data.cf.CalculatedField; - -import java.util.Comparator; -import java.util.List; - -@Data -@EqualsAndHashCode(callSuper = true) -@ToString(callSuper = true) -public class CalculatedFieldExportData> extends EntityExportData { - - public static final Comparator calculatedFieldsComparator = Comparator.comparing(CalculatedField::getName); - - @JsonProperty(index = 102) - @JsonIgnoreProperties({"entityId", "createdTime", "version"}) - private List calculatedFields; - - @JsonIgnore - @Override - public boolean hasCalculatedFields() { - return calculatedFields != null; - } - - @Override - public CalculatedFieldExportData sort() { - super.sort(); - if (calculatedFields != null && !calculatedFields.isEmpty()) { - calculatedFields.sort(calculatedFieldsComparator); - } - return this; - } - -} diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/sync/ie/DeviceExportData.java b/common/data/src/main/java/org/thingsboard/server/common/data/sync/ie/DeviceExportData.java index a391067c74..98948ca642 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/sync/ie/DeviceExportData.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/sync/ie/DeviceExportData.java @@ -27,7 +27,7 @@ import org.thingsboard.server.common.data.security.DeviceCredentials; @EqualsAndHashCode(callSuper = true) @ToString(callSuper = true) @Data -public class DeviceExportData extends CalculatedFieldExportData { +public class DeviceExportData extends EntityExportData { @JsonProperty(index = 3) @JsonIgnoreProperties({"id", "deviceId", "createdTime", "version"}) diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/sync/ie/DeviceProfileExportData.java b/common/data/src/main/java/org/thingsboard/server/common/data/sync/ie/DeviceProfileExportData.java deleted file mode 100644 index b6b4d9bdcf..0000000000 --- a/common/data/src/main/java/org/thingsboard/server/common/data/sync/ie/DeviceProfileExportData.java +++ /dev/null @@ -1,28 +0,0 @@ -/** - * 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.common.data.sync.ie; - -import lombok.Data; -import lombok.EqualsAndHashCode; -import lombok.ToString; -import org.thingsboard.server.common.data.DeviceProfile; - -@EqualsAndHashCode(callSuper = true) -@ToString(callSuper = true) -@Data -public class DeviceProfileExportData extends CalculatedFieldExportData { - -} diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/sync/ie/EntityExportData.java b/common/data/src/main/java/org/thingsboard/server/common/data/sync/ie/EntityExportData.java index 5b7d95db1a..072be6acf2 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/sync/ie/EntityExportData.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/sync/ie/EntityExportData.java @@ -26,6 +26,7 @@ import com.fasterxml.jackson.annotation.JsonTypeInfo.As; import lombok.Data; import org.thingsboard.server.common.data.EntityType; import org.thingsboard.server.common.data.ExportableEntity; +import org.thingsboard.server.common.data.cf.CalculatedField; import org.thingsboard.server.common.data.id.EntityId; import org.thingsboard.server.common.data.relation.EntityRelation; import org.thingsboard.server.common.data.sync.JsonTbEntity; @@ -37,10 +38,7 @@ import java.util.Map; @JsonIgnoreProperties(ignoreUnknown = true) @JsonTypeInfo(use = JsonTypeInfo.Id.NAME, property = "entityType", include = As.EXISTING_PROPERTY, visible = true, defaultImpl = EntityExportData.class) @JsonSubTypes({ - @Type(name = "DEVICE_PROFILE", value = DeviceProfileExportData.class), - @Type(name = "ASSET_PROFILE", value = AssetProfileExportData.class), @Type(name = "DEVICE", value = DeviceExportData.class), - @Type(name = "ASSET", value = AssetExportData.class), @Type(name = "RULE_CHAIN", value = RuleChainExportData.class), @Type(name = "WIDGET_TYPE", value = WidgetTypeExportData.class), @Type(name = "WIDGETS_BUNDLE", value = WidgetsBundleExportData.class) @@ -58,6 +56,8 @@ public class EntityExportData> { public static final Comparator attrComparator = Comparator .comparing(AttributeExportData::getKey).thenComparing(AttributeExportData::getLastUpdateTs); + public static final Comparator calculatedFieldsComparator = Comparator.comparing(CalculatedField::getName); + @JsonProperty(index = 2) @JsonTbEntity private E entity; @@ -68,6 +68,9 @@ public class EntityExportData> { private List relations; @JsonProperty(index = 101) private Map> attributes; + @JsonProperty(index = 102) + @JsonIgnoreProperties({"id", "entityId", "createdTime", "version"}) + private List calculatedFields; public EntityExportData sort() { if (relations != null && !relations.isEmpty()) { @@ -76,6 +79,9 @@ public class EntityExportData> { if (attributes != null && !attributes.isEmpty()) { attributes.values().forEach(list -> list.sort(attrComparator)); } + if (calculatedFields != null && !calculatedFields.isEmpty()) { + calculatedFields.sort(calculatedFieldsComparator); + } return this; } @@ -101,7 +107,7 @@ public class EntityExportData> { @JsonIgnore public boolean hasCalculatedFields() { - return false; + return calculatedFields != null; } } diff --git a/dao/src/main/java/org/thingsboard/server/dao/cf/CalculatedFieldDao.java b/dao/src/main/java/org/thingsboard/server/dao/cf/CalculatedFieldDao.java index 3f04ab6c67..a966977968 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/cf/CalculatedFieldDao.java +++ b/dao/src/main/java/org/thingsboard/server/dao/cf/CalculatedFieldDao.java @@ -22,11 +22,10 @@ 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.dao.Dao; -import org.thingsboard.server.dao.ExportableEntityDao; import java.util.List; -public interface CalculatedFieldDao extends Dao, ExportableEntityDao { +public interface CalculatedFieldDao extends Dao { List findAllByTenantId(TenantId tenantId); diff --git a/dao/src/main/java/org/thingsboard/server/dao/model/ModelConstants.java b/dao/src/main/java/org/thingsboard/server/dao/model/ModelConstants.java index e2b21ed59b..148908d063 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/model/ModelConstants.java +++ b/dao/src/main/java/org/thingsboard/server/dao/model/ModelConstants.java @@ -729,7 +729,6 @@ public class ModelConstants { public static final String CALCULATED_FIELD_CONFIGURATION_VERSION = "configuration_version"; public static final String CALCULATED_FIELD_CONFIGURATION = "configuration"; public static final String CALCULATED_FIELD_VERSION = "version"; - public static final String CALCULATED_FIELD_EXTERNAL_ID = "external_id"; /** * Calculated field links constants. diff --git a/dao/src/main/java/org/thingsboard/server/dao/model/sql/CalculatedFieldEntity.java b/dao/src/main/java/org/thingsboard/server/dao/model/sql/CalculatedFieldEntity.java index 349091a1ae..de6a1365b1 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/model/sql/CalculatedFieldEntity.java +++ b/dao/src/main/java/org/thingsboard/server/dao/model/sql/CalculatedFieldEntity.java @@ -40,7 +40,6 @@ import static org.thingsboard.server.dao.model.ModelConstants.CALCULATED_FIELD_C import static org.thingsboard.server.dao.model.ModelConstants.CALCULATED_FIELD_CONFIGURATION_VERSION; import static org.thingsboard.server.dao.model.ModelConstants.CALCULATED_FIELD_ENTITY_ID; import static org.thingsboard.server.dao.model.ModelConstants.CALCULATED_FIELD_ENTITY_TYPE; -import static org.thingsboard.server.dao.model.ModelConstants.CALCULATED_FIELD_EXTERNAL_ID; import static org.thingsboard.server.dao.model.ModelConstants.CALCULATED_FIELD_NAME; import static org.thingsboard.server.dao.model.ModelConstants.CALCULATED_FIELD_TABLE_NAME; import static org.thingsboard.server.dao.model.ModelConstants.CALCULATED_FIELD_TENANT_ID_COLUMN; @@ -82,9 +81,6 @@ public class CalculatedFieldEntity extends BaseVersionedEntity @Column(name = DEBUG_SETTINGS) private String debugSettings; - @Column(name = CALCULATED_FIELD_EXTERNAL_ID) - private UUID externalId; - public CalculatedFieldEntity() { super(); } @@ -101,9 +97,6 @@ public class CalculatedFieldEntity extends BaseVersionedEntity this.configuration = JacksonUtil.valueToTree(calculatedField.getConfiguration()); this.version = calculatedField.getVersion(); this.debugSettings = JacksonUtil.toString(calculatedField.getDebugSettings()); - if (calculatedField.getExternalId() != null) { - this.externalId = calculatedField.getExternalId().getId(); - } } @Override @@ -118,9 +111,6 @@ public class CalculatedFieldEntity extends BaseVersionedEntity calculatedField.setConfiguration(JacksonUtil.treeToValue(configuration, CalculatedFieldConfiguration.class)); calculatedField.setVersion(version); calculatedField.setDebugSettings(JacksonUtil.fromString(debugSettings, DebugSettings.class)); - if (externalId != null) { - calculatedField.setExternalId(new CalculatedFieldId(externalId)); - } return calculatedField; } diff --git a/dao/src/main/java/org/thingsboard/server/dao/sql/cf/CalculatedFieldRepository.java b/dao/src/main/java/org/thingsboard/server/dao/sql/cf/CalculatedFieldRepository.java index e7e41ed2d0..0f48f3b00d 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/sql/cf/CalculatedFieldRepository.java +++ b/dao/src/main/java/org/thingsboard/server/dao/sql/cf/CalculatedFieldRepository.java @@ -18,16 +18,13 @@ package org.thingsboard.server.dao.sql.cf; import org.springframework.data.domain.Page; import org.springframework.data.domain.Pageable; import org.springframework.data.jpa.repository.JpaRepository; -import org.springframework.stereotype.Repository; import org.thingsboard.server.common.data.id.CalculatedFieldId; -import org.thingsboard.server.dao.ExportableEntityRepository; import org.thingsboard.server.dao.model.sql.CalculatedFieldEntity; import java.util.List; import java.util.UUID; -@Repository -public interface CalculatedFieldRepository extends JpaRepository, ExportableEntityRepository { +public interface CalculatedFieldRepository extends JpaRepository { boolean existsByTenantIdAndEntityId(UUID tenantId, UUID entityId); @@ -37,16 +34,10 @@ public interface CalculatedFieldRepository extends JpaRepository findAllByTenantIdAndEntityId(UUID tenantId, UUID entityId, Pageable pageable); - Page findByTenantId(UUID tenantId, Pageable pageable); - List findAllByTenantId(UUID tenantId); List removeAllByTenantIdAndEntityId(UUID tenantId, UUID entityId); long countByTenantIdAndEntityId(UUID tenantId, UUID entityId); - CalculatedFieldId findExternalIdById(UUID id); - - CalculatedFieldEntity findByTenantIdAndName(UUID tenantId, String name); - } diff --git a/dao/src/main/java/org/thingsboard/server/dao/sql/cf/DefaultNativeCalculatedFieldRepository.java b/dao/src/main/java/org/thingsboard/server/dao/sql/cf/DefaultNativeCalculatedFieldRepository.java index fae3468f1e..e59ff3f4e6 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/sql/cf/DefaultNativeCalculatedFieldRepository.java +++ b/dao/src/main/java/org/thingsboard/server/dao/sql/cf/DefaultNativeCalculatedFieldRepository.java @@ -92,7 +92,6 @@ public class DefaultNativeCalculatedFieldRepository implements NativeCalculatedF calculatedField.setConfiguration(JacksonUtil.treeToValue(configuration, CalculatedFieldConfiguration.class)); calculatedField.setVersion(version); calculatedField.setDebugSettings(JacksonUtil.fromString(debugSettings, DebugSettings.class)); - calculatedField.setExternalId(externalIdObj != null ? new CalculatedFieldId(UUID.fromString((String) externalIdObj)) : null); return calculatedField; }).collect(Collectors.toList()); diff --git a/dao/src/main/java/org/thingsboard/server/dao/sql/cf/JpaCalculatedFieldDao.java b/dao/src/main/java/org/thingsboard/server/dao/sql/cf/JpaCalculatedFieldDao.java index 01e7888fbf..8922eaca4e 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/sql/cf/JpaCalculatedFieldDao.java +++ b/dao/src/main/java/org/thingsboard/server/dao/sql/cf/JpaCalculatedFieldDao.java @@ -88,26 +88,6 @@ public class JpaCalculatedFieldDao extends JpaAbstractDao findByTenantId(UUID tenantId, PageLink pageLink) { - return DaoUtil.toPageData(calculatedFieldRepository.findByTenantId(tenantId, DaoUtil.toPageable(pageLink))); - } - - @Override - public CalculatedField findByTenantIdAndName(UUID tenantId, String name) { - return DaoUtil.getData(calculatedFieldRepository.findByTenantIdAndName(tenantId, name)); - } - - @Override - public CalculatedField findByTenantIdAndExternalId(UUID tenantId, UUID externalId) { - return DaoUtil.getData(calculatedFieldRepository.findByTenantIdAndExternalId(tenantId, externalId)); - } - - @Override - public CalculatedFieldId getExternalIdByInternal(CalculatedFieldId internalId) { - return calculatedFieldRepository.findExternalIdById(internalId.getId()); - } - @Override protected Class getEntityClass() { return CalculatedFieldEntity.class; diff --git a/dao/src/main/resources/sql/schema-entities.sql b/dao/src/main/resources/sql/schema-entities.sql index 5bd34c4f1d..d158912690 100644 --- a/dao/src/main/resources/sql/schema-entities.sql +++ b/dao/src/main/resources/sql/schema-entities.sql @@ -936,9 +936,7 @@ CREATE TABLE IF NOT EXISTS calculated_field ( configuration varchar(1000000), version BIGINT DEFAULT 1, debug_settings varchar(1024), - external_id UUID, - CONSTRAINT calculated_field_unq_key UNIQUE (entity_id, name), - CONSTRAINT calculated_field_external_id_unq_key UNIQUE (tenant_id, external_id) + CONSTRAINT calculated_field_unq_key UNIQUE (entity_id, name) ); CREATE TABLE IF NOT EXISTS calculated_field_link ( diff --git a/dao/src/test/java/org/thingsboard/server/dao/service/CalculatedFieldServiceTest.java b/dao/src/test/java/org/thingsboard/server/dao/service/CalculatedFieldServiceTest.java index 5a783355d7..2985aa7620 100644 --- a/dao/src/test/java/org/thingsboard/server/dao/service/CalculatedFieldServiceTest.java +++ b/dao/src/test/java/org/thingsboard/server/dao/service/CalculatedFieldServiceTest.java @@ -101,20 +101,6 @@ public class CalculatedFieldServiceTest extends AbstractServiceTest { .hasMessage("Calculated Field with such name is already in exists!"); } - @Test - public void testSaveCalculatedFieldWithExistingExternalId() { - Device device = createTestDevice(); - CalculatedField calculatedField = getCalculatedField(device.getId(), device.getId()); - calculatedField.setExternalId(new CalculatedFieldId(UUID.fromString("2ef69d0a-89cf-4868-86f8-c50551d87ebe"))); - - calculatedFieldService.save(calculatedField); - - calculatedField.setName("Test 2"); - assertThatThrownBy(() -> calculatedFieldService.save(calculatedField)) - .isInstanceOf(DataValidationException.class) - .hasMessage("Calculated Field with such external id already exists!"); - } - @Test public void testFindCalculatedFieldById() { CalculatedField savedCalculatedField = saveValidCalculatedField(); From 9f2d399f4ca35fe125cd60b3abcdf2e99b1c6524 Mon Sep 17 00:00:00 2001 From: Andrii Landiak Date: Mon, 3 Mar 2025 17:00:49 +0200 Subject: [PATCH 159/345] Revert AssetExportService --- .../service/sync/ie/exporting/impl/AssetExportService.java | 7 ------- 1 file changed, 7 deletions(-) diff --git a/application/src/main/java/org/thingsboard/server/service/sync/ie/exporting/impl/AssetExportService.java b/application/src/main/java/org/thingsboard/server/service/sync/ie/exporting/impl/AssetExportService.java index fbda92b6be..56d5bb0934 100644 --- a/application/src/main/java/org/thingsboard/server/service/sync/ie/exporting/impl/AssetExportService.java +++ b/application/src/main/java/org/thingsboard/server/service/sync/ie/exporting/impl/AssetExportService.java @@ -15,7 +15,6 @@ */ package org.thingsboard.server.service.sync.ie.exporting.impl; -import lombok.RequiredArgsConstructor; import org.springframework.stereotype.Service; import org.thingsboard.server.common.data.EntityType; import org.thingsboard.server.common.data.asset.Asset; @@ -28,7 +27,6 @@ import java.util.Set; @Service @TbCoreComponent -@RequiredArgsConstructor public class AssetExportService extends BaseEntityExportService> { @Override @@ -37,11 +35,6 @@ public class AssetExportService extends BaseEntityExportService newExportData() { - return new EntityExportData<>(); - } - @Override public Set getSupportedEntityTypes() { return Set.of(EntityType.ASSET); From f318fa0ebdd4b0a86a9751bfc32edcc94905d231 Mon Sep 17 00:00:00 2001 From: Dmytro Skarzhynets Date: Mon, 3 Mar 2025 17:23:01 +0200 Subject: [PATCH 160/345] Save attributes strategies: handle attributes deleted subscription notification in DefaultTelemetrySubscriptionService --- .../main/data/upgrade/basic/schema_update.sql | 106 +++++------- .../queue/DefaultTbCoreConsumerService.java | 2 +- .../DefaultSubscriptionManagerService.java | 14 +- .../SubscriptionManagerService.java | 4 +- .../subscription/TbSubscriptionUtils.java | 3 +- .../DefaultTelemetrySubscriptionService.java | 36 +++-- ...faultTelemetrySubscriptionServiceTest.java | 152 +++++++++++++++++- common/proto/src/main/proto/queue.proto | 3 +- .../engine/api/AttributesDeleteRequest.java | 10 +- .../api/AttributesDeleteRequestTest.java | 39 +++++ 10 files changed, 268 insertions(+), 101 deletions(-) create mode 100644 rule-engine/rule-engine-api/src/test/java/org/thingsboard/rule/engine/api/AttributesDeleteRequestTest.java diff --git a/application/src/main/data/upgrade/basic/schema_update.sql b/application/src/main/data/upgrade/basic/schema_update.sql index 215ac5248d..04dce447c5 100644 --- a/application/src/main/data/upgrade/basic/schema_update.sql +++ b/application/src/main/data/upgrade/basic/schema_update.sql @@ -16,78 +16,52 @@ -- UPDATE SAVE TIME SERIES NODES START -DO $$ - BEGIN - -- Check if the rule_node table exists - IF EXISTS ( - SELECT 1 - FROM information_schema.tables - WHERE table_name = 'rule_node' - ) THEN +UPDATE rule_node +SET configuration = ( + (configuration::jsonb - 'skipLatestPersistence') + || jsonb_build_object( + 'processingSettings', jsonb_build_object( + 'type', 'ADVANCED', + 'timeseries', jsonb_build_object('type', 'ON_EVERY_MESSAGE'), + 'latest', jsonb_build_object('type', 'SKIP'), + 'webSockets', jsonb_build_object('type', 'ON_EVERY_MESSAGE'), + 'calculatedFields', jsonb_build_object('type', 'ON_EVERY_MESSAGE') + ) + ) + )::text, + configuration_version = 1 +WHERE type = 'org.thingsboard.rule.engine.telemetry.TbMsgTimeseriesNode' + AND configuration_version = 0 + AND configuration::jsonb ->> 'skipLatestPersistence' = 'true'; - UPDATE rule_node - SET configuration = ( - (configuration::jsonb - 'skipLatestPersistence') - || jsonb_build_object( - 'processingSettings', jsonb_build_object( - 'type', 'ADVANCED', - 'timeseries', jsonb_build_object('type', 'ON_EVERY_MESSAGE'), - 'latest', jsonb_build_object('type', 'SKIP'), - 'webSockets', jsonb_build_object('type', 'ON_EVERY_MESSAGE'), - 'calculatedFields', jsonb_build_object('type', 'ON_EVERY_MESSAGE') - ) - ) - )::text, - configuration_version = 1 - WHERE type = 'org.thingsboard.rule.engine.telemetry.TbMsgTimeseriesNode' - AND configuration_version = 0 - AND configuration::jsonb ->> 'skipLatestPersistence' = 'true'; - - UPDATE rule_node - SET configuration = ( - (configuration::jsonb - 'skipLatestPersistence') - || jsonb_build_object( - 'processingSettings', jsonb_build_object( - 'type', 'ON_EVERY_MESSAGE' - ) - ) - )::text, - configuration_version = 1 - WHERE type = 'org.thingsboard.rule.engine.telemetry.TbMsgTimeseriesNode' - AND configuration_version = 0 - AND (configuration::jsonb ->> 'skipLatestPersistence' != 'true' OR configuration::jsonb ->> 'skipLatestPersistence' IS NULL); - - END IF; - END; -$$; +UPDATE rule_node +SET configuration = ( + (configuration::jsonb - 'skipLatestPersistence') + || jsonb_build_object( + 'processingSettings', jsonb_build_object( + 'type', 'ON_EVERY_MESSAGE' + ) + ) + )::text, + configuration_version = 1 +WHERE type = 'org.thingsboard.rule.engine.telemetry.TbMsgTimeseriesNode' + AND configuration_version = 0 + AND (configuration::jsonb ->> 'skipLatestPersistence' != 'true' OR configuration::jsonb ->> 'skipLatestPersistence' IS NULL); -- UPDATE SAVE TIME SERIES NODES END -- UPDATE SAVE ATTRIBUTES NODES START -DO $$ - BEGIN - -- Check if the rule_node table exists - IF EXISTS ( - SELECT 1 - FROM information_schema.tables - WHERE table_name = 'rule_node' - ) THEN - - UPDATE rule_node - SET configuration = ( - configuration::jsonb - || jsonb_build_object( - 'processingSettings', jsonb_build_object('type', 'ON_EVERY_MESSAGE') - ) - )::text, - configuration_version = 3 - WHERE type = 'org.thingsboard.rule.engine.telemetry.TbMsgAttributesNode' - AND configuration_version = 2; - - END IF; - END; -$$; +UPDATE rule_node +SET configuration = ( + configuration::jsonb + || jsonb_build_object( + 'processingSettings', jsonb_build_object('type', 'ON_EVERY_MESSAGE') + ) + )::text, + configuration_version = 3 +WHERE type = 'org.thingsboard.rule.engine.telemetry.TbMsgAttributesNode' + AND configuration_version = 2; -- UPDATE SAVE ATTRIBUTES NODES END diff --git a/application/src/main/java/org/thingsboard/server/service/queue/DefaultTbCoreConsumerService.java b/application/src/main/java/org/thingsboard/server/service/queue/DefaultTbCoreConsumerService.java index 7219179b2c..0152e34d50 100644 --- a/application/src/main/java/org/thingsboard/server/service/queue/DefaultTbCoreConsumerService.java +++ b/application/src/main/java/org/thingsboard/server/service/queue/DefaultTbCoreConsumerService.java @@ -545,7 +545,7 @@ public class DefaultTbCoreConsumerService extends AbstractConsumerService attributes, TbCallback callback) { - onAttributesUpdate(tenantId, entityId, scope, attributes, true, callback); - } - - @Override - public void onAttributesUpdate(TenantId tenantId, EntityId entityId, String scope, List attributes, boolean notifyDevice, TbCallback callback) { getEntityUpdatesInfo(entityId).attributesUpdateTs = System.currentTimeMillis(); processAttributesUpdate(entityId, scope, attributes); if (entityId.getEntityType() == EntityType.DEVICE) { @@ -223,16 +214,13 @@ public class DefaultSubscriptionManagerService extends TbApplicationEventListene } @Override - public void onAttributesDelete(TenantId tenantId, EntityId entityId, String scope, List keys, boolean notifyDevice, TbCallback callback) { + public void onAttributesDelete(TenantId tenantId, EntityId entityId, String scope, List keys, TbCallback callback) { processAttributesUpdate(entityId, scope, keys.stream().map(key -> new BaseAttributeKvEntry(0, new StringDataEntry(key, ""))).collect(Collectors.toList())); if (entityId.getEntityType() == EntityType.DEVICE) { if (TbAttributeSubscriptionScope.SERVER_SCOPE.name().equalsIgnoreCase(scope) || TbAttributeSubscriptionScope.ANY_SCOPE.name().equalsIgnoreCase(scope)) { deleteDeviceInactivityTimeout(tenantId, entityId, keys); - } else if (TbAttributeSubscriptionScope.SHARED_SCOPE.name().equalsIgnoreCase(scope) && notifyDevice) { - clusterService.pushMsgToCore(DeviceAttributesEventNotificationMsg.onDelete(tenantId, - new DeviceId(entityId.getId()), scope, keys), null); } } callback.onSuccess(); diff --git a/application/src/main/java/org/thingsboard/server/service/subscription/SubscriptionManagerService.java b/application/src/main/java/org/thingsboard/server/service/subscription/SubscriptionManagerService.java index d199f18b75..3fd58a1243 100644 --- a/application/src/main/java/org/thingsboard/server/service/subscription/SubscriptionManagerService.java +++ b/application/src/main/java/org/thingsboard/server/service/subscription/SubscriptionManagerService.java @@ -39,9 +39,7 @@ public interface SubscriptionManagerService extends ApplicationListener attributes, TbCallback callback); - void onAttributesUpdate(TenantId tenantId, EntityId entityId, String scope, List attributes, boolean notifyDevice, TbCallback callback); - - void onAttributesDelete(TenantId tenantId, EntityId entityId, String scope, List keys, boolean notifyDevice, TbCallback empty); + void onAttributesDelete(TenantId tenantId, EntityId entityId, String scope, List keys, TbCallback empty); void onTimeSeriesDelete(TenantId tenantId, EntityId entityId, List keys, TbCallback callback); diff --git a/application/src/main/java/org/thingsboard/server/service/subscription/TbSubscriptionUtils.java b/application/src/main/java/org/thingsboard/server/service/subscription/TbSubscriptionUtils.java index 68c0f52f71..1d5e85cc22 100644 --- a/application/src/main/java/org/thingsboard/server/service/subscription/TbSubscriptionUtils.java +++ b/application/src/main/java/org/thingsboard/server/service/subscription/TbSubscriptionUtils.java @@ -209,7 +209,7 @@ public class TbSubscriptionUtils { return ToCoreMsg.newBuilder().setToSubscriptionMgrMsg(msgBuilder.build()).build(); } - public static ToCoreMsg toAttributesDeleteProto(TenantId tenantId, EntityId entityId, String scope, List keys, boolean notifyDevice) { + public static ToCoreMsg toAttributesDeleteProto(TenantId tenantId, EntityId entityId, String scope, List keys) { TbAttributeDeleteProto.Builder builder = TbAttributeDeleteProto.newBuilder(); builder.setEntityType(entityId.getEntityType().name()); builder.setEntityIdMSB(entityId.getId().getMostSignificantBits()); @@ -218,7 +218,6 @@ public class TbSubscriptionUtils { builder.setTenantIdLSB(tenantId.getId().getLeastSignificantBits()); builder.setScope(scope); builder.addAllKeys(keys); - builder.setNotifyDevice(notifyDevice); SubscriptionMgrMsgProto.Builder msgBuilder = SubscriptionMgrMsgProto.newBuilder(); msgBuilder.setAttrDelete(builder); diff --git a/application/src/main/java/org/thingsboard/server/service/telemetry/DefaultTelemetrySubscriptionService.java b/application/src/main/java/org/thingsboard/server/service/telemetry/DefaultTelemetrySubscriptionService.java index 3af2c04643..252f696c72 100644 --- a/application/src/main/java/org/thingsboard/server/service/telemetry/DefaultTelemetrySubscriptionService.java +++ b/application/src/main/java/org/thingsboard/server/service/telemetry/DefaultTelemetrySubscriptionService.java @@ -211,7 +211,7 @@ public class DefaultTelemetrySubscriptionService extends AbstractSubscriptionSer } if (strategy.sendWsUpdate()) { - addWsCallback(resultFuture, success -> onAttributesUpdate(tenantId, entityId, request.getScope().name(), request.getEntries(), request.isNotifyDevice())); + addWsCallback(resultFuture, success -> onAttributesUpdate(tenantId, entityId, request.getScope().name(), request.getEntries())); } } @@ -223,11 +223,25 @@ public class DefaultTelemetrySubscriptionService extends AbstractSubscriptionSer @Override public void deleteAttributesInternal(AttributesDeleteRequest request) { - ListenableFuture> deleteFuture = attrService.removeAll(request.getTenantId(), request.getEntityId(), request.getScope(), request.getKeys()); - DonAsynchron.withCallback(deleteFuture, result -> { - calculatedFieldQueueService.pushRequestToQueue(request, result, request.getCallback()); - }, safeCallback(request.getCallback()), tsCallBackExecutor); - addWsCallback(deleteFuture, success -> onAttributesDelete(request.getTenantId(), request.getEntityId(), request.getScope().name(), request.getKeys(), request.isNotifyDevice())); + TenantId tenantId = request.getTenantId(); + EntityId entityId = request.getEntityId(); + + ListenableFuture> deleteFuture = attrService.removeAll(tenantId, entityId, request.getScope(), request.getKeys()); + + addMainCallback(deleteFuture, + result -> calculatedFieldQueueService.pushRequestToQueue(request, result, request.getCallback()), + t -> request.getCallback().onFailure(t) + ); + + if (entityId.getEntityType() == EntityType.DEVICE + && TbAttributeSubscriptionScope.SHARED_SCOPE.name().equalsIgnoreCase(request.getScope().name()) + && request.isNotifyDevice()) { + addMainCallback(deleteFuture, success -> clusterService.pushMsgToCore( + DeviceAttributesEventNotificationMsg.onDelete(tenantId, new DeviceId(entityId.getId()), DataConstants.SHARED_SCOPE, request.getKeys()), null + )); + } + + addWsCallback(deleteFuture, success -> onAttributesDelete(tenantId, entityId, request.getScope().name(), request.getKeys())); } @Override @@ -312,16 +326,16 @@ public class DefaultTelemetrySubscriptionService extends AbstractSubscriptionSer } } - private void onAttributesUpdate(TenantId tenantId, EntityId entityId, String scope, List attributes, boolean notifyDevice) { + private void onAttributesUpdate(TenantId tenantId, EntityId entityId, String scope, List attributes) { forwardToSubscriptionManagerService(tenantId, entityId, - subscriptionManagerService -> subscriptionManagerService.onAttributesUpdate(tenantId, entityId, scope, attributes, notifyDevice, TbCallback.EMPTY), + subscriptionManagerService -> subscriptionManagerService.onAttributesUpdate(tenantId, entityId, scope, attributes, TbCallback.EMPTY), () -> TbSubscriptionUtils.toAttributesUpdateProto(tenantId, entityId, scope, attributes)); } - private void onAttributesDelete(TenantId tenantId, EntityId entityId, String scope, List keys, boolean notifyDevice) { + private void onAttributesDelete(TenantId tenantId, EntityId entityId, String scope, List keys) { forwardToSubscriptionManagerService(tenantId, entityId, - subscriptionManagerService -> subscriptionManagerService.onAttributesDelete(tenantId, entityId, scope, keys, notifyDevice, TbCallback.EMPTY), - () -> TbSubscriptionUtils.toAttributesDeleteProto(tenantId, entityId, scope, keys, notifyDevice)); + subscriptionManagerService -> subscriptionManagerService.onAttributesDelete(tenantId, entityId, scope, keys, TbCallback.EMPTY), + () -> TbSubscriptionUtils.toAttributesDeleteProto(tenantId, entityId, scope, keys)); } private void onTimeSeriesUpdate(TenantId tenantId, EntityId entityId, List ts) { diff --git a/application/src/test/java/org/thingsboard/server/service/telemetry/DefaultTelemetrySubscriptionServiceTest.java b/application/src/test/java/org/thingsboard/server/service/telemetry/DefaultTelemetrySubscriptionServiceTest.java index 8bdcbc1349..44b7f146a5 100644 --- a/application/src/test/java/org/thingsboard/server/service/telemetry/DefaultTelemetrySubscriptionServiceTest.java +++ b/application/src/test/java/org/thingsboard/server/service/telemetry/DefaultTelemetrySubscriptionServiceTest.java @@ -29,6 +29,7 @@ import org.junit.jupiter.params.provider.MethodSource; import org.mockito.Mock; import org.mockito.junit.jupiter.MockitoExtension; import org.springframework.test.util.ReflectionTestUtils; +import org.thingsboard.rule.engine.api.AttributesDeleteRequest; import org.thingsboard.rule.engine.api.AttributesSaveRequest; import org.thingsboard.rule.engine.api.TimeseriesSaveRequest; import org.thingsboard.server.cluster.TbClusterService; @@ -421,7 +422,7 @@ class DefaultTelemetrySubscriptionServiceTest { } if (sendWsUpdate) { - then(subscriptionManagerService).should().onAttributesUpdate(tenantId, entityId, request.getScope().name(), request.getEntries(), request.isNotifyDevice(), TbCallback.EMPTY); + then(subscriptionManagerService).should().onAttributesUpdate(tenantId, entityId, request.getScope().name(), request.getEntries(), TbCallback.EMPTY); } else { then(subscriptionManagerService).shouldHaveNoInteractions(); } @@ -633,6 +634,155 @@ class DefaultTelemetrySubscriptionServiceTest { then(clusterService).should(never()).pushMsgToCore(any(), any()); } + /* --- Delete attributes API --- */ + + @Test + void shouldThrowErrorWhenTryingToDeleteAttributesForApiUsageState() { + // GIVEN + var request = AttributesDeleteRequest.builder() + .tenantId(tenantId) + .entityId(new ApiUsageStateId(UUID.randomUUID())) + .scope(AttributeScope.SHARED_SCOPE) + .keys(List.of("attributeKeyToDelete1", "attributeKeyToDelete2")) + .notifyDevice(true) + .build(); + + // WHEN + assertThatThrownBy(() -> telemetryService.deleteAttributes(request)) + .isInstanceOf(RuntimeException.class) + .hasMessage("Can't update API Usage State!"); + + // THEN + then(attrService).shouldHaveNoInteractions(); + } + + @Test + void shouldSendAttributesDeletedNotificationWhenDeviceSharedAttributesAreDeletedAndNotifyDeviceIsTrue() { + // GIVEN + var deviceId = DeviceId.fromString("cc51e450-53e1-11ee-883e-e56b48fd2088"); + List keys = List.of("attributeKeyToDelete1", "attributeKeyToDelete2"); + + var request = AttributesDeleteRequest.builder() + .tenantId(tenantId) + .entityId(deviceId) + .scope(AttributeScope.SHARED_SCOPE) + .keys(keys) + .notifyDevice(true) + .build(); + + given(attrService.removeAll(tenantId, deviceId, request.getScope(), keys)).willReturn(immediateFuture(keys)); + + // WHEN + telemetryService.deleteAttributes(request); + + // THEN + var expectedAttributesDeletedMsg = DeviceAttributesEventNotificationMsg.onDelete(tenantId, deviceId, "SHARED_SCOPE", List.of("attributeKeyToDelete1", "attributeKeyToDelete2")); + + then(clusterService).should().pushMsgToCore(eq(expectedAttributesDeletedMsg), isNull()); + } + + @ParameterizedTest + @EnumSource( + value = EntityType.class, + names = {"DEVICE", "API_USAGE_STATE"}, // API usage state excluded due to coverage in another test + mode = EnumSource.Mode.EXCLUDE + ) + void shouldNotSendAttributesDeletedNotificationWhenEntityIsNotDevice(EntityType entityType) { + // GIVEN + var nonDeviceId = EntityIdFactory.getByTypeAndUuid(entityType, "cc51e450-53e1-11ee-883e-e56b48fd2088"); + List keys = List.of("attributeKeyToDelete1", "attributeKeyToDelete2"); + + var request = AttributesDeleteRequest.builder() + .tenantId(tenantId) + .entityId(nonDeviceId) + .scope(AttributeScope.SHARED_SCOPE) + .keys(keys) + .notifyDevice(true) + .build(); + + given(attrService.removeAll(tenantId, nonDeviceId, request.getScope(), keys)).willReturn(immediateFuture(keys)); + + // WHEN + telemetryService.deleteAttributes(request); + + // THEN + then(clusterService).should(never()).pushMsgToCore(any(), any()); + } + + @ParameterizedTest + @EnumSource( + value = AttributeScope.class, + names = "SHARED_SCOPE", + mode = EnumSource.Mode.EXCLUDE + ) + void shouldNotSendAttributesDeletedNotificationWhenAttributesAreNotShared(AttributeScope notSharedScope) { + // GIVEN + var deviceId = DeviceId.fromString("cc51e450-53e1-11ee-883e-e56b48fd2088"); + List keys = List.of("attributeKeyToDelete1", "attributeKeyToDelete2"); + + var request = AttributesDeleteRequest.builder() + .tenantId(tenantId) + .entityId(deviceId) + .scope(notSharedScope) + .keys(keys) + .notifyDevice(true) + .build(); + + given(attrService.removeAll(tenantId, deviceId, request.getScope(), keys)).willReturn(immediateFuture(keys)); + + // WHEN + telemetryService.deleteAttributes(request); + + // THEN + then(clusterService).should(never()).pushMsgToCore(any(), any()); + } + + @Test + void shouldNotSendAttributesDeletedNotificationWhenNotifyDeviceIsFalse() { + // GIVEN + var deviceId = DeviceId.fromString("cc51e450-53e1-11ee-883e-e56b48fd2088"); + List keys = List.of("attributeKeyToDelete1", "attributeKeyToDelete2"); + + var request = AttributesDeleteRequest.builder() + .tenantId(tenantId) + .entityId(deviceId) + .scope(AttributeScope.SHARED_SCOPE) + .keys(keys) + .notifyDevice(false) + .build(); + + given(attrService.removeAll(tenantId, deviceId, request.getScope(), keys)).willReturn(immediateFuture(keys)); + + // WHEN + telemetryService.deleteAttributes(request); + + // THEN + then(clusterService).should(never()).pushMsgToCore(any(), any()); + } + + @Test + void shouldNotSendAttributesDeletedNotificationWhenAttributesDeleteFailed() { + // GIVEN + var deviceId = DeviceId.fromString("cc51e450-53e1-11ee-883e-e56b48fd2088"); + List keys = List.of("attributeKeyToDelete1", "attributeKeyToDelete2"); + + var request = AttributesDeleteRequest.builder() + .tenantId(tenantId) + .entityId(deviceId) + .scope(AttributeScope.SHARED_SCOPE) + .keys(keys) + .notifyDevice(true) + .build(); + + given(attrService.removeAll(tenantId, deviceId, request.getScope(), keys)).willReturn(immediateFailedFuture(new RuntimeException("failed to delete"))); + + // WHEN + telemetryService.deleteAttributes(request); + + // THEN + then(clusterService).should(never()).pushMsgToCore(any(), any()); + } + // used to emulate versions returned by save APIs private static List listOfNNumbers(int N) { return LongStream.range(0, N).boxed().toList(); diff --git a/common/proto/src/main/proto/queue.proto b/common/proto/src/main/proto/queue.proto index 77dab2cb57..3007d48584 100644 --- a/common/proto/src/main/proto/queue.proto +++ b/common/proto/src/main/proto/queue.proto @@ -1079,7 +1079,8 @@ message TbAttributeDeleteProto { int64 tenantIdLSB = 5; string scope = 6; repeated string keys = 7; - bool notifyDevice = 8; + // not used anymore since device notification are now handled in DefaultTelemetrySubscriptionService instead of DefaultSubscriptionManagerService + bool notifyDevice = 8 [deprecated = true]; } message TbTimeSeriesDeleteProto { diff --git a/rule-engine/rule-engine-api/src/main/java/org/thingsboard/rule/engine/api/AttributesDeleteRequest.java b/rule-engine/rule-engine-api/src/main/java/org/thingsboard/rule/engine/api/AttributesDeleteRequest.java index 2ab6923899..374fcc45f6 100644 --- a/rule-engine/rule-engine-api/src/main/java/org/thingsboard/rule/engine/api/AttributesDeleteRequest.java +++ b/rule-engine/rule-engine-api/src/main/java/org/thingsboard/rule/engine/api/AttributesDeleteRequest.java @@ -21,6 +21,7 @@ import lombok.AccessLevel; import lombok.AllArgsConstructor; import lombok.Getter; import lombok.ToString; +import org.thingsboard.common.util.NoOpFutureCallback; import org.thingsboard.server.common.data.AttributeScope; import org.thingsboard.server.common.data.id.CalculatedFieldId; import org.thingsboard.server.common.data.id.EntityId; @@ -30,6 +31,8 @@ import org.thingsboard.server.common.data.msg.TbMsgType; import java.util.List; import java.util.UUID; +import static java.util.Objects.requireNonNullElse; + @Getter @ToString @AllArgsConstructor(access = AccessLevel.PRIVATE) @@ -61,8 +64,7 @@ public class AttributesDeleteRequest implements CalculatedFieldSystemAwareReques private TbMsgType tbMsgType; private FutureCallback callback; - Builder() { - } + Builder() {} public Builder tenantId(TenantId tenantId) { this.tenantId = tenantId; @@ -134,7 +136,9 @@ public class AttributesDeleteRequest implements CalculatedFieldSystemAwareReques } public AttributesDeleteRequest build() { - return new AttributesDeleteRequest(tenantId, entityId, scope, keys, notifyDevice, previousCalculatedFieldIds, tbMsgId, tbMsgType, callback); + return new AttributesDeleteRequest( + tenantId, entityId, scope, keys, notifyDevice, previousCalculatedFieldIds, tbMsgId, tbMsgType, requireNonNullElse(callback, NoOpFutureCallback.instance()) + ); } } diff --git a/rule-engine/rule-engine-api/src/test/java/org/thingsboard/rule/engine/api/AttributesDeleteRequestTest.java b/rule-engine/rule-engine-api/src/test/java/org/thingsboard/rule/engine/api/AttributesDeleteRequestTest.java new file mode 100644 index 0000000000..9b4a825a66 --- /dev/null +++ b/rule-engine/rule-engine-api/src/test/java/org/thingsboard/rule/engine/api/AttributesDeleteRequestTest.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.rule.engine.api; + +import org.junit.jupiter.api.Test; +import org.thingsboard.common.util.NoOpFutureCallback; + +import static org.assertj.core.api.Assertions.assertThat; + +class AttributesDeleteRequestTest { + + @Test + void testDefaultCallbackIsNoOp() { + var request = AttributesDeleteRequest.builder().build(); + + assertThat(request.getCallback()).isEqualTo(NoOpFutureCallback.instance()); + } + + @Test + void testNullCallbackIsNoOp() { + var request = AttributesDeleteRequest.builder().callback(null).build(); + + assertThat(request.getCallback()).isEqualTo(NoOpFutureCallback.instance()); + } + +} From 9d9fd93c89f7b23b976fd869b994f1aaa66c4617 Mon Sep 17 00:00:00 2001 From: Dmytro Skarzhynets Date: Mon, 3 Mar 2025 17:23:42 +0200 Subject: [PATCH 161/345] Save attributes strategies: improve proto deprecation note --- common/proto/src/main/proto/queue.proto | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/common/proto/src/main/proto/queue.proto b/common/proto/src/main/proto/queue.proto index 3007d48584..969b47f847 100644 --- a/common/proto/src/main/proto/queue.proto +++ b/common/proto/src/main/proto/queue.proto @@ -1079,7 +1079,7 @@ message TbAttributeDeleteProto { int64 tenantIdLSB = 5; string scope = 6; repeated string keys = 7; - // not used anymore since device notification are now handled in DefaultTelemetrySubscriptionService instead of DefaultSubscriptionManagerService + // Deprecated since 4.0, not used anymore since device notification are now handled in DefaultTelemetrySubscriptionService instead of DefaultSubscriptionManagerService bool notifyDevice = 8 [deprecated = true]; } From 977d60bbcbbfd9dca3dd26f14dba0ffe7d5eeb23 Mon Sep 17 00:00:00 2001 From: mpetrov Date: Mon, 3 Mar 2025 17:24:22 +0200 Subject: [PATCH 162/345] Removed provision type --- .../mobile-action-editor.component.html | 21 +------------------ .../action/mobile-action-editor.component.ts | 11 +--------- .../action/mobile-action-editor.models.ts | 6 +++--- .../components/widget/widget.component.ts | 6 ++---- ui-ngx/src/app/shared/models/widget.models.ts | 21 ++----------------- .../assets/locale/locale.constant-en_US.json | 11 +--------- 6 files changed, 10 insertions(+), 66 deletions(-) diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/action/mobile-action-editor.component.html b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/action/mobile-action-editor.component.html index 84c1dec938..c21595eea9 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/action/mobile-action-editor.component.html +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/action/mobile-action-editor.component.html @@ -36,26 +36,7 @@ - -
-
{{ 'widget-action.mobile.provisioning-device.type' | translate }}*
- - - - {{ widgetMobileProvisionTypeTranslationMap.get(mobileProvisionType[actionType]) | translate }} - - - - warning - - -
+ ( - [ - [ WidgetMobileProvisionType.smartConfigEspTouch, 'widget-action.mobile.provisioning-device.smart-config-esp-touch' ], - [ WidgetMobileProvisionType.smartConfigEspTouchV2, 'widget-action.mobile.provisioning-device.smart-config-esp-touch-v2' ], - [ WidgetMobileProvisionType.wifi, 'widget-action.mobile.provisioning-device.wifi' ], - [ WidgetMobileProvisionType.softAp, 'widget-action.mobile.provisioning-device.soft-ap' ] - ] -); - export const widgetActionTypes = Object.keys(WidgetActionType) as WidgetActionType[]; export const widgetActionTypeTranslationMap = new Map( @@ -632,7 +616,7 @@ export const widgetMobileActionTypeTranslationMap = new Map { export interface ProvisionSuccessDescriptor { handleProvisionSuccessFunction: TbFunction; - provisionType: WidgetMobileProvisionType; } export interface ProcessImageDescriptor { 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 7325872f91..9e347adecc 100644 --- a/ui-ngx/src/assets/locale/locale.constant-en_US.json +++ b/ui-ngx/src/assets/locale/locale.constant-en_US.json @@ -6534,16 +6534,7 @@ "URL": "URL", "url-required": "URL is required.", "mobile": { - "provisioning-device": { - "label": "Provisioning device", - "type": "Provisioning device type", - "select-type": "Select provisioning device type", - "type-required": "Provisioning device type is required", - "smart-config-esp-touch": "Smart Config ESP-Touch", - "smart-config-esp-touch-v2": "Smart Config ESP-Touch v2", - "wifi": "Wi-Fi Provisioning via Bluetooth Low Energy BLE/QR code", - "soft-ap": "SoftAP" - }, + "provision-device": "Provision device", "action-type": "Mobile action type", "select-action-type": "Select mobile action type", "action-type-required": "Mobile action type is required", From 749a0b5c612339afdb501fbadd202fa12482ff23 Mon Sep 17 00:00:00 2001 From: Igor Kulikov Date: Mon, 3 Mar 2025 18:10:23 +0200 Subject: [PATCH 163/345] UI: Trip data layer implementation. --- .../lib/maps/data-layer/data-layer-utils.ts | 2 +- .../lib/maps/data-layer/map-data-layer.ts | 2 +- .../lib/maps/data-layer/trips-data-layer.ts | 229 ++++++++++++++---- .../home/components/widget/lib/maps/map.ts | 33 ++- .../widget/lib/maps/models/map.models.ts | 20 +- .../panels/map-timeline-panel.component.ts | 7 +- .../map/map-data-layer-dialog.component.html | 1 + .../map/map-data-layer-dialog.component.ts | 2 +- 8 files changed, 233 insertions(+), 63 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 f5872844c9..6e4ec1e485 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 @@ -86,7 +86,7 @@ const bindTooltipActions = (map: TbMap, tooltip: L.Popup, settings: DataLay if (action) { element.onclick = ($event) => { - map.dataItemClick($event, action, data.$datasource); + map.dataItemClick($event, action, data); return false; }; } diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/maps/data-layer/map-data-layer.ts b/ui-ngx/src/app/modules/home/components/widget/lib/maps/data-layer/map-data-layer.ts index 1c4e58a320..e3f326b729 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/maps/data-layer/map-data-layer.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/maps/data-layer/map-data-layer.ts @@ -108,7 +108,7 @@ export abstract class TbDataLayerItem { - this.dataLayer.getMap().dataItemClick(event.originalEvent, clickAction, this.data.$datasource); + this.dataLayer.getMap().dataItemClick(event.originalEvent, clickAction, this.data); }); } } diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/maps/data-layer/trips-data-layer.ts b/ui-ngx/src/app/modules/home/components/widget/lib/maps/data-layer/trips-data-layer.ts index a7b0d74d4d..45aa31def5 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/maps/data-layer/trips-data-layer.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/maps/data-layer/trips-data-layer.ts @@ -17,19 +17,20 @@ import { DataLayerColorProcessor, DataLayerPatternProcessor, - MapDataLayerType, TbDataLayerItem, + MapDataLayerType, + TbDataLayerItem, TbMapDataLayer, UnplacedMapDataItem } from '@home/components/widget/lib/maps/data-layer/map-data-layer'; import { BaseMarkerShapeSettings, - calculateInterpolationRatio, calculateLastPoints, - ClusterMarkerColorFunction, + calculateInterpolationRatio, + calculateLastPoints, DataLayerColorType, - defaultBaseMarkersDataLayerSettings, defaultBaseTripsDataLayerSettings, findRotationAngle, - interpolateLineSegment, isValidLatLng, + interpolateLineSegment, + isValidLatLng, loadImageWithAspect, MapStringFunction, MapType, @@ -40,14 +41,13 @@ import { MarkerImageSettings, MarkerImageType, MarkerPositionFunction, - MarkersDataLayerSettings, MarkerShapeSettings, MarkerType, TbMapDatasource, TripsDataLayerSettings } from '@home/components/widget/lib/maps/models/map.models'; import { forkJoin, Observable, of } from 'rxjs'; -import { FormattedData } from '@shared/models/widget.models'; +import { FormattedData, WidgetActionType } from '@shared/models/widget.models'; import { createColorMarkerIconElement, createColorMarkerShapeURI, @@ -57,7 +57,7 @@ import tinycolor from 'tinycolor2'; import { MatIconRegistry } from '@angular/material/icon'; import { DomSanitizer } from '@angular/platform-browser'; import { catchError, map, switchMap } from 'rxjs/operators'; -import L, { PolylineDecorator } from 'leaflet'; +import L from 'leaflet'; import { CompiledTbFunction } from '@shared/models/js-function.models'; import { deepClone, @@ -74,6 +74,7 @@ import { DataKeyType } from '@shared/models/telemetry/telemetry.models'; import moment from 'moment/moment'; import { TbImageMap } from '@home/components/widget/lib/maps/image-map'; import { createTooltip, updateTooltip } from '@home/components/widget/lib/maps/data-layer/data-layer-utils'; +import _ from 'lodash'; type TripRouteData = {[time: number]: FormattedData}; @@ -88,11 +89,12 @@ class TbTripDataItem { private labelOffset: L.PointTuple; private polyline: L.Polyline; - private polylineDecorator: PolylineDecorator; + private polylineDecorator: L.PolylineDecorator; private points: L.FeatureGroup; private currentTime: number; private currentPositionData: FormattedData; + private pointData: FormattedData; constructor(private rawRouteData: FormattedData[], private latestData: FormattedData, @@ -105,11 +107,13 @@ class TbTripDataItem { private create() { this.updateCurrentPosition(); this.layer = L.featureGroup(); - let pointData = this.currentPositionData; + this.pointData = this.currentPositionData; if (this.latestData) { - pointData = {...pointData, ...this.latestData}; + this.pointData = {...this.pointData, ...this.latestData}; } - this.createMarker(pointData); + this.createPath(); + this.updatePoints(); + this.createMarker(); try { this.dataLayer.getDataLayerContainer().addLayer(this.layer); } catch (e) { @@ -121,38 +125,50 @@ class TbTripDataItem { this.rawRouteData = rawRouteData; this.tripRouteData = this.prepareTripRouteData(); this.updateCurrentPosition(true); - let pointData = this.currentPositionData; + this.pointData = this.currentPositionData; if (this.latestData) { - pointData = {...pointData, ...this.latestData}; + this.pointData = {...this.pointData, ...this.latestData}; } - this.updateMarker(pointData); + this.updatePath(); + this.updatePoints(); + this.updateMarker(); } public updateLatestData(latestData: FormattedData) { this.latestData = latestData; + this.pointData = this.currentPositionData; + if (this.latestData) { + this.pointData = {...this.pointData, ...this.latestData}; + } this.updateAppearance(); } public updateAppearance() { - let data = this.currentPositionData; - if (this.latestData) { - data = {...data, ...this.latestData}; - } + this.updatePathAppearance(); const dsData = this.dataLayer.getMap().getData(); if (this.settings.tooltip.show) { updateTooltip(this.dataLayer.getMap(), this.markerTooltip, - this.settings.tooltip, this.dataLayer.dataLayerTooltipProcessor, data, dsData); + this.settings.tooltip, this.dataLayer.dataLayerTooltipProcessor, this.pointData, dsData); } - this.updateMarkerIcon(data, dsData); + this.updatePoints(); + this.updateMarkerIcon(this.pointData, dsData); } public updateCurrentTime() { this.updateCurrentPosition(); - let pointData = this.currentPositionData; + this.pointData = this.currentPositionData; if (this.latestData) { - pointData = {...pointData, ...this.latestData}; + this.pointData = {...this.pointData, ...this.latestData}; } - this.updateMarker(pointData); + this.updatePath(); + this.updateMarker(); + } + + public invalidateCoordinates(): void { + this.tripRouteData = this.prepareTripRouteData(); + this.updatePath(); + this.updatePoints(); + this.updateMarker(); } public remove() { @@ -164,32 +180,117 @@ class TbTripDataItem { return this.layer; } - private createMarker(data: FormattedData) { + public calculateAnchors(): number[] { + const entries = Object.entries(this.tripRouteData); const dsData = this.dataLayer.getMap().getData(); - const location = this.dataLayer.extractLocation(data, dsData); + return entries.filter(data => this.dataLayer.getMap().getLocationSnapFilterFunction().execute(data[1], dsData)) + .map(data => parseInt(data[0], 10)); + } + + private createMarker() { + const dsData = this.dataLayer.getMap().getData(); + const location = this.dataLayer.extractLocation(this.pointData, dsData); this.marker = L.marker(location, { - tbMarkerData: data, + tbMarkerData: this.pointData, snapIgnore: true }); this.marker.addTo(this.layer); - this.updateMarkerIcon(data, dsData); + this.updateMarkerIcon(this.pointData, dsData); if (this.settings.tooltip?.show) { this.markerTooltip = createTooltip(this.dataLayer.getMap(), - this.marker, this.settings.tooltip, data, () => true); + this.marker, this.settings.tooltip, this.pointData, () => true); updateTooltip(this.dataLayer.getMap(), this.markerTooltip, - this.settings.tooltip, this.dataLayer.dataLayerTooltipProcessor, data, dsData); + this.settings.tooltip, this.dataLayer.dataLayerTooltipProcessor, this.pointData, dsData); + } + const clickAction = this.settings.click; + if (clickAction && clickAction.type !== WidgetActionType.doNothing) { + this.marker.on('click', (event) => { + this.dataLayer.getMap().dataItemClick(event.originalEvent, clickAction, this.pointData); + }); } } - private updateMarker(data: FormattedData) { + private updateMarker() { const dsData = this.dataLayer.getMap().getData(); - this.marker.options.tbMarkerData = data; - this.updateMarkerLocation(data, dsData); + this.marker.options.tbMarkerData = this.pointData; + this.updateMarkerLocation(this.pointData, dsData); if (this.settings.tooltip.show) { updateTooltip(this.dataLayer.getMap(), this.markerTooltip, - this.settings.tooltip, this.dataLayer.dataLayerTooltipProcessor, data, dsData); + this.settings.tooltip, this.dataLayer.dataLayerTooltipProcessor, this.pointData, dsData); + } + this.updateMarkerIcon(this.pointData, dsData); + } + + private createPath() { + if (this.settings.showPath) { + const formattedRouteData = _.values(this.tripRouteData); + const dsData = this.dataLayer.getMap().getData(); + const locations = formattedRouteData.map(data => this.dataLayer.extractLocation(data, dsData)); + const pathStyle = this.dataLayer.getPathStyle(this.pointData, dsData); + this.polyline = L.polyline(locations, pathStyle); + this.polyline.addTo(this.layer); + if (this.settings.usePathDecorator) { + this.polylineDecorator = new L.PolylineDecorator(this.polyline, this.dataLayer.getPathDecoratorStyle(pathStyle.color)); + this.polylineDecorator.addTo(this.layer); + } + } + } + + private updatePath() { + if (this.settings.showPath) { + const formattedRouteData = _.values(this.tripRouteData); + const dsData = this.dataLayer.getMap().getData(); + const locations = formattedRouteData.map(data => this.dataLayer.extractLocation(data, dsData)); + this.polyline.setLatLngs(locations); + if (this.settings.usePathDecorator) { + this.polylineDecorator.setPaths(this.polyline); + } + this.updatePathAppearance(); + } + } + + private updatePoints() { + if (this.settings.showPoints) { + if (!this.points) { + this.points = L.featureGroup(); + this.points.addTo(this.layer); + } + this.points.clearLayers(); + const formattedRouteData = _.values(this.tripRouteData); + const dsData = this.dataLayer.getMap().getData(); + const pointsList = formattedRouteData.filter(data => !!this.dataLayer.extractLocationData(data)); + for (const pData of pointsList) { + let pointData = pData; + if (this.latestData) { + pointData = {...pointData, ...this.latestData}; + } + const pointColor = this.dataLayer.pointColorProcessor.processColor(pointData, dsData); + const point = L.circleMarker(this.dataLayer.extractLocation(pointData, dsData), { + stroke: false, + fillOpacity: 1, + fillColor: pointColor, + radius: this.settings.pointSize + }); + if (this.settings.pointTooltip?.show) { + const pointTooltip = createTooltip(this.dataLayer.getMap(), + point, this.settings.pointTooltip, pointData, () => true); + updateTooltip(this.dataLayer.getMap(), pointTooltip, + this.settings.pointTooltip, this.dataLayer.pointTooltipProcessor, pointData, dsData); + } + this.points.addLayer(point); + } + } + } + + private updatePathAppearance() { + if (this.settings.showPath) { + const dsData = this.dataLayer.getMap().getData(); + const pathStyle = this.dataLayer.getPathStyle(this.pointData, dsData); + this.polyline.setStyle(pathStyle); + if (this.settings.usePathDecorator) { + this.polylineDecorator.setPatterns(this.dataLayer.getPathDecoratorStyle(pathStyle.color).patterns) + } } - this.updateMarkerIcon(data, dsData); } private updateMarkerLocation(data: FormattedData, dsData: FormattedData[]) { @@ -231,6 +332,7 @@ class TbTripDataItem { const maxTime = this.dataLayer.getMap().getMaxTime(); const timeStep = this.dataLayer.getMap().getTimeStep(); const timeline = this.dataLayer.getMap().hasTimeline(); + const dsData = this.dataLayer.getMap().getData(); for (const data of this.rawRouteData) { const currentTime = data.time; const normalizeTime = timeline ? (minTime @@ -260,7 +362,9 @@ class TbTripDataItem { } } if (this.settings.rotateMarker) { - result[timeStamp[i]].rotationAngle += findRotationAngle(result[timeStamp[i]], result[timeStamp[i + 1]], xKey, yKey); + const startPoint = this.dataLayer.extractLocation(result[timeStamp[i]], dsData); + const endPoint = this.dataLayer.extractLocation(result[timeStamp[i + 1]], dsData); + result[timeStamp[i]].rotationAngle += findRotationAngle(startPoint, endPoint); } } } @@ -584,10 +688,6 @@ export class TbTripsDataLayer extends TbMapDataLayer { const entityId = rawTripData[0].entityId; @@ -614,9 +714,12 @@ export class TbTripsDataLayer extends TbMapDataLayer item.invalidateCoordinates()); + } + public updateTripsLatestData(tripsLatestData: FormattedData[]) { this.latestTripsData = tripsLatestData.filter(d => d.$datasource.mapDataIds.includes(this.mapDataId)); - console.log(`Update trips latest data`); this.tripItems.forEach((item, entityId) => { const latestData = this.latestTripsData.find(d => d.entityId === entityId); item.updateLatestData(latestData); @@ -624,22 +727,24 @@ export class TbTripsDataLayer extends TbMapDataLayer { item.updateCurrentTime(); }); } public updateAppearance() { - console.log(`Update trips appearance`); this.tripItems.forEach(item => { item.updateAppearance(); }); } public calculateAnchors(): number[] { - return []; + let anchors: number[] = []; + this.tripItems.forEach(item => { + const tripAnchors = item.calculateAnchors(); + anchors = [...new Set([...anchors, ...tripAnchors])]; + }); + return anchors; } private clearIncorrectFirsLastDatapoint(dataSource: FormattedData[]): FormattedData[] { @@ -705,7 +810,7 @@ export class TbTripsDataLayer extends TbMapDataLayer): {x: number; y: number} { + public extractLocationData(data: FormattedData): {x: number; y: number} { if (data) { const xKeyVal = data[this.settings.xKey.label]; const yKeyVal = data[this.settings.yKey.label]; @@ -740,4 +845,36 @@ export class TbTripsDataLayer extends TbMapDataLayer, dsData: FormattedData[]): L.PolylineOptions { + const pathStroke = this.pathStrokeColorProcessor.processColor(data, dsData); + return { + interactive: false, + color: pathStroke, + opacity: 1, + weight: this.settings.pathStrokeWeight, + pmIgnore: true + }; + } + + public getPathDecoratorStyle(pathStroke: string): L.PolylineDecoratorOptions { + const symbolConstructor = L.Symbol[this.settings.pathDecoratorSymbol]; + return { + patterns: [ + { + offset: this.settings.pathDecoratorOffset, + endOffset: this.settings.pathEndDecoratorOffset, + repeat: this.settings.pathDecoratorRepeat, + symbol: symbolConstructor({ + pixelSize: this.settings.pathDecoratorSymbolSize, + polygon: false, + pathOptions: { + color: this.settings.pathDecoratorSymbolColor ? this.settings.pathDecoratorSymbolColor : pathStroke, + stroke: true + } + }) + } + ] + }; + } + } 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 11af4338e9..6c7d384c81 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 @@ -18,7 +18,7 @@ import { additionalMapDataSourcesToDatasources, BaseMapSettings, CustomActionData, - DataKeyValuePair, + DataKeyValuePair, MapBooleanFunction, MapStringFunction, MapType, mergeMapDatasources, mergeUnplacedDataItemsArrays, @@ -34,12 +34,12 @@ import { formattedDataFormDatasourceData, isDefined, isDefinedAndNotNull, isUndefined, - mergeDeepIgnoreArray + mergeDeepIgnoreArray, parseTbFunction } from '@core/utils'; import { DeepPartial } from '@shared/models/common'; import L from 'leaflet'; import { forkJoin, Observable, of } from 'rxjs'; -import { switchMap, tap } from 'rxjs/operators'; +import { map, switchMap, tap } from 'rxjs/operators'; import '@home/components/widget/lib/maps/leaflet/leaflet-tb'; import { MapDataLayerType, @@ -71,6 +71,7 @@ import TooltipPositioningSide = JQueryTooltipster.TooltipPositioningSide; import { MapTimelinePanelComponent } from '@home/components/widget/lib/maps/panels/map-timeline-panel.component'; import { ComponentRef } from '@angular/core'; import { TbTripsDataLayer } from '@home/components/widget/lib/maps/data-layer/trips-data-layer'; +import { CompiledTbFunction } from '@shared/models/js-function.models'; type TooltipInstancesData = {root: HTMLElement, instances: ITooltipsterInstance[]}; @@ -88,7 +89,7 @@ export abstract class TbMap { protected dataLayers: TbMapDataLayer[]; protected tripDataLayers: TbTripsDataLayer[]; - protected dsData: FormattedData[]; + protected dsData: FormattedData[] = []; protected timeline = false; protected minTime: number; @@ -113,6 +114,7 @@ export abstract class TbMap { protected timeLineComponentRef: ComponentRef; protected timeLineComponent: MapTimelinePanelComponent; + protected locationSnapFilterFunction: CompiledTbFunction; protected addMarkerDataLayers: TbMapDataLayer[]; protected addPolygonDataLayers: TbMapDataLayer[]; @@ -183,7 +185,16 @@ export abstract class TbMap { if (this.map.zoomControl) { this.map.zoomControl.setPosition(this.settings.controlsPosition); } - return this.doSetupControls(); + const setup = [this.doSetupControls()]; + if (this.timeline && this.settings.tripTimeline.snapToRealLocation) { + setup.push(parseTbFunction(this.getCtx().http, this.settings.tripTimeline.locationSnapFilter, ['data', 'dsData']).pipe( + map((parsed) => { + this.locationSnapFilterFunction = parsed; + return null; + }) + )); + } + return forkJoin(setup); } private initMap() { @@ -263,7 +274,6 @@ export abstract class TbMap { this.updateBounds(); }); } - const setup = allDataLayers.map(dl => dl.setup()); forkJoin(setup).subscribe( () => { @@ -919,6 +929,7 @@ export abstract class TbMap { protected invalidateDataLayersCoordinates(): void { this.dataLayers.forEach(dl => dl.invalidateCoordinates()); + this.tripDataLayers.forEach(dl => dl.invalidateCoordinates()); } protected getSidebar(): L.TB.SidebarControl { @@ -953,16 +964,16 @@ export abstract class TbMap { this.updateTripsAnchors(); } - public dataItemClick($event: Event, action: WidgetAction, entityInfo: TbMapDatasource) { + public dataItemClick($event: Event, action: WidgetAction, data: FormattedData) { if ($event) { $event.preventDefault(); $event.stopPropagation(); } - const { entityId, entityName, entityLabel, entityType } = entityInfo; + const { entityId, entityName, entityLabel, entityType } = data.$datasource; this.ctx.actionsApi.handleWidgetAction($event, action, { entityType, id: entityId - }, entityName, null, entityLabel); + }, entityName, data, entityLabel); } public selectItem(item: TbDataLayerItem, cancel = false, force = false): boolean { @@ -1063,6 +1074,10 @@ export abstract class TbMap { return this.currentTime; } + public getLocationSnapFilterFunction(): CompiledTbFunction { + return this.locationSnapFilterFunction; + } + public destroy() { if (this.mapResize$) { this.mapResize$.disconnect(); diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/maps/models/map.models.ts b/ui-ngx/src/app/modules/home/components/widget/lib/maps/models/map.models.ts index 79fb8a7c04..826b47fc91 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/maps/models/map.models.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/maps/models/map.models.ts @@ -23,7 +23,16 @@ import { WidgetActionType } from '@shared/models/widget.models'; import { DataKeyType } from '@shared/models/telemetry/telemetry.models'; -import { guid, hashCode, isDefinedAndNotNull, isNotEmptyStr, isString, isUndefined, mergeDeep } from '@core/utils'; +import { + guid, + hashCode, + isDefinedAndNotNull, + isNotEmptyStr, + isString, + isUndefined, + isUndefinedOrNull, + mergeDeep +} from '@core/utils'; import { AbstractControl, ValidationErrors, ValidatorFn } from '@angular/forms'; import { materialColors } from '@shared/models/material.models'; import L from 'leaflet'; @@ -1040,6 +1049,9 @@ export interface CustomActionData { export type MapStringFunction = (data: FormattedData, dsData: FormattedData[]) => string; +export type MapBooleanFunction = (data: FormattedData, + dsData: FormattedData[]) => boolean; + export type MarkerImageFunction = (data: FormattedData, markerImages: string[], dsData: FormattedData[]) => MarkerImageInfo; @@ -1322,11 +1334,11 @@ export const interpolateLineSegment = ( }; } -export const findRotationAngle = (startPoint: FormattedData, endPoint: FormattedData, xKey: string, yKey: string): number => { - if (isUndefined(startPoint) || isUndefined(endPoint)) { +export const findRotationAngle = (startPoint: L.LatLng, endPoint: L.LatLng): number => { + if (isUndefinedOrNull(startPoint) || isUndefinedOrNull(endPoint)) { return 0; } - let angle = -Math.atan2(endPoint[xKey] - startPoint[xKey], endPoint[yKey] - startPoint[yKey]); + let angle = -Math.atan2(endPoint.lat - startPoint.lat, endPoint.lng - startPoint.lng); angle = angle * 180 / Math.PI; return parseInt(angle.toFixed(2), 10); } diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/maps/panels/map-timeline-panel.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/maps/panels/map-timeline-panel.component.ts index 7b2b428c5c..7d165c5a24 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/maps/panels/map-timeline-panel.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/maps/panels/map-timeline-panel.component.ts @@ -27,7 +27,7 @@ import { } from '@angular/core'; import { TripTimelineSettings } from '@home/components/widget/lib/maps/models/map.models'; import { DateFormatProcessor } from '@shared/models/widget-settings.models'; -import { interval, Observable, Subscription } from 'rxjs'; +import { interval, Subscription } from 'rxjs'; import { filter } from 'rxjs/operators'; import { takeUntilDestroyed } from '@angular/core/rxjs-interop'; @@ -75,6 +75,11 @@ export class MapTimelinePanelComponent implements OnInit { set currentTime(time: number) { if (this.currentTimeValue !== time) { this.currentTimeValue = time; + if (this.hasData) { + this.index = Math.ceil((this.currentTimeValue - this.minValue) / this.settings.timeStep); + } else { + this.index = 0; + } this.updateTimestampDisplayValue(); this.cd.markForCheck(); } diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/map/map-data-layer-dialog.component.html b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/map/map-data-layer-dialog.component.html index ffd2166aab..07e0fa43e9 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/map/map-data-layer-dialog.component.html +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/map/map-data-layer-dialog.component.html @@ -285,6 +285,7 @@ px diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/map/map-data-layer-dialog.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/map/map-data-layer-dialog.component.ts index 7d1d4a4b50..8e57e891c8 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/map/map-data-layer-dialog.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/map/map-data-layer-dialog.component.ts @@ -155,7 +155,7 @@ export class MapDataLayerDialogComponent extends DialogComponent Date: Mon, 3 Mar 2025 19:23:02 +0200 Subject: [PATCH 164/345] fixed NPE, fixed flaky test --- .../subscription/DefaultTbEntityDataSubscriptionService.java | 4 +++- .../org/thingsboard/server/controller/WebsocketApiTest.java | 3 ++- 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/application/src/main/java/org/thingsboard/server/service/subscription/DefaultTbEntityDataSubscriptionService.java b/application/src/main/java/org/thingsboard/server/service/subscription/DefaultTbEntityDataSubscriptionService.java index 78d7f01404..9e9ca42e83 100644 --- a/application/src/main/java/org/thingsboard/server/service/subscription/DefaultTbEntityDataSubscriptionService.java +++ b/application/src/main/java/org/thingsboard/server/service/subscription/DefaultTbEntityDataSubscriptionService.java @@ -436,7 +436,9 @@ public class DefaultTbEntityDataSubscriptionService implements TbEntityDataSubsc ctx.sendWsMsg(update); } else { ctx.doFetchAlarmCount(); - ctx.createAlarmSubscriptions(); + if (entitiesIds != null) { + ctx.createAlarmSubscriptions(); + } TbAlarmCountSubCtx finalCtx = ctx; ScheduledFuture task = scheduler.scheduleWithFixedDelay( () -> refreshDynamicQuery(finalCtx), 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 69d50c61d1..9801907d3b 100644 --- a/application/src/test/java/org/thingsboard/server/controller/WebsocketApiTest.java +++ b/application/src/test/java/org/thingsboard/server/controller/WebsocketApiTest.java @@ -83,7 +83,8 @@ import static org.springframework.test.web.servlet.result.MockMvcResultMatchers. @Slf4j @DaoSqlTest @TestPropertySource(properties = { - "server.ws.alarms_per_alarm_status_subscription_cache_size=5" + "server.ws.alarms_per_alarm_status_subscription_cache_size=5", + "server.ws.dynamic_page_link.refresh_interval=15" }) public class WebsocketApiTest extends AbstractControllerTest { @Autowired From eadc70581725b9f234e89dedda5d3d06e0020233 Mon Sep 17 00:00:00 2001 From: IrynaMatveieva Date: Tue, 4 Mar 2025 09:16:24 +0200 Subject: [PATCH 165/345] added upgrade script for tenant profile --- .../main/data/upgrade/basic/schema_update.sql | 32 ++++++++++++++++++- .../DefaultTenantProfileConfiguration.java | 10 +++--- 2 files changed, 36 insertions(+), 6 deletions(-) diff --git a/application/src/main/data/upgrade/basic/schema_update.sql b/application/src/main/data/upgrade/basic/schema_update.sql index ddfac0d768..d694a2bcf6 100644 --- a/application/src/main/data/upgrade/basic/schema_update.sql +++ b/application/src/main/data/upgrade/basic/schema_update.sql @@ -63,4 +63,34 @@ $$; -- UPDATE SAVE TIME SERIES NODES END -ALTER TABLE api_usage_state ADD COLUMN IF NOT EXISTS version BIGINT DEFAULT 1; \ No newline at end of file +ALTER TABLE api_usage_state ADD COLUMN IF NOT EXISTS version BIGINT DEFAULT 1; + +-- UPDATE TENANT PROFILE CALCULATED FIELD LIMITS START + +DO $$ + BEGIN + -- Check if the tenant_profile table exists + IF EXISTS ( + SELECT 1 + FROM information_schema.tables + WHERE table_name = 'tenant_profile' + ) THEN + + UPDATE tenant_profile + SET profile_data = profile_data + || jsonb_build_object( + 'configuration', profile_data->'configuration' || jsonb_build_object( + 'maxCalculatedFieldsPerEntity', COALESCE(profile_data->'configuration'->>'maxCalculatedFieldsPerEntity', '5')::bigint, + 'maxArgumentsPerCF', COALESCE(profile_data->'configuration'->>'maxArgumentsPerCF', '10')::bigint, + 'maxDataPointsPerRollingArg', COALESCE(profile_data->'configuration'->>'maxDataPointsPerRollingArg', '1000')::bigint, + 'maxStateSizeInKBytes', COALESCE(profile_data->'configuration'->>'maxStateSizeInKBytes', '32')::bigint, + 'maxSingleValueArgumentSizeInKBytes', COALESCE(profile_data->'configuration'->>'maxSingleValueArgumentSizeInKBytes', '2')::bigint + ) + ) + WHERE profile_data->'configuration' IS NOT NULL; + +END IF; + END; +$$; + +-- UPDATE TENANT PROFILE CALCULATED FIELD LIMITS END diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/tenant/profile/DefaultTenantProfileConfiguration.java b/common/data/src/main/java/org/thingsboard/server/common/data/tenant/profile/DefaultTenantProfileConfiguration.java index 9be8a126b2..9c86785a5e 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/tenant/profile/DefaultTenantProfileConfiguration.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/tenant/profile/DefaultTenantProfileConfiguration.java @@ -135,11 +135,11 @@ public class DefaultTenantProfileConfiguration implements TenantProfileConfigura private double warnThreshold; - private long maxCalculatedFieldsPerEntity; - private long maxArgumentsPerCF; - private long maxDataPointsPerRollingArg; - private long maxStateSizeInKBytes; - private long maxSingleValueArgumentSizeInKBytes; + private long maxCalculatedFieldsPerEntity = 5; + private long maxArgumentsPerCF = 10; + private long maxDataPointsPerRollingArg = 1000; + private long maxStateSizeInKBytes = 32; + private long maxSingleValueArgumentSizeInKBytes = 2; @Override public long getProfileThreshold(ApiUsageRecordKey key) { From 6a759ae061ca025cca9b5dd62b78559857eae8f1 Mon Sep 17 00:00:00 2001 From: Andrii Landiak Date: Tue, 4 Mar 2025 09:17:16 +0200 Subject: [PATCH 166/345] Fix testVcWithCalculatedFields_betweenTenants --- .../server/service/sync/vc/VersionControlTest.java | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) 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 d90ca60333..bb0d490ce0 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 @@ -575,7 +575,7 @@ public class VersionControlTest extends AbstractControllerTest { public void testVcWithCalculatedFields_betweenTenants() throws Exception { Asset asset = createAsset(null, null, "Asset 1"); Device device = createDevice(null, null, "Device 1", "test1"); - createCalculatedField("CalculatedField1", device.getId(), asset.getId()); + CalculatedField calculatedField = createCalculatedField("CalculatedField1", device.getId(), asset.getId()); String versionId = createVersion("calculated fields of asset and device", EntityType.ASSET, EntityType.DEVICE, EntityType.DEVICE_PROFILE, EntityType.ASSET_PROFILE); loginTenant2(); @@ -585,16 +585,15 @@ public class VersionControlTest extends AbstractControllerTest { Asset importedAsset = findAsset(asset.getName()); Device importedDevice = findDevice(device.getName()); - CalculatedField importedCalculatedField = findCalculatedFieldByEntityId(importedDevice.getId()); checkImportedEntity(tenantId1, device, tenantId2, importedDevice); checkImportedEntity(tenantId1, asset, tenantId2, importedAsset); List importedCalculatedFields = findCalculatedFieldsByEntityId(importedDevice.getId()); assertThat(importedCalculatedFields).size().isOne(); assertThat(importedCalculatedFields.get(0)).satisfies(importedField -> { - assertThat(importedField.getName()).isEqualTo(importedCalculatedField.getName()); - assertThat(importedField.getType()).isEqualTo(importedCalculatedField.getType()); - assertThat(importedField.getId()).isNotEqualTo(importedCalculatedField.getId()); + assertThat(importedField.getName()).isEqualTo(calculatedField.getName()); + assertThat(importedField.getType()).isEqualTo(calculatedField.getType()); + assertThat(importedField.getId()).isNotEqualTo(calculatedField.getId()); }); } From 7994802e4bc3390c6c6701ff726289072ce2890c Mon Sep 17 00:00:00 2001 From: IrynaMatveieva Date: Tue, 4 Mar 2025 09:39:31 +0200 Subject: [PATCH 167/345] added default values on the UI --- ui-ngx/src/app/shared/models/tenant.model.ts | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/ui-ngx/src/app/shared/models/tenant.model.ts b/ui-ngx/src/app/shared/models/tenant.model.ts index 38ff63e3c6..23896141db 100644 --- a/ui-ngx/src/app/shared/models/tenant.model.ts +++ b/ui-ngx/src/app/shared/models/tenant.model.ts @@ -156,11 +156,11 @@ export function createTenantProfileConfiguration(type: TenantProfileType): Tenan rpcTtlDays: 0, queueStatsTtlDays: 0, ruleEngineExceptionsTtlDays: 0, - maxCalculatedFieldsPerEntity: 0, - maxArgumentsPerCF: 0, - maxDataPointsPerRollingArg: 0, - maxStateSizeInKBytes: 0, - maxSingleValueArgumentSizeInKBytes: 0, + maxCalculatedFieldsPerEntity: 5, + maxArgumentsPerCF: 10, + maxDataPointsPerRollingArg: 1000, + maxStateSizeInKBytes: 32, + maxSingleValueArgumentSizeInKBytes: 2, calculatedFieldDebugEventsRateLimit: '' }; configuration = {...defaultConfiguration, type: TenantProfileType.DEFAULT}; From afd727b097ae4253c7bd8fe445a0c9197eb2d4ad Mon Sep 17 00:00:00 2001 From: IrynaMatveieva Date: Tue, 4 Mar 2025 11:22:51 +0200 Subject: [PATCH 168/345] removed check for changing value when update state and moved timeout to config param --- .../server/actors/ActorSystemContext.java | 4 ++++ ...CalculatedFieldEntityMessageProcessor.java | 21 ++++++++----------- .../ctx/state/SingleValueArgumentEntry.java | 4 ---- .../src/main/resources/thingsboard.yml | 2 ++ 4 files changed, 15 insertions(+), 16 deletions(-) diff --git a/application/src/main/java/org/thingsboard/server/actors/ActorSystemContext.java b/application/src/main/java/org/thingsboard/server/actors/ActorSystemContext.java index 50a777840c..9ab7adfcc6 100644 --- a/application/src/main/java/org/thingsboard/server/actors/ActorSystemContext.java +++ b/application/src/main/java/org/thingsboard/server/actors/ActorSystemContext.java @@ -644,6 +644,10 @@ public class ActorSystemContext { @Getter private String deviceStateNodeRateLimitConfig; + @Value("${actors.calculated_fields.calculation_result_timeout:5}") + @Getter + private long cfCalculationResultTimeout; + @Getter @Setter private TbActorSystem actorSystem; diff --git a/application/src/main/java/org/thingsboard/server/actors/calculatedField/CalculatedFieldEntityMessageProcessor.java b/application/src/main/java/org/thingsboard/server/actors/calculatedField/CalculatedFieldEntityMessageProcessor.java index f7fc204c0f..279628c566 100644 --- a/application/src/main/java/org/thingsboard/server/actors/calculatedField/CalculatedFieldEntityMessageProcessor.java +++ b/application/src/main/java/org/thingsboard/server/actors/calculatedField/CalculatedFieldEntityMessageProcessor.java @@ -275,9 +275,9 @@ public class CalculatedFieldEntityMessageProcessor extends AbstractContextAwareM private void processStateIfReady(CalculatedFieldCtx ctx, List cfIdList, CalculatedFieldState state, UUID tbMsgId, TbMsgType tbMsgType, TbCallback callback) throws CalculatedFieldException { CalculatedFieldEntityCtxId ctxId = new CalculatedFieldEntityCtxId(tenantId, ctx.getCfId(), entityId); boolean stateSizeOk; - if (ctx.isInitialized() && state.isReady()) { - try { - CalculatedFieldResult calculationResult = state.performCalculation(ctx).get(5, TimeUnit.SECONDS); + try { + if (ctx.isInitialized() && state.isReady()) { + CalculatedFieldResult calculationResult = state.performCalculation(ctx).get(systemContext.getCfCalculationResultTimeout(), TimeUnit.SECONDS); state.checkStateSize(ctxId, ctx.getMaxStateSize()); stateSizeOk = state.isSizeOk(); if (stateSizeOk) { @@ -286,21 +286,18 @@ public class CalculatedFieldEntityMessageProcessor extends AbstractContextAwareM systemContext.persistCalculatedFieldDebugEvent(tenantId, ctx.getCfId(), entityId, state.getArguments(), tbMsgId, tbMsgType, JacksonUtil.writeValueAsString(calculationResult.getResult()), null); } } - } catch (Exception e) { - throw CalculatedFieldException.builder().ctx(ctx).eventEntity(entityId).msgId(tbMsgId).msgType(tbMsgType).arguments(state.getArguments()).cause(e).build(); } - } else { + } catch (Exception e) { + throw CalculatedFieldException.builder().ctx(ctx).eventEntity(entityId).msgId(tbMsgId).msgType(tbMsgType).arguments(state.getArguments()).cause(e).build(); + } finally { state.checkStateSize(ctxId, ctx.getMaxStateSize()); stateSizeOk = state.isSizeOk(); if (stateSizeOk) { - callback.onSuccess(); // State was updated but no calculation performed; + cfStateService.persistState(ctxId, state, callback); + } else { + removeStateAndRaiseSizeException(ctxId, CalculatedFieldException.builder().ctx(ctx).eventEntity(entityId).errorMessage(ctx.getSizeExceedsLimitMessage()).build(), callback); } } - if (stateSizeOk) { - cfStateService.persistState(ctxId, state, callback); - } else { - removeStateAndRaiseSizeException(ctxId, CalculatedFieldException.builder().ctx(ctx).eventEntity(entityId).errorMessage(ctx.getSizeExceedsLimitMessage()).build(), callback); - } } private void removeStateAndRaiseSizeException(CalculatedFieldEntityCtxId ctxId, CalculatedFieldException ex, TbCallback callback) { diff --git a/application/src/main/java/org/thingsboard/server/service/cf/ctx/state/SingleValueArgumentEntry.java b/application/src/main/java/org/thingsboard/server/service/cf/ctx/state/SingleValueArgumentEntry.java index a064a99935..a237f3d022 100644 --- a/application/src/main/java/org/thingsboard/server/service/cf/ctx/state/SingleValueArgumentEntry.java +++ b/application/src/main/java/org/thingsboard/server/service/cf/ctx/state/SingleValueArgumentEntry.java @@ -100,10 +100,6 @@ public class SingleValueArgumentEntry implements ArgumentEntry { if (newVersion == null || this.version == null || newVersion > this.version) { this.ts = singleValueEntry.getTs(); this.version = newVersion; - BasicKvEntry newValue = singleValueEntry.getKvEntryValue(); - if (this.kvEntryValue != null && this.kvEntryValue.getValue().equals(newValue.getValue())) { - return false; - } this.kvEntryValue = singleValueEntry.getKvEntryValue(); return true; } diff --git a/application/src/main/resources/thingsboard.yml b/application/src/main/resources/thingsboard.yml index ec9925bf06..d0ccef8ba9 100644 --- a/application/src/main/resources/thingsboard.yml +++ b/application/src/main/resources/thingsboard.yml @@ -512,6 +512,8 @@ actors: enabled: "${ACTORS_CALCULATED_FIELD_DEBUG_MODE_RATE_LIMITS_PER_TENANT_ENABLED:true}" # The value of DEBUG mode rate limit. By default, no more than 50 thousand events per hour configuration: "${ACTORS_CALCULATED_FIELD_DEBUG_MODE_RATE_LIMITS_PER_TENANT_CONFIGURATION:50000:3600}" + # Time in seconds to receive calculation result. + calculation_result_timeout: "${ACTORS_CALCULATION_RESULT_TIMEOUT_SEC:5}" debug: settings: From 864802fa6bc90b2ef6019a35d3309ff0d9ecfe3c Mon Sep 17 00:00:00 2001 From: Artem Dzhereleiko Date: Tue, 4 Mar 2025 11:26:04 +0200 Subject: [PATCH 169/345] UI: Format for tank scale --- .../system/scada_symbols/cylindrical-tank.svg | 219 ++++++----------- .../system/scada_symbols/elevated-tank.svg | 219 ++++++----------- .../scada_symbols/horizontal-tank-hp.svg | 140 +++++------ .../system/scada_symbols/horizontal-tank.svg | 219 ++++++----------- .../scada_symbols/large-cylindrical-tank.svg | 219 ++++++----------- .../large-stand-cylindrical-tank.svg | 217 ++++++----------- .../large-stand-vertical-tank.svg | 219 ++++++----------- .../scada_symbols/large-vertical-tank.svg | 219 ++++++----------- .../json/system/scada_symbols/pool-hp.svg | 140 +++++------ .../scada_symbols/short-vertical-tank-hp.svg | 140 +++++------ .../scada_symbols/small-cylindrical-tank.svg | 219 ++++++----------- .../scada_symbols/small-spherical-tank.svg | 219 ++++++----------- .../system/scada_symbols/spherical-tank.svg | 219 ++++++----------- .../scada_symbols/stand-cylindrical-tank.svg | 219 ++++++----------- .../scada_symbols/stand-horizontal-tank.svg | 219 ++++++----------- .../stand-vertical-short-tank.svg | 219 ++++++----------- .../scada_symbols/stand-vertical-tank.svg | 217 ++++++----------- .../scada_symbols/vertical-short-tank.svg | 220 ++++++------------ .../system/scada_symbols/vertical-tank-hp.svg | 140 +++++------ .../system/scada_symbols/vertical-tank.svg | 220 ++++++------------ .../assets/locale/locale.constant-en_US.json | 1 + 21 files changed, 1295 insertions(+), 2768 deletions(-) diff --git a/application/src/main/data/json/system/scada_symbols/cylindrical-tank.svg b/application/src/main/data/json/system/scada_symbols/cylindrical-tank.svg index 513809cca5..ab0b5415bd 100644 --- a/application/src/main/data/json/system/scada_symbols/cylindrical-tank.svg +++ b/application/src/main/data/json/system/scada_symbols/cylindrical-tank.svg @@ -33,7 +33,7 @@ }, { "tag": "scale", - "stateRenderFunction": "if (!ctx.properties.scale) {\n element.hide();\n} else {\n var scaleSet = element.remember('scaleSet');\n if (!scaleSet) {\n element.remember('scaleSet', true);\n element.clear();\n \n var majorIntervals = ctx.properties.majorIntervals;\n var minorIntervals = ctx.properties.minorIntervals;\n \n var start = 205;\n var majorIntervalLength = 760 / majorIntervals;\n var minorIntervalLength = majorIntervalLength / minorIntervals;\n for (var i = 0; i < majorIntervals + 1; i++) {\n var y = start + i * majorIntervalLength;\n var line = ctx.svg.line(340, y, 372, y).stroke({ width: 3 }).attr({class: 'majorTick'});\n element.add(line);\n var majorText = (100 - i * (100/majorIntervals)).toFixed(0);\n var majorTickText = ctx.svg.text(majorText);\n majorTickText.attr({x: 330, y: y + 2, 'text-anchor': 'end', class: 'majorTickText'});\n majorTickText.first().attr({'dominant-baseline': 'middle'});\n element.add(majorTickText);\n if (i < majorIntervals) {\n drawMinorTicks(y, minorIntervals, minorIntervalLength);\n }\n }\n }\n \n var majorFont = ctx.properties.majorFont;\n var majorColor = ctx.properties.majorColor;\n var minorColor = ctx.properties.minorColor;\n if (ctx.values.critical) {\n majorColor = ctx.properties.majorCriticalColor;\n minorColor = ctx.properties.minorCriticalColor;\n } else if (ctx.values.warning) {\n majorColor = ctx.properties.minorWarningColor;\n minorColor = ctx.properties.minorWarningColor;\n }\n \n var majorTicks = element.find('line.majorTick');\n majorTicks.forEach(t => t.attr({stroke: majorColor}));\n \n var majorTicksText = element.find('text.majorTickText');\n ctx.api.font(majorTicksText, majorFont, majorColor);\n \n var minorTicks = element.find('line.minorTick');\n minorTicks.forEach(t => t.attr({stroke: minorColor}));\n \n var elementCriticalAnimation = element.remember('criticalAnimation');\n var criticalAnimation = ctx.values.critical && ctx.values.criticalAnimation;\n\n if (elementCriticalAnimation !== criticalAnimation) {\n element.remember('criticalAnimation', criticalAnimation);\n if (criticalAnimation) {\n ctx.api.cssAnimate(element, 500).attr({opacity: 0.15}).loop(0, true);\n } else {\n ctx.api.resetCssAnimation(element);\n }\n }\n}\n\nfunction drawMinorTicks(start, minorIntervals, minorIntervalLength) {\n for (var i = 1; i < minorIntervals; i++) {\n var minorY = start + i * minorIntervalLength;\n var minorLine = ctx.svg.line(352, minorY, 372, minorY).stroke({ width: 3 }).attr({class: 'minorTick'});\n element.add(minorLine);\n }\n}", + "stateRenderFunction": "if (!ctx.properties.scale) {\n element.hide();\n} else {\n var scaleSet = element.remember('scaleSet');\n if (!scaleSet) {\n element.remember('scaleSet', true);\n element.clear();\n \n var majorIntervals = ctx.properties.majorIntervals;\n var minorIntervals = ctx.properties.minorIntervals;\n \n var start = 205;\n var majorIntervalLength = 760 / majorIntervals;\n var minorIntervalLength = majorIntervalLength / minorIntervals;\n var tankCapacity = ctx.properties.scaleDisplayFormat ? 100 : (ctx.values.tankCapacity || 100);\n for (var i = 0; i < majorIntervals + 1; i++) {\n var y = start + i * majorIntervalLength;\n var line = ctx.svg.line(340, y, 372, y).stroke({ width: 3 }).attr({class: 'majorTick'});\n element.add(line);\n var majorText = ctx.api.formatValue((tankCapacity - i * (tankCapacity/majorIntervals)).toFixed(0), 0, ctx.properties.majorUnits, false);\n var majorTickText = ctx.svg.text(majorText);\n majorTickText.attr({x: 330, y: y + 2, 'text-anchor': 'end', class: 'majorTickText'});\n majorTickText.first().attr({'dominant-baseline': 'middle'});\n element.add(majorTickText);\n if (i < majorIntervals) {\n drawMinorTicks(y, minorIntervals, minorIntervalLength);\n }\n }\n }\n \n var majorFont = ctx.properties.majorFont;\n var majorColor = ctx.properties.majorColor;\n var minorColor = ctx.properties.minorColor;\n if (ctx.values.critical) {\n majorColor = ctx.properties.majorCriticalColor;\n minorColor = ctx.properties.minorCriticalColor;\n } else if (ctx.values.warning) {\n majorColor = ctx.properties.minorWarningColor;\n minorColor = ctx.properties.minorWarningColor;\n }\n \n var majorTicks = element.find('line.majorTick');\n majorTicks.forEach(t => t.attr({stroke: majorColor}));\n \n var majorTicksText = element.find('text.majorTickText');\n ctx.api.font(majorTicksText, majorFont, majorColor);\n \n var minorTicks = element.find('line.minorTick');\n minorTicks.forEach(t => t.attr({stroke: minorColor}));\n \n var elementCriticalAnimation = element.remember('criticalAnimation');\n var criticalAnimation = ctx.values.critical && ctx.values.criticalAnimation;\n\n if (elementCriticalAnimation !== criticalAnimation) {\n element.remember('criticalAnimation', criticalAnimation);\n if (criticalAnimation) {\n ctx.api.cssAnimate(element, 500).attr({opacity: 0.15}).loop(0, true);\n } else {\n ctx.api.resetCssAnimation(element);\n }\n }\n}\n\nfunction drawMinorTicks(start, minorIntervals, minorIntervalLength) {\n for (var i = 1; i < minorIntervals; i++) {\n var minorY = start + i * minorIntervalLength;\n var minorLine = ctx.svg.line(352, minorY, 372, minorY).stroke({ width: 3 }).attr({class: 'minorTick'});\n element.add(minorLine);\n }\n}", "actions": null }, { @@ -278,80 +278,43 @@ "name": "{i18n:scada.symbol.tank-color}", "type": "color", "default": "#E5E5E5", - "required": null, - "subLabel": null, - "divider": null, - "fieldSuffix": null, - "disableOnProperty": null, - "rowClass": "", - "fieldClass": "", - "min": null, - "max": null, - "step": null + "disabled": false, + "visible": true }, { "id": "fluidColor", "name": "{i18n:scada.symbol.fluid-color}", "type": "color", "default": "#1EC1F480", - "required": null, - "subLabel": null, - "divider": null, - "fieldSuffix": null, - "disableOnProperty": null, - "rowClass": "", - "fieldClass": "", - "min": null, - "max": null, - "step": null + "disabled": false, + "visible": true }, { "id": "valueBox", "name": "{i18n:scada.symbol.value-box}", "type": "switch", "default": true, - "required": null, - "subLabel": null, - "divider": null, - "fieldSuffix": null, - "disableOnProperty": null, - "rowClass": "", - "fieldClass": "", - "min": null, - "max": null, - "step": null + "disabled": false, + "visible": true }, { "id": "valueBoxColor", "name": "{i18n:scada.symbol.value-box}", "type": "color", "default": "#F3F3F3", - "required": null, - "subLabel": null, - "divider": null, - "fieldSuffix": null, "disableOnProperty": "valueBox", - "rowClass": "", - "fieldClass": "", - "min": null, - "max": null, - "step": null + "disabled": true, + "visible": true }, { "id": "valueUnits", "name": "{i18n:scada.symbol.value-text}", "type": "units", "default": "gal", - "required": null, "subLabel": "{i18n:scada.symbol.units}", - "divider": null, - "fieldSuffix": null, "disableOnProperty": "valueBox", - "rowClass": "", - "fieldClass": "", - "min": null, - "max": null, - "step": null + "disabled": true, + "visible": true }, { "id": "valueTextFont", @@ -364,80 +327,78 @@ "weight": "500", "style": "normal" }, - "required": null, - "subLabel": null, - "divider": null, - "fieldSuffix": null, "disableOnProperty": "valueBox", - "rowClass": "", - "fieldClass": "", - "min": null, - "max": null, - "step": null + "disabled": true, + "visible": true }, { "id": "valueTextColor", "name": "{i18n:scada.symbol.value-text}", "type": "color", "default": "#0000008A", - "required": null, - "subLabel": null, - "divider": null, - "fieldSuffix": null, "disableOnProperty": "valueBox", - "rowClass": "", - "fieldClass": "", - "min": null, - "max": null, - "step": null + "disabled": true, + "visible": true }, { "id": "scale", "name": "{i18n:scada.symbol.scale}", "type": "switch", "default": true, - "required": null, - "subLabel": null, - "divider": null, - "fieldSuffix": null, - "disableOnProperty": null, - "rowClass": "", - "fieldClass": "", - "min": null, - "max": null, - "step": null + "disabled": false, + "visible": true + }, + { + "id": "scaleDisplayFormat", + "name": "{i18n:scada.symbol.scale}", + "type": "select", + "default": true, + "subLabel": "{i18n:scada.symbol.display-format}", + "disableOnProperty": "scale", + "items": [ + { + "value": true, + "label": "Percentage" + }, + { + "value": false, + "label": "Metric" + } + ], + "disabled": false, + "visible": true }, { "id": "transparent", "name": "{i18n:scada.symbol.transparent-mode}", "type": "switch", "default": false, - "required": null, - "subLabel": null, - "divider": null, - "fieldSuffix": null, "disableOnProperty": "scale", - "rowClass": "", - "fieldClass": "", - "min": null, - "max": null, - "step": null + "disabled": false, + "visible": true }, { "id": "majorIntervals", "name": "{i18n:scada.symbol.major-ticks}", "type": "number", "default": 10, - "required": null, "subLabel": "{i18n:scada.symbol.intervals}", "divider": true, - "fieldSuffix": null, "disableOnProperty": "scale", - "rowClass": "", - "fieldClass": "", "min": 1, - "max": null, - "step": 1 + "step": 1, + "disabled": false, + "visible": true + }, + { + "id": "majorUnits", + "name": "{i18n:scada.symbol.major-ticks}", + "type": "units", + "subLabel": "{i18n:scada.symbol.units}", + "divider": true, + "disableOnProperty": "scale", + "disabled": false, + "visible": true }, { "id": "majorFont", @@ -450,128 +411,84 @@ "weight": "500", "style": "normal" }, - "required": null, - "subLabel": null, - "divider": null, - "fieldSuffix": null, "disableOnProperty": "scale", - "rowClass": "", - "fieldClass": "", - "min": null, - "max": null, - "step": null + "disabled": false, + "visible": true }, { "id": "majorColor", "name": "{i18n:scada.symbol.major-ticks-color}", "type": "color", "default": "#00000061", - "required": null, "subLabel": "{i18n:scada.symbol.normal}", "divider": true, - "fieldSuffix": null, "disableOnProperty": "scale", - "rowClass": "", - "fieldClass": "", - "min": null, - "max": null, - "step": null + "disabled": false, + "visible": true }, { "id": "majorWarningColor", "name": "{i18n:scada.symbol.major-ticks-color}", "type": "color", "default": "#FAA405", - "required": null, "subLabel": "{i18n:scada.symbol.warning}", "divider": true, - "fieldSuffix": null, "disableOnProperty": "scale", - "rowClass": "", - "fieldClass": "", - "min": null, - "max": null, - "step": null + "disabled": false, + "visible": true }, { "id": "majorCriticalColor", "name": "{i18n:scada.symbol.major-ticks-color}", "type": "color", "default": "#D12730", - "required": null, "subLabel": "{i18n:scada.symbol.critical}", - "divider": null, - "fieldSuffix": null, "disableOnProperty": "scale", - "rowClass": "", - "fieldClass": "", - "min": null, - "max": null, - "step": null + "disabled": false, + "visible": true }, { "id": "minorIntervals", "name": "{i18n:scada.symbol.minor-ticks}", "type": "number", "default": 5, - "required": null, "subLabel": "{i18n:scada.symbol.intervals}", - "divider": null, - "fieldSuffix": null, "disableOnProperty": "scale", - "rowClass": "", - "fieldClass": "", "min": 1, - "max": null, - "step": null + "disabled": false, + "visible": true }, { "id": "minorColor", "name": "{i18n:scada.symbol.minor-ticks-color}", "type": "color", "default": "#0000001F", - "required": null, "subLabel": "{i18n:scada.symbol.normal}", "divider": true, - "fieldSuffix": null, "disableOnProperty": "scale", - "rowClass": "", - "fieldClass": "", - "min": null, - "max": null, - "step": null + "disabled": false, + "visible": true }, { "id": "minorWarningColor", "name": "{i18n:scada.symbol.minor-ticks-color}", "type": "color", "default": "#FAA405", - "required": null, "subLabel": "{i18n:scada.symbol.warning}", "divider": true, - "fieldSuffix": null, "disableOnProperty": "scale", - "rowClass": "", - "fieldClass": "", - "min": null, - "max": null, - "step": null + "disabled": false, + "visible": true }, { "id": "minorCriticalColor", "name": "{i18n:scada.symbol.minor-ticks-color}", "type": "color", "default": "#D12730", - "required": null, "subLabel": "{i18n:scada.symbol.critical}", - "divider": null, - "fieldSuffix": null, "disableOnProperty": "scale", - "rowClass": "", - "fieldClass": "", - "min": null, - "max": null, - "step": null + "disabled": false, + "visible": true } ] }]]> diff --git a/application/src/main/data/json/system/scada_symbols/elevated-tank.svg b/application/src/main/data/json/system/scada_symbols/elevated-tank.svg index 9db211457e..7019e3221a 100644 --- a/application/src/main/data/json/system/scada_symbols/elevated-tank.svg +++ b/application/src/main/data/json/system/scada_symbols/elevated-tank.svg @@ -34,7 +34,7 @@ }, { "tag": "scale", - "stateRenderFunction": "if (!ctx.properties.scale) {\n element.hide();\n} else {\n var scaleSet = element.remember('scaleSet');\n if (!scaleSet) {\n element.remember('scaleSet', true);\n element.clear();\n \n var majorIntervals = ctx.properties.majorIntervals;\n var minorIntervals = ctx.properties.minorIntervals;\n \n var start = 265;\n var majorIntervalLength = 895 / majorIntervals;\n var minorIntervalLength = majorIntervalLength / minorIntervals;\n for (var i = 0; i < majorIntervals + 1; i++) {\n var y = start + i * majorIntervalLength;\n var line = ctx.svg.line(825, y, 857, y).stroke({ width: 3 }).attr({class: 'majorTick'});\n element.add(line);\n var majorText = (100 - i * (100/majorIntervals)).toFixed(0);\n var majorTickText = ctx.svg.text(majorText);\n majorTickText.attr({x: 815, y: y + 2, 'text-anchor': 'end', class: 'majorTickText'});\n majorTickText.first().attr({'dominant-baseline': 'middle'});\n element.add(majorTickText);\n if (i < majorIntervals) {\n drawMinorTicks(y, minorIntervals, minorIntervalLength);\n }\n }\n }\n \n var majorFont = ctx.properties.majorFont;\n var majorColor = ctx.properties.majorColor;\n var minorColor = ctx.properties.minorColor;\n if (ctx.values.critical) {\n majorColor = ctx.properties.majorCriticalColor;\n minorColor = ctx.properties.minorCriticalColor;\n } else if (ctx.values.warning) {\n majorColor = ctx.properties.minorWarningColor;\n minorColor = ctx.properties.minorWarningColor;\n }\n \n var majorTicks = element.find('line.majorTick');\n majorTicks.forEach(t => t.attr({stroke: majorColor}));\n \n var majorTicksText = element.find('text.majorTickText');\n ctx.api.font(majorTicksText, majorFont, majorColor);\n \n var minorTicks = element.find('line.minorTick');\n minorTicks.forEach(t => t.attr({stroke: minorColor}));\n \n var elementCriticalAnimation = element.remember('criticalAnimation');\n var criticalAnimation = ctx.values.critical && ctx.values.criticalAnimation;\n\n if (elementCriticalAnimation !== criticalAnimation) {\n element.remember('criticalAnimation', criticalAnimation);\n if (criticalAnimation) {\n ctx.api.cssAnimate(element, 500).attr({opacity: 0.15}).loop(0, true);\n } else {\n ctx.api.resetCssAnimation(element);\n }\n }\n}\n\nfunction drawMinorTicks(start, minorIntervals, minorIntervalLength) {\n for (var i = 1; i < minorIntervals; i++) {\n var minorY = start + i * minorIntervalLength;\n var minorLine = ctx.svg.line(837, minorY, 857, minorY).stroke({ width: 3 }).attr({class: 'minorTick'});\n element.add(minorLine);\n }\n}", + "stateRenderFunction": "if (!ctx.properties.scale) {\n element.hide();\n} else {\n var scaleSet = element.remember('scaleSet');\n if (!scaleSet) {\n element.remember('scaleSet', true);\n element.clear();\n \n var majorIntervals = ctx.properties.majorIntervals;\n var minorIntervals = ctx.properties.minorIntervals;\n \n var start = 265;\n var majorIntervalLength = 895 / majorIntervals;\n var minorIntervalLength = majorIntervalLength / minorIntervals;\n var tankCapacity = ctx.properties.scaleDisplayFormat ? 100 : (ctx.values.tankCapacity || 100);\n for (var i = 0; i < majorIntervals + 1; i++) {\n var y = start + i * majorIntervalLength;\n var line = ctx.svg.line(825, y, 857, y).stroke({ width: 3 }).attr({class: 'majorTick'});\n element.add(line);\n var majorText = ctx.api.formatValue((tankCapacity - i * (tankCapacity/majorIntervals)).toFixed(0), 0, ctx.properties.majorUnits, false);\n var majorTickText = ctx.svg.text(majorText);\n majorTickText.attr({x: 815, y: y + 2, 'text-anchor': 'end', class: 'majorTickText'});\n majorTickText.first().attr({'dominant-baseline': 'middle'});\n element.add(majorTickText);\n if (i < majorIntervals) {\n drawMinorTicks(y, minorIntervals, minorIntervalLength);\n }\n }\n }\n \n var majorFont = ctx.properties.majorFont;\n var majorColor = ctx.properties.majorColor;\n var minorColor = ctx.properties.minorColor;\n if (ctx.values.critical) {\n majorColor = ctx.properties.majorCriticalColor;\n minorColor = ctx.properties.minorCriticalColor;\n } else if (ctx.values.warning) {\n majorColor = ctx.properties.minorWarningColor;\n minorColor = ctx.properties.minorWarningColor;\n }\n \n var majorTicks = element.find('line.majorTick');\n majorTicks.forEach(t => t.attr({stroke: majorColor}));\n \n var majorTicksText = element.find('text.majorTickText');\n ctx.api.font(majorTicksText, majorFont, majorColor);\n \n var minorTicks = element.find('line.minorTick');\n minorTicks.forEach(t => t.attr({stroke: minorColor}));\n \n var elementCriticalAnimation = element.remember('criticalAnimation');\n var criticalAnimation = ctx.values.critical && ctx.values.criticalAnimation;\n\n if (elementCriticalAnimation !== criticalAnimation) {\n element.remember('criticalAnimation', criticalAnimation);\n if (criticalAnimation) {\n ctx.api.cssAnimate(element, 500).attr({opacity: 0.15}).loop(0, true);\n } else {\n ctx.api.resetCssAnimation(element);\n }\n }\n}\n\nfunction drawMinorTicks(start, minorIntervals, minorIntervalLength) {\n for (var i = 1; i < minorIntervals; i++) {\n var minorY = start + i * minorIntervalLength;\n var minorLine = ctx.svg.line(837, minorY, 857, minorY).stroke({ width: 3 }).attr({class: 'minorTick'});\n element.add(minorLine);\n }\n}", "actions": null }, { @@ -274,80 +274,43 @@ "name": "{i18n:scada.symbol.tank-color}", "type": "color", "default": "#E5E5E5", - "required": null, - "subLabel": null, - "divider": null, - "fieldSuffix": null, - "disableOnProperty": null, - "rowClass": "", - "fieldClass": "", - "min": null, - "max": null, - "step": null + "disabled": false, + "visible": true }, { "id": "fluidColor", "name": "{i18n:scada.symbol.fluid-color}", "type": "color", "default": "#1EC1F480", - "required": null, - "subLabel": null, - "divider": null, - "fieldSuffix": null, - "disableOnProperty": null, - "rowClass": "", - "fieldClass": "", - "min": null, - "max": null, - "step": null + "disabled": false, + "visible": true }, { "id": "valueBox", "name": "{i18n:scada.symbol.value-box}", "type": "switch", "default": true, - "required": null, - "subLabel": null, - "divider": null, - "fieldSuffix": null, - "disableOnProperty": null, - "rowClass": "", - "fieldClass": "", - "min": null, - "max": null, - "step": null + "disabled": false, + "visible": true }, { "id": "valueBoxColor", "name": "{i18n:scada.symbol.value-box}", "type": "color", "default": "#F3F3F3", - "required": null, - "subLabel": null, - "divider": null, - "fieldSuffix": null, "disableOnProperty": "valueBox", - "rowClass": "", - "fieldClass": "", - "min": null, - "max": null, - "step": null + "disabled": false, + "visible": true }, { "id": "valueUnits", "name": "{i18n:scada.symbol.value-text}", "type": "units", "default": "gal", - "required": null, "subLabel": "{i18n:scada.symbol.units}", - "divider": null, - "fieldSuffix": null, "disableOnProperty": "valueBox", - "rowClass": "", - "fieldClass": "", - "min": null, - "max": null, - "step": null + "disabled": false, + "visible": true }, { "id": "valueTextFont", @@ -360,80 +323,78 @@ "weight": "500", "style": "normal" }, - "required": null, - "subLabel": null, - "divider": null, - "fieldSuffix": null, "disableOnProperty": "valueBox", - "rowClass": "", - "fieldClass": "", - "min": null, - "max": null, - "step": null + "disabled": false, + "visible": true }, { "id": "valueTextColor", "name": "{i18n:scada.symbol.value-text}", "type": "color", "default": "#0000008A", - "required": null, - "subLabel": null, - "divider": null, - "fieldSuffix": null, "disableOnProperty": "valueBox", - "rowClass": "", - "fieldClass": "", - "min": null, - "max": null, - "step": null + "disabled": false, + "visible": true }, { "id": "transparent", "name": "{i18n:scada.symbol.transparent-mode}", "type": "switch", "default": false, - "required": null, - "subLabel": null, - "divider": null, - "fieldSuffix": null, - "disableOnProperty": null, - "rowClass": "", - "fieldClass": "", - "min": null, - "max": null, - "step": null + "disabled": false, + "visible": true }, { "id": "scale", "name": "{i18n:scada.symbol.scale}", "type": "switch", "default": false, - "required": null, - "subLabel": null, - "divider": null, - "fieldSuffix": null, "disableOnProperty": "transparent", - "rowClass": "", - "fieldClass": "", - "min": null, - "max": null, - "step": null + "disabled": false, + "visible": true + }, + { + "id": "scaleDisplayFormat", + "name": "{i18n:scada.symbol.scale}", + "type": "select", + "default": true, + "subLabel": "{i18n:scada.symbol.display-format}", + "disableOnProperty": "scale", + "items": [ + { + "value": true, + "label": "Percentage" + }, + { + "value": false, + "label": "Metric" + } + ], + "disabled": false, + "visible": true }, { "id": "majorIntervals", "name": "{i18n:scada.symbol.major-ticks}", "type": "number", "default": 10, - "required": null, "subLabel": "{i18n:scada.symbol.intervals}", "divider": true, - "fieldSuffix": null, "disableOnProperty": "scale", - "rowClass": "", - "fieldClass": "", "min": 1, - "max": null, - "step": 1 + "step": 1, + "disabled": false, + "visible": true + }, + { + "id": "majorUnits", + "name": "{i18n:scada.symbol.major-ticks}", + "type": "units", + "subLabel": "{i18n:scada.symbol.units}", + "divider": true, + "disableOnProperty": "scale", + "disabled": false, + "visible": true }, { "id": "majorFont", @@ -446,128 +407,84 @@ "weight": "500", "style": "normal" }, - "required": null, - "subLabel": null, - "divider": null, - "fieldSuffix": null, "disableOnProperty": "scale", - "rowClass": "", - "fieldClass": "", - "min": null, - "max": null, - "step": null + "disabled": false, + "visible": true }, { "id": "majorColor", "name": "{i18n:scada.symbol.major-ticks-color}", "type": "color", "default": "#00000061", - "required": null, "subLabel": "{i18n:scada.symbol.normal}", "divider": true, - "fieldSuffix": null, "disableOnProperty": "scale", - "rowClass": "", - "fieldClass": "", - "min": null, - "max": null, - "step": null + "disabled": false, + "visible": true }, { "id": "majorWarningColor", "name": "{i18n:scada.symbol.major-ticks-color}", "type": "color", "default": "#FAA405", - "required": null, "subLabel": "{i18n:scada.symbol.warning}", "divider": true, - "fieldSuffix": null, "disableOnProperty": "scale", - "rowClass": "", - "fieldClass": "", - "min": null, - "max": null, - "step": null + "disabled": false, + "visible": true }, { "id": "majorCriticalColor", "name": "{i18n:scada.symbol.major-ticks-color}", "type": "color", "default": "#D12730", - "required": null, "subLabel": "{i18n:scada.symbol.critical}", - "divider": null, - "fieldSuffix": null, "disableOnProperty": "scale", - "rowClass": "", - "fieldClass": "", - "min": null, - "max": null, - "step": null + "disabled": false, + "visible": true }, { "id": "minorIntervals", "name": "{i18n:scada.symbol.minor-ticks}", "type": "number", "default": 5, - "required": null, "subLabel": "{i18n:scada.symbol.intervals}", - "divider": null, - "fieldSuffix": null, "disableOnProperty": "scale", - "rowClass": "", - "fieldClass": "", "min": 1, - "max": null, - "step": null + "disabled": false, + "visible": true }, { "id": "minorColor", "name": "{i18n:scada.symbol.minor-ticks-color}", "type": "color", "default": "#0000001F", - "required": null, "subLabel": "{i18n:scada.symbol.normal}", "divider": true, - "fieldSuffix": null, "disableOnProperty": "scale", - "rowClass": "", - "fieldClass": "", - "min": null, - "max": null, - "step": null + "disabled": false, + "visible": true }, { "id": "minorWarningColor", "name": "{i18n:scada.symbol.minor-ticks-color}", "type": "color", "default": "#FAA405", - "required": null, "subLabel": "{i18n:scada.symbol.warning}", "divider": true, - "fieldSuffix": null, "disableOnProperty": "scale", - "rowClass": "", - "fieldClass": "", - "min": null, - "max": null, - "step": null + "disabled": false, + "visible": true }, { "id": "minorCriticalColor", "name": "{i18n:scada.symbol.minor-ticks-color}", "type": "color", "default": "#D12730", - "required": null, "subLabel": "{i18n:scada.symbol.critical}", - "divider": null, - "fieldSuffix": null, "disableOnProperty": "scale", - "rowClass": "", - "fieldClass": "", - "min": null, - "max": null, - "step": null + "disabled": false, + "visible": true } ] }]]> diff --git a/application/src/main/data/json/system/scada_symbols/horizontal-tank-hp.svg b/application/src/main/data/json/system/scada_symbols/horizontal-tank-hp.svg index cff6a97eb9..2adde5881f 100644 --- a/application/src/main/data/json/system/scada_symbols/horizontal-tank-hp.svg +++ b/application/src/main/data/json/system/scada_symbols/horizontal-tank-hp.svg @@ -38,7 +38,7 @@ }, { "tag": "scale", - "stateRenderFunction": "if (!ctx.properties.scale) {\n element.hide();\n} else {\n var scaleSet = element.remember('scaleSet');\n if (!scaleSet) {\n element.remember('scaleSet', true);\n element.clear();\n \n var majorIntervals = ctx.properties.majorIntervals;\n var minorIntervals = ctx.properties.minorIntervals;\n \n var start = 3;\n var majorIntervalLength = 592 / majorIntervals;\n var minorIntervalLength = majorIntervalLength / minorIntervals;\n for (var i = 0; i < majorIntervals + 1; i++) {\n var y = start + i * majorIntervalLength;\n var line = ctx.svg.line(208, y, 240, y).stroke({ width: 3 }).attr({class: 'majorTick'});\n element.add(line);\n var majorText = (100 - i * (100/majorIntervals)).toFixed(0);\n var majorTickText = ctx.svg.text(majorText);\n if (i === 0) {\n majorTickText.attr({x: 198, y: y + 10, 'text-anchor': 'end', class: 'majorTickText'});\n } else if (i === majorIntervals) {\n majorTickText.attr({x: 198, y: y - 5, 'text-anchor': 'end', class: 'majorTickText'});\n } else {\n majorTickText.attr({x: 198, y: y + 2, 'text-anchor': 'end', class: 'majorTickText'});\n }\n majorTickText.first().attr({'dominant-baseline': 'middle'});\n element.add(majorTickText);\n if (i < majorIntervals) {\n drawMinorTicks(y, minorIntervals, minorIntervalLength);\n }\n }\n }\n \n var majorFont = ctx.properties.majorFont;\n var majorColor = ctx.properties.majorColor;\n var minorColor = ctx.properties.minorColor;\n \n var majorTicks = element.find('line.majorTick');\n majorTicks.forEach(t => t.attr({stroke: majorColor}));\n \n var majorTicksText = element.find('text.majorTickText');\n ctx.api.font(majorTicksText, majorFont, majorColor);\n \n var minorTicks = element.find('line.minorTick');\n minorTicks.forEach(t => t.attr({stroke: minorColor}));\n}\n\nfunction drawMinorTicks(start, minorIntervals, minorIntervalLength) {\n for (var i = 1; i < minorIntervals; i++) {\n var minorY = start + i * minorIntervalLength;\n var minorLine = ctx.svg.line(220, minorY, 240, minorY).stroke({ width: 3 }).attr({class: 'minorTick'});\n element.add(minorLine);\n }\n}", + "stateRenderFunction": "if (!ctx.properties.scale) {\n element.hide();\n} else {\n var scaleSet = element.remember('scaleSet');\n if (!scaleSet) {\n element.remember('scaleSet', true);\n element.clear();\n \n var majorIntervals = ctx.properties.majorIntervals;\n var minorIntervals = ctx.properties.minorIntervals;\n \n var start = 3;\n var majorIntervalLength = 592 / majorIntervals;\n var minorIntervalLength = majorIntervalLength / minorIntervals;\n var tankCapacity = ctx.properties.scaleDisplayFormat ? 100 : (ctx.values.tankCapacity || 100);\n for (var i = 0; i < majorIntervals + 1; i++) {\n var y = start + i * majorIntervalLength;\n var line = ctx.svg.line(208, y, 240, y).stroke({ width: 3 }).attr({class: 'majorTick'});\n element.add(line);\n var majorText = ctx.api.formatValue((tankCapacity - i * (tankCapacity/majorIntervals)).toFixed(0), 0, ctx.properties.majorUnits, false);\n var majorTickText = ctx.svg.text(majorText);\n if (i === 0) {\n majorTickText.attr({x: 198, y: y + 10, 'text-anchor': 'end', class: 'majorTickText'});\n } else if (i === majorIntervals) {\n majorTickText.attr({x: 198, y: y - 5, 'text-anchor': 'end', class: 'majorTickText'});\n } else {\n majorTickText.attr({x: 198, y: y + 2, 'text-anchor': 'end', class: 'majorTickText'});\n }\n majorTickText.first().attr({'dominant-baseline': 'middle'});\n element.add(majorTickText);\n if (i < majorIntervals) {\n drawMinorTicks(y, minorIntervals, minorIntervalLength);\n }\n }\n }\n \n var majorFont = ctx.properties.majorFont;\n var majorColor = ctx.properties.majorColor;\n var minorColor = ctx.properties.minorColor;\n \n var majorTicks = element.find('line.majorTick');\n majorTicks.forEach(t => t.attr({stroke: majorColor}));\n \n var majorTicksText = element.find('text.majorTickText');\n ctx.api.font(majorTicksText, majorFont, majorColor);\n \n var minorTicks = element.find('line.minorTick');\n minorTicks.forEach(t => t.attr({stroke: minorColor}));\n}\n\nfunction drawMinorTicks(start, minorIntervals, minorIntervalLength) {\n for (var i = 1; i < minorIntervals; i++) {\n var minorY = start + i * minorIntervalLength;\n var minorLine = ctx.svg.line(220, minorY, 240, minorY).stroke({ width: 3 }).attr({class: 'minorTick'});\n element.add(minorLine);\n }\n}", "actions": null }, { @@ -307,64 +307,67 @@ "name": "{i18n:scada.symbol.tank-color}", "type": "color", "default": "#EBEBEB", - "required": null, - "subLabel": null, - "divider": null, - "fieldSuffix": null, - "disableOnProperty": null, - "rowClass": "", - "fieldClass": "", - "min": null, - "max": null, - "step": null + "disabled": false, + "visible": true }, { "id": "fluidColor", "name": "{i18n:scada.symbol.fluid-color}", "type": "color", "default": "#C8DFF7", - "required": null, - "subLabel": null, - "divider": null, - "fieldSuffix": null, - "disableOnProperty": null, - "rowClass": "", - "fieldClass": "", - "min": null, - "max": null, - "step": null + "disabled": false, + "visible": true }, { "id": "scale", "name": "{i18n:scada.symbol.scale}", "type": "switch", "default": true, - "required": null, - "subLabel": null, - "divider": null, - "fieldSuffix": null, - "disableOnProperty": null, - "rowClass": "", - "fieldClass": "", - "min": null, - "max": null, - "step": null + "disabled": false, + "visible": true + }, + { + "id": "scaleDisplayFormat", + "name": "{i18n:scada.symbol.scale}", + "type": "select", + "default": true, + "subLabel": "{i18n:scada.symbol.display-format}", + "disableOnProperty": "scale", + "items": [ + { + "value": true, + "label": "Percentage" + }, + { + "value": false, + "label": "Metric" + } + ], + "disabled": false, + "visible": true }, { "id": "majorIntervals", "name": "{i18n:scada.symbol.major-ticks}", "type": "number", "default": 10, - "required": null, "subLabel": "{i18n:scada.symbol.intervals}", "divider": false, - "fieldSuffix": null, "disableOnProperty": "scale", - "rowClass": "", - "fieldClass": "", "min": 1, - "max": null, - "step": 1 + "step": 1, + "disabled": false, + "visible": true + }, + { + "id": "majorUnits", + "name": "{i18n:scada.symbol.major-ticks}", + "type": "units", + "subLabel": "{i18n:scada.symbol.units}", + "divider": true, + "disableOnProperty": "scale", + "disabled": false, + "visible": true }, { "id": "majorFont", @@ -377,96 +380,57 @@ "weight": "500", "style": "normal" }, - "required": null, - "subLabel": null, - "divider": null, - "fieldSuffix": null, "disableOnProperty": "scale", - "rowClass": "", - "fieldClass": "", - "min": null, - "max": null, - "step": null + "disabled": false, + "visible": true }, { "id": "majorColor", "name": "{i18n:scada.symbol.major-ticks}", "type": "color", "default": "#00000061", - "required": null, - "subLabel": null, - "divider": null, - "fieldSuffix": null, "disableOnProperty": "scale", - "rowClass": "", - "fieldClass": "", - "min": null, - "max": null, - "step": null + "disabled": false, + "visible": true }, { "id": "minorIntervals", "name": "{i18n:scada.symbol.minor-ticks}", "type": "number", "default": 5, - "required": null, "subLabel": "{i18n:scada.symbol.intervals}", - "divider": null, - "fieldSuffix": null, "disableOnProperty": "scale", - "rowClass": "", - "fieldClass": "", "min": 1, - "max": null, - "step": null + "disabled": false, + "visible": true }, { "id": "minorColor", "name": "{i18n:scada.symbol.minor-ticks}", "type": "color", "default": "#0000001F", - "required": null, - "subLabel": null, - "divider": null, - "fieldSuffix": null, "disableOnProperty": "scale", - "rowClass": "", - "fieldClass": "", - "min": null, - "max": null, - "step": null + "disabled": false, + "visible": true }, { "id": "warningColor", "name": "{i18n:scada.symbol.alarm-colors}", "type": "color", "default": "#FAA405", - "required": null, "subLabel": "{i18n:scada.symbol.warning}", "divider": true, - "fieldSuffix": null, - "disableOnProperty": null, - "rowClass": "", - "fieldClass": "", - "min": null, - "max": null, - "step": null + "disabled": false, + "visible": true }, { "id": "criticalColor", "name": "{i18n:scada.symbol.alarm-colors}", "type": "color", "default": "#D12730", - "required": null, "subLabel": "{i18n:scada.symbol.critical}", - "divider": null, - "fieldSuffix": null, - "disableOnProperty": null, - "rowClass": "", - "fieldClass": "", - "min": null, - "max": null, - "step": null + "disabled": false, + "visible": true } ] }]]> diff --git a/application/src/main/data/json/system/scada_symbols/horizontal-tank.svg b/application/src/main/data/json/system/scada_symbols/horizontal-tank.svg index 7875b5dde0..f410d709f4 100644 --- a/application/src/main/data/json/system/scada_symbols/horizontal-tank.svg +++ b/application/src/main/data/json/system/scada_symbols/horizontal-tank.svg @@ -33,7 +33,7 @@ }, { "tag": "scale", - "stateRenderFunction": "if (!ctx.properties.scale) {\n element.hide();\n} else {\n var scaleSet = element.remember('scaleSet');\n if (!scaleSet) {\n element.remember('scaleSet', true);\n element.clear();\n \n var majorIntervals = ctx.properties.majorIntervals;\n var minorIntervals = ctx.properties.minorIntervals;\n \n var start = 17;\n var majorIntervalLength = 568 / majorIntervals;\n var minorIntervalLength = majorIntervalLength / minorIntervals;\n for (var i = 0; i < majorIntervals + 1; i++) {\n var y = start + i * majorIntervalLength;\n var line = ctx.svg.line(715, y, 747, y).stroke({ width: 3 }).attr({class: 'majorTick'});\n element.add(line);\n var majorText = (100 - i * (100/majorIntervals)).toFixed(0);\n var majorTickText = ctx.svg.text(majorText);\n majorTickText.attr({x: 705, y: y + 2, 'text-anchor': 'end', class: 'majorTickText'});\n majorTickText.first().attr({'dominant-baseline': 'middle'});\n element.add(majorTickText);\n if (i < majorIntervals) {\n drawMinorTicks(y, minorIntervals, minorIntervalLength);\n }\n }\n }\n \n var majorFont = ctx.properties.majorFont;\n var majorColor = ctx.properties.majorColor;\n var minorColor = ctx.properties.minorColor;\n if (ctx.values.critical) {\n majorColor = ctx.properties.majorCriticalColor;\n minorColor = ctx.properties.minorCriticalColor;\n } else if (ctx.values.warning) {\n majorColor = ctx.properties.minorWarningColor;\n minorColor = ctx.properties.minorWarningColor;\n }\n \n var majorTicks = element.find('line.majorTick');\n majorTicks.forEach(t => t.attr({stroke: majorColor}));\n \n var majorTicksText = element.find('text.majorTickText');\n ctx.api.font(majorTicksText, majorFont, majorColor);\n \n var minorTicks = element.find('line.minorTick');\n minorTicks.forEach(t => t.attr({stroke: minorColor}));\n \n var elementCriticalAnimation = element.remember('criticalAnimation');\n var criticalAnimation = ctx.values.critical && ctx.values.criticalAnimation;\n\n if (elementCriticalAnimation !== criticalAnimation) {\n element.remember('criticalAnimation', criticalAnimation);\n if (criticalAnimation) {\n ctx.api.cssAnimate(element, 500).attr({opacity: 0.15}).loop(0, true);\n } else {\n ctx.api.resetCssAnimation(element);\n }\n }\n}\n\nfunction drawMinorTicks(start, minorIntervals, minorIntervalLength) {\n for (var i = 1; i < minorIntervals; i++) {\n var minorY = start + i * minorIntervalLength;\n var minorLine = ctx.svg.line(727, minorY, 747, minorY).stroke({ width: 3 }).attr({class: 'minorTick'});\n element.add(minorLine);\n }\n}", + "stateRenderFunction": "if (!ctx.properties.scale) {\n element.hide();\n} else {\n var scaleSet = element.remember('scaleSet');\n if (!scaleSet) {\n element.remember('scaleSet', true);\n element.clear();\n \n var majorIntervals = ctx.properties.majorIntervals;\n var minorIntervals = ctx.properties.minorIntervals;\n \n var start = 17;\n var majorIntervalLength = 568 / majorIntervals;\n var minorIntervalLength = majorIntervalLength / minorIntervals;\n var tankCapacity = ctx.properties.scaleDisplayFormat ? 100 : (ctx.values.tankCapacity || 100);\n for (var i = 0; i < majorIntervals + 1; i++) {\n var y = start + i * majorIntervalLength;\n var line = ctx.svg.line(715, y, 747, y).stroke({ width: 3 }).attr({class: 'majorTick'});\n element.add(line);\n var majorText = ctx.api.formatValue((tankCapacity - i * (tankCapacity/majorIntervals)).toFixed(0), 0, ctx.properties.majorUnits, false);\n var majorTickText = ctx.svg.text(majorText);\n majorTickText.attr({x: 705, y: y + 2, 'text-anchor': 'end', class: 'majorTickText'});\n majorTickText.first().attr({'dominant-baseline': 'middle'});\n element.add(majorTickText);\n if (i < majorIntervals) {\n drawMinorTicks(y, minorIntervals, minorIntervalLength);\n }\n }\n }\n \n var majorFont = ctx.properties.majorFont;\n var majorColor = ctx.properties.majorColor;\n var minorColor = ctx.properties.minorColor;\n if (ctx.values.critical) {\n majorColor = ctx.properties.majorCriticalColor;\n minorColor = ctx.properties.minorCriticalColor;\n } else if (ctx.values.warning) {\n majorColor = ctx.properties.minorWarningColor;\n minorColor = ctx.properties.minorWarningColor;\n }\n \n var majorTicks = element.find('line.majorTick');\n majorTicks.forEach(t => t.attr({stroke: majorColor}));\n \n var majorTicksText = element.find('text.majorTickText');\n ctx.api.font(majorTicksText, majorFont, majorColor);\n \n var minorTicks = element.find('line.minorTick');\n minorTicks.forEach(t => t.attr({stroke: minorColor}));\n \n var elementCriticalAnimation = element.remember('criticalAnimation');\n var criticalAnimation = ctx.values.critical && ctx.values.criticalAnimation;\n\n if (elementCriticalAnimation !== criticalAnimation) {\n element.remember('criticalAnimation', criticalAnimation);\n if (criticalAnimation) {\n ctx.api.cssAnimate(element, 500).attr({opacity: 0.15}).loop(0, true);\n } else {\n ctx.api.resetCssAnimation(element);\n }\n }\n}\n\nfunction drawMinorTicks(start, minorIntervals, minorIntervalLength) {\n for (var i = 1; i < minorIntervals; i++) {\n var minorY = start + i * minorIntervalLength;\n var minorLine = ctx.svg.line(727, minorY, 747, minorY).stroke({ width: 3 }).attr({class: 'minorTick'});\n element.add(minorLine);\n }\n}", "actions": null }, { @@ -278,80 +278,43 @@ "name": "{i18n:scada.symbol.tank-color}", "type": "color", "default": "#E5E5E5", - "required": null, - "subLabel": null, - "divider": null, - "fieldSuffix": null, - "disableOnProperty": null, - "rowClass": "", - "fieldClass": "", - "min": null, - "max": null, - "step": null + "disabled": false, + "visible": true }, { "id": "fluidColor", "name": "{i18n:scada.symbol.fluid-color}", "type": "color", "default": "#1EC1F480", - "required": null, - "subLabel": null, - "divider": null, - "fieldSuffix": null, - "disableOnProperty": null, - "rowClass": "", - "fieldClass": "", - "min": null, - "max": null, - "step": null + "disabled": false, + "visible": true }, { "id": "valueBox", "name": "{i18n:scada.symbol.value-box}", "type": "switch", "default": true, - "required": null, - "subLabel": null, - "divider": null, - "fieldSuffix": null, - "disableOnProperty": null, - "rowClass": "", - "fieldClass": "", - "min": null, - "max": null, - "step": null + "disabled": false, + "visible": true }, { "id": "valueBoxColor", "name": "{i18n:scada.symbol.value-box}", "type": "color", "default": "#F3F3F3", - "required": null, - "subLabel": null, - "divider": null, - "fieldSuffix": null, "disableOnProperty": "valueBox", - "rowClass": "", - "fieldClass": "", - "min": null, - "max": null, - "step": null + "disabled": false, + "visible": true }, { "id": "valueUnits", "name": "{i18n:scada.symbol.value-text}", "type": "units", "default": "gal", - "required": null, "subLabel": "{i18n:scada.symbol.units}", - "divider": null, - "fieldSuffix": null, "disableOnProperty": "valueBox", - "rowClass": "", - "fieldClass": "", - "min": null, - "max": null, - "step": null + "disabled": false, + "visible": true }, { "id": "valueTextFont", @@ -364,80 +327,78 @@ "weight": "500", "style": "normal" }, - "required": null, - "subLabel": null, - "divider": null, - "fieldSuffix": null, "disableOnProperty": "valueBox", - "rowClass": "", - "fieldClass": "", - "min": null, - "max": null, - "step": null + "disabled": false, + "visible": true }, { "id": "valueTextColor", "name": "{i18n:scada.symbol.value-text}", "type": "color", "default": "#0000008A", - "required": null, - "subLabel": null, - "divider": null, - "fieldSuffix": null, "disableOnProperty": "valueBox", - "rowClass": "", - "fieldClass": "", - "min": null, - "max": null, - "step": null + "disabled": false, + "visible": true }, { "id": "scale", "name": "{i18n:scada.symbol.scale}", "type": "switch", "default": true, - "required": null, - "subLabel": null, - "divider": null, - "fieldSuffix": null, - "disableOnProperty": null, - "rowClass": "", - "fieldClass": "", - "min": null, - "max": null, - "step": null + "disabled": false, + "visible": true + }, + { + "id": "scaleDisplayFormat", + "name": "{i18n:scada.symbol.scale}", + "type": "select", + "default": true, + "subLabel": "{i18n:scada.symbol.display-format}", + "disableOnProperty": "scale", + "items": [ + { + "value": true, + "label": "Percentage" + }, + { + "value": false, + "label": "Metric" + } + ], + "disabled": false, + "visible": true }, { "id": "transparent", "name": "{i18n:scada.symbol.transparent-mode}", "type": "switch", "default": false, - "required": null, - "subLabel": null, - "divider": null, - "fieldSuffix": null, "disableOnProperty": "scale", - "rowClass": "", - "fieldClass": "", - "min": null, - "max": null, - "step": null + "disabled": false, + "visible": true }, { "id": "majorIntervals", "name": "{i18n:scada.symbol.major-ticks}", "type": "number", "default": 10, - "required": null, "subLabel": "{i18n:scada.symbol.intervals}", "divider": true, - "fieldSuffix": null, "disableOnProperty": "scale", - "rowClass": "", - "fieldClass": "", "min": 1, - "max": null, - "step": 1 + "step": 1, + "disabled": false, + "visible": true + }, + { + "id": "majorUnits", + "name": "{i18n:scada.symbol.major-ticks}", + "type": "units", + "subLabel": "{i18n:scada.symbol.units}", + "divider": true, + "disableOnProperty": "scale", + "disabled": false, + "visible": true }, { "id": "majorFont", @@ -450,128 +411,84 @@ "weight": "500", "style": "normal" }, - "required": null, - "subLabel": null, - "divider": null, - "fieldSuffix": null, "disableOnProperty": "scale", - "rowClass": "", - "fieldClass": "", - "min": null, - "max": null, - "step": null + "disabled": false, + "visible": true }, { "id": "majorColor", "name": "{i18n:scada.symbol.major-ticks-color}", "type": "color", "default": "#00000061", - "required": null, "subLabel": "{i18n:scada.symbol.normal}", "divider": true, - "fieldSuffix": null, "disableOnProperty": "scale", - "rowClass": "", - "fieldClass": "", - "min": null, - "max": null, - "step": null + "disabled": false, + "visible": true }, { "id": "majorWarningColor", "name": "{i18n:scada.symbol.major-ticks-color}", "type": "color", "default": "#FAA405", - "required": null, "subLabel": "{i18n:scada.symbol.warning}", "divider": true, - "fieldSuffix": null, "disableOnProperty": "scale", - "rowClass": "", - "fieldClass": "", - "min": null, - "max": null, - "step": null + "disabled": false, + "visible": true }, { "id": "majorCriticalColor", "name": "{i18n:scada.symbol.major-ticks-color}", "type": "color", "default": "#D12730", - "required": null, "subLabel": "{i18n:scada.symbol.critical}", - "divider": null, - "fieldSuffix": null, "disableOnProperty": "scale", - "rowClass": "", - "fieldClass": "", - "min": null, - "max": null, - "step": null + "disabled": false, + "visible": true }, { "id": "minorIntervals", "name": "{i18n:scada.symbol.minor-ticks}", "type": "number", "default": 5, - "required": null, "subLabel": "{i18n:scada.symbol.intervals}", - "divider": null, - "fieldSuffix": null, "disableOnProperty": "scale", - "rowClass": "", - "fieldClass": "", "min": 1, - "max": null, - "step": null + "disabled": false, + "visible": true }, { "id": "minorColor", "name": "{i18n:scada.symbol.minor-ticks-color}", "type": "color", "default": "#0000001F", - "required": null, "subLabel": "{i18n:scada.symbol.normal}", "divider": true, - "fieldSuffix": null, "disableOnProperty": "scale", - "rowClass": "", - "fieldClass": "", - "min": null, - "max": null, - "step": null + "disabled": false, + "visible": true }, { "id": "minorWarningColor", "name": "{i18n:scada.symbol.minor-ticks-color}", "type": "color", "default": "#FAA405", - "required": null, "subLabel": "{i18n:scada.symbol.warning}", "divider": true, - "fieldSuffix": null, "disableOnProperty": "scale", - "rowClass": "", - "fieldClass": "", - "min": null, - "max": null, - "step": null + "disabled": false, + "visible": true }, { "id": "minorCriticalColor", "name": "{i18n:scada.symbol.minor-ticks-color}", "type": "color", "default": "#D12730", - "required": null, "subLabel": "{i18n:scada.symbol.critical}", - "divider": null, - "fieldSuffix": null, "disableOnProperty": "scale", - "rowClass": "", - "fieldClass": "", - "min": null, - "max": null, - "step": null + "disabled": false, + "visible": true } ] }]]> diff --git a/application/src/main/data/json/system/scada_symbols/large-cylindrical-tank.svg b/application/src/main/data/json/system/scada_symbols/large-cylindrical-tank.svg index 772e34f0f7..98f61f0094 100644 --- a/application/src/main/data/json/system/scada_symbols/large-cylindrical-tank.svg +++ b/application/src/main/data/json/system/scada_symbols/large-cylindrical-tank.svg @@ -33,7 +33,7 @@ }, { "tag": "scale", - "stateRenderFunction": "if (!ctx.properties.scale) {\n element.hide();\n} else {\n var scaleSet = element.remember('scaleSet');\n if (!scaleSet) {\n element.remember('scaleSet', true);\n element.clear();\n \n var majorIntervals = ctx.properties.majorIntervals;\n var minorIntervals = ctx.properties.minorIntervals;\n \n var start = 60;\n var majorIntervalLength = 910 / majorIntervals;\n var minorIntervalLength = majorIntervalLength / minorIntervals;\n for (var i = 0; i < majorIntervals + 1; i++) {\n var y = start + i * majorIntervalLength;\n var line = ctx.svg.line(656, y, 688, y).stroke({ width: 3 }).attr({class: 'majorTick'});\n element.add(line);\n var majorText = (100 - i * (100/majorIntervals)).toFixed(0);\n var majorTickText = ctx.svg.text(majorText);\n majorTickText.attr({x: 646, y: y + 2, 'text-anchor': 'end', class: 'majorTickText'});\n majorTickText.first().attr({'dominant-baseline': 'middle'});\n element.add(majorTickText);\n if (i < majorIntervals) {\n drawMinorTicks(y, minorIntervals, minorIntervalLength);\n }\n }\n }\n \n var majorFont = ctx.properties.majorFont;\n var majorColor = ctx.properties.majorColor;\n var minorColor = ctx.properties.minorColor;\n if (ctx.values.critical) {\n majorColor = ctx.properties.majorCriticalColor;\n minorColor = ctx.properties.minorCriticalColor;\n } else if (ctx.values.warning) {\n majorColor = ctx.properties.minorWarningColor;\n minorColor = ctx.properties.minorWarningColor;\n }\n \n var majorTicks = element.find('line.majorTick');\n majorTicks.forEach(t => t.attr({stroke: majorColor}));\n \n var majorTicksText = element.find('text.majorTickText');\n ctx.api.font(majorTicksText, majorFont, majorColor);\n \n var minorTicks = element.find('line.minorTick');\n minorTicks.forEach(t => t.attr({stroke: minorColor}));\n \n var elementCriticalAnimation = element.remember('criticalAnimation');\n var criticalAnimation = ctx.values.critical && ctx.values.criticalAnimation;\n\n if (elementCriticalAnimation !== criticalAnimation) {\n element.remember('criticalAnimation', criticalAnimation);\n if (criticalAnimation) {\n ctx.api.cssAnimate(element, 500).attr({opacity: 0.15}).loop(0, true);\n } else {\n ctx.api.resetCssAnimation(element);\n }\n }\n}\n\nfunction drawMinorTicks(start, minorIntervals, minorIntervalLength) {\n for (var i = 1; i < minorIntervals; i++) {\n var minorY = start + i * minorIntervalLength;\n var minorLine = ctx.svg.line(668, minorY, 688, minorY).stroke({ width: 3 }).attr({class: 'minorTick'});\n element.add(minorLine);\n }\n}", + "stateRenderFunction": "if (!ctx.properties.scale) {\n element.hide();\n} else {\n var scaleSet = element.remember('scaleSet');\n if (!scaleSet) {\n element.remember('scaleSet', true);\n element.clear();\n \n var majorIntervals = ctx.properties.majorIntervals;\n var minorIntervals = ctx.properties.minorIntervals;\n \n var start = 60;\n var majorIntervalLength = 910 / majorIntervals;\n var minorIntervalLength = majorIntervalLength / minorIntervals;\n var tankCapacity = ctx.properties.scaleDisplayFormat ? 100 : (ctx.values.tankCapacity || 100);\n for (var i = 0; i < majorIntervals + 1; i++) {\n var y = start + i * majorIntervalLength;\n var line = ctx.svg.line(656, y, 688, y).stroke({ width: 3 }).attr({class: 'majorTick'});\n element.add(line);\n var majorText = ctx.api.formatValue((tankCapacity - i * (tankCapacity/majorIntervals)).toFixed(0), 0, ctx.properties.majorUnits, false);\n var majorTickText = ctx.svg.text(majorText);\n majorTickText.attr({x: 646, y: y + 2, 'text-anchor': 'end', class: 'majorTickText'});\n majorTickText.first().attr({'dominant-baseline': 'middle'});\n element.add(majorTickText);\n if (i < majorIntervals) {\n drawMinorTicks(y, minorIntervals, minorIntervalLength);\n }\n }\n }\n \n var majorFont = ctx.properties.majorFont;\n var majorColor = ctx.properties.majorColor;\n var minorColor = ctx.properties.minorColor;\n if (ctx.values.critical) {\n majorColor = ctx.properties.majorCriticalColor;\n minorColor = ctx.properties.minorCriticalColor;\n } else if (ctx.values.warning) {\n majorColor = ctx.properties.minorWarningColor;\n minorColor = ctx.properties.minorWarningColor;\n }\n \n var majorTicks = element.find('line.majorTick');\n majorTicks.forEach(t => t.attr({stroke: majorColor}));\n \n var majorTicksText = element.find('text.majorTickText');\n ctx.api.font(majorTicksText, majorFont, majorColor);\n \n var minorTicks = element.find('line.minorTick');\n minorTicks.forEach(t => t.attr({stroke: minorColor}));\n \n var elementCriticalAnimation = element.remember('criticalAnimation');\n var criticalAnimation = ctx.values.critical && ctx.values.criticalAnimation;\n\n if (elementCriticalAnimation !== criticalAnimation) {\n element.remember('criticalAnimation', criticalAnimation);\n if (criticalAnimation) {\n ctx.api.cssAnimate(element, 500).attr({opacity: 0.15}).loop(0, true);\n } else {\n ctx.api.resetCssAnimation(element);\n }\n }\n}\n\nfunction drawMinorTicks(start, minorIntervals, minorIntervalLength) {\n for (var i = 1; i < minorIntervals; i++) {\n var minorY = start + i * minorIntervalLength;\n var minorLine = ctx.svg.line(668, minorY, 688, minorY).stroke({ width: 3 }).attr({class: 'minorTick'});\n element.add(minorLine);\n }\n}", "actions": null }, { @@ -278,80 +278,43 @@ "name": "{i18n:scada.symbol.tank-color}", "type": "color", "default": "#E5E5E5", - "required": null, - "subLabel": null, - "divider": null, - "fieldSuffix": null, - "disableOnProperty": null, - "rowClass": "", - "fieldClass": "", - "min": null, - "max": null, - "step": null + "disabled": false, + "visible": true }, { "id": "fluidColor", "name": "{i18n:scada.symbol.fluid-color}", "type": "color", "default": "#1EC1F480", - "required": null, - "subLabel": null, - "divider": null, - "fieldSuffix": null, - "disableOnProperty": null, - "rowClass": "", - "fieldClass": "", - "min": null, - "max": null, - "step": null + "disabled": false, + "visible": true }, { "id": "valueBox", "name": "{i18n:scada.symbol.value-box}", "type": "switch", "default": true, - "required": null, - "subLabel": null, - "divider": null, - "fieldSuffix": null, - "disableOnProperty": null, - "rowClass": "", - "fieldClass": "", - "min": null, - "max": null, - "step": null + "disabled": false, + "visible": true }, { "id": "valueBoxColor", "name": "{i18n:scada.symbol.value-box}", "type": "color", "default": "#F3F3F3", - "required": null, - "subLabel": null, - "divider": null, - "fieldSuffix": null, "disableOnProperty": "valueBox", - "rowClass": "", - "fieldClass": "", - "min": null, - "max": null, - "step": null + "disabled": false, + "visible": true }, { "id": "valueUnits", "name": "{i18n:scada.symbol.value-text}", "type": "units", "default": "gal", - "required": null, "subLabel": "{i18n:scada.symbol.units}", - "divider": null, - "fieldSuffix": null, "disableOnProperty": "valueBox", - "rowClass": "", - "fieldClass": "", - "min": null, - "max": null, - "step": null + "disabled": false, + "visible": true }, { "id": "valueTextFont", @@ -364,80 +327,78 @@ "weight": "500", "style": "normal" }, - "required": null, - "subLabel": null, - "divider": null, - "fieldSuffix": null, "disableOnProperty": "valueBox", - "rowClass": "", - "fieldClass": "", - "min": null, - "max": null, - "step": null + "disabled": false, + "visible": true }, { "id": "valueTextColor", "name": "{i18n:scada.symbol.value-text}", "type": "color", "default": "#0000008A", - "required": null, - "subLabel": null, - "divider": null, - "fieldSuffix": null, "disableOnProperty": "valueBox", - "rowClass": "", - "fieldClass": "", - "min": null, - "max": null, - "step": null + "disabled": false, + "visible": true }, { "id": "scale", "name": "{i18n:scada.symbol.scale}", "type": "switch", "default": true, - "required": null, - "subLabel": null, - "divider": null, - "fieldSuffix": null, - "disableOnProperty": null, - "rowClass": "", - "fieldClass": "", - "min": null, - "max": null, - "step": null + "disabled": false, + "visible": true + }, + { + "id": "scaleDisplayFormat", + "name": "{i18n:scada.symbol.scale}", + "type": "select", + "default": true, + "subLabel": "{i18n:scada.symbol.display-format}", + "disableOnProperty": "scale", + "items": [ + { + "value": true, + "label": "Percentage" + }, + { + "value": false, + "label": "Metric" + } + ], + "disabled": false, + "visible": true }, { "id": "transparent", "name": "{i18n:scada.symbol.transparent-mode}", "type": "switch", "default": false, - "required": null, - "subLabel": null, - "divider": null, - "fieldSuffix": null, "disableOnProperty": "scale", - "rowClass": "", - "fieldClass": "", - "min": null, - "max": null, - "step": null + "disabled": false, + "visible": true }, { "id": "majorIntervals", "name": "{i18n:scada.symbol.major-ticks}", "type": "number", "default": 10, - "required": null, "subLabel": "{i18n:scada.symbol.intervals}", "divider": true, - "fieldSuffix": null, "disableOnProperty": "scale", - "rowClass": "", - "fieldClass": "", "min": 1, - "max": null, - "step": 1 + "step": 1, + "disabled": false, + "visible": true + }, + { + "id": "majorUnits", + "name": "{i18n:scada.symbol.major-ticks}", + "type": "units", + "subLabel": "{i18n:scada.symbol.units}", + "divider": true, + "disableOnProperty": "scale", + "disabled": false, + "visible": true }, { "id": "majorFont", @@ -450,128 +411,84 @@ "weight": "500", "style": "normal" }, - "required": null, - "subLabel": null, - "divider": null, - "fieldSuffix": null, "disableOnProperty": "scale", - "rowClass": "", - "fieldClass": "", - "min": null, - "max": null, - "step": null + "disabled": false, + "visible": true }, { "id": "majorColor", "name": "{i18n:scada.symbol.major-ticks-color}", "type": "color", "default": "#00000061", - "required": null, "subLabel": "{i18n:scada.symbol.normal}", "divider": true, - "fieldSuffix": null, "disableOnProperty": "scale", - "rowClass": "", - "fieldClass": "", - "min": null, - "max": null, - "step": null + "disabled": false, + "visible": true }, { "id": "majorWarningColor", "name": "{i18n:scada.symbol.major-ticks-color}", "type": "color", "default": "#FAA405", - "required": null, "subLabel": "{i18n:scada.symbol.warning}", "divider": true, - "fieldSuffix": null, "disableOnProperty": "scale", - "rowClass": "", - "fieldClass": "", - "min": null, - "max": null, - "step": null + "disabled": false, + "visible": true }, { "id": "majorCriticalColor", "name": "{i18n:scada.symbol.major-ticks-color}", "type": "color", "default": "#D12730", - "required": null, "subLabel": "{i18n:scada.symbol.critical}", - "divider": null, - "fieldSuffix": null, "disableOnProperty": "scale", - "rowClass": "", - "fieldClass": "", - "min": null, - "max": null, - "step": null + "disabled": false, + "visible": true }, { "id": "minorIntervals", "name": "{i18n:scada.symbol.minor-ticks}", "type": "number", "default": 5, - "required": null, "subLabel": "{i18n:scada.symbol.intervals}", - "divider": null, - "fieldSuffix": null, "disableOnProperty": "scale", - "rowClass": "", - "fieldClass": "", "min": 1, - "max": null, - "step": null + "disabled": false, + "visible": true }, { "id": "minorColor", "name": "{i18n:scada.symbol.minor-ticks-color}", "type": "color", "default": "#0000001F", - "required": null, "subLabel": "{i18n:scada.symbol.normal}", "divider": true, - "fieldSuffix": null, "disableOnProperty": "scale", - "rowClass": "", - "fieldClass": "", - "min": null, - "max": null, - "step": null + "disabled": false, + "visible": true }, { "id": "minorWarningColor", "name": "{i18n:scada.symbol.minor-ticks-color}", "type": "color", "default": "#FAA405", - "required": null, "subLabel": "{i18n:scada.symbol.warning}", "divider": true, - "fieldSuffix": null, "disableOnProperty": "scale", - "rowClass": "", - "fieldClass": "", - "min": null, - "max": null, - "step": null + "disabled": false, + "visible": true }, { "id": "minorCriticalColor", "name": "{i18n:scada.symbol.minor-ticks-color}", "type": "color", "default": "#D12730", - "required": null, "subLabel": "{i18n:scada.symbol.critical}", - "divider": null, - "fieldSuffix": null, "disableOnProperty": "scale", - "rowClass": "", - "fieldClass": "", - "min": null, - "max": null, - "step": null + "disabled": false, + "visible": true } ] }]]> diff --git a/application/src/main/data/json/system/scada_symbols/large-stand-cylindrical-tank.svg b/application/src/main/data/json/system/scada_symbols/large-stand-cylindrical-tank.svg index dc9cb47268..ac94438463 100644 --- a/application/src/main/data/json/system/scada_symbols/large-stand-cylindrical-tank.svg +++ b/application/src/main/data/json/system/scada_symbols/large-stand-cylindrical-tank.svg @@ -34,7 +34,7 @@ }, { "tag": "scale", - "stateRenderFunction": "if (!ctx.properties.scale) {\n element.hide();\n} else {\n var scaleSet = element.remember('scaleSet');\n if (!scaleSet) {\n element.remember('scaleSet', true);\n element.clear();\n \n var majorIntervals = ctx.properties.majorIntervals;\n var minorIntervals = ctx.properties.minorIntervals;\n \n var start = 60;\n var majorIntervalLength = 910 / majorIntervals;\n var minorIntervalLength = majorIntervalLength / minorIntervals;\n for (var i = 0; i < majorIntervals + 1; i++) {\n var y = start + i * majorIntervalLength;\n var line = ctx.svg.line(656, y, 688, y).stroke({ width: 3 }).attr({class: 'majorTick'});\n element.add(line);\n var majorText = (100 - i * (100/majorIntervals)).toFixed(0);\n var majorTickText = ctx.svg.text(majorText);\n majorTickText.attr({x: 646, y: y + 2, 'text-anchor': 'end', class: 'majorTickText'});\n majorTickText.first().attr({'dominant-baseline': 'middle'});\n element.add(majorTickText);\n if (i < majorIntervals) {\n drawMinorTicks(y, minorIntervals, minorIntervalLength);\n }\n }\n }\n \n var majorFont = ctx.properties.majorFont;\n var majorColor = ctx.properties.majorColor;\n var minorColor = ctx.properties.minorColor;\n if (ctx.values.critical) {\n majorColor = ctx.properties.majorCriticalColor;\n minorColor = ctx.properties.minorCriticalColor;\n } else if (ctx.values.warning) {\n majorColor = ctx.properties.minorWarningColor;\n minorColor = ctx.properties.minorWarningColor;\n }\n \n var majorTicks = element.find('line.majorTick');\n majorTicks.forEach(t => t.attr({stroke: majorColor}));\n \n var majorTicksText = element.find('text.majorTickText');\n ctx.api.font(majorTicksText, majorFont, majorColor);\n \n var minorTicks = element.find('line.minorTick');\n minorTicks.forEach(t => t.attr({stroke: minorColor}));\n \n var elementCriticalAnimation = element.remember('criticalAnimation');\n var criticalAnimation = ctx.values.critical && ctx.values.criticalAnimation;\n\n if (elementCriticalAnimation !== criticalAnimation) {\n element.remember('criticalAnimation', criticalAnimation);\n if (criticalAnimation) {\n ctx.api.cssAnimate(element, 500).attr({opacity: 0.15}).loop(0, true);\n } else {\n ctx.api.resetCssAnimation(element);\n }\n }\n}\n\nfunction drawMinorTicks(start, minorIntervals, minorIntervalLength) {\n for (var i = 1; i < minorIntervals; i++) {\n var minorY = start + i * minorIntervalLength;\n var minorLine = ctx.svg.line(668, minorY, 688, minorY).stroke({ width: 3 }).attr({class: 'minorTick'});\n element.add(minorLine);\n }\n}", + "stateRenderFunction": "if (!ctx.properties.scale) {\n element.hide();\n} else {\n var scaleSet = element.remember('scaleSet');\n if (!scaleSet) {\n element.remember('scaleSet', true);\n element.clear();\n \n var majorIntervals = ctx.properties.majorIntervals;\n var minorIntervals = ctx.properties.minorIntervals;\n \n var start = 60;\n var majorIntervalLength = 910 / majorIntervals;\n var minorIntervalLength = majorIntervalLength / minorIntervals;\n var tankCapacity = ctx.properties.scaleDisplayFormat ? 100 : (ctx.values.tankCapacity || 100);\n for (var i = 0; i < majorIntervals + 1; i++) {\n var y = start + i * majorIntervalLength;\n var line = ctx.svg.line(656, y, 688, y).stroke({ width: 3 }).attr({class: 'majorTick'});\n element.add(line);\n var majorText = ctx.api.formatValue((tankCapacity - i * (tankCapacity/majorIntervals)).toFixed(0), 0, ctx.properties.majorUnits, false);\n var majorTickText = ctx.svg.text(majorText);\n majorTickText.attr({x: 646, y: y + 2, 'text-anchor': 'end', class: 'majorTickText'});\n majorTickText.first().attr({'dominant-baseline': 'middle'});\n element.add(majorTickText);\n if (i < majorIntervals) {\n drawMinorTicks(y, minorIntervals, minorIntervalLength);\n }\n }\n }\n \n var majorFont = ctx.properties.majorFont;\n var majorColor = ctx.properties.majorColor;\n var minorColor = ctx.properties.minorColor;\n if (ctx.values.critical) {\n majorColor = ctx.properties.majorCriticalColor;\n minorColor = ctx.properties.minorCriticalColor;\n } else if (ctx.values.warning) {\n majorColor = ctx.properties.minorWarningColor;\n minorColor = ctx.properties.minorWarningColor;\n }\n \n var majorTicks = element.find('line.majorTick');\n majorTicks.forEach(t => t.attr({stroke: majorColor}));\n \n var majorTicksText = element.find('text.majorTickText');\n ctx.api.font(majorTicksText, majorFont, majorColor);\n \n var minorTicks = element.find('line.minorTick');\n minorTicks.forEach(t => t.attr({stroke: minorColor}));\n \n var elementCriticalAnimation = element.remember('criticalAnimation');\n var criticalAnimation = ctx.values.critical && ctx.values.criticalAnimation;\n\n if (elementCriticalAnimation !== criticalAnimation) {\n element.remember('criticalAnimation', criticalAnimation);\n if (criticalAnimation) {\n ctx.api.cssAnimate(element, 500).attr({opacity: 0.15}).loop(0, true);\n } else {\n ctx.api.resetCssAnimation(element);\n }\n }\n}\n\nfunction drawMinorTicks(start, minorIntervals, minorIntervalLength) {\n for (var i = 1; i < minorIntervals; i++) {\n var minorY = start + i * minorIntervalLength;\n var minorLine = ctx.svg.line(668, minorY, 688, minorY).stroke({ width: 3 }).attr({class: 'minorTick'});\n element.add(minorLine);\n }\n}", "actions": null }, { @@ -279,80 +279,43 @@ "name": "{i18n:scada.symbol.tank-color}", "type": "color", "default": "#E5E5E5", - "required": null, - "subLabel": null, - "divider": null, - "fieldSuffix": null, - "disableOnProperty": null, - "rowClass": "", - "fieldClass": "", - "min": null, - "max": null, - "step": null + "disabled": false, + "visible": true }, { "id": "fluidColor", "name": "{i18n:scada.symbol.fluid-color}", "type": "color", "default": "#1EC1F480", - "required": null, - "subLabel": null, - "divider": null, - "fieldSuffix": null, - "disableOnProperty": null, - "rowClass": "", - "fieldClass": "", - "min": null, - "max": null, - "step": null + "disabled": false, + "visible": true }, { "id": "valueBox", "name": "{i18n:scada.symbol.value-box}", "type": "switch", "default": true, - "required": null, - "subLabel": null, - "divider": null, - "fieldSuffix": null, - "disableOnProperty": null, - "rowClass": "", - "fieldClass": "", - "min": null, - "max": null, - "step": null + "disabled": false, + "visible": true }, { "id": "valueBoxColor", "name": "{i18n:scada.symbol.value-box}", "type": "color", "default": "#F3F3F3", - "required": null, - "subLabel": null, - "divider": null, - "fieldSuffix": null, "disableOnProperty": "valueBox", - "rowClass": "", - "fieldClass": "", - "min": null, - "max": null, - "step": null + "disabled": false, + "visible": true }, { "id": "valueUnits", "name": "{i18n:scada.symbol.value-text}", "type": "units", "default": "gal", - "required": null, "subLabel": "{i18n:scada.symbol.units}", - "divider": null, - "fieldSuffix": null, "disableOnProperty": "valueBox", - "rowClass": "", - "fieldClass": "", - "min": null, - "max": null, - "step": null + "disabled": false, + "visible": true }, { "id": "valueTextFont", @@ -365,80 +328,76 @@ "weight": "500", "style": "normal" }, - "required": null, - "subLabel": null, - "divider": null, - "fieldSuffix": null, "disableOnProperty": "valueBox", - "rowClass": "", - "fieldClass": "", - "min": null, - "max": null, - "step": null + "disabled": false, + "visible": true }, { "id": "valueTextColor", "name": "{i18n:scada.symbol.value-text}", "type": "color", "default": "#0000008A", - "required": null, - "subLabel": null, - "divider": null, - "fieldSuffix": null, "disableOnProperty": "valueBox", - "rowClass": "", - "fieldClass": "", - "min": null, - "max": null, - "step": null + "disabled": false, + "visible": true }, { "id": "scale", "name": "{i18n:scada.symbol.scale}", "type": "switch", "default": true, - "required": null, - "subLabel": null, - "divider": null, - "fieldSuffix": null, - "disableOnProperty": null, - "rowClass": "", - "fieldClass": "", - "min": null, - "max": null, - "step": null + "disabled": false, + "visible": true + }, + { + "id": "scaleDisplayFormat", + "name": "{i18n:scada.symbol.scale}", + "type": "select", + "default": true, + "subLabel": "{i18n:scada.symbol.display-format}", + "disableOnProperty": "scale", + "items": [ + { + "value": true, + "label": "Percentage" + }, + { + "value": false, + "label": "Metric" + } + ] }, { "id": "transparent", "name": "{i18n:scada.symbol.transparent-mode}", "type": "switch", "default": false, - "required": null, - "subLabel": null, - "divider": null, - "fieldSuffix": null, "disableOnProperty": "scale", - "rowClass": "", - "fieldClass": "", - "min": null, - "max": null, - "step": null + "disabled": false, + "visible": true }, { "id": "majorIntervals", "name": "{i18n:scada.symbol.major-ticks}", "type": "number", "default": 10, - "required": null, "subLabel": "{i18n:scada.symbol.intervals}", "divider": true, - "fieldSuffix": null, "disableOnProperty": "scale", - "rowClass": "", - "fieldClass": "", "min": 1, - "max": null, - "step": 1 + "step": 1, + "disabled": false, + "visible": true + }, + { + "id": "majorUnits", + "name": "{i18n:scada.symbol.major-ticks}", + "type": "units", + "subLabel": "{i18n:scada.symbol.units}", + "divider": true, + "disableOnProperty": "scale", + "disabled": false, + "visible": true }, { "id": "majorFont", @@ -451,128 +410,84 @@ "weight": "500", "style": "normal" }, - "required": null, - "subLabel": null, - "divider": null, - "fieldSuffix": null, "disableOnProperty": "scale", - "rowClass": "", - "fieldClass": "", - "min": null, - "max": null, - "step": null + "disabled": false, + "visible": true }, { "id": "majorColor", "name": "{i18n:scada.symbol.major-ticks-color}", "type": "color", "default": "#00000061", - "required": null, "subLabel": "{i18n:scada.symbol.normal}", "divider": true, - "fieldSuffix": null, "disableOnProperty": "scale", - "rowClass": "", - "fieldClass": "", - "min": null, - "max": null, - "step": null + "disabled": false, + "visible": true }, { "id": "majorWarningColor", "name": "{i18n:scada.symbol.major-ticks-color}", "type": "color", "default": "#FAA405", - "required": null, "subLabel": "{i18n:scada.symbol.warning}", "divider": true, - "fieldSuffix": null, "disableOnProperty": "scale", - "rowClass": "", - "fieldClass": "", - "min": null, - "max": null, - "step": null + "disabled": false, + "visible": true }, { "id": "majorCriticalColor", "name": "{i18n:scada.symbol.major-ticks-color}", "type": "color", "default": "#D12730", - "required": null, "subLabel": "{i18n:scada.symbol.critical}", - "divider": null, - "fieldSuffix": null, "disableOnProperty": "scale", - "rowClass": "", - "fieldClass": "", - "min": null, - "max": null, - "step": null + "disabled": false, + "visible": true }, { "id": "minorIntervals", "name": "{i18n:scada.symbol.minor-ticks}", "type": "number", "default": 5, - "required": null, "subLabel": "{i18n:scada.symbol.intervals}", - "divider": null, - "fieldSuffix": null, "disableOnProperty": "scale", - "rowClass": "", - "fieldClass": "", "min": 1, - "max": null, - "step": null + "disabled": false, + "visible": true }, { "id": "minorColor", "name": "{i18n:scada.symbol.minor-ticks-color}", "type": "color", "default": "#0000001F", - "required": null, "subLabel": "{i18n:scada.symbol.normal}", "divider": true, - "fieldSuffix": null, "disableOnProperty": "scale", - "rowClass": "", - "fieldClass": "", - "min": null, - "max": null, - "step": null + "disabled": false, + "visible": true }, { "id": "minorWarningColor", "name": "{i18n:scada.symbol.minor-ticks-color}", "type": "color", "default": "#FAA405", - "required": null, "subLabel": "{i18n:scada.symbol.warning}", "divider": true, - "fieldSuffix": null, "disableOnProperty": "scale", - "rowClass": "", - "fieldClass": "", - "min": null, - "max": null, - "step": null + "disabled": false, + "visible": true }, { "id": "minorCriticalColor", "name": "{i18n:scada.symbol.minor-ticks-color}", "type": "color", "default": "#D12730", - "required": null, "subLabel": "{i18n:scada.symbol.critical}", - "divider": null, - "fieldSuffix": null, "disableOnProperty": "scale", - "rowClass": "", - "fieldClass": "", - "min": null, - "max": null, - "step": null + "disabled": false, + "visible": true } ] }]]> diff --git a/application/src/main/data/json/system/scada_symbols/large-stand-vertical-tank.svg b/application/src/main/data/json/system/scada_symbols/large-stand-vertical-tank.svg index b7cb67f2a0..5b534b615c 100644 --- a/application/src/main/data/json/system/scada_symbols/large-stand-vertical-tank.svg +++ b/application/src/main/data/json/system/scada_symbols/large-stand-vertical-tank.svg @@ -34,7 +34,7 @@ }, { "tag": "scale", - "stateRenderFunction": "if (!ctx.properties.scale) {\n element.hide();\n} else {\n var scaleSet = element.remember('scaleSet');\n if (!scaleSet) {\n element.remember('scaleSet', true);\n element.clear();\n \n var majorIntervals = ctx.properties.majorIntervals;\n var minorIntervals = ctx.properties.minorIntervals;\n \n var start = 203;\n var majorIntervalLength = 763 / majorIntervals;\n var minorIntervalLength = majorIntervalLength / minorIntervals;\n for (var i = 0; i < majorIntervals + 1; i++) {\n var y = start + i * majorIntervalLength;\n var line = ctx.svg.line(676, y, 708, y).stroke({ width: 3 }).attr({class: 'majorTick'});\n element.add(line);\n var majorText = (100 - i * (100/majorIntervals)).toFixed(0);\n var majorTickText = ctx.svg.text(majorText);\n majorTickText.attr({x: 666, y: y + 2, 'text-anchor': 'end', class: 'majorTickText'});\n majorTickText.first().attr({'dominant-baseline': 'middle'});\n element.add(majorTickText);\n if (i < majorIntervals) {\n drawMinorTicks(y, minorIntervals, minorIntervalLength);\n }\n }\n }\n \n var majorFont = ctx.properties.majorFont;\n var majorColor = ctx.properties.majorColor;\n var minorColor = ctx.properties.minorColor;\n if (ctx.values.critical) {\n majorColor = ctx.properties.majorCriticalColor;\n minorColor = ctx.properties.minorCriticalColor;\n } else if (ctx.values.warning) {\n majorColor = ctx.properties.minorWarningColor;\n minorColor = ctx.properties.minorWarningColor;\n }\n \n var majorTicks = element.find('line.majorTick');\n majorTicks.forEach(t => t.attr({stroke: majorColor}));\n \n var majorTicksText = element.find('text.majorTickText');\n ctx.api.font(majorTicksText, majorFont, majorColor);\n \n var minorTicks = element.find('line.minorTick');\n minorTicks.forEach(t => t.attr({stroke: minorColor}));\n \n var elementCriticalAnimation = element.remember('criticalAnimation');\n var criticalAnimation = ctx.values.critical && ctx.values.criticalAnimation;\n\n if (elementCriticalAnimation !== criticalAnimation) {\n element.remember('criticalAnimation', criticalAnimation);\n if (criticalAnimation) {\n ctx.api.cssAnimate(element, 500).attr({opacity: 0.15}).loop(0, true);\n } else {\n ctx.api.resetCssAnimation(element);\n }\n }\n}\n\nfunction drawMinorTicks(start, minorIntervals, minorIntervalLength) {\n for (var i = 1; i < minorIntervals; i++) {\n var minorY = start + i * minorIntervalLength;\n var minorLine = ctx.svg.line(688, minorY, 708, minorY).stroke({ width: 3 }).attr({class: 'minorTick'});\n element.add(minorLine);\n }\n}", + "stateRenderFunction": "if (!ctx.properties.scale) {\n element.hide();\n} else {\n var scaleSet = element.remember('scaleSet');\n if (!scaleSet) {\n element.remember('scaleSet', true);\n element.clear();\n \n var majorIntervals = ctx.properties.majorIntervals;\n var minorIntervals = ctx.properties.minorIntervals;\n \n var start = 203;\n var majorIntervalLength = 763 / majorIntervals;\n var minorIntervalLength = majorIntervalLength / minorIntervals;\n var tankCapacity = ctx.properties.scaleDisplayFormat ? 100 : (ctx.values.tankCapacity || 100);\n for (var i = 0; i < majorIntervals + 1; i++) {\n var y = start + i * majorIntervalLength;\n var line = ctx.svg.line(676, y, 708, y).stroke({ width: 3 }).attr({class: 'majorTick'});\n element.add(line);\n var majorText = ctx.api.formatValue((tankCapacity - i * (tankCapacity/majorIntervals)).toFixed(0), 0, ctx.properties.majorUnits, false);\n var majorTickText = ctx.svg.text(majorText);\n majorTickText.attr({x: 666, y: y + 2, 'text-anchor': 'end', class: 'majorTickText'});\n majorTickText.first().attr({'dominant-baseline': 'middle'});\n element.add(majorTickText);\n if (i < majorIntervals) {\n drawMinorTicks(y, minorIntervals, minorIntervalLength);\n }\n }\n }\n \n var majorFont = ctx.properties.majorFont;\n var majorColor = ctx.properties.majorColor;\n var minorColor = ctx.properties.minorColor;\n if (ctx.values.critical) {\n majorColor = ctx.properties.majorCriticalColor;\n minorColor = ctx.properties.minorCriticalColor;\n } else if (ctx.values.warning) {\n majorColor = ctx.properties.minorWarningColor;\n minorColor = ctx.properties.minorWarningColor;\n }\n \n var majorTicks = element.find('line.majorTick');\n majorTicks.forEach(t => t.attr({stroke: majorColor}));\n \n var majorTicksText = element.find('text.majorTickText');\n ctx.api.font(majorTicksText, majorFont, majorColor);\n \n var minorTicks = element.find('line.minorTick');\n minorTicks.forEach(t => t.attr({stroke: minorColor}));\n \n var elementCriticalAnimation = element.remember('criticalAnimation');\n var criticalAnimation = ctx.values.critical && ctx.values.criticalAnimation;\n\n if (elementCriticalAnimation !== criticalAnimation) {\n element.remember('criticalAnimation', criticalAnimation);\n if (criticalAnimation) {\n ctx.api.cssAnimate(element, 500).attr({opacity: 0.15}).loop(0, true);\n } else {\n ctx.api.resetCssAnimation(element);\n }\n }\n}\n\nfunction drawMinorTicks(start, minorIntervals, minorIntervalLength) {\n for (var i = 1; i < minorIntervals; i++) {\n var minorY = start + i * minorIntervalLength;\n var minorLine = ctx.svg.line(688, minorY, 708, minorY).stroke({ width: 3 }).attr({class: 'minorTick'});\n element.add(minorLine);\n }\n}", "actions": null }, { @@ -279,80 +279,43 @@ "name": "{i18n:scada.symbol.tank-color}", "type": "color", "default": "#E5E5E5", - "required": null, - "subLabel": null, - "divider": null, - "fieldSuffix": null, - "disableOnProperty": null, - "rowClass": "", - "fieldClass": "", - "min": null, - "max": null, - "step": null + "disabled": false, + "visible": true }, { "id": "fluidColor", "name": "{i18n:scada.symbol.fluid-color}", "type": "color", "default": "#1EC1F480", - "required": null, - "subLabel": null, - "divider": null, - "fieldSuffix": null, - "disableOnProperty": null, - "rowClass": "", - "fieldClass": "", - "min": null, - "max": null, - "step": null + "disabled": false, + "visible": true }, { "id": "valueBox", "name": "{i18n:scada.symbol.value-box}", "type": "switch", "default": true, - "required": null, - "subLabel": null, - "divider": null, - "fieldSuffix": null, - "disableOnProperty": null, - "rowClass": "", - "fieldClass": "", - "min": null, - "max": null, - "step": null + "disabled": false, + "visible": true }, { "id": "valueBoxColor", "name": "{i18n:scada.symbol.value-box}", "type": "color", "default": "#F3F3F3", - "required": null, - "subLabel": null, - "divider": null, - "fieldSuffix": null, "disableOnProperty": "valueBox", - "rowClass": "", - "fieldClass": "", - "min": null, - "max": null, - "step": null + "disabled": false, + "visible": true }, { "id": "valueUnits", "name": "{i18n:scada.symbol.value-text}", "type": "units", "default": "gal", - "required": null, "subLabel": "{i18n:scada.symbol.units}", - "divider": null, - "fieldSuffix": null, "disableOnProperty": "valueBox", - "rowClass": "", - "fieldClass": "", - "min": null, - "max": null, - "step": null + "disabled": false, + "visible": true }, { "id": "valueTextFont", @@ -365,80 +328,78 @@ "weight": "500", "style": "normal" }, - "required": null, - "subLabel": null, - "divider": null, - "fieldSuffix": null, "disableOnProperty": "valueBox", - "rowClass": "", - "fieldClass": "", - "min": null, - "max": null, - "step": null + "disabled": false, + "visible": true }, { "id": "valueTextColor", "name": "{i18n:scada.symbol.value-text}", "type": "color", "default": "#0000008A", - "required": null, - "subLabel": null, - "divider": null, - "fieldSuffix": null, "disableOnProperty": "valueBox", - "rowClass": "", - "fieldClass": "", - "min": null, - "max": null, - "step": null + "disabled": false, + "visible": true }, { "id": "scale", "name": "{i18n:scada.symbol.scale}", "type": "switch", "default": true, - "required": null, - "subLabel": null, - "divider": null, - "fieldSuffix": null, - "disableOnProperty": null, - "rowClass": "", - "fieldClass": "", - "min": null, - "max": null, - "step": null + "disabled": false, + "visible": true + }, + { + "id": "scaleDisplayFormat", + "name": "{i18n:scada.symbol.scale}", + "type": "select", + "default": true, + "subLabel": "{i18n:scada.symbol.display-format}", + "disableOnProperty": "scale", + "items": [ + { + "value": true, + "label": "Percentage" + }, + { + "value": false, + "label": "Metric" + } + ], + "disabled": false, + "visible": true }, { "id": "transparent", "name": "{i18n:scada.symbol.transparent-mode}", "type": "switch", "default": false, - "required": null, - "subLabel": null, - "divider": null, - "fieldSuffix": null, "disableOnProperty": "scale", - "rowClass": "", - "fieldClass": "", - "min": null, - "max": null, - "step": null + "disabled": false, + "visible": true }, { "id": "majorIntervals", "name": "{i18n:scada.symbol.major-ticks}", "type": "number", "default": 10, - "required": null, "subLabel": "{i18n:scada.symbol.intervals}", "divider": true, - "fieldSuffix": null, "disableOnProperty": "scale", - "rowClass": "", - "fieldClass": "", "min": 1, - "max": null, - "step": 1 + "step": 1, + "disabled": false, + "visible": true + }, + { + "id": "majorUnits", + "name": "{i18n:scada.symbol.major-ticks}", + "type": "units", + "subLabel": "{i18n:scada.symbol.units}", + "divider": true, + "disableOnProperty": "scale", + "disabled": false, + "visible": true }, { "id": "majorFont", @@ -451,128 +412,84 @@ "weight": "500", "style": "normal" }, - "required": null, - "subLabel": null, - "divider": null, - "fieldSuffix": null, "disableOnProperty": "scale", - "rowClass": "", - "fieldClass": "", - "min": null, - "max": null, - "step": null + "disabled": false, + "visible": true }, { "id": "majorColor", "name": "{i18n:scada.symbol.major-ticks-color}", "type": "color", "default": "#00000061", - "required": null, "subLabel": "{i18n:scada.symbol.normal}", "divider": true, - "fieldSuffix": null, "disableOnProperty": "scale", - "rowClass": "", - "fieldClass": "", - "min": null, - "max": null, - "step": null + "disabled": false, + "visible": true }, { "id": "majorWarningColor", "name": "{i18n:scada.symbol.major-ticks-color}", "type": "color", "default": "#FAA405", - "required": null, "subLabel": "{i18n:scada.symbol.warning}", "divider": true, - "fieldSuffix": null, "disableOnProperty": "scale", - "rowClass": "", - "fieldClass": "", - "min": null, - "max": null, - "step": null + "disabled": false, + "visible": true }, { "id": "majorCriticalColor", "name": "{i18n:scada.symbol.major-ticks-color}", "type": "color", "default": "#D12730", - "required": null, "subLabel": "{i18n:scada.symbol.critical}", - "divider": null, - "fieldSuffix": null, "disableOnProperty": "scale", - "rowClass": "", - "fieldClass": "", - "min": null, - "max": null, - "step": null + "disabled": false, + "visible": true }, { "id": "minorIntervals", "name": "{i18n:scada.symbol.minor-ticks}", "type": "number", "default": 5, - "required": null, "subLabel": "{i18n:scada.symbol.intervals}", - "divider": null, - "fieldSuffix": null, "disableOnProperty": "scale", - "rowClass": "", - "fieldClass": "", "min": 1, - "max": null, - "step": null + "disabled": false, + "visible": true }, { "id": "minorColor", "name": "{i18n:scada.symbol.minor-ticks-color}", "type": "color", "default": "#0000001F", - "required": null, "subLabel": "{i18n:scada.symbol.normal}", "divider": true, - "fieldSuffix": null, "disableOnProperty": "scale", - "rowClass": "", - "fieldClass": "", - "min": null, - "max": null, - "step": null + "disabled": false, + "visible": true }, { "id": "minorWarningColor", "name": "{i18n:scada.symbol.minor-ticks-color}", "type": "color", "default": "#FAA405", - "required": null, "subLabel": "{i18n:scada.symbol.warning}", "divider": true, - "fieldSuffix": null, "disableOnProperty": "scale", - "rowClass": "", - "fieldClass": "", - "min": null, - "max": null, - "step": null + "disabled": false, + "visible": true }, { "id": "minorCriticalColor", "name": "{i18n:scada.symbol.minor-ticks-color}", "type": "color", "default": "#D12730", - "required": null, "subLabel": "{i18n:scada.symbol.critical}", - "divider": null, - "fieldSuffix": null, "disableOnProperty": "scale", - "rowClass": "", - "fieldClass": "", - "min": null, - "max": null, - "step": null + "disabled": false, + "visible": true } ] }]]> diff --git a/application/src/main/data/json/system/scada_symbols/large-vertical-tank.svg b/application/src/main/data/json/system/scada_symbols/large-vertical-tank.svg index 3b9420e04d..33bd6c2f39 100644 --- a/application/src/main/data/json/system/scada_symbols/large-vertical-tank.svg +++ b/application/src/main/data/json/system/scada_symbols/large-vertical-tank.svg @@ -33,7 +33,7 @@ }, { "tag": "scale", - "stateRenderFunction": "if (!ctx.properties.scale) {\n element.hide();\n} else {\n var scaleSet = element.remember('scaleSet');\n if (!scaleSet) {\n element.remember('scaleSet', true);\n element.clear();\n \n var majorIntervals = ctx.properties.majorIntervals;\n var minorIntervals = ctx.properties.minorIntervals;\n \n var start = 203;\n var majorIntervalLength = 763 / majorIntervals;\n var minorIntervalLength = majorIntervalLength / minorIntervals;\n for (var i = 0; i < majorIntervals + 1; i++) {\n var y = start + i * majorIntervalLength;\n var line = ctx.svg.line(676, y, 708, y).stroke({ width: 3 }).attr({class: 'majorTick'});\n element.add(line);\n var majorText = (100 - i * (100/majorIntervals)).toFixed(0);\n var majorTickText = ctx.svg.text(majorText);\n majorTickText.attr({x: 666, y: y + 2, 'text-anchor': 'end', class: 'majorTickText'});\n majorTickText.first().attr({'dominant-baseline': 'middle'});\n element.add(majorTickText);\n if (i < majorIntervals) {\n drawMinorTicks(y, minorIntervals, minorIntervalLength);\n }\n }\n }\n \n var majorFont = ctx.properties.majorFont;\n var majorColor = ctx.properties.majorColor;\n var minorColor = ctx.properties.minorColor;\n if (ctx.values.critical) {\n majorColor = ctx.properties.majorCriticalColor;\n minorColor = ctx.properties.minorCriticalColor;\n } else if (ctx.values.warning) {\n majorColor = ctx.properties.minorWarningColor;\n minorColor = ctx.properties.minorWarningColor;\n }\n \n var majorTicks = element.find('line.majorTick');\n majorTicks.forEach(t => t.attr({stroke: majorColor}));\n \n var majorTicksText = element.find('text.majorTickText');\n ctx.api.font(majorTicksText, majorFont, majorColor);\n \n var minorTicks = element.find('line.minorTick');\n minorTicks.forEach(t => t.attr({stroke: minorColor}));\n \n var elementCriticalAnimation = element.remember('criticalAnimation');\n var criticalAnimation = ctx.values.critical && ctx.values.criticalAnimation;\n\n if (elementCriticalAnimation !== criticalAnimation) {\n element.remember('criticalAnimation', criticalAnimation);\n if (criticalAnimation) {\n ctx.api.cssAnimate(element, 500).attr({opacity: 0.15}).loop(0, true);\n } else {\n ctx.api.resetCssAnimation(element);\n }\n }\n}\n\nfunction drawMinorTicks(start, minorIntervals, minorIntervalLength) {\n for (var i = 1; i < minorIntervals; i++) {\n var minorY = start + i * minorIntervalLength;\n var minorLine = ctx.svg.line(688, minorY, 708, minorY).stroke({ width: 3 }).attr({class: 'minorTick'});\n element.add(minorLine);\n }\n}", + "stateRenderFunction": "if (!ctx.properties.scale) {\n element.hide();\n} else {\n var scaleSet = element.remember('scaleSet');\n if (!scaleSet) {\n element.remember('scaleSet', true);\n element.clear();\n \n var majorIntervals = ctx.properties.majorIntervals;\n var minorIntervals = ctx.properties.minorIntervals;\n \n var start = 203;\n var majorIntervalLength = 763 / majorIntervals;\n var minorIntervalLength = majorIntervalLength / minorIntervals;\n var tankCapacity = ctx.properties.scaleDisplayFormat ? 100 : (ctx.values.tankCapacity || 100);\n for (var i = 0; i < majorIntervals + 1; i++) {\n var y = start + i * majorIntervalLength;\n var line = ctx.svg.line(676, y, 708, y).stroke({ width: 3 }).attr({class: 'majorTick'});\n element.add(line);\n var majorText = ctx.api.formatValue((tankCapacity - i * (tankCapacity/majorIntervals)).toFixed(0), 0, ctx.properties.majorUnits, false);\n var majorTickText = ctx.svg.text(majorText);\n majorTickText.attr({x: 666, y: y + 2, 'text-anchor': 'end', class: 'majorTickText'});\n majorTickText.first().attr({'dominant-baseline': 'middle'});\n element.add(majorTickText);\n if (i < majorIntervals) {\n drawMinorTicks(y, minorIntervals, minorIntervalLength);\n }\n }\n }\n \n var majorFont = ctx.properties.majorFont;\n var majorColor = ctx.properties.majorColor;\n var minorColor = ctx.properties.minorColor;\n if (ctx.values.critical) {\n majorColor = ctx.properties.majorCriticalColor;\n minorColor = ctx.properties.minorCriticalColor;\n } else if (ctx.values.warning) {\n majorColor = ctx.properties.minorWarningColor;\n minorColor = ctx.properties.minorWarningColor;\n }\n \n var majorTicks = element.find('line.majorTick');\n majorTicks.forEach(t => t.attr({stroke: majorColor}));\n \n var majorTicksText = element.find('text.majorTickText');\n ctx.api.font(majorTicksText, majorFont, majorColor);\n \n var minorTicks = element.find('line.minorTick');\n minorTicks.forEach(t => t.attr({stroke: minorColor}));\n \n var elementCriticalAnimation = element.remember('criticalAnimation');\n var criticalAnimation = ctx.values.critical && ctx.values.criticalAnimation;\n\n if (elementCriticalAnimation !== criticalAnimation) {\n element.remember('criticalAnimation', criticalAnimation);\n if (criticalAnimation) {\n ctx.api.cssAnimate(element, 500).attr({opacity: 0.15}).loop(0, true);\n } else {\n ctx.api.resetCssAnimation(element);\n }\n }\n}\n\nfunction drawMinorTicks(start, minorIntervals, minorIntervalLength) {\n for (var i = 1; i < minorIntervals; i++) {\n var minorY = start + i * minorIntervalLength;\n var minorLine = ctx.svg.line(688, minorY, 708, minorY).stroke({ width: 3 }).attr({class: 'minorTick'});\n element.add(minorLine);\n }\n}", "actions": null }, { @@ -278,80 +278,43 @@ "name": "{i18n:scada.symbol.tank-color}", "type": "color", "default": "#E5E5E5", - "required": null, - "subLabel": null, - "divider": null, - "fieldSuffix": null, - "disableOnProperty": null, - "rowClass": "", - "fieldClass": "", - "min": null, - "max": null, - "step": null + "disabled": false, + "visible": true }, { "id": "fluidColor", "name": "{i18n:scada.symbol.fluid-color}", "type": "color", "default": "#1EC1F480", - "required": null, - "subLabel": null, - "divider": null, - "fieldSuffix": null, - "disableOnProperty": null, - "rowClass": "", - "fieldClass": "", - "min": null, - "max": null, - "step": null + "disabled": false, + "visible": true }, { "id": "valueBox", "name": "{i18n:scada.symbol.value-box}", "type": "switch", "default": true, - "required": null, - "subLabel": null, - "divider": null, - "fieldSuffix": null, - "disableOnProperty": null, - "rowClass": "", - "fieldClass": "", - "min": null, - "max": null, - "step": null + "disabled": false, + "visible": true }, { "id": "valueBoxColor", "name": "{i18n:scada.symbol.value-box}", "type": "color", "default": "#F3F3F3", - "required": null, - "subLabel": null, - "divider": null, - "fieldSuffix": null, "disableOnProperty": "valueBox", - "rowClass": "", - "fieldClass": "", - "min": null, - "max": null, - "step": null + "disabled": false, + "visible": true }, { "id": "valueUnits", "name": "{i18n:scada.symbol.value-text}", "type": "units", "default": "gal", - "required": null, "subLabel": "{i18n:scada.symbol.units}", - "divider": null, - "fieldSuffix": null, "disableOnProperty": "valueBox", - "rowClass": "", - "fieldClass": "", - "min": null, - "max": null, - "step": null + "disabled": false, + "visible": true }, { "id": "valueTextFont", @@ -364,80 +327,78 @@ "weight": "500", "style": "normal" }, - "required": null, - "subLabel": null, - "divider": null, - "fieldSuffix": null, "disableOnProperty": "valueBox", - "rowClass": "", - "fieldClass": "", - "min": null, - "max": null, - "step": null + "disabled": false, + "visible": true }, { "id": "valueTextColor", "name": "{i18n:scada.symbol.value-text}", "type": "color", "default": "#0000008A", - "required": null, - "subLabel": null, - "divider": null, - "fieldSuffix": null, "disableOnProperty": "valueBox", - "rowClass": "", - "fieldClass": "", - "min": null, - "max": null, - "step": null + "disabled": false, + "visible": true }, { "id": "scale", "name": "{i18n:scada.symbol.scale}", "type": "switch", "default": true, - "required": null, - "subLabel": null, - "divider": null, - "fieldSuffix": null, - "disableOnProperty": null, - "rowClass": "", - "fieldClass": "", - "min": null, - "max": null, - "step": null + "disabled": false, + "visible": true + }, + { + "id": "scaleDisplayFormat", + "name": "{i18n:scada.symbol.scale}", + "type": "select", + "default": true, + "subLabel": "{i18n:scada.symbol.display-format}", + "disableOnProperty": "scale", + "items": [ + { + "value": true, + "label": "Percentage" + }, + { + "value": false, + "label": "Metric" + } + ], + "disabled": false, + "visible": true }, { "id": "transparent", "name": "{i18n:scada.symbol.transparent-mode}", "type": "switch", "default": false, - "required": null, - "subLabel": null, - "divider": null, - "fieldSuffix": null, "disableOnProperty": "scale", - "rowClass": "", - "fieldClass": "", - "min": null, - "max": null, - "step": null + "disabled": false, + "visible": true }, { "id": "majorIntervals", "name": "{i18n:scada.symbol.major-ticks}", "type": "number", "default": 10, - "required": null, "subLabel": "{i18n:scada.symbol.intervals}", "divider": true, - "fieldSuffix": null, "disableOnProperty": "scale", - "rowClass": "", - "fieldClass": "", "min": 1, - "max": null, - "step": 1 + "step": 1, + "disabled": false, + "visible": true + }, + { + "id": "majorUnits", + "name": "{i18n:scada.symbol.major-ticks}", + "type": "units", + "subLabel": "{i18n:scada.symbol.units}", + "divider": true, + "disableOnProperty": "scale", + "disabled": false, + "visible": true }, { "id": "majorFont", @@ -450,128 +411,84 @@ "weight": "500", "style": "normal" }, - "required": null, - "subLabel": null, - "divider": null, - "fieldSuffix": null, "disableOnProperty": "scale", - "rowClass": "", - "fieldClass": "", - "min": null, - "max": null, - "step": null + "disabled": false, + "visible": true }, { "id": "majorColor", "name": "{i18n:scada.symbol.major-ticks-color}", "type": "color", "default": "#00000061", - "required": null, "subLabel": "{i18n:scada.symbol.normal}", "divider": true, - "fieldSuffix": null, "disableOnProperty": "scale", - "rowClass": "", - "fieldClass": "", - "min": null, - "max": null, - "step": null + "disabled": false, + "visible": true }, { "id": "majorWarningColor", "name": "{i18n:scada.symbol.major-ticks-color}", "type": "color", "default": "#FAA405", - "required": null, "subLabel": "{i18n:scada.symbol.warning}", "divider": true, - "fieldSuffix": null, "disableOnProperty": "scale", - "rowClass": "", - "fieldClass": "", - "min": null, - "max": null, - "step": null + "disabled": false, + "visible": true }, { "id": "majorCriticalColor", "name": "{i18n:scada.symbol.major-ticks-color}", "type": "color", "default": "#D12730", - "required": null, "subLabel": "{i18n:scada.symbol.critical}", - "divider": null, - "fieldSuffix": null, "disableOnProperty": "scale", - "rowClass": "", - "fieldClass": "", - "min": null, - "max": null, - "step": null + "disabled": false, + "visible": true }, { "id": "minorIntervals", "name": "{i18n:scada.symbol.minor-ticks}", "type": "number", "default": 5, - "required": null, "subLabel": "{i18n:scada.symbol.intervals}", - "divider": null, - "fieldSuffix": null, "disableOnProperty": "scale", - "rowClass": "", - "fieldClass": "", "min": 1, - "max": null, - "step": null + "disabled": false, + "visible": true }, { "id": "minorColor", "name": "{i18n:scada.symbol.minor-ticks-color}", "type": "color", "default": "#0000001F", - "required": null, "subLabel": "{i18n:scada.symbol.normal}", "divider": true, - "fieldSuffix": null, "disableOnProperty": "scale", - "rowClass": "", - "fieldClass": "", - "min": null, - "max": null, - "step": null + "disabled": false, + "visible": true }, { "id": "minorWarningColor", "name": "{i18n:scada.symbol.minor-ticks-color}", "type": "color", "default": "#FAA405", - "required": null, "subLabel": "{i18n:scada.symbol.warning}", "divider": true, - "fieldSuffix": null, "disableOnProperty": "scale", - "rowClass": "", - "fieldClass": "", - "min": null, - "max": null, - "step": null + "disabled": false, + "visible": true }, { "id": "minorCriticalColor", "name": "{i18n:scada.symbol.minor-ticks-color}", "type": "color", "default": "#D12730", - "required": null, "subLabel": "{i18n:scada.symbol.critical}", - "divider": null, - "fieldSuffix": null, "disableOnProperty": "scale", - "rowClass": "", - "fieldClass": "", - "min": null, - "max": null, - "step": null + "disabled": false, + "visible": true } ] }]]> diff --git a/application/src/main/data/json/system/scada_symbols/pool-hp.svg b/application/src/main/data/json/system/scada_symbols/pool-hp.svg index 357563a687..74757b77d6 100644 --- a/application/src/main/data/json/system/scada_symbols/pool-hp.svg +++ b/application/src/main/data/json/system/scada_symbols/pool-hp.svg @@ -38,7 +38,7 @@ }, { "tag": "scale", - "stateRenderFunction": "if (!ctx.properties.scale) {\n element.hide();\n} else {\n var scaleSet = element.remember('scaleSet');\n if (!scaleSet) {\n element.remember('scaleSet', true);\n element.clear();\n \n var majorIntervals = ctx.properties.majorIntervals;\n var minorIntervals = ctx.properties.minorIntervals;\n \n var start = 3;\n var majorIntervalLength = 792 / majorIntervals;\n var minorIntervalLength = majorIntervalLength / minorIntervals;\n for (var i = 0; i < majorIntervals + 1; i++) {\n var y = start + i * majorIntervalLength;\n var line = ctx.svg.line(298, y, 330, y).stroke({ width: 3 }).attr({class: 'majorTick'});\n element.add(line);\n var majorText = (100 - i * (100/majorIntervals)).toFixed(0);\n var majorTickText = ctx.svg.text(majorText);\n if (i === 0) {\n majorTickText.attr({x: 288, y: y + 10, 'text-anchor': 'end', class: 'majorTickText'});\n } else if (i === majorIntervals) {\n majorTickText.attr({x: 288, y: y - 5, 'text-anchor': 'end', class: 'majorTickText'});\n } else {\n majorTickText.attr({x: 288, y: y + 2, 'text-anchor': 'end', class: 'majorTickText'});\n }\n majorTickText.first().attr({'dominant-baseline': 'middle'});\n element.add(majorTickText);\n if (i < majorIntervals) {\n drawMinorTicks(y, minorIntervals, minorIntervalLength);\n }\n }\n }\n \n var majorFont = ctx.properties.majorFont;\n var majorColor = ctx.properties.majorColor;\n var minorColor = ctx.properties.minorColor;\n \n var majorTicks = element.find('line.majorTick');\n majorTicks.forEach(t => t.attr({stroke: majorColor}));\n \n var majorTicksText = element.find('text.majorTickText');\n ctx.api.font(majorTicksText, majorFont, majorColor);\n \n var minorTicks = element.find('line.minorTick');\n minorTicks.forEach(t => t.attr({stroke: minorColor}));\n}\n\nfunction drawMinorTicks(start, minorIntervals, minorIntervalLength) {\n for (var i = 1; i < minorIntervals; i++) {\n var minorY = start + i * minorIntervalLength;\n var minorLine = ctx.svg.line(310, minorY, 330, minorY).stroke({ width: 3 }).attr({class: 'minorTick'});\n element.add(minorLine);\n }\n}", + "stateRenderFunction": "if (!ctx.properties.scale) {\n element.hide();\n} else {\n var scaleSet = element.remember('scaleSet');\n if (!scaleSet) {\n element.remember('scaleSet', true);\n element.clear();\n \n var majorIntervals = ctx.properties.majorIntervals;\n var minorIntervals = ctx.properties.minorIntervals;\n \n var start = 3;\n var majorIntervalLength = 792 / majorIntervals;\n var minorIntervalLength = majorIntervalLength / minorIntervals;\n var tankCapacity = ctx.properties.scaleDisplayFormat ? 100 : (ctx.values.tankCapacity || 100);\n for (var i = 0; i < majorIntervals + 1; i++) {\n var y = start + i * majorIntervalLength;\n var line = ctx.svg.line(298, y, 330, y).stroke({ width: 3 }).attr({class: 'majorTick'});\n element.add(line);\n var majorText = ctx.api.formatValue((tankCapacity - i * (tankCapacity/majorIntervals)).toFixed(0), 0, ctx.properties.majorUnits, false);\n var majorTickText = ctx.svg.text(majorText);\n if (i === 0) {\n majorTickText.attr({x: 288, y: y + 10, 'text-anchor': 'end', class: 'majorTickText'});\n } else if (i === majorIntervals) {\n majorTickText.attr({x: 288, y: y - 5, 'text-anchor': 'end', class: 'majorTickText'});\n } else {\n majorTickText.attr({x: 288, y: y + 2, 'text-anchor': 'end', class: 'majorTickText'});\n }\n majorTickText.first().attr({'dominant-baseline': 'middle'});\n element.add(majorTickText);\n if (i < majorIntervals) {\n drawMinorTicks(y, minorIntervals, minorIntervalLength);\n }\n }\n }\n \n var majorFont = ctx.properties.majorFont;\n var majorColor = ctx.properties.majorColor;\n var minorColor = ctx.properties.minorColor;\n \n var majorTicks = element.find('line.majorTick');\n majorTicks.forEach(t => t.attr({stroke: majorColor}));\n \n var majorTicksText = element.find('text.majorTickText');\n ctx.api.font(majorTicksText, majorFont, majorColor);\n \n var minorTicks = element.find('line.minorTick');\n minorTicks.forEach(t => t.attr({stroke: minorColor}));\n}\n\nfunction drawMinorTicks(start, minorIntervals, minorIntervalLength) {\n for (var i = 1; i < minorIntervals; i++) {\n var minorY = start + i * minorIntervalLength;\n var minorLine = ctx.svg.line(310, minorY, 330, minorY).stroke({ width: 3 }).attr({class: 'minorTick'});\n element.add(minorLine);\n }\n}", "actions": null }, { @@ -307,64 +307,57 @@ "name": "{i18n:scada.symbol.tank-color}", "type": "color", "default": "#EBEBEB", - "required": null, - "subLabel": null, - "divider": null, - "fieldSuffix": null, - "disableOnProperty": null, - "rowClass": "", - "fieldClass": "", - "min": null, - "max": null, - "step": null + "disabled": false, + "visible": true }, { "id": "fluidColor", "name": "{i18n:scada.symbol.fluid-color}", "type": "color", "default": "#C8DFF7", - "required": null, - "subLabel": null, - "divider": null, - "fieldSuffix": null, - "disableOnProperty": null, - "rowClass": "", - "fieldClass": "", - "min": null, - "max": null, - "step": null + "disabled": false, + "visible": true }, { "id": "scale", "name": "{i18n:scada.symbol.scale}", "type": "switch", "default": true, - "required": null, - "subLabel": null, - "divider": null, - "fieldSuffix": null, - "disableOnProperty": null, - "rowClass": "", - "fieldClass": "", - "min": null, - "max": null, - "step": null + "disabled": false, + "visible": true + }, + { + "id": "scaleDisplayFormat", + "name": "{i18n:scada.symbol.scale}", + "type": "select", + "default": true, + "subLabel": "{i18n:scada.symbol.display-format}", + "disableOnProperty": "scale", + "items": [ + { + "value": true, + "label": "Percentage" + }, + { + "value": false, + "label": "Metric" + } + ], + "disabled": false, + "visible": true }, { "id": "majorIntervals", "name": "{i18n:scada.symbol.major-ticks}", "type": "number", "default": 10, - "required": null, "subLabel": "{i18n:scada.symbol.intervals}", "divider": false, - "fieldSuffix": null, "disableOnProperty": "scale", - "rowClass": "", - "fieldClass": "", "min": 1, - "max": null, - "step": 1 + "step": 1, + "disabled": false, + "visible": true }, { "id": "majorFont", @@ -377,96 +370,67 @@ "weight": "500", "style": "normal" }, - "required": null, - "subLabel": null, - "divider": null, - "fieldSuffix": null, "disableOnProperty": "scale", - "rowClass": "", - "fieldClass": "", - "min": null, - "max": null, - "step": null + "disabled": false, + "visible": true }, { "id": "majorColor", "name": "{i18n:scada.symbol.major-ticks}", "type": "color", "default": "#00000061", - "required": null, - "subLabel": null, - "divider": null, - "fieldSuffix": null, "disableOnProperty": "scale", - "rowClass": "", - "fieldClass": "", - "min": null, - "max": null, - "step": null + "disabled": false, + "visible": true }, { "id": "minorIntervals", "name": "{i18n:scada.symbol.minor-ticks}", "type": "number", "default": 5, - "required": null, "subLabel": "{i18n:scada.symbol.intervals}", - "divider": null, - "fieldSuffix": null, "disableOnProperty": "scale", - "rowClass": "", - "fieldClass": "", "min": 1, - "max": null, - "step": null + "disabled": false, + "visible": true + }, + { + "id": "majorUnits", + "name": "{i18n:scada.symbol.major-ticks}", + "type": "units", + "subLabel": "{i18n:scada.symbol.units}", + "divider": true, + "disableOnProperty": "scale", + "disabled": false, + "visible": true }, { "id": "minorColor", "name": "{i18n:scada.symbol.minor-ticks}", "type": "color", "default": "#0000001F", - "required": null, - "subLabel": null, - "divider": null, - "fieldSuffix": null, "disableOnProperty": "scale", - "rowClass": "", - "fieldClass": "", - "min": null, - "max": null, - "step": null + "disabled": false, + "visible": true }, { "id": "warningColor", "name": "{i18n:scada.symbol.alarm-colors}", "type": "color", "default": "#FAA405", - "required": null, "subLabel": "{i18n:scada.symbol.warning}", "divider": true, - "fieldSuffix": null, - "disableOnProperty": null, - "rowClass": "", - "fieldClass": "", - "min": null, - "max": null, - "step": null + "disabled": false, + "visible": true }, { "id": "criticalColor", "name": "{i18n:scada.symbol.alarm-colors}", "type": "color", "default": "#D12730", - "required": null, "subLabel": "{i18n:scada.symbol.critical}", - "divider": null, - "fieldSuffix": null, - "disableOnProperty": null, - "rowClass": "", - "fieldClass": "", - "min": null, - "max": null, - "step": null + "disabled": false, + "visible": true } ] }]]> diff --git a/application/src/main/data/json/system/scada_symbols/short-vertical-tank-hp.svg b/application/src/main/data/json/system/scada_symbols/short-vertical-tank-hp.svg index 1f635fc810..1ce12cafe2 100644 --- a/application/src/main/data/json/system/scada_symbols/short-vertical-tank-hp.svg +++ b/application/src/main/data/json/system/scada_symbols/short-vertical-tank-hp.svg @@ -38,7 +38,7 @@ }, { "tag": "scale", - "stateRenderFunction": "if (!ctx.properties.scale) {\n element.hide();\n} else {\n var scaleSet = element.remember('scaleSet');\n if (!scaleSet) {\n element.remember('scaleSet', true);\n element.clear();\n \n var majorIntervals = ctx.properties.majorIntervals;\n var minorIntervals = ctx.properties.minorIntervals;\n \n var start = 3;\n var majorIntervalLength = 594 / majorIntervals;\n var minorIntervalLength = majorIntervalLength / minorIntervals;\n for (var i = 0; i < majorIntervals + 1; i++) {\n var y = start + i * majorIntervalLength;\n var line = ctx.svg.line(170, y, 202, y).stroke({ width: 3 }).attr({class: 'majorTick'});\n element.add(line);\n var majorText = (100 - i * (100/majorIntervals)).toFixed(0);\n var majorTickText = ctx.svg.text(majorText);\n if (i === 0) {\n majorTickText.attr({x: 160, y: y + 10, 'text-anchor': 'end', class: 'majorTickText'});\n } else if (i === majorIntervals) {\n majorTickText.attr({x: 160, y: y - 5, 'text-anchor': 'end', class: 'majorTickText'});\n } else {\n majorTickText.attr({x: 160, y: y + 2, 'text-anchor': 'end', class: 'majorTickText'});\n }\n majorTickText.first().attr({'dominant-baseline': 'middle'});\n element.add(majorTickText);\n if (i < majorIntervals) {\n drawMinorTicks(y, minorIntervals, minorIntervalLength);\n }\n }\n }\n \n var majorFont = ctx.properties.majorFont;\n var majorColor = ctx.properties.majorColor;\n var minorColor = ctx.properties.minorColor;\n \n var majorTicks = element.find('line.majorTick');\n majorTicks.forEach(t => t.attr({stroke: majorColor}));\n \n var majorTicksText = element.find('text.majorTickText');\n ctx.api.font(majorTicksText, majorFont, majorColor);\n \n var minorTicks = element.find('line.minorTick');\n minorTicks.forEach(t => t.attr({stroke: minorColor}));\n}\n\nfunction drawMinorTicks(start, minorIntervals, minorIntervalLength) {\n for (var i = 1; i < minorIntervals; i++) {\n var minorY = start + i * minorIntervalLength;\n var minorLine = ctx.svg.line(182, minorY, 202, minorY).stroke({ width: 3 }).attr({class: 'minorTick'});\n element.add(minorLine);\n }\n}", + "stateRenderFunction": "if (!ctx.properties.scale) {\n element.hide();\n} else {\n var scaleSet = element.remember('scaleSet');\n if (!scaleSet) {\n element.remember('scaleSet', true);\n element.clear();\n \n var majorIntervals = ctx.properties.majorIntervals;\n var minorIntervals = ctx.properties.minorIntervals;\n \n var start = 3;\n var majorIntervalLength = 594 / majorIntervals;\n var minorIntervalLength = majorIntervalLength / minorIntervals;\n var tankCapacity = ctx.properties.scaleDisplayFormat ? 100 : (ctx.values.tankCapacity || 100);\n for (var i = 0; i < majorIntervals + 1; i++) {\n var y = start + i * majorIntervalLength;\n var line = ctx.svg.line(170, y, 202, y).stroke({ width: 3 }).attr({class: 'majorTick'});\n element.add(line);\n var majorText = ctx.api.formatValue((tankCapacity - i * (tankCapacity/majorIntervals)).toFixed(0), 0, ctx.properties.majorUnits, false);\n var majorTickText = ctx.svg.text(majorText);\n if (i === 0) {\n majorTickText.attr({x: 160, y: y + 10, 'text-anchor': 'end', class: 'majorTickText'});\n } else if (i === majorIntervals) {\n majorTickText.attr({x: 160, y: y - 5, 'text-anchor': 'end', class: 'majorTickText'});\n } else {\n majorTickText.attr({x: 160, y: y + 2, 'text-anchor': 'end', class: 'majorTickText'});\n }\n majorTickText.first().attr({'dominant-baseline': 'middle'});\n element.add(majorTickText);\n if (i < majorIntervals) {\n drawMinorTicks(y, minorIntervals, minorIntervalLength);\n }\n }\n }\n \n var majorFont = ctx.properties.majorFont;\n var majorColor = ctx.properties.majorColor;\n var minorColor = ctx.properties.minorColor;\n \n var majorTicks = element.find('line.majorTick');\n majorTicks.forEach(t => t.attr({stroke: majorColor}));\n \n var majorTicksText = element.find('text.majorTickText');\n ctx.api.font(majorTicksText, majorFont, majorColor);\n \n var minorTicks = element.find('line.minorTick');\n minorTicks.forEach(t => t.attr({stroke: minorColor}));\n}\n\nfunction drawMinorTicks(start, minorIntervals, minorIntervalLength) {\n for (var i = 1; i < minorIntervals; i++) {\n var minorY = start + i * minorIntervalLength;\n var minorLine = ctx.svg.line(182, minorY, 202, minorY).stroke({ width: 3 }).attr({class: 'minorTick'});\n element.add(minorLine);\n }\n}", "actions": null }, { @@ -307,64 +307,67 @@ "name": "{i18n:scada.symbol.tank-color}", "type": "color", "default": "#EBEBEB", - "required": null, - "subLabel": null, - "divider": null, - "fieldSuffix": null, - "disableOnProperty": null, - "rowClass": "", - "fieldClass": "", - "min": null, - "max": null, - "step": null + "disabled": false, + "visible": true }, { "id": "fluidColor", "name": "{i18n:scada.symbol.fluid-color}", "type": "color", "default": "#C8DFF7", - "required": null, - "subLabel": null, - "divider": null, - "fieldSuffix": null, - "disableOnProperty": null, - "rowClass": "", - "fieldClass": "", - "min": null, - "max": null, - "step": null + "disabled": false, + "visible": true }, { "id": "scale", "name": "{i18n:scada.symbol.scale}", "type": "switch", "default": true, - "required": null, - "subLabel": null, - "divider": null, - "fieldSuffix": null, - "disableOnProperty": null, - "rowClass": "", - "fieldClass": "", - "min": null, - "max": null, - "step": null + "disabled": false, + "visible": true + }, + { + "id": "scaleDisplayFormat", + "name": "{i18n:scada.symbol.scale}", + "type": "select", + "default": true, + "subLabel": "{i18n:scada.symbol.display-format}", + "disableOnProperty": "scale", + "items": [ + { + "value": true, + "label": "Percentage" + }, + { + "value": false, + "label": "Metric" + } + ], + "disabled": false, + "visible": true }, { "id": "majorIntervals", "name": "{i18n:scada.symbol.major-ticks}", "type": "number", "default": 10, - "required": null, "subLabel": "{i18n:scada.symbol.intervals}", "divider": false, - "fieldSuffix": null, "disableOnProperty": "scale", - "rowClass": "", - "fieldClass": "", "min": 1, - "max": null, - "step": 1 + "step": 1, + "disabled": false, + "visible": true + }, + { + "id": "majorUnits", + "name": "{i18n:scada.symbol.major-ticks}", + "type": "units", + "subLabel": "{i18n:scada.symbol.units}", + "divider": true, + "disableOnProperty": "scale", + "disabled": false, + "visible": true }, { "id": "majorFont", @@ -377,96 +380,57 @@ "weight": "500", "style": "normal" }, - "required": null, - "subLabel": null, - "divider": null, - "fieldSuffix": null, "disableOnProperty": "scale", - "rowClass": "", - "fieldClass": "", - "min": null, - "max": null, - "step": null + "disabled": false, + "visible": true }, { "id": "majorColor", "name": "{i18n:scada.symbol.major-ticks}", "type": "color", "default": "#00000061", - "required": null, - "subLabel": null, - "divider": null, - "fieldSuffix": null, "disableOnProperty": "scale", - "rowClass": "", - "fieldClass": "", - "min": null, - "max": null, - "step": null + "disabled": false, + "visible": true }, { "id": "minorIntervals", "name": "{i18n:scada.symbol.minor-ticks}", "type": "number", "default": 5, - "required": null, "subLabel": "{i18n:scada.symbol.intervals}", - "divider": null, - "fieldSuffix": null, "disableOnProperty": "scale", - "rowClass": "", - "fieldClass": "", "min": 1, - "max": null, - "step": null + "disabled": false, + "visible": true }, { "id": "minorColor", "name": "{i18n:scada.symbol.minor-ticks}", "type": "color", "default": "#0000001F", - "required": null, - "subLabel": null, - "divider": null, - "fieldSuffix": null, "disableOnProperty": "scale", - "rowClass": "", - "fieldClass": "", - "min": null, - "max": null, - "step": null + "disabled": false, + "visible": true }, { "id": "warningColor", "name": "{i18n:scada.symbol.alarm-colors}", "type": "color", "default": "#FAA405", - "required": null, "subLabel": "{i18n:scada.symbol.warning}", "divider": true, - "fieldSuffix": null, - "disableOnProperty": null, - "rowClass": "", - "fieldClass": "", - "min": null, - "max": null, - "step": null + "disabled": false, + "visible": true }, { "id": "criticalColor", "name": "{i18n:scada.symbol.alarm-colors}", "type": "color", "default": "#D12730", - "required": null, "subLabel": "{i18n:scada.symbol.critical}", - "divider": null, - "fieldSuffix": null, - "disableOnProperty": null, - "rowClass": "", - "fieldClass": "", - "min": null, - "max": null, - "step": null + "disabled": false, + "visible": true } ] }]]> diff --git a/application/src/main/data/json/system/scada_symbols/small-cylindrical-tank.svg b/application/src/main/data/json/system/scada_symbols/small-cylindrical-tank.svg index 85bad5d9e0..9af2be50c4 100644 --- a/application/src/main/data/json/system/scada_symbols/small-cylindrical-tank.svg +++ b/application/src/main/data/json/system/scada_symbols/small-cylindrical-tank.svg @@ -34,7 +34,7 @@ }, { "tag": "scale", - "stateRenderFunction": "if (!ctx.properties.scale) {\n element.hide();\n} else {\n var scaleSet = element.remember('scaleSet');\n if (!scaleSet) {\n element.remember('scaleSet', true);\n element.clear();\n \n var majorIntervals = ctx.properties.majorIntervals;\n var minorIntervals = ctx.properties.minorIntervals;\n \n var start = 45;\n var majorIntervalLength = 525 / majorIntervals;\n var minorIntervalLength = majorIntervalLength / minorIntervals;\n for (var i = 0; i < majorIntervals + 1; i++) {\n var y = start + i * majorIntervalLength;\n var line = ctx.svg.line(340, y, 372, y).stroke({ width: 3 }).attr({class: 'majorTick'});\n element.add(line);\n var majorText = (100 - i * (100/majorIntervals)).toFixed(0);\n var majorTickText = ctx.svg.text(majorText);\n majorTickText.attr({x: 330, y: y + 2, 'text-anchor': 'end', class: 'majorTickText'});\n majorTickText.first().attr({'dominant-baseline': 'middle'});\n element.add(majorTickText);\n if (i < majorIntervals) {\n drawMinorTicks(y, minorIntervals, minorIntervalLength);\n }\n }\n }\n \n var majorFont = ctx.properties.majorFont;\n var majorColor = ctx.properties.majorColor;\n var minorColor = ctx.properties.minorColor;\n if (ctx.values.critical) {\n majorColor = ctx.properties.majorCriticalColor;\n minorColor = ctx.properties.minorCriticalColor;\n } else if (ctx.values.warning) {\n majorColor = ctx.properties.minorWarningColor;\n minorColor = ctx.properties.minorWarningColor;\n }\n \n var majorTicks = element.find('line.majorTick');\n majorTicks.forEach(t => t.attr({stroke: majorColor}));\n \n var majorTicksText = element.find('text.majorTickText');\n ctx.api.font(majorTicksText, majorFont, majorColor);\n \n var minorTicks = element.find('line.minorTick');\n minorTicks.forEach(t => t.attr({stroke: minorColor}));\n \n var elementCriticalAnimation = element.remember('criticalAnimation');\n var criticalAnimation = ctx.values.critical && ctx.values.criticalAnimation;\n\n if (elementCriticalAnimation !== criticalAnimation) {\n element.remember('criticalAnimation', criticalAnimation);\n if (criticalAnimation) {\n ctx.api.cssAnimate(element, 500).attr({opacity: 0.15}).loop(0, true);\n } else {\n ctx.api.resetCssAnimation(element);\n }\n }\n}\n\nfunction drawMinorTicks(start, minorIntervals, minorIntervalLength) {\n for (var i = 1; i < minorIntervals; i++) {\n var minorY = start + i * minorIntervalLength;\n var minorLine = ctx.svg.line(352, minorY, 372, minorY).stroke({ width: 3 }).attr({class: 'minorTick'});\n element.add(minorLine);\n }\n}", + "stateRenderFunction": "if (!ctx.properties.scale) {\n element.hide();\n} else {\n var scaleSet = element.remember('scaleSet');\n if (!scaleSet) {\n element.remember('scaleSet', true);\n element.clear();\n \n var majorIntervals = ctx.properties.majorIntervals;\n var minorIntervals = ctx.properties.minorIntervals;\n \n var start = 45;\n var majorIntervalLength = 525 / majorIntervals;\n var minorIntervalLength = majorIntervalLength / minorIntervals;\n var tankCapacity = ctx.properties.scaleDisplayFormat ? 100 : (ctx.values.tankCapacity || 100);\n for (var i = 0; i < majorIntervals + 1; i++) {\n var y = start + i * majorIntervalLength;\n var line = ctx.svg.line(340, y, 372, y).stroke({ width: 3 }).attr({class: 'majorTick'});\n element.add(line);\n var majorText = ctx.api.formatValue((tankCapacity - i * (tankCapacity/majorIntervals)).toFixed(0), 0, ctx.properties.majorUnits, false);\n var majorTickText = ctx.svg.text(majorText);\n majorTickText.attr({x: 330, y: y + 2, 'text-anchor': 'end', class: 'majorTickText'});\n majorTickText.first().attr({'dominant-baseline': 'middle'});\n element.add(majorTickText);\n if (i < majorIntervals) {\n drawMinorTicks(y, minorIntervals, minorIntervalLength);\n }\n }\n }\n \n var majorFont = ctx.properties.majorFont;\n var majorColor = ctx.properties.majorColor;\n var minorColor = ctx.properties.minorColor;\n if (ctx.values.critical) {\n majorColor = ctx.properties.majorCriticalColor;\n minorColor = ctx.properties.minorCriticalColor;\n } else if (ctx.values.warning) {\n majorColor = ctx.properties.minorWarningColor;\n minorColor = ctx.properties.minorWarningColor;\n }\n \n var majorTicks = element.find('line.majorTick');\n majorTicks.forEach(t => t.attr({stroke: majorColor}));\n \n var majorTicksText = element.find('text.majorTickText');\n ctx.api.font(majorTicksText, majorFont, majorColor);\n \n var minorTicks = element.find('line.minorTick');\n minorTicks.forEach(t => t.attr({stroke: minorColor}));\n \n var elementCriticalAnimation = element.remember('criticalAnimation');\n var criticalAnimation = ctx.values.critical && ctx.values.criticalAnimation;\n\n if (elementCriticalAnimation !== criticalAnimation) {\n element.remember('criticalAnimation', criticalAnimation);\n if (criticalAnimation) {\n ctx.api.cssAnimate(element, 500).attr({opacity: 0.15}).loop(0, true);\n } else {\n ctx.api.resetCssAnimation(element);\n }\n }\n}\n\nfunction drawMinorTicks(start, minorIntervals, minorIntervalLength) {\n for (var i = 1; i < minorIntervals; i++) {\n var minorY = start + i * minorIntervalLength;\n var minorLine = ctx.svg.line(352, minorY, 372, minorY).stroke({ width: 3 }).attr({class: 'minorTick'});\n element.add(minorLine);\n }\n}", "actions": null }, { @@ -279,80 +279,43 @@ "name": "{i18n:scada.symbol.tank-color}", "type": "color", "default": "#E5E5E5", - "required": null, - "subLabel": null, - "divider": null, - "fieldSuffix": null, - "disableOnProperty": null, - "rowClass": "", - "fieldClass": "", - "min": null, - "max": null, - "step": null + "disabled": false, + "visible": true }, { "id": "fluidColor", "name": "{i18n:scada.symbol.fluid-color}", "type": "color", "default": "#1EC1F480", - "required": null, - "subLabel": null, - "divider": null, - "fieldSuffix": null, - "disableOnProperty": null, - "rowClass": "", - "fieldClass": "", - "min": null, - "max": null, - "step": null + "disabled": false, + "visible": true }, { "id": "valueBox", "name": "{i18n:scada.symbol.value-box}", "type": "switch", "default": true, - "required": null, - "subLabel": null, - "divider": null, - "fieldSuffix": null, - "disableOnProperty": null, - "rowClass": "", - "fieldClass": "", - "min": null, - "max": null, - "step": null + "disabled": false, + "visible": true }, { "id": "valueBoxColor", "name": "{i18n:scada.symbol.value-box}", "type": "color", "default": "#F3F3F3", - "required": null, - "subLabel": null, - "divider": null, - "fieldSuffix": null, "disableOnProperty": "valueBox", - "rowClass": "", - "fieldClass": "", - "min": null, - "max": null, - "step": null + "disabled": true, + "visible": true }, { "id": "valueUnits", "name": "{i18n:scada.symbol.value-text}", "type": "units", "default": "gal", - "required": null, "subLabel": "{i18n:scada.symbol.units}", - "divider": null, - "fieldSuffix": null, "disableOnProperty": "valueBox", - "rowClass": "", - "fieldClass": "", - "min": null, - "max": null, - "step": null + "disabled": true, + "visible": true }, { "id": "valueTextFont", @@ -365,80 +328,78 @@ "weight": "500", "style": "normal" }, - "required": null, - "subLabel": null, - "divider": null, - "fieldSuffix": null, "disableOnProperty": "valueBox", - "rowClass": "", - "fieldClass": "", - "min": null, - "max": null, - "step": null + "disabled": true, + "visible": true }, { "id": "valueTextColor", "name": "{i18n:scada.symbol.value-text}", "type": "color", "default": "#0000008A", - "required": null, - "subLabel": null, - "divider": null, - "fieldSuffix": null, "disableOnProperty": "valueBox", - "rowClass": "", - "fieldClass": "", - "min": null, - "max": null, - "step": null + "disabled": true, + "visible": true }, { "id": "scale", "name": "{i18n:scada.symbol.scale}", "type": "switch", "default": true, - "required": null, - "subLabel": null, - "divider": null, - "fieldSuffix": null, - "disableOnProperty": null, - "rowClass": "", - "fieldClass": "", - "min": null, - "max": null, - "step": null + "disabled": false, + "visible": true + }, + { + "id": "scaleDisplayFormat", + "name": "{i18n:scada.symbol.scale}", + "type": "select", + "default": true, + "subLabel": "{i18n:scada.symbol.display-format}", + "disableOnProperty": "scale", + "items": [ + { + "value": true, + "label": "Percentage" + }, + { + "value": false, + "label": "Metric" + } + ], + "disabled": false, + "visible": true }, { "id": "transparent", "name": "{i18n:scada.symbol.transparent-mode}", "type": "switch", "default": false, - "required": null, - "subLabel": null, - "divider": null, - "fieldSuffix": null, "disableOnProperty": "scale", - "rowClass": "", - "fieldClass": "", - "min": null, - "max": null, - "step": null + "disabled": false, + "visible": true }, { "id": "majorIntervals", "name": "{i18n:scada.symbol.major-ticks}", "type": "number", "default": 5, - "required": null, "subLabel": "{i18n:scada.symbol.intervals}", "divider": true, - "fieldSuffix": null, "disableOnProperty": "scale", - "rowClass": "", - "fieldClass": "", "min": 1, - "max": null, - "step": 1 + "step": 1, + "disabled": false, + "visible": true + }, + { + "id": "majorUnits", + "name": "{i18n:scada.symbol.major-ticks}", + "type": "units", + "subLabel": "{i18n:scada.symbol.units}", + "divider": true, + "disableOnProperty": "scale", + "disabled": false, + "visible": true }, { "id": "majorFont", @@ -451,128 +412,84 @@ "weight": "500", "style": "normal" }, - "required": null, - "subLabel": null, - "divider": null, - "fieldSuffix": null, "disableOnProperty": "scale", - "rowClass": "", - "fieldClass": "", - "min": null, - "max": null, - "step": null + "disabled": false, + "visible": true }, { "id": "majorColor", "name": "{i18n:scada.symbol.major-ticks-color}", "type": "color", "default": "#00000061", - "required": null, "subLabel": "{i18n:scada.symbol.normal}", "divider": true, - "fieldSuffix": null, "disableOnProperty": "scale", - "rowClass": "", - "fieldClass": "", - "min": null, - "max": null, - "step": null + "disabled": false, + "visible": true }, { "id": "majorWarningColor", "name": "{i18n:scada.symbol.major-ticks-color}", "type": "color", "default": "#FAA405", - "required": null, "subLabel": "{i18n:scada.symbol.warning}", "divider": true, - "fieldSuffix": null, "disableOnProperty": "scale", - "rowClass": "", - "fieldClass": "", - "min": null, - "max": null, - "step": null + "disabled": false, + "visible": true }, { "id": "majorCriticalColor", "name": "{i18n:scada.symbol.major-ticks-color}", "type": "color", "default": "#D12730", - "required": null, "subLabel": "{i18n:scada.symbol.critical}", - "divider": null, - "fieldSuffix": null, "disableOnProperty": "scale", - "rowClass": "", - "fieldClass": "", - "min": null, - "max": null, - "step": null + "disabled": false, + "visible": true }, { "id": "minorIntervals", "name": "{i18n:scada.symbol.minor-ticks}", "type": "number", "default": 5, - "required": null, "subLabel": "{i18n:scada.symbol.intervals}", - "divider": null, - "fieldSuffix": null, "disableOnProperty": "scale", - "rowClass": "", - "fieldClass": "", "min": 1, - "max": null, - "step": null + "disabled": false, + "visible": true }, { "id": "minorColor", "name": "{i18n:scada.symbol.minor-ticks-color}", "type": "color", "default": "#0000001F", - "required": null, "subLabel": "{i18n:scada.symbol.normal}", "divider": true, - "fieldSuffix": null, "disableOnProperty": "scale", - "rowClass": "", - "fieldClass": "", - "min": null, - "max": null, - "step": null + "disabled": false, + "visible": true }, { "id": "minorWarningColor", "name": "{i18n:scada.symbol.minor-ticks-color}", "type": "color", "default": "#FAA405", - "required": null, "subLabel": "{i18n:scada.symbol.warning}", "divider": true, - "fieldSuffix": null, "disableOnProperty": "scale", - "rowClass": "", - "fieldClass": "", - "min": null, - "max": null, - "step": null + "disabled": false, + "visible": true }, { "id": "minorCriticalColor", "name": "{i18n:scada.symbol.minor-ticks-color}", "type": "color", "default": "#D12730", - "required": null, "subLabel": "{i18n:scada.symbol.critical}", - "divider": null, - "fieldSuffix": null, "disableOnProperty": "scale", - "rowClass": "", - "fieldClass": "", - "min": null, - "max": null, - "step": null + "disabled": false, + "visible": true } ] }]]> diff --git a/application/src/main/data/json/system/scada_symbols/small-spherical-tank.svg b/application/src/main/data/json/system/scada_symbols/small-spherical-tank.svg index 7e6e4fa8a6..e0b50e3200 100644 --- a/application/src/main/data/json/system/scada_symbols/small-spherical-tank.svg +++ b/application/src/main/data/json/system/scada_symbols/small-spherical-tank.svg @@ -34,7 +34,7 @@ }, { "tag": "scale", - "stateRenderFunction": "if (!ctx.properties.scale) {\n element.hide();\n} else {\n var scaleSet = element.remember('scaleSet');\n if (!scaleSet) {\n element.remember('scaleSet', true);\n element.clear();\n \n var majorIntervals = ctx.properties.majorIntervals;\n var minorIntervals = ctx.properties.minorIntervals;\n \n var start = 23;\n var majorIntervalLength = 560 / majorIntervals;\n var minorIntervalLength = majorIntervalLength / minorIntervals;\n for (var i = 0; i < majorIntervals + 1; i++) {\n var y = start + i * majorIntervalLength;\n var line = ctx.svg.line(268, y, 300, y).stroke({ width: 3 }).attr({class: 'majorTick'});\n element.add(line);\n var majorText = (100 - i * (100/majorIntervals)).toFixed(0);\n var majorTickText = ctx.svg.text(majorText);\n majorTickText.attr({x: 258, y: y + 2, 'text-anchor': 'end', class: 'majorTickText'});\n majorTickText.first().attr({'dominant-baseline': 'middle'});\n element.add(majorTickText);\n if (i < majorIntervals) {\n drawMinorTicks(y, minorIntervals, minorIntervalLength);\n }\n }\n }\n \n var majorFont = ctx.properties.majorFont;\n var majorColor = ctx.properties.majorColor;\n var minorColor = ctx.properties.minorColor;\n if (ctx.values.critical) {\n majorColor = ctx.properties.majorCriticalColor;\n minorColor = ctx.properties.minorCriticalColor;\n } else if (ctx.values.warning) {\n majorColor = ctx.properties.minorWarningColor;\n minorColor = ctx.properties.minorWarningColor;\n }\n \n var majorTicks = element.find('line.majorTick');\n majorTicks.forEach(t => t.attr({stroke: majorColor}));\n \n var majorTicksText = element.find('text.majorTickText');\n ctx.api.font(majorTicksText, majorFont, majorColor);\n \n var minorTicks = element.find('line.minorTick');\n minorTicks.forEach(t => t.attr({stroke: minorColor}));\n \n var elementCriticalAnimation = element.remember('criticalAnimation');\n var criticalAnimation = ctx.values.critical && ctx.values.criticalAnimation;\n\n if (elementCriticalAnimation !== criticalAnimation) {\n element.remember('criticalAnimation', criticalAnimation);\n if (criticalAnimation) {\n ctx.api.cssAnimate(element, 500).attr({opacity: 0.15}).loop(0, true);\n } else {\n ctx.api.resetCssAnimation(element);\n }\n }\n}\n\nfunction drawMinorTicks(start, minorIntervals, minorIntervalLength) {\n for (var i = 1; i < minorIntervals; i++) {\n var minorY = start + i * minorIntervalLength;\n var minorLine = ctx.svg.line(280, minorY, 300, minorY).stroke({ width: 3 }).attr({class: 'minorTick'});\n element.add(minorLine);\n }\n}", + "stateRenderFunction": "if (!ctx.properties.scale) {\n element.hide();\n} else {\n var scaleSet = element.remember('scaleSet');\n if (!scaleSet) {\n element.remember('scaleSet', true);\n element.clear();\n \n var majorIntervals = ctx.properties.majorIntervals;\n var minorIntervals = ctx.properties.minorIntervals;\n \n var start = 23;\n var majorIntervalLength = 560 / majorIntervals;\n var minorIntervalLength = majorIntervalLength / minorIntervals;\n var tankCapacity = ctx.properties.scaleDisplayFormat ? 100 : (ctx.values.tankCapacity || 100);\n for (var i = 0; i < majorIntervals + 1; i++) {\n var y = start + i * majorIntervalLength;\n var line = ctx.svg.line(268, y, 300, y).stroke({ width: 3 }).attr({class: 'majorTick'});\n element.add(line);\n var majorText = ctx.api.formatValue((tankCapacity - i * (tankCapacity/majorIntervals)).toFixed(0), 0, ctx.properties.majorUnits, false);\n var majorTickText = ctx.svg.text(majorText);\n majorTickText.attr({x: 258, y: y + 2, 'text-anchor': 'end', class: 'majorTickText'});\n majorTickText.first().attr({'dominant-baseline': 'middle'});\n element.add(majorTickText);\n if (i < majorIntervals) {\n drawMinorTicks(y, minorIntervals, minorIntervalLength);\n }\n }\n }\n \n var majorFont = ctx.properties.majorFont;\n var majorColor = ctx.properties.majorColor;\n var minorColor = ctx.properties.minorColor;\n if (ctx.values.critical) {\n majorColor = ctx.properties.majorCriticalColor;\n minorColor = ctx.properties.minorCriticalColor;\n } else if (ctx.values.warning) {\n majorColor = ctx.properties.minorWarningColor;\n minorColor = ctx.properties.minorWarningColor;\n }\n \n var majorTicks = element.find('line.majorTick');\n majorTicks.forEach(t => t.attr({stroke: majorColor}));\n \n var majorTicksText = element.find('text.majorTickText');\n ctx.api.font(majorTicksText, majorFont, majorColor);\n \n var minorTicks = element.find('line.minorTick');\n minorTicks.forEach(t => t.attr({stroke: minorColor}));\n \n var elementCriticalAnimation = element.remember('criticalAnimation');\n var criticalAnimation = ctx.values.critical && ctx.values.criticalAnimation;\n\n if (elementCriticalAnimation !== criticalAnimation) {\n element.remember('criticalAnimation', criticalAnimation);\n if (criticalAnimation) {\n ctx.api.cssAnimate(element, 500).attr({opacity: 0.15}).loop(0, true);\n } else {\n ctx.api.resetCssAnimation(element);\n }\n }\n}\n\nfunction drawMinorTicks(start, minorIntervals, minorIntervalLength) {\n for (var i = 1; i < minorIntervals; i++) {\n var minorY = start + i * minorIntervalLength;\n var minorLine = ctx.svg.line(280, minorY, 300, minorY).stroke({ width: 3 }).attr({class: 'minorTick'});\n element.add(minorLine);\n }\n}", "actions": null }, { @@ -279,80 +279,43 @@ "name": "{i18n:scada.symbol.tank-color}", "type": "color", "default": "#E5E5E5", - "required": null, - "subLabel": null, - "divider": null, - "fieldSuffix": null, - "disableOnProperty": null, - "rowClass": "", - "fieldClass": "", - "min": null, - "max": null, - "step": null + "disabled": false, + "visible": true }, { "id": "fluidColor", "name": "{i18n:scada.symbol.fluid-color}", "type": "color", "default": "#1EC1F480", - "required": null, - "subLabel": null, - "divider": null, - "fieldSuffix": null, - "disableOnProperty": null, - "rowClass": "", - "fieldClass": "", - "min": null, - "max": null, - "step": null + "disabled": false, + "visible": true }, { "id": "valueBox", "name": "{i18n:scada.symbol.value-box}", "type": "switch", "default": true, - "required": null, - "subLabel": null, - "divider": null, - "fieldSuffix": null, - "disableOnProperty": null, - "rowClass": "", - "fieldClass": "", - "min": null, - "max": null, - "step": null + "disabled": false, + "visible": true }, { "id": "valueBoxColor", "name": "{i18n:scada.symbol.value-box}", "type": "color", "default": "#F3F3F3", - "required": null, - "subLabel": null, - "divider": null, - "fieldSuffix": null, "disableOnProperty": "valueBox", - "rowClass": "", - "fieldClass": "", - "min": null, - "max": null, - "step": null + "disabled": true, + "visible": true }, { "id": "valueUnits", "name": "{i18n:scada.symbol.value-text}", "type": "units", "default": "gal", - "required": null, "subLabel": "{i18n:scada.symbol.units}", - "divider": null, - "fieldSuffix": null, "disableOnProperty": "valueBox", - "rowClass": "", - "fieldClass": "", - "min": null, - "max": null, - "step": null + "disabled": true, + "visible": true }, { "id": "valueTextFont", @@ -365,80 +328,78 @@ "weight": "500", "style": "normal" }, - "required": null, - "subLabel": null, - "divider": null, - "fieldSuffix": null, "disableOnProperty": "valueBox", - "rowClass": "", - "fieldClass": "", - "min": null, - "max": null, - "step": null + "disabled": true, + "visible": true }, { "id": "valueTextColor", "name": "{i18n:scada.symbol.value-text}", "type": "color", "default": "#0000008A", - "required": null, - "subLabel": null, - "divider": null, - "fieldSuffix": null, "disableOnProperty": "valueBox", - "rowClass": "", - "fieldClass": "", - "min": null, - "max": null, - "step": null + "disabled": true, + "visible": true }, { "id": "scale", "name": "{i18n:scada.symbol.scale}", "type": "switch", "default": true, - "required": null, - "subLabel": null, - "divider": null, - "fieldSuffix": null, - "disableOnProperty": null, - "rowClass": "", - "fieldClass": "", - "min": null, - "max": null, - "step": null + "disabled": false, + "visible": true + }, + { + "id": "scaleDisplayFormat", + "name": "{i18n:scada.symbol.scale}", + "type": "select", + "default": true, + "subLabel": "{i18n:scada.symbol.display-format}", + "disableOnProperty": "scale", + "items": [ + { + "value": true, + "label": "Percentage" + }, + { + "value": false, + "label": "Metric" + } + ], + "disabled": false, + "visible": true }, { "id": "transparent", "name": "{i18n:scada.symbol.transparent-mode}", "type": "switch", "default": false, - "required": null, - "subLabel": null, - "divider": null, - "fieldSuffix": null, "disableOnProperty": "scale", - "rowClass": "", - "fieldClass": "", - "min": null, - "max": null, - "step": null + "disabled": false, + "visible": true }, { "id": "majorIntervals", "name": "{i18n:scada.symbol.major-ticks}", "type": "number", "default": 5, - "required": null, "subLabel": "{i18n:scada.symbol.intervals}", "divider": true, - "fieldSuffix": null, "disableOnProperty": "scale", - "rowClass": "", - "fieldClass": "", "min": 1, - "max": null, - "step": 1 + "step": 1, + "disabled": false, + "visible": true + }, + { + "id": "majorUnits", + "name": "{i18n:scada.symbol.major-ticks}", + "type": "units", + "subLabel": "{i18n:scada.symbol.units}", + "divider": true, + "disableOnProperty": "scale", + "disabled": false, + "visible": true }, { "id": "majorFont", @@ -451,128 +412,84 @@ "weight": "500", "style": "normal" }, - "required": null, - "subLabel": null, - "divider": null, - "fieldSuffix": null, "disableOnProperty": "scale", - "rowClass": "", - "fieldClass": "", - "min": null, - "max": null, - "step": null + "disabled": false, + "visible": true }, { "id": "majorColor", "name": "{i18n:scada.symbol.major-ticks-color}", "type": "color", "default": "#00000061", - "required": null, "subLabel": "{i18n:scada.symbol.normal}", "divider": true, - "fieldSuffix": null, "disableOnProperty": "scale", - "rowClass": "", - "fieldClass": "", - "min": null, - "max": null, - "step": null + "disabled": false, + "visible": true }, { "id": "majorWarningColor", "name": "{i18n:scada.symbol.major-ticks-color}", "type": "color", "default": "#FAA405", - "required": null, "subLabel": "{i18n:scada.symbol.warning}", "divider": true, - "fieldSuffix": null, "disableOnProperty": "scale", - "rowClass": "", - "fieldClass": "", - "min": null, - "max": null, - "step": null + "disabled": false, + "visible": true }, { "id": "majorCriticalColor", "name": "{i18n:scada.symbol.major-ticks-color}", "type": "color", "default": "#D12730", - "required": null, "subLabel": "{i18n:scada.symbol.critical}", - "divider": null, - "fieldSuffix": null, "disableOnProperty": "scale", - "rowClass": "", - "fieldClass": "", - "min": null, - "max": null, - "step": null + "disabled": false, + "visible": true }, { "id": "minorIntervals", "name": "{i18n:scada.symbol.minor-ticks}", "type": "number", "default": 5, - "required": null, "subLabel": "{i18n:scada.symbol.intervals}", - "divider": null, - "fieldSuffix": null, "disableOnProperty": "scale", - "rowClass": "", - "fieldClass": "", "min": 1, - "max": null, - "step": null + "disabled": false, + "visible": true }, { "id": "minorColor", "name": "{i18n:scada.symbol.minor-ticks-color}", "type": "color", "default": "#0000001F", - "required": null, "subLabel": "{i18n:scada.symbol.normal}", "divider": true, - "fieldSuffix": null, "disableOnProperty": "scale", - "rowClass": "", - "fieldClass": "", - "min": null, - "max": null, - "step": null + "disabled": false, + "visible": true }, { "id": "minorWarningColor", "name": "{i18n:scada.symbol.minor-ticks-color}", "type": "color", "default": "#FAA405", - "required": null, "subLabel": "{i18n:scada.symbol.warning}", "divider": true, - "fieldSuffix": null, "disableOnProperty": "scale", - "rowClass": "", - "fieldClass": "", - "min": null, - "max": null, - "step": null + "disabled": false, + "visible": true }, { "id": "minorCriticalColor", "name": "{i18n:scada.symbol.minor-ticks-color}", "type": "color", "default": "#D12730", - "required": null, "subLabel": "{i18n:scada.symbol.critical}", - "divider": null, - "fieldSuffix": null, "disableOnProperty": "scale", - "rowClass": "", - "fieldClass": "", - "min": null, - "max": null, - "step": null + "disabled": false, + "visible": true } ] }]]> diff --git a/application/src/main/data/json/system/scada_symbols/spherical-tank.svg b/application/src/main/data/json/system/scada_symbols/spherical-tank.svg index 4184bcf593..11698d6636 100644 --- a/application/src/main/data/json/system/scada_symbols/spherical-tank.svg +++ b/application/src/main/data/json/system/scada_symbols/spherical-tank.svg @@ -34,7 +34,7 @@ }, { "tag": "scale", - "stateRenderFunction": "if (!ctx.properties.scale) {\n element.hide();\n} else {\n var scaleSet = element.remember('scaleSet');\n if (!scaleSet) {\n element.remember('scaleSet', true);\n element.clear();\n \n var majorIntervals = ctx.properties.majorIntervals;\n var minorIntervals = ctx.properties.minorIntervals;\n \n var start = 23;\n var majorIntervalLength = 960 / majorIntervals;\n var minorIntervalLength = majorIntervalLength / minorIntervals;\n for (var i = 0; i < majorIntervals + 1; i++) {\n var y = start + i * majorIntervalLength;\n var line = ctx.svg.line(458, y, 490, y).stroke({ width: 3 }).attr({class: 'majorTick'});\n element.add(line);\n var majorText = (100 - i * (100/majorIntervals)).toFixed(0);\n var majorTickText = ctx.svg.text(majorText);\n majorTickText.attr({x: 448, y: y + 2, 'text-anchor': 'end', class: 'majorTickText'});\n majorTickText.first().attr({'dominant-baseline': 'middle'});\n element.add(majorTickText);\n if (i < majorIntervals) {\n drawMinorTicks(y, minorIntervals, minorIntervalLength);\n }\n }\n }\n \n var majorFont = ctx.properties.majorFont;\n var majorColor = ctx.properties.majorColor;\n var minorColor = ctx.properties.minorColor;\n if (ctx.values.critical) {\n majorColor = ctx.properties.majorCriticalColor;\n minorColor = ctx.properties.minorCriticalColor;\n } else if (ctx.values.warning) {\n majorColor = ctx.properties.minorWarningColor;\n minorColor = ctx.properties.minorWarningColor;\n }\n \n var majorTicks = element.find('line.majorTick');\n majorTicks.forEach(t => t.attr({stroke: majorColor}));\n \n var majorTicksText = element.find('text.majorTickText');\n ctx.api.font(majorTicksText, majorFont, majorColor);\n \n var minorTicks = element.find('line.minorTick');\n minorTicks.forEach(t => t.attr({stroke: minorColor}));\n \n var elementCriticalAnimation = element.remember('criticalAnimation');\n var criticalAnimation = ctx.values.critical && ctx.values.criticalAnimation;\n\n if (elementCriticalAnimation !== criticalAnimation) {\n element.remember('criticalAnimation', criticalAnimation);\n if (criticalAnimation) {\n ctx.api.cssAnimate(element, 500).attr({opacity: 0.15}).loop(0, true);\n } else {\n ctx.api.resetCssAnimation(element);\n }\n }\n}\n\nfunction drawMinorTicks(start, minorIntervals, minorIntervalLength) {\n for (var i = 1; i < minorIntervals; i++) {\n var minorY = start + i * minorIntervalLength;\n var minorLine = ctx.svg.line(470, minorY, 490, minorY).stroke({ width: 3 }).attr({class: 'minorTick'});\n element.add(minorLine);\n }\n}", + "stateRenderFunction": "if (!ctx.properties.scale) {\n element.hide();\n} else {\n var scaleSet = element.remember('scaleSet');\n if (!scaleSet) {\n element.remember('scaleSet', true);\n element.clear();\n \n var majorIntervals = ctx.properties.majorIntervals;\n var minorIntervals = ctx.properties.minorIntervals;\n \n var start = 23;\n var majorIntervalLength = 960 / majorIntervals;\n var minorIntervalLength = majorIntervalLength / minorIntervals;\n var tankCapacity = ctx.properties.scaleDisplayFormat ? 100 : (ctx.values.tankCapacity || 100);\n for (var i = 0; i < majorIntervals + 1; i++) {\n var y = start + i * majorIntervalLength;\n var line = ctx.svg.line(458, y, 490, y).stroke({ width: 3 }).attr({class: 'majorTick'});\n element.add(line);\n var majorText = ctx.api.formatValue((tankCapacity - i * (tankCapacity/majorIntervals)).toFixed(0), 0, ctx.properties.majorUnits, false);\n var majorTickText = ctx.svg.text(majorText);\n majorTickText.attr({x: 448, y: y + 2, 'text-anchor': 'end', class: 'majorTickText'});\n majorTickText.first().attr({'dominant-baseline': 'middle'});\n element.add(majorTickText);\n if (i < majorIntervals) {\n drawMinorTicks(y, minorIntervals, minorIntervalLength);\n }\n }\n }\n \n var majorFont = ctx.properties.majorFont;\n var majorColor = ctx.properties.majorColor;\n var minorColor = ctx.properties.minorColor;\n if (ctx.values.critical) {\n majorColor = ctx.properties.majorCriticalColor;\n minorColor = ctx.properties.minorCriticalColor;\n } else if (ctx.values.warning) {\n majorColor = ctx.properties.minorWarningColor;\n minorColor = ctx.properties.minorWarningColor;\n }\n \n var majorTicks = element.find('line.majorTick');\n majorTicks.forEach(t => t.attr({stroke: majorColor}));\n \n var majorTicksText = element.find('text.majorTickText');\n ctx.api.font(majorTicksText, majorFont, majorColor);\n \n var minorTicks = element.find('line.minorTick');\n minorTicks.forEach(t => t.attr({stroke: minorColor}));\n \n var elementCriticalAnimation = element.remember('criticalAnimation');\n var criticalAnimation = ctx.values.critical && ctx.values.criticalAnimation;\n\n if (elementCriticalAnimation !== criticalAnimation) {\n element.remember('criticalAnimation', criticalAnimation);\n if (criticalAnimation) {\n ctx.api.cssAnimate(element, 500).attr({opacity: 0.15}).loop(0, true);\n } else {\n ctx.api.resetCssAnimation(element);\n }\n }\n}\n\nfunction drawMinorTicks(start, minorIntervals, minorIntervalLength) {\n for (var i = 1; i < minorIntervals; i++) {\n var minorY = start + i * minorIntervalLength;\n var minorLine = ctx.svg.line(470, minorY, 490, minorY).stroke({ width: 3 }).attr({class: 'minorTick'});\n element.add(minorLine);\n }\n}", "actions": null }, { @@ -279,80 +279,43 @@ "name": "{i18n:scada.symbol.tank-color}", "type": "color", "default": "#E5E5E5", - "required": null, - "subLabel": null, - "divider": null, - "fieldSuffix": null, - "disableOnProperty": null, - "rowClass": "", - "fieldClass": "", - "min": null, - "max": null, - "step": null + "disabled": false, + "visible": true }, { "id": "fluidColor", "name": "{i18n:scada.symbol.fluid-color}", "type": "color", "default": "#1EC1F480", - "required": null, - "subLabel": null, - "divider": null, - "fieldSuffix": null, - "disableOnProperty": null, - "rowClass": "", - "fieldClass": "", - "min": null, - "max": null, - "step": null + "disabled": false, + "visible": true }, { "id": "valueBox", "name": "{i18n:scada.symbol.value-box}", "type": "switch", "default": true, - "required": null, - "subLabel": null, - "divider": null, - "fieldSuffix": null, - "disableOnProperty": null, - "rowClass": "", - "fieldClass": "", - "min": null, - "max": null, - "step": null + "disabled": false, + "visible": true }, { "id": "valueBoxColor", "name": "{i18n:scada.symbol.value-box}", "type": "color", "default": "#F3F3F3", - "required": null, - "subLabel": null, - "divider": null, - "fieldSuffix": null, "disableOnProperty": "valueBox", - "rowClass": "", - "fieldClass": "", - "min": null, - "max": null, - "step": null + "disabled": true, + "visible": true }, { "id": "valueUnits", "name": "{i18n:scada.symbol.value-text}", "type": "units", "default": "gal", - "required": null, "subLabel": "{i18n:scada.symbol.units}", - "divider": null, - "fieldSuffix": null, "disableOnProperty": "valueBox", - "rowClass": "", - "fieldClass": "", - "min": null, - "max": null, - "step": null + "disabled": true, + "visible": true }, { "id": "valueTextFont", @@ -365,80 +328,78 @@ "weight": "500", "style": "normal" }, - "required": null, - "subLabel": null, - "divider": null, - "fieldSuffix": null, "disableOnProperty": "valueBox", - "rowClass": "", - "fieldClass": "", - "min": null, - "max": null, - "step": null + "disabled": true, + "visible": true }, { "id": "valueTextColor", "name": "{i18n:scada.symbol.value-text}", "type": "color", "default": "#0000008A", - "required": null, - "subLabel": null, - "divider": null, - "fieldSuffix": null, "disableOnProperty": "valueBox", - "rowClass": "", - "fieldClass": "", - "min": null, - "max": null, - "step": null + "disabled": true, + "visible": true }, { "id": "scale", "name": "{i18n:scada.symbol.scale}", "type": "switch", "default": true, - "required": null, - "subLabel": null, - "divider": null, - "fieldSuffix": null, - "disableOnProperty": null, - "rowClass": "", - "fieldClass": "", - "min": null, - "max": null, - "step": null + "disabled": false, + "visible": true + }, + { + "id": "scaleDisplayFormat", + "name": "{i18n:scada.symbol.scale}", + "type": "select", + "default": true, + "subLabel": "{i18n:scada.symbol.display-format}", + "disableOnProperty": "scale", + "items": [ + { + "value": true, + "label": "Percentage" + }, + { + "value": false, + "label": "Metric" + } + ], + "disabled": false, + "visible": true }, { "id": "transparent", "name": "{i18n:scada.symbol.transparent-mode}", "type": "switch", "default": false, - "required": null, - "subLabel": null, - "divider": null, - "fieldSuffix": null, "disableOnProperty": "scale", - "rowClass": "", - "fieldClass": "", - "min": null, - "max": null, - "step": null + "disabled": false, + "visible": true }, { "id": "majorIntervals", "name": "{i18n:scada.symbol.major-ticks}", "type": "number", "default": 10, - "required": null, "subLabel": "{i18n:scada.symbol.intervals}", "divider": true, - "fieldSuffix": null, "disableOnProperty": "scale", - "rowClass": "", - "fieldClass": "", "min": 1, - "max": null, - "step": 1 + "step": 1, + "disabled": false, + "visible": true + }, + { + "id": "majorUnits", + "name": "{i18n:scada.symbol.major-ticks}", + "type": "units", + "subLabel": "{i18n:scada.symbol.units}", + "divider": true, + "disableOnProperty": "scale", + "disabled": false, + "visible": true }, { "id": "majorFont", @@ -451,128 +412,84 @@ "weight": "500", "style": "normal" }, - "required": null, - "subLabel": null, - "divider": null, - "fieldSuffix": null, "disableOnProperty": "scale", - "rowClass": "", - "fieldClass": "", - "min": null, - "max": null, - "step": null + "disabled": false, + "visible": true }, { "id": "majorColor", "name": "{i18n:scada.symbol.major-ticks-color}", "type": "color", "default": "#00000061", - "required": null, "subLabel": "{i18n:scada.symbol.normal}", "divider": true, - "fieldSuffix": null, "disableOnProperty": "scale", - "rowClass": "", - "fieldClass": "", - "min": null, - "max": null, - "step": null + "disabled": false, + "visible": true }, { "id": "majorWarningColor", "name": "{i18n:scada.symbol.major-ticks-color}", "type": "color", "default": "#FAA405", - "required": null, "subLabel": "{i18n:scada.symbol.warning}", "divider": true, - "fieldSuffix": null, "disableOnProperty": "scale", - "rowClass": "", - "fieldClass": "", - "min": null, - "max": null, - "step": null + "disabled": false, + "visible": true }, { "id": "majorCriticalColor", "name": "{i18n:scada.symbol.major-ticks-color}", "type": "color", "default": "#D12730", - "required": null, "subLabel": "{i18n:scada.symbol.critical}", - "divider": null, - "fieldSuffix": null, "disableOnProperty": "scale", - "rowClass": "", - "fieldClass": "", - "min": null, - "max": null, - "step": null + "disabled": false, + "visible": true }, { "id": "minorIntervals", "name": "{i18n:scada.symbol.minor-ticks}", "type": "number", "default": 5, - "required": null, "subLabel": "{i18n:scada.symbol.intervals}", - "divider": null, - "fieldSuffix": null, "disableOnProperty": "scale", - "rowClass": "", - "fieldClass": "", "min": 1, - "max": null, - "step": null + "disabled": false, + "visible": true }, { "id": "minorColor", "name": "{i18n:scada.symbol.minor-ticks-color}", "type": "color", "default": "#0000001F", - "required": null, "subLabel": "{i18n:scada.symbol.normal}", "divider": true, - "fieldSuffix": null, "disableOnProperty": "scale", - "rowClass": "", - "fieldClass": "", - "min": null, - "max": null, - "step": null + "disabled": false, + "visible": true }, { "id": "minorWarningColor", "name": "{i18n:scada.symbol.minor-ticks-color}", "type": "color", "default": "#FAA405", - "required": null, "subLabel": "{i18n:scada.symbol.warning}", "divider": true, - "fieldSuffix": null, "disableOnProperty": "scale", - "rowClass": "", - "fieldClass": "", - "min": null, - "max": null, - "step": null + "disabled": false, + "visible": true }, { "id": "minorCriticalColor", "name": "{i18n:scada.symbol.minor-ticks-color}", "type": "color", "default": "#D12730", - "required": null, "subLabel": "{i18n:scada.symbol.critical}", - "divider": null, - "fieldSuffix": null, "disableOnProperty": "scale", - "rowClass": "", - "fieldClass": "", - "min": null, - "max": null, - "step": null + "disabled": false, + "visible": true } ] }]]> diff --git a/application/src/main/data/json/system/scada_symbols/stand-cylindrical-tank.svg b/application/src/main/data/json/system/scada_symbols/stand-cylindrical-tank.svg index 2ad4ac9ec8..9ee0eb5122 100644 --- a/application/src/main/data/json/system/scada_symbols/stand-cylindrical-tank.svg +++ b/application/src/main/data/json/system/scada_symbols/stand-cylindrical-tank.svg @@ -34,7 +34,7 @@ }, { "tag": "scale", - "stateRenderFunction": "if (!ctx.properties.scale) {\n element.hide();\n} else {\n var scaleSet = element.remember('scaleSet');\n if (!scaleSet) {\n element.remember('scaleSet', true);\n element.clear();\n \n var majorIntervals = ctx.properties.majorIntervals;\n var minorIntervals = ctx.properties.minorIntervals;\n \n var start = 205;\n var majorIntervalLength = 760 / majorIntervals;\n var minorIntervalLength = majorIntervalLength / minorIntervals;\n for (var i = 0; i < majorIntervals + 1; i++) {\n var y = start + i * majorIntervalLength;\n var line = ctx.svg.line(340, y, 372, y).stroke({ width: 3 }).attr({class: 'majorTick'});\n element.add(line);\n var majorText = (100 - i * (100/majorIntervals)).toFixed(0);\n var majorTickText = ctx.svg.text(majorText);\n majorTickText.attr({x: 330, y: y + 2, 'text-anchor': 'end', class: 'majorTickText'});\n majorTickText.first().attr({'dominant-baseline': 'middle'});\n element.add(majorTickText);\n if (i < majorIntervals) {\n drawMinorTicks(y, minorIntervals, minorIntervalLength);\n }\n }\n }\n \n var majorFont = ctx.properties.majorFont;\n var majorColor = ctx.properties.majorColor;\n var minorColor = ctx.properties.minorColor;\n if (ctx.values.critical) {\n majorColor = ctx.properties.majorCriticalColor;\n minorColor = ctx.properties.minorCriticalColor;\n } else if (ctx.values.warning) {\n majorColor = ctx.properties.minorWarningColor;\n minorColor = ctx.properties.minorWarningColor;\n }\n \n var majorTicks = element.find('line.majorTick');\n majorTicks.forEach(t => t.attr({stroke: majorColor}));\n \n var majorTicksText = element.find('text.majorTickText');\n ctx.api.font(majorTicksText, majorFont, majorColor);\n \n var minorTicks = element.find('line.minorTick');\n minorTicks.forEach(t => t.attr({stroke: minorColor}));\n \n var elementCriticalAnimation = element.remember('criticalAnimation');\n var criticalAnimation = ctx.values.critical && ctx.values.criticalAnimation;\n\n if (elementCriticalAnimation !== criticalAnimation) {\n element.remember('criticalAnimation', criticalAnimation);\n if (criticalAnimation) {\n ctx.api.cssAnimate(element, 500).attr({opacity: 0.15}).loop(0, true);\n } else {\n ctx.api.resetCssAnimation(element);\n }\n }\n}\n\nfunction drawMinorTicks(start, minorIntervals, minorIntervalLength) {\n for (var i = 1; i < minorIntervals; i++) {\n var minorY = start + i * minorIntervalLength;\n var minorLine = ctx.svg.line(352, minorY, 372, minorY).stroke({ width: 3 }).attr({class: 'minorTick'});\n element.add(minorLine);\n }\n}", + "stateRenderFunction": "if (!ctx.properties.scale) {\n element.hide();\n} else {\n var scaleSet = element.remember('scaleSet');\n if (!scaleSet) {\n element.remember('scaleSet', true);\n element.clear();\n \n var majorIntervals = ctx.properties.majorIntervals;\n var minorIntervals = ctx.properties.minorIntervals;\n \n var start = 205;\n var majorIntervalLength = 760 / majorIntervals;\n var minorIntervalLength = majorIntervalLength / minorIntervals;\n var tankCapacity = ctx.properties.scaleDisplayFormat ? 100 : (ctx.values.tankCapacity || 100);\n for (var i = 0; i < majorIntervals + 1; i++) {\n var y = start + i * majorIntervalLength;\n var line = ctx.svg.line(340, y, 372, y).stroke({ width: 3 }).attr({class: 'majorTick'});\n element.add(line);\n var majorText = ctx.api.formatValue((tankCapacity - i * (tankCapacity/majorIntervals)).toFixed(0), 0, ctx.properties.majorUnits, false);\n var majorTickText = ctx.svg.text(majorText);\n majorTickText.attr({x: 330, y: y + 2, 'text-anchor': 'end', class: 'majorTickText'});\n majorTickText.first().attr({'dominant-baseline': 'middle'});\n element.add(majorTickText);\n if (i < majorIntervals) {\n drawMinorTicks(y, minorIntervals, minorIntervalLength);\n }\n }\n }\n \n var majorFont = ctx.properties.majorFont;\n var majorColor = ctx.properties.majorColor;\n var minorColor = ctx.properties.minorColor;\n if (ctx.values.critical) {\n majorColor = ctx.properties.majorCriticalColor;\n minorColor = ctx.properties.minorCriticalColor;\n } else if (ctx.values.warning) {\n majorColor = ctx.properties.minorWarningColor;\n minorColor = ctx.properties.minorWarningColor;\n }\n \n var majorTicks = element.find('line.majorTick');\n majorTicks.forEach(t => t.attr({stroke: majorColor}));\n \n var majorTicksText = element.find('text.majorTickText');\n ctx.api.font(majorTicksText, majorFont, majorColor);\n \n var minorTicks = element.find('line.minorTick');\n minorTicks.forEach(t => t.attr({stroke: minorColor}));\n \n var elementCriticalAnimation = element.remember('criticalAnimation');\n var criticalAnimation = ctx.values.critical && ctx.values.criticalAnimation;\n\n if (elementCriticalAnimation !== criticalAnimation) {\n element.remember('criticalAnimation', criticalAnimation);\n if (criticalAnimation) {\n ctx.api.cssAnimate(element, 500).attr({opacity: 0.15}).loop(0, true);\n } else {\n ctx.api.resetCssAnimation(element);\n }\n }\n}\n\nfunction drawMinorTicks(start, minorIntervals, minorIntervalLength) {\n for (var i = 1; i < minorIntervals; i++) {\n var minorY = start + i * minorIntervalLength;\n var minorLine = ctx.svg.line(352, minorY, 372, minorY).stroke({ width: 3 }).attr({class: 'minorTick'});\n element.add(minorLine);\n }\n}", "actions": null }, { @@ -279,80 +279,43 @@ "name": "{i18n:scada.symbol.tank-color}", "type": "color", "default": "#E5E5E5", - "required": null, - "subLabel": null, - "divider": null, - "fieldSuffix": null, - "disableOnProperty": null, - "rowClass": "", - "fieldClass": "", - "min": null, - "max": null, - "step": null + "disabled": false, + "visible": true }, { "id": "fluidColor", "name": "{i18n:scada.symbol.fluid-color}", "type": "color", "default": "#1EC1F480", - "required": null, - "subLabel": null, - "divider": null, - "fieldSuffix": null, - "disableOnProperty": null, - "rowClass": "", - "fieldClass": "", - "min": null, - "max": null, - "step": null + "disabled": false, + "visible": true }, { "id": "valueBox", "name": "{i18n:scada.symbol.value-box}", "type": "switch", "default": true, - "required": null, - "subLabel": null, - "divider": null, - "fieldSuffix": null, - "disableOnProperty": null, - "rowClass": "", - "fieldClass": "", - "min": null, - "max": null, - "step": null + "disabled": false, + "visible": true }, { "id": "valueBoxColor", "name": "{i18n:scada.symbol.value-box}", "type": "color", "default": "#F3F3F3", - "required": null, - "subLabel": null, - "divider": null, - "fieldSuffix": null, "disableOnProperty": "valueBox", - "rowClass": "", - "fieldClass": "", - "min": null, - "max": null, - "step": null + "disabled": false, + "visible": true }, { "id": "valueUnits", "name": "{i18n:scada.symbol.value-text}", "type": "units", "default": "gal", - "required": null, "subLabel": "{i18n:scada.symbol.units}", - "divider": null, - "fieldSuffix": null, "disableOnProperty": "valueBox", - "rowClass": "", - "fieldClass": "", - "min": null, - "max": null, - "step": null + "disabled": false, + "visible": true }, { "id": "valueTextFont", @@ -365,80 +328,78 @@ "weight": "500", "style": "normal" }, - "required": null, - "subLabel": null, - "divider": null, - "fieldSuffix": null, "disableOnProperty": "valueBox", - "rowClass": "", - "fieldClass": "", - "min": null, - "max": null, - "step": null + "disabled": false, + "visible": true }, { "id": "valueTextColor", "name": "{i18n:scada.symbol.value-text}", "type": "color", "default": "#0000008A", - "required": null, - "subLabel": null, - "divider": null, - "fieldSuffix": null, "disableOnProperty": "valueBox", - "rowClass": "", - "fieldClass": "", - "min": null, - "max": null, - "step": null + "disabled": false, + "visible": true }, { "id": "scale", "name": "{i18n:scada.symbol.scale}", "type": "switch", "default": true, - "required": null, - "subLabel": null, - "divider": null, - "fieldSuffix": null, - "disableOnProperty": null, - "rowClass": "", - "fieldClass": "", - "min": null, - "max": null, - "step": null + "disabled": false, + "visible": true + }, + { + "id": "scaleDisplayFormat", + "name": "{i18n:scada.symbol.scale}", + "type": "select", + "default": true, + "subLabel": "{i18n:scada.symbol.display-format}", + "disableOnProperty": "scale", + "items": [ + { + "value": true, + "label": "Percentage" + }, + { + "value": false, + "label": "Metric" + } + ], + "disabled": false, + "visible": true }, { "id": "transparent", "name": "{i18n:scada.symbol.transparent-mode}", "type": "switch", "default": false, - "required": null, - "subLabel": null, - "divider": null, - "fieldSuffix": null, "disableOnProperty": "scale", - "rowClass": "", - "fieldClass": "", - "min": null, - "max": null, - "step": null + "disabled": false, + "visible": true }, { "id": "majorIntervals", "name": "{i18n:scada.symbol.major-ticks}", "type": "number", "default": 10, - "required": null, "subLabel": "{i18n:scada.symbol.intervals}", "divider": true, - "fieldSuffix": null, "disableOnProperty": "scale", - "rowClass": "", - "fieldClass": "", "min": 1, - "max": null, - "step": 1 + "step": 1, + "disabled": false, + "visible": true + }, + { + "id": "majorUnits", + "name": "{i18n:scada.symbol.major-ticks}", + "type": "units", + "subLabel": "{i18n:scada.symbol.units}", + "divider": true, + "disableOnProperty": "scale", + "disabled": false, + "visible": true }, { "id": "majorFont", @@ -451,128 +412,84 @@ "weight": "500", "style": "normal" }, - "required": null, - "subLabel": null, - "divider": null, - "fieldSuffix": null, "disableOnProperty": "scale", - "rowClass": "", - "fieldClass": "", - "min": null, - "max": null, - "step": null + "disabled": false, + "visible": true }, { "id": "majorColor", "name": "{i18n:scada.symbol.major-ticks-color}", "type": "color", "default": "#00000061", - "required": null, "subLabel": "{i18n:scada.symbol.normal}", "divider": true, - "fieldSuffix": null, "disableOnProperty": "scale", - "rowClass": "", - "fieldClass": "", - "min": null, - "max": null, - "step": null + "disabled": false, + "visible": true }, { "id": "majorWarningColor", "name": "{i18n:scada.symbol.major-ticks-color}", "type": "color", "default": "#FAA405", - "required": null, "subLabel": "{i18n:scada.symbol.warning}", "divider": true, - "fieldSuffix": null, "disableOnProperty": "scale", - "rowClass": "", - "fieldClass": "", - "min": null, - "max": null, - "step": null + "disabled": false, + "visible": true }, { "id": "majorCriticalColor", "name": "{i18n:scada.symbol.major-ticks-color}", "type": "color", "default": "#D12730", - "required": null, "subLabel": "{i18n:scada.symbol.critical}", - "divider": null, - "fieldSuffix": null, "disableOnProperty": "scale", - "rowClass": "", - "fieldClass": "", - "min": null, - "max": null, - "step": null + "disabled": false, + "visible": true }, { "id": "minorIntervals", "name": "{i18n:scada.symbol.minor-ticks}", "type": "number", "default": 5, - "required": null, "subLabel": "{i18n:scada.symbol.intervals}", - "divider": null, - "fieldSuffix": null, "disableOnProperty": "scale", - "rowClass": "", - "fieldClass": "", "min": 1, - "max": null, - "step": null + "disabled": false, + "visible": true }, { "id": "minorColor", "name": "{i18n:scada.symbol.minor-ticks-color}", "type": "color", "default": "#0000001F", - "required": null, "subLabel": "{i18n:scada.symbol.normal}", "divider": true, - "fieldSuffix": null, "disableOnProperty": "scale", - "rowClass": "", - "fieldClass": "", - "min": null, - "max": null, - "step": null + "disabled": false, + "visible": true }, { "id": "minorWarningColor", "name": "{i18n:scada.symbol.minor-ticks-color}", "type": "color", "default": "#FAA405", - "required": null, "subLabel": "{i18n:scada.symbol.warning}", "divider": true, - "fieldSuffix": null, "disableOnProperty": "scale", - "rowClass": "", - "fieldClass": "", - "min": null, - "max": null, - "step": null + "disabled": false, + "visible": true }, { "id": "minorCriticalColor", "name": "{i18n:scada.symbol.minor-ticks-color}", "type": "color", "default": "#D12730", - "required": null, "subLabel": "{i18n:scada.symbol.critical}", - "divider": null, - "fieldSuffix": null, "disableOnProperty": "scale", - "rowClass": "", - "fieldClass": "", - "min": null, - "max": null, - "step": null + "disabled": false, + "visible": true } ] }]]> diff --git a/application/src/main/data/json/system/scada_symbols/stand-horizontal-tank.svg b/application/src/main/data/json/system/scada_symbols/stand-horizontal-tank.svg index 8c3745ec3d..5ec5f34b8f 100644 --- a/application/src/main/data/json/system/scada_symbols/stand-horizontal-tank.svg +++ b/application/src/main/data/json/system/scada_symbols/stand-horizontal-tank.svg @@ -34,7 +34,7 @@ }, { "tag": "scale", - "stateRenderFunction": "if (!ctx.properties.scale) {\n element.hide();\n} else {\n var scaleSet = element.remember('scaleSet');\n if (!scaleSet) {\n element.remember('scaleSet', true);\n element.clear();\n \n var majorIntervals = ctx.properties.majorIntervals;\n var minorIntervals = ctx.properties.minorIntervals;\n \n var start = 17;\n var majorIntervalLength = 568 / majorIntervals;\n var minorIntervalLength = majorIntervalLength / minorIntervals;\n for (var i = 0; i < majorIntervals + 1; i++) {\n var y = start + i * majorIntervalLength;\n var line = ctx.svg.line(715, y, 747, y).stroke({ width: 3 }).attr({class: 'majorTick'});\n element.add(line);\n var majorText = (100 - i * (100/majorIntervals)).toFixed(0);\n var majorTickText = ctx.svg.text(majorText);\n majorTickText.attr({x: 705, y: y + 2, 'text-anchor': 'end', class: 'majorTickText'});\n majorTickText.first().attr({'dominant-baseline': 'middle'});\n element.add(majorTickText);\n if (i < majorIntervals) {\n drawMinorTicks(y, minorIntervals, minorIntervalLength);\n }\n }\n }\n \n var majorFont = ctx.properties.majorFont;\n var majorColor = ctx.properties.majorColor;\n var minorColor = ctx.properties.minorColor;\n if (ctx.values.critical) {\n majorColor = ctx.properties.majorCriticalColor;\n minorColor = ctx.properties.minorCriticalColor;\n } else if (ctx.values.warning) {\n majorColor = ctx.properties.minorWarningColor;\n minorColor = ctx.properties.minorWarningColor;\n }\n \n var majorTicks = element.find('line.majorTick');\n majorTicks.forEach(t => t.attr({stroke: majorColor}));\n \n var majorTicksText = element.find('text.majorTickText');\n ctx.api.font(majorTicksText, majorFont, majorColor);\n \n var minorTicks = element.find('line.minorTick');\n minorTicks.forEach(t => t.attr({stroke: minorColor}));\n \n var elementCriticalAnimation = element.remember('criticalAnimation');\n var criticalAnimation = ctx.values.critical && ctx.values.criticalAnimation;\n\n if (elementCriticalAnimation !== criticalAnimation) {\n element.remember('criticalAnimation', criticalAnimation);\n if (criticalAnimation) {\n ctx.api.cssAnimate(element, 500).attr({opacity: 0.15}).loop(0, true);\n } else {\n ctx.api.resetCssAnimation(element);\n }\n }\n}\n\nfunction drawMinorTicks(start, minorIntervals, minorIntervalLength) {\n for (var i = 1; i < minorIntervals; i++) {\n var minorY = start + i * minorIntervalLength;\n var minorLine = ctx.svg.line(727, minorY, 747, minorY).stroke({ width: 3 }).attr({class: 'minorTick'});\n element.add(minorLine);\n }\n}", + "stateRenderFunction": "if (!ctx.properties.scale) {\n element.hide();\n} else {\n var scaleSet = element.remember('scaleSet');\n if (!scaleSet) {\n element.remember('scaleSet', true);\n element.clear();\n \n var majorIntervals = ctx.properties.majorIntervals;\n var minorIntervals = ctx.properties.minorIntervals;\n \n var start = 17;\n var majorIntervalLength = 568 / majorIntervals;\n var minorIntervalLength = majorIntervalLength / minorIntervals;\n var tankCapacity = ctx.properties.scaleDisplayFormat ? 100 : (ctx.values.tankCapacity || 100);\n for (var i = 0; i < majorIntervals + 1; i++) {\n var y = start + i * majorIntervalLength;\n var line = ctx.svg.line(715, y, 747, y).stroke({ width: 3 }).attr({class: 'majorTick'});\n element.add(line);\n var majorText = ctx.api.formatValue((tankCapacity - i * (tankCapacity/majorIntervals)).toFixed(0), 0, ctx.properties.majorUnits, false);\n var majorTickText = ctx.svg.text(majorText);\n majorTickText.attr({x: 705, y: y + 2, 'text-anchor': 'end', class: 'majorTickText'});\n majorTickText.first().attr({'dominant-baseline': 'middle'});\n element.add(majorTickText);\n if (i < majorIntervals) {\n drawMinorTicks(y, minorIntervals, minorIntervalLength);\n }\n }\n }\n \n var majorFont = ctx.properties.majorFont;\n var majorColor = ctx.properties.majorColor;\n var minorColor = ctx.properties.minorColor;\n if (ctx.values.critical) {\n majorColor = ctx.properties.majorCriticalColor;\n minorColor = ctx.properties.minorCriticalColor;\n } else if (ctx.values.warning) {\n majorColor = ctx.properties.minorWarningColor;\n minorColor = ctx.properties.minorWarningColor;\n }\n \n var majorTicks = element.find('line.majorTick');\n majorTicks.forEach(t => t.attr({stroke: majorColor}));\n \n var majorTicksText = element.find('text.majorTickText');\n ctx.api.font(majorTicksText, majorFont, majorColor);\n \n var minorTicks = element.find('line.minorTick');\n minorTicks.forEach(t => t.attr({stroke: minorColor}));\n \n var elementCriticalAnimation = element.remember('criticalAnimation');\n var criticalAnimation = ctx.values.critical && ctx.values.criticalAnimation;\n\n if (elementCriticalAnimation !== criticalAnimation) {\n element.remember('criticalAnimation', criticalAnimation);\n if (criticalAnimation) {\n ctx.api.cssAnimate(element, 500).attr({opacity: 0.15}).loop(0, true);\n } else {\n ctx.api.resetCssAnimation(element);\n }\n }\n}\n\nfunction drawMinorTicks(start, minorIntervals, minorIntervalLength) {\n for (var i = 1; i < minorIntervals; i++) {\n var minorY = start + i * minorIntervalLength;\n var minorLine = ctx.svg.line(727, minorY, 747, minorY).stroke({ width: 3 }).attr({class: 'minorTick'});\n element.add(minorLine);\n }\n}", "actions": null }, { @@ -279,80 +279,43 @@ "name": "{i18n:scada.symbol.tank-color}", "type": "color", "default": "#E5E5E5", - "required": null, - "subLabel": null, - "divider": null, - "fieldSuffix": null, - "disableOnProperty": null, - "rowClass": "", - "fieldClass": "", - "min": null, - "max": null, - "step": null + "disabled": false, + "visible": true }, { "id": "fluidColor", "name": "{i18n:scada.symbol.fluid-color}", "type": "color", "default": "#1EC1F480", - "required": null, - "subLabel": null, - "divider": null, - "fieldSuffix": null, - "disableOnProperty": null, - "rowClass": "", - "fieldClass": "", - "min": null, - "max": null, - "step": null + "disabled": false, + "visible": true }, { "id": "valueBox", "name": "{i18n:scada.symbol.value-box}", "type": "switch", "default": true, - "required": null, - "subLabel": null, - "divider": null, - "fieldSuffix": null, - "disableOnProperty": null, - "rowClass": "", - "fieldClass": "", - "min": null, - "max": null, - "step": null + "disabled": false, + "visible": true }, { "id": "valueBoxColor", "name": "{i18n:scada.symbol.value-box}", "type": "color", "default": "#F3F3F3", - "required": null, - "subLabel": null, - "divider": null, - "fieldSuffix": null, "disableOnProperty": "valueBox", - "rowClass": "", - "fieldClass": "", - "min": null, - "max": null, - "step": null + "disabled": false, + "visible": true }, { "id": "valueUnits", "name": "{i18n:scada.symbol.value-text}", "type": "units", "default": "gal", - "required": null, "subLabel": "{i18n:scada.symbol.units}", - "divider": null, - "fieldSuffix": null, "disableOnProperty": "valueBox", - "rowClass": "", - "fieldClass": "", - "min": null, - "max": null, - "step": null + "disabled": false, + "visible": true }, { "id": "valueTextFont", @@ -365,80 +328,78 @@ "weight": "500", "style": "normal" }, - "required": null, - "subLabel": null, - "divider": null, - "fieldSuffix": null, "disableOnProperty": "valueBox", - "rowClass": "", - "fieldClass": "", - "min": null, - "max": null, - "step": null + "disabled": false, + "visible": true }, { "id": "valueTextColor", "name": "{i18n:scada.symbol.value-text}", "type": "color", "default": "#0000008A", - "required": null, - "subLabel": null, - "divider": null, - "fieldSuffix": null, "disableOnProperty": "valueBox", - "rowClass": "", - "fieldClass": "", - "min": null, - "max": null, - "step": null + "disabled": false, + "visible": true }, { "id": "scale", "name": "{i18n:scada.symbol.scale}", "type": "switch", "default": true, - "required": null, - "subLabel": null, - "divider": null, - "fieldSuffix": null, - "disableOnProperty": null, - "rowClass": "", - "fieldClass": "", - "min": null, - "max": null, - "step": null + "disabled": false, + "visible": true + }, + { + "id": "scaleDisplayFormat", + "name": "{i18n:scada.symbol.scale}", + "type": "select", + "default": true, + "subLabel": "{i18n:scada.symbol.display-format}", + "disableOnProperty": "scale", + "items": [ + { + "value": true, + "label": "Percentage" + }, + { + "value": false, + "label": "Metric" + } + ], + "disabled": false, + "visible": true }, { "id": "transparent", "name": "{i18n:scada.symbol.transparent-mode}", "type": "switch", "default": false, - "required": null, - "subLabel": null, - "divider": null, - "fieldSuffix": null, "disableOnProperty": "scale", - "rowClass": "", - "fieldClass": "", - "min": null, - "max": null, - "step": null + "disabled": false, + "visible": true }, { "id": "majorIntervals", "name": "{i18n:scada.symbol.major-ticks}", "type": "number", "default": 10, - "required": null, "subLabel": "{i18n:scada.symbol.intervals}", "divider": true, - "fieldSuffix": null, "disableOnProperty": "scale", - "rowClass": "", - "fieldClass": "", "min": 1, - "max": null, - "step": 1 + "step": 1, + "disabled": false, + "visible": true + }, + { + "id": "majorUnits", + "name": "{i18n:scada.symbol.major-ticks}", + "type": "units", + "subLabel": "{i18n:scada.symbol.units}", + "divider": true, + "disableOnProperty": "scale", + "disabled": false, + "visible": true }, { "id": "majorFont", @@ -451,128 +412,84 @@ "weight": "500", "style": "normal" }, - "required": null, - "subLabel": null, - "divider": null, - "fieldSuffix": null, "disableOnProperty": "scale", - "rowClass": "", - "fieldClass": "", - "min": null, - "max": null, - "step": null + "disabled": false, + "visible": true }, { "id": "majorColor", "name": "{i18n:scada.symbol.major-ticks-color}", "type": "color", "default": "#00000061", - "required": null, "subLabel": "{i18n:scada.symbol.normal}", "divider": true, - "fieldSuffix": null, "disableOnProperty": "scale", - "rowClass": "", - "fieldClass": "", - "min": null, - "max": null, - "step": null + "disabled": false, + "visible": true }, { "id": "majorWarningColor", "name": "{i18n:scada.symbol.major-ticks-color}", "type": "color", "default": "#FAA405", - "required": null, "subLabel": "{i18n:scada.symbol.warning}", "divider": true, - "fieldSuffix": null, "disableOnProperty": "scale", - "rowClass": "", - "fieldClass": "", - "min": null, - "max": null, - "step": null + "disabled": false, + "visible": true }, { "id": "majorCriticalColor", "name": "{i18n:scada.symbol.major-ticks-color}", "type": "color", "default": "#D12730", - "required": null, "subLabel": "{i18n:scada.symbol.critical}", - "divider": null, - "fieldSuffix": null, "disableOnProperty": "scale", - "rowClass": "", - "fieldClass": "", - "min": null, - "max": null, - "step": null + "disabled": false, + "visible": true }, { "id": "minorIntervals", "name": "{i18n:scada.symbol.minor-ticks}", "type": "number", "default": 5, - "required": null, "subLabel": "{i18n:scada.symbol.intervals}", - "divider": null, - "fieldSuffix": null, "disableOnProperty": "scale", - "rowClass": "", - "fieldClass": "", "min": 1, - "max": null, - "step": null + "disabled": false, + "visible": true }, { "id": "minorColor", "name": "{i18n:scada.symbol.minor-ticks-color}", "type": "color", "default": "#0000001F", - "required": null, "subLabel": "{i18n:scada.symbol.normal}", "divider": true, - "fieldSuffix": null, "disableOnProperty": "scale", - "rowClass": "", - "fieldClass": "", - "min": null, - "max": null, - "step": null + "disabled": false, + "visible": true }, { "id": "minorWarningColor", "name": "{i18n:scada.symbol.minor-ticks-color}", "type": "color", "default": "#FAA405", - "required": null, "subLabel": "{i18n:scada.symbol.warning}", "divider": true, - "fieldSuffix": null, "disableOnProperty": "scale", - "rowClass": "", - "fieldClass": "", - "min": null, - "max": null, - "step": null + "disabled": false, + "visible": true }, { "id": "minorCriticalColor", "name": "{i18n:scada.symbol.minor-ticks-color}", "type": "color", "default": "#D12730", - "required": null, "subLabel": "{i18n:scada.symbol.critical}", - "divider": null, - "fieldSuffix": null, "disableOnProperty": "scale", - "rowClass": "", - "fieldClass": "", - "min": null, - "max": null, - "step": null + "disabled": false, + "visible": true } ] }]]> diff --git a/application/src/main/data/json/system/scada_symbols/stand-vertical-short-tank.svg b/application/src/main/data/json/system/scada_symbols/stand-vertical-short-tank.svg index 43eac50d35..a92ba1e274 100644 --- a/application/src/main/data/json/system/scada_symbols/stand-vertical-short-tank.svg +++ b/application/src/main/data/json/system/scada_symbols/stand-vertical-short-tank.svg @@ -35,7 +35,7 @@ }, { "tag": "scale", - "stateRenderFunction": "if (!ctx.properties.scale) {\n element.hide();\n} else {\n var scaleSet = element.remember('scaleSet');\n if (!scaleSet) {\n element.remember('scaleSet', true);\n element.clear();\n \n var majorIntervals = ctx.properties.majorIntervals;\n var minorIntervals = ctx.properties.minorIntervals;\n \n var start = 137;\n var majorIntervalLength = 442 / majorIntervals;\n var minorIntervalLength = majorIntervalLength / minorIntervals;\n for (var i = 0; i < majorIntervals + 1; i++) {\n var y = start + i * majorIntervalLength;\n var line = ctx.svg.line(523, y, 555, y).stroke({ width: 3 }).attr({class: 'majorTick'});\n element.add(line);\n var majorText = (100 - i * (100/majorIntervals)).toFixed(0);\n var majorTickText = ctx.svg.text(majorText);\n majorTickText.attr({x: 513, y: y + 2, 'text-anchor': 'end', class: 'majorTickText'});\n majorTickText.first().attr({'dominant-baseline': 'middle'});\n element.add(majorTickText);\n if (i < majorIntervals) {\n drawMinorTicks(y, minorIntervals, minorIntervalLength);\n }\n }\n }\n \n var majorFont = ctx.properties.majorFont;\n var majorColor = ctx.properties.majorColor;\n var minorColor = ctx.properties.minorColor;\n if (ctx.values.critical) {\n majorColor = ctx.properties.majorCriticalColor;\n minorColor = ctx.properties.minorCriticalColor;\n } else if (ctx.values.warning) {\n majorColor = ctx.properties.minorWarningColor;\n minorColor = ctx.properties.minorWarningColor;\n }\n \n var majorTicks = element.find('line.majorTick');\n majorTicks.forEach(t => t.attr({stroke: majorColor}));\n \n var majorTicksText = element.find('text.majorTickText');\n ctx.api.font(majorTicksText, majorFont, majorColor);\n \n var minorTicks = element.find('line.minorTick');\n minorTicks.forEach(t => t.attr({stroke: minorColor}));\n \n var elementCriticalAnimation = element.remember('criticalAnimation');\n var criticalAnimation = ctx.values.critical && ctx.values.criticalAnimation;\n\n if (elementCriticalAnimation !== criticalAnimation) {\n element.remember('criticalAnimation', criticalAnimation);\n if (criticalAnimation) {\n ctx.api.cssAnimate(element, 500).attr({opacity: 0.15}).loop(0, true);\n } else {\n ctx.api.resetCssAnimation(element);\n }\n }\n}\n\nfunction drawMinorTicks(start, minorIntervals, minorIntervalLength) {\n for (var i = 1; i < minorIntervals; i++) {\n var minorY = start + i * minorIntervalLength;\n var minorLine = ctx.svg.line(535, minorY, 555, minorY).stroke({ width: 3 }).attr({class: 'minorTick'});\n element.add(minorLine);\n }\n}", + "stateRenderFunction": "if (!ctx.properties.scale) {\n element.hide();\n} else {\n var scaleSet = element.remember('scaleSet');\n if (!scaleSet) {\n element.remember('scaleSet', true);\n element.clear();\n \n var majorIntervals = ctx.properties.majorIntervals;\n var minorIntervals = ctx.properties.minorIntervals;\n \n var start = 137;\n var majorIntervalLength = 442 / majorIntervals;\n var minorIntervalLength = majorIntervalLength / minorIntervals;\n var tankCapacity = ctx.properties.scaleDisplayFormat ? 100 : (ctx.values.tankCapacity || 100);\n for (var i = 0; i < majorIntervals + 1; i++) {\n var y = start + i * majorIntervalLength;\n var line = ctx.svg.line(523, y, 555, y).stroke({ width: 3 }).attr({class: 'majorTick'});\n element.add(line);\n var majorText = ctx.api.formatValue((tankCapacity - i * (tankCapacity/majorIntervals)).toFixed(0), 0, ctx.properties.majorUnits, false);\n var majorTickText = ctx.svg.text(majorText);\n majorTickText.attr({x: 513, y: y + 2, 'text-anchor': 'end', class: 'majorTickText'});\n majorTickText.first().attr({'dominant-baseline': 'middle'});\n element.add(majorTickText);\n if (i < majorIntervals) {\n drawMinorTicks(y, minorIntervals, minorIntervalLength);\n }\n }\n }\n \n var majorFont = ctx.properties.majorFont;\n var majorColor = ctx.properties.majorColor;\n var minorColor = ctx.properties.minorColor;\n if (ctx.values.critical) {\n majorColor = ctx.properties.majorCriticalColor;\n minorColor = ctx.properties.minorCriticalColor;\n } else if (ctx.values.warning) {\n majorColor = ctx.properties.minorWarningColor;\n minorColor = ctx.properties.minorWarningColor;\n }\n \n var majorTicks = element.find('line.majorTick');\n majorTicks.forEach(t => t.attr({stroke: majorColor}));\n \n var majorTicksText = element.find('text.majorTickText');\n ctx.api.font(majorTicksText, majorFont, majorColor);\n \n var minorTicks = element.find('line.minorTick');\n minorTicks.forEach(t => t.attr({stroke: minorColor}));\n \n var elementCriticalAnimation = element.remember('criticalAnimation');\n var criticalAnimation = ctx.values.critical && ctx.values.criticalAnimation;\n\n if (elementCriticalAnimation !== criticalAnimation) {\n element.remember('criticalAnimation', criticalAnimation);\n if (criticalAnimation) {\n ctx.api.cssAnimate(element, 500).attr({opacity: 0.15}).loop(0, true);\n } else {\n ctx.api.resetCssAnimation(element);\n }\n }\n}\n\nfunction drawMinorTicks(start, minorIntervals, minorIntervalLength) {\n for (var i = 1; i < minorIntervals; i++) {\n var minorY = start + i * minorIntervalLength;\n var minorLine = ctx.svg.line(535, minorY, 555, minorY).stroke({ width: 3 }).attr({class: 'minorTick'});\n element.add(minorLine);\n }\n}", "actions": null }, { @@ -280,80 +280,43 @@ "name": "{i18n:scada.symbol.tank-color}", "type": "color", "default": "#E5E5E5", - "required": null, - "subLabel": null, - "divider": null, - "fieldSuffix": null, - "disableOnProperty": null, - "rowClass": "", - "fieldClass": "", - "min": null, - "max": null, - "step": null + "disabled": false, + "visible": true }, { "id": "fluidColor", "name": "{i18n:scada.symbol.fluid-color}", "type": "color", "default": "#1EC1F480", - "required": null, - "subLabel": null, - "divider": null, - "fieldSuffix": null, - "disableOnProperty": null, - "rowClass": "", - "fieldClass": "", - "min": null, - "max": null, - "step": null + "disabled": false, + "visible": true }, { "id": "valueBox", "name": "{i18n:scada.symbol.value-box}", "type": "switch", "default": true, - "required": null, - "subLabel": null, - "divider": null, - "fieldSuffix": null, - "disableOnProperty": null, - "rowClass": "", - "fieldClass": "", - "min": null, - "max": null, - "step": null + "disabled": false, + "visible": true }, { "id": "valueBoxColor", "name": "{i18n:scada.symbol.value-box}", "type": "color", "default": "#F3F3F3", - "required": null, - "subLabel": null, - "divider": null, - "fieldSuffix": null, "disableOnProperty": "valueBox", - "rowClass": "", - "fieldClass": "", - "min": null, - "max": null, - "step": null + "disabled": false, + "visible": true }, { "id": "valueUnits", "name": "{i18n:scada.symbol.value-text}", "type": "units", "default": "gal", - "required": null, "subLabel": "{i18n:scada.symbol.units}", - "divider": null, - "fieldSuffix": null, "disableOnProperty": "valueBox", - "rowClass": "", - "fieldClass": "", - "min": null, - "max": null, - "step": null + "disabled": false, + "visible": true }, { "id": "valueTextFont", @@ -366,80 +329,78 @@ "weight": "500", "style": "normal" }, - "required": null, - "subLabel": null, - "divider": null, - "fieldSuffix": null, "disableOnProperty": "valueBox", - "rowClass": "", - "fieldClass": "", - "min": null, - "max": null, - "step": null + "disabled": false, + "visible": true }, { "id": "valueTextColor", "name": "{i18n:scada.symbol.value-text}", "type": "color", "default": "#0000008A", - "required": null, - "subLabel": null, - "divider": null, - "fieldSuffix": null, "disableOnProperty": "valueBox", - "rowClass": "", - "fieldClass": "", - "min": null, - "max": null, - "step": null + "disabled": false, + "visible": true }, { "id": "scale", "name": "{i18n:scada.symbol.scale}", "type": "switch", "default": true, - "required": null, - "subLabel": null, - "divider": null, - "fieldSuffix": null, - "disableOnProperty": null, - "rowClass": "", - "fieldClass": "", - "min": null, - "max": null, - "step": null + "disabled": false, + "visible": true + }, + { + "id": "scaleDisplayFormat", + "name": "{i18n:scada.symbol.scale}", + "type": "select", + "default": true, + "subLabel": "{i18n:scada.symbol.display-format}", + "disableOnProperty": "scale", + "items": [ + { + "value": true, + "label": "Percentage" + }, + { + "value": false, + "label": "Metric" + } + ], + "disabled": false, + "visible": true }, { "id": "transparent", "name": "{i18n:scada.symbol.transparent-mode}", "type": "switch", "default": false, - "required": null, - "subLabel": null, - "divider": null, - "fieldSuffix": null, "disableOnProperty": "scale", - "rowClass": "", - "fieldClass": "", - "min": null, - "max": null, - "step": null + "disabled": false, + "visible": true }, { "id": "majorIntervals", "name": "{i18n:scada.symbol.major-ticks}", "type": "number", "default": 5, - "required": null, "subLabel": "{i18n:scada.symbol.intervals}", "divider": true, - "fieldSuffix": null, "disableOnProperty": "scale", - "rowClass": "", - "fieldClass": "", "min": 1, - "max": null, - "step": 1 + "step": 1, + "disabled": false, + "visible": true + }, + { + "id": "majorUnits", + "name": "{i18n:scada.symbol.major-ticks}", + "type": "units", + "subLabel": "{i18n:scada.symbol.units}", + "divider": true, + "disableOnProperty": "scale", + "disabled": false, + "visible": true }, { "id": "majorFont", @@ -452,128 +413,84 @@ "weight": "500", "style": "normal" }, - "required": null, - "subLabel": null, - "divider": null, - "fieldSuffix": null, "disableOnProperty": "scale", - "rowClass": "", - "fieldClass": "", - "min": null, - "max": null, - "step": null + "disabled": false, + "visible": true }, { "id": "majorColor", "name": "{i18n:scada.symbol.major-ticks-color}", "type": "color", "default": "#00000061", - "required": null, "subLabel": "{i18n:scada.symbol.normal}", "divider": true, - "fieldSuffix": null, "disableOnProperty": "scale", - "rowClass": "", - "fieldClass": "", - "min": null, - "max": null, - "step": null + "disabled": false, + "visible": true }, { "id": "majorWarningColor", "name": "{i18n:scada.symbol.major-ticks-color}", "type": "color", "default": "#FAA405", - "required": null, "subLabel": "{i18n:scada.symbol.warning}", "divider": true, - "fieldSuffix": null, "disableOnProperty": "scale", - "rowClass": "", - "fieldClass": "", - "min": null, - "max": null, - "step": null + "disabled": false, + "visible": true }, { "id": "majorCriticalColor", "name": "{i18n:scada.symbol.major-ticks-color}", "type": "color", "default": "#D12730", - "required": null, "subLabel": "{i18n:scada.symbol.critical}", - "divider": null, - "fieldSuffix": null, "disableOnProperty": "scale", - "rowClass": "", - "fieldClass": "", - "min": null, - "max": null, - "step": null + "disabled": false, + "visible": true }, { "id": "minorIntervals", "name": "{i18n:scada.symbol.minor-ticks}", "type": "number", "default": 5, - "required": null, "subLabel": "{i18n:scada.symbol.intervals}", - "divider": null, - "fieldSuffix": null, "disableOnProperty": "scale", - "rowClass": "", - "fieldClass": "", "min": 1, - "max": null, - "step": null + "disabled": false, + "visible": true }, { "id": "minorColor", "name": "{i18n:scada.symbol.minor-ticks-color}", "type": "color", "default": "#0000001F", - "required": null, "subLabel": "{i18n:scada.symbol.normal}", "divider": true, - "fieldSuffix": null, "disableOnProperty": "scale", - "rowClass": "", - "fieldClass": "", - "min": null, - "max": null, - "step": null + "disabled": false, + "visible": true }, { "id": "minorWarningColor", "name": "{i18n:scada.symbol.minor-ticks-color}", "type": "color", "default": "#FAA405", - "required": null, "subLabel": "{i18n:scada.symbol.warning}", "divider": true, - "fieldSuffix": null, "disableOnProperty": "scale", - "rowClass": "", - "fieldClass": "", - "min": null, - "max": null, - "step": null + "disabled": false, + "visible": true }, { "id": "minorCriticalColor", "name": "{i18n:scada.symbol.minor-ticks-color}", "type": "color", "default": "#D12730", - "required": null, "subLabel": "{i18n:scada.symbol.critical}", - "divider": null, - "fieldSuffix": null, "disableOnProperty": "scale", - "rowClass": "", - "fieldClass": "", - "min": null, - "max": null, - "step": null + "disabled": false, + "visible": true } ] }]]> diff --git a/application/src/main/data/json/system/scada_symbols/stand-vertical-tank.svg b/application/src/main/data/json/system/scada_symbols/stand-vertical-tank.svg index 07c3ea050c..1b19157341 100644 --- a/application/src/main/data/json/system/scada_symbols/stand-vertical-tank.svg +++ b/application/src/main/data/json/system/scada_symbols/stand-vertical-tank.svg @@ -34,7 +34,7 @@ }, { "tag": "scale", - "stateRenderFunction": "if (!ctx.properties.scale) {\n element.hide();\n} else {\n var scaleSet = element.remember('scaleSet');\n if (!scaleSet) {\n element.remember('scaleSet', true);\n element.clear();\n \n var majorIntervals = ctx.properties.majorIntervals;\n var minorIntervals = ctx.properties.minorIntervals;\n \n var start = 205;\n var majorIntervalLength = 760 / majorIntervals;\n var minorIntervalLength = majorIntervalLength / minorIntervals;\n for (var i = 0; i < majorIntervals + 1; i++) {\n var y = start + i * majorIntervalLength;\n var line = ctx.svg.line(340, y, 372, y).stroke({ width: 3 }).attr({class: 'majorTick'});\n element.add(line);\n var majorText = (100 - i * (100/majorIntervals)).toFixed(0);\n var majorTickText = ctx.svg.text(majorText);\n majorTickText.attr({x: 330, y: y + 2, 'text-anchor': 'end', class: 'majorTickText'});\n majorTickText.first().attr({'dominant-baseline': 'middle'});\n element.add(majorTickText);\n if (i < majorIntervals) {\n drawMinorTicks(y, minorIntervals, minorIntervalLength);\n }\n }\n }\n \n var majorFont = ctx.properties.majorFont;\n var majorColor = ctx.properties.majorColor;\n var minorColor = ctx.properties.minorColor;\n if (ctx.values.critical) {\n majorColor = ctx.properties.majorCriticalColor;\n minorColor = ctx.properties.minorCriticalColor;\n } else if (ctx.values.warning) {\n majorColor = ctx.properties.minorWarningColor;\n minorColor = ctx.properties.minorWarningColor;\n }\n \n var majorTicks = element.find('line.majorTick');\n majorTicks.forEach(t => t.attr({stroke: majorColor}));\n \n var majorTicksText = element.find('text.majorTickText');\n ctx.api.font(majorTicksText, majorFont, majorColor);\n \n var minorTicks = element.find('line.minorTick');\n minorTicks.forEach(t => t.attr({stroke: minorColor}));\n \n var elementCriticalAnimation = element.remember('criticalAnimation');\n var criticalAnimation = ctx.values.critical && ctx.values.criticalAnimation;\n\n if (elementCriticalAnimation !== criticalAnimation) {\n element.remember('criticalAnimation', criticalAnimation);\n if (criticalAnimation) {\n ctx.api.cssAnimate(element, 500).attr({opacity: 0.15}).loop(0, true);\n } else {\n ctx.api.resetCssAnimation(element);\n }\n }\n}\n\nfunction drawMinorTicks(start, minorIntervals, minorIntervalLength) {\n for (var i = 1; i < minorIntervals; i++) {\n var minorY = start + i * minorIntervalLength;\n var minorLine = ctx.svg.line(352, minorY, 372, minorY).stroke({ width: 3 }).attr({class: 'minorTick'});\n element.add(minorLine);\n }\n}", + "stateRenderFunction": "if (!ctx.properties.scale) {\n element.hide();\n} else {\n var scaleSet = element.remember('scaleSet');\n if (!scaleSet) {\n element.remember('scaleSet', true);\n element.clear();\n \n var majorIntervals = ctx.properties.majorIntervals;\n var minorIntervals = ctx.properties.minorIntervals;\n \n var start = 205;\n var majorIntervalLength = 760 / majorIntervals;\n var minorIntervalLength = majorIntervalLength / minorIntervals;\n var tankCapacity = ctx.properties.scaleDisplayFormat ? 100 : (ctx.values.tankCapacity || 100);\n for (var i = 0; i < majorIntervals + 1; i++) {\n var y = start + i * majorIntervalLength;\n var line = ctx.svg.line(340, y, 372, y).stroke({ width: 3 }).attr({class: 'majorTick'});\n element.add(line);\n var majorText = ctx.api.formatValue((tankCapacity - i * (tankCapacity/majorIntervals)).toFixed(0), 0, ctx.properties.majorUnits, false);\n var majorTickText = ctx.svg.text(majorText);\n majorTickText.attr({x: 330, y: y + 2, 'text-anchor': 'end', class: 'majorTickText'});\n majorTickText.first().attr({'dominant-baseline': 'middle'});\n element.add(majorTickText);\n if (i < majorIntervals) {\n drawMinorTicks(y, minorIntervals, minorIntervalLength);\n }\n }\n }\n \n var majorFont = ctx.properties.majorFont;\n var majorColor = ctx.properties.majorColor;\n var minorColor = ctx.properties.minorColor;\n if (ctx.values.critical) {\n majorColor = ctx.properties.majorCriticalColor;\n minorColor = ctx.properties.minorCriticalColor;\n } else if (ctx.values.warning) {\n majorColor = ctx.properties.minorWarningColor;\n minorColor = ctx.properties.minorWarningColor;\n }\n \n var majorTicks = element.find('line.majorTick');\n majorTicks.forEach(t => t.attr({stroke: majorColor}));\n \n var majorTicksText = element.find('text.majorTickText');\n ctx.api.font(majorTicksText, majorFont, majorColor);\n \n var minorTicks = element.find('line.minorTick');\n minorTicks.forEach(t => t.attr({stroke: minorColor}));\n \n var elementCriticalAnimation = element.remember('criticalAnimation');\n var criticalAnimation = ctx.values.critical && ctx.values.criticalAnimation;\n\n if (elementCriticalAnimation !== criticalAnimation) {\n element.remember('criticalAnimation', criticalAnimation);\n if (criticalAnimation) {\n ctx.api.cssAnimate(element, 500).attr({opacity: 0.15}).loop(0, true);\n } else {\n ctx.api.resetCssAnimation(element);\n }\n }\n}\n\nfunction drawMinorTicks(start, minorIntervals, minorIntervalLength) {\n for (var i = 1; i < minorIntervals; i++) {\n var minorY = start + i * minorIntervalLength;\n var minorLine = ctx.svg.line(352, minorY, 372, minorY).stroke({ width: 3 }).attr({class: 'minorTick'});\n element.add(minorLine);\n }\n}", "actions": null }, { @@ -279,80 +279,43 @@ "name": "{i18n:scada.symbol.tank-color}", "type": "color", "default": "#E5E5E5", - "required": null, - "subLabel": null, - "divider": null, - "fieldSuffix": null, - "disableOnProperty": null, - "rowClass": "", - "fieldClass": "", - "min": null, - "max": null, - "step": null + "disabled": false, + "visible": true }, { "id": "fluidColor", "name": "{i18n:scada.symbol.fluid-color}", "type": "color", "default": "#1EC1F480", - "required": null, - "subLabel": null, - "divider": null, - "fieldSuffix": null, - "disableOnProperty": null, - "rowClass": "", - "fieldClass": "", - "min": null, - "max": null, - "step": null + "disabled": false, + "visible": true }, { "id": "valueBox", "name": "{i18n:scada.symbol.value-box}", "type": "switch", "default": true, - "required": null, - "subLabel": null, - "divider": null, - "fieldSuffix": null, - "disableOnProperty": null, - "rowClass": "", - "fieldClass": "", - "min": null, - "max": null, - "step": null + "disabled": false, + "visible": true }, { "id": "valueBoxColor", "name": "{i18n:scada.symbol.value-box}", "type": "color", "default": "#F3F3F3", - "required": null, - "subLabel": null, - "divider": null, - "fieldSuffix": null, "disableOnProperty": "valueBox", - "rowClass": "", - "fieldClass": "", - "min": null, - "max": null, - "step": null + "disabled": false, + "visible": true }, { "id": "valueUnits", "name": "{i18n:scada.symbol.value-text}", "type": "units", "default": "gal", - "required": null, "subLabel": "{i18n:scada.symbol.units}", - "divider": null, - "fieldSuffix": null, "disableOnProperty": "valueBox", - "rowClass": "", - "fieldClass": "", - "min": null, - "max": null, - "step": null + "disabled": false, + "visible": true }, { "id": "valueTextFont", @@ -365,80 +328,76 @@ "weight": "500", "style": "normal" }, - "required": null, - "subLabel": null, - "divider": null, - "fieldSuffix": null, "disableOnProperty": "valueBox", - "rowClass": "", - "fieldClass": "", - "min": null, - "max": null, - "step": null + "disabled": false, + "visible": true }, { "id": "valueTextColor", "name": "{i18n:scada.symbol.value-text}", "type": "color", "default": "#0000008A", - "required": null, - "subLabel": null, - "divider": null, - "fieldSuffix": null, "disableOnProperty": "valueBox", - "rowClass": "", - "fieldClass": "", - "min": null, - "max": null, - "step": null + "disabled": false, + "visible": true }, { "id": "scale", "name": "{i18n:scada.symbol.scale}", "type": "switch", "default": true, - "required": null, - "subLabel": null, - "divider": null, - "fieldSuffix": null, - "disableOnProperty": null, - "rowClass": "", - "fieldClass": "", - "min": null, - "max": null, - "step": null + "disabled": false, + "visible": true + }, + { + "id": "scaleDisplayFormat", + "name": "{i18n:scada.symbol.scale}", + "type": "select", + "default": true, + "subLabel": "{i18n:scada.symbol.display-format}", + "disableOnProperty": "scale", + "items": [ + { + "value": true, + "label": "Percentage" + }, + { + "value": false, + "label": "Metric" + } + ] }, { "id": "transparent", "name": "{i18n:scada.symbol.transparent-mode}", "type": "switch", "default": false, - "required": null, - "subLabel": null, - "divider": null, - "fieldSuffix": null, "disableOnProperty": "scale", - "rowClass": "", - "fieldClass": "", - "min": null, - "max": null, - "step": null + "disabled": false, + "visible": true }, { "id": "majorIntervals", "name": "{i18n:scada.symbol.major-ticks}", "type": "number", "default": 10, - "required": null, "subLabel": "{i18n:scada.symbol.intervals}", "divider": true, - "fieldSuffix": null, "disableOnProperty": "scale", - "rowClass": "", - "fieldClass": "", "min": 1, - "max": null, - "step": 1 + "step": 1, + "disabled": false, + "visible": true + }, + { + "id": "majorUnits", + "name": "{i18n:scada.symbol.major-ticks}", + "type": "units", + "subLabel": "{i18n:scada.symbol.units}", + "divider": true, + "disableOnProperty": "scale", + "disabled": false, + "visible": true }, { "id": "majorFont", @@ -451,128 +410,84 @@ "weight": "500", "style": "normal" }, - "required": null, - "subLabel": null, - "divider": null, - "fieldSuffix": null, "disableOnProperty": "scale", - "rowClass": "", - "fieldClass": "", - "min": null, - "max": null, - "step": null + "disabled": false, + "visible": true }, { "id": "majorColor", "name": "{i18n:scada.symbol.major-ticks-color}", "type": "color", "default": "#00000061", - "required": null, "subLabel": "{i18n:scada.symbol.normal}", "divider": true, - "fieldSuffix": null, "disableOnProperty": "scale", - "rowClass": "", - "fieldClass": "", - "min": null, - "max": null, - "step": null + "disabled": false, + "visible": true }, { "id": "majorWarningColor", "name": "{i18n:scada.symbol.major-ticks-color}", "type": "color", "default": "#FAA405", - "required": null, "subLabel": "{i18n:scada.symbol.warning}", "divider": true, - "fieldSuffix": null, "disableOnProperty": "scale", - "rowClass": "", - "fieldClass": "", - "min": null, - "max": null, - "step": null + "disabled": false, + "visible": true }, { "id": "majorCriticalColor", "name": "{i18n:scada.symbol.major-ticks-color}", "type": "color", "default": "#D12730", - "required": null, "subLabel": "{i18n:scada.symbol.critical}", - "divider": null, - "fieldSuffix": null, "disableOnProperty": "scale", - "rowClass": "", - "fieldClass": "", - "min": null, - "max": null, - "step": null + "disabled": false, + "visible": true }, { "id": "minorIntervals", "name": "{i18n:scada.symbol.minor-ticks}", "type": "number", "default": 5, - "required": null, "subLabel": "{i18n:scada.symbol.intervals}", - "divider": null, - "fieldSuffix": null, "disableOnProperty": "scale", - "rowClass": "", - "fieldClass": "", "min": 1, - "max": null, - "step": null + "disabled": false, + "visible": true }, { "id": "minorColor", "name": "{i18n:scada.symbol.minor-ticks-color}", "type": "color", "default": "#0000001F", - "required": null, "subLabel": "{i18n:scada.symbol.normal}", "divider": true, - "fieldSuffix": null, "disableOnProperty": "scale", - "rowClass": "", - "fieldClass": "", - "min": null, - "max": null, - "step": null + "disabled": false, + "visible": true }, { "id": "minorWarningColor", "name": "{i18n:scada.symbol.minor-ticks-color}", "type": "color", "default": "#FAA405", - "required": null, "subLabel": "{i18n:scada.symbol.warning}", "divider": true, - "fieldSuffix": null, "disableOnProperty": "scale", - "rowClass": "", - "fieldClass": "", - "min": null, - "max": null, - "step": null + "disabled": false, + "visible": true }, { "id": "minorCriticalColor", "name": "{i18n:scada.symbol.minor-ticks-color}", "type": "color", "default": "#D12730", - "required": null, "subLabel": "{i18n:scada.symbol.critical}", - "divider": null, - "fieldSuffix": null, "disableOnProperty": "scale", - "rowClass": "", - "fieldClass": "", - "min": null, - "max": null, - "step": null + "disabled": false, + "visible": true } ] }]]> diff --git a/application/src/main/data/json/system/scada_symbols/vertical-short-tank.svg b/application/src/main/data/json/system/scada_symbols/vertical-short-tank.svg index d4358e8fa7..23fe4a2733 100644 --- a/application/src/main/data/json/system/scada_symbols/vertical-short-tank.svg +++ b/application/src/main/data/json/system/scada_symbols/vertical-short-tank.svg @@ -34,7 +34,7 @@ }, { "tag": "scale", - "stateRenderFunction": "if (!ctx.properties.scale) {\n element.hide();\n} else {\n var scaleSet = element.remember('scaleSet');\n if (!scaleSet) {\n element.remember('scaleSet', true);\n element.clear();\n \n var majorIntervals = ctx.properties.majorIntervals;\n var minorIntervals = ctx.properties.minorIntervals;\n \n var start = 137;\n var majorIntervalLength = 442 / majorIntervals;\n var minorIntervalLength = majorIntervalLength / minorIntervals;\n for (var i = 0; i < majorIntervals + 1; i++) {\n var y = start + i * majorIntervalLength;\n var line = ctx.svg.line(523, y, 555, y).stroke({ width: 3 }).attr({class: 'majorTick'});\n element.add(line);\n var majorText = (100 - i * (100/majorIntervals)).toFixed(0);\n var majorTickText = ctx.svg.text(majorText);\n majorTickText.attr({x: 513, y: y + 2, 'text-anchor': 'end', class: 'majorTickText'});\n majorTickText.first().attr({'dominant-baseline': 'middle'});\n element.add(majorTickText);\n if (i < majorIntervals) {\n drawMinorTicks(y, minorIntervals, minorIntervalLength);\n }\n }\n }\n \n var majorFont = ctx.properties.majorFont;\n var majorColor = ctx.properties.majorColor;\n var minorColor = ctx.properties.minorColor;\n if (ctx.values.critical) {\n majorColor = ctx.properties.majorCriticalColor;\n minorColor = ctx.properties.minorCriticalColor;\n } else if (ctx.values.warning) {\n majorColor = ctx.properties.minorWarningColor;\n minorColor = ctx.properties.minorWarningColor;\n }\n \n var majorTicks = element.find('line.majorTick');\n majorTicks.forEach(t => t.attr({stroke: majorColor}));\n \n var majorTicksText = element.find('text.majorTickText');\n ctx.api.font(majorTicksText, majorFont, majorColor);\n \n var minorTicks = element.find('line.minorTick');\n minorTicks.forEach(t => t.attr({stroke: minorColor}));\n \n var elementCriticalAnimation = element.remember('criticalAnimation');\n var criticalAnimation = ctx.values.critical && ctx.values.criticalAnimation;\n\n if (elementCriticalAnimation !== criticalAnimation) {\n element.remember('criticalAnimation', criticalAnimation);\n if (criticalAnimation) {\n ctx.api.cssAnimate(element, 500).attr({opacity: 0.15}).loop(0, true);\n } else {\n ctx.api.resetCssAnimation(element);\n }\n }\n}\n\nfunction drawMinorTicks(start, minorIntervals, minorIntervalLength) {\n for (var i = 1; i < minorIntervals; i++) {\n var minorY = start + i * minorIntervalLength;\n var minorLine = ctx.svg.line(535, minorY, 555, minorY).stroke({ width: 3 }).attr({class: 'minorTick'});\n element.add(minorLine);\n }\n}", + "stateRenderFunction": "if (!ctx.properties.scale) {\n element.hide();\n} else {\n var scaleSet = element.remember('scaleSet');\n if (!scaleSet) {\n element.remember('scaleSet', true);\n element.clear();\n \n var majorIntervals = ctx.properties.majorIntervals;\n var minorIntervals = ctx.properties.minorIntervals;\n \n var start = 137;\n var majorIntervalLength = 442 / majorIntervals;\n var minorIntervalLength = majorIntervalLength / minorIntervals;\n var tankCapacity = ctx.properties.scaleDisplayFormat ? 100 : (ctx.values.tankCapacity || 100);\n for (var i = 0; i < majorIntervals + 1; i++) {\n var y = start + i * majorIntervalLength;\n var line = ctx.svg.line(523, y, 555, y).stroke({ width: 3 }).attr({class: 'majorTick'});\n element.add(line);\n var majorText = ctx.api.formatValue((tankCapacity - i * (tankCapacity/majorIntervals)).toFixed(0), 0, ctx.properties.majorUnits, false);\n var majorTickText = ctx.svg.text(majorText);\n majorTickText.attr({x: 513, y: y + 2, 'text-anchor': 'end', class: 'majorTickText'});\n majorTickText.first().attr({'dominant-baseline': 'middle'});\n element.add(majorTickText);\n if (i < majorIntervals) {\n drawMinorTicks(y, minorIntervals, minorIntervalLength);\n }\n }\n }\n \n var majorFont = ctx.properties.majorFont;\n var majorColor = ctx.properties.majorColor;\n var minorColor = ctx.properties.minorColor;\n if (ctx.values.critical) {\n majorColor = ctx.properties.majorCriticalColor;\n minorColor = ctx.properties.minorCriticalColor;\n } else if (ctx.values.warning) {\n majorColor = ctx.properties.minorWarningColor;\n minorColor = ctx.properties.minorWarningColor;\n }\n \n var majorTicks = element.find('line.majorTick');\n majorTicks.forEach(t => t.attr({stroke: majorColor}));\n \n var majorTicksText = element.find('text.majorTickText');\n ctx.api.font(majorTicksText, majorFont, majorColor);\n \n var minorTicks = element.find('line.minorTick');\n minorTicks.forEach(t => t.attr({stroke: minorColor}));\n \n var elementCriticalAnimation = element.remember('criticalAnimation');\n var criticalAnimation = ctx.values.critical && ctx.values.criticalAnimation;\n\n if (elementCriticalAnimation !== criticalAnimation) {\n element.remember('criticalAnimation', criticalAnimation);\n if (criticalAnimation) {\n ctx.api.cssAnimate(element, 500).attr({opacity: 0.15}).loop(0, true);\n } else {\n ctx.api.resetCssAnimation(element);\n }\n }\n}\n\nfunction drawMinorTicks(start, minorIntervals, minorIntervalLength) {\n for (var i = 1; i < minorIntervals; i++) {\n var minorY = start + i * minorIntervalLength;\n var minorLine = ctx.svg.line(535, minorY, 555, minorY).stroke({ width: 3 }).attr({class: 'minorTick'});\n element.add(minorLine);\n }\n}", "actions": null }, { @@ -279,80 +279,43 @@ "name": "{i18n:scada.symbol.tank-color}", "type": "color", "default": "#E5E5E5", - "required": null, - "subLabel": null, - "divider": null, - "fieldSuffix": null, - "disableOnProperty": null, - "rowClass": "", - "fieldClass": "", - "min": null, - "max": null, - "step": null + "disabled": false, + "visible": true }, { "id": "fluidColor", "name": "{i18n:scada.symbol.fluid-color}", "type": "color", "default": "#1EC1F480", - "required": null, - "subLabel": null, - "divider": null, - "fieldSuffix": null, - "disableOnProperty": null, - "rowClass": "", - "fieldClass": "", - "min": null, - "max": null, - "step": null + "disabled": false, + "visible": true }, { "id": "valueBox", "name": "{i18n:scada.symbol.value-box}", "type": "switch", "default": true, - "required": null, - "subLabel": null, - "divider": null, - "fieldSuffix": null, - "disableOnProperty": null, - "rowClass": "", - "fieldClass": "", - "min": null, - "max": null, - "step": null + "disabled": false, + "visible": true }, { "id": "valueBoxColor", "name": "{i18n:scada.symbol.value-box}", "type": "color", "default": "#F3F3F3", - "required": null, - "subLabel": null, - "divider": null, - "fieldSuffix": null, "disableOnProperty": "valueBox", - "rowClass": "", - "fieldClass": "", - "min": null, - "max": null, - "step": null + "disabled": false, + "visible": true }, { "id": "valueUnits", "name": "{i18n:scada.symbol.value-text}", "type": "units", "default": "gal", - "required": null, "subLabel": "{i18n:scada.symbol.units}", - "divider": null, - "fieldSuffix": null, "disableOnProperty": "valueBox", - "rowClass": "", - "fieldClass": "", - "min": null, - "max": null, - "step": null + "disabled": false, + "visible": true }, { "id": "valueTextFont", @@ -365,80 +328,79 @@ "weight": "500", "style": "normal" }, - "required": null, - "subLabel": null, - "divider": null, - "fieldSuffix": null, "disableOnProperty": "valueBox", - "rowClass": "", - "fieldClass": "", - "min": null, - "max": null, - "step": null + "disabled": false, + "visible": true }, { "id": "valueTextColor", "name": "{i18n:scada.symbol.value-text}", "type": "color", "default": "#0000008A", - "required": null, - "subLabel": null, - "divider": null, - "fieldSuffix": null, "disableOnProperty": "valueBox", - "rowClass": "", - "fieldClass": "", - "min": null, - "max": null, - "step": null + "disabled": false, + "visible": true }, { "id": "scale", "name": "{i18n:scada.symbol.scale}", "type": "switch", "default": true, - "required": null, - "subLabel": null, - "divider": null, - "fieldSuffix": null, - "disableOnProperty": null, - "rowClass": "", - "fieldClass": "", - "min": null, - "max": null, - "step": null + "disabled": false, + "visible": true + }, + { + "id": "scaleDisplayFormat", + "name": "{i18n:scada.symbol.scale}", + "type": "select", + "default": true, + "subLabel": "{i18n:scada.symbol.display-format}", + "disableOnProperty": "scale", + "items": [ + { + "value": true, + "label": "Percentage" + }, + { + "value": false, + "label": "Metric" + } + ], + "disabled": false, + "visible": true }, { "id": "transparent", "name": "{i18n:scada.symbol.transparent-mode}", "type": "switch", "default": false, - "required": null, - "subLabel": null, - "divider": null, - "fieldSuffix": null, "disableOnProperty": "scale", - "rowClass": "", - "fieldClass": "", - "min": null, - "max": null, - "step": null + "disabled": false, + "visible": true }, { "id": "majorIntervals", "name": "{i18n:scada.symbol.major-ticks}", "type": "number", "default": 5, - "required": null, "subLabel": "{i18n:scada.symbol.intervals}", "divider": true, - "fieldSuffix": null, "disableOnProperty": "scale", - "rowClass": "", - "fieldClass": "", "min": 1, - "max": null, - "step": 1 + "step": 1, + "disabled": false, + "visible": true + }, + { + "id": "majorUnits", + "name": "{i18n:scada.symbol.major-ticks}", + "type": "units", + "required": false, + "subLabel": "{i18n:scada.symbol.units}", + "divider": true, + "disableOnProperty": "scale", + "disabled": false, + "visible": true }, { "id": "majorFont", @@ -451,128 +413,84 @@ "weight": "500", "style": "normal" }, - "required": null, - "subLabel": null, - "divider": null, - "fieldSuffix": null, "disableOnProperty": "scale", - "rowClass": "", - "fieldClass": "", - "min": null, - "max": null, - "step": null + "disabled": false, + "visible": true }, { "id": "majorColor", "name": "{i18n:scada.symbol.major-ticks-color}", "type": "color", "default": "#00000061", - "required": null, "subLabel": "{i18n:scada.symbol.normal}", "divider": true, - "fieldSuffix": null, "disableOnProperty": "scale", - "rowClass": "", - "fieldClass": "", - "min": null, - "max": null, - "step": null + "disabled": false, + "visible": true }, { "id": "majorWarningColor", "name": "{i18n:scada.symbol.major-ticks-color}", "type": "color", "default": "#FAA405", - "required": null, "subLabel": "{i18n:scada.symbol.warning}", "divider": true, - "fieldSuffix": null, "disableOnProperty": "scale", - "rowClass": "", - "fieldClass": "", - "min": null, - "max": null, - "step": null + "disabled": false, + "visible": true }, { "id": "majorCriticalColor", "name": "{i18n:scada.symbol.major-ticks-color}", "type": "color", "default": "#D12730", - "required": null, "subLabel": "{i18n:scada.symbol.critical}", - "divider": null, - "fieldSuffix": null, "disableOnProperty": "scale", - "rowClass": "", - "fieldClass": "", - "min": null, - "max": null, - "step": null + "disabled": false, + "visible": true }, { "id": "minorIntervals", "name": "{i18n:scada.symbol.minor-ticks}", "type": "number", "default": 5, - "required": null, "subLabel": "{i18n:scada.symbol.intervals}", - "divider": null, - "fieldSuffix": null, "disableOnProperty": "scale", - "rowClass": "", - "fieldClass": "", "min": 1, - "max": null, - "step": null + "disabled": false, + "visible": true }, { "id": "minorColor", "name": "{i18n:scada.symbol.minor-ticks-color}", "type": "color", "default": "#0000001F", - "required": null, "subLabel": "{i18n:scada.symbol.normal}", "divider": true, - "fieldSuffix": null, "disableOnProperty": "scale", - "rowClass": "", - "fieldClass": "", - "min": null, - "max": null, - "step": null + "disabled": false, + "visible": true }, { "id": "minorWarningColor", "name": "{i18n:scada.symbol.minor-ticks-color}", "type": "color", "default": "#FAA405", - "required": null, "subLabel": "{i18n:scada.symbol.warning}", "divider": true, - "fieldSuffix": null, "disableOnProperty": "scale", - "rowClass": "", - "fieldClass": "", - "min": null, - "max": null, - "step": null + "disabled": false, + "visible": true }, { "id": "minorCriticalColor", "name": "{i18n:scada.symbol.minor-ticks-color}", "type": "color", "default": "#D12730", - "required": null, "subLabel": "{i18n:scada.symbol.critical}", - "divider": null, - "fieldSuffix": null, "disableOnProperty": "scale", - "rowClass": "", - "fieldClass": "", - "min": null, - "max": null, - "step": null + "disabled": false, + "visible": true } ] }]]> diff --git a/application/src/main/data/json/system/scada_symbols/vertical-tank-hp.svg b/application/src/main/data/json/system/scada_symbols/vertical-tank-hp.svg index 3e29a4e657..e1b57d7906 100644 --- a/application/src/main/data/json/system/scada_symbols/vertical-tank-hp.svg +++ b/application/src/main/data/json/system/scada_symbols/vertical-tank-hp.svg @@ -38,7 +38,7 @@ }, { "tag": "scale", - "stateRenderFunction": "if (!ctx.properties.scale) {\n element.hide();\n} else {\n var scaleSet = element.remember('scaleSet');\n if (!scaleSet) {\n element.remember('scaleSet', true);\n element.clear();\n \n var majorIntervals = ctx.properties.majorIntervals;\n var minorIntervals = ctx.properties.minorIntervals;\n \n var start = 3;\n var majorIntervalLength = 994 / majorIntervals;\n var minorIntervalLength = majorIntervalLength / minorIntervals;\n for (var i = 0; i < majorIntervals + 1; i++) {\n var y = start + i * majorIntervalLength;\n var line = ctx.svg.line(160, y, 192, y).stroke({ width: 3 }).attr({class: 'majorTick'});\n element.add(line);\n var majorText = (100 - i * (100/majorIntervals)).toFixed(0);\n var majorTickText = ctx.svg.text(majorText);\n if (i === 0) {\n majorTickText.attr({x: 150, y: y + 10, 'text-anchor': 'end', class: 'majorTickText'});\n } else if (i === majorIntervals) {\n majorTickText.attr({x: 150, y: y - 5, 'text-anchor': 'end', class: 'majorTickText'});\n } else {\n majorTickText.attr({x: 150, y: y + 2, 'text-anchor': 'end', class: 'majorTickText'});\n }\n majorTickText.first().attr({'dominant-baseline': 'middle'});\n element.add(majorTickText);\n if (i < majorIntervals) {\n drawMinorTicks(y, minorIntervals, minorIntervalLength);\n }\n }\n }\n \n var majorFont = ctx.properties.majorFont;\n var majorColor = ctx.properties.majorColor;\n var minorColor = ctx.properties.minorColor;\n \n var majorTicks = element.find('line.majorTick');\n majorTicks.forEach(t => t.attr({stroke: majorColor}));\n \n var majorTicksText = element.find('text.majorTickText');\n ctx.api.font(majorTicksText, majorFont, majorColor);\n \n var minorTicks = element.find('line.minorTick');\n minorTicks.forEach(t => t.attr({stroke: minorColor}));\n}\n\nfunction drawMinorTicks(start, minorIntervals, minorIntervalLength) {\n for (var i = 1; i < minorIntervals; i++) {\n var minorY = start + i * minorIntervalLength;\n var minorLine = ctx.svg.line(172, minorY, 192, minorY).stroke({ width: 3 }).attr({class: 'minorTick'});\n element.add(minorLine);\n }\n}", + "stateRenderFunction": "if (!ctx.properties.scale) {\n element.hide();\n} else {\n var scaleSet = element.remember('scaleSet');\n if (!scaleSet) {\n element.remember('scaleSet', true);\n element.clear();\n \n var majorIntervals = ctx.properties.majorIntervals;\n var minorIntervals = ctx.properties.minorIntervals;\n \n var start = 3;\n var majorIntervalLength = 994 / majorIntervals;\n var minorIntervalLength = majorIntervalLength / minorIntervals;\n var tankCapacity = ctx.properties.scaleDisplayFormat ? 100 : (ctx.values.tankCapacity || 100);\n for (var i = 0; i < majorIntervals + 1; i++) {\n var y = start + i * majorIntervalLength;\n var line = ctx.svg.line(160, y, 192, y).stroke({ width: 3 }).attr({class: 'majorTick'});\n element.add(line);\n var majorText = ctx.api.formatValue((tankCapacity - i * (tankCapacity/majorIntervals)).toFixed(0), 0, ctx.properties.majorUnits, false);\n var majorTickText = ctx.svg.text(majorText);\n if (i === 0) {\n majorTickText.attr({x: 150, y: y + 10, 'text-anchor': 'end', class: 'majorTickText'});\n } else if (i === majorIntervals) {\n majorTickText.attr({x: 150, y: y - 5, 'text-anchor': 'end', class: 'majorTickText'});\n } else {\n majorTickText.attr({x: 150, y: y + 2, 'text-anchor': 'end', class: 'majorTickText'});\n }\n majorTickText.first().attr({'dominant-baseline': 'middle'});\n element.add(majorTickText);\n if (i < majorIntervals) {\n drawMinorTicks(y, minorIntervals, minorIntervalLength);\n }\n }\n }\n \n var majorFont = ctx.properties.majorFont;\n var majorColor = ctx.properties.majorColor;\n var minorColor = ctx.properties.minorColor;\n \n var majorTicks = element.find('line.majorTick');\n majorTicks.forEach(t => t.attr({stroke: majorColor}));\n \n var majorTicksText = element.find('text.majorTickText');\n ctx.api.font(majorTicksText, majorFont, majorColor);\n \n var minorTicks = element.find('line.minorTick');\n minorTicks.forEach(t => t.attr({stroke: minorColor}));\n}\n\nfunction drawMinorTicks(start, minorIntervals, minorIntervalLength) {\n for (var i = 1; i < minorIntervals; i++) {\n var minorY = start + i * minorIntervalLength;\n var minorLine = ctx.svg.line(172, minorY, 192, minorY).stroke({ width: 3 }).attr({class: 'minorTick'});\n element.add(minorLine);\n }\n}", "actions": null }, { @@ -307,64 +307,67 @@ "name": "{i18n:scada.symbol.tank-color}", "type": "color", "default": "#EBEBEB", - "required": null, - "subLabel": null, - "divider": null, - "fieldSuffix": null, - "disableOnProperty": null, - "rowClass": "", - "fieldClass": "", - "min": null, - "max": null, - "step": null + "disabled": false, + "visible": true }, { "id": "fluidColor", "name": "{i18n:scada.symbol.fluid-color}", "type": "color", "default": "#C8DFF7", - "required": null, - "subLabel": null, - "divider": null, - "fieldSuffix": null, - "disableOnProperty": null, - "rowClass": "", - "fieldClass": "", - "min": null, - "max": null, - "step": null + "disabled": false, + "visible": true }, { "id": "scale", "name": "{i18n:scada.symbol.scale}", "type": "switch", "default": true, - "required": null, - "subLabel": null, - "divider": null, - "fieldSuffix": null, - "disableOnProperty": null, - "rowClass": "", - "fieldClass": "", - "min": null, - "max": null, - "step": null + "disabled": false, + "visible": true + }, + { + "id": "scaleDisplayFormat", + "name": "{i18n:scada.symbol.scale}", + "type": "select", + "default": true, + "subLabel": "{i18n:scada.symbol.display-format}", + "disableOnProperty": "scale", + "items": [ + { + "value": true, + "label": "Percentage" + }, + { + "value": false, + "label": "Metric" + } + ], + "disabled": false, + "visible": true }, { "id": "majorIntervals", "name": "{i18n:scada.symbol.major-ticks}", "type": "number", "default": 10, - "required": null, "subLabel": "{i18n:scada.symbol.intervals}", "divider": false, - "fieldSuffix": null, "disableOnProperty": "scale", - "rowClass": "", - "fieldClass": "", "min": 1, - "max": null, - "step": 1 + "step": 1, + "disabled": false, + "visible": true + }, + { + "id": "majorUnits", + "name": "{i18n:scada.symbol.major-ticks}", + "type": "units", + "subLabel": "{i18n:scada.symbol.units}", + "divider": true, + "disableOnProperty": "scale", + "disabled": false, + "visible": true }, { "id": "majorFont", @@ -377,96 +380,57 @@ "weight": "500", "style": "normal" }, - "required": null, - "subLabel": null, - "divider": null, - "fieldSuffix": null, "disableOnProperty": "scale", - "rowClass": "", - "fieldClass": "", - "min": null, - "max": null, - "step": null + "disabled": false, + "visible": true }, { "id": "majorColor", "name": "{i18n:scada.symbol.major-ticks}", "type": "color", "default": "#00000061", - "required": null, - "subLabel": null, - "divider": null, - "fieldSuffix": null, "disableOnProperty": "scale", - "rowClass": "", - "fieldClass": "", - "min": null, - "max": null, - "step": null + "disabled": false, + "visible": true }, { "id": "minorIntervals", "name": "{i18n:scada.symbol.minor-ticks}", "type": "number", "default": 5, - "required": null, "subLabel": "{i18n:scada.symbol.intervals}", - "divider": null, - "fieldSuffix": null, "disableOnProperty": "scale", - "rowClass": "", - "fieldClass": "", "min": 1, - "max": null, - "step": null + "disabled": false, + "visible": true }, { "id": "minorColor", "name": "{i18n:scada.symbol.minor-ticks}", "type": "color", "default": "#0000001F", - "required": null, - "subLabel": null, - "divider": null, - "fieldSuffix": null, "disableOnProperty": "scale", - "rowClass": "", - "fieldClass": "", - "min": null, - "max": null, - "step": null + "disabled": false, + "visible": true }, { "id": "warningColor", "name": "{i18n:scada.symbol.alarm-colors}", "type": "color", "default": "#FAA405", - "required": null, "subLabel": "{i18n:scada.symbol.warning}", "divider": true, - "fieldSuffix": null, - "disableOnProperty": null, - "rowClass": "", - "fieldClass": "", - "min": null, - "max": null, - "step": null + "disabled": false, + "visible": true }, { "id": "criticalColor", "name": "{i18n:scada.symbol.alarm-colors}", "type": "color", "default": "#D12730", - "required": null, "subLabel": "{i18n:scada.symbol.critical}", - "divider": null, - "fieldSuffix": null, - "disableOnProperty": null, - "rowClass": "", - "fieldClass": "", - "min": null, - "max": null, - "step": null + "disabled": false, + "visible": true } ] }]]> diff --git a/application/src/main/data/json/system/scada_symbols/vertical-tank.svg b/application/src/main/data/json/system/scada_symbols/vertical-tank.svg index de7f907e76..868256e3df 100644 --- a/application/src/main/data/json/system/scada_symbols/vertical-tank.svg +++ b/application/src/main/data/json/system/scada_symbols/vertical-tank.svg @@ -33,7 +33,7 @@ }, { "tag": "scale", - "stateRenderFunction": "if (!ctx.properties.scale) {\n element.hide();\n} else {\n var scaleSet = element.remember('scaleSet');\n if (!scaleSet) {\n element.remember('scaleSet', true);\n element.clear();\n \n var majorIntervals = ctx.properties.majorIntervals;\n var minorIntervals = ctx.properties.minorIntervals;\n \n var start = 205;\n var majorIntervalLength = 760 / majorIntervals;\n var minorIntervalLength = majorIntervalLength / minorIntervals;\n for (var i = 0; i < majorIntervals + 1; i++) {\n var y = start + i * majorIntervalLength;\n var line = ctx.svg.line(340, y, 372, y).stroke({ width: 3 }).attr({class: 'majorTick'});\n element.add(line);\n var majorText = (100 - i * (100/majorIntervals)).toFixed(0);\n var majorTickText = ctx.svg.text(majorText);\n majorTickText.attr({x: 330, y: y + 2, 'text-anchor': 'end', class: 'majorTickText'});\n majorTickText.first().attr({'dominant-baseline': 'middle'});\n element.add(majorTickText);\n if (i < majorIntervals) {\n drawMinorTicks(y, minorIntervals, minorIntervalLength);\n }\n }\n }\n \n var majorFont = ctx.properties.majorFont;\n var majorColor = ctx.properties.majorColor;\n var minorColor = ctx.properties.minorColor;\n if (ctx.values.critical) {\n majorColor = ctx.properties.majorCriticalColor;\n minorColor = ctx.properties.minorCriticalColor;\n } else if (ctx.values.warning) {\n majorColor = ctx.properties.minorWarningColor;\n minorColor = ctx.properties.minorWarningColor;\n }\n \n var majorTicks = element.find('line.majorTick');\n majorTicks.forEach(t => t.attr({stroke: majorColor}));\n \n var majorTicksText = element.find('text.majorTickText');\n ctx.api.font(majorTicksText, majorFont, majorColor);\n \n var minorTicks = element.find('line.minorTick');\n minorTicks.forEach(t => t.attr({stroke: minorColor}));\n \n var elementCriticalAnimation = element.remember('criticalAnimation');\n var criticalAnimation = ctx.values.critical && ctx.values.criticalAnimation;\n\n if (elementCriticalAnimation !== criticalAnimation) {\n element.remember('criticalAnimation', criticalAnimation);\n if (criticalAnimation) {\n ctx.api.cssAnimate(element, 500).attr({opacity: 0.15}).loop(0, true);\n } else {\n ctx.api.resetCssAnimation(element);\n }\n }\n}\n\nfunction drawMinorTicks(start, minorIntervals, minorIntervalLength) {\n for (var i = 1; i < minorIntervals; i++) {\n var minorY = start + i * minorIntervalLength;\n var minorLine = ctx.svg.line(352, minorY, 372, minorY).stroke({ width: 3 }).attr({class: 'minorTick'});\n element.add(minorLine);\n }\n}", + "stateRenderFunction": "if (!ctx.properties.scale) {\n element.hide();\n} else {\n var scaleSet = element.remember('scaleSet');\n if (!scaleSet) {\n element.remember('scaleSet', true);\n element.clear();\n \n var majorIntervals = ctx.properties.majorIntervals;\n var minorIntervals = ctx.properties.minorIntervals;\n \n var start = 205;\n var majorIntervalLength = 760 / majorIntervals;\n var minorIntervalLength = majorIntervalLength / minorIntervals;\n var tankCapacity = ctx.properties.scaleDisplayFormat ? 100 : (ctx.values.tankCapacity || 100);\n for (var i = 0; i < majorIntervals + 1; i++) {\n var y = start + i * majorIntervalLength;\n var line = ctx.svg.line(340, y, 372, y).stroke({ width: 3 }).attr({class: 'majorTick'});\n element.add(line);\n var majorText = ctx.api.formatValue((tankCapacity - i * (tankCapacity/majorIntervals)).toFixed(0), 0, ctx.properties.majorUnits, false);\n var majorTickText = ctx.svg.text(majorText);\n majorTickText.attr({x: 330, y: y + 2, 'text-anchor': 'end', class: 'majorTickText'});\n majorTickText.first().attr({'dominant-baseline': 'middle'});\n element.add(majorTickText);\n if (i < majorIntervals) {\n drawMinorTicks(y, minorIntervals, minorIntervalLength);\n }\n }\n }\n \n var majorFont = ctx.properties.majorFont;\n var majorColor = ctx.properties.majorColor;\n var minorColor = ctx.properties.minorColor;\n if (ctx.values.critical) {\n majorColor = ctx.properties.majorCriticalColor;\n minorColor = ctx.properties.minorCriticalColor;\n } else if (ctx.values.warning) {\n majorColor = ctx.properties.minorWarningColor;\n minorColor = ctx.properties.minorWarningColor;\n }\n \n var majorTicks = element.find('line.majorTick');\n majorTicks.forEach(t => t.attr({stroke: majorColor}));\n \n var majorTicksText = element.find('text.majorTickText');\n ctx.api.font(majorTicksText, majorFont, majorColor);\n \n var minorTicks = element.find('line.minorTick');\n minorTicks.forEach(t => t.attr({stroke: minorColor}));\n \n var elementCriticalAnimation = element.remember('criticalAnimation');\n var criticalAnimation = ctx.values.critical && ctx.values.criticalAnimation;\n\n if (elementCriticalAnimation !== criticalAnimation) {\n element.remember('criticalAnimation', criticalAnimation);\n if (criticalAnimation) {\n ctx.api.cssAnimate(element, 500).attr({opacity: 0.15}).loop(0, true);\n } else {\n ctx.api.resetCssAnimation(element);\n }\n }\n}\n\nfunction drawMinorTicks(start, minorIntervals, minorIntervalLength) {\n for (var i = 1; i < minorIntervals; i++) {\n var minorY = start + i * minorIntervalLength;\n var minorLine = ctx.svg.line(352, minorY, 372, minorY).stroke({ width: 3 }).attr({class: 'minorTick'});\n element.add(minorLine);\n }\n}", "actions": null }, { @@ -278,80 +278,43 @@ "name": "{i18n:scada.symbol.tank-color}", "type": "color", "default": "#E5E5E5", - "required": null, - "subLabel": null, - "divider": null, - "fieldSuffix": null, - "disableOnProperty": null, - "rowClass": "", - "fieldClass": "", - "min": null, - "max": null, - "step": null + "disabled": false, + "visible": true }, { "id": "fluidColor", "name": "{i18n:scada.symbol.fluid-color}", "type": "color", "default": "#1EC1F480", - "required": null, - "subLabel": null, - "divider": null, - "fieldSuffix": null, - "disableOnProperty": null, - "rowClass": "", - "fieldClass": "", - "min": null, - "max": null, - "step": null + "disabled": false, + "visible": true }, { "id": "valueBox", "name": "{i18n:scada.symbol.value-box}", "type": "switch", "default": true, - "required": null, - "subLabel": null, - "divider": null, - "fieldSuffix": null, - "disableOnProperty": null, - "rowClass": "", - "fieldClass": "", - "min": null, - "max": null, - "step": null + "disabled": false, + "visible": true }, { "id": "valueBoxColor", "name": "{i18n:scada.symbol.value-box}", "type": "color", "default": "#F3F3F3", - "required": null, - "subLabel": null, - "divider": null, - "fieldSuffix": null, "disableOnProperty": "valueBox", - "rowClass": "", - "fieldClass": "", - "min": null, - "max": null, - "step": null + "disabled": false, + "visible": true }, { "id": "valueUnits", "name": "{i18n:scada.symbol.value-text}", "type": "units", "default": "gal", - "required": null, "subLabel": "{i18n:scada.symbol.units}", - "divider": null, - "fieldSuffix": null, "disableOnProperty": "valueBox", - "rowClass": "", - "fieldClass": "", - "min": null, - "max": null, - "step": null + "disabled": false, + "visible": true }, { "id": "valueTextFont", @@ -364,80 +327,79 @@ "weight": "500", "style": "normal" }, - "required": null, - "subLabel": null, - "divider": null, - "fieldSuffix": null, "disableOnProperty": "valueBox", - "rowClass": "", - "fieldClass": "", - "min": null, - "max": null, - "step": null + "disabled": false, + "visible": true }, { "id": "valueTextColor", "name": "{i18n:scada.symbol.value-text}", "type": "color", "default": "#0000008A", - "required": null, - "subLabel": null, - "divider": null, - "fieldSuffix": null, "disableOnProperty": "valueBox", - "rowClass": "", - "fieldClass": "", - "min": null, - "max": null, - "step": null + "disabled": false, + "visible": true }, { "id": "scale", "name": "{i18n:scada.symbol.scale}", "type": "switch", "default": true, - "required": null, - "subLabel": null, - "divider": null, - "fieldSuffix": null, - "disableOnProperty": null, - "rowClass": "", - "fieldClass": "", - "min": null, - "max": null, - "step": null + "disabled": false, + "visible": true + }, + { + "id": "scaleDisplayFormat", + "name": "{i18n:scada.symbol.scale}", + "type": "select", + "default": true, + "subLabel": "{i18n:scada.symbol.display-format}", + "divider": false, + "disableOnProperty": "scale", + "items": [ + { + "value": true, + "label": "Percentage" + }, + { + "value": false, + "label": "Metric" + } + ], + "disabled": false, + "visible": true }, { "id": "transparent", "name": "{i18n:scada.symbol.transparent-mode}", "type": "switch", "default": false, - "required": null, - "subLabel": null, - "divider": null, - "fieldSuffix": null, "disableOnProperty": "scale", - "rowClass": "", - "fieldClass": "", - "min": null, - "max": null, - "step": null + "disabled": false, + "visible": true }, { "id": "majorIntervals", "name": "{i18n:scada.symbol.major-ticks}", "type": "number", "default": 10, - "required": null, "subLabel": "{i18n:scada.symbol.intervals}", "divider": true, - "fieldSuffix": null, "disableOnProperty": "scale", - "rowClass": "", - "fieldClass": "", "min": 1, - "max": null, - "step": 1 + "step": 1, + "disabled": false, + "visible": true + }, + { + "id": "majorUnits", + "name": "{i18n:scada.symbol.major-ticks}", + "type": "units", + "subLabel": "{i18n:scada.symbol.units}", + "divider": true, + "disableOnProperty": "scale", + "disabled": false, + "visible": true }, { "id": "majorFont", @@ -450,128 +412,84 @@ "weight": "500", "style": "normal" }, - "required": null, - "subLabel": null, - "divider": null, - "fieldSuffix": null, "disableOnProperty": "scale", - "rowClass": "", - "fieldClass": "", - "min": null, - "max": null, - "step": null + "disabled": false, + "visible": true }, { "id": "majorColor", "name": "{i18n:scada.symbol.major-ticks-color}", "type": "color", "default": "#00000061", - "required": null, "subLabel": "{i18n:scada.symbol.normal}", "divider": true, - "fieldSuffix": null, "disableOnProperty": "scale", - "rowClass": "", - "fieldClass": "", - "min": null, - "max": null, - "step": null + "disabled": false, + "visible": true }, { "id": "majorWarningColor", "name": "{i18n:scada.symbol.major-ticks-color}", "type": "color", "default": "#FAA405", - "required": null, "subLabel": "{i18n:scada.symbol.warning}", "divider": true, - "fieldSuffix": null, "disableOnProperty": "scale", - "rowClass": "", - "fieldClass": "", - "min": null, - "max": null, - "step": null + "disabled": false, + "visible": true }, { "id": "majorCriticalColor", "name": "{i18n:scada.symbol.major-ticks-color}", "type": "color", "default": "#D12730", - "required": null, "subLabel": "{i18n:scada.symbol.critical}", - "divider": null, - "fieldSuffix": null, "disableOnProperty": "scale", - "rowClass": "", - "fieldClass": "", - "min": null, - "max": null, - "step": null + "disabled": false, + "visible": true }, { "id": "minorIntervals", "name": "{i18n:scada.symbol.minor-ticks}", "type": "number", "default": 5, - "required": null, "subLabel": "{i18n:scada.symbol.intervals}", - "divider": null, - "fieldSuffix": null, "disableOnProperty": "scale", - "rowClass": "", - "fieldClass": "", "min": 1, - "max": null, - "step": null + "disabled": false, + "visible": true }, { "id": "minorColor", "name": "{i18n:scada.symbol.minor-ticks-color}", "type": "color", "default": "#0000001F", - "required": null, "subLabel": "{i18n:scada.symbol.normal}", "divider": true, - "fieldSuffix": null, "disableOnProperty": "scale", - "rowClass": "", - "fieldClass": "", - "min": null, - "max": null, - "step": null + "disabled": false, + "visible": true }, { "id": "minorWarningColor", "name": "{i18n:scada.symbol.minor-ticks-color}", "type": "color", "default": "#FAA405", - "required": null, "subLabel": "{i18n:scada.symbol.warning}", "divider": true, - "fieldSuffix": null, "disableOnProperty": "scale", - "rowClass": "", - "fieldClass": "", - "min": null, - "max": null, - "step": null + "disabled": false, + "visible": true }, { "id": "minorCriticalColor", "name": "{i18n:scada.symbol.minor-ticks-color}", "type": "color", "default": "#D12730", - "required": null, "subLabel": "{i18n:scada.symbol.critical}", - "divider": null, - "fieldSuffix": null, "disableOnProperty": "scale", - "rowClass": "", - "fieldClass": "", - "min": null, - "max": null, - "step": null + "disabled": false, + "visible": true } ] }]]> 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 de97184f5a..ba7683c30e 100644 --- a/ui-ngx/src/assets/locale/locale.constant-en_US.json +++ b/ui-ngx/src/assets/locale/locale.constant-en_US.json @@ -3217,6 +3217,7 @@ "top-fluid-color": "Top fluid color", "bottom-fluid-color": "Bottom fluid color", "display": "Display", + "display-format": "Display format", "value": "Value", "decimals": "Decimals", "units": "Units", From 8b30d1d93be0b4b5743ff9a77b05395cafc2e661 Mon Sep 17 00:00:00 2001 From: IrynaMatveieva Date: Tue, 4 Mar 2025 11:31:13 +0200 Subject: [PATCH 170/345] fixed test for new logic --- .../service/cf/ctx/state/SingleValueArgumentEntryTest.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/application/src/test/java/org/thingsboard/server/service/cf/ctx/state/SingleValueArgumentEntryTest.java b/application/src/test/java/org/thingsboard/server/service/cf/ctx/state/SingleValueArgumentEntryTest.java index e83e30663d..2c48ed9167 100644 --- a/application/src/test/java/org/thingsboard/server/service/cf/ctx/state/SingleValueArgumentEntryTest.java +++ b/application/src/test/java/org/thingsboard/server/service/cf/ctx/state/SingleValueArgumentEntryTest.java @@ -71,6 +71,6 @@ public class SingleValueArgumentEntryTest { @Test void testUpdateEntryWhenValueWasNotChanged() { - assertThat(entry.updateEntry(new SingleValueArgumentEntry(ts + 18, new LongDataEntry("key", 11L), 237L))).isFalse(); + assertThat(entry.updateEntry(new SingleValueArgumentEntry(ts + 18, new LongDataEntry("key", 11L), 364L))).isTrue(); } } \ No newline at end of file From 264a767efeaa2df3b9aebc9fe15d451b5ec3f325 Mon Sep 17 00:00:00 2001 From: Dmytro Skarzhynets Date: Tue, 4 Mar 2025 11:44:41 +0200 Subject: [PATCH 171/345] Save attributes strategies: preserve backwards compatibility when deprecating 'notifyDevice' --- .../queue/DefaultTbCoreConsumerService.java | 17 +++++++++++++---- .../DefaultSubscriptionManagerService.java | 11 +++++++++++ .../SubscriptionManagerService.java | 9 +++++++++ common/proto/src/main/proto/queue.proto | 7 +++++-- 4 files changed, 38 insertions(+), 6 deletions(-) diff --git a/application/src/main/java/org/thingsboard/server/service/queue/DefaultTbCoreConsumerService.java b/application/src/main/java/org/thingsboard/server/service/queue/DefaultTbCoreConsumerService.java index 0152e34d50..4f0522e62a 100644 --- a/application/src/main/java/org/thingsboard/server/service/queue/DefaultTbCoreConsumerService.java +++ b/application/src/main/java/org/thingsboard/server/service/queue/DefaultTbCoreConsumerService.java @@ -542,10 +542,19 @@ public class DefaultTbCoreConsumerService extends AbstractConsumerService keys, TbCallback callback) { + onAttributesDelete(tenantId, entityId, scope, keys, false, callback); + } + + @Override + public void onAttributesDelete(TenantId tenantId, EntityId entityId, String scope, List keys, boolean notifyDevice, TbCallback callback) { processAttributesUpdate(entityId, scope, keys.stream().map(key -> new BaseAttributeKvEntry(0, new StringDataEntry(key, ""))).collect(Collectors.toList())); if (entityId.getEntityType() == EntityType.DEVICE) { if (TbAttributeSubscriptionScope.SERVER_SCOPE.name().equalsIgnoreCase(scope) || TbAttributeSubscriptionScope.ANY_SCOPE.name().equalsIgnoreCase(scope)) { deleteDeviceInactivityTimeout(tenantId, entityId, keys); + } else if (TbAttributeSubscriptionScope.SHARED_SCOPE.name().equalsIgnoreCase(scope) && notifyDevice) { + clusterService.pushMsgToCore(DeviceAttributesEventNotificationMsg.onDelete(tenantId, + new DeviceId(entityId.getId()), scope, keys), null); } } callback.onSuccess(); diff --git a/application/src/main/java/org/thingsboard/server/service/subscription/SubscriptionManagerService.java b/application/src/main/java/org/thingsboard/server/service/subscription/SubscriptionManagerService.java index 3fd58a1243..57d4fda5f8 100644 --- a/application/src/main/java/org/thingsboard/server/service/subscription/SubscriptionManagerService.java +++ b/application/src/main/java/org/thingsboard/server/service/subscription/SubscriptionManagerService.java @@ -41,6 +41,15 @@ public interface SubscriptionManagerService extends ApplicationListener keys, TbCallback empty); + /** + * This method is retained solely for backwards compatibility, specifically to handle + * legacy proto messages that include the notifyDevice field. + * + * @deprecated as of 4.0, this method will be removed in future releases. + */ + @Deprecated(forRemoval = true, since = "4.0") + void onAttributesDelete(TenantId tenantId, EntityId entityId, String scope, List keys, boolean notifyDevice, TbCallback empty); + void onTimeSeriesDelete(TenantId tenantId, EntityId entityId, List keys, TbCallback callback); void onAlarmUpdate(TenantId tenantId, EntityId entityId, AlarmInfo alarm, TbCallback callback); diff --git a/common/proto/src/main/proto/queue.proto b/common/proto/src/main/proto/queue.proto index 969b47f847..76477d7801 100644 --- a/common/proto/src/main/proto/queue.proto +++ b/common/proto/src/main/proto/queue.proto @@ -1079,8 +1079,11 @@ message TbAttributeDeleteProto { int64 tenantIdLSB = 5; string scope = 6; repeated string keys = 7; - // Deprecated since 4.0, not used anymore since device notification are now handled in DefaultTelemetrySubscriptionService instead of DefaultSubscriptionManagerService - bool notifyDevice = 8 [deprecated = true]; + // DEPRECATED. FOR REMOVAL + // Since 4.0, this field is no longer used. + // Device notifications are now handled directly by DefaultTelemetrySubscriptionService, + // eliminating the need to pass this parameter through the queue and proto to DefaultSubscriptionManagerService. + optional bool notifyDevice = 8 [deprecated = true]; } message TbTimeSeriesDeleteProto { From 6265ff45a8adca7c139d226d81054d287ca406bf Mon Sep 17 00:00:00 2001 From: Andrii Shvaika Date: Tue, 4 Mar 2025 11:54:28 +0200 Subject: [PATCH 172/345] merge function draft --- .../api/tbel/DefaultTbelInvokeService.java | 2 + .../script/api/tbel/TbTimeWindow.java | 3 + .../thingsboard/script/api/tbel/TbUtils.java | 60 +++++++++++++--- .../api/tbel/TbelCfTsMultiDoubleVal.java | 34 +++++++++ .../script/api/tbel/TbUtilsTest.java | 72 ++++++++++++++++++- 5 files changed, 160 insertions(+), 11 deletions(-) diff --git a/common/script/script-api/src/main/java/org/thingsboard/script/api/tbel/DefaultTbelInvokeService.java b/common/script/script-api/src/main/java/org/thingsboard/script/api/tbel/DefaultTbelInvokeService.java index 962d384a46..38fed7a2aa 100644 --- a/common/script/script-api/src/main/java/org/thingsboard/script/api/tbel/DefaultTbelInvokeService.java +++ b/common/script/script-api/src/main/java/org/thingsboard/script/api/tbel/DefaultTbelInvokeService.java @@ -138,6 +138,8 @@ public class DefaultTbelInvokeService extends AbstractScriptInvokeService implem parserConfig.registerDataType("TbelCfTsDoubleVal", TbelCfTsDoubleVal.class, TbelCfTsDoubleVal::memorySize); parserConfig.registerDataType("TbelCfTsRollingData", TbelCfTsRollingData.class, TbelCfTsRollingData::memorySize); parserConfig.registerDataType("TbTimeWindow", TbTimeWindow.class, TbTimeWindow::memorySize); + parserConfig.registerDataType("TbelCfTsDoubleVal", TbelCfTsMultiDoubleVal.class, TbelCfTsMultiDoubleVal::memorySize); + TbUtils.register(parserConfig); executor = MoreExecutors.listeningDecorator(ThingsBoardExecutors.newWorkStealingPool(threadPoolSize, "tbel-executor")); try { diff --git a/common/script/script-api/src/main/java/org/thingsboard/script/api/tbel/TbTimeWindow.java b/common/script/script-api/src/main/java/org/thingsboard/script/api/tbel/TbTimeWindow.java index 5048611838..b430b23757 100644 --- a/common/script/script-api/src/main/java/org/thingsboard/script/api/tbel/TbTimeWindow.java +++ b/common/script/script-api/src/main/java/org/thingsboard/script/api/tbel/TbTimeWindow.java @@ -35,4 +35,7 @@ public class TbTimeWindow implements TbelCfObject { return OBJ_SIZE; } + public boolean matches(long ts) { + return ts >= startTs && ts < endTs; + } } diff --git a/common/script/script-api/src/main/java/org/thingsboard/script/api/tbel/TbUtils.java b/common/script/script-api/src/main/java/org/thingsboard/script/api/tbel/TbUtils.java index 4796cf12bb..e6a0b8d043 100644 --- a/common/script/script-api/src/main/java/org/thingsboard/script/api/tbel/TbUtils.java +++ b/common/script/script-api/src/main/java/org/thingsboard/script/api/tbel/TbUtils.java @@ -1,12 +1,12 @@ /** * 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 - *

+ * + * 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. @@ -368,8 +368,16 @@ public class TbUtils { byte[].class, int.class))); parserConfig.addImport("parseBinaryArrayToInt", new MethodStub(TbUtils.class.getMethod("parseBinaryArrayToInt", byte[].class, int.class, int.class))); - parserConfig.addImport("merge", new MethodStub(TbUtils.class.getMethod("mergeCfTsRollingArgs", + parserConfig.addImport("merge", new MethodStub(TbUtils.class.getMethod("merge", TbelCfTsRollingArg.class, TbelCfTsRollingArg.class))); + parserConfig.addImport("merge", new MethodStub(TbUtils.class.getMethod("merge", + TbelCfTsRollingArg.class, TbelCfTsRollingArg.class, TbTimeWindow.class))); + parserConfig.addImport("merge", new MethodStub(TbUtils.class.getMethod("merge", + TbelCfTsRollingArg.class, TbelCfTsRollingArg.class, TbTimeWindow.class, Map.class))); + parserConfig.addImport("merge", new MethodStub(TbUtils.class.getMethod("merge", + List.class, TbTimeWindow.class))); + parserConfig.addImport("merge", new MethodStub(TbUtils.class.getMethod("merge", + List.class, TbTimeWindow.class, Map.class))); } public static String btoa(String input) { @@ -1510,11 +1518,28 @@ public class TbUtils { return hex; } - public static TbelCfTsRollingData mergeCfTsRollingArgs(TbelCfTsRollingArg a, TbelCfTsRollingArg b) { - return mergeCfTsRollingArgs(Arrays.asList(a, b), null); + public static TbelCfTsRollingData merge(TbelCfTsRollingArg a, TbelCfTsRollingArg b) { + return merge(Arrays.asList(a, b), null, null); } - public static TbelCfTsRollingData mergeCfTsRollingArgs(List args, Map settings) { + public static TbelCfTsRollingData merge(TbelCfTsRollingArg a, TbelCfTsRollingArg b, TbTimeWindow timeWindow) { + return merge(Arrays.asList(a, b), timeWindow, null); + } + + public static TbelCfTsRollingData merge(TbelCfTsRollingArg a, TbelCfTsRollingArg b, TbTimeWindow timeWindow, Map settings) { + return merge(Arrays.asList(a, b), timeWindow, settings); + } + + public static TbelCfTsRollingData merge(List args, TbTimeWindow timeWindow) { + return merge(args, timeWindow, null); + } + + public static TbelCfTsRollingData merge(List args, TbTimeWindow timeWindow, Map settings) { + boolean ignoreNaN = true; + if (settings != null && settings.containsKey("ignoreNaN")) { + ignoreNaN = Boolean.parseBoolean(settings.get("ignoreNaN").toString()); + } + TreeSet allTimestamps = new TreeSet<>(); long startTs = Long.MAX_VALUE; long endTs = Long.MIN_VALUE; @@ -1532,7 +1557,7 @@ public class TbUtils { double[] result = new double[args.size()]; Arrays.fill(result, Double.NaN); - var tw = new TbTimeWindow(startTs, endTs, allTimestamps.size()); + var tw = timeWindow != null ? timeWindow : new TbTimeWindow(startTs, endTs, allTimestamps.size()); for (long ts : allTimestamps) { for (int i = 0; i < args.size(); i++) { @@ -1543,7 +1568,22 @@ public class TbUtils { lastIndex[i]++; } } - data.add(new TbelCfTsMultiDoubleVal(ts, Arrays.copyOf(result, result.length))); + if (tw.matches(ts)) { + if (ignoreNaN) { + boolean skip = false; + for (int i = 0; i < args.size(); i++) { + if (Double.isNaN(result[i])) { + skip = true; + break; + } + } + if (!skip) { + data.add(new TbelCfTsMultiDoubleVal(ts, Arrays.copyOf(result, result.length))); + } + } else { + data.add(new TbelCfTsMultiDoubleVal(ts, Arrays.copyOf(result, result.length))); + } + } } return new TbelCfTsRollingData(tw, data); diff --git a/common/script/script-api/src/main/java/org/thingsboard/script/api/tbel/TbelCfTsMultiDoubleVal.java b/common/script/script-api/src/main/java/org/thingsboard/script/api/tbel/TbelCfTsMultiDoubleVal.java index 33e4a65bb8..2743bbd0de 100644 --- a/common/script/script-api/src/main/java/org/thingsboard/script/api/tbel/TbelCfTsMultiDoubleVal.java +++ b/common/script/script-api/src/main/java/org/thingsboard/script/api/tbel/TbelCfTsMultiDoubleVal.java @@ -15,6 +15,7 @@ */ package org.thingsboard.script.api.tbel; +import com.fasterxml.jackson.annotation.JsonIgnore; import lombok.Data; @Data @@ -25,6 +26,39 @@ public class TbelCfTsMultiDoubleVal implements TbelCfObject { private final long ts; private final double[] values; + @JsonIgnore + public double getV1() { + return getV(0); + } + + @JsonIgnore + public double getV2() { + return getV(1); + } + + @JsonIgnore + public double getV3() { + return getV(2); + } + + @JsonIgnore + public double getV4() { + return getV(3); + } + + @JsonIgnore + public double getV5() { + return getV(4); + } + + private double getV(int idx) { + if (values.length < idx + 1) { + throw new IllegalArgumentException("Can't get value at index " + idx + ". There are " + values.length + " values present."); + } else { + return values[idx]; + } + } + @Override public long memorySize() { return OBJ_SIZE + values.length * 8L; diff --git a/common/script/script-api/src/test/java/org/thingsboard/script/api/tbel/TbUtilsTest.java b/common/script/script-api/src/test/java/org/thingsboard/script/api/tbel/TbUtilsTest.java index fbd81948d6..f19a323068 100644 --- a/common/script/script-api/src/test/java/org/thingsboard/script/api/tbel/TbUtilsTest.java +++ b/common/script/script-api/src/test/java/org/thingsboard/script/api/tbel/TbUtilsTest.java @@ -1109,7 +1109,7 @@ public class TbUtilsTest { String validInput = Base64.getEncoder().encodeToString(new byte[]{1, 2, 3, 4, 5}); ExecutionArrayList actual = TbUtils.base64ToBytesList(ctx, validInput); ExecutionArrayList expected = new ExecutionArrayList<>(ctx); - expected.addAll(List.of((byte) 1, (byte)2, (byte)3, (byte)4, (byte)5)); + expected.addAll(List.of((byte) 1, (byte) 2, (byte) 3, (byte) 4, (byte) 5)); Assertions.assertEquals(expected, actual); String emptyInput = Base64.getEncoder().encodeToString(new byte[]{}); @@ -1123,6 +1123,7 @@ public class TbUtilsTest { TbUtils.base64ToBytesList(ctx, null); }); } + @Test public void bytesToHex_Test() { byte[] bb = {(byte) 0xBB, (byte) 0xAA}; @@ -1136,6 +1137,75 @@ public class TbUtilsTest { Assertions.assertEquals(expected, actual); } + @Test + public void merge_two_rolling_args_ts_match_test() { + TbTimeWindow tw = new TbTimeWindow(0, 60000, 1000); + TbelCfTsRollingArg arg1 = new TbelCfTsRollingArg(tw, Arrays.asList(new TbelCfTsDoubleVal(1000, 1), new TbelCfTsDoubleVal(5000, 2), new TbelCfTsDoubleVal(15000, 3))); + TbelCfTsRollingArg arg2 = new TbelCfTsRollingArg(tw, Arrays.asList(new TbelCfTsDoubleVal(1000, 11), new TbelCfTsDoubleVal(5000, 12), new TbelCfTsDoubleVal(15000, 13))); + + var result = TbUtils.merge(arg1, arg2); + Assertions.assertEquals(3, result.getSize()); + Assertions.assertNotNull(result.getValues()); + Assertions.assertNotNull(result.getValues().get(0)); + Assertions.assertEquals(1000L, result.getValues().get(0).getTs()); + Assertions.assertEquals(1, result.getValues().get(0).getValues()[0]); + Assertions.assertEquals(11, result.getValues().get(0).getValues()[1]); + } + + @Test + public void merge_two_rolling_args_with_timewindow_test() { + TbTimeWindow tw = new TbTimeWindow(0, 60000, 1000); + TbelCfTsRollingArg arg1 = new TbelCfTsRollingArg(tw, Arrays.asList(new TbelCfTsDoubleVal(1000, 1), new TbelCfTsDoubleVal(5000, 2), new TbelCfTsDoubleVal(15000, 3))); + TbelCfTsRollingArg arg2 = new TbelCfTsRollingArg(tw, Arrays.asList(new TbelCfTsDoubleVal(1000, 11), new TbelCfTsDoubleVal(5000, 12), new TbelCfTsDoubleVal(15000, 13))); + + var result = TbUtils.merge(arg1, arg2, new TbTimeWindow(0, 10000, 1000)); + Assertions.assertEquals(2, result.getSize()); + Assertions.assertNotNull(result.getValues()); + Assertions.assertNotNull(result.getValues().get(0)); + Assertions.assertEquals(1000L, result.getValues().get(0).getTs()); + Assertions.assertEquals(1, result.getValues().get(0).getValues()[0]); + Assertions.assertEquals(11, result.getValues().get(0).getValues()[1]); + } + + @Test + public void merge_two_rolling_args_ts_mismatch_default_test() { + TbTimeWindow tw = new TbTimeWindow(0, 60000, 1000); + TbelCfTsRollingArg arg1 = new TbelCfTsRollingArg(tw, Arrays.asList(new TbelCfTsDoubleVal(100, 1), new TbelCfTsDoubleVal(5000, 2), new TbelCfTsDoubleVal(15000, 3))); + TbelCfTsRollingArg arg2 = new TbelCfTsRollingArg(tw, Arrays.asList(new TbelCfTsDoubleVal(200, 11), new TbelCfTsDoubleVal(5000, 12), new TbelCfTsDoubleVal(15000, 13))); + + var result = TbUtils.merge(arg1, arg2); + Assertions.assertEquals(3, result.getSize()); + Assertions.assertNotNull(result.getValues()); + + TbelCfTsMultiDoubleVal item0 = result.getValues().get(0); + Assertions.assertNotNull(item0); + Assertions.assertEquals(200L, item0.getTs()); + Assertions.assertEquals(1, item0.getValues()[0]); + Assertions.assertEquals(11, item0.getValues()[1]); + } + + @Test + public void merge_two_rolling_args_ts_mismatch_ignore_nan_disabled_test() { + TbTimeWindow tw = new TbTimeWindow(0, 60000, 1000); + TbelCfTsRollingArg arg1 = new TbelCfTsRollingArg(tw, Arrays.asList(new TbelCfTsDoubleVal(100, 1), new TbelCfTsDoubleVal(5000, 2), new TbelCfTsDoubleVal(15000, 3))); + TbelCfTsRollingArg arg2 = new TbelCfTsRollingArg(tw, Arrays.asList(new TbelCfTsDoubleVal(200, 11), new TbelCfTsDoubleVal(5000, 12), new TbelCfTsDoubleVal(15000, 13))); + + var result = TbUtils.merge(Arrays.asList(arg1, arg2), new TbTimeWindow(0, 60000, 1000), Collections.singletonMap("ignoreNaN", false)); + Assertions.assertEquals(4, result.getSize()); + Assertions.assertNotNull(result.getValues()); + + TbelCfTsMultiDoubleVal item0 = result.getValues().get(0); + Assertions.assertNotNull(item0); + Assertions.assertEquals(100L, item0.getTs()); + Assertions.assertEquals(1, item0.getValues()[0]); + Assertions.assertEquals(Double.NaN, item0.getValues()[1]); + + TbelCfTsMultiDoubleVal item1 = result.getValues().get(1); + Assertions.assertEquals(200L, item1.getTs()); + Assertions.assertEquals(1, item1.getValues()[0]); + Assertions.assertEquals(11, item1.getValues()[1]); + } + private static List toList(byte[] data) { List result = new ArrayList<>(data.length); for (Byte b : data) { From 759bc94d1d797f521c3326ad777c4b45834197ff Mon Sep 17 00:00:00 2001 From: Andrii Shvaika Date: Tue, 4 Mar 2025 11:57:57 +0200 Subject: [PATCH 173/345] Review comments --- .../server/actors/ActorSystemContext.java | 2 +- .../CalculatedFieldEntityMessageProcessor.java | 13 +++++++------ application/src/main/resources/thingsboard.yml | 2 +- 3 files changed, 9 insertions(+), 8 deletions(-) diff --git a/application/src/main/java/org/thingsboard/server/actors/ActorSystemContext.java b/application/src/main/java/org/thingsboard/server/actors/ActorSystemContext.java index 9ab7adfcc6..de3864df9f 100644 --- a/application/src/main/java/org/thingsboard/server/actors/ActorSystemContext.java +++ b/application/src/main/java/org/thingsboard/server/actors/ActorSystemContext.java @@ -644,7 +644,7 @@ public class ActorSystemContext { @Getter private String deviceStateNodeRateLimitConfig; - @Value("${actors.calculated_fields.calculation_result_timeout:5}") + @Value("${actors.calculated_fields.calculation_timeout:5}") @Getter private long cfCalculationResultTimeout; diff --git a/application/src/main/java/org/thingsboard/server/actors/calculatedField/CalculatedFieldEntityMessageProcessor.java b/application/src/main/java/org/thingsboard/server/actors/calculatedField/CalculatedFieldEntityMessageProcessor.java index 279628c566..e42ebb593e 100644 --- a/application/src/main/java/org/thingsboard/server/actors/calculatedField/CalculatedFieldEntityMessageProcessor.java +++ b/application/src/main/java/org/thingsboard/server/actors/calculatedField/CalculatedFieldEntityMessageProcessor.java @@ -274,13 +274,13 @@ public class CalculatedFieldEntityMessageProcessor extends AbstractContextAwareM private void processStateIfReady(CalculatedFieldCtx ctx, List cfIdList, CalculatedFieldState state, UUID tbMsgId, TbMsgType tbMsgType, TbCallback callback) throws CalculatedFieldException { CalculatedFieldEntityCtxId ctxId = new CalculatedFieldEntityCtxId(tenantId, ctx.getCfId(), entityId); - boolean stateSizeOk; + boolean stateSizeChecked = false; try { if (ctx.isInitialized() && state.isReady()) { CalculatedFieldResult calculationResult = state.performCalculation(ctx).get(systemContext.getCfCalculationResultTimeout(), TimeUnit.SECONDS); state.checkStateSize(ctxId, ctx.getMaxStateSize()); - stateSizeOk = state.isSizeOk(); - if (stateSizeOk) { + stateSizeChecked = true; + if (state.isSizeOk()) { cfService.pushMsgToRuleEngine(tenantId, entityId, calculationResult, cfIdList, callback); if (DebugModeUtil.isDebugAllAvailable(ctx.getCalculatedField())) { systemContext.persistCalculatedFieldDebugEvent(tenantId, ctx.getCfId(), entityId, state.getArguments(), tbMsgId, tbMsgType, JacksonUtil.writeValueAsString(calculationResult.getResult()), null); @@ -290,9 +290,10 @@ public class CalculatedFieldEntityMessageProcessor extends AbstractContextAwareM } catch (Exception e) { throw CalculatedFieldException.builder().ctx(ctx).eventEntity(entityId).msgId(tbMsgId).msgType(tbMsgType).arguments(state.getArguments()).cause(e).build(); } finally { - state.checkStateSize(ctxId, ctx.getMaxStateSize()); - stateSizeOk = state.isSizeOk(); - if (stateSizeOk) { + if (!stateSizeChecked) { + state.checkStateSize(ctxId, ctx.getMaxStateSize()); + } + if (state.isSizeOk()) { cfStateService.persistState(ctxId, state, callback); } else { removeStateAndRaiseSizeException(ctxId, CalculatedFieldException.builder().ctx(ctx).eventEntity(entityId).errorMessage(ctx.getSizeExceedsLimitMessage()).build(), callback); diff --git a/application/src/main/resources/thingsboard.yml b/application/src/main/resources/thingsboard.yml index d0ccef8ba9..53258a4296 100644 --- a/application/src/main/resources/thingsboard.yml +++ b/application/src/main/resources/thingsboard.yml @@ -513,7 +513,7 @@ actors: # The value of DEBUG mode rate limit. By default, no more than 50 thousand events per hour configuration: "${ACTORS_CALCULATED_FIELD_DEBUG_MODE_RATE_LIMITS_PER_TENANT_CONFIGURATION:50000:3600}" # Time in seconds to receive calculation result. - calculation_result_timeout: "${ACTORS_CALCULATION_RESULT_TIMEOUT_SEC:5}" + calculation_timeout: "${ACTORS_CALCULATION_TIMEOUT_SEC:5}" debug: settings: From 964beaae3f1db61d1d98ed0b8492e08a3a9f9936 Mon Sep 17 00:00:00 2001 From: dashevchenko Date: Tue, 4 Mar 2025 12:07:19 +0200 Subject: [PATCH 174/345] fixed flaky tests --- .../entitiy/EdqsEntityServiceTest.java | 4 +- .../service/entitiy/EntityServiceTest.java | 204 +++--------------- 2 files changed, 36 insertions(+), 172 deletions(-) diff --git a/application/src/test/java/org/thingsboard/server/service/entitiy/EdqsEntityServiceTest.java b/application/src/test/java/org/thingsboard/server/service/entitiy/EdqsEntityServiceTest.java index f4cdf3038b..7f29821afd 100644 --- a/application/src/test/java/org/thingsboard/server/service/entitiy/EdqsEntityServiceTest.java +++ b/application/src/test/java/org/thingsboard/server/service/entitiy/EdqsEntityServiceTest.java @@ -108,9 +108,9 @@ public class EdqsEntityServiceTest extends EntityServiceTest { } @Override - protected List findByQueryAndCheckTelemetry(EntityDataQuery query, EntityKeyType entityKeyType, String key, List expectedTelemetries) { + protected List findByQueryAndCheckTelemetry(EntityDataQuery query, EntityKeyType entityKeyType, String key, List expectedTelemetries) { return await().atMost(15, TimeUnit.SECONDS).until(() -> findEntitiesTelemetry(query, entityKeyType, key, expectedTelemetries), - loadedTelemetry -> loadedTelemetry.containsAll(expectedTelemetries)); + loadedEntities -> loadedEntities.stream().map(entityData -> entityData.getLatest().get(entityKeyType).get(key).getValue()).toList().containsAll(expectedTelemetries)); } @Override 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 dc1f46eb3c..18687cedba 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 @@ -413,22 +413,10 @@ public class EntityServiceTest extends AbstractControllerTest { List entityFields = Collections.singletonList(new EntityKey(EntityKeyType.ENTITY_FIELD, "name")); List latestValues = Collections.singletonList(new EntityKey(EntityKeyType.ATTRIBUTE, "temperature")); - EntityDataQuery query = new EntityDataQuery(filter, pageLink, entityFields, latestValues, null); - PageData data = findByQueryAndCheck(query, 25); - List loadedEntities = new ArrayList<>(data.getData()); - while (data.hasNext()) { - query = query.next(); - data = findByQuery(query); - loadedEntities.addAll(data.getData()); - } - Assert.assertEquals(25, loadedEntities.size()); - List loadedTemperatures = loadedEntities.stream().map(entityData -> - entityData.getLatest().get(EntityKeyType.ATTRIBUTE).get("temperature").getValue()).collect(Collectors.toList()); List deviceTemperatures = temperatures.stream().map(aLong -> Long.toString(aLong)).collect(Collectors.toList()); - Assert.assertEquals(deviceTemperatures, loadedTemperatures); - //count query - countByQueryAndCheck(query, 25); + EntityDataQuery query = new EntityDataQuery(filter, pageLink, entityFields, latestValues, null); + findByQueryAndCheckTelemetry(query, EntityKeyType.ATTRIBUTE, "temperature", deviceTemperatures); pageLink = new EntityDataPageLink(10, 0, null, sortOrder); KeyFilter highTemperatureFilter = new KeyFilter(); @@ -439,25 +427,10 @@ public class EntityServiceTest extends AbstractControllerTest { highTemperatureFilter.setPredicate(predicate); List keyFilters = Collections.singletonList(highTemperatureFilter); - query = new EntityDataQuery(filter, pageLink, entityFields, latestValues, keyFilters); - data = findByQueryAndCheck(query, highTemperatures.size()); - - loadedEntities = new ArrayList<>(data.getData()); - while (data.hasNext()) { - query = query.next(); - data = findByQuery(query); - loadedEntities.addAll(data.getData()); - } - Assert.assertEquals(highTemperatures.size(), loadedEntities.size()); - - List loadedHighTemperatures = loadedEntities.stream().map(entityData -> - entityData.getLatest().get(EntityKeyType.ATTRIBUTE).get("temperature").getValue()).collect(Collectors.toList()); List deviceHighTemperatures = highTemperatures.stream().map(aLong -> Long.toString(aLong)).collect(Collectors.toList()); - Assert.assertEquals(deviceHighTemperatures, loadedHighTemperatures); - - //count query - countByQueryAndCheck(query, deviceHighTemperatures.size()); + query = new EntityDataQuery(filter, pageLink, entityFields, latestValues, keyFilters); + findByQueryAndCheckTelemetry(query, EntityKeyType.ATTRIBUTE, "temperature", deviceHighTemperatures); deviceService.deleteDevicesByTenantId(tenantId); } @@ -590,20 +563,12 @@ public class EntityServiceTest extends AbstractControllerTest { List entityFields = Collections.singletonList(new EntityKey(EntityKeyType.ENTITY_FIELD, "name")); List latestValues = Collections.singletonList(new EntityKey(EntityKeyType.ATTRIBUTE, "temperature")); + List deviceTemperatures = temperatures.stream().map(aLong -> Long.toString(aLong)).collect(Collectors.toList()); + EntityDataQuery query = new EntityDataQuery(filter, pageLink, entityFields, latestValues, null); - PageData data = findByQueryAndCheck(query, 25); - List loadedEntities = new ArrayList<>(data.getData()); - while (data.hasNext()) { - query = query.next(); - data = findByQuery(query); - loadedEntities.addAll(data.getData()); - } - Assert.assertEquals(25, loadedEntities.size()); + List loadedEntities = findByQueryAndCheckTelemetry(query, EntityKeyType.ATTRIBUTE, "temperature", deviceTemperatures); + loadedEntities.forEach(entity -> Assert.assertTrue(devices.stream().map(Device::getId).collect(Collectors.toSet()).contains(entity.getEntityId()))); - List loadedTemperatures = loadedEntities.stream().map(entityData -> - entityData.getLatest().get(EntityKeyType.ATTRIBUTE).get("temperature").getValue()).collect(Collectors.toList()); - List deviceTemperatures = temperatures.stream().map(aLong -> Long.toString(aLong)).collect(Collectors.toList()); - Assert.assertEquals(deviceTemperatures, loadedTemperatures); pageLink = new EntityDataPageLink(10, 0, null, sortOrder); KeyFilter highTemperatureFilter = new KeyFilter(); @@ -616,21 +581,8 @@ public class EntityServiceTest extends AbstractControllerTest { query = new EntityDataQuery(filter, pageLink, entityFields, latestValues, keyFilters); - data = findByQuery(query); - - loadedEntities = new ArrayList<>(data.getData()); - while (data.hasNext()) { - query = query.next(); - data = findByQuery(query); - loadedEntities.addAll(data.getData()); - } - Assert.assertEquals(highTemperatures.size(), loadedEntities.size()); - - List loadedHighTemperatures = loadedEntities.stream().map(entityData -> - entityData.getLatest().get(EntityKeyType.ATTRIBUTE).get("temperature").getValue()).collect(Collectors.toList()); List deviceHighTemperatures = highTemperatures.stream().map(aLong -> Long.toString(aLong)).collect(Collectors.toList()); - - Assert.assertEquals(deviceHighTemperatures, loadedHighTemperatures); + findByQueryAndCheckTelemetry(query, EntityKeyType.ATTRIBUTE, "temperature", deviceHighTemperatures); deviceService.deleteDevicesByTenantId(tenantId); } @@ -664,18 +616,9 @@ public class EntityServiceTest extends AbstractControllerTest { List latestValues = Collections.singletonList(new EntityKey(EntityKeyType.ATTRIBUTE, "consumption")); EntityDataQuery query = new EntityDataQuery(filter, pageLink, entityFields, latestValues, null); - PageData data = findByQueryAndCheck(query, 5); - List loadedEntities = new ArrayList<>(data.getData()); - while (data.hasNext()) { - query = query.next(); - data = findByQuery(query); - loadedEntities.addAll(data.getData()); - } - Assert.assertEquals(5, loadedEntities.size()); - List loadedTemperatures = loadedEntities.stream().map(entityData -> - entityData.getLatest().get(EntityKeyType.ATTRIBUTE).get("consumption").getValue()).collect(Collectors.toList()); + List deviceTemperatures = consumptions.stream().map(aLong -> Long.toString(aLong)).collect(Collectors.toList()); - Assert.assertEquals(deviceTemperatures, loadedTemperatures); + findByQueryAndCheckTelemetry(query, EntityKeyType.ATTRIBUTE, "consumption", deviceTemperatures); pageLink = new EntityDataPageLink(10, 0, null, sortOrder); KeyFilter highTemperatureFilter = new KeyFilter(); @@ -688,21 +631,8 @@ public class EntityServiceTest extends AbstractControllerTest { query = new EntityDataQuery(filter, pageLink, entityFields, latestValues, keyFilters); - data = findByQuery(query); - - loadedEntities = new ArrayList<>(data.getData()); - while (data.hasNext()) { - query = query.next(); - data = findByQuery(query); - loadedEntities.addAll(data.getData()); - } - Assert.assertEquals(highConsumptions.size(), loadedEntities.size()); - - List loadedHighTemperatures = loadedEntities.stream().map(entityData -> - entityData.getLatest().get(EntityKeyType.ATTRIBUTE).get("consumption").getValue()).collect(Collectors.toList()); List deviceHighTemperatures = highConsumptions.stream().map(aLong -> Long.toString(aLong)).collect(Collectors.toList()); - - Assert.assertEquals(deviceHighTemperatures, loadedHighTemperatures); + findByQueryAndCheckTelemetry(query, EntityKeyType.ATTRIBUTE, "consumption", deviceHighTemperatures); deviceService.deleteDevicesByTenantId(tenantId); } @@ -1659,89 +1589,48 @@ public class EntityServiceTest extends AbstractControllerTest { List keyFiltersNotEqualTemperature = Collections.singletonList(notEqualTemperatureFilter); //Greater Operation + List deviceTemperatures = greaterTemperatures.stream().map(aLong -> Long.toString(aLong)).collect(Collectors.toList()); EntityDataPageLink pageLink = new EntityDataPageLink(100, 0, null, sortOrder); EntityDataQuery query = new EntityDataQuery(filter, pageLink, entityFields, latestValues, keyFiltersGreaterTemperature); - PageData data = findByQueryAndCheck(query, greaterTemperatures.size()); - List loadedEntities = getLoadedEntities(data, query); - Assert.assertEquals(greaterTemperatures.size(), loadedEntities.size()); - List loadedTemperatures = loadedEntities.stream().map(entityData -> - entityData.getLatest().get(EntityKeyType.CLIENT_ATTRIBUTE).get("temperature").getValue()).collect(Collectors.toList()); - List deviceTemperatures = greaterTemperatures.stream().map(aLong -> Long.toString(aLong)).collect(Collectors.toList()); - - Assert.assertEquals(deviceTemperatures, loadedTemperatures); + findByQueryAndCheckTelemetry(query, EntityKeyType.CLIENT_ATTRIBUTE, "temperature", deviceTemperatures); //Greater or equal Operation + deviceTemperatures = greaterOrEqualTemperatures.stream().map(aLong -> Long.toString(aLong)).collect(Collectors.toList()); pageLink = new EntityDataPageLink(100, 0, null, sortOrder); query = new EntityDataQuery(filter, pageLink, entityFields, latestValues, keyFiltersGreaterOrEqualTemperature); - data = findByQueryAndCheck(query, greaterOrEqualTemperatures.size()); - loadedEntities = getLoadedEntities(data, query); - Assert.assertEquals(greaterOrEqualTemperatures.size(), loadedEntities.size()); - loadedTemperatures = loadedEntities.stream().map(entityData -> - entityData.getLatest().get(EntityKeyType.CLIENT_ATTRIBUTE).get("temperature").getValue()).collect(Collectors.toList()); - deviceTemperatures = greaterOrEqualTemperatures.stream().map(aLong -> Long.toString(aLong)).collect(Collectors.toList()); - - Assert.assertEquals(deviceTemperatures, loadedTemperatures); + findByQueryAndCheckTelemetry(query, EntityKeyType.CLIENT_ATTRIBUTE, "temperature", deviceTemperatures); //Less Operation + deviceTemperatures = lessTemperatures.stream().map(aLong -> Long.toString(aLong)).collect(Collectors.toList()); pageLink = new EntityDataPageLink(100, 0, null, sortOrder); query = new EntityDataQuery(filter, pageLink, entityFields, latestValues, keyFiltersLessTemperature); - data = findByQueryAndCheck(query, lessTemperatures.size()); - loadedEntities = getLoadedEntities(data, query); - Assert.assertEquals(lessTemperatures.size(), loadedEntities.size()); - - loadedTemperatures = loadedEntities.stream().map(entityData -> - entityData.getLatest().get(EntityKeyType.CLIENT_ATTRIBUTE).get("temperature").getValue()).collect(Collectors.toList()); - deviceTemperatures = lessTemperatures.stream().map(aLong -> Long.toString(aLong)).collect(Collectors.toList()); - - Assert.assertEquals(deviceTemperatures, loadedTemperatures); + findByQueryAndCheckTelemetry(query, EntityKeyType.CLIENT_ATTRIBUTE, "temperature", deviceTemperatures); //Less or equal Operation + deviceTemperatures = lessOrEqualTemperatures.stream().map(aLong -> Long.toString(aLong)).collect(Collectors.toList()); pageLink = new EntityDataPageLink(100, 0, null, sortOrder); query = new EntityDataQuery(filter, pageLink, entityFields, latestValues, keyFiltersLessOrEqualTemperature); - data = findByQueryAndCheck(query, lessOrEqualTemperatures.size()); - loadedEntities = getLoadedEntities(data, query); - Assert.assertEquals(lessOrEqualTemperatures.size(), loadedEntities.size()); - - loadedTemperatures = loadedEntities.stream().map(entityData -> - entityData.getLatest().get(EntityKeyType.CLIENT_ATTRIBUTE).get("temperature").getValue()).collect(Collectors.toList()); - deviceTemperatures = lessOrEqualTemperatures.stream().map(aLong -> Long.toString(aLong)).collect(Collectors.toList()); - - Assert.assertEquals(deviceTemperatures, loadedTemperatures); + findByQueryAndCheckTelemetry(query, EntityKeyType.CLIENT_ATTRIBUTE, "temperature", deviceTemperatures); //Equal Operation + deviceTemperatures = equalTemperatures.stream().map(aLong -> Long.toString(aLong)).collect(Collectors.toList()); pageLink = new EntityDataPageLink(100, 0, null, sortOrder); query = new EntityDataQuery(filter, pageLink, entityFields, latestValues, keyFiltersEqualTemperature); - data = findByQueryAndCheck(query, equalTemperatures.size()); - loadedEntities = getLoadedEntities(data, query); - Assert.assertEquals(equalTemperatures.size(), loadedEntities.size()); - - loadedTemperatures = loadedEntities.stream().map(entityData -> - entityData.getLatest().get(EntityKeyType.CLIENT_ATTRIBUTE).get("temperature").getValue()).collect(Collectors.toList()); - deviceTemperatures = equalTemperatures.stream().map(aLong -> Long.toString(aLong)).collect(Collectors.toList()); - - Assert.assertEquals(deviceTemperatures, loadedTemperatures); + findByQueryAndCheckTelemetry(query, EntityKeyType.CLIENT_ATTRIBUTE, "temperature", deviceTemperatures); //Not equal Operation + deviceTemperatures = notEqualTemperatures.stream().map(aLong -> Long.toString(aLong)).collect(Collectors.toList()); pageLink = new EntityDataPageLink(100, 0, null, sortOrder); query = new EntityDataQuery(filter, pageLink, entityFields, latestValues, keyFiltersNotEqualTemperature); - data = findByQueryAndCheck(query, notEqualTemperatures.size()); - loadedEntities = getLoadedEntities(data, query); - Assert.assertEquals(notEqualTemperatures.size(), loadedEntities.size()); - - loadedTemperatures = loadedEntities.stream().map(entityData -> - entityData.getLatest().get(EntityKeyType.CLIENT_ATTRIBUTE).get("temperature").getValue()).collect(Collectors.toList()); - deviceTemperatures = notEqualTemperatures.stream().map(aLong -> Long.toString(aLong)).collect(Collectors.toList()); - - Assert.assertEquals(deviceTemperatures, loadedTemperatures); - + findByQueryAndCheckTelemetry(query, EntityKeyType.CLIENT_ATTRIBUTE, "temperature", deviceTemperatures); deviceService.deleteDevicesByTenantId(tenantId); } @@ -1786,24 +1675,10 @@ public class EntityServiceTest extends AbstractControllerTest { List entityFields = Collections.singletonList(new EntityKey(EntityKeyType.ENTITY_FIELD, "name")); List latestValues = Collections.singletonList(new EntityKey(EntityKeyType.TIME_SERIES, "temperature")); - EntityDataQuery query = new EntityDataQuery(filter, pageLink, entityFields, latestValues, null); - PageData data = findByQueryAndCheck(query, 67); - - List loadedEntities = new ArrayList<>(data.getData()); - while (data.hasNext()) { - query = query.next(); - data = findByQuery(query); - loadedEntities.addAll(data.getData()); - } - Assert.assertEquals(67, loadedEntities.size()); - List loadedTemperatures = new ArrayList<>(); - for (Device device : devices) { - loadedTemperatures.add(loadedEntities.stream().filter(entityData -> entityData.getEntityId().equals(device.getId())).findFirst().orElse(null) - .getLatest().get(EntityKeyType.TIME_SERIES).get("temperature").getValue()); - } List deviceTemperatures = temperatures.stream().map(aDouble -> Double.toString(aDouble)).collect(Collectors.toList()); - Assert.assertEquals(deviceTemperatures, loadedTemperatures); + EntityDataQuery query = new EntityDataQuery(filter, pageLink, entityFields, latestValues, null); + findByQueryAndCheckTelemetry(query, EntityKeyType.TIME_SERIES, "temperature", deviceTemperatures); pageLink = new EntityDataPageLink(10, 0, null, sortOrder); KeyFilter highTemperatureFilter = new KeyFilter(); @@ -1814,22 +1689,10 @@ public class EntityServiceTest extends AbstractControllerTest { highTemperatureFilter.setPredicate(predicate); List keyFilters = Collections.singletonList(highTemperatureFilter); - query = new EntityDataQuery(filter, pageLink, entityFields, latestValues, keyFilters); - data = findByQueryAndCheck(query, highTemperatures.size()); - - loadedEntities = new ArrayList<>(data.getData()); - while (data.hasNext()) { - query = query.next(); - data = findByQuery(query); - loadedEntities.addAll(data.getData()); - } - Assert.assertEquals(highTemperatures.size(), loadedEntities.size()); - - List loadedHighTemperatures = loadedEntities.stream().map(entityData -> - entityData.getLatest().get(EntityKeyType.TIME_SERIES).get("temperature").getValue()).collect(Collectors.toList()); List deviceHighTemperatures = highTemperatures.stream().map(aDouble -> Double.toString(aDouble)).collect(Collectors.toList()); - Assert.assertEquals(deviceHighTemperatures, loadedHighTemperatures); + query = new EntityDataQuery(filter, pageLink, entityFields, latestValues, keyFilters); + findByQueryAndCheckTelemetry(query, EntityKeyType.TIME_SERIES, "temperature", deviceHighTemperatures); deviceService.deleteDevicesByTenantId(tenantId); } @@ -2509,13 +2372,14 @@ public class EntityServiceTest extends AbstractControllerTest { return result; } - protected List findByQueryAndCheckTelemetry(EntityDataQuery query, EntityKeyType entityKeyType, String key, List expectedTelemetry) { - List entitiesTelemetry = findEntitiesTelemetry(query, entityKeyType, key, expectedTelemetry); + protected List findByQueryAndCheckTelemetry(EntityDataQuery query, EntityKeyType entityKeyType, String key, List expectedTelemetry) { + List loadedEntities = findEntitiesTelemetry(query, entityKeyType, key, expectedTelemetry); + List entitiesTelemetry = loadedEntities.stream().map(entityData -> entityData.getLatest().get(entityKeyType).get(key).getValue()).toList(); assertThat(entitiesTelemetry).containsExactlyInAnyOrderElementsOf(expectedTelemetry); - return entitiesTelemetry; + return loadedEntities; } - protected List findEntitiesTelemetry(EntityDataQuery query, EntityKeyType entityKeyType, String key, List expectedTelemetries) { + protected List findEntitiesTelemetry(EntityDataQuery query, EntityKeyType entityKeyType, String key, List expectedTelemetries) { PageData data = findByQueryAndCheck(query, expectedTelemetries.size()); List loadedEntities = new ArrayList<>(data.getData()); while (data.hasNext()) { @@ -2523,7 +2387,7 @@ public class EntityServiceTest extends AbstractControllerTest { data = findByQuery(query); loadedEntities.addAll(data.getData()); } - return loadedEntities.stream().map(entityData -> entityData.getLatest().get(entityKeyType).get(key).getValue()).toList(); + return loadedEntities; } protected long countByQuery(CustomerId customerId, EntityCountQuery query) { From 8d4c90b389aeb697987928262f0c0dc265115018 Mon Sep 17 00:00:00 2001 From: mpetrov Date: Tue, 4 Mar 2025 12:29:23 +0200 Subject: [PATCH 175/345] Changed device to deviceName --- .../src/app/modules/home/components/widget/widget.component.ts | 2 +- ui-ngx/src/app/shared/models/widget.models.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/ui-ngx/src/app/modules/home/components/widget/widget.component.ts b/ui-ngx/src/app/modules/home/components/widget/widget.component.ts index bb41315c5c..ca11c2f4e0 100644 --- a/ui-ngx/src/app/modules/home/components/widget/widget.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/widget.component.ts @@ -1294,7 +1294,7 @@ export class WidgetComponent extends PageComponent implements OnInit, OnChanges, } break; case WidgetMobileActionType.provisionDevice: - const deviceName = actionResult.device.name; + const deviceName = actionResult.deviceName; if (isNotEmptyTbFunction(mobileAction.handleProvisionSuccessFunction)) { compileTbFunction(this.http, mobileAction.handleProvisionSuccessFunction, 'deviceName', '$event', 'widgetContext', 'entityId', 'entityName', 'additionalParams', 'entityLabel').subscribe( diff --git a/ui-ngx/src/app/shared/models/widget.models.ts b/ui-ngx/src/app/shared/models/widget.models.ts index db88ba0283..132c82cfd1 100644 --- a/ui-ngx/src/app/shared/models/widget.models.ts +++ b/ui-ngx/src/app/shared/models/widget.models.ts @@ -639,7 +639,7 @@ export interface MobileLocationResult { } export interface MobileDeviceProvisioningResult { - device: Device; + deviceName: string; } export type MobileActionResult = MobileLaunchResult & From 49e627a8a66edeb398572465408ae9db7abec0bc Mon Sep 17 00:00:00 2001 From: mpetrov Date: Tue, 4 Mar 2025 13:07:52 +0200 Subject: [PATCH 176/345] Fixed highlight bug --- .../scada-symbol/scada-symbol-editor.models.ts | 5 ++--- ui-ngx/src/app/shared/models/ace/ace.models.ts | 2 ++ .../app/shared/models/calculated-field.models.ts | 14 +++++++++++++- 3 files changed, 17 insertions(+), 4 deletions(-) diff --git a/ui-ngx/src/app/modules/home/pages/scada-symbol/scada-symbol-editor.models.ts b/ui-ngx/src/app/modules/home/pages/scada-symbol/scada-symbol-editor.models.ts index 13b55d4f67..cc5a9baf4f 100644 --- a/ui-ngx/src/app/modules/home/pages/scada-symbol/scada-symbol-editor.models.ts +++ b/ui-ngx/src/app/modules/home/pages/scada-symbol/scada-symbol-editor.models.ts @@ -35,7 +35,8 @@ import { AceHighlightRule, AceHighlightRules, dotOperatorHighlightRule, - endGroupHighlightRule + endGroupHighlightRule, + identifierRe } from '@shared/models/ace/ace.models'; import { HelpLinks, ValueType } from '@shared/models/constants'; import { formPropertyCompletions } from '@shared/models/dynamic-form.models'; @@ -924,8 +925,6 @@ export class ScadaSymbolElement { } -const identifierRe = /[a-zA-Z$_\u00a1-\uffff][a-zA-Z\d$_\u00a1-\uffff]*/; - const scadaSymbolCtxObjectHighlightRule: AceHighlightRule = { token: 'tb.scada-symbol-ctx', regex: /\bctx\b/, diff --git a/ui-ngx/src/app/shared/models/ace/ace.models.ts b/ui-ngx/src/app/shared/models/ace/ace.models.ts index 5287886b84..9533f80d51 100644 --- a/ui-ngx/src/app/shared/models/ace/ace.models.ts +++ b/ui-ngx/src/app/shared/models/ace/ace.models.ts @@ -376,5 +376,7 @@ export const endGroupHighlightRule: AceHighlightRule = { next: 'no_regex' }; +export const identifierRe = /[a-zA-Z$_\u00a1-\uffff][a-zA-Z\d$_\u00a1-\uffff]*/; + diff --git a/ui-ngx/src/app/shared/models/calculated-field.models.ts b/ui-ngx/src/app/shared/models/calculated-field.models.ts index f6d7aaaa1e..fef90613a8 100644 --- a/ui-ngx/src/app/shared/models/calculated-field.models.ts +++ b/ui-ngx/src/app/shared/models/calculated-field.models.ts @@ -32,7 +32,8 @@ import { AceHighlightRule, AceHighlightRules, dotOperatorHighlightRule, - endGroupHighlightRule + endGroupHighlightRule, + identifierRe } from '@shared/models/ace/ace.models'; export interface CalculatedField extends Omit, 'label'>, HasVersion, HasEntityDebugSettings, HasTenantId, ExportableEntity { @@ -480,6 +481,17 @@ export const getCalculatedFieldArgumentsHighlights = ( ? 'calculatedFieldRollingArgumentValue' : 'calculatedFieldSingleArgumentValue' })), + no_regex: [ + { + token: 'tb.identifier', + regex: identifierRe, + }, + { + token: 'tb.paren.lparen', + regex: '\\(', + next: 'start' + } + ], ...calculatedFieldSingleArgumentValueHighlightRules, ...calculatedFieldRollingArgumentValueHighlightRules, ...calculatedFieldTimeWindowArgumentValueHighlightRules From 933d77427ce0eb12967d24f94be279e26fbd5eed Mon Sep 17 00:00:00 2001 From: Andrii Shvaika Date: Tue, 4 Mar 2025 13:13:09 +0200 Subject: [PATCH 177/345] Merge function --- .../cf/ctx/state/TsRollingArgumentEntry.java | 2 +- .../script/api/tbel/TbTimeWindow.java | 1 - .../thingsboard/script/api/tbel/TbUtils.java | 81 --------------- .../script/api/tbel/TbelCfTsRollingArg.java | 98 ++++++++++++++++++- .../script/api/tbel/TbUtilsTest.java | 69 ------------- .../api/tbel/TbelCfTsRollingArgTest.java | 88 ++++++++++++++++- .../shared/models/calculated-field.models.ts | 89 +++++++++++++---- 7 files changed, 254 insertions(+), 174 deletions(-) diff --git a/application/src/main/java/org/thingsboard/server/service/cf/ctx/state/TsRollingArgumentEntry.java b/application/src/main/java/org/thingsboard/server/service/cf/ctx/state/TsRollingArgumentEntry.java index 1114d993a3..b5a680a072 100644 --- a/application/src/main/java/org/thingsboard/server/service/cf/ctx/state/TsRollingArgumentEntry.java +++ b/application/src/main/java/org/thingsboard/server/service/cf/ctx/state/TsRollingArgumentEntry.java @@ -89,7 +89,7 @@ public class TsRollingArgumentEntry implements ArgumentEntry { for (var e : tsRecords.entrySet()) { values.add(new TbelCfTsDoubleVal(e.getKey(), e.getValue())); } - return new TbelCfTsRollingArg(limit, timeWindow, values); + return new TbelCfTsRollingArg(timeWindow, values); } @Override diff --git a/common/script/script-api/src/main/java/org/thingsboard/script/api/tbel/TbTimeWindow.java b/common/script/script-api/src/main/java/org/thingsboard/script/api/tbel/TbTimeWindow.java index b430b23757..1761ef6d0a 100644 --- a/common/script/script-api/src/main/java/org/thingsboard/script/api/tbel/TbTimeWindow.java +++ b/common/script/script-api/src/main/java/org/thingsboard/script/api/tbel/TbTimeWindow.java @@ -28,7 +28,6 @@ public class TbTimeWindow implements TbelCfObject { private long startTs; private long endTs; - private int limit; @Override public long memorySize() { diff --git a/common/script/script-api/src/main/java/org/thingsboard/script/api/tbel/TbUtils.java b/common/script/script-api/src/main/java/org/thingsboard/script/api/tbel/TbUtils.java index e6a0b8d043..a43cc30f23 100644 --- a/common/script/script-api/src/main/java/org/thingsboard/script/api/tbel/TbUtils.java +++ b/common/script/script-api/src/main/java/org/thingsboard/script/api/tbel/TbUtils.java @@ -368,16 +368,6 @@ public class TbUtils { byte[].class, int.class))); parserConfig.addImport("parseBinaryArrayToInt", new MethodStub(TbUtils.class.getMethod("parseBinaryArrayToInt", byte[].class, int.class, int.class))); - parserConfig.addImport("merge", new MethodStub(TbUtils.class.getMethod("merge", - TbelCfTsRollingArg.class, TbelCfTsRollingArg.class))); - parserConfig.addImport("merge", new MethodStub(TbUtils.class.getMethod("merge", - TbelCfTsRollingArg.class, TbelCfTsRollingArg.class, TbTimeWindow.class))); - parserConfig.addImport("merge", new MethodStub(TbUtils.class.getMethod("merge", - TbelCfTsRollingArg.class, TbelCfTsRollingArg.class, TbTimeWindow.class, Map.class))); - parserConfig.addImport("merge", new MethodStub(TbUtils.class.getMethod("merge", - List.class, TbTimeWindow.class))); - parserConfig.addImport("merge", new MethodStub(TbUtils.class.getMethod("merge", - List.class, TbTimeWindow.class, Map.class))); } public static String btoa(String input) { @@ -1518,76 +1508,5 @@ public class TbUtils { return hex; } - public static TbelCfTsRollingData merge(TbelCfTsRollingArg a, TbelCfTsRollingArg b) { - return merge(Arrays.asList(a, b), null, null); - } - - public static TbelCfTsRollingData merge(TbelCfTsRollingArg a, TbelCfTsRollingArg b, TbTimeWindow timeWindow) { - return merge(Arrays.asList(a, b), timeWindow, null); - } - - public static TbelCfTsRollingData merge(TbelCfTsRollingArg a, TbelCfTsRollingArg b, TbTimeWindow timeWindow, Map settings) { - return merge(Arrays.asList(a, b), timeWindow, settings); - } - - public static TbelCfTsRollingData merge(List args, TbTimeWindow timeWindow) { - return merge(args, timeWindow, null); - } - - public static TbelCfTsRollingData merge(List args, TbTimeWindow timeWindow, Map settings) { - boolean ignoreNaN = true; - if (settings != null && settings.containsKey("ignoreNaN")) { - ignoreNaN = Boolean.parseBoolean(settings.get("ignoreNaN").toString()); - } - - TreeSet allTimestamps = new TreeSet<>(); - long startTs = Long.MAX_VALUE; - long endTs = Long.MIN_VALUE; - for (TbelCfTsRollingArg arg : args) { - for (TbelCfTsDoubleVal val : arg.getValues()) { - allTimestamps.add(val.getTs()); - } - startTs = Math.min(startTs, arg.getTimeWindow().getStartTs()); - endTs = Math.max(endTs, arg.getTimeWindow().getEndTs()); - } - - List data = new ArrayList<>(); - - int[] lastIndex = new int[args.size()]; - double[] result = new double[args.size()]; - Arrays.fill(result, Double.NaN); - - var tw = timeWindow != null ? timeWindow : new TbTimeWindow(startTs, endTs, allTimestamps.size()); - - for (long ts : allTimestamps) { - for (int i = 0; i < args.size(); i++) { - var arg = args.get(i); - var values = arg.getValues(); - while (lastIndex[i] < values.size() && values.get(lastIndex[i]).getTs() <= ts) { - result[i] = values.get(lastIndex[i]).getValue(); - lastIndex[i]++; - } - } - if (tw.matches(ts)) { - if (ignoreNaN) { - boolean skip = false; - for (int i = 0; i < args.size(); i++) { - if (Double.isNaN(result[i])) { - skip = true; - break; - } - } - if (!skip) { - data.add(new TbelCfTsMultiDoubleVal(ts, Arrays.copyOf(result, result.length))); - } - } else { - data.add(new TbelCfTsMultiDoubleVal(ts, Arrays.copyOf(result, result.length))); - } - } - } - - return new TbelCfTsRollingData(tw, data); - } - } diff --git a/common/script/script-api/src/main/java/org/thingsboard/script/api/tbel/TbelCfTsRollingArg.java b/common/script/script-api/src/main/java/org/thingsboard/script/api/tbel/TbelCfTsRollingArg.java index 74c9d77454..3b5a7ac9bd 100644 --- a/common/script/script-api/src/main/java/org/thingsboard/script/api/tbel/TbelCfTsRollingArg.java +++ b/common/script/script-api/src/main/java/org/thingsboard/script/api/tbel/TbelCfTsRollingArg.java @@ -19,11 +19,15 @@ import com.fasterxml.jackson.annotation.JsonCreator; import com.fasterxml.jackson.annotation.JsonIgnore; import com.fasterxml.jackson.annotation.JsonProperty; import lombok.Getter; +import org.thingsboard.common.util.JacksonUtil; import java.util.ArrayList; +import java.util.Arrays; import java.util.Collections; import java.util.Iterator; import java.util.List; +import java.util.Map; +import java.util.TreeSet; import java.util.function.Consumer; import static org.thingsboard.script.api.tbel.TbelCfTsDoubleVal.OBJ_SIZE; @@ -44,9 +48,9 @@ public class TbelCfTsRollingArg implements TbelCfArg, Iterable values) { + public TbelCfTsRollingArg(long timeWindow, List values) { long ts = System.currentTimeMillis(); - this.timeWindow = new TbTimeWindow(ts - timeWindow, ts, limit); + this.timeWindow = new TbTimeWindow(ts - timeWindow, ts); this.values = Collections.unmodifiableList(values); } @@ -104,6 +108,14 @@ public class TbelCfTsRollingArg implements TbelCfArg, Iterable settings) { + return mergeAll(Collections.singletonList(other), settings); + } + + public TbelCfTsRollingData mergeAll(List others) { + return mergeAll(others, null); + } + + public TbelCfTsRollingData mergeAll(List others, Map settings) { + List args = new ArrayList<>(others.size() + 1); + args.add(this); + args.addAll(others); + + boolean ignoreNaN = true; + if (settings != null && settings.containsKey("ignoreNaN")) { + ignoreNaN = Boolean.parseBoolean(settings.get("ignoreNaN").toString()); + } + + TbTimeWindow timeWindow = null; + if (settings != null && settings.containsKey("timeWindow")) { + var twVar = settings.get("timeWindow"); + if (twVar instanceof TbTimeWindow) { + timeWindow = (TbTimeWindow) settings.get("timeWindow"); + } else if (twVar instanceof Map twMap) { + timeWindow = new TbTimeWindow(Long.valueOf(twMap.get("startTs").toString()), Long.valueOf(twMap.get("endTs").toString())); + } else { + timeWindow = JacksonUtil.fromString(settings.get("timeWindow").toString(), TbTimeWindow.class); + } + } + + TreeSet allTimestamps = new TreeSet<>(); + long startTs = Long.MAX_VALUE; + long endTs = Long.MIN_VALUE; + for (TbelCfTsRollingArg arg : args) { + for (TbelCfTsDoubleVal val : arg.getValues()) { + allTimestamps.add(val.getTs()); + } + startTs = Math.min(startTs, arg.getTimeWindow().getStartTs()); + endTs = Math.max(endTs, arg.getTimeWindow().getEndTs()); + } + + List data = new ArrayList<>(); + + int[] lastIndex = new int[args.size()]; + double[] result = new double[args.size()]; + Arrays.fill(result, Double.NaN); + + for (long ts : allTimestamps) { + for (int i = 0; i < args.size(); i++) { + var arg = args.get(i); + var values = arg.getValues(); + while (lastIndex[i] < values.size() && values.get(lastIndex[i]).getTs() <= ts) { + result[i] = values.get(lastIndex[i]).getValue(); + lastIndex[i]++; + } + } + if (timeWindow == null || timeWindow.matches(ts)) { + if (ignoreNaN) { + boolean skip = false; + for (int i = 0; i < args.size(); i++) { + if (Double.isNaN(result[i])) { + skip = true; + break; + } + } + if (!skip) { + data.add(new TbelCfTsMultiDoubleVal(ts, Arrays.copyOf(result, result.length))); + } + } else { + data.add(new TbelCfTsMultiDoubleVal(ts, Arrays.copyOf(result, result.length))); + } + } + } + + return new TbelCfTsRollingData(timeWindow != null ? timeWindow : new TbTimeWindow(startTs, endTs), data); + } + + @JsonIgnore public int getSize() { return values.size(); diff --git a/common/script/script-api/src/test/java/org/thingsboard/script/api/tbel/TbUtilsTest.java b/common/script/script-api/src/test/java/org/thingsboard/script/api/tbel/TbUtilsTest.java index f19a323068..71c7a8b78a 100644 --- a/common/script/script-api/src/test/java/org/thingsboard/script/api/tbel/TbUtilsTest.java +++ b/common/script/script-api/src/test/java/org/thingsboard/script/api/tbel/TbUtilsTest.java @@ -1137,75 +1137,6 @@ public class TbUtilsTest { Assertions.assertEquals(expected, actual); } - @Test - public void merge_two_rolling_args_ts_match_test() { - TbTimeWindow tw = new TbTimeWindow(0, 60000, 1000); - TbelCfTsRollingArg arg1 = new TbelCfTsRollingArg(tw, Arrays.asList(new TbelCfTsDoubleVal(1000, 1), new TbelCfTsDoubleVal(5000, 2), new TbelCfTsDoubleVal(15000, 3))); - TbelCfTsRollingArg arg2 = new TbelCfTsRollingArg(tw, Arrays.asList(new TbelCfTsDoubleVal(1000, 11), new TbelCfTsDoubleVal(5000, 12), new TbelCfTsDoubleVal(15000, 13))); - - var result = TbUtils.merge(arg1, arg2); - Assertions.assertEquals(3, result.getSize()); - Assertions.assertNotNull(result.getValues()); - Assertions.assertNotNull(result.getValues().get(0)); - Assertions.assertEquals(1000L, result.getValues().get(0).getTs()); - Assertions.assertEquals(1, result.getValues().get(0).getValues()[0]); - Assertions.assertEquals(11, result.getValues().get(0).getValues()[1]); - } - - @Test - public void merge_two_rolling_args_with_timewindow_test() { - TbTimeWindow tw = new TbTimeWindow(0, 60000, 1000); - TbelCfTsRollingArg arg1 = new TbelCfTsRollingArg(tw, Arrays.asList(new TbelCfTsDoubleVal(1000, 1), new TbelCfTsDoubleVal(5000, 2), new TbelCfTsDoubleVal(15000, 3))); - TbelCfTsRollingArg arg2 = new TbelCfTsRollingArg(tw, Arrays.asList(new TbelCfTsDoubleVal(1000, 11), new TbelCfTsDoubleVal(5000, 12), new TbelCfTsDoubleVal(15000, 13))); - - var result = TbUtils.merge(arg1, arg2, new TbTimeWindow(0, 10000, 1000)); - Assertions.assertEquals(2, result.getSize()); - Assertions.assertNotNull(result.getValues()); - Assertions.assertNotNull(result.getValues().get(0)); - Assertions.assertEquals(1000L, result.getValues().get(0).getTs()); - Assertions.assertEquals(1, result.getValues().get(0).getValues()[0]); - Assertions.assertEquals(11, result.getValues().get(0).getValues()[1]); - } - - @Test - public void merge_two_rolling_args_ts_mismatch_default_test() { - TbTimeWindow tw = new TbTimeWindow(0, 60000, 1000); - TbelCfTsRollingArg arg1 = new TbelCfTsRollingArg(tw, Arrays.asList(new TbelCfTsDoubleVal(100, 1), new TbelCfTsDoubleVal(5000, 2), new TbelCfTsDoubleVal(15000, 3))); - TbelCfTsRollingArg arg2 = new TbelCfTsRollingArg(tw, Arrays.asList(new TbelCfTsDoubleVal(200, 11), new TbelCfTsDoubleVal(5000, 12), new TbelCfTsDoubleVal(15000, 13))); - - var result = TbUtils.merge(arg1, arg2); - Assertions.assertEquals(3, result.getSize()); - Assertions.assertNotNull(result.getValues()); - - TbelCfTsMultiDoubleVal item0 = result.getValues().get(0); - Assertions.assertNotNull(item0); - Assertions.assertEquals(200L, item0.getTs()); - Assertions.assertEquals(1, item0.getValues()[0]); - Assertions.assertEquals(11, item0.getValues()[1]); - } - - @Test - public void merge_two_rolling_args_ts_mismatch_ignore_nan_disabled_test() { - TbTimeWindow tw = new TbTimeWindow(0, 60000, 1000); - TbelCfTsRollingArg arg1 = new TbelCfTsRollingArg(tw, Arrays.asList(new TbelCfTsDoubleVal(100, 1), new TbelCfTsDoubleVal(5000, 2), new TbelCfTsDoubleVal(15000, 3))); - TbelCfTsRollingArg arg2 = new TbelCfTsRollingArg(tw, Arrays.asList(new TbelCfTsDoubleVal(200, 11), new TbelCfTsDoubleVal(5000, 12), new TbelCfTsDoubleVal(15000, 13))); - - var result = TbUtils.merge(Arrays.asList(arg1, arg2), new TbTimeWindow(0, 60000, 1000), Collections.singletonMap("ignoreNaN", false)); - Assertions.assertEquals(4, result.getSize()); - Assertions.assertNotNull(result.getValues()); - - TbelCfTsMultiDoubleVal item0 = result.getValues().get(0); - Assertions.assertNotNull(item0); - Assertions.assertEquals(100L, item0.getTs()); - Assertions.assertEquals(1, item0.getValues()[0]); - Assertions.assertEquals(Double.NaN, item0.getValues()[1]); - - TbelCfTsMultiDoubleVal item1 = result.getValues().get(1); - Assertions.assertEquals(200L, item1.getTs()); - Assertions.assertEquals(1, item1.getValues()[0]); - Assertions.assertEquals(11, item1.getValues()[1]); - } - private static List toList(byte[] data) { List result = new ArrayList<>(data.length); for (Byte b : data) { diff --git a/common/script/script-api/src/test/java/org/thingsboard/script/api/tbel/TbelCfTsRollingArgTest.java b/common/script/script-api/src/test/java/org/thingsboard/script/api/tbel/TbelCfTsRollingArgTest.java index 327b477b6f..69eba2fab2 100644 --- a/common/script/script-api/src/test/java/org/thingsboard/script/api/tbel/TbelCfTsRollingArgTest.java +++ b/common/script/script-api/src/test/java/org/thingsboard/script/api/tbel/TbelCfTsRollingArgTest.java @@ -15,10 +15,15 @@ */ package org.thingsboard.script.api.tbel; +import org.junit.jupiter.api.Assertions; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; +import org.thingsboard.common.util.JacksonUtil; +import java.util.Arrays; +import java.util.Collections; import java.util.List; +import java.util.Map; import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatThrownBy; @@ -33,7 +38,7 @@ public class TbelCfTsRollingArgTest { @BeforeEach void setUp() { rollingArg = new TbelCfTsRollingArg( - new TbTimeWindow(ts - 30000, ts - 10, 10), + new TbTimeWindow(ts - 30000, ts - 10), List.of( new TbelCfTsDoubleVal(ts - 10, Double.NaN), new TbelCfTsDoubleVal(ts - 20, 2.0), @@ -98,7 +103,7 @@ public class TbelCfTsRollingArgTest { void testFirstAndLastWhenOnlyNaNAndIgnoreNaNIsFalse() { assertThat(rollingArg.first()).isEqualTo(2.0); rollingArg = new TbelCfTsRollingArg( - new TbTimeWindow(ts - 30000, ts - 10, 10), + new TbTimeWindow(ts - 30000, ts - 10), List.of( new TbelCfTsDoubleVal(ts - 10, Double.NaN), new TbelCfTsDoubleVal(ts - 40, Double.NaN), @@ -117,7 +122,7 @@ public class TbelCfTsRollingArgTest { @Test void testEmptyValues() { - rollingArg = new TbelCfTsRollingArg(new TbTimeWindow(0, 10, 10), List.of()); + rollingArg = new TbelCfTsRollingArg(new TbTimeWindow(0, 10), List.of()); assertThatThrownBy(rollingArg::sum).isInstanceOf(IllegalArgumentException.class).hasMessage("Rolling argument values are empty."); assertThatThrownBy(rollingArg::max).isInstanceOf(IllegalArgumentException.class).hasMessage("Rolling argument values are empty."); assertThatThrownBy(rollingArg::min).isInstanceOf(IllegalArgumentException.class).hasMessage("Rolling argument values are empty."); @@ -128,4 +133,81 @@ public class TbelCfTsRollingArgTest { assertThatThrownBy(rollingArg::last).isInstanceOf(IllegalArgumentException.class).hasMessage("Rolling argument values are empty."); } + @Test + public void merge_two_rolling_args_ts_match_test() { + TbTimeWindow tw = new TbTimeWindow(0, 60000); + TbelCfTsRollingArg arg1 = new TbelCfTsRollingArg(tw, Arrays.asList(new TbelCfTsDoubleVal(1000, 1), new TbelCfTsDoubleVal(5000, 2), new TbelCfTsDoubleVal(15000, 3))); + TbelCfTsRollingArg arg2 = new TbelCfTsRollingArg(tw, Arrays.asList(new TbelCfTsDoubleVal(1000, 11), new TbelCfTsDoubleVal(5000, 12), new TbelCfTsDoubleVal(15000, 13))); + + var result = arg1.merge(arg2); + Assertions.assertEquals(3, result.getSize()); + Assertions.assertNotNull(result.getValues()); + Assertions.assertNotNull(result.getValues().get(0)); + Assertions.assertEquals(1000L, result.getValues().get(0).getTs()); + Assertions.assertEquals(1, result.getValues().get(0).getValues()[0]); + Assertions.assertEquals(11, result.getValues().get(0).getValues()[1]); + } + + @Test + public void merge_two_rolling_args_with_timewindow_test() { + TbTimeWindow tw = new TbTimeWindow(0, 60000); + TbelCfTsRollingArg arg1 = new TbelCfTsRollingArg(tw, Arrays.asList(new TbelCfTsDoubleVal(1000, 1), new TbelCfTsDoubleVal(5000, 2), new TbelCfTsDoubleVal(15000, 3))); + TbelCfTsRollingArg arg2 = new TbelCfTsRollingArg(tw, Arrays.asList(new TbelCfTsDoubleVal(1000, 11), new TbelCfTsDoubleVal(5000, 12), new TbelCfTsDoubleVal(15000, 13))); + + var result = arg1.merge(arg2, Collections.singletonMap("timeWindow", new TbTimeWindow(0, 10000))); + Assertions.assertEquals(2, result.getSize()); + Assertions.assertNotNull(result.getValues()); + Assertions.assertNotNull(result.getValues().get(0)); + Assertions.assertEquals(1000L, result.getValues().get(0).getTs()); + Assertions.assertEquals(1, result.getValues().get(0).getValues()[0]); + Assertions.assertEquals(11, result.getValues().get(0).getValues()[1]); + + result = arg1.merge(arg2, Collections.singletonMap("timeWindow", Map.of("startTs", 0L, "endTs", 10000))); + Assertions.assertEquals(2, result.getSize()); + Assertions.assertNotNull(result.getValues()); + Assertions.assertNotNull(result.getValues().get(0)); + Assertions.assertEquals(1000L, result.getValues().get(0).getTs()); + Assertions.assertEquals(1, result.getValues().get(0).getValues()[0]); + Assertions.assertEquals(11, result.getValues().get(0).getValues()[1]); + } + + @Test + public void merge_two_rolling_args_ts_mismatch_default_test() { + TbTimeWindow tw = new TbTimeWindow(0, 60000); + TbelCfTsRollingArg arg1 = new TbelCfTsRollingArg(tw, Arrays.asList(new TbelCfTsDoubleVal(100, 1), new TbelCfTsDoubleVal(5000, 2), new TbelCfTsDoubleVal(15000, 3))); + TbelCfTsRollingArg arg2 = new TbelCfTsRollingArg(tw, Arrays.asList(new TbelCfTsDoubleVal(200, 11), new TbelCfTsDoubleVal(5000, 12), new TbelCfTsDoubleVal(15000, 13))); + + var result = arg1.merge(arg2); + Assertions.assertEquals(3, result.getSize()); + Assertions.assertNotNull(result.getValues()); + + TbelCfTsMultiDoubleVal item0 = result.getValues().get(0); + Assertions.assertNotNull(item0); + Assertions.assertEquals(200L, item0.getTs()); + Assertions.assertEquals(1, item0.getValues()[0]); + Assertions.assertEquals(11, item0.getValues()[1]); + } + + @Test + public void merge_two_rolling_args_ts_mismatch_ignore_nan_disabled_test() { + TbTimeWindow tw = new TbTimeWindow(0, 60000); + TbelCfTsRollingArg arg1 = new TbelCfTsRollingArg(tw, Arrays.asList(new TbelCfTsDoubleVal(100, 1), new TbelCfTsDoubleVal(5000, 2), new TbelCfTsDoubleVal(15000, 3))); + TbelCfTsRollingArg arg2 = new TbelCfTsRollingArg(tw, Arrays.asList(new TbelCfTsDoubleVal(200, 11), new TbelCfTsDoubleVal(5000, 12), new TbelCfTsDoubleVal(15000, 13))); + + var result = arg1.merge(arg2, Collections.singletonMap("ignoreNaN", false)); + Assertions.assertEquals(4, result.getSize()); + Assertions.assertNotNull(result.getValues()); + + TbelCfTsMultiDoubleVal item0 = result.getValues().get(0); + Assertions.assertNotNull(item0); + Assertions.assertEquals(100L, item0.getTs()); + Assertions.assertEquals(1, item0.getValues()[0]); + Assertions.assertEquals(Double.NaN, item0.getValues()[1]); + + TbelCfTsMultiDoubleVal item1 = result.getValues().get(1); + Assertions.assertEquals(200L, item1.getTs()); + Assertions.assertEquals(1, item1.getValues()[0]); + Assertions.assertEquals(11, item1.getValues()[1]); + } + } \ No newline at end of file diff --git a/ui-ngx/src/app/shared/models/calculated-field.models.ts b/ui-ngx/src/app/shared/models/calculated-field.models.ts index f6d7aaaa1e..4467fecc79 100644 --- a/ui-ngx/src/app/shared/models/calculated-field.models.ts +++ b/ui-ngx/src/app/shared/models/calculated-field.models.ts @@ -272,7 +272,7 @@ export const CalculatedFieldAttributeValueArgumentAutocomplete = { export const CalculatedFieldRollingValueArgumentFunctionsAutocomplete = { max: { meta: 'function', - description: 'Computes the maximum value in the list of rolling argument values. Returns NaN if any value is NaN and ignoreNaN is false.', + description: 'Returns the maximum value of the rolling argument values. Returns NaN if any value is NaN and ignoreNaN is false.', args: [ { name: 'ignoreNaN', @@ -288,7 +288,7 @@ export const CalculatedFieldRollingValueArgumentFunctionsAutocomplete = { }, min: { meta: 'function', - description: 'Computes the minimum value in the list of rolling argument values. Returns NaN if any value is NaN and ignoreNaN is false.', + description: 'Returns the minimum value of the rolling argument values. Returns NaN if any value is NaN and ignoreNaN is false.', args: [ { name: 'ignoreNaN', @@ -304,7 +304,7 @@ export const CalculatedFieldRollingValueArgumentFunctionsAutocomplete = { }, mean: { meta: 'function', - description: 'Computes the mean value of the rolling argument values list. Returns NaN if any value is NaN and ignoreNaN is false.', + description: 'Computes the mean value of the rolling argument values. Returns NaN if any value is NaN and ignoreNaN is false.', args: [ { name: 'ignoreNaN', @@ -318,9 +318,25 @@ export const CalculatedFieldRollingValueArgumentFunctionsAutocomplete = { type: 'number' } }, + avg: { + meta: 'function', + description: 'Computes the average value of the rolling argument values. Returns NaN if any value is NaN and ignoreNaN is false.', + args: [ + { + name: 'ignoreNaN', + description: 'Whether to ignore NaN values. Equals true by default.', + type: 'boolean', + optional: true, + } + ], + return: { + description: 'The average value, or NaN if applicable', + type: 'number' + } + }, std: { meta: 'function', - description: 'Computes the standard deviation in the list of rolling argument values. Returns NaN if any value is NaN and ignoreNaN is false.', + description: 'Computes the standard deviation of the rolling argument values. Returns NaN if any value is NaN and ignoreNaN is false.', args: [ { name: 'ignoreNaN', @@ -336,7 +352,7 @@ export const CalculatedFieldRollingValueArgumentFunctionsAutocomplete = { }, median: { meta: 'function', - description: 'Computes the median value of the rolling argument values list. Returns NaN if any value is NaN and ignoreNaN is false.', + description: 'Computes the median value of the rolling argument values. Returns NaN if any value is NaN and ignoreNaN is false.', args: [ { name: 'ignoreNaN', @@ -352,7 +368,7 @@ export const CalculatedFieldRollingValueArgumentFunctionsAutocomplete = { }, count: { meta: 'function', - description: 'Counts values in the list of rolling argument values. Counts non-NaN values if ignoreNaN is true, otherwise - total size.', + description: 'Counts values of the rolling argument. Counts non-NaN values if ignoreNaN is true, otherwise - total size.', args: [ { name: 'ignoreNaN', @@ -368,7 +384,7 @@ export const CalculatedFieldRollingValueArgumentFunctionsAutocomplete = { }, last: { meta: 'function', - description: 'Returns the last non-NaN value in the list of rolling argument values if ignoreNaN is true, otherwise - the last value.', + description: 'Returns the last non-NaN value of the rolling argument values if ignoreNaN is true, otherwise - the last value.', args: [ { name: 'ignoreNaN', @@ -384,7 +400,7 @@ export const CalculatedFieldRollingValueArgumentFunctionsAutocomplete = { }, first: { meta: 'function', - description: 'Returns the first non-NaN value in the list of rolling argument values if ignoreNaN is true, otherwise - the first value.', + description: 'Returns the first non-NaN value of the rolling argument values if ignoreNaN is true, otherwise - the first value.', args: [ { name: 'ignoreNaN', @@ -400,7 +416,7 @@ export const CalculatedFieldRollingValueArgumentFunctionsAutocomplete = { }, sum: { meta: 'function', - description: 'Computes the sum of values in the list of rolling argument values. Returns NaN if any value is NaN and ignoreNaN is false.', + description: 'Computes the sum of rolling argument values. Returns NaN if any value is NaN and ignoreNaN is false.', args: [ { name: 'ignoreNaN', @@ -413,12 +429,56 @@ export const CalculatedFieldRollingValueArgumentFunctionsAutocomplete = { description: 'The sum of values, or NaN if applicable', type: 'number' } + }, + merge: { + meta: 'function', + description: 'Merges current object with other time series rolling argument into a single object by aligning their timestamped values. Supports optional configurable settings.', + args: [ + { + name: 'other', + description: "A time series rolling argument to be merged with the current object.", + type: "object", + optional: true + }, + { + name: "settings", + description: "Optional settings controlling the merging process. Supported keys: 'ignoreNaN' (boolean, equals true by default) to determine whether NaN values should be ignored; 'timeWindow' (object, empty by default) to apply time window filtering.", + type: "object", + optional: true + } + ], + return: { + description: 'A new object containing merged timestamped values from all provided arguments, aligned based on timestamps and filtered according to settings.', + type: '{ values: { ts: number; values: number[]; }[]; timeWindow: { startTs: number; endTs: number } }; }', + } + }, + mergeAll: { + meta: 'function', + description: 'Merges current object with other time series rolling arguments into a single object by aligning their timestamped values. Supports optional configurable settings.', + args: [ + { + name: 'others', + description: "A list of time series rolling arguments to be merged with the current object.", + type: "object[]", + optional: true + }, + { + name: "settings", + description: "Optional settings controlling the merging process. Supported keys: 'ignoreNaN' (boolean, equals true by default) to determine whether NaN values should be ignored; 'timeWindow' (object, empty by default) to apply time window filtering.", + type: "object", + optional: true + } + ], + return: { + description: 'A new object containing merged timestamped values from all provided arguments, aligned based on timestamps and filtered according to settings.', + type: '{ values: { ts: number; values: number[]; }[]; timeWindow: { startTs: number; endTs: number } }; }', + } } }; export const CalculatedFieldRollingValueArgumentAutocomplete = { meta: 'object', - type: '{ values: { ts: number; value: any; }[]; timeWindow: { startTs: number; endTs: number; limit: number } }; }', + type: '{ values: { ts: number; value: number; }[]; timeWindow: { startTs: number; endTs: number } }; }', description: 'Calculated field rolling value argument.', children: { ...CalculatedFieldRollingValueArgumentFunctionsAutocomplete, @@ -429,7 +489,7 @@ export const CalculatedFieldRollingValueArgumentAutocomplete = { }, timeWindow: { meta: 'object', - type: '{ startTs: number; endTs: number; limit: number }', + type: '{ startTs: number; endTs: number }', description: 'Time window configuration', children: { startTs: { @@ -441,11 +501,6 @@ export const CalculatedFieldRollingValueArgumentAutocomplete = { meta: 'number', type: 'number', description: 'End time stamp', - }, - limit: { - meta: 'number', - type: 'number', - description: 'Limit', } } } @@ -504,7 +559,7 @@ const calculatedFieldSingleArgumentValueHighlightRules: AceHighlightRules = { } const calculatedFieldRollingArgumentValueFunctionsHighlightRules: Array = - ['max', 'min', 'mean', 'std', 'median', 'count', 'last', 'first', 'sum'].map(funcName => ({ + ['max', 'min', 'avg', 'mean', 'std', 'median', 'count', 'last', 'first', 'sum', 'merge', 'mergeAll'].map(funcName => ({ token: 'tb.calculated-field-func', regex: `\\b${funcName}\\b`, next: 'no_regex' From 4cb5072ac6531ed9f84a9e0dfba35f4a6bd4bc62 Mon Sep 17 00:00:00 2001 From: mpetrov Date: Tue, 4 Mar 2025 13:54:15 +0200 Subject: [PATCH 178/345] Reworked provision to toasts --- .../common/action/mobile-action-editor.models.ts | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/action/mobile-action-editor.models.ts b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/action/mobile-action-editor.models.ts index 5164b6c8b8..54e70dc34b 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/action/mobile-action-editor.models.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/action/mobile-action-editor.models.ts @@ -140,10 +140,12 @@ const processLocationFunction: TbFunction = const provisioningSuccessFunction: TbFunction = '// Function body to handle device provision success. \n' + - '// - device - device that was successfully provisioned.\n' + - 'function showDeviceProvisioningSuccess(deviceName, message) {\n' + + '// - deviceName - name of device that was successfully provisioned.\n' + + 'showDeviceProvisioningSuccess(deviceName);\n' + + '\n' + + 'function showDeviceProvisioningSuccess(deviceName) {\n' + ' setTimeout(function() {\n' + - ' widgetContext.dialogs.alert(`Device ` + deviceName + ` was successfully provisioned`, message).subscribe();\n' + + ' widgetContext.showSuccessToast(`Device ` + deviceName + ` was successfully provisioned`).subscribe();\n' + ' }, 100);\n' + '}\n'; @@ -154,7 +156,7 @@ const handleEmptyResultFunctionTemplate: TbFunction = '\n' + 'function showEmptyResultDialog(message) {\n' + ' setTimeout(function() {\n' + - ' widgetContext.dialogs.alert(\'Empty result\', message).subscribe();\n' + + ' widgetContext.showInfoToast(message).subscribe();\n' + ' }, 100);\n' + '}\n'; From 79d0e706d2188a6740f3b0fc83a0fd3df411960d Mon Sep 17 00:00:00 2001 From: IrynaMatveieva Date: Tue, 4 Mar 2025 14:02:53 +0200 Subject: [PATCH 179/345] updated upgrade script --- .../main/data/upgrade/basic/schema_update.sql | 37 ++++++------------- 1 file changed, 12 insertions(+), 25 deletions(-) diff --git a/application/src/main/data/upgrade/basic/schema_update.sql b/application/src/main/data/upgrade/basic/schema_update.sql index d694a2bcf6..e9cbf8ef8e 100644 --- a/application/src/main/data/upgrade/basic/schema_update.sql +++ b/application/src/main/data/upgrade/basic/schema_update.sql @@ -67,30 +67,17 @@ ALTER TABLE api_usage_state ADD COLUMN IF NOT EXISTS version BIGINT DEFAULT 1; -- UPDATE TENANT PROFILE CALCULATED FIELD LIMITS START -DO $$ - BEGIN - -- Check if the tenant_profile table exists - IF EXISTS ( - SELECT 1 - FROM information_schema.tables - WHERE table_name = 'tenant_profile' - ) THEN - - UPDATE tenant_profile - SET profile_data = profile_data - || jsonb_build_object( - 'configuration', profile_data->'configuration' || jsonb_build_object( - 'maxCalculatedFieldsPerEntity', COALESCE(profile_data->'configuration'->>'maxCalculatedFieldsPerEntity', '5')::bigint, - 'maxArgumentsPerCF', COALESCE(profile_data->'configuration'->>'maxArgumentsPerCF', '10')::bigint, - 'maxDataPointsPerRollingArg', COALESCE(profile_data->'configuration'->>'maxDataPointsPerRollingArg', '1000')::bigint, - 'maxStateSizeInKBytes', COALESCE(profile_data->'configuration'->>'maxStateSizeInKBytes', '32')::bigint, - 'maxSingleValueArgumentSizeInKBytes', COALESCE(profile_data->'configuration'->>'maxSingleValueArgumentSizeInKBytes', '2')::bigint - ) - ) - WHERE profile_data->'configuration' IS NOT NULL; - -END IF; - END; -$$; +UPDATE tenant_profile +SET profile_data = profile_data + || jsonb_build_object( + 'configuration', profile_data->'configuration' || jsonb_build_object( + 'maxCalculatedFieldsPerEntity', COALESCE(profile_data->'configuration'->>'maxCalculatedFieldsPerEntity', '5')::bigint, + 'maxArgumentsPerCF', COALESCE(profile_data->'configuration'->>'maxArgumentsPerCF', '10')::bigint, + 'maxDataPointsPerRollingArg', COALESCE(profile_data->'configuration'->>'maxDataPointsPerRollingArg', '1000')::bigint, + 'maxStateSizeInKBytes', COALESCE(profile_data->'configuration'->>'maxStateSizeInKBytes', '32')::bigint, + 'maxSingleValueArgumentSizeInKBytes', COALESCE(profile_data->'configuration'->>'maxSingleValueArgumentSizeInKBytes', '2')::bigint + ) + ) +WHERE profile_data->'configuration'->>'maxCalculatedFieldsPerEntity' IS NULL; -- UPDATE TENANT PROFILE CALCULATED FIELD LIMITS END From e62b062d5b0ff24ddc70d57101743303fbe0188e Mon Sep 17 00:00:00 2001 From: mpetrov Date: Tue, 4 Mar 2025 14:57:46 +0200 Subject: [PATCH 180/345] Added copy argument name button --- ...lated-field-arguments-table.component.html | 19 ++++++++++++++----- ...lated-field-arguments-table.component.scss | 18 ++++++++++++++++++ .../components/event/event-table-config.ts | 4 ++-- .../assets/locale/locale.constant-en_US.json | 1 + 4 files changed, 35 insertions(+), 7 deletions(-) diff --git a/ui-ngx/src/app/modules/home/components/calculated-fields/components/arguments-table/calculated-field-arguments-table.component.html b/ui-ngx/src/app/modules/home/components/calculated-fields/components/arguments-table/calculated-field-arguments-table.component.html index 6bab01f976..8a362f7eda 100644 --- a/ui-ngx/src/app/modules/home/components/calculated-fields/components/arguments-table/calculated-field-arguments-table.component.html +++ b/ui-ngx/src/app/modules/home/components/calculated-fields/components/arguments-table/calculated-field-arguments-table.component.html @@ -20,18 +20,27 @@ - +
{{ 'common.name' | translate }}
- -
{{ argument.argumentName }}
+ +
+
{{ argument.argumentName }}
+ +
- + {{ 'entity.entity-type' | translate }} - +
@if (argument.refEntityId?.entityType === ArgumentEntityType.Tenant) { {{ 'calculated-fields.argument-current-tenant' | translate }} diff --git a/ui-ngx/src/app/modules/home/components/calculated-fields/components/arguments-table/calculated-field-arguments-table.component.scss b/ui-ngx/src/app/modules/home/components/calculated-fields/components/arguments-table/calculated-field-arguments-table.component.scss index 877a749afa..ae8fd25170 100644 --- a/ui-ngx/src/app/modules/home/components/calculated-fields/components/arguments-table/calculated-field-arguments-table.component.scss +++ b/ui-ngx/src/app/modules/home/components/calculated-fields/components/arguments-table/calculated-field-arguments-table.component.scss @@ -28,6 +28,17 @@ .key-text { font-size: 13px; } + + .copy-argument-name { + visibility: hidden; + transition: visibility 0.1s; + } + + .argument-name-cell:hover { + .copy-argument-name { + visibility: visible; + } + } } .max-args-warning { @@ -61,4 +72,11 @@ padding: 0 28px 0 0; } } + + .copy-argument-name { + .mat-icon { + font-size: 16px; + padding: 4px; + } + } } diff --git a/ui-ngx/src/app/modules/home/components/event/event-table-config.ts b/ui-ngx/src/app/modules/home/components/event/event-table-config.ts index 6c334d2a5e..56a17cc7ae 100644 --- a/ui-ngx/src/app/modules/home/components/event/event-table-config.ts +++ b/ui-ngx/src/app/modules/home/components/event/event-table-config.ts @@ -369,7 +369,7 @@ export class EventTableConfig extends EntityTableConfig { (entity) => entity.body.entityId, { name: this.translate.instant('event.copy-entity-id'), - icon: 'content_paste', + icon: 'content_copy', style: { padding: '4px', 'font-size': '16px', @@ -389,7 +389,7 @@ export class EventTableConfig extends EntityTableConfig { false, { name: this.translate.instant('event.copy-message-id'), - icon: 'content_paste', + icon: 'content_copy', style: { padding: '4px', 'font-size': '16px', 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 02c604d8ae..96d8bf205f 100644 --- a/ui-ngx/src/assets/locale/locale.constant-en_US.json +++ b/ui-ngx/src/assets/locale/locale.constant-en_US.json @@ -1031,6 +1031,7 @@ "argument-type": "Argument type", "see-debug-events": "See debug events", "attribute": "Attribute", + "copy-argument-name": "Copy argument name", "timeseries-key": "Time series key", "device-name": "Device name", "latest-telemetry": "Latest telemetry", From 6d401e74899789da489dd275daf809fe32201096 Mon Sep 17 00:00:00 2001 From: mpetrov Date: Tue, 4 Mar 2025 15:15:14 +0200 Subject: [PATCH 181/345] Fixed delete argument on sorting --- .../calculated-field-arguments-table.component.html | 2 +- .../calculated-field-arguments-table.component.ts | 5 +++-- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/ui-ngx/src/app/modules/home/components/calculated-fields/components/arguments-table/calculated-field-arguments-table.component.html b/ui-ngx/src/app/modules/home/components/calculated-fields/components/arguments-table/calculated-field-arguments-table.component.html index 8a362f7eda..162bd3aa1e 100644 --- a/ui-ngx/src/app/modules/home/components/calculated-fields/components/arguments-table/calculated-field-arguments-table.component.html +++ b/ui-ngx/src/app/modules/home/components/calculated-fields/components/arguments-table/calculated-field-arguments-table.component.html @@ -107,7 +107,7 @@
@if (fieldFormGroup.get('type').value === CalculatedFieldType.SIMPLE) { - - - {{ (outputFormGroup.get('type').value === OutputType.Timeseries - ? 'calculated-fields.timeseries-key' - : 'calculated-fields.attribute-key') - | translate }} - - - @if (outputFormGroup.get('name').errors && outputFormGroup.get('name').touched) { - - @if (outputFormGroup.get('name').hasError('required')) { - {{ 'common.hint.key-required' | translate }} - } @else if (outputFormGroup.get('name').hasError('pattern')) { - {{ 'common.hint.key-pattern' | translate }} - } @else if (outputFormGroup.get('name').hasError('maxlength')) { - {{ 'common.hint.key-max-length' | translate }} - } - - } - +
+ + + {{ (outputFormGroup.get('type').value === OutputType.Timeseries + ? 'calculated-fields.timeseries-key' + : 'calculated-fields.attribute-key') + | translate }} + + + @if (outputFormGroup.get('name').errors && outputFormGroup.get('name').touched) { + + @if (outputFormGroup.get('name').hasError('required')) { + {{ 'common.hint.key-required' | translate }} + } @else if (outputFormGroup.get('name').hasError('pattern')) { + {{ 'common.hint.key-pattern' | translate }} + } @else if (outputFormGroup.get('name').hasError('maxlength')) { + {{ 'common.hint.key-max-length' | translate }} + } + + } + + + {{ 'calculated-fields.decimals-by-default' | translate }} + + @if (outputFormGroup.get('decimalsByDefault').errors && outputFormGroup.get('decimalsByDefault').touched) { + {{ 'calculated-fields.hint.decimals-range' | translate }} + } + +
}
diff --git a/ui-ngx/src/app/modules/home/components/calculated-fields/components/dialog/calculated-field-dialog.component.ts b/ui-ngx/src/app/modules/home/components/calculated-fields/components/dialog/calculated-field-dialog.component.ts index 757231364b..127c1c8c1c 100644 --- a/ui-ngx/src/app/modules/home/components/calculated-fields/components/dialog/calculated-field-dialog.component.ts +++ b/ui-ngx/src/app/modules/home/components/calculated-fields/components/dialog/calculated-field-dialog.component.ts @@ -33,7 +33,7 @@ import { OutputType, OutputTypeTranslations } from '@shared/models/calculated-field.models'; -import { oneSpaceInsideRegex } from '@shared/models/regex.constants'; +import { digitsRegex, oneSpaceInsideRegex } from '@shared/models/regex.constants'; import { AttributeScope } from '@shared/models/telemetry/telemetry.models'; import { EntityType } from '@shared/models/entity-type.models'; import { map, startWith, switchMap } from 'rxjs/operators'; @@ -61,7 +61,8 @@ export class CalculatedFieldDialogComponent extends DialogComponent Date: Wed, 5 Mar 2025 12:36:57 +0200 Subject: [PATCH 195/345] UI: Maps - introduce button to toggle drag-drop mode. --- .../maps/data-layer/latest-map-data-layer.ts | 26 ++++- .../components/widget/lib/maps/map-layer.ts | 5 +- .../home/components/widget/lib/maps/map.scss | 3 + .../home/components/widget/lib/maps/map.ts | 101 ++++++++++++++---- .../widget/lib/maps/models/map.models.ts | 2 + .../common/map/map-settings.component.html | 5 + .../common/map/map-settings.component.ts | 37 ++++++- .../assets/locale/locale.constant-en_US.json | 4 +- 8 files changed, 158 insertions(+), 25 deletions(-) diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/maps/data-layer/latest-map-data-layer.ts b/ui-ngx/src/app/modules/home/components/widget/lib/maps/data-layer/latest-map-data-layer.ts index fe8639f7a0..7c77056894 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/maps/data-layer/latest-map-data-layer.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/maps/data-layer/latest-map-data-layer.ts @@ -114,6 +114,18 @@ export abstract class TbLatestDataLayerItem, dsData: FormattedData[]): void { this.data = data; this.doUpdate(data, dsData); @@ -151,7 +163,7 @@ export abstract class TbLatestDataLayerItem item.editModeUpdated()); } + private updateItemsDragMode() { + this.layerItems.forEach(item => item.dragModeUpdated()); + } + public abstract placeItem(item: UnplacedMapDataItem, layer: L.Layer): void; protected abstract isValidLayerData(layerData: FormattedData): boolean; diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/maps/map-layer.ts b/ui-ngx/src/app/modules/home/components/widget/lib/maps/map-layer.ts index a139eeb4c0..c6f8f446a6 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/maps/map-layer.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/maps/map-layer.ts @@ -161,7 +161,7 @@ class TbGoogleMapLayer extends TbMapLayer { } private loadGoogle(): Observable { - const apiKey = this.settings.apiKey; + const apiKey = this.settings.apiKey || defaultGoogleMapLayerSettings.apiKey; if (TbGoogleMapLayer.loadedApiKeysGlobal[apiKey]) { return of(true); } else { @@ -213,7 +213,8 @@ class TbHereMapLayer extends TbMapLayer { } protected createLayer(): Observable { - const layer = L.tileLayer.provider(this.settings.layerType, {useV3: true, apiKey: this.settings.apiKey} as any); + const apiKey = this.settings.apiKey || defaultHereMapLayerSettings.apiKey; + const layer = L.tileLayer.provider(this.settings.layerType, {useV3: true, apiKey} as any); return of(layer); } diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/maps/map.scss b/ui-ngx/src/app/modules/home/components/widget/lib/maps/map.scss index 9c764cbb0e..fc60862be4 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/maps/map.scss +++ b/ui-ngx/src/app/modules/home/components/widget/lib/maps/map.scss @@ -138,6 +138,9 @@ &.tb-rotate { mask-image: url('data:image/svg+xml,'); } + &.tb-drag-mode { + mask-image: url('data:image/svg+xml,'); + } &.tb-place-marker { mask-image: url('data:image/svg+xml,'); } 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 f279a9fb68..6225ff878d 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 @@ -109,6 +109,7 @@ export abstract class TbMap { protected customActionsToolbar: L.TB.TopToolbarControl; protected editToolbar: L.TB.BottomToolbarControl; + protected dragModeButton: L.TB.ToolbarButton; protected addMarkerButton: L.TB.ToolbarButton; protected addRectangleButton: L.TB.ToolbarButton; protected addPolygonButton: L.TB.ToolbarButton; @@ -127,10 +128,12 @@ export abstract class TbMap { private tooltipInstances: TooltipInstancesData[] = []; private currentPopover: TbPopoverComponent; - private currentAddButton: L.TB.ToolbarButton; + private currentEditButton: L.TB.ToolbarButton; + + private dragMode = true; private get isPlacingItem(): boolean { - return !!this.currentAddButton; + return !!this.currentEditButton; } protected constructor(protected ctx: WidgetContext, @@ -188,6 +191,7 @@ export abstract class TbMap { if (this.map.zoomControl) { this.map.zoomControl.setPosition(this.settings.controlsPosition); } + this.dragMode = !this.settings.dragModeButton; const setup = [this.doSetupControls()]; if (this.timeline && this.settings.tripTimeline.snapToRealLocation) { setup.push(parseTbFunction(this.getCtx().http, this.settings.tripTimeline.locationSnapFilter, ['data', 'dsData']).pipe( @@ -393,12 +397,24 @@ export abstract class TbMap { this.map.pm.applyGlobalOptions(); } + const dragSupportedDataLayers = this.latestDataLayers.filter(dl => dl.isDragEnabled()); + const showDragModeButton = this.settings.dragModeButton && dragSupportedDataLayers.length; const addSupportedDataLayers = this.latestDataLayers.filter(dl => dl.isAddEnabled()); - if (addSupportedDataLayers.length) { + if (showDragModeButton || addSupportedDataLayers.length) { const drawToolbar = L.TB.toolbar({ position: this.settings.controlsPosition }).addTo(this.map); + if (showDragModeButton) { + this.dragModeButton = drawToolbar.toolbarButton({ + id: 'dragMode', + title: this.ctx.translate.instant('widgets.maps.data-layer.drag-drop-mode'), + iconClass: 'tb-drag-mode', + click: (e, button) => { + this.toggleDragMode(e, button); + } + }); + } this.addMarkerDataLayers = addSupportedDataLayers.filter(dl => dl.dataLayerType() === 'markers'); if (this.addMarkerDataLayers.length) { this.addMarkerButton = drawToolbar.toolbarButton({ @@ -448,6 +464,32 @@ export abstract class TbMap { } } + private toggleDragMode(e: MouseEvent, button: L.TB.ToolbarButton): void { + if (this.dragMode) { + this.disableDragMode(); + } else { + this.dragMode = true; + this.latestDataLayers.forEach(dl => dl.dragModeUpdated()); + this.updatePlaceItemState(button); + this.editToolbar.open([ + { + id: 'cancel', + iconClass: 'tb-close', + title: this.ctx.translate.instant('action.cancel'), + showText: true, + click: this.disableDragMode + } + ], false); + } + } + + private disableDragMode = () => { + this.dragMode = false; + this.latestDataLayers.forEach(dl => dl.dragModeUpdated()); + this.updatePlaceItemState(); + this.editToolbar.close(); + } + private placeMarker(e: MouseEvent, button: L.TB.ToolbarButton): void { this.placeItem(e, button, this.addMarkerDataLayers, (entity) => this.prepareDrawMode('Marker', { placeMarker: this.ctx.translate.instant('widgets.maps.data-layer.marker.place-marker-hint-with-entity', {entityName: entity.entity.entityDisplayName}) @@ -479,6 +521,7 @@ export abstract class TbMap { private placeItem(e: MouseEvent, button: L.TB.ToolbarButton, dataLayers: TbLatestMapDataLayer[], prepareDrawMode: (entity: UnplacedMapDataItem) => void): void { if (this.isPlacingItem) { + this.finishAdd(); return; } this.updatePlaceItemState(button); @@ -692,6 +735,10 @@ export abstract class TbMap { } private finishAdd = () => { + if (this.currentPopover) { + this.currentPopover.hide(); + this.currentPopover = null; + } this.map.off('pm:create'); this.map.pm.disableDraw(); this.latestDataLayers.forEach(dl => dl.enableEditMode()); @@ -706,15 +753,15 @@ export abstract class TbMap { L.DomUtil.addClass(this.map.pm.Draw[shape]._hintMarker.getTooltip()._container, 'tb-place-item-label'); } - private updatePlaceItemState(addButton?: L.TB.ToolbarButton, disabled = false): void { - if (addButton) { + private updatePlaceItemState(editButton?: L.TB.ToolbarButton, disabled = false): void { + if (editButton) { this.deselectItem(false, true); - addButton.setActive(true); - } else if (this.currentAddButton) { - this.currentAddButton.setActive(false); + editButton.setActive(true); + } else if (this.currentEditButton) { + this.currentEditButton.setActive(false); } - this.currentAddButton = addButton; - this.updateAddButtonsStates(disabled); + this.currentEditButton = editButton; + this.updateEditButtonsStates(disabled); } private createdControlButtonTooltip(root: HTMLElement, side: TooltipPositioningSide) { @@ -784,7 +831,7 @@ export abstract class TbMap { this.updateTripsAppearance(); this.updateTripsAnchors(); this.updateBounds(); - this.updateAddButtonsStates(); + this.updateEditButtonsStates(); } private updateTrips(subscription: IWidgetSubscription) { @@ -886,22 +933,28 @@ export abstract class TbMap { } } - private updateAddButtonsStates(disabled = false) { - if (this.currentAddButton || disabled) { - if (this.addMarkerButton && this.addMarkerButton !== this.currentAddButton) { + private updateEditButtonsStates(disabled = false) { + if (this.currentEditButton || disabled) { + if (this.dragModeButton && this.dragModeButton !== this.currentEditButton) { + this.dragModeButton.setDisabled(true); + } + if (this.addMarkerButton && this.addMarkerButton !== this.currentEditButton) { this.addMarkerButton.setDisabled(true); } - if (this.addRectangleButton && this.addRectangleButton !== this.currentAddButton) { + if (this.addRectangleButton && this.addRectangleButton !== this.currentEditButton) { this.addRectangleButton.setDisabled(true); } - if (this.addPolygonButton && this.addPolygonButton !== this.currentAddButton) { + if (this.addPolygonButton && this.addPolygonButton !== this.currentEditButton) { this.addPolygonButton.setDisabled(true); } - if (this.addCircleButton && this.addCircleButton !== this.currentAddButton) { + if (this.addCircleButton && this.addCircleButton !== this.currentEditButton) { this.addCircleButton.setDisabled(true); } - this.customActionsToolbar.setDisabled(true); + this.customActionsToolbar?.setDisabled(true); } else { + if (this.dragModeButton) { + this.dragModeButton.setDisabled(false); + } if (this.addMarkerButton) { this.addMarkerButton.setDisabled(!this.addMarkerDataLayers.some(dl => dl.isEnabled() && dl.hasUnplacedItems())); } @@ -914,7 +967,7 @@ export abstract class TbMap { if (this.addCircleButton) { this.addCircleButton.setDisabled(!this.addCircleDataLayers.some(dl => dl.isEnabled() && dl.hasUnplacedItems())); } - this.customActionsToolbar.setDisabled(false); + this.customActionsToolbar?.setDisabled(false); } } @@ -979,7 +1032,7 @@ export abstract class TbMap { } public enabledDataLayersUpdated() { - this.updateAddButtonsStates(); + this.updateEditButtonsStates(); this.updateTripsAnchors(); } @@ -1027,6 +1080,14 @@ export abstract class TbMap { return this.editToolbar; } + public useDragModeButton(): boolean { + return this.settings.dragModeButton; + } + + public dragModeEnabled(): boolean { + return this.dragMode; + } + public saveItemData(datasource: TbMapDatasource, data: DataKeyValuePair[]): Observable { const attributeService = this.ctx.$injector.get(AttributeService); const attributes: AttributeData[] = []; diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/maps/models/map.models.ts b/ui-ngx/src/app/modules/home/components/widget/lib/maps/models/map.models.ts index d28e04b4fc..0f0e64f5b7 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/maps/models/map.models.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/maps/models/map.models.ts @@ -660,6 +660,7 @@ export interface BaseMapSettings { additionalDataSources: AdditionalMapDataSourceSettings[]; controlsPosition: MapControlsPosition; zoomActions: MapZoomAction[]; + dragModeButton: boolean; fitMapBounds: boolean; useDefaultCenterPosition: boolean; defaultCenterPosition?: string; @@ -682,6 +683,7 @@ export const defaultBaseMapSettings: BaseMapSettings = { additionalDataSources: [], controlsPosition: MapControlsPosition.topleft, zoomActions: [MapZoomAction.scroll, MapZoomAction.doubleClick, MapZoomAction.controlButtons], + dragModeButton: false, fitMapBounds: true, useDefaultCenterPosition: false, defaultCenterPosition: '0,0', 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 0ee29c54c5..97c686732a 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 @@ -99,6 +99,11 @@ +
+ + {{ 'widgets.maps.control.switch-to-drag-mode-using-button' | translate }} + +
diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/map/map-settings.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/map/map-settings.component.ts index 74536f8fd5..6f1650b19f 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/map/map-settings.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/map/map-settings.component.ts @@ -26,10 +26,13 @@ import { Validators } from '@angular/forms'; import { + DataLayerEditAction, defaultImageMapSourceSettings, - ImageMapSourceSettings, imageMapSourceSettingsValidator, + ImageMapSourceSettings, + imageMapSourceSettingsValidator, mapControlPositions, mapControlsPositionTranslationMap, + MapDataLayerSettings, MapDataLayerType, MapSetting, MapType, @@ -109,6 +112,8 @@ export class MapSettingsComponent implements OnInit, ControlValueAccessor, Valid dataLayerMode: MapDataLayerType = 'markers'; + showDragButtonModeButtonSettings = false; + constructor(private fb: UntypedFormBuilder, private dialog: MatDialog, private destroyRef: DestroyRef) { @@ -139,6 +144,7 @@ export class MapSettingsComponent implements OnInit, ControlValueAccessor, Valid additionalDataSources: [null, []], controlsPosition: [null, []], zoomActions: [null, []], + dragModeButton: [null, []], fitMapBounds: [null, []], useDefaultCenterPosition: [null, []], defaultCenterPosition: [null, []], @@ -167,6 +173,14 @@ export class MapSettingsComponent implements OnInit, ControlValueAccessor, Valid ).subscribe((mapType: MapType) => { this.mapTypeChanged(mapType); }); + merge(this.mapSettingsFormGroup.get('markers').valueChanges, + this.mapSettingsFormGroup.get('polygons').valueChanges, + this.mapSettingsFormGroup.get('circles').valueChanges + ).pipe( + takeUntilDestroyed(this.destroyRef) + ).subscribe(() => { + this.updateDragButtonModeSettings(); + }); } registerOnChange(fn: any): void { @@ -192,6 +206,7 @@ export class MapSettingsComponent implements OnInit, ControlValueAccessor, Valid value, {emitEvent: false} ); this.updateValidators(); + this.updateDragButtonModeSettings(); } public validate(_c: UntypedFormControl) { @@ -237,6 +252,26 @@ export class MapSettingsComponent implements OnInit, ControlValueAccessor, Valid } } + private updateDragButtonModeSettings() { + const markers: MapDataLayerSettings[] = this.mapSettingsFormGroup.get('markers').value; + const circles: MapDataLayerSettings[] = this.mapSettingsFormGroup.get('circles').value; + let dragModeButtonSettingsEnabled = markers.some(d => d.edit && d.edit.enabledActions && d.edit.enabledActions.includes(DataLayerEditAction.move)); + if (!dragModeButtonSettingsEnabled) { + const polygons: MapDataLayerSettings[] = this.mapSettingsFormGroup.get('polygons').value; + dragModeButtonSettingsEnabled = polygons.some(d => d.edit && d.edit.enabledActions && d.edit.enabledActions.includes(DataLayerEditAction.move)); + } + if (!dragModeButtonSettingsEnabled) { + const circles: MapDataLayerSettings[] = this.mapSettingsFormGroup.get('circles').value; + dragModeButtonSettingsEnabled = circles.some(d => d.edit && d.edit.enabledActions && d.edit.enabledActions.includes(DataLayerEditAction.move)); + } + this.showDragButtonModeButtonSettings = dragModeButtonSettingsEnabled; + if (dragModeButtonSettingsEnabled) { + this.mapSettingsFormGroup.get('dragModeButton').enable({emitEvent: false}); + } else { + this.mapSettingsFormGroup.get('dragModeButton').disable({emitEvent: false}); + } + } + private updateModel() { this.modelValue = this.mapSettingsFormGroup.getRawValue(); this.propagateChange(this.modelValue); 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 6eaa7cd206..4c336831fa 100644 --- a/ui-ngx/src/assets/locale/locale.constant-en_US.json +++ b/ui-ngx/src/assets/locale/locale.constant-en_US.json @@ -7704,7 +7704,8 @@ "zoom-actions": "Zoom actions", "zoom-scroll": "Scroll", "zoom-double-click": "Double click", - "zoom-control-buttons": "Control buttons" + "zoom-control-buttons": "Control buttons", + "switch-to-drag-mode-using-button": "Switch to drag mode using button" }, "timeline": { "control-panel": "Timeline control panel", @@ -7839,6 +7840,7 @@ "action-remove": "Remove", "edit-instruments": "Instruments", "enable-snapping": "Enable snapping to other vertices for precision drawing", + "drag-drop-mode": "Drag-drop mode", "trip": { "no-trips": "No trips configured", "add-trip": "Add trip", From 622580c97cbdec03d47d0e6e0431d20d3e152cdd Mon Sep 17 00:00:00 2001 From: ViacheslavKlimov Date: Wed, 5 Mar 2025 12:48:59 +0200 Subject: [PATCH 196/345] Update GitHub workflow Ubuntu version to 22.04 --- .github/workflows/check-configuration-files.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/check-configuration-files.yml b/.github/workflows/check-configuration-files.yml index f280e326c2..21357a8a3c 100644 --- a/.github/workflows/check-configuration-files.yml +++ b/.github/workflows/check-configuration-files.yml @@ -32,7 +32,7 @@ on: jobs: build: name: Check thingsboard.yml file - runs-on: ubuntu-20.04 + runs-on: ubuntu-22.04 steps: - name: Checkout code uses: actions/checkout@v2 From c990169223b52f4c90b66d81fcc65ab93c098e20 Mon Sep 17 00:00:00 2001 From: ViacheslavKlimov Date: Wed, 5 Mar 2025 13:07:38 +0200 Subject: [PATCH 197/345] Update GitHub workflow Python version to 3.13.2 --- .github/workflows/check-configuration-files.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/check-configuration-files.yml b/.github/workflows/check-configuration-files.yml index 21357a8a3c..561b7d0019 100644 --- a/.github/workflows/check-configuration-files.yml +++ b/.github/workflows/check-configuration-files.yml @@ -36,10 +36,10 @@ jobs: steps: - name: Checkout code uses: actions/checkout@v2 - - name: Set up Python 3.10 + - name: Set up Python 3.13 uses: actions/setup-python@v3 with: - python-version: "3.10.2" + python-version: "3.13.2" architecture: "x64" env: AGENT_TOOLSDIRECTORY: /opt/hostedtoolcache From d53f359bca5a34260e5b00cd116dd46cebe48a60 Mon Sep 17 00:00:00 2001 From: mpetrov Date: Wed, 5 Mar 2025 13:40:05 +0200 Subject: [PATCH 198/345] Allowed underscore in argument name --- ui-ngx/src/app/shared/models/regex.constants.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ui-ngx/src/app/shared/models/regex.constants.ts b/ui-ngx/src/app/shared/models/regex.constants.ts index f78d492d96..b8b1be4e83 100644 --- a/ui-ngx/src/app/shared/models/regex.constants.ts +++ b/ui-ngx/src/app/shared/models/regex.constants.ts @@ -16,6 +16,6 @@ export const oneSpaceInsideRegex = /^\s*\S+(?:\s\S+)*\s*$/; -export const charsWithNumRegex = /^[a-zA-Z]+[a-zA-Z0-9]*$/; +export const charsWithNumRegex = /^[a-zA-Z_]+[a-zA-Z0-9_]*$/; export const digitsRegex = /^\d*$/; From a991719585a5a7388a71f6a43ebcd9ea9afabec9 Mon Sep 17 00:00:00 2001 From: mpetrov Date: Wed, 5 Mar 2025 14:02:57 +0200 Subject: [PATCH 199/345] Fixed highlight on type change bug --- .../calculated-field-dialog.component.html | 38 +++++++++---------- 1 file changed, 18 insertions(+), 20 deletions(-) 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 1feb76035d..a136d3ea8b 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 @@ -76,30 +76,28 @@
{{ 'calculated-fields.expression' | translate }}
- @if (fieldFormGroup.get('type').value === CalculatedFieldType.SIMPLE) { - - - @if (configFormGroup.get('expressionSIMPLE').errors && configFormGroup.get('expressionSIMPLE').touched) { - - @if (configFormGroup.get('expressionSIMPLE').hasError('required')) { - {{ 'calculated-fields.hint.expression-required' | translate }} - } @else if (configFormGroup.get('expressionSIMPLE').hasError('pattern')) { - {{ 'calculated-fields.hint.expression-invalid' | translate }} - } @else if (configFormGroup.get('expressionSIMPLE').hasError('maxLength')) { - {{ 'calculated-fields.hint.expression-max-length' | translate }} - } - - } @else { - {{ 'calculated-fields.hint.expression' | translate }} - } - - } @else { + + + @if (configFormGroup.get('expressionSIMPLE').errors && configFormGroup.get('expressionSIMPLE').touched) { + + @if (configFormGroup.get('expressionSIMPLE').hasError('required')) { + {{ 'calculated-fields.hint.expression-required' | translate }} + } @else if (configFormGroup.get('expressionSIMPLE').hasError('pattern')) { + {{ 'calculated-fields.hint.expression-invalid' | translate }} + } @else if (configFormGroup.get('expressionSIMPLE').hasError('maxLength')) { + {{ 'calculated-fields.hint.expression-max-length' | translate }} + } + + } @else { + {{ 'calculated-fields.hint.expression' | translate }} + } + +
- } +
{{ 'calculated-fields.output' | translate }}
From bb5d0233fc3f42fed16d2828cd5363a6a6e25f4e Mon Sep 17 00:00:00 2001 From: nick Date: Wed, 5 Mar 2025 14:57:23 +0200 Subject: [PATCH 200/345] tbel: tbDate test function with TZ => "UTC" --- .../service/script/TbelInvokeDocsIoTest.java | 101 +++++++++++------- 1 file changed, 61 insertions(+), 40 deletions(-) diff --git a/application/src/test/java/org/thingsboard/server/service/script/TbelInvokeDocsIoTest.java b/application/src/test/java/org/thingsboard/server/service/script/TbelInvokeDocsIoTest.java index 8b6c739d31..bf86f98e6e 100644 --- a/application/src/test/java/org/thingsboard/server/service/script/TbelInvokeDocsIoTest.java +++ b/application/src/test/java/org/thingsboard/server/service/script/TbelInvokeDocsIoTest.java @@ -2238,46 +2238,49 @@ class TbelInvokeDocsIoTest extends AbstractTbelInvokeTest { @Test public void tbDateFunction_Test() throws ExecutionException, InterruptedException { + String stringDateUTC = "2024-01-01T10:00:00.00Z"; + TbDate d = new TbDate(stringDateUTC); + msgStr = "{}"; decoderStr = String.format(""" - var d = new Date(2024, 1, 1, 10, 0, 0, 0); // TZ => "UTC" - var dIsoY1 = d.toISOString(); // return 2024-01-01T08:00:00Z + var d = new Date("%s"); // TZ => "UTC" + var dIsoY1 = d.toISOString(); // return 2024-01-01T10:00:00Z d.addYears(1); - var dIsoY2 = d.toISOString(); // return 2025-01-01T08:00:00Z + var dIsoY2 = d.toISOString(); // return 2025-01-01T10:00:00Z d.addYears(-2); - var dIsoY3 = d.toISOString(); // return 2023-01-01T08:00:00Z + var dIsoY3 = d.toISOString(); // return 2023-01-01T10:00:00Z d.addMonths(2); - var dIsoM1 = d.toISOString(); // return 2023-03-01T08:00:00Z + var dIsoM1 = d.toISOString(); // return 2023-03-01T10:00:00Z d.addMonths(10); - var dIsoM2 = d.toISOString(); // return 2024-01-01T08:00:00Z + var dIsoM2 = d.toISOString(); // return 2024-01-01T10:00:00Z d.addMonths(-13); - var dIsoM3 = d.toISOString(); // return 2022-12-01T08:00:00Z + var dIsoM3 = d.toISOString(); // return 2022-12-01T10:00:00Z d.addWeeks(4); - var dIsoW1 = d.toISOString(); // return 2022-12-29T08:00:00Z + var dIsoW1 = d.toISOString(); // return 2022-12-29T10:00:00Z d.addWeeks(-5); - var dIsoW2 = d.toISOString(); // return 2022-11-24T08:00:00Z + var dIsoW2 = d.toISOString(); // return 2022-11-24T10:00:00Z d.addDays(6); - var dIsoD1 = d.toISOString(); // return 2022-11-30T08:00:00Z + var dIsoD1 = d.toISOString(); // return 2022-11-30T10:00:00Z d.addDays(45); - var dIsoD2 = d.toISOString(); // return 2023-01-14T08:00:00Z + var dIsoD2 = d.toISOString(); // return 2023-01-14T10:00:00Z d.addDays(-50); - var dIsoD3 = d.toISOString(); // return 2022-11-25T08:00:00Z + var dIsoD3 = d.toISOString(); // return 2022-11-25T10:00:00Z d.addHours(23); - var dIsoH1 = d.toISOString(); // return 2022-11-26T07:00:00Z + var dIsoH1 = d.toISOString(); // return 2022-11-26T09:00:00Z d.addHours(-47); - var dIsoH2 = d.toISOString(); // return 2022-11-24T08:00:00Z + var dIsoH2 = d.toISOString(); // return 2022-11-24T10:00:00Z d.addMinutes(59); - var dIsoMin1 = d.toISOString(); // return 2022-11-24T08:59:00Z + var dIsoMin1 = d.toISOString(); // return 2022-11-24T10:59:00Z d.addMinutes(-60); - var dIsoMin2 = d.toISOString(); // return 2022-11-24T07:59:00Z + var dIsoMin2 = d.toISOString(); // return 2022-11-24T09:59:00Z d.addSeconds(59); - var dIsoS1 = d.toISOString(); // return 2022-11-24T07:59:59Z + var dIsoS1 = d.toISOString(); // return 2022-11-24T09:59:59Z d.addSeconds(-60); - var dIsoS2 = d.toISOString(); // return 2022-11-24T07:58:59Z + var dIsoS2 = d.toISOString(); // return 2022-11-24T09:58:59Z d.addNanos(999999); - var dIsoN1 = d.toISOString(); // return 2022-11-24T07:58:59.000999999Z + var dIsoN1 = d.toISOString(); // return 2022-11-24T09:58:59.000999999Z d.addNanos(-1000000); - var dIsoN2 = d.toISOString(); // return 2022-11-24T07:58:58.999999999Z + var dIsoN2 = d.toISOString(); // return 2022-11-24T09:58:58.999999999Z return { "dIsoY1": dIsoY1, "dIsoY2": dIsoY2, @@ -2299,27 +2302,45 @@ class TbelInvokeDocsIoTest extends AbstractTbelInvokeTest { "dIsoN1": dIsoN1, "dIsoN2": dIsoN2 } - """); + """, stringDateUTC); LinkedHashMap expected = new LinkedHashMap<>(); - expected.put("dIsoY1", "2024-01-01T08:00:00Z"); - expected.put("dIsoY2", "2025-01-01T08:00:00Z"); - expected.put("dIsoY3", "2023-01-01T08:00:00Z"); - expected.put("dIsoM1", "2023-03-01T08:00:00Z"); - expected.put("dIsoM2", "2024-01-01T08:00:00Z"); - expected.put("dIsoM3", "2022-12-01T08:00:00Z"); - expected.put("dIsoW1", "2022-12-29T08:00:00Z"); - expected.put("dIsoW2", "2022-11-24T08:00:00Z"); - expected.put("dIsoD1", "2022-11-30T08:00:00Z"); - expected.put("dIsoD2", "2023-01-14T08:00:00Z"); - expected.put("dIsoD3", "2022-11-25T08:00:00Z"); - expected.put("dIsoH1", "2022-11-26T07:00:00Z"); - expected.put("dIsoH2", "2022-11-24T08:00:00Z"); - expected.put("dIsoMin1", "2022-11-24T08:59:00Z"); - expected.put("dIsoMin2", "2022-11-24T07:59:00Z"); - expected.put("dIsoS1", "2022-11-24T07:59:59Z"); - expected.put("dIsoS2", "2022-11-24T07:58:59Z"); - expected.put("dIsoN1", "2022-11-24T07:58:59.000999999Z"); - expected.put("dIsoN2", "2022-11-24T07:58:58.999999999Z"); + expected.put("dIsoY1", d.toISOString()); + d.addYears(1); + expected.put("dIsoY2", d.toISOString()); + d.addYears(-2); + expected.put("dIsoY3", d.toISOString()); + d.addMonths(2); + expected.put("dIsoM1", d.toISOString()); + d.addMonths(10); + expected.put("dIsoM2", d.toISOString()); + d.addMonths(-13); + expected.put("dIsoM3", d.toISOString()); + d.addWeeks(4); + expected.put("dIsoW1", d.toISOString()); + d.addWeeks(-5); + expected.put("dIsoW2", d.toISOString()); + d.addDays(6); + expected.put("dIsoD1", d.toISOString()); + d.addDays(45); + expected.put("dIsoD2", d.toISOString()); + d.addDays(-50); + expected.put("dIsoD3", d.toISOString()); + d.addHours(23); + expected.put("dIsoH1", d.toISOString()); + d.addHours(-47); + expected.put("dIsoH2", d.toISOString()); + d.addMinutes(59); + expected.put("dIsoMin1", d.toISOString()); + d.addMinutes(-60); + expected.put("dIsoMin2", d.toISOString()); + d.addSeconds(59); + expected.put("dIsoS1", d.toISOString()); + d.addSeconds(-60); + expected.put("dIsoS2", d.toISOString()); + d.addNanos(999999); + expected.put("dIsoN1", d.toISOString()); + d.addNanos(-1000000); + expected.put("dIsoN2", d.toISOString()); Object actual = invokeScript(evalScript(decoderStr), msgStr); assertEquals(expected, actual); } From 32a4a7ce35627e4fae92739587df37acf2e01bf9 Mon Sep 17 00:00:00 2001 From: ViacheslavKlimov Date: Wed, 5 Mar 2025 15:26:58 +0200 Subject: [PATCH 201/345] EDQS: introduce API auto-enable option --- .../service/edqs/DefaultEdqsApiService.java | 11 ++++++++++- .../server/service/edqs/DefaultEdqsService.java | 16 ++++++++++------ application/src/main/resources/thingsboard.yml | 7 +++++-- .../EdqsEntityQueryControllerTest.java | 3 ++- .../service/entitiy/EdqsEntityServiceTest.java | 3 ++- .../test/resources/application-test.properties | 2 +- .../server/common/msg/edqs/EdqsApiService.java | 2 ++ .../dao/sql/query/DummyEdqsApiService.java | 5 +++++ docker/tb-core-edqs.env | 2 +- 9 files changed, 38 insertions(+), 13 deletions(-) diff --git a/application/src/main/java/org/thingsboard/server/service/edqs/DefaultEdqsApiService.java b/application/src/main/java/org/thingsboard/server/service/edqs/DefaultEdqsApiService.java index d78ea85fa7..51c963ed2f 100644 --- a/application/src/main/java/org/thingsboard/server/service/edqs/DefaultEdqsApiService.java +++ b/application/src/main/java/org/thingsboard/server/service/edqs/DefaultEdqsApiService.java @@ -22,6 +22,7 @@ import jakarta.annotation.PostConstruct; import jakarta.annotation.PreDestroy; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Value; import org.springframework.boot.autoconfigure.condition.ConditionalOnExpression; import org.springframework.stereotype.Service; import org.thingsboard.common.util.JacksonUtil; @@ -43,13 +44,16 @@ import java.util.UUID; @Service @Slf4j @RequiredArgsConstructor -@ConditionalOnExpression("'${queue.edqs.api_enabled:true}' == 'true' && ('${service.type:null}' == 'monolith' || '${service.type:null}' == 'tb-core')") +@ConditionalOnExpression("'${queue.edqs.api.supported:true}' == 'true' && ('${service.type:null}' == 'monolith' || '${service.type:null}' == 'tb-core')") public class DefaultEdqsApiService implements EdqsApiService { private final EdqsPartitionService edqsPartitionService; private final EdqsClientQueueFactory queueFactory; private TbQueueRequestTemplate, TbProtoQueueMsg> requestTemplate; + @Value("${queue.edqs.api.auto_enable:true}") + private boolean autoEnable; + private Boolean apiEnabled = null; @PostConstruct @@ -100,6 +104,11 @@ public class DefaultEdqsApiService implements EdqsApiService { return true; } + @Override + public boolean isAutoEnable() { + return autoEnable; + } + @PreDestroy private void stop() { requestTemplate.stop(); diff --git a/application/src/main/java/org/thingsboard/server/service/edqs/DefaultEdqsService.java b/application/src/main/java/org/thingsboard/server/service/edqs/DefaultEdqsService.java index 65f75c1aa4..e823dee4e7 100644 --- a/application/src/main/java/org/thingsboard/server/service/edqs/DefaultEdqsService.java +++ b/application/src/main/java/org/thingsboard/server/service/edqs/DefaultEdqsService.java @@ -118,7 +118,7 @@ public class DefaultEdqsService implements EdqsService { .syncRequest(new EdqsSyncRequest()) .build()); } - } else if (edqsApiService.isSupported()) { + } else if (edqsApiService.isSupported() && edqsApiService.isAutoEnable()) { // only if topic/RocksDB is not empty and sync is finished edqsApiService.setEnabled(true); } @@ -159,11 +159,15 @@ public class DefaultEdqsService implements EdqsService { edqsSyncService.sync(); saveSyncState(EdqsSyncStatus.FINISHED); - if (edqsApiService.isSupported()) { - broadcast(ToCoreEdqsMsg.builder() - .apiEnabled(Boolean.TRUE) - .build()); - } + if (edqsApiService.isSupported()) + if (edqsApiService.isAutoEnable()) { + log.info("EDQS sync is finished, auto-enabling API"); + broadcast(ToCoreEdqsMsg.builder() + .apiEnabled(Boolean.TRUE) + .build()); + } else { + log.info("EDQS sync is finished, but leaving API disabled"); + } } catch (Exception e) { log.error("Failed to complete sync", e); saveSyncState(EdqsSyncStatus.FAILED); diff --git a/application/src/main/resources/thingsboard.yml b/application/src/main/resources/thingsboard.yml index 7e4e0708d8..b01a0cf55e 100644 --- a/application/src/main/resources/thingsboard.yml +++ b/application/src/main/resources/thingsboard.yml @@ -1731,8 +1731,11 @@ queue: entity_batch_size: "${TB_EDQS_SYNC_ENTITY_BATCH_SIZE:10000}" # Batch size of timeseries data being synced with EDQS ts_batch_size: "${TB_EDQS_SYNC_TS_BATCH_SIZE:10000}" - # Whether to forward entity data query requests to EDQS (otherwise use PostgreSQL implementation) - api_enabled: "${TB_EDQS_API_ENABLED:false}" + api: + # Whether to forward entity data query requests to EDQS (otherwise use PostgreSQL implementation) + supported: "${TB_EDQS_API_SUPPORTED:false}" + # Whether to auto-enable EDQS API (if queue.edqs.api.supported is true) when sync of data to Kafka is finished + auto_enable: "${TB_EDQS_API_AUTO_ENABLE:true}" # Mode of EDQS: local (for monolith) or remote (with separate EDQS microservices) mode: "${TB_EDQS_MODE:local}" local: diff --git a/application/src/test/java/org/thingsboard/server/controller/EdqsEntityQueryControllerTest.java b/application/src/test/java/org/thingsboard/server/controller/EdqsEntityQueryControllerTest.java index a3ce628406..ce5221ab89 100644 --- a/application/src/test/java/org/thingsboard/server/controller/EdqsEntityQueryControllerTest.java +++ b/application/src/test/java/org/thingsboard/server/controller/EdqsEntityQueryControllerTest.java @@ -37,7 +37,8 @@ import static org.awaitility.Awaitility.await; // "queue.type=kafka", // uncomment to use Kafka // "queue.kafka.bootstrap.servers=10.7.1.254:9092", "queue.edqs.sync.enabled=true", - "queue.edqs.api_enabled=true", + "queue.edqs.api.supported=true", + "queue.edqs.api.auto_enable=true", "queue.edqs.mode=local" }) public class EdqsEntityQueryControllerTest extends EntityQueryControllerTest { diff --git a/application/src/test/java/org/thingsboard/server/service/entitiy/EdqsEntityServiceTest.java b/application/src/test/java/org/thingsboard/server/service/entitiy/EdqsEntityServiceTest.java index 7f29821afd..4d444ad637 100644 --- a/application/src/test/java/org/thingsboard/server/service/entitiy/EdqsEntityServiceTest.java +++ b/application/src/test/java/org/thingsboard/server/service/entitiy/EdqsEntityServiceTest.java @@ -51,7 +51,8 @@ import static org.awaitility.Awaitility.await; @DaoSqlTest @TestPropertySource(properties = { "queue.edqs.sync.enabled=true", - "queue.edqs.api_enabled=true", + "queue.edqs.api.supported=true", + "queue.edqs.api.auto_enable=true", "queue.edqs.mode=local" }) public class EdqsEntityServiceTest extends EntityServiceTest { diff --git a/application/src/test/resources/application-test.properties b/application/src/test/resources/application-test.properties index 3d0c377c2e..b8bdcf67e6 100644 --- a/application/src/test/resources/application-test.properties +++ b/application/src/test/resources/application-test.properties @@ -60,4 +60,4 @@ server.log_controller_error_stack_trace=false transport.gateway.dashboard.sync.enabled=false queue.edqs.sync.enabled=false -queue.edqs.api_enabled=false +queue.edqs.api.supported=false diff --git a/common/message/src/main/java/org/thingsboard/server/common/msg/edqs/EdqsApiService.java b/common/message/src/main/java/org/thingsboard/server/common/msg/edqs/EdqsApiService.java index 58a86823ca..05864fe863 100644 --- a/common/message/src/main/java/org/thingsboard/server/common/msg/edqs/EdqsApiService.java +++ b/common/message/src/main/java/org/thingsboard/server/common/msg/edqs/EdqsApiService.java @@ -31,4 +31,6 @@ public interface EdqsApiService { boolean isSupported(); + boolean isAutoEnable(); + } diff --git a/dao/src/main/java/org/thingsboard/server/dao/sql/query/DummyEdqsApiService.java b/dao/src/main/java/org/thingsboard/server/dao/sql/query/DummyEdqsApiService.java index 7b3aed253c..e486d3b645 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/sql/query/DummyEdqsApiService.java +++ b/dao/src/main/java/org/thingsboard/server/dao/sql/query/DummyEdqsApiService.java @@ -50,4 +50,9 @@ public class DummyEdqsApiService implements EdqsApiService { return false; } + @Override + public boolean isAutoEnable() { + return false; + } + } diff --git a/docker/tb-core-edqs.env b/docker/tb-core-edqs.env index 1417b780ec..45472ec907 100644 --- a/docker/tb-core-edqs.env +++ b/docker/tb-core-edqs.env @@ -2,4 +2,4 @@ TB_EDQS_MODE=remote TB_EDQS_SYNC_ENABLED=true -TB_EDQS_API_ENABLED=true \ No newline at end of file +TB_EDQS_API_SUPPORTED=true From c404dd2cf593e81cf599a21dab482eec031210fd Mon Sep 17 00:00:00 2001 From: IrynaMatveieva Date: Wed, 5 Mar 2025 15:42:26 +0200 Subject: [PATCH 202/345] added check for single value size --- .../CalculatedFieldEntityMessageProcessor.java | 2 +- ...DefaultCalculatedFieldProcessingService.java | 2 +- .../cf/ctx/state/BaseCalculatedFieldState.java | 17 ++++++++++++++++- .../cf/ctx/state/CalculatedFieldCtx.java | 2 ++ .../cf/ctx/state/CalculatedFieldState.java | 4 +++- .../state/ScriptCalculatedFieldStateTest.java | 4 ++-- .../state/SimpleCalculatedFieldStateTest.java | 6 +++--- 7 files changed, 28 insertions(+), 9 deletions(-) diff --git a/application/src/main/java/org/thingsboard/server/actors/calculatedField/CalculatedFieldEntityMessageProcessor.java b/application/src/main/java/org/thingsboard/server/actors/calculatedField/CalculatedFieldEntityMessageProcessor.java index e42ebb593e..c75b47c8a0 100644 --- a/application/src/main/java/org/thingsboard/server/actors/calculatedField/CalculatedFieldEntityMessageProcessor.java +++ b/application/src/main/java/org/thingsboard/server/actors/calculatedField/CalculatedFieldEntityMessageProcessor.java @@ -242,7 +242,7 @@ public class CalculatedFieldEntityMessageProcessor extends AbstractContextAwareM justRestored = true; } if (state.isSizeOk()) { - if (state.updateState(newArgValues) || justRestored) { + if (state.updateState(newArgValues, ctx) || justRestored) { cfIdList = new ArrayList<>(cfIdList); cfIdList.add(ctx.getCfId()); processStateIfReady(ctx, cfIdList, state, tbMsgId, tbMsgType, callback); diff --git a/application/src/main/java/org/thingsboard/server/service/cf/DefaultCalculatedFieldProcessingService.java b/application/src/main/java/org/thingsboard/server/service/cf/DefaultCalculatedFieldProcessingService.java index f69f5cd50b..2f73788fad 100644 --- a/application/src/main/java/org/thingsboard/server/service/cf/DefaultCalculatedFieldProcessingService.java +++ b/application/src/main/java/org/thingsboard/server/service/cf/DefaultCalculatedFieldProcessingService.java @@ -136,7 +136,7 @@ public class DefaultCalculatedFieldProcessingService implements CalculatedFieldP throw new RuntimeException("Error getting future result for key: " + entry.getKey(), e); } } - ))); + )), ctx); return result; }, calculatedFieldCallbackExecutor); } diff --git a/application/src/main/java/org/thingsboard/server/service/cf/ctx/state/BaseCalculatedFieldState.java b/application/src/main/java/org/thingsboard/server/service/cf/ctx/state/BaseCalculatedFieldState.java index d59a58f296..b2a4321961 100644 --- a/application/src/main/java/org/thingsboard/server/service/cf/ctx/state/BaseCalculatedFieldState.java +++ b/application/src/main/java/org/thingsboard/server/service/cf/ctx/state/BaseCalculatedFieldState.java @@ -25,6 +25,8 @@ import java.util.HashMap; import java.util.List; import java.util.Map; +import static org.thingsboard.server.utils.CalculatedFieldUtils.toSingleValueArgumentProto; + @Data @AllArgsConstructor public abstract class BaseCalculatedFieldState implements CalculatedFieldState { @@ -43,7 +45,7 @@ public abstract class BaseCalculatedFieldState implements CalculatedFieldState { } @Override - public boolean updateState(Map argumentValues) { + public boolean updateState(Map argumentValues, CalculatedFieldCtx ctx) { if (arguments == null) { arguments = new HashMap<>(); } @@ -53,6 +55,9 @@ public abstract class BaseCalculatedFieldState implements CalculatedFieldState { for (Map.Entry entry : argumentValues.entrySet()) { String key = entry.getKey(); ArgumentEntry newEntry = entry.getValue(); + + checkArgumentSize(key, newEntry, ctx); + ArgumentEntry existingEntry = arguments.get(key); if (existingEntry == null || newEntry.isForceResetPrevious()) { @@ -81,6 +86,16 @@ public abstract class BaseCalculatedFieldState implements CalculatedFieldState { } } + @Override + public void checkArgumentSize(String name, ArgumentEntry entry, CalculatedFieldCtx ctx) { + if (entry instanceof TsRollingArgumentEntry) { + return; + } + if (ctx.getMaxSingleValueArgumentSize() > 0 && toSingleValueArgumentProto(name, (SingleValueArgumentEntry) entry).getSerializedSize() > ctx.getMaxSingleValueArgumentSize()) { + throw new IllegalArgumentException("Single value size exceeds the maximum allowed limit. The argument will not be used for calculation."); + } + } + protected abstract void validateNewEntry(ArgumentEntry newEntry); } diff --git a/application/src/main/java/org/thingsboard/server/service/cf/ctx/state/CalculatedFieldCtx.java b/application/src/main/java/org/thingsboard/server/service/cf/ctx/state/CalculatedFieldCtx.java index d3b7020c25..399e0d9f84 100644 --- a/application/src/main/java/org/thingsboard/server/service/cf/ctx/state/CalculatedFieldCtx.java +++ b/application/src/main/java/org/thingsboard/server/service/cf/ctx/state/CalculatedFieldCtx.java @@ -71,6 +71,7 @@ public class CalculatedFieldCtx { private long maxDataPointsPerRollingArg; private long maxStateSize; + private long maxSingleValueArgumentSize; public CalculatedFieldCtx(CalculatedField calculatedField, TbelInvokeService tbelInvokeService, ApiLimitService apiLimitService) { this.calculatedField = calculatedField; @@ -104,6 +105,7 @@ public class CalculatedFieldCtx { this.maxDataPointsPerRollingArg = apiLimitService.getLimit(tenantId, DefaultTenantProfileConfiguration::getMaxDataPointsPerRollingArg); this.maxStateSize = apiLimitService.getLimit(tenantId, DefaultTenantProfileConfiguration::getMaxStateSizeInKBytes) * 1024; + this.maxSingleValueArgumentSize = apiLimitService.getLimit(tenantId, DefaultTenantProfileConfiguration::getMaxSingleValueArgumentSizeInKBytes) * 1024; } public void init() { diff --git a/application/src/main/java/org/thingsboard/server/service/cf/ctx/state/CalculatedFieldState.java b/application/src/main/java/org/thingsboard/server/service/cf/ctx/state/CalculatedFieldState.java index 70587d8b1e..501cb5f54c 100644 --- a/application/src/main/java/org/thingsboard/server/service/cf/ctx/state/CalculatedFieldState.java +++ b/application/src/main/java/org/thingsboard/server/service/cf/ctx/state/CalculatedFieldState.java @@ -44,7 +44,7 @@ public interface CalculatedFieldState { void setRequiredArguments(List requiredArguments); - boolean updateState(Map argumentValues); + boolean updateState(Map argumentValues, CalculatedFieldCtx ctx); ListenableFuture performCalculation(CalculatedFieldCtx ctx); @@ -60,4 +60,6 @@ public interface CalculatedFieldState { void checkStateSize(CalculatedFieldEntityCtxId ctxId, long maxStateSize); + void checkArgumentSize(String name, ArgumentEntry entry, CalculatedFieldCtx ctx); + } diff --git a/application/src/test/java/org/thingsboard/server/service/cf/ctx/state/ScriptCalculatedFieldStateTest.java b/application/src/test/java/org/thingsboard/server/service/cf/ctx/state/ScriptCalculatedFieldStateTest.java index 2cced15f59..78144a0150 100644 --- a/application/src/test/java/org/thingsboard/server/service/cf/ctx/state/ScriptCalculatedFieldStateTest.java +++ b/application/src/test/java/org/thingsboard/server/service/cf/ctx/state/ScriptCalculatedFieldStateTest.java @@ -90,7 +90,7 @@ public class ScriptCalculatedFieldStateTest { state.arguments = new HashMap<>(Map.of("assetHumidity", assetHumidityArgEntry)); Map newArgs = Map.of("deviceTemperature", deviceTemperatureArgEntry); - boolean stateUpdated = state.updateState(newArgs); + boolean stateUpdated = state.updateState(newArgs, ctx); assertThat(stateUpdated).isTrue(); assertThat(state.getArguments()).containsExactlyInAnyOrderEntriesOf( @@ -107,7 +107,7 @@ public class ScriptCalculatedFieldStateTest { SingleValueArgumentEntry newArgEntry = new SingleValueArgumentEntry(ts, new LongDataEntry("assetHumidity", 41L), 349L); Map newArgs = Map.of("assetHumidity", newArgEntry); - boolean stateUpdated = state.updateState(newArgs); + boolean stateUpdated = state.updateState(newArgs, ctx); assertThat(stateUpdated).isTrue(); assertThat(state.getArguments()).containsExactlyInAnyOrderEntriesOf( diff --git a/application/src/test/java/org/thingsboard/server/service/cf/ctx/state/SimpleCalculatedFieldStateTest.java b/application/src/test/java/org/thingsboard/server/service/cf/ctx/state/SimpleCalculatedFieldStateTest.java index e179e8e706..f146eb434f 100644 --- a/application/src/test/java/org/thingsboard/server/service/cf/ctx/state/SimpleCalculatedFieldStateTest.java +++ b/application/src/test/java/org/thingsboard/server/service/cf/ctx/state/SimpleCalculatedFieldStateTest.java @@ -88,7 +88,7 @@ public class SimpleCalculatedFieldStateTest { )); Map newArgs = Map.of("key3", key3ArgEntry); - boolean stateUpdated = state.updateState(newArgs); + boolean stateUpdated = state.updateState(newArgs, ctx); assertThat(stateUpdated).isTrue(); assertThat(state.getArguments()).containsExactlyInAnyOrderEntriesOf( @@ -106,7 +106,7 @@ public class SimpleCalculatedFieldStateTest { SingleValueArgumentEntry newArgEntry = new SingleValueArgumentEntry(System.currentTimeMillis(), new LongDataEntry("key1", 18L), 190L); Map newArgs = Map.of("key1", newArgEntry); - boolean stateUpdated = state.updateState(newArgs); + boolean stateUpdated = state.updateState(newArgs, ctx); assertThat(stateUpdated).isTrue(); assertThat(state.getArguments()).containsExactlyInAnyOrderEntriesOf(Map.of("key1", newArgEntry)); @@ -120,7 +120,7 @@ public class SimpleCalculatedFieldStateTest { )); Map newArgs = Map.of("key3", new TsRollingArgumentEntry(10, 30000L)); - assertThatThrownBy(() -> state.updateState(newArgs)) + assertThatThrownBy(() -> state.updateState(newArgs, ctx)) .isInstanceOf(IllegalArgumentException.class) .hasMessage("Rolling argument entry is not supported for simple calculated fields."); } From 547bb7e169cd1dd699013dbac0842a9abbec3fde Mon Sep 17 00:00:00 2001 From: mpetrov Date: Wed, 5 Mar 2025 16:18:06 +0200 Subject: [PATCH 203/345] refactoring --- .../calculated-fields/calculated-fields-table-config.ts | 4 ++-- .../components/dialog/calculated-field-dialog.component.html | 1 + ui-ngx/src/app/shared/models/calculated-field.models.ts | 2 +- 3 files changed, 4 insertions(+), 3 deletions(-) diff --git a/ui-ngx/src/app/modules/home/components/calculated-fields/calculated-fields-table-config.ts b/ui-ngx/src/app/modules/home/components/calculated-fields/calculated-fields-table-config.ts index 400e9bad1c..cd9f0373db 100644 --- a/ui-ngx/src/app/modules/home/components/calculated-fields/calculated-fields-table-config.ts +++ b/ui-ngx/src/app/modules/home/components/calculated-fields/calculated-fields-table-config.ts @@ -120,11 +120,11 @@ export class CalculatedFieldsTableConfig extends EntityTableConfig { const expressionLabel = this.getExpressionLabel(entity); - return expressionLabel.length < 50 ? expressionLabel : `${expressionLabel.substring(0, 49)}…`; + return expressionLabel.length < 45 ? expressionLabel : `${expressionLabel.substring(0, 44)}…`; } expressionColumn.cellTooltipFunction = entity => { const expressionLabel = this.getExpressionLabel(entity); - return expressionLabel.length < 50 ? null : expressionLabel + return expressionLabel.length < 45 ? null : expressionLabel }; this.columns.push(new DateEntityTableColumn('createdTime', 'common.created-time', this.datePipe, '150px')); 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 a136d3ea8b..471c722145 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 @@ -98,6 +98,7 @@ formControlName="expressionSCRIPT" functionName="calculate" [functionArgs]="functionArgs$ | async" + [disableUndefinedCheck]="true" [scriptLanguage]="ScriptLanguage.TBEL" [highlightRules]="argumentsHighlightRules$ | async" [editorCompleter]="argumentsEditorCompleter$ | async" diff --git a/ui-ngx/src/app/shared/models/calculated-field.models.ts b/ui-ngx/src/app/shared/models/calculated-field.models.ts index d7c6754b9a..7083ce7ae0 100644 --- a/ui-ngx/src/app/shared/models/calculated-field.models.ts +++ b/ui-ngx/src/app/shared/models/calculated-field.models.ts @@ -608,5 +608,5 @@ const calculatedFieldTimeWindowArgumentValueHighlightRules: AceHighlightRules = export const calculatedFieldDefaultScript = 'return {\n' + ' // Convert Fahrenheit to Celsius\n' + - ' "temperatureC": (temperature - 32) / 1.8\n' + + ' "temperatureCelsius": (temperature.value - 32) / 1.8\n' + '};' From fdad68997ae67ed1ec91ab4c09491f0055c82e4f Mon Sep 17 00:00:00 2001 From: mpetrov Date: Wed, 5 Mar 2025 16:19:20 +0200 Subject: [PATCH 204/345] refactoring --- .../components/dialog/calculated-field-dialog.component.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ui-ngx/src/app/modules/home/components/calculated-fields/components/dialog/calculated-field-dialog.component.ts b/ui-ngx/src/app/modules/home/components/calculated-fields/components/dialog/calculated-field-dialog.component.ts index 127c1c8c1c..0c3e7464cd 100644 --- a/ui-ngx/src/app/modules/home/components/calculated-fields/components/dialog/calculated-field-dialog.component.ts +++ b/ui-ngx/src/app/modules/home/components/calculated-fields/components/dialog/calculated-field-dialog.component.ts @@ -128,7 +128,7 @@ export class CalculatedFieldDialogComponent extends DialogComponent Date: Wed, 5 Mar 2025 17:12:26 +0200 Subject: [PATCH 205/345] added calculated fields rule node --- .../server/actors/ActorSystemContext.java | 6 + .../actors/ruleChain/DefaultTbContext.java | 6 + .../DefaultCalculatedFieldQueueService.java | 48 +++- .../ctx/state/SingleValueArgumentEntry.java | 8 +- .../DefaultTelemetrySubscriptionService.java | 2 +- ...faultTelemetrySubscriptionServiceTest.java | 2 +- .../api}/CalculatedFieldQueueService.java | 10 +- .../rule/engine/api/TbContext.java | 2 + .../telemetry/TbCalculatedFieldsNode.java | 212 ++++++++++++++++++ 9 files changed, 279 insertions(+), 17 deletions(-) rename {application/src/main/java/org/thingsboard/server/service/cf => rule-engine/rule-engine-api/src/main/java/org/thingsboard/rule/engine/api}/CalculatedFieldQueueService.java (83%) create mode 100644 rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/telemetry/TbCalculatedFieldsNode.java diff --git a/application/src/main/java/org/thingsboard/server/actors/ActorSystemContext.java b/application/src/main/java/org/thingsboard/server/actors/ActorSystemContext.java index de3864df9f..442ff1d11f 100644 --- a/application/src/main/java/org/thingsboard/server/actors/ActorSystemContext.java +++ b/application/src/main/java/org/thingsboard/server/actors/ActorSystemContext.java @@ -31,6 +31,7 @@ import org.springframework.data.redis.core.RedisTemplate; import org.springframework.scheduling.annotation.Scheduled; import org.springframework.stereotype.Component; import org.thingsboard.common.util.JacksonUtil; +import org.thingsboard.rule.engine.api.CalculatedFieldQueueService; import org.thingsboard.rule.engine.api.MailService; import org.thingsboard.rule.engine.api.NotificationCenter; import org.thingsboard.rule.engine.api.RuleEngineDeviceStateManager; @@ -544,6 +545,11 @@ public class ActorSystemContext { @Getter private CalculatedFieldStateService calculatedFieldStateService; + @Lazy + @Autowired(required = false) + @Getter + private CalculatedFieldQueueService calculatedFieldQueueService; + @Lazy @Autowired(required = false) @Getter diff --git a/application/src/main/java/org/thingsboard/server/actors/ruleChain/DefaultTbContext.java b/application/src/main/java/org/thingsboard/server/actors/ruleChain/DefaultTbContext.java index d162b6a09a..a5b43d3223 100644 --- a/application/src/main/java/org/thingsboard/server/actors/ruleChain/DefaultTbContext.java +++ b/application/src/main/java/org/thingsboard/server/actors/ruleChain/DefaultTbContext.java @@ -23,6 +23,7 @@ import org.bouncycastle.util.Arrays; import org.thingsboard.common.util.DebugModeUtil; import org.thingsboard.common.util.JacksonUtil; import org.thingsboard.common.util.ListeningExecutor; +import org.thingsboard.rule.engine.api.CalculatedFieldQueueService; import org.thingsboard.rule.engine.api.MailService; import org.thingsboard.rule.engine.api.NotificationCenter; import org.thingsboard.rule.engine.api.RuleEngineAlarmService; @@ -902,6 +903,11 @@ public class DefaultTbContext implements TbContext { return mainCtx.getCalculatedFieldService(); } + @Override + public CalculatedFieldQueueService getCalculatedFieldQueueService() { + return mainCtx.getCalculatedFieldQueueService(); + } + @Override public boolean isExternalNodeForceAck() { return mainCtx.isExternalNodeForceAck(); diff --git a/application/src/main/java/org/thingsboard/server/service/cf/DefaultCalculatedFieldQueueService.java b/application/src/main/java/org/thingsboard/server/service/cf/DefaultCalculatedFieldQueueService.java index 3ca0b220ff..c1ffb10e79 100644 --- a/application/src/main/java/org/thingsboard/server/service/cf/DefaultCalculatedFieldQueueService.java +++ b/application/src/main/java/org/thingsboard/server/service/cf/DefaultCalculatedFieldQueueService.java @@ -21,6 +21,7 @@ import lombok.extern.slf4j.Slf4j; import org.springframework.stereotype.Service; import org.thingsboard.rule.engine.api.AttributesDeleteRequest; import org.thingsboard.rule.engine.api.AttributesSaveRequest; +import org.thingsboard.rule.engine.api.CalculatedFieldQueueService; import org.thingsboard.rule.engine.api.TimeseriesDeleteRequest; import org.thingsboard.rule.engine.api.TimeseriesSaveRequest; import org.thingsboard.server.cluster.TbClusterService; @@ -47,6 +48,7 @@ import org.thingsboard.server.service.cf.ctx.state.CalculatedFieldCtx; import org.thingsboard.server.service.profile.TbAssetProfileCache; import org.thingsboard.server.service.profile.TbDeviceProfileCache; +import java.util.Collections; import java.util.EnumSet; import java.util.List; import java.util.Set; @@ -89,6 +91,14 @@ public class DefaultCalculatedFieldQueueService implements CalculatedFieldQueueS () -> toCalculatedFieldTelemetryMsgProto(request, result), callback); } + @Override + public void pushRequestToQueue(TimeseriesSaveRequest request, FutureCallback callback) { + var tenantId = request.getTenantId(); + var entityId = request.getEntityId(); + checkEntityAndPushToQueue(tenantId, entityId, cf -> cf.matches(request.getEntries()), cf -> cf.linkMatches(entityId, request.getEntries()), + () -> toCalculatedFieldTelemetryMsgProto(request), callback); + } + @Override public void pushRequestToQueue(AttributesSaveRequest request, List result, FutureCallback callback) { var tenantId = request.getTenantId(); @@ -97,6 +107,14 @@ public class DefaultCalculatedFieldQueueService implements CalculatedFieldQueueS () -> toCalculatedFieldTelemetryMsgProto(request, result), callback); } + @Override + public void pushRequestToQueue(AttributesSaveRequest request, FutureCallback callback) { + var tenantId = request.getTenantId(); + var entityId = request.getEntityId(); + checkEntityAndPushToQueue(tenantId, entityId, cf -> cf.matches(request.getEntries(), request.getScope()), cf -> cf.linkMatches(entityId, request.getEntries(), request.getScope()), + () -> toCalculatedFieldTelemetryMsgProto(request), callback); + } + @Override public void pushRequestToQueue(AttributesDeleteRequest request, List result, FutureCallback callback) { var tenantId = request.getTenantId(); @@ -152,22 +170,34 @@ public class DefaultCalculatedFieldQueueService implements CalculatedFieldQueueS }; } + private ToCalculatedFieldMsg toCalculatedFieldTelemetryMsgProto(TimeseriesSaveRequest request) { + return toCalculatedFieldTelemetryMsgProto(request, null); + } + private ToCalculatedFieldMsg toCalculatedFieldTelemetryMsgProto(TimeseriesSaveRequest request, TimeseriesSaveResult result) { ToCalculatedFieldMsg.Builder msg = ToCalculatedFieldMsg.newBuilder(); CalculatedFieldTelemetryMsgProto.Builder telemetryMsg = buildTelemetryMsgProto(request.getTenantId(), request.getEntityId(), request.getPreviousCalculatedFieldIds(), request.getTbMsgId(), request.getTbMsgType()); + List entries = request.getEntries(); - List versions = result.getVersions(); + List versions = result != null ? result.getVersions() : Collections.emptyList(); + for (int i = 0; i < entries.size(); i++) { - long tsVersion = versions.get(i); - TsKvProto tsProto = toTsKvProto(entries.get(i)).toBuilder().setVersion(tsVersion).build(); - telemetryMsg.addTsData(tsProto); + TsKvProto.Builder tsProtoBuilder = toTsKvProto(entries.get(i)).toBuilder(); + if (result != null) { + tsProtoBuilder.setVersion(versions.get(i)); + } + telemetryMsg.addTsData(tsProtoBuilder.build()); } - msg.setTelemetryMsg(telemetryMsg.build()); + msg.setTelemetryMsg(telemetryMsg.build()); return msg.build(); } + private ToCalculatedFieldMsg toCalculatedFieldTelemetryMsgProto(AttributesSaveRequest request) { + return toCalculatedFieldTelemetryMsgProto(request, null); + } + private ToCalculatedFieldMsg toCalculatedFieldTelemetryMsgProto(AttributesSaveRequest request, List versions) { ToCalculatedFieldMsg.Builder msg = ToCalculatedFieldMsg.newBuilder(); @@ -175,9 +205,11 @@ public class DefaultCalculatedFieldQueueService implements CalculatedFieldQueueS telemetryMsg.setScope(AttributeScopeProto.valueOf(request.getScope().name())); List entries = request.getEntries(); for (int i = 0; i < entries.size(); i++) { - long attrVersion = versions.get(i); - AttributeValueProto attrProto = ProtoUtils.toProto(entries.get(i)).toBuilder().setVersion(attrVersion).build(); - telemetryMsg.addAttrData(attrProto); + AttributeValueProto.Builder attrProtoBuilder = ProtoUtils.toProto(entries.get(i)).toBuilder(); + if (versions != null) { + attrProtoBuilder.setVersion(versions.get(i)); + } + telemetryMsg.addAttrData(attrProtoBuilder.build()); } msg.setTelemetryMsg(telemetryMsg.build()); diff --git a/application/src/main/java/org/thingsboard/server/service/cf/ctx/state/SingleValueArgumentEntry.java b/application/src/main/java/org/thingsboard/server/service/cf/ctx/state/SingleValueArgumentEntry.java index a237f3d022..bfe9eed24f 100644 --- a/application/src/main/java/org/thingsboard/server/service/cf/ctx/state/SingleValueArgumentEntry.java +++ b/application/src/main/java/org/thingsboard/server/service/cf/ctx/state/SingleValueArgumentEntry.java @@ -42,13 +42,17 @@ public class SingleValueArgumentEntry implements ArgumentEntry { public SingleValueArgumentEntry(TsKvProto entry) { this.ts = entry.getTs(); - this.version = entry.getVersion(); + if (entry.hasVersion()) { + this.version = entry.getVersion(); + } this.kvEntryValue = ProtoUtils.fromProto(entry.getKv()); } public SingleValueArgumentEntry(AttributeValueProto entry) { this.ts = entry.getLastUpdateTs(); - this.version = entry.getVersion(); + if (entry.hasVersion()) { + this.version = entry.getVersion(); + } this.kvEntryValue = ProtoUtils.basicKvEntryFromProto(entry); } diff --git a/application/src/main/java/org/thingsboard/server/service/telemetry/DefaultTelemetrySubscriptionService.java b/application/src/main/java/org/thingsboard/server/service/telemetry/DefaultTelemetrySubscriptionService.java index 2bdbd917ab..31ae9e8055 100644 --- a/application/src/main/java/org/thingsboard/server/service/telemetry/DefaultTelemetrySubscriptionService.java +++ b/application/src/main/java/org/thingsboard/server/service/telemetry/DefaultTelemetrySubscriptionService.java @@ -31,6 +31,7 @@ import org.thingsboard.common.util.DonAsynchron; import org.thingsboard.common.util.ThingsBoardThreadFactory; import org.thingsboard.rule.engine.api.AttributesDeleteRequest; import org.thingsboard.rule.engine.api.AttributesSaveRequest; +import org.thingsboard.rule.engine.api.CalculatedFieldQueueService; import org.thingsboard.rule.engine.api.RuleEngineTelemetryService; import org.thingsboard.rule.engine.api.TimeseriesDeleteRequest; import org.thingsboard.rule.engine.api.TimeseriesSaveRequest; @@ -50,7 +51,6 @@ import org.thingsboard.server.dao.attributes.AttributesService; import org.thingsboard.server.dao.timeseries.TimeseriesService; import org.thingsboard.server.dao.util.KvUtils; import org.thingsboard.server.service.apiusage.TbApiUsageStateService; -import org.thingsboard.server.service.cf.CalculatedFieldQueueService; import org.thingsboard.server.service.entitiy.entityview.TbEntityViewService; import org.thingsboard.server.service.subscription.TbSubscriptionUtils; diff --git a/application/src/test/java/org/thingsboard/server/service/telemetry/DefaultTelemetrySubscriptionServiceTest.java b/application/src/test/java/org/thingsboard/server/service/telemetry/DefaultTelemetrySubscriptionServiceTest.java index b8bfa13e50..b47bea18d8 100644 --- a/application/src/test/java/org/thingsboard/server/service/telemetry/DefaultTelemetrySubscriptionServiceTest.java +++ b/application/src/test/java/org/thingsboard/server/service/telemetry/DefaultTelemetrySubscriptionServiceTest.java @@ -28,6 +28,7 @@ import org.junit.jupiter.params.provider.MethodSource; import org.mockito.Mock; import org.mockito.junit.jupiter.MockitoExtension; import org.springframework.test.util.ReflectionTestUtils; +import org.thingsboard.rule.engine.api.CalculatedFieldQueueService; import org.thingsboard.rule.engine.api.TimeseriesSaveRequest; import org.thingsboard.server.cluster.TbClusterService; import org.thingsboard.server.common.data.ApiUsageRecordKey; @@ -56,7 +57,6 @@ import org.thingsboard.server.queue.discovery.PartitionService; import org.thingsboard.server.queue.discovery.QueueKey; import org.thingsboard.server.queue.discovery.event.PartitionChangeEvent; import org.thingsboard.server.service.apiusage.TbApiUsageStateService; -import org.thingsboard.server.service.cf.CalculatedFieldQueueService; import org.thingsboard.server.service.entitiy.entityview.TbEntityViewService; import org.thingsboard.server.service.subscription.SubscriptionManagerService; diff --git a/application/src/main/java/org/thingsboard/server/service/cf/CalculatedFieldQueueService.java b/rule-engine/rule-engine-api/src/main/java/org/thingsboard/rule/engine/api/CalculatedFieldQueueService.java similarity index 83% rename from application/src/main/java/org/thingsboard/server/service/cf/CalculatedFieldQueueService.java rename to rule-engine/rule-engine-api/src/main/java/org/thingsboard/rule/engine/api/CalculatedFieldQueueService.java index b688993a8d..b4eb24baea 100644 --- a/application/src/main/java/org/thingsboard/server/service/cf/CalculatedFieldQueueService.java +++ b/rule-engine/rule-engine-api/src/main/java/org/thingsboard/rule/engine/api/CalculatedFieldQueueService.java @@ -13,13 +13,9 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package org.thingsboard.server.service.cf; +package org.thingsboard.rule.engine.api; import com.google.common.util.concurrent.FutureCallback; -import org.thingsboard.rule.engine.api.AttributesDeleteRequest; -import org.thingsboard.rule.engine.api.AttributesSaveRequest; -import org.thingsboard.rule.engine.api.TimeseriesDeleteRequest; -import org.thingsboard.rule.engine.api.TimeseriesSaveRequest; import org.thingsboard.server.common.data.kv.TimeseriesSaveResult; import java.util.List; @@ -34,8 +30,12 @@ public interface CalculatedFieldQueueService { */ void pushRequestToQueue(TimeseriesSaveRequest request, TimeseriesSaveResult result, FutureCallback callback); + void pushRequestToQueue(TimeseriesSaveRequest request, FutureCallback callback); + void pushRequestToQueue(AttributesSaveRequest request, List result, FutureCallback callback); + void pushRequestToQueue(AttributesSaveRequest request, FutureCallback callback); + void pushRequestToQueue(AttributesDeleteRequest request, List result, FutureCallback callback); void pushRequestToQueue(TimeseriesDeleteRequest request, List result, FutureCallback callback); diff --git a/rule-engine/rule-engine-api/src/main/java/org/thingsboard/rule/engine/api/TbContext.java b/rule-engine/rule-engine-api/src/main/java/org/thingsboard/rule/engine/api/TbContext.java index 6ba4eae0e4..1abad50a8d 100644 --- a/rule-engine/rule-engine-api/src/main/java/org/thingsboard/rule/engine/api/TbContext.java +++ b/rule-engine/rule-engine-api/src/main/java/org/thingsboard/rule/engine/api/TbContext.java @@ -360,6 +360,8 @@ public interface TbContext { CalculatedFieldService getCalculatedFieldService(); + CalculatedFieldQueueService getCalculatedFieldQueueService(); + boolean isExternalNodeForceAck(); /** diff --git a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/telemetry/TbCalculatedFieldsNode.java b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/telemetry/TbCalculatedFieldsNode.java new file mode 100644 index 0000000000..ac0e61d9be --- /dev/null +++ b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/telemetry/TbCalculatedFieldsNode.java @@ -0,0 +1,212 @@ +/** + * 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.rule.engine.telemetry; + +import com.fasterxml.jackson.core.type.TypeReference; +import com.fasterxml.jackson.databind.JsonNode; +import com.google.common.util.concurrent.FutureCallback; +import com.google.gson.JsonParser; +import jakarta.annotation.Nullable; +import lombok.extern.slf4j.Slf4j; +import org.thingsboard.common.util.JacksonUtil; +import org.thingsboard.rule.engine.api.AttributesDeleteRequest; +import org.thingsboard.rule.engine.api.AttributesSaveRequest; +import org.thingsboard.rule.engine.api.EmptyNodeConfiguration; +import org.thingsboard.rule.engine.api.RuleNode; +import org.thingsboard.rule.engine.api.TbContext; +import org.thingsboard.rule.engine.api.TbNode; +import org.thingsboard.rule.engine.api.TbNodeConfiguration; +import org.thingsboard.rule.engine.api.TbNodeException; +import org.thingsboard.rule.engine.api.TimeseriesDeleteRequest; +import org.thingsboard.rule.engine.api.TimeseriesSaveRequest; +import org.thingsboard.rule.engine.api.util.TbNodeUtils; +import org.thingsboard.server.common.adaptor.JsonConverter; +import org.thingsboard.server.common.data.AttributeScope; +import org.thingsboard.server.common.data.kv.AttributeKvEntry; +import org.thingsboard.server.common.data.kv.BasicTsKvEntry; +import org.thingsboard.server.common.data.kv.KvEntry; +import org.thingsboard.server.common.data.kv.TsKvEntry; +import org.thingsboard.server.common.data.plugin.ComponentType; +import org.thingsboard.server.common.msg.TbMsg; + +import java.util.ArrayList; +import java.util.List; +import java.util.Map; + +import static org.thingsboard.server.common.data.DataConstants.SCOPE; +import static org.thingsboard.server.common.data.msg.TbMsgType.ATTRIBUTES_DELETED; +import static org.thingsboard.server.common.data.msg.TbMsgType.POST_ATTRIBUTES_REQUEST; +import static org.thingsboard.server.common.data.msg.TbMsgType.POST_TELEMETRY_REQUEST; +import static org.thingsboard.server.common.data.msg.TbMsgType.TIMESERIES_DELETED; + +@Slf4j +@RuleNode( + type = ComponentType.ACTION, + name = "push to calculated fields", + configClazz = EmptyNodeConfiguration.class, + nodeDescription = "Pushes messages to the calculated fields for further processing", + nodeDetails = "Forwards incoming messages to the calculated fields, where they will be processed to compute values based on predefined calculation rules without persisting any data in the database.", + configDirective = "tbNodeEmptyConfig", + icon = "send" +) +public class TbCalculatedFieldsNode implements TbNode { + + private EmptyNodeConfiguration config; + + @Override + public void init(TbContext ctx, TbNodeConfiguration configuration) throws TbNodeException { + this.config = TbNodeUtils.convert(configuration, EmptyNodeConfiguration.class); + } + + @Override + public void onMsg(TbContext ctx, TbMsg msg) { + if (!msg.isTypeOneOf(POST_ATTRIBUTES_REQUEST, POST_TELEMETRY_REQUEST, ATTRIBUTES_DELETED, TIMESERIES_DELETED)) { + ctx.tellFailure(msg, new IllegalArgumentException("Unsupported msg type: " + msg.getType())); + return; + } + + String src = msg.getData(); + + if (msg.isTypeOf(POST_TELEMETRY_REQUEST)) { + processPostTelemetryRequest(ctx, msg, src); + } else if (msg.isTypeOf(POST_ATTRIBUTES_REQUEST)) { + processPostAttributesRequest(ctx, msg, src); + } else if (msg.isTypeOf(TIMESERIES_DELETED)) { + processTimeSeriesDeleted(ctx, msg, src); + } else { + processAttributesDeleted(ctx, msg, src); + } + + } + + private void processPostTelemetryRequest(TbContext ctx, TbMsg msg, String src) { + Map> tsKvMap = JsonConverter.convertToTelemetry(JsonParser.parseString(src), System.currentTimeMillis()); + if (tsKvMap.isEmpty()) { + ctx.tellFailure(msg, new IllegalArgumentException("Msg body is empty: " + src)); + return; + } + + List tsKvEntryList = new ArrayList<>(); + for (Map.Entry> tsKvEntry : tsKvMap.entrySet()) { + for (KvEntry kvEntry : tsKvEntry.getValue()) { + tsKvEntryList.add(new BasicTsKvEntry(tsKvEntry.getKey(), kvEntry)); + } + } + + TimeseriesSaveRequest timeseriesSaveRequest = TimeseriesSaveRequest.builder() + .tenantId(ctx.getTenantId()) + .customerId(msg.getCustomerId()) + .entityId(msg.getOriginator()) + .entries(tsKvEntryList) + .previousCalculatedFieldIds(msg.getPreviousCalculatedFieldIds()) + .tbMsgId(msg.getId()) + .tbMsgType(msg.getInternalType()) + .callback(new TelemetryNodeCallback(ctx, msg)) + .build(); + + ctx.getCalculatedFieldQueueService().pushRequestToQueue(timeseriesSaveRequest, timeseriesSaveRequest.getCallback()); + } + + private void processPostAttributesRequest(TbContext ctx, TbMsg msg, String src) { + List newAttributes = new ArrayList<>(JsonConverter.convertToAttributes(JsonParser.parseString(src))); + + if (newAttributes.isEmpty()) { + ctx.tellSuccess(msg); + return; + } + + AttributesSaveRequest attributesSaveRequest = AttributesSaveRequest.builder() + .tenantId(ctx.getTenantId()) + .entityId(msg.getOriginator()) + .scope(AttributeScope.valueOf(msg.getMetaData().getValue(SCOPE))) + .entries(newAttributes) + .previousCalculatedFieldIds(msg.getPreviousCalculatedFieldIds()) + .tbMsgId(msg.getId()) + .tbMsgType(msg.getInternalType()) + .callback(new TelemetryNodeCallback(ctx, msg)) + .build(); + ctx.getCalculatedFieldQueueService().pushRequestToQueue(attributesSaveRequest, attributesSaveRequest.getCallback()); + } + + private void processTimeSeriesDeleted(TbContext ctx, TbMsg msg, String src) { + JsonNode dataJson = JacksonUtil.toJsonNode(msg.getData()); + List keysToDelete = JacksonUtil.convertValue(dataJson.get("timeSeries"), new TypeReference<>() { + }); + + if (keysToDelete.isEmpty()) { + ctx.tellSuccess(msg); + return; + } + + TimeseriesDeleteRequest timeseriesDeleteRequest = TimeseriesDeleteRequest.builder() + .tenantId(ctx.getTenantId()) + .entityId(msg.getOriginator()) + .keys(keysToDelete) + .previousCalculatedFieldIds(msg.getPreviousCalculatedFieldIds()) + .tbMsgId(msg.getId()) + .tbMsgType(msg.getInternalType()) + .callback(new FutureCallback>() { + @Override + public void onSuccess(@Nullable List tmp) { + } + + @Override + public void onFailure(Throwable t) { + } + }) + .build(); + + ctx.getCalculatedFieldQueueService().pushRequestToQueue(timeseriesDeleteRequest, keysToDelete, getCalculatedFieldCallback(timeseriesDeleteRequest.getCallback(), keysToDelete)); + } + + private void processAttributesDeleted(TbContext ctx, TbMsg msg, String src) { + JsonNode dataJson = JacksonUtil.toJsonNode(msg.getData()); + List keysToDelete = JacksonUtil.convertValue(dataJson.get("attributes"), new TypeReference<>() { + }); + + if (keysToDelete.isEmpty()) { + ctx.tellSuccess(msg); + return; + } + + AttributesDeleteRequest attributesDeleteRequest = AttributesDeleteRequest.builder() + .tenantId(ctx.getTenantId()) + .entityId(msg.getOriginator()) + .scope(AttributeScope.valueOf(msg.getMetaData().getValue(SCOPE))) + .keys(keysToDelete) + .previousCalculatedFieldIds(msg.getPreviousCalculatedFieldIds()) + .tbMsgId(msg.getId()) + .tbMsgType(msg.getInternalType()) + .callback(new TelemetryNodeCallback(ctx, msg)) + .build(); + ctx.getCalculatedFieldQueueService().pushRequestToQueue(attributesDeleteRequest, keysToDelete, attributesDeleteRequest.getCallback()); + } + + private FutureCallback getCalculatedFieldCallback(FutureCallback> originalCallback, List keys) { + return new FutureCallback() { + @Override + public void onSuccess(Void unused) { + originalCallback.onSuccess(keys); + } + + @Override + public void onFailure(Throwable t) { + originalCallback.onFailure(t); + } + }; + } + +} From e5b8a24598a65f49d805f1f03276ee9017d686eb Mon Sep 17 00:00:00 2001 From: Igor Kulikov Date: Wed, 5 Mar 2025 18:42:25 +0200 Subject: [PATCH 206/345] UI: Improve map widget import/export. Add help assets for new maps. --- .../app/core/services/item-buffer.service.ts | 154 ++++++------------ .../lib/maps/data-layer/circles-data-layer.ts | 2 +- .../lib/maps/data-layer/data-layer-utils.ts | 2 +- .../maps/data-layer/latest-map-data-layer.ts | 2 +- .../lib/maps/data-layer/map-data-layer.ts | 7 +- .../lib/maps/data-layer/markers-data-layer.ts | 4 +- .../maps/data-layer/polygons-data-layer.ts | 2 +- .../lib/maps/data-layer/shapes-data-layer.ts | 2 +- .../lib/maps/data-layer/trips-data-layer.ts | 2 +- .../components/widget/lib/maps/geo-map.ts | 2 +- .../components/widget/lib/maps/image-map.ts | 2 +- .../components/widget/lib/maps/map-layer.ts | 2 +- .../widget/lib/maps/map-widget.component.ts | 5 +- .../widget/lib/maps/map-widget.models.ts | 2 +- .../home/components/widget/lib/maps/map.ts | 6 +- .../panels/map-timeline-panel.component.ts | 2 +- ...-layer-color-settings-panel.component.html | 2 +- ...ta-layer-color-settings-panel.component.ts | 5 +- .../data-layer-color-settings.component.ts | 6 +- ...data-layer-pattern-settings.component.html | 2 +- .../data-layer-pattern-settings.component.ts | 5 +- .../image-map-source-settings.component.ts | 4 +- .../map/map-action-button-row.component.ts | 2 +- .../map-action-buttons-settings.component.ts | 2 +- .../map/map-data-layer-dialog.component.html | 11 +- .../map/map-data-layer-dialog.component.ts | 32 +++- .../map/map-data-layer-row.component.ts | 2 +- .../common/map/map-data-layers.component.ts | 2 +- .../map/map-data-source-row.component.ts | 2 +- .../common/map/map-data-sources.component.ts | 2 +- .../common/map/map-layer-row.component.ts | 2 +- .../map/map-layer-settings-panel.component.ts | 2 +- .../common/map/map-layers.component.ts | 2 +- .../common/map/map-settings.component.ts | 3 +- .../marker-clustering-settings.component.ts | 2 +- .../marker-image-settings-panel.component.ts | 2 +- .../map/marker-image-settings.component.ts | 2 +- .../map/marker-shape-settings.component.html | 2 +- .../map/marker-shape-settings.component.ts | 4 +- .../common/map/marker-shapes.component.ts | 2 +- .../map/trip-timeline-settings.component.ts | 13 +- .../map/legacy/circle-settings.component.html | 8 +- .../marker-clustering-settings.component.html | 2 +- .../legacy/markers-settings.component.html | 10 +- .../legacy/polygon-settings.component.html | 8 +- ...p-animation-common-settings.component.html | 2 +- ...p-animation-marker-settings.component.html | 4 +- ...rip-animation-path-settings.component.html | 2 +- ...ip-animation-point-settings.component.html | 4 +- .../map/map-widget-settings.component.ts | 2 +- .../import-export/import-export.service.ts | 12 +- ui-ngx/src/app/shared/models/alias.models.ts | 39 +++++ .../app/shared/models/query/query.models.ts | 40 +++++ .../models/widget/maps/map-export.models.ts | 151 +++++++++++++++++ .../models/widget/maps}/map.models.ts | 2 +- .../widget/maps}/marker-shape.models.ts | 2 +- .../models/widget/widget-export.models.ts | 35 ++++ .../lib/map-legacy/clustering_color_fn.md | 65 ++++++++ .../en_US/widget/lib/map-legacy/color_fn.md | 42 +++++ .../en_US/widget/lib/map-legacy/label_fn.md | 40 +++++ .../widget/lib/map-legacy/map_fn_args.md | 10 ++ .../widget/lib/map-legacy/marker_image_fn.md | 62 +++++++ .../widget/lib/map-legacy/path_color_fn.md | 42 +++++ .../lib/map-legacy/path_point_color_fn.md | 43 +++++ .../{map => map-legacy}/polygon_color_fn.md | 2 +- .../lib/map-legacy/polygon_tooltip_fn.md | 40 +++++ .../widget/lib/map-legacy/position_fn.md | 65 ++++++++ .../en_US/widget/lib/map-legacy/tooltip_fn.md | 40 +++++ .../lib/map-legacy/trip_point_as_anchor_fn.md | 34 ++++ .../widget/lib/map/circle_fill_color_fn.md | 24 +++ .../en_US/widget/lib/map/circle_label_fn.md | 22 +++ .../widget/lib/map/circle_stroke_color_fn.md | 24 +++ .../en_US/widget/lib/map/circle_tooltip_fn.md | 22 +++ .../widget/lib/map/clustering_color_fn.md | 6 +- .../help/en_US/widget/lib/map/color_fn.md | 22 +-- .../en_US/widget/lib/map/color_fn_examples.md | 19 +++ .../help/en_US/widget/lib/map/label_fn.md | 22 +-- .../en_US/widget/lib/map/label_fn_examples.md | 19 +++ .../help/en_US/widget/lib/map/map_fn_args.md | 12 +- .../en_US/widget/lib/map/marker_image_fn.md | 18 +- .../widget/lib/map/marker_image_fn_args.md | 10 ++ .../en_US/widget/lib/map/path_color_fn.md | 24 +-- .../widget/lib/map/path_point_color_fn.md | 25 +-- .../widget/lib/map/path_point_tooltip_fn.md | 22 +++ .../widget/lib/map/polygon_fill_color_fn.md | 24 +++ .../en_US/widget/lib/map/polygon_label_fn.md | 22 +++ .../widget/lib/map/polygon_stroke_color_fn.md | 24 +++ .../widget/lib/map/polygon_tooltip_fn.md | 24 +-- .../help/en_US/widget/lib/map/position_fn.md | 6 +- .../help/en_US/widget/lib/map/tooltip_fn.md | 24 +-- .../widget/lib/map/tooltip_fn_examples.md | 19 +++ .../widget/lib/map/trip_point_as_anchor_fn.md | 4 +- ui-ngx/src/typings/leaflet-extend-tb.d.ts | 4 +- 93 files changed, 1214 insertions(+), 349 deletions(-) create mode 100644 ui-ngx/src/app/shared/models/widget/maps/map-export.models.ts rename ui-ngx/src/app/{modules/home/components/widget/lib/maps/models => shared/models/widget/maps}/map.models.ts (99%) rename ui-ngx/src/app/{modules/home/components/widget/lib/maps/models => shared/models/widget/maps}/marker-shape.models.ts (98%) create mode 100644 ui-ngx/src/app/shared/models/widget/widget-export.models.ts create mode 100644 ui-ngx/src/assets/help/en_US/widget/lib/map-legacy/clustering_color_fn.md create mode 100644 ui-ngx/src/assets/help/en_US/widget/lib/map-legacy/color_fn.md create mode 100644 ui-ngx/src/assets/help/en_US/widget/lib/map-legacy/label_fn.md create mode 100644 ui-ngx/src/assets/help/en_US/widget/lib/map-legacy/map_fn_args.md create mode 100644 ui-ngx/src/assets/help/en_US/widget/lib/map-legacy/marker_image_fn.md create mode 100644 ui-ngx/src/assets/help/en_US/widget/lib/map-legacy/path_color_fn.md create mode 100644 ui-ngx/src/assets/help/en_US/widget/lib/map-legacy/path_point_color_fn.md rename ui-ngx/src/assets/help/en_US/widget/lib/{map => map-legacy}/polygon_color_fn.md (94%) create mode 100644 ui-ngx/src/assets/help/en_US/widget/lib/map-legacy/polygon_tooltip_fn.md create mode 100644 ui-ngx/src/assets/help/en_US/widget/lib/map-legacy/position_fn.md create mode 100644 ui-ngx/src/assets/help/en_US/widget/lib/map-legacy/tooltip_fn.md create mode 100644 ui-ngx/src/assets/help/en_US/widget/lib/map-legacy/trip_point_as_anchor_fn.md create mode 100644 ui-ngx/src/assets/help/en_US/widget/lib/map/circle_fill_color_fn.md create mode 100644 ui-ngx/src/assets/help/en_US/widget/lib/map/circle_label_fn.md create mode 100644 ui-ngx/src/assets/help/en_US/widget/lib/map/circle_stroke_color_fn.md create mode 100644 ui-ngx/src/assets/help/en_US/widget/lib/map/circle_tooltip_fn.md create mode 100644 ui-ngx/src/assets/help/en_US/widget/lib/map/color_fn_examples.md create mode 100644 ui-ngx/src/assets/help/en_US/widget/lib/map/label_fn_examples.md create mode 100644 ui-ngx/src/assets/help/en_US/widget/lib/map/marker_image_fn_args.md create mode 100644 ui-ngx/src/assets/help/en_US/widget/lib/map/path_point_tooltip_fn.md create mode 100644 ui-ngx/src/assets/help/en_US/widget/lib/map/polygon_fill_color_fn.md create mode 100644 ui-ngx/src/assets/help/en_US/widget/lib/map/polygon_label_fn.md create mode 100644 ui-ngx/src/assets/help/en_US/widget/lib/map/polygon_stroke_color_fn.md create mode 100644 ui-ngx/src/assets/help/en_US/widget/lib/map/tooltip_fn_examples.md diff --git a/ui-ngx/src/app/core/services/item-buffer.service.ts b/ui-ngx/src/app/core/services/item-buffer.service.ts index 8806b15867..cef9bbe7a3 100644 --- a/ui-ngx/src/app/core/services/item-buffer.service.ts +++ b/ui-ngx/src/app/core/services/item-buffer.service.ts @@ -16,7 +16,7 @@ import { Injectable } from '@angular/core'; import { BreakpointId, Dashboard, DashboardLayoutId } from '@app/shared/models/dashboard.models'; -import { AliasesInfo, EntityAlias, EntityAliases, EntityAliasInfo } from '@shared/models/alias.models'; +import { AliasesInfo, EntityAlias, EntityAliases, EntityAliasInfo, getEntityAliasId } from '@shared/models/alias.models'; import { Datasource, DatasourceType, @@ -34,7 +34,8 @@ import { map } from 'rxjs/operators'; import { FcRuleNode, ruleNodeTypeDescriptors } from '@shared/models/rule-node.models'; import { RuleChainService } from '@core/http/rule-chain.service'; import { RuleChainImport } from '@shared/models/rule-chain.models'; -import { Filter, FilterInfo, Filters, FiltersInfo } from '@shared/models/query/query.models'; +import { Filter, FilterInfo, Filters, FiltersInfo, getFilterId } from '@shared/models/query/query.models'; +import { getWidgetExportDefinition } from '@shared/models/widget/widget-export.models'; const WIDGET_ITEM = 'widget_item'; const WIDGET_REFERENCE = 'widget_reference'; @@ -47,6 +48,7 @@ export interface WidgetItem { filtersInfo: FiltersInfo; originalSize: WidgetSize; originalColumns: number; + widgetExportInfo?: any; } export interface WidgetReference { @@ -139,12 +141,18 @@ export class ItemBufferService { } } } + let widgetExportInfo: any; + const exportDefinition = getWidgetExportDefinition(widget); + if (exportDefinition) { + widgetExportInfo = exportDefinition.prepareExportInfo(dashboard, widget); + } return { widget, aliasesInfo, filtersInfo, originalSize, - originalColumns + originalColumns, + widgetExportInfo }; } @@ -189,6 +197,7 @@ export class ItemBufferService { const filtersInfo = widgetItem.filtersInfo; const originalColumns = widgetItem.originalColumns; const originalSize = widgetItem.originalSize; + const widgetExportInfo = widgetItem.widgetExportInfo; let targetRow = -1; let targetColumn = -1; if (position) { @@ -199,7 +208,7 @@ export class ItemBufferService { return this.addWidgetToDashboard(targetDashboard, targetState, targetLayout, widget, aliasesInfo, filtersInfo, onAliasesUpdateFunction, onFiltersUpdateFunction, - originalColumns, originalSize, targetRow, targetColumn, breakpoint).pipe( + originalColumns, originalSize, targetRow, targetColumn, breakpoint, widgetExportInfo).pipe( map(() => widget) ); } else { @@ -248,7 +257,8 @@ export class ItemBufferService { originalSize: WidgetSize, row: number, column: number, - breakpoint = 'default'): Observable { + breakpoint = 'default', + widgetExportInfo?: any): Observable { let theDashboard: Dashboard; if (dashboard) { theDashboard = dashboard; @@ -258,26 +268,39 @@ export class ItemBufferService { theDashboard = this.dashboardUtils.validateAndUpdateDashboard(theDashboard); let callAliasUpdateFunction = false; let callFilterUpdateFunction = false; + let newEntityAliases: EntityAliases; + let newFilters: Filters; + const exportDefinition = getWidgetExportDefinition(widget); + if (exportDefinition && widgetExportInfo || aliasesInfo) { + newEntityAliases = deepClone(dashboard.configuration.entityAliases); + } + if (exportDefinition && widgetExportInfo || filtersInfo) { + newFilters = deepClone(dashboard.configuration.filters); + } if (aliasesInfo) { - const newEntityAliases = this.updateAliases(theDashboard, widget, aliasesInfo); - const aliasesUpdated = !isEqual(newEntityAliases, theDashboard.configuration.entityAliases); - if (aliasesUpdated) { - theDashboard.configuration.entityAliases = newEntityAliases; - if (onAliasesUpdateFunction) { - callAliasUpdateFunction = true; - } - } + this.updateAliases(widget, newEntityAliases, aliasesInfo); } if (filtersInfo) { - const newFilters = this.updateFilters(theDashboard, widget, filtersInfo); - const filtersUpdated = !isEqual(newFilters, theDashboard.configuration.filters); - if (filtersUpdated) { - theDashboard.configuration.filters = newFilters; - if (onFiltersUpdateFunction) { - callFilterUpdateFunction = true; - } + this.updateFilters(widget, newFilters, filtersInfo); + } + if (exportDefinition && widgetExportInfo) { + exportDefinition.updateFromExportInfo(widget, newEntityAliases, newFilters, widgetExportInfo); + } + const aliasesUpdated = newEntityAliases && !isEqual(newEntityAliases, theDashboard.configuration.entityAliases); + if (aliasesUpdated) { + theDashboard.configuration.entityAliases = newEntityAliases; + if (onAliasesUpdateFunction) { + callAliasUpdateFunction = true; } } + const filtersUpdated = newFilters && !isEqual(newFilters, theDashboard.configuration.filters); + if (filtersUpdated) { + theDashboard.configuration.filters = newFilters; + if (onFiltersUpdateFunction) { + callFilterUpdateFunction = true; + } + } + this.dashboardUtils.addWidgetToLayout(theDashboard, targetState, targetLayout, widget, originalColumns, originalSize, row, column, breakpoint); if (callAliasUpdateFunction) { @@ -430,14 +453,13 @@ export class ItemBufferService { }; } - private updateAliases(dashboard: Dashboard, widget: Widget, aliasesInfo: AliasesInfo): EntityAliases { - const entityAliases = deepClone(dashboard.configuration.entityAliases); + private updateAliases(widget: Widget, entityAliases: EntityAliases, aliasesInfo: AliasesInfo): void { let aliasInfo: EntityAliasInfo; let newAliasId: string; for (const datasourceIndexStr of Object.keys(aliasesInfo.datasourceAliases)) { const datasourceIndex = Number(datasourceIndexStr); aliasInfo = aliasesInfo.datasourceAliases[datasourceIndex]; - newAliasId = this.getEntityAliasId(entityAliases, aliasInfo); + newAliasId = getEntityAliasId(entityAliases, aliasInfo); if (widget.type === widgetType.alarm) { widget.config.alarmSource.entityAliasId = newAliasId; } else { @@ -446,7 +468,7 @@ export class ItemBufferService { } if (aliasesInfo.targetDeviceAlias) { aliasInfo = aliasesInfo.targetDeviceAlias; - newAliasId = this.getEntityAliasId(entityAliases, aliasInfo); + newAliasId = getEntityAliasId(entityAliases, aliasInfo); if (widget.config.targetDevice?.type !== TargetDeviceType.entity) { widget.config.targetDevice = { type: TargetDeviceType.entity @@ -454,101 +476,21 @@ export class ItemBufferService { } widget.config.targetDevice.entityAliasId = newAliasId; } - return entityAliases; } - private updateFilters(dashboard: Dashboard, widget: Widget, filtersInfo: FiltersInfo): Filters { - const filters = deepClone(dashboard.configuration.filters); + private updateFilters(widget: Widget, filters: Filters, filtersInfo: FiltersInfo): void { let filterInfo: FilterInfo; let newFilterId: string; for (const datasourceIndexStr of Object.keys(filtersInfo.datasourceFilters)) { const datasourceIndex = Number(datasourceIndexStr); filterInfo = filtersInfo.datasourceFilters[datasourceIndex]; - newFilterId = this.getFilterId(filters, filterInfo); + newFilterId = getFilterId(filters, filterInfo); if (widget.type === widgetType.alarm) { widget.config.alarmSource.filterId = newFilterId; } else { widget.config.datasources[datasourceIndex].filterId = newFilterId; } } - return filters; - } - - private isEntityAliasEqual(alias1: EntityAliasInfo, alias2: EntityAliasInfo): boolean { - return isEqual(alias1.filter, alias2.filter); - } - - private getEntityAliasId(entityAliases: EntityAliases, aliasInfo: EntityAliasInfo): string { - let newAliasId: string; - for (const aliasId of Object.keys(entityAliases)) { - if (this.isEntityAliasEqual(entityAliases[aliasId], aliasInfo)) { - newAliasId = aliasId; - break; - } - } - if (!newAliasId) { - const newAliasName = this.createEntityAliasName(entityAliases, aliasInfo.alias); - newAliasId = this.utils.guid(); - entityAliases[newAliasId] = {id: newAliasId, alias: newAliasName, filter: aliasInfo.filter}; - } - return newAliasId; - } - - private createEntityAliasName(entityAliases: EntityAliases, alias: string): string { - let c = 0; - let newAlias = alias; - let unique = false; - while (!unique) { - unique = true; - for (const entAliasId of Object.keys(entityAliases)) { - const entAlias = entityAliases[entAliasId]; - if (newAlias === entAlias.alias) { - c++; - newAlias = alias + c; - unique = false; - } - } - } - return newAlias; - } - - private isFilterEqual(filter1: FilterInfo, filter2: FilterInfo): boolean { - return isEqual(filter1.keyFilters, filter2.keyFilters); - } - - private getFilterId(filters: Filters, filterInfo: FilterInfo): string { - let newFilterId: string; - for (const filterId of Object.keys(filters)) { - if (this.isFilterEqual(filters[filterId], filterInfo)) { - newFilterId = filterId; - break; - } - } - if (!newFilterId) { - const newFilterName = this.createFilterName(filters, filterInfo.filter); - newFilterId = this.utils.guid(); - filters[newFilterId] = {id: newFilterId, filter: newFilterName, - keyFilters: filterInfo.keyFilters, editable: filterInfo.editable}; - } - return newFilterId; - } - - private createFilterName(filters: Filters, filter: string): string { - let c = 0; - let newFilter = filter; - let unique = false; - while (!unique) { - unique = true; - for (const entFilterId of Object.keys(filters)) { - const entFilter = filters[entFilterId]; - if (newFilter === entFilter.filter) { - c++; - newFilter = filter + c; - unique = false; - } - } - } - return newFilter; } private storeSet(key: string, elem: any) { diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/maps/data-layer/circles-data-layer.ts b/ui-ngx/src/app/modules/home/components/widget/lib/maps/data-layer/circles-data-layer.ts index 140e4f770b..62b36b59c6 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/maps/data-layer/circles-data-layer.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/maps/data-layer/circles-data-layer.ts @@ -20,7 +20,7 @@ import { isJSON, MapDataLayerType, TbCircleData, TbMapDatasource -} from '@home/components/widget/lib/maps/models/map.models'; +} from '@shared/models/widget/maps/map.models'; import L from 'leaflet'; import { FormattedData } from '@shared/models/widget.models'; import { TbShapesDataLayer } from '@home/components/widget/lib/maps/data-layer/shapes-data-layer'; 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 6e4ec1e485..d5329586be 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 @@ -18,7 +18,7 @@ import { DataLayerTooltipSettings, DataLayerTooltipTrigger, processTooltipTemplate, TbMapDatasource -} from '@home/components/widget/lib/maps/models/map.models'; +} from '@shared/models/widget/maps/map.models'; import { TbMap } from '@home/components/widget/lib/maps/map'; import { FormattedData } from '@shared/models/widget.models'; import L from 'leaflet'; diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/maps/data-layer/latest-map-data-layer.ts b/ui-ngx/src/app/modules/home/components/widget/lib/maps/data-layer/latest-map-data-layer.ts index 7c77056894..3a537ba8e7 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/maps/data-layer/latest-map-data-layer.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/maps/data-layer/latest-map-data-layer.ts @@ -18,7 +18,7 @@ import { DataLayerEditAction, MapDataLayerSettings, TbMapDatasource -} from '@home/components/widget/lib/maps/models/map.models'; +} from '@shared/models/widget/maps/map.models'; import { TbMap } from '@home/components/widget/lib/maps/map'; import { FormattedData, WidgetActionType } from '@shared/models/widget.models'; import { Observable } from 'rxjs'; diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/maps/data-layer/map-data-layer.ts b/ui-ngx/src/app/modules/home/components/widget/lib/maps/data-layer/map-data-layer.ts index ce3f9862de..3ae61642ce 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/maps/data-layer/map-data-layer.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/maps/data-layer/map-data-layer.ts @@ -20,7 +20,7 @@ import { MapDataLayerSettings, MapDataLayerType, mapDataSourceSettingsToDatasource, MapStringFunction, MapType, TbMapDatasource -} from '@home/components/widget/lib/maps/models/map.models'; +} from '@shared/models/widget/maps/map.models'; import { createLabelFromPattern, guid, isDefined, @@ -82,6 +82,7 @@ export class DataLayerColorProcessor { private settings: DataLayerColorSettings) {} public setup(): Observable { + this.color = this.settings.color; if (this.settings.type === DataLayerColorType.function) { return parseTbFunction(this.dataLayer.getCtx().http, this.settings.colorFunction, ['data', 'dsData']).pipe( map((parsed) => { @@ -90,7 +91,6 @@ export class DataLayerColorProcessor { }) ); } else { - this.color = this.settings.color; return of(null) } } @@ -99,6 +99,9 @@ export class DataLayerColorProcessor { let color: string; if (this.settings.type === DataLayerColorType.function) { color = safeExecuteTbFunction(this.colorFunction, [data, dsData]); + if (!color) { + color = this.color; + } } else { color = this.color; } 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 5e9517b7fc..106c33a530 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 @@ -34,7 +34,7 @@ import { MarkerShapeSettings, MarkerType, TbMapDatasource -} from '@home/components/widget/lib/maps/models/map.models'; +} from '@shared/models/widget/maps/map.models'; import L, { FeatureGroup } from 'leaflet'; import { FormattedData } from '@shared/models/widget.models'; import { forkJoin, Observable, of } from 'rxjs'; @@ -55,7 +55,7 @@ import { createColorMarkerIconElement, createColorMarkerShapeURI, MarkerShape -} from '@home/components/widget/lib/maps/models/marker-shape.models'; +} from '@shared/models/widget/maps/marker-shape.models'; import { MatIconRegistry } from '@angular/material/icon'; import { DomSanitizer } from '@angular/platform-browser'; import { diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/maps/data-layer/polygons-data-layer.ts b/ui-ngx/src/app/modules/home/components/widget/lib/maps/data-layer/polygons-data-layer.ts index 5d4f9a6ae3..d2178169dc 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/maps/data-layer/polygons-data-layer.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/maps/data-layer/polygons-data-layer.ts @@ -19,7 +19,7 @@ import { isCutPolygon, isJSON, MapDataLayerType, PolygonsDataLayerSettings, TbMapDatasource, TbPolyData, TbPolygonCoordinates, TbPolygonRawCoordinates -} from '@home/components/widget/lib/maps/models/map.models'; +} from '@shared/models/widget/maps/map.models'; import L from 'leaflet'; import { FormattedData } from '@shared/models/widget.models'; import { TbShapesDataLayer } from '@home/components/widget/lib/maps/data-layer/shapes-data-layer'; diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/maps/data-layer/shapes-data-layer.ts b/ui-ngx/src/app/modules/home/components/widget/lib/maps/data-layer/shapes-data-layer.ts index 39c703b00a..ec6c5aba80 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/maps/data-layer/shapes-data-layer.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/maps/data-layer/shapes-data-layer.ts @@ -14,7 +14,7 @@ /// limitations under the License. /// -import { ShapeDataLayerSettings, TbMapDatasource } from '@home/components/widget/lib/maps/models/map.models'; +import { ShapeDataLayerSettings, TbMapDatasource } from '@shared/models/widget/maps/map.models'; import L from 'leaflet'; import { TbMap } from '@home/components/widget/lib/maps/map'; import { forkJoin, Observable } from 'rxjs'; diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/maps/data-layer/trips-data-layer.ts b/ui-ngx/src/app/modules/home/components/widget/lib/maps/data-layer/trips-data-layer.ts index 27d726f15e..368ca42be8 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/maps/data-layer/trips-data-layer.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/maps/data-layer/trips-data-layer.ts @@ -23,7 +23,7 @@ import { MapDataLayerType, TbMapDatasource, TripsDataLayerSettings -} from '@home/components/widget/lib/maps/models/map.models'; +} from '@shared/models/widget/maps/map.models'; import { forkJoin, Observable } from 'rxjs'; import { FormattedData, WidgetActionType } from '@shared/models/widget.models'; import { map } from 'rxjs/operators'; diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/maps/geo-map.ts b/ui-ngx/src/app/modules/home/components/widget/lib/maps/geo-map.ts index 72ddacf44e..fd03990d26 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/maps/geo-map.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/maps/geo-map.ts @@ -25,7 +25,7 @@ import { TbPolygonCoordinates, TbPolygonRawCoordinate, TbPolygonRawCoordinates -} from '@home/components/widget/lib/maps/models/map.models'; +} from '@shared/models/widget/maps/map.models'; import { WidgetContext } from '@home/models/widget-component.models'; import { DeepPartial } from '@shared/models/common'; import { forkJoin, Observable, of } from 'rxjs'; diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/maps/image-map.ts b/ui-ngx/src/app/modules/home/components/widget/lib/maps/image-map.ts index a063bf786b..219f5d63ff 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/maps/image-map.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/maps/image-map.ts @@ -24,7 +24,7 @@ import { loadImageWithAspect, MapZoomAction, TbCircleData, TbPolygonCoordinate, TbPolygonCoordinates, TbPolygonRawCoordinate, TbPolygonRawCoordinates -} from '@home/components/widget/lib/maps/models/map.models'; +} from '@shared/models/widget/maps/map.models'; import { WidgetContext } from '@home/models/widget-component.models'; import { DeepPartial } from '@shared/models/common'; import { Observable, of, ReplaySubject, switchMap } from 'rxjs'; diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/maps/map-layer.ts b/ui-ngx/src/app/modules/home/components/widget/lib/maps/map-layer.ts index c6f8f446a6..304100828b 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/maps/map-layer.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/maps/map-layer.ts @@ -28,7 +28,7 @@ import { MapProvider, OpenStreetMapLayerSettings, TencentMapLayerSettings -} from '@home/components/widget/lib/maps/models/map.models'; +} from '@shared/models/widget/maps/map.models'; import { WidgetContext } from '@home/models/widget-component.models'; import { DeepPartial } from '@shared/models/common'; import { mergeDeep } from '@core/utils'; diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/maps/map-widget.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/maps/map-widget.component.ts index a462a01356..013a6c88a9 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/maps/map-widget.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/maps/map-widget.component.ts @@ -15,14 +15,12 @@ /// import { - AfterViewInit, ChangeDetectorRef, Component, ElementRef, Input, OnDestroy, OnInit, - Renderer2, TemplateRef, ViewChild, ViewEncapsulation @@ -36,7 +34,7 @@ import { WidgetContext } from '@home/models/widget-component.models'; import { Observable } from 'rxjs'; import { backgroundStyle, ComponentStyle, overlayStyle } from '@shared/models/widget-settings.models'; import { TbMap } from '@home/components/widget/lib/maps/map'; -import { MapSetting } from '@home/components/widget/lib/maps/models/map.models'; +import { MapSetting } from '@shared/models/widget/maps/map.models'; import { WidgetComponent } from '@home/components/widget/widget.component'; import { ImagePipe } from '@shared/pipe/image.pipe'; import { DomSanitizer } from '@angular/platform-browser'; @@ -69,7 +67,6 @@ export class MapWidgetComponent implements OnInit, OnDestroy { constructor(public widgetComponent: WidgetComponent, private imagePipe: ImagePipe, private sanitizer: DomSanitizer, - private renderer: Renderer2, private cd: ChangeDetectorRef) { } diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/maps/map-widget.models.ts b/ui-ngx/src/app/modules/home/components/widget/lib/maps/map-widget.models.ts index 19f1db5838..d25e56bbf5 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/maps/map-widget.models.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/maps/map-widget.models.ts @@ -14,7 +14,7 @@ /// limitations under the License. /// -import { defaultMapSettings, MapSetting, MapType } from '@home/components/widget/lib/maps/models/map.models'; +import { defaultMapSettings, MapSetting, MapType } from '@shared/models/widget/maps/map.models'; import { BackgroundSettings, BackgroundType } from '@shared/models/widget-settings.models'; import { mergeDeep } from '@core/utils'; import { WidgetContext } from '@home/models/widget-component.models'; 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 6225ff878d..64b02d83bd 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 @@ -27,7 +27,7 @@ import { TbMapDatasource, TbPolygonCoordinates, TbPolygonRawCoordinates -} from '@home/components/widget/lib/maps/models/map.models'; +} from '@shared/models/widget/maps/map.models'; import { WidgetContext } from '@home/models/widget-component.models'; import { formattedDataArrayFromDatasourceData, @@ -63,7 +63,7 @@ import { SelectMapEntityPanelComponent } from '@home/components/widget/lib/maps/panels/select-map-entity-panel.component'; import { TbPopoverComponent } from '@shared/components/popover.component'; -import { createPlaceItemIcon } from '@home/components/widget/lib/maps/models/marker-shape.models'; +import { createPlaceItemIcon } from '@shared/models/widget/maps/marker-shape.models'; import { MatIconRegistry } from '@angular/material/icon'; import { DomSanitizer } from '@angular/platform-browser'; import { MapTimelinePanelComponent } from '@home/components/widget/lib/maps/panels/map-timeline-panel.component'; @@ -464,7 +464,7 @@ export abstract class TbMap { } } - private toggleDragMode(e: MouseEvent, button: L.TB.ToolbarButton): void { + private toggleDragMode(_e: MouseEvent, button: L.TB.ToolbarButton): void { if (this.dragMode) { this.disableDragMode(); } else { diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/maps/panels/map-timeline-panel.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/maps/panels/map-timeline-panel.component.ts index 7d165c5a24..a6504ab96e 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/maps/panels/map-timeline-panel.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/maps/panels/map-timeline-panel.component.ts @@ -25,7 +25,7 @@ import { Output, ViewEncapsulation } from '@angular/core'; -import { TripTimelineSettings } from '@home/components/widget/lib/maps/models/map.models'; +import { TripTimelineSettings } from '@shared/models/widget/maps/map.models'; import { DateFormatProcessor } from '@shared/models/widget-settings.models'; import { interval, Subscription } from 'rxjs'; import { filter } from 'rxjs/operators'; diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/map/data-layer-color-settings-panel.component.html b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/map/data-layer-color-settings-panel.component.html index 4991d03011..e512e06a32 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/map/data-layer-color-settings-panel.component.html +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/map/data-layer-color-settings-panel.component.html @@ -63,7 +63,7 @@ [functionArgs]="['data', 'dsData']" [globalVariables]="functionScopeVariables" functionTitle="{{ 'widgets.maps.data-layer.color-function' | translate }}" - helpId="widget/lib/map/color_fn"> + [helpId]="helpId">
diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/map/data-layer-color-settings-panel.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/map/data-layer-color-settings-panel.component.ts index 17fcaa310a..2ce10c25ed 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/map/data-layer-color-settings-panel.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/map/data-layer-color-settings-panel.component.ts @@ -22,7 +22,7 @@ import { Store } from '@ngrx/store'; import { AppState } from '@core/core.state'; import { WidgetService } from '@core/http/widget.service'; import { takeUntilDestroyed } from '@angular/core/rxjs-interop'; -import { DataLayerColorSettings, DataLayerColorType } from '@home/components/widget/lib/maps/models/map.models'; +import { DataLayerColorSettings, DataLayerColorType } from '@shared/models/widget/maps/map.models'; @Component({ selector: 'tb-data-layer-color-settings-panel', @@ -36,6 +36,9 @@ export class DataLayerColorSettingsPanelComponent extends PageComponent implemen @Input() colorSettings: DataLayerColorSettings; + @Input() + helpId = 'widget/lib/map/color_fn'; + @Input() popover: TbPopoverComponent; diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/map/data-layer-color-settings.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/map/data-layer-color-settings.component.ts index 4df7290f9d..75cfb9c7bc 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/map/data-layer-color-settings.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/map/data-layer-color-settings.component.ts @@ -19,7 +19,7 @@ import { ControlValueAccessor, NG_VALUE_ACCESSOR } from '@angular/forms'; import { ComponentStyle } from '@shared/models/widget-settings.models'; import { MatButton } from '@angular/material/button'; import { TbPopoverService } from '@shared/components/popover.service'; -import { DataLayerColorSettings, DataLayerColorType } from '@home/components/widget/lib/maps/models/map.models'; +import { DataLayerColorSettings, DataLayerColorType } from '@shared/models/widget/maps/map.models'; import { DataLayerColorSettingsPanelComponent } from '@home/components/widget/lib/settings/common/map/data-layer-color-settings-panel.component'; @@ -41,6 +41,9 @@ export class DataLayerColorSettingsComponent implements ControlValueAccessor { @Input() disabled: boolean; + @Input() + helpId = 'widget/lib/map/color_fn'; + DataLayerColorType = DataLayerColorType; modelValue: DataLayerColorSettings; @@ -82,6 +85,7 @@ export class DataLayerColorSettingsComponent implements ControlValueAccessor { } else { const ctx: any = { colorSettings: this.modelValue, + helpId: this.helpId }; const colorSettingsPanelPopover = this.popoverService.displayPopover(trigger, this.renderer, this.viewContainerRef, DataLayerColorSettingsPanelComponent, 'left', true, null, diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/map/data-layer-pattern-settings.component.html b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/map/data-layer-pattern-settings.component.html index 4286bf1c22..7912c1aeb5 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/map/data-layer-pattern-settings.component.html +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/map/data-layer-pattern-settings.component.html @@ -46,7 +46,7 @@ [globalVariables]="functionScopeVariables" [functionArgs]="['data', 'dsData']" functionTitle="{{ (patternType === 'label' ? 'widgets.maps.data-layer.label-function' : 'widgets.maps.data-layer.tooltip-function') | translate }}" - helpId="{{ patternType === 'label' ? 'widget/lib/map/label_fn' : 'widget/lib/map/tooltip_fn' }}"> + [helpId]="helpId">
diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/map/data-layer-pattern-settings.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/map/data-layer-pattern-settings.component.ts index e13e7eec1c..22fc96bcbc 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/map/data-layer-pattern-settings.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/map/data-layer-pattern-settings.component.ts @@ -32,7 +32,7 @@ import { DataLayerPatternSettings, DataLayerPatternType, DataLayerTooltipSettings, dataLayerTooltipTriggers, dataLayerTooltipTriggerTranslationMap -} from '@home/components/widget/lib/maps/models/map.models'; +} from '@shared/models/widget/maps/map.models'; import { coerceBoolean } from '@shared/decorators/coercion'; import { MapSettingsContext } from '@home/components/widget/lib/settings/common/map/map-settings.component.models'; @@ -71,6 +71,9 @@ export class DataLayerPatternSettingsComponent implements OnInit, ControlValueAc @Input() patternType: 'label' | 'tooltip' = 'label'; + @Input() + helpId = 'widget/lib/map/label_fn'; + @Input() patternTitle: string; diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/map/image-map-source-settings.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/map/image-map-source-settings.component.ts index 7867d41363..5b43386664 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/map/image-map-source-settings.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/map/image-map-source-settings.component.ts @@ -14,7 +14,7 @@ /// limitations under the License. /// -import { Component, DestroyRef, forwardRef, Input, OnInit, ViewEncapsulation } from '@angular/core'; +import { Component, DestroyRef, forwardRef, Input, OnInit } from '@angular/core'; import { ControlValueAccessor, NG_VALIDATORS, @@ -26,7 +26,7 @@ import { Validators } from '@angular/forms'; import { takeUntilDestroyed } from '@angular/core/rxjs-interop'; -import { ImageMapSourceSettings, ImageSourceType } from '@home/components/widget/lib/maps/models/map.models'; +import { ImageMapSourceSettings, ImageSourceType } from '@shared/models/widget/maps/map.models'; import { DataKey, DatasourceType, widgetType } from '@shared/models/widget.models'; import { MapSettingsContext } from '@home/components/widget/lib/settings/common/map/map-settings.component.models'; import { DataKeyType } from '@shared/models/telemetry/telemetry.models'; diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/map/map-action-button-row.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/map/map-action-button-row.component.ts index 9f045d3db5..466c6a39d6 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/map/map-action-button-row.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/map/map-action-button-row.component.ts @@ -24,7 +24,7 @@ import { ValidationErrors, Validator } from '@angular/forms'; -import { MapActionButtonSettings } from '@home/components/widget/lib/maps/models/map.models'; +import { MapActionButtonSettings } from '@shared/models/widget/maps/map.models'; import { WidgetAction, WidgetActionType, widgetType } from '@shared/models/widget.models'; import { takeUntilDestroyed } from '@angular/core/rxjs-interop'; import { isEmptyStr } from '@core/utils'; diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/map/map-action-buttons-settings.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/map/map-action-buttons-settings.component.ts index cfee949885..0260676e03 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/map/map-action-buttons-settings.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/map/map-action-buttons-settings.component.ts @@ -26,7 +26,7 @@ import { import { defaultMapActionButtonSettings, MapActionButtonSettings -} from '@home/components/widget/lib/maps/models/map.models'; +} from '@shared/models/widget/maps/map.models'; import { CdkDragDrop } from '@angular/cdk/drag-drop'; import { takeUntilDestroyed } from '@angular/core/rxjs-interop'; diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/map/map-data-layer-dialog.component.html b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/map/map-data-layer-dialog.component.html index 07e0fa43e9..ecabc191e2 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/map/map-data-layer-dialog.component.html +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/map/map-data-layer-dialog.component.html @@ -256,7 +256,7 @@ px - +
@@ -334,13 +334,14 @@ px - +
@@ -350,7 +351,7 @@
widgets.maps.data-layer.fill-color
- +
widgets.maps.data-layer.stroke
@@ -359,18 +360,20 @@ px - +
diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/map/map-data-layer-dialog.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/map/map-data-layer-dialog.component.ts index 8e57e891c8..6771bdb226 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/map/map-data-layer-dialog.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/map/map-data-layer-dialog.component.ts @@ -29,7 +29,7 @@ import { MarkerType, pathDecoratorSymbols, pathDecoratorSymbolTranslationMap, PolygonsDataLayerSettings, ShapeDataLayerSettings, TripsDataLayerSettings -} from '@home/components/widget/lib/maps/models/map.models'; +} from '@shared/models/widget/maps/map.models'; import { Store } from '@ngrx/store'; import { AppState } from '@core/core.state'; import { Router } from '@angular/router'; @@ -94,6 +94,36 @@ export class MapDataLayerDialogComponent extends DialogComponent, protected router: Router, @Inject(MAT_DIALOG_DATA) public data: MapDataLayerDialogData, diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/map/map-data-layer-row.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/map/map-data-layer-row.component.ts index 835aed171e..ae47852f33 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/map/map-data-layer-row.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/map/map-data-layer-row.component.ts @@ -42,7 +42,7 @@ import { MarkersDataLayerSettings, PolygonsDataLayerSettings, TripsDataLayerSettings -} from '@home/components/widget/lib/maps/models/map.models'; +} from '@shared/models/widget/maps/map.models'; import { DataKey, DatasourceType, datasourceTypeTranslationMap, widgetType } from '@shared/models/widget.models'; import { EntityType } from '@shared/models/entity-type.models'; import { DataKeyType } from '@shared/models/telemetry/telemetry.models'; diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/map/map-data-layers.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/map/map-data-layers.component.ts index 17e4baa22e..747ef21681 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/map/map-data-layers.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/map/map-data-layers.component.ts @@ -35,7 +35,7 @@ import { mapDataLayerValid, mapDataLayerValidator, MapType -} from '@home/components/widget/lib/maps/models/map.models'; +} from '@shared/models/widget/maps/map.models'; import { MapSettingsComponent } from '@home/components/widget/lib/settings/common/map/map-settings.component'; import { MapSettingsContext } from '@home/components/widget/lib/settings/common/map/map-settings.component.models'; diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/map/map-data-source-row.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/map/map-data-source-row.component.ts index d41d6b4df5..a163e8322f 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/map/map-data-source-row.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/map/map-data-source-row.component.ts @@ -33,7 +33,7 @@ import { Validators } from '@angular/forms'; import { takeUntilDestroyed } from '@angular/core/rxjs-interop'; -import { AdditionalMapDataSourceSettings } from '@home/components/widget/lib/maps/models/map.models'; +import { AdditionalMapDataSourceSettings } from '@shared/models/widget/maps/map.models'; import { DataKey, DatasourceType, datasourceTypeTranslationMap, widgetType } from '@shared/models/widget.models'; import { EntityType } from '@shared/models/entity-type.models'; import { DataKeyType } from '@shared/models/telemetry/telemetry.models'; diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/map/map-data-sources.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/map/map-data-sources.component.ts index 828d11ab8f..681a135fed 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/map/map-data-sources.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/map/map-data-sources.component.ts @@ -33,7 +33,7 @@ import { additionalMapDataSourceValid, additionalMapDataSourceValidator, defaultAdditionalMapDataSourceSettings -} from '@home/components/widget/lib/maps/models/map.models'; +} from '@shared/models/widget/maps/map.models'; import { MapSettingsContext } from '@home/components/widget/lib/settings/common/map/map-settings.component.models'; @Component({ diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/map/map-layer-row.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/map/map-layer-row.component.ts index a4ade55caf..7112a0c3c2 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/map/map-layer-row.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/map/map-layer-row.component.ts @@ -53,7 +53,7 @@ import { openStreetMapLayerTranslationMap, tencentLayerTranslationMap, tencentLayerTypes -} from '@home/components/widget/lib/maps/models/map.models'; +} from '@shared/models/widget/maps/map.models'; import { deepClone } from '@core/utils'; import { MapLayerSettingsPanelComponent diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/map/map-layer-settings-panel.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/map/map-layer-settings-panel.component.ts index 60c2c66863..f27ecf2eca 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/map/map-layer-settings-panel.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/map/map-layer-settings-panel.component.ts @@ -33,7 +33,7 @@ import { openStreetMapLayerTranslationMap, tencentLayerTranslationMap, tencentLayerTypes -} from '@home/components/widget/lib/maps/models/map.models'; +} from '@shared/models/widget/maps/map.models'; import { TranslateService } from '@ngx-translate/core'; @Component({ diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/map/map-layers.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/map/map-layers.component.ts index 41dcb46cc6..60e9f981fa 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/map/map-layers.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/map/map-layers.component.ts @@ -35,7 +35,7 @@ import { mapLayerValid, mapLayerValidator, MapProvider -} from '@home/components/widget/lib/maps/models/map.models'; +} from '@shared/models/widget/maps/map.models'; @Component({ selector: 'tb-map-layers', diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/map/map-settings.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/map/map-settings.component.ts index 6f1650b19f..2abaab17f7 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/map/map-settings.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/map/map-settings.component.ts @@ -38,7 +38,7 @@ import { MapType, mapZoomActions, mapZoomActionTranslationMap -} from '@home/components/widget/lib/maps/models/map.models'; +} from '@shared/models/widget/maps/map.models'; import { takeUntilDestroyed } from '@angular/core/rxjs-interop'; import { merge, Observable } from 'rxjs'; import { coerceBoolean } from '@shared/decorators/coercion'; @@ -254,7 +254,6 @@ export class MapSettingsComponent implements OnInit, ControlValueAccessor, Valid private updateDragButtonModeSettings() { const markers: MapDataLayerSettings[] = this.mapSettingsFormGroup.get('markers').value; - const circles: MapDataLayerSettings[] = this.mapSettingsFormGroup.get('circles').value; let dragModeButtonSettingsEnabled = markers.some(d => d.edit && d.edit.enabledActions && d.edit.enabledActions.includes(DataLayerEditAction.move)); if (!dragModeButtonSettingsEnabled) { const polygons: MapDataLayerSettings[] = this.mapSettingsFormGroup.get('polygons').value; diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/map/marker-clustering-settings.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/map/marker-clustering-settings.component.ts index 00db2a1fa8..8240b5d6dd 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/map/marker-clustering-settings.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/map/marker-clustering-settings.component.ts @@ -27,7 +27,7 @@ import { } from '@angular/forms'; import { WidgetService } from '@core/http/widget.service'; import { takeUntilDestroyed } from '@angular/core/rxjs-interop'; -import { MarkerClusteringSettings } from '@home/components/widget/lib/maps/models/map.models'; +import { MarkerClusteringSettings } from '@shared/models/widget/maps/map.models'; import { merge } from 'rxjs'; @Component({ diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/map/marker-image-settings-panel.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/map/marker-image-settings-panel.component.ts index d8e0a2b083..72edca5e81 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/map/marker-image-settings-panel.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/map/marker-image-settings-panel.component.ts @@ -22,7 +22,7 @@ import { Store } from '@ngrx/store'; import { AppState } from '@core/core.state'; import { WidgetService } from '@core/http/widget.service'; import { takeUntilDestroyed } from '@angular/core/rxjs-interop'; -import { MarkerImageSettings, MarkerImageType } from '@home/components/widget/lib/maps/models/map.models'; +import { MarkerImageSettings, MarkerImageType } from '@shared/models/widget/maps/map.models'; @Component({ selector: 'tb-marker-image-settings-panel', diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/map/marker-image-settings.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/map/marker-image-settings.component.ts index f7323ca467..21600392df 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/map/marker-image-settings.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/map/marker-image-settings.component.ts @@ -18,7 +18,7 @@ import { ChangeDetectorRef, Component, forwardRef, Input, Renderer2, ViewContain import { ControlValueAccessor, NG_VALUE_ACCESSOR } from '@angular/forms'; import { MatButton } from '@angular/material/button'; import { TbPopoverService } from '@shared/components/popover.service'; -import { MarkerImageSettings, MarkerImageType } from '@home/components/widget/lib/maps/models/map.models'; +import { MarkerImageSettings, MarkerImageType } from '@shared/models/widget/maps/map.models'; import { MarkerImageSettingsPanelComponent } from '@home/components/widget/lib/settings/common/map/marker-image-settings-panel.component'; diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/map/marker-shape-settings.component.html b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/map/marker-shape-settings.component.html index 59931cce66..0a78698f0f 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/map/marker-shape-settings.component.html +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/map/marker-shape-settings.component.html @@ -30,5 +30,5 @@
- + diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/map/marker-shape-settings.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/map/marker-shape-settings.component.ts index 729817681e..98651a397d 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/map/marker-shape-settings.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/map/marker-shape-settings.component.ts @@ -33,7 +33,7 @@ import { } from '@angular/forms'; import { MatButton } from '@angular/material/button'; import { TbPopoverService } from '@shared/components/popover.service'; -import { MarkerIconSettings, MarkerShapeSettings, MarkerType } from '@home/components/widget/lib/maps/models/map.models'; +import { MarkerIconSettings, MarkerShapeSettings, MarkerType } from '@shared/models/widget/maps/map.models'; import { takeUntilDestroyed } from '@angular/core/rxjs-interop'; import { Observable } from 'rxjs'; import { DomSanitizer, SafeHtml, SafeUrl } from '@angular/platform-browser'; @@ -41,7 +41,7 @@ import { MatIconRegistry } from '@angular/material/icon'; import { createColorMarkerIconElement, createColorMarkerShapeURI -} from '@home/components/widget/lib/maps/models/marker-shape.models'; +} from '@shared/models/widget/maps/marker-shape.models'; import tinycolor from 'tinycolor2'; import { map, share } from 'rxjs/operators'; import { MarkerShapesComponent } from '@home/components/widget/lib/settings/common/map/marker-shapes.component'; diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/map/marker-shapes.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/map/marker-shapes.component.ts index 797e9f6215..0e4d9abb97 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/map/marker-shapes.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/map/marker-shapes.component.ts @@ -23,7 +23,7 @@ import { createColorMarkerShapeURI, MarkerShape, markerShapes, tripMarkerShapes -} from '@home/components/widget/lib/maps/models/marker-shape.models'; +} from '@shared/models/widget/maps/marker-shape.models'; import { Observable } from 'rxjs'; import { DomSanitizer, SafeUrl } from '@angular/platform-browser'; import { MatIconRegistry } from '@angular/material/icon'; diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/map/trip-timeline-settings.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/map/trip-timeline-settings.component.ts index fd9afb8740..5afeefcbe6 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/map/trip-timeline-settings.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/map/trip-timeline-settings.component.ts @@ -29,16 +29,8 @@ import { merge } from 'rxjs'; import { WidgetService } from '@core/http/widget.service'; import { takeUntilDestroyed } from '@angular/core/rxjs-interop'; import { - DataLayerPatternSettings, - DataLayerPatternType, - DataLayerTooltipSettings, - dataLayerTooltipTriggers, - dataLayerTooltipTriggerTranslationMap, - pathDecoratorSymbols, pathDecoratorSymbolTranslationMap, TripTimelineSettings -} from '@home/components/widget/lib/maps/models/map.models'; -import { coerceBoolean } from '@shared/decorators/coercion'; -import { MapSettingsContext } from '@home/components/widget/lib/settings/common/map/map-settings.component.models'; +} from '@shared/models/widget/maps/map.models'; @Component({ selector: 'tb-trip-timeline-settings', @@ -166,7 +158,4 @@ export class TripTimelineSettingsComponent implements OnInit, ControlValueAccess this.modelValue = this.tripTimelineSettingsFormGroup.getRawValue(); this.propagateChange(this.modelValue); } - - protected readonly pathDecoratorSymbols = pathDecoratorSymbols; - protected readonly pathDecoratorSymbolTranslationMap = pathDecoratorSymbolTranslationMap; } diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/map/legacy/circle-settings.component.html b/ui-ngx/src/app/modules/home/components/widget/lib/settings/map/legacy/circle-settings.component.html index 49605f4ced..54f9bf4788 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/settings/map/legacy/circle-settings.component.html +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/map/legacy/circle-settings.component.html @@ -66,7 +66,7 @@ [globalVariables]="functionScopeVariables" [functionArgs]="['data', 'dsData', 'dsIndex']" functionTitle="{{ 'widgets.maps.circle-label-function' | translate }}" - helpId="widget/lib/map/label_fn"> + helpId="widget/lib/map-legacy/label_fn"> @@ -110,7 +110,7 @@ [globalVariables]="functionScopeVariables" [functionArgs]="['data', 'dsData', 'dsIndex']" functionTitle="{{ 'widgets.maps.circle-tooltip-function' | translate }}" - helpId="widget/lib/map/polygon_tooltip_fn"> + helpId="widget/lib/map-legacy/polygon_tooltip_fn"> @@ -146,7 +146,7 @@ [globalVariables]="functionScopeVariables" [functionArgs]="['data', 'dsData', 'dsIndex']" functionTitle="{{ 'widgets.maps.circle-fill-color-function' | translate }}" - helpId="widget/lib/map/polygon_color_fn"> + helpId="widget/lib/map-legacy/polygon_color_fn"> @@ -186,7 +186,7 @@ [globalVariables]="functionScopeVariables" [functionArgs]="['data', 'dsData', 'dsIndex']" functionTitle="{{ 'widgets.maps.circle-stroke-color-function' | translate }}" - helpId="widget/lib/map/polygon_color_fn"> + helpId="widget/lib/map-legacy/polygon_color_fn"> diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/map/legacy/marker-clustering-settings.component.html b/ui-ngx/src/app/modules/home/components/widget/lib/settings/map/legacy/marker-clustering-settings.component.html index e3201cb4c5..21b4f19e8d 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/settings/map/legacy/marker-clustering-settings.component.html +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/map/legacy/marker-clustering-settings.component.html @@ -82,7 +82,7 @@ [globalVariables]="functionScopeVariables" [functionArgs]="['data', 'childCount']" functionTitle="{{ 'widgets.maps.marker-color-function' | translate }}" - helpId="widget/lib/map/clustering_color_fn"> + helpId="widget/lib/map-legacy/clustering_color_fn"> diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/map/legacy/markers-settings.component.html b/ui-ngx/src/app/modules/home/components/widget/lib/settings/map/legacy/markers-settings.component.html index 99b4f0fa29..4111a22987 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/settings/map/legacy/markers-settings.component.html +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/map/legacy/markers-settings.component.html @@ -35,7 +35,7 @@ [globalVariables]="functionScopeVariables" [functionArgs]="['origXPos', 'origYPos', 'data', 'dsData', 'dsIndex', 'aspect']" functionTitle="{{ 'widgets.maps.position-function' | translate }}" - helpId="widget/lib/map/position_fn"> + helpId="widget/lib/map-legacy/position_fn"> {{ 'widgets.maps.draggable-marker' | translate }} @@ -68,7 +68,7 @@ [globalVariables]="functionScopeVariables" [functionArgs]="['data', 'dsData', 'dsIndex']" functionTitle="{{ 'widgets.maps.label-function' | translate }}" - helpId="widget/lib/map/label_fn"> + helpId="widget/lib/map-legacy/label_fn"> @@ -112,7 +112,7 @@ [globalVariables]="functionScopeVariables" [functionArgs]="['data', 'dsData', 'dsIndex']" functionTitle="{{ 'widgets.maps.tooltip-function' | translate }}" - helpId="widget/lib/map/tooltip_fn"> + helpId="widget/lib/map-legacy/tooltip_fn">
@@ -151,7 +151,7 @@ [globalVariables]="functionScopeVariables" [functionArgs]="['data', 'dsData', 'dsIndex']" functionTitle="{{ 'widgets.maps.color-function' | translate }}" - helpId="widget/lib/map/color_fn"> + helpId="widget/lib/map-legacy/color_fn"> @@ -185,7 +185,7 @@ [globalVariables]="functionScopeVariables" [functionArgs]="['data', 'images', 'dsData', 'dsIndex']" functionTitle="{{ 'widgets.maps.marker-image-function' | translate }}" - helpId="widget/lib/map/marker_image_fn"> + helpId="widget/lib/map-legacy/marker_image_fn"> + helpId="widget/lib/map-legacy/label_fn"> @@ -110,7 +110,7 @@ [globalVariables]="functionScopeVariables" [functionArgs]="['data', 'dsData', 'dsIndex']" functionTitle="{{ 'widgets.maps.polygon-tooltip-function' | translate }}" - helpId="widget/lib/map/polygon_tooltip_fn"> + helpId="widget/lib/map-legacy/polygon_tooltip_fn"> @@ -146,7 +146,7 @@ [globalVariables]="functionScopeVariables" [functionArgs]="['data', 'dsData', 'dsIndex']" functionTitle="{{ 'widgets.maps.polygon-color-function' | translate }}" - helpId="widget/lib/map/polygon_color_fn"> + helpId="widget/lib/map-legacy/polygon_color_fn"> @@ -186,7 +186,7 @@ [globalVariables]="functionScopeVariables" [functionArgs]="['data', 'dsData', 'dsIndex']" functionTitle="{{ 'widgets.maps.polygon-stroke-color-function' | translate }}" - helpId="widget/lib/map/polygon_color_fn"> + helpId="widget/lib/map-legacy/polygon_color_fn"> diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/map/legacy/trip-animation-common-settings.component.html b/ui-ngx/src/app/modules/home/components/widget/lib/settings/map/legacy/trip-animation-common-settings.component.html index 1ac03e042a..59393ef944 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/settings/map/legacy/trip-animation-common-settings.component.html +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/map/legacy/trip-animation-common-settings.component.html @@ -85,7 +85,7 @@ [globalVariables]="functionScopeVariables" [functionArgs]="['data', 'dsData', 'dsIndex']" functionTitle="{{ 'widgets.maps.tooltip-function' | translate }}" - helpId="widget/lib/map/tooltip_fn"> + helpId="widget/lib/map-legacy/tooltip_fn"> diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/map/legacy/trip-animation-marker-settings.component.html b/ui-ngx/src/app/modules/home/components/widget/lib/settings/map/legacy/trip-animation-marker-settings.component.html index b869d12fb8..dfb2dd8ce5 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/settings/map/legacy/trip-animation-marker-settings.component.html +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/map/legacy/trip-animation-marker-settings.component.html @@ -50,7 +50,7 @@ [globalVariables]="functionScopeVariables" [functionArgs]="['data', 'dsData', 'dsIndex']" functionTitle="{{ 'widgets.maps.label-function' | translate }}" - helpId="widget/lib/map/label_fn"> + helpId="widget/lib/map-legacy/label_fn"> @@ -84,7 +84,7 @@ [globalVariables]="functionScopeVariables" [functionArgs]="['data', 'images', 'dsData', 'dsIndex']" functionTitle="{{ 'widgets.maps.marker-image-function' | translate }}" - helpId="widget/lib/map/marker_image_fn"> + helpId="widget/lib/map-legacy/marker_image_fn"> + helpId="widget/lib/map-legacy/path_color_fn"> diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/map/legacy/trip-animation-point-settings.component.html b/ui-ngx/src/app/modules/home/components/widget/lib/settings/map/legacy/trip-animation-point-settings.component.html index ea7b2d77ca..d98a88f6f1 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/settings/map/legacy/trip-animation-point-settings.component.html +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/map/legacy/trip-animation-point-settings.component.html @@ -59,7 +59,7 @@ [globalVariables]="functionScopeVariables" [functionArgs]="['data', 'dsData', 'dsIndex']" functionTitle="{{ 'widgets.maps.point-color-function' | translate }}" - helpId="widget/lib/map/path_point_color_fn"> + helpId="widget/lib/map-legacy/path_point_color_fn"> @@ -80,7 +80,7 @@ [globalVariables]="functionScopeVariables" [functionArgs]="['data', 'dsData', 'dsIndex']" functionTitle="{{ 'widgets.maps.point-as-anchor-function' | translate }}" - helpId="widget/lib/map/trip_point_as_anchor_fn"> + helpId="widget/lib/map-legacy/trip_point_as_anchor_fn"> diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/map/map-widget-settings.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/settings/map/map-widget-settings.component.ts index aea4ef376c..221f820d68 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/settings/map/map-widget-settings.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/map/map-widget-settings.component.ts @@ -19,7 +19,7 @@ import { WidgetSettings, WidgetSettingsComponent } from '@shared/models/widget.m import { UntypedFormBuilder, UntypedFormGroup } from '@angular/forms'; import { Store } from '@ngrx/store'; import { AppState } from '@core/core.state'; -import { isDefinedAndNotNull, mergeDeep, mergeDeepIgnoreArray } from '@core/utils'; +import { isDefinedAndNotNull, mergeDeepIgnoreArray } from '@core/utils'; import { mapWidgetDefaultSettings, MapWidgetSettings } from '@home/components/widget/lib/maps/map-widget.models'; import { WidgetConfigComponentData } from '@home/models/widget-component.models'; diff --git a/ui-ngx/src/app/shared/import-export/import-export.service.ts b/ui-ngx/src/app/shared/import-export/import-export.service.ts index 5212c15fd4..23a8007278 100644 --- a/ui-ngx/src/app/shared/import-export/import-export.service.ts +++ b/ui-ngx/src/app/shared/import-export/import-export.service.ts @@ -307,23 +307,23 @@ export class ImportExportService { } } return this.addImportedWidget(dashboard, targetState, targetLayoutFunction, widget, - aliasesInfo, filtersInfo, onAliasesUpdateFunction, onFiltersUpdateFunction, originalColumns, originalSize); + aliasesInfo, filtersInfo, onAliasesUpdateFunction, onFiltersUpdateFunction, originalColumns, originalSize, widgetItem.widgetExportInfo); } )); } else { return this.addImportedWidget(dashboard, targetState, targetLayoutFunction, widget, - aliasesInfo, filtersInfo, onAliasesUpdateFunction, onFiltersUpdateFunction, originalColumns, originalSize); + aliasesInfo, filtersInfo, onAliasesUpdateFunction, onFiltersUpdateFunction, originalColumns, originalSize, widgetItem.widgetExportInfo); } } ) ); } else { return this.addImportedWidget(dashboard, targetState, targetLayoutFunction, widget, - aliasesInfo, filtersInfo, onAliasesUpdateFunction, onFiltersUpdateFunction, originalColumns, originalSize); + aliasesInfo, filtersInfo, onAliasesUpdateFunction, onFiltersUpdateFunction, originalColumns, originalSize, widgetItem.widgetExportInfo); } } else { return this.addImportedWidget(dashboard, targetState, targetLayoutFunction, widget, - aliasesInfo, filtersInfo, onAliasesUpdateFunction, onFiltersUpdateFunction, originalColumns, originalSize); + aliasesInfo, filtersInfo, onAliasesUpdateFunction, onFiltersUpdateFunction, originalColumns, originalSize, widgetItem.widgetExportInfo); } } }), @@ -1033,11 +1033,11 @@ export class ImportExportService { filtersInfo: FiltersInfo, onAliasesUpdateFunction: () => void, onFiltersUpdateFunction: () => void, - originalColumns: number, originalSize: WidgetSize): Observable { + originalColumns: number, originalSize: WidgetSize, widgetExportInfo: any): Observable { return targetLayoutFunction().pipe( mergeMap((targetLayout) => this.itembuffer.addWidgetToDashboard(dashboard, targetState, targetLayout, widget, aliasesInfo, filtersInfo, onAliasesUpdateFunction, onFiltersUpdateFunction, - originalColumns, originalSize, -1, -1).pipe( + originalColumns, originalSize, -1, -1, 'default', widgetExportInfo).pipe( map(() => ({widget, layoutId: targetLayout} as ImportWidgetResult)) ) )); diff --git a/ui-ngx/src/app/shared/models/alias.models.ts b/ui-ngx/src/app/shared/models/alias.models.ts index 4005e451af..bc325a0692 100644 --- a/ui-ngx/src/app/shared/models/alias.models.ts +++ b/ui-ngx/src/app/shared/models/alias.models.ts @@ -18,6 +18,7 @@ import { EntityType } from '@shared/models/entity-type.models'; import { EntityId } from '@shared/models/id/entity-id'; import { EntitySearchDirection, RelationEntityTypeFilter } from '@shared/models/relation.models'; import { EntityFilter } from '@shared/models/query/query.models'; +import { guid, isEqual } from '@core/utils'; export enum AliasFilterType { singleEntity = 'singleEntity', @@ -210,3 +211,41 @@ export interface EntityAliasFilterResult { entityFilter: EntityFilter; entityParamName?: string; } + +export const getEntityAliasId = (entityAliases: EntityAliases, aliasInfo: EntityAliasInfo): string => { + let newAliasId: string; + for (const aliasId of Object.keys(entityAliases)) { + if (isEntityAliasEqual(entityAliases[aliasId], aliasInfo)) { + newAliasId = aliasId; + break; + } + } + if (!newAliasId) { + const newAliasName = createEntityAliasName(entityAliases, aliasInfo.alias); + newAliasId = guid(); + entityAliases[newAliasId] = {id: newAliasId, alias: newAliasName, filter: aliasInfo.filter}; + } + return newAliasId; +} + +const isEntityAliasEqual = (alias1: EntityAliasInfo, alias2: EntityAliasInfo): boolean => { + return isEqual(alias1.filter, alias2.filter); +} + +const createEntityAliasName = (entityAliases: EntityAliases, alias: string): string => { + let c = 0; + let newAlias = alias; + let unique = false; + while (!unique) { + unique = true; + for (const entAliasId of Object.keys(entityAliases)) { + const entAlias = entityAliases[entAliasId]; + if (newAlias === entAlias.alias) { + c++; + newAlias = alias + c; + unique = false; + } + } + } + return newAlias; +} diff --git a/ui-ngx/src/app/shared/models/query/query.models.ts b/ui-ngx/src/app/shared/models/query/query.models.ts index 98c0cce970..65128fe1eb 100644 --- a/ui-ngx/src/app/shared/models/query/query.models.ts +++ b/ui-ngx/src/app/shared/models/query/query.models.ts @@ -23,6 +23,7 @@ import { EntityType } from '@shared/models/entity-type.models'; import { DataKey, Datasource, DatasourceType } from '@shared/models/widget.models'; import { PageData } from '@shared/models/page/page-data'; import { + guid, isArraysEqualIgnoreUndefined, isDefined, isDefinedAndNotNull, @@ -921,3 +922,42 @@ export function updateDatasourceFromEntityInfo(datasource: Datasource, entity: E } } } + +export const getFilterId = (filters: Filters, filterInfo: FilterInfo): string => { + let newFilterId: string; + for (const filterId of Object.keys(filters)) { + if (isFilterEqual(filters[filterId], filterInfo)) { + newFilterId = filterId; + break; + } + } + if (!newFilterId) { + const newFilterName = createFilterName(filters, filterInfo.filter); + newFilterId = guid(); + filters[newFilterId] = {id: newFilterId, filter: newFilterName, + keyFilters: filterInfo.keyFilters, editable: filterInfo.editable}; + } + return newFilterId; +} + +const isFilterEqual = (filter1: FilterInfo, filter2: FilterInfo): boolean => { + return isEqual(filter1.keyFilters, filter2.keyFilters); +} + +const createFilterName = (filters: Filters, filter: string): string => { + let c = 0; + let newFilter = filter; + let unique = false; + while (!unique) { + unique = true; + for (const entFilterId of Object.keys(filters)) { + const entFilter = filters[entFilterId]; + if (newFilter === entFilter.filter) { + c++; + newFilter = filter + c; + unique = false; + } + } + } + return newFilter; +} diff --git a/ui-ngx/src/app/shared/models/widget/maps/map-export.models.ts b/ui-ngx/src/app/shared/models/widget/maps/map-export.models.ts new file mode 100644 index 0000000000..f7750db731 --- /dev/null +++ b/ui-ngx/src/app/shared/models/widget/maps/map-export.models.ts @@ -0,0 +1,151 @@ +/// +/// 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 { EntityAliases, EntityAliasInfo, getEntityAliasId } from '@shared/models/alias.models'; +import { FilterInfo, Filters, getFilterId } from '@shared/models/query/query.models'; +import { Dashboard } from '@shared/models/dashboard.models'; +import { DatasourceType, Widget } from '@shared/models/widget.models'; +import { BaseMapSettings, MapDataSourceSettings, MapType } from '@shared/models/widget/maps/map.models'; +import { WidgetExportDefinition } from '@shared/models/widget/widget-export.models'; + +interface ExportDataSourceInfo { + aliases: {[dataLayerIndex: number]: EntityAliasInfo}; + filters: {[dataLayerIndex: number]: FilterInfo}; +} + +interface MapDatasourcesInfo { + trips?: ExportDataSourceInfo; + markers?: ExportDataSourceInfo; + polygons?: ExportDataSourceInfo; + circles?: ExportDataSourceInfo; + additionalDataSources?: ExportDataSourceInfo; +} + +export const MapExportDefinition: WidgetExportDefinition = { + testWidget(widget: Widget): boolean { + if (widget?.config?.settings) { + const settings = widget.config.settings; + if (settings.mapType && [MapType.image, MapType.geoMap].includes(settings.mapType)) { + if (settings.trips && Array.isArray(settings.trips)) { + return true; + } + if (settings.markers && Array.isArray(settings.markers)) { + return true; + } + if (settings.polygons && Array.isArray(settings.polygons)) { + return true; + } + if (settings.circles && Array.isArray(settings.circles)) { + return true; + } + } + } + return false; + }, + prepareExportInfo(dashboard: Dashboard, widget: Widget): MapDatasourcesInfo { + const settings: BaseMapSettings = widget.config.settings as BaseMapSettings; + const info: MapDatasourcesInfo = {}; + if (settings.trips?.length) { + info.trips = prepareExportDataSourcesInfo(dashboard, settings.trips); + } + if (settings.markers?.length) { + info.markers = prepareExportDataSourcesInfo(dashboard, settings.markers); + } + if (settings.polygons?.length) { + info.polygons = prepareExportDataSourcesInfo(dashboard, settings.polygons); + } + if (settings.circles?.length) { + info.circles = prepareExportDataSourcesInfo(dashboard, settings.circles); + } + if (settings.additionalDataSources?.length) { + info.additionalDataSources = prepareExportDataSourcesInfo(dashboard, settings.additionalDataSources); + } + return info; + }, + updateFromExportInfo(widget: Widget, entityAliases: EntityAliases, filters: Filters, info: MapDatasourcesInfo): void { + const settings: BaseMapSettings = widget.config.settings as BaseMapSettings; + if (info?.trips) { + updateMapDatasourceFromExportInfo(entityAliases, filters, settings.trips, info.trips); + } + if (info?.markers) { + updateMapDatasourceFromExportInfo(entityAliases, filters, settings.markers, info.markers); + } + if (info?.polygons) { + updateMapDatasourceFromExportInfo(entityAliases, filters, settings.polygons, info.polygons); + } + if (info?.circles) { + updateMapDatasourceFromExportInfo(entityAliases, filters, settings.circles, info.circles); + } + if (info?.additionalDataSources) { + updateMapDatasourceFromExportInfo(entityAliases, filters, settings.additionalDataSources, info.additionalDataSources); + } + } +}; + +const updateMapDatasourceFromExportInfo = (entityAliases: EntityAliases, + filters: Filters, settings: MapDataSourceSettings[], info: ExportDataSourceInfo): void => { + if (info.aliases) { + for (const dsIndexStr of Object.keys(info.aliases)) { + const dsIndex = Number(dsIndexStr); + if (settings[dsIndex] && settings[dsIndex].dsType === DatasourceType.entity) { + const aliasInfo = info.aliases[dsIndex]; + settings[dsIndex].dsEntityAliasId = getEntityAliasId(entityAliases, aliasInfo); + } + } + } + if (info.filters) { + for (const dsIndexStr of Object.keys(info.filters)) { + const dsIndex = Number(dsIndexStr); + if (settings[dsIndex] && settings[dsIndex].dsType === DatasourceType.entity) { + const filterInfo = info.filters[dsIndex]; + settings[dsIndex].dsFilterId = getFilterId(filters, filterInfo); + } + } + } +} + +const prepareExportDataSourcesInfo = (dashboard: Dashboard, settings: MapDataSourceSettings[]): ExportDataSourceInfo => { + const info: ExportDataSourceInfo = { + aliases: {}, + filters: {} + }; + settings.forEach((dsSettings, index) => { + prepareExportDataSourceInfo(dashboard, info, dsSettings, index); + }); + return info; +} + +const prepareExportDataSourceInfo = (dashboard: Dashboard, info: ExportDataSourceInfo, settings: MapDataSourceSettings, index: number): void => { + if (settings.dsType === DatasourceType.entity) { + const entityAlias = dashboard.configuration.entityAliases[settings.dsEntityAliasId]; + if (entityAlias) { + info.aliases[index] = { + alias: entityAlias.alias, + filter: entityAlias.filter + }; + } + if (settings.dsFilterId && dashboard.configuration.filters) { + const filter = dashboard.configuration.filters[settings.dsFilterId]; + if (filter) { + info.filters[index] = { + filter: filter.filter, + keyFilters: filter.keyFilters, + editable: filter.editable + }; + } + } + } +} diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/maps/models/map.models.ts b/ui-ngx/src/app/shared/models/widget/maps/map.models.ts similarity index 99% rename from ui-ngx/src/app/modules/home/components/widget/lib/maps/models/map.models.ts rename to ui-ngx/src/app/shared/models/widget/maps/map.models.ts index 0f0e64f5b7..8b44534c68 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/maps/models/map.models.ts +++ b/ui-ngx/src/app/shared/models/widget/maps/map.models.ts @@ -39,7 +39,7 @@ import { TbFunction } from '@shared/models/js-function.models'; import { Observable, Observer, of, switchMap } from 'rxjs'; import { map } from 'rxjs/operators'; import { ImagePipe } from '@shared/pipe/image.pipe'; -import { MarkerShape } from '@home/components/widget/lib/maps/models/marker-shape.models'; +import { MarkerShape } from '@shared/models/widget/maps/marker-shape.models'; import { DateFormatSettings, simpleDateFormat } from '@shared/models/widget-settings.models'; export enum MapType { diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/maps/models/marker-shape.models.ts b/ui-ngx/src/app/shared/models/widget/maps/marker-shape.models.ts similarity index 98% rename from ui-ngx/src/app/modules/home/components/widget/lib/maps/models/marker-shape.models.ts rename to ui-ngx/src/app/shared/models/widget/maps/marker-shape.models.ts index fa7007b886..7a4c25d879 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/maps/models/marker-shape.models.ts +++ b/ui-ngx/src/app/shared/models/widget/maps/marker-shape.models.ts @@ -205,7 +205,7 @@ export const createColorMarkerIconElement = (iconRegistry: MatIconRegistry, domS let placeItemIconURI$: Observable; -export const createPlaceItemIcon= (iconRegistry: MatIconRegistry, domSanitizer: DomSanitizer): Observable => { +export const createPlaceItemIcon = (iconRegistry: MatIconRegistry, domSanitizer: DomSanitizer): Observable => { if (placeItemIconURI$) { return placeItemIconURI$; } diff --git a/ui-ngx/src/app/shared/models/widget/widget-export.models.ts b/ui-ngx/src/app/shared/models/widget/widget-export.models.ts new file mode 100644 index 0000000000..5b47010285 --- /dev/null +++ b/ui-ngx/src/app/shared/models/widget/widget-export.models.ts @@ -0,0 +1,35 @@ +/// +/// 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 { Widget } from '@shared/models/widget.models'; +import { Dashboard } from '@shared/models/dashboard.models'; +import { EntityAliases } from '@shared/models/alias.models'; +import { Filters } from '@shared/models/query/query.models'; +import { MapExportDefinition } from '@shared/models/widget/maps/map-export.models'; + +export interface WidgetExportDefinition { + testWidget(widget: Widget): boolean; + prepareExportInfo(dashboard: Dashboard, widget: Widget): T; + updateFromExportInfo(widget: Widget, entityAliases: EntityAliases, filters: Filters, info: T): void; +} + +const widgetExportDefinitions: WidgetExportDefinition[] = [ + MapExportDefinition +]; + +export const getWidgetExportDefinition = (widget: Widget): WidgetExportDefinition => { + return widgetExportDefinitions.find(def => def.testWidget(widget)); +} diff --git a/ui-ngx/src/assets/help/en_US/widget/lib/map-legacy/clustering_color_fn.md b/ui-ngx/src/assets/help/en_US/widget/lib/map-legacy/clustering_color_fn.md new file mode 100644 index 0000000000..ba58fa3cd3 --- /dev/null +++ b/ui-ngx/src/assets/help/en_US/widget/lib/map-legacy/clustering_color_fn.md @@ -0,0 +1,65 @@ +#### Clustering marker function + +
+
+ +*function (data, childCount): string* + +A JavaScript function used to compute clustering marker color. + +**Parameters:** + +
    +
  • data: FormattedData[] + - the array of total markers contained within each cluster.
    + Represents basic entity properties (ex. entityId, entityName)
    and provides access to other entity attributes/timeseries declared in widget datasource configuration. +
  • +
  • childCount: number - the total number of markers contained within that cluster +
  • +
+ +**Returns:** + +Should return string value presenting color of the marker. + +In case no data is returned, color value from **Color** settings field will be used. + +
+ +##### Examples + +
    +
  • +Calculate color depending on temperature telemetry value: +
  • + + +```javascript +let customColor; +for (let markerData of data) { + if (markerData.temperature > 40) { + customColor = 'red' + } +} +return customColor ? customColor : 'green'; +{:copy-code} +``` + +
  • +Calculate color depending on childCount: +
  • + +```javascript +if (childCount < 10) { + return 'green'; +} else if (childCount < 100) { + return 'yellow'; +} else { + return 'red'; +} +{:copy-code} +``` + +
+
+
diff --git a/ui-ngx/src/assets/help/en_US/widget/lib/map-legacy/color_fn.md b/ui-ngx/src/assets/help/en_US/widget/lib/map-legacy/color_fn.md new file mode 100644 index 0000000000..2f461e28a6 --- /dev/null +++ b/ui-ngx/src/assets/help/en_US/widget/lib/map-legacy/color_fn.md @@ -0,0 +1,42 @@ +#### Marker color function + +
+
+ +*function (data, dsData, dsIndex): string* + +A JavaScript function used to compute color of the marker. + +**Parameters:** + +
    + {% include widget/lib/map-legacy/map_fn_args %} +
+ +**Returns:** + +Should return string value presenting color of the marker. + +In case no data is returned, color value from **Color** settings field will be used. + +
+ +##### Examples + +* Calculate color depending on `temperature` telemetry value for `colorpin` device type: + +```javascript +var type = data['Type']; +if (type == 'colorpin') { + var temperature = data['temperature']; + if (typeof temperature !== undefined) { + var percent = (temperature + 60)/120 * 100; + return tinycolor.mix('blue', 'red', percent).toHexString(); + } + return 'blue'; +} +{:copy-code} +``` + +
+
diff --git a/ui-ngx/src/assets/help/en_US/widget/lib/map-legacy/label_fn.md b/ui-ngx/src/assets/help/en_US/widget/lib/map-legacy/label_fn.md new file mode 100644 index 0000000000..adfdcc287c --- /dev/null +++ b/ui-ngx/src/assets/help/en_US/widget/lib/map-legacy/label_fn.md @@ -0,0 +1,40 @@ +#### Marker label function + +
+
+ +*function (data, dsData, dsIndex): string* + +A JavaScript function used to compute text or HTML code of the marker label. + +**Parameters:** + +
    + {% include widget/lib/map-legacy/map_fn_args %} +
+ +**Returns:** + +Should return string value presenting text or HTML of the marker label. + +
+ +##### Examples + +* Display styled label with corresponding latest telemetry data for `energy meter` or `thermometer` device types: + +```javascript +var deviceType = data['Type']; +if (typeof deviceType !== undefined) { + if (deviceType == "energy meter") { + return '${entityName}, ${energy:2} kWt'; + } else if (deviceType == "thermometer") { + return '${entityName}, ${temperature:2} °C'; + } +} +return data.entityName; +{:copy-code} +``` + +
+
diff --git a/ui-ngx/src/assets/help/en_US/widget/lib/map-legacy/map_fn_args.md b/ui-ngx/src/assets/help/en_US/widget/lib/map-legacy/map_fn_args.md new file mode 100644 index 0000000000..7a4f24f0ac --- /dev/null +++ b/ui-ngx/src/assets/help/en_US/widget/lib/map-legacy/map_fn_args.md @@ -0,0 +1,10 @@ +
  • data: FormattedData - A FormattedData object associated with marker or data point of the route.
    + Represents basic entity properties (ex. entityId, entityName)
    and provides access to other entity attributes/timeseries declared in widget datasource configuration. +
  • +
  • dsData: FormattedData[] - All available data associated with markers or routes data points 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 widget datasource configuration. +
  • +
  • dsIndex number - index of the current marker data or route data point in dsData array.
    + Note: The data argument is equivalent to dsData[dsIndex] expression. +
  • diff --git a/ui-ngx/src/assets/help/en_US/widget/lib/map-legacy/marker_image_fn.md b/ui-ngx/src/assets/help/en_US/widget/lib/map-legacy/marker_image_fn.md new file mode 100644 index 0000000000..2ef0f4d645 --- /dev/null +++ b/ui-ngx/src/assets/help/en_US/widget/lib/map-legacy/marker_image_fn.md @@ -0,0 +1,62 @@ +#### Marker image function + +
    +
    + +*function (data, images, dsData, dsIndex): {url: string, size: number}* + +A JavaScript function used to compute marker image. + +**Parameters:** + +
      + {% include widget/lib/map-legacy/map_fn_args %} +
    + +**Returns:** + +Should return marker image data having the following structure: + +```typescript +{ + url: string, + size: number +} +``` + +- *url* - marker image url; +- *size* - marker image size; + +In case no data is returned, default marker image will be used. + +
    + +##### Examples + +
      +
    • +Calculate image url depending on temperature telemetry value for thermometer device type.
      +Let's assume 4 images are defined in Marker images section. Each image corresponds to particular temperature level: +
    • +
    + +```javascript +var type = data['Type']; +if (type == 'thermometer') { + var res = { + url: images[0], + size: 40 + } + var temperature = data['temperature']; + if (typeof temperature !== undefined) { + var percent = (temperature + 60)/120; + var index = Math.min(3, Math.floor(4 * percent)); + res.url = images[index]; + } + return res; +} +{:copy-code} +``` + +
    +
    diff --git a/ui-ngx/src/assets/help/en_US/widget/lib/map-legacy/path_color_fn.md b/ui-ngx/src/assets/help/en_US/widget/lib/map-legacy/path_color_fn.md new file mode 100644 index 0000000000..01d3d09ea7 --- /dev/null +++ b/ui-ngx/src/assets/help/en_US/widget/lib/map-legacy/path_color_fn.md @@ -0,0 +1,42 @@ +#### Path color function + +
    +
    + +*function (data, dsData, dsIndex): string* + +A JavaScript function used to compute color of the trip path. + +**Parameters:** + +
      + {% include widget/lib/map-legacy/map_fn_args %} +
    + +**Returns:** + +Should return string value presenting color of the trip path. + +In case no data is returned, color value from **Path color** settings field will be used. + +
    + +##### Examples + +* Calculate color depending on `temperature` telemetry value for `colorpin` device type: + +```javascript +var type = data['Type']; +if (type == 'colorpin') { + var temperature = data['temperature']; + if (typeof temperature !== undefined) { + var percent = (temperature + 60)/120 * 100; + return tinycolor.mix('blue', 'red', percent).toHexString(); + } + return 'blue'; +} +{:copy-code} +``` + +
    +
    diff --git a/ui-ngx/src/assets/help/en_US/widget/lib/map-legacy/path_point_color_fn.md b/ui-ngx/src/assets/help/en_US/widget/lib/map-legacy/path_point_color_fn.md new file mode 100644 index 0000000000..83a4af5bc4 --- /dev/null +++ b/ui-ngx/src/assets/help/en_US/widget/lib/map-legacy/path_point_color_fn.md @@ -0,0 +1,43 @@ +#### Path point color function + +
    +
    + +*function (data, dsData, dsIndex): string* + +A JavaScript function used to compute color of the trip path point. + +**Parameters:** + +
      + {% include widget/lib/map-legacy/map_fn_args %} +
    + +**Returns:** + +Should return string value presenting color of the trip path point. + +In case no data is returned, color value from **Point color** settings field will be used. + +
    + +##### Examples + +* Calculate color depending on `temperature` telemetry value for `colorpin` device type: + +```javascript +var type = data['Type']; +if (type == 'colorpin') { + var temperature = data['temperature']; + if (typeof temperature !== undefined) { + var percent = (temperature + 60)/120 * 100; + return tinycolor.mix('blue', 'red', percent).toHexString(); + } + return 'blue'; +} +{:copy-code} +``` + +
    +
    + diff --git a/ui-ngx/src/assets/help/en_US/widget/lib/map/polygon_color_fn.md b/ui-ngx/src/assets/help/en_US/widget/lib/map-legacy/polygon_color_fn.md similarity index 94% rename from ui-ngx/src/assets/help/en_US/widget/lib/map/polygon_color_fn.md rename to ui-ngx/src/assets/help/en_US/widget/lib/map-legacy/polygon_color_fn.md index b4cc0c5223..45edf0d2a5 100644 --- a/ui-ngx/src/assets/help/en_US/widget/lib/map/polygon_color_fn.md +++ b/ui-ngx/src/assets/help/en_US/widget/lib/map-legacy/polygon_color_fn.md @@ -10,7 +10,7 @@ A JavaScript function used to compute color of the polygon. **Parameters:**
      - {% include widget/lib/map/map_fn_args %} + {% include widget/lib/map-legacy/map_fn_args %}
    **Returns:** diff --git a/ui-ngx/src/assets/help/en_US/widget/lib/map-legacy/polygon_tooltip_fn.md b/ui-ngx/src/assets/help/en_US/widget/lib/map-legacy/polygon_tooltip_fn.md new file mode 100644 index 0000000000..b583e93228 --- /dev/null +++ b/ui-ngx/src/assets/help/en_US/widget/lib/map-legacy/polygon_tooltip_fn.md @@ -0,0 +1,40 @@ +#### Polygon tooltip function + +
    +
    + +*function (data, dsData, dsIndex): string* + +A JavaScript function used to compute text or HTML code to be displayed in the polygon tooltip. + +**Parameters:** + +
      + {% include widget/lib/map-legacy/map_fn_args %} +
    + +**Returns:** + +Should return string value presenting text or HTML for the polygon tooltip. + +
    + +##### Examples + +* Display details with corresponding telemetry data for `energy meter` or `thermostat` device types: + +```javascript +var deviceType = data['Type']; +if (typeof deviceType !== undefined) { + if (deviceType == "energy meter") { + return '${entityName}
    Energy: ${energy:2} kWt
    '; + } else if (deviceType == "thermostat") { + return '${entityName}
    Temperature: ${temperature:2} °C
    '; + } +} +return data.entityName; +{:copy-code} +``` + +
    +
    diff --git a/ui-ngx/src/assets/help/en_US/widget/lib/map-legacy/position_fn.md b/ui-ngx/src/assets/help/en_US/widget/lib/map-legacy/position_fn.md new file mode 100644 index 0000000000..fa33714fbd --- /dev/null +++ b/ui-ngx/src/assets/help/en_US/widget/lib/map-legacy/position_fn.md @@ -0,0 +1,65 @@ +#### Position conversion function + +
    +
    + +*function (origXPos, origYPos, data, dsData, dsIndex, aspect): {x: number, y: number}* + +A JavaScript function used to convert original relative x, y coordinates of the marker. + +**Parameters:** + +
      +
    • origXPos: number - original relative x coordinate as double from 0 to 1.
    • +
    • origYPos: number - original relative y coordinate as double from 0 to 1.
    • + {% include widget/lib/map-legacy/map_fn_args %} +
    • aspect: number - image map aspect ratio.
    • +
    + +**Returns:** + +Should return position data having the following structure: + +```typescript +{ + x: number, + y: number +} +``` + +- *x* - new relative x coordinate as double from 0 to 1; +- *y* - new relative y coordinate as double from 0 to 1; + +
    + +##### Examples + +* Scale the coordinates to half the original: + +```javascript +return {x: origXPos / 2, y: origYPos / 2}; +{:copy-code} +``` + +* Detect markers with same positions and place them with minimum overlap: + +```javascript +var xPos = data.xPos; +var yPos = data.yPos; +var locationGroup = dsData.filter((item) => item.xPos === xPos && item.yPos === yPos); +if (locationGroup.length > 1) { + const count = locationGroup.length; + const index = locationGroup.indexOf(data); + const radius = 0.035; + const angle = (360 / count) * index - 45; + const x = xPos + radius * Math.sin(angle*Math.PI/180) / aspect; + const y = yPos + radius * Math.cos(angle*Math.PI/180); + return {x: x, y: y}; +} else { + return {x: xPos, y: yPos}; +} +{:copy-code} +``` + +
    +
    diff --git a/ui-ngx/src/assets/help/en_US/widget/lib/map-legacy/tooltip_fn.md b/ui-ngx/src/assets/help/en_US/widget/lib/map-legacy/tooltip_fn.md new file mode 100644 index 0000000000..8b290c2215 --- /dev/null +++ b/ui-ngx/src/assets/help/en_US/widget/lib/map-legacy/tooltip_fn.md @@ -0,0 +1,40 @@ +#### Marker tooltip function + +
    +
    + +*function (data, dsData, dsIndex): string* + +A JavaScript function used to compute text or HTML code to be displayed in the marker, point or polygon tooltip. + +**Parameters:** + +
      + {% include widget/lib/map-legacy/map_fn_args %} +
    + +**Returns:** + +Should return string value presenting text or HTML for the tooltip. + +
    + +##### Examples + +* Display details with corresponding telemetry data for `thermostat` device type: + +```javascript +var deviceType = data['Type']; +if (typeof deviceType !== undefined) { + if (deviceType == "energy meter") { + return '${entityName}
    Energy: ${energy:2} kWt
    '; + } else if (deviceType == "thermometer") { + return '${entityName}
    Temperature: ${temperature:2} °C
    '; + } +} +return data.entityName; +{:copy-code} +``` + +
    +
    diff --git a/ui-ngx/src/assets/help/en_US/widget/lib/map-legacy/trip_point_as_anchor_fn.md b/ui-ngx/src/assets/help/en_US/widget/lib/map-legacy/trip_point_as_anchor_fn.md new file mode 100644 index 0000000000..eb11261f3d --- /dev/null +++ b/ui-ngx/src/assets/help/en_US/widget/lib/map-legacy/trip_point_as_anchor_fn.md @@ -0,0 +1,34 @@ +#### Point as anchor function + +
    +
    + +*function (data, dsData, dsIndex): boolean* + +A JavaScript function evaluating whether to use trip point as time anchor used in time selector. + +**Parameters:** + +
      + {% include widget/lib/map-legacy/map_fn_args %} +
    + +**Returns:** + +`true` if the point should be decided as anchor, `false` otherwise. + +In case no data is returned, the point is not used as anchor. + +
    + +##### Examples + +* Make anchors with 5 seconds step interval: + +```javascript +return data.time % 5000 < 1000; +{:copy-code} +``` + +
    +
    diff --git a/ui-ngx/src/assets/help/en_US/widget/lib/map/circle_fill_color_fn.md b/ui-ngx/src/assets/help/en_US/widget/lib/map/circle_fill_color_fn.md new file mode 100644 index 0000000000..9a7a383876 --- /dev/null +++ b/ui-ngx/src/assets/help/en_US/widget/lib/map/circle_fill_color_fn.md @@ -0,0 +1,24 @@ +#### Circle fill color function + +
    +
    + +*function (data, dsData): string* + +A JavaScript function used to compute fill color of the circle. + +**Parameters:** + +
      + {% include widget/lib/map/map_fn_args %} +
    + +**Returns:** + +Should return string value presenting fill color of the circle. + +In case no data is returned, color value from **Color** settings field will be used. + +
    + +{% include widget/lib/map/color_fn_examples %} diff --git a/ui-ngx/src/assets/help/en_US/widget/lib/map/circle_label_fn.md b/ui-ngx/src/assets/help/en_US/widget/lib/map/circle_label_fn.md new file mode 100644 index 0000000000..1b73148876 --- /dev/null +++ b/ui-ngx/src/assets/help/en_US/widget/lib/map/circle_label_fn.md @@ -0,0 +1,22 @@ +#### Circle label function + +
    +
    + +*function (data, dsData): string* + +A JavaScript function used to compute text or HTML code of the circle label. + +**Parameters:** + +
      + {% include widget/lib/map/map_fn_args %} +
    + +**Returns:** + +Should return string value presenting text or HTML of the circle label. + +
    + +{% include widget/lib/map/label_fn_examples %} diff --git a/ui-ngx/src/assets/help/en_US/widget/lib/map/circle_stroke_color_fn.md b/ui-ngx/src/assets/help/en_US/widget/lib/map/circle_stroke_color_fn.md new file mode 100644 index 0000000000..31d0a0f82a --- /dev/null +++ b/ui-ngx/src/assets/help/en_US/widget/lib/map/circle_stroke_color_fn.md @@ -0,0 +1,24 @@ +#### Circle stroke color function + +
    +
    + +*function (data, dsData): string* + +A JavaScript function used to compute stroke color of the circle. + +**Parameters:** + +
      + {% include widget/lib/map/map_fn_args %} +
    + +**Returns:** + +Should return string value presenting stroke color of the circle. + +In case no data is returned, color value from **Color** settings field will be used. + +
    + +{% include widget/lib/map/color_fn_examples %} diff --git a/ui-ngx/src/assets/help/en_US/widget/lib/map/circle_tooltip_fn.md b/ui-ngx/src/assets/help/en_US/widget/lib/map/circle_tooltip_fn.md new file mode 100644 index 0000000000..1ea037f084 --- /dev/null +++ b/ui-ngx/src/assets/help/en_US/widget/lib/map/circle_tooltip_fn.md @@ -0,0 +1,22 @@ +#### Circle tooltip function + +
    +
    + +*function (data, dsData): string* + +A JavaScript function used to compute text or HTML code to be displayed in the circle tooltip. + +**Parameters:** + +
      + {% include widget/lib/map/map_fn_args %} +
    + +**Returns:** + +Should return string value presenting text or HTML for the tooltip. + +
    + +{% include widget/lib/map/tooltip_fn_examples %} diff --git a/ui-ngx/src/assets/help/en_US/widget/lib/map/clustering_color_fn.md b/ui-ngx/src/assets/help/en_US/widget/lib/map/clustering_color_fn.md index ba58fa3cd3..33d4df2f18 100644 --- a/ui-ngx/src/assets/help/en_US/widget/lib/map/clustering_color_fn.md +++ b/ui-ngx/src/assets/help/en_US/widget/lib/map/clustering_color_fn.md @@ -10,9 +10,9 @@ A JavaScript function used to compute clustering marker color. **Parameters:**
      -
    • data: FormattedData[] +
    • data: FormattedData[] - the array of total markers contained within each cluster.
      - Represents basic entity properties (ex. entityId, entityName)
      and provides access to other entity attributes/timeseries declared in widget datasource configuration. + Represents basic entity properties (ex. entityId, entityName)
      and provides access to other entity attributes/timeseries declared in datasource of the data layer configuration.
    • childCount: number - the total number of markers contained within that cluster
    • @@ -22,7 +22,7 @@ A JavaScript function used to compute clustering marker color. Should return string value presenting color of the marker. -In case no data is returned, color value from **Color** settings field will be used. +In case no data is returned, default colors will be used depending on number of markers within that cluster.
      diff --git a/ui-ngx/src/assets/help/en_US/widget/lib/map/color_fn.md b/ui-ngx/src/assets/help/en_US/widget/lib/map/color_fn.md index ede068d68f..fdecf9596c 100644 --- a/ui-ngx/src/assets/help/en_US/widget/lib/map/color_fn.md +++ b/ui-ngx/src/assets/help/en_US/widget/lib/map/color_fn.md @@ -3,7 +3,7 @@

      -*function (data, dsData, dsIndex): string* +*function (data, dsData): string* A JavaScript function used to compute color of the marker. @@ -21,22 +21,4 @@ In case no data is returned, color value from **Color** settings field will be u
      -##### Examples - -* Calculate color depending on `temperature` telemetry value for `colorpin` device type: - -```javascript -var type = data['Type']; -if (type == 'colorpin') { - var temperature = data['temperature']; - if (typeof temperature !== undefined) { - var percent = (temperature + 60)/120 * 100; - return tinycolor.mix('blue', 'red', percent).toHexString(); - } - return 'blue'; -} -{:copy-code} -``` - -
      -
      +{% include widget/lib/map/color_fn_examples %} diff --git a/ui-ngx/src/assets/help/en_US/widget/lib/map/color_fn_examples.md b/ui-ngx/src/assets/help/en_US/widget/lib/map/color_fn_examples.md new file mode 100644 index 0000000000..8c3631bde7 --- /dev/null +++ b/ui-ngx/src/assets/help/en_US/widget/lib/map/color_fn_examples.md @@ -0,0 +1,19 @@ +##### Examples + +* Calculate color depending on `temperature` telemetry value for `thermostat` device type: + +```javascript +var type = data.Type; +if (type == 'thermostat') { + var temperature = data.temperature; + if (typeof temperature !== undefined) { + var percent = (temperature + 60)/120 * 100; + return tinycolor.mix('blue', 'red', percent).toHexString(); + } + return 'blue'; +} +{:copy-code} +``` + +
      +
      diff --git a/ui-ngx/src/assets/help/en_US/widget/lib/map/label_fn.md b/ui-ngx/src/assets/help/en_US/widget/lib/map/label_fn.md index eb3e6f981e..0028d77b82 100644 --- a/ui-ngx/src/assets/help/en_US/widget/lib/map/label_fn.md +++ b/ui-ngx/src/assets/help/en_US/widget/lib/map/label_fn.md @@ -3,7 +3,7 @@

      -*function (data, dsData, dsIndex): string* +*function (data, dsData): string* A JavaScript function used to compute text or HTML code of the marker label. @@ -19,22 +19,4 @@ Should return string value presenting text or HTML of the marker label.
      -##### Examples - -* Display styled label with corresponding latest telemetry data for `energy meter` or `thermometer` device types: - -```javascript -var deviceType = data['Type']; -if (typeof deviceType !== undefined) { - if (deviceType == "energy meter") { - return '${entityName}, ${energy:2} kWt'; - } else if (deviceType == "thermometer") { - return '${entityName}, ${temperature:2} °C'; - } -} -return data.entityName; -{:copy-code} -``` - -
      -
      +{% include widget/lib/map/label_fn_examples %} diff --git a/ui-ngx/src/assets/help/en_US/widget/lib/map/label_fn_examples.md b/ui-ngx/src/assets/help/en_US/widget/lib/map/label_fn_examples.md new file mode 100644 index 0000000000..2e2ecfabec --- /dev/null +++ b/ui-ngx/src/assets/help/en_US/widget/lib/map/label_fn_examples.md @@ -0,0 +1,19 @@ +##### Examples + +* Display styled label with corresponding latest telemetry data for `energy meter` or `thermometer` device types: + +```javascript +var deviceType = data.Type; +if (typeof deviceType !== undefined) { + if (deviceType == "energy meter") { + return '${entityName}, ${energy:2} kWt'; + } else if (deviceType == "thermometer") { + return '${entityName}, ${temperature:2} °C'; + } +} +return data.entityName; +{:copy-code} +``` + +
      +
      diff --git a/ui-ngx/src/assets/help/en_US/widget/lib/map/map_fn_args.md b/ui-ngx/src/assets/help/en_US/widget/lib/map/map_fn_args.md index 7a4f24f0ac..a627e1867f 100644 --- a/ui-ngx/src/assets/help/en_US/widget/lib/map/map_fn_args.md +++ b/ui-ngx/src/assets/help/en_US/widget/lib/map/map_fn_args.md @@ -1,10 +1,8 @@ -
    • data: FormattedData - A FormattedData object associated with marker or data point of the route.
      - Represents basic entity properties (ex. entityId, entityName)
      and provides access to other entity attributes/timeseries declared in widget datasource configuration. +
    • 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.
    • -
    • dsData: FormattedData[] - All available data associated with markers or routes data points as array of FormattedData objects
      +
    • 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 widget datasource configuration. -
    • -
    • dsIndex number - index of the current marker data or route data point in dsData array.
      - Note: The data argument is equivalent to dsData[dsIndex] expression. + 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/help/en_US/widget/lib/map/marker_image_fn.md b/ui-ngx/src/assets/help/en_US/widget/lib/map/marker_image_fn.md index 565de0147a..ed8b82d4cc 100644 --- a/ui-ngx/src/assets/help/en_US/widget/lib/map/marker_image_fn.md +++ b/ui-ngx/src/assets/help/en_US/widget/lib/map/marker_image_fn.md @@ -3,14 +3,14 @@

      -*function (data, images, dsData, dsIndex): {url: string, size: number}* +*function (data, images, dsData): {url: string, size: number}* A JavaScript function used to compute marker image. **Parameters:**
        - {% include widget/lib/map/map_fn_args %} + {% include widget/lib/map/marker_image_fn_args %}
      **Returns:** @@ -18,14 +18,18 @@ A JavaScript function used to compute marker image. Should return marker image data having the following structure: ```typescript -{ - url: string, - size: number +{ + url: string; + size: number; + markerOffset?: [number, number]; + tooltipOffset?: [number, number]; } ``` - *url* - marker image url; - *size* - marker image size; +- *markerOffset* - optional array of two numbers presenting relative horizontal and vertical offset of the marker image; +- *tooltipOffset* - optional array of two numbers presenting relative horizontal and vertical offset of the marker image tooltip; In case no data is returned, default marker image will be used. @@ -41,13 +45,13 @@ Let's assume 4 images are defined in Marker images section. Each image co
    ```javascript -var type = data['Type']; +var type = data.Type; if (type == 'thermometer') { var res = { url: images[0], size: 40 } - var temperature = data['temperature']; + var temperature = data.temperature; if (typeof temperature !== undefined) { var percent = (temperature + 60)/120; var index = Math.min(3, Math.floor(4 * percent)); diff --git a/ui-ngx/src/assets/help/en_US/widget/lib/map/marker_image_fn_args.md b/ui-ngx/src/assets/help/en_US/widget/lib/map/marker_image_fn_args.md new file mode 100644 index 0000000000..e86bd3106f --- /dev/null +++ b/ui-ngx/src/assets/help/en_US/widget/lib/map/marker_image_fn_args.md @@ -0,0 +1,10 @@ +
  • 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 Marker 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/help/en_US/widget/lib/map/path_color_fn.md b/ui-ngx/src/assets/help/en_US/widget/lib/map/path_color_fn.md index c4df0a99a9..7f90b9c8f7 100644 --- a/ui-ngx/src/assets/help/en_US/widget/lib/map/path_color_fn.md +++ b/ui-ngx/src/assets/help/en_US/widget/lib/map/path_color_fn.md @@ -3,7 +3,7 @@

    -*function (data, dsData, dsIndex): string* +*function (data, dsData): string* A JavaScript function used to compute color of the trip path. @@ -17,26 +17,8 @@ A JavaScript function used to compute color of the trip path. Should return string value presenting color of the trip path. -In case no data is returned, color value from **Path color** settings field will be used. +In case no data is returned, color value from **Color** settings field will be used.
    -##### Examples - -* Calculate color depending on `temperature` telemetry value for `colorpin` device type: - -```javascript -var type = data['Type']; -if (type == 'colorpin') { - var temperature = data['temperature']; - if (typeof temperature !== undefined) { - var percent = (temperature + 60)/120 * 100; - return tinycolor.mix('blue', 'red', percent).toHexString(); - } - return 'blue'; -} -{:copy-code} -``` - -
    -
    +{% include widget/lib/map/color_fn_examples %} diff --git a/ui-ngx/src/assets/help/en_US/widget/lib/map/path_point_color_fn.md b/ui-ngx/src/assets/help/en_US/widget/lib/map/path_point_color_fn.md index de092704c3..be95f136ad 100644 --- a/ui-ngx/src/assets/help/en_US/widget/lib/map/path_point_color_fn.md +++ b/ui-ngx/src/assets/help/en_US/widget/lib/map/path_point_color_fn.md @@ -3,7 +3,7 @@

    -*function (data, dsData, dsIndex): string* +*function (data, dsData): string* A JavaScript function used to compute color of the trip path point. @@ -17,27 +17,8 @@ A JavaScript function used to compute color of the trip path point. Should return string value presenting color of the trip path point. -In case no data is returned, color value from **Point color** settings field will be used. +In case no data is returned, color value from **Color** settings field will be used.
    -##### Examples - -* Calculate color depending on `temperature` telemetry value for `colorpin` device type: - -```javascript -var type = data['Type']; -if (type == 'colorpin') { - var temperature = data['temperature']; - if (typeof temperature !== undefined) { - var percent = (temperature + 60)/120 * 100; - return tinycolor.mix('blue', 'red', percent).toHexString(); - } - return 'blue'; -} -{:copy-code} -``` - -
    -
    - +{% include widget/lib/map/color_fn_examples %} diff --git a/ui-ngx/src/assets/help/en_US/widget/lib/map/path_point_tooltip_fn.md b/ui-ngx/src/assets/help/en_US/widget/lib/map/path_point_tooltip_fn.md new file mode 100644 index 0000000000..4ce996626a --- /dev/null +++ b/ui-ngx/src/assets/help/en_US/widget/lib/map/path_point_tooltip_fn.md @@ -0,0 +1,22 @@ +#### Path point tooltip function + +
    +
    + +*function (data, dsData): string* + +A JavaScript function used to compute text or HTML code to be displayed in the trip path point tooltip. + +**Parameters:** + +
      + {% include widget/lib/map/map_fn_args %} +
    + +**Returns:** + +Should return string value presenting text or HTML for the tooltip. + +
    + +{% include widget/lib/map/tooltip_fn_examples %} diff --git a/ui-ngx/src/assets/help/en_US/widget/lib/map/polygon_fill_color_fn.md b/ui-ngx/src/assets/help/en_US/widget/lib/map/polygon_fill_color_fn.md new file mode 100644 index 0000000000..ef1ca8c54c --- /dev/null +++ b/ui-ngx/src/assets/help/en_US/widget/lib/map/polygon_fill_color_fn.md @@ -0,0 +1,24 @@ +#### Polygon fill color function + +
    +
    + +*function (data, dsData): string* + +A JavaScript function used to compute fill color of the polygon. + +**Parameters:** + +
      + {% include widget/lib/map/map_fn_args %} +
    + +**Returns:** + +Should return string value presenting fill color of the polygon. + +In case no data is returned, color value from **Color** settings field will be used. + +
    + +{% include widget/lib/map/color_fn_examples %} diff --git a/ui-ngx/src/assets/help/en_US/widget/lib/map/polygon_label_fn.md b/ui-ngx/src/assets/help/en_US/widget/lib/map/polygon_label_fn.md new file mode 100644 index 0000000000..00bc0ee50b --- /dev/null +++ b/ui-ngx/src/assets/help/en_US/widget/lib/map/polygon_label_fn.md @@ -0,0 +1,22 @@ +#### Polygon label function + +
    +
    + +*function (data, dsData): string* + +A JavaScript function used to compute text or HTML code of the polygon label. + +**Parameters:** + +
      + {% include widget/lib/map/map_fn_args %} +
    + +**Returns:** + +Should return string value presenting text or HTML of the polygon label. + +
    + +{% include widget/lib/map/label_fn_examples %} diff --git a/ui-ngx/src/assets/help/en_US/widget/lib/map/polygon_stroke_color_fn.md b/ui-ngx/src/assets/help/en_US/widget/lib/map/polygon_stroke_color_fn.md new file mode 100644 index 0000000000..4419ab991c --- /dev/null +++ b/ui-ngx/src/assets/help/en_US/widget/lib/map/polygon_stroke_color_fn.md @@ -0,0 +1,24 @@ +#### Polygon stroke color function + +
    +
    + +*function (data, dsData): string* + +A JavaScript function used to compute stroke color of the polygon. + +**Parameters:** + +
      + {% include widget/lib/map/map_fn_args %} +
    + +**Returns:** + +Should return string value presenting stroke color of the polygon. + +In case no data is returned, color value from **Color** settings field will be used. + +
    + +{% include widget/lib/map/color_fn_examples %} diff --git a/ui-ngx/src/assets/help/en_US/widget/lib/map/polygon_tooltip_fn.md b/ui-ngx/src/assets/help/en_US/widget/lib/map/polygon_tooltip_fn.md index 86de41c185..9c3a5c8e5e 100644 --- a/ui-ngx/src/assets/help/en_US/widget/lib/map/polygon_tooltip_fn.md +++ b/ui-ngx/src/assets/help/en_US/widget/lib/map/polygon_tooltip_fn.md @@ -3,7 +3,7 @@

    -*function (data, dsData, dsIndex): string* +*function (data, dsData): string* A JavaScript function used to compute text or HTML code to be displayed in the polygon tooltip. @@ -15,26 +15,8 @@ A JavaScript function used to compute text or HTML code to be displayed in the p **Returns:** -Should return string value presenting text or HTML for the polygon tooltip. +Should return string value presenting text or HTML for the tooltip.
    -##### Examples - -* Display details with corresponding telemetry data for `energy meter` or `thermostat` device types: - -```javascript -var deviceType = data['Type']; -if (typeof deviceType !== undefined) { - if (deviceType == "energy meter") { - return '${entityName}
    Energy: ${energy:2} kWt
    '; - } else if (deviceType == "thermostat") { - return '${entityName}
    Temperature: ${temperature:2} °C
    '; - } -} -return data.entityName; -{:copy-code} -``` - -
    -
    +{% include widget/lib/map/tooltip_fn_examples %} diff --git a/ui-ngx/src/assets/help/en_US/widget/lib/map/position_fn.md b/ui-ngx/src/assets/help/en_US/widget/lib/map/position_fn.md index 5687e4fe84..f85c691cdd 100644 --- a/ui-ngx/src/assets/help/en_US/widget/lib/map/position_fn.md +++ b/ui-ngx/src/assets/help/en_US/widget/lib/map/position_fn.md @@ -3,7 +3,7 @@

    -*function (origXPos, origYPos, data, dsData, dsIndex, aspect): {x: number, y: number}* +*function (origXPos, origYPos, data, dsData, aspect): {x: number, y: number}* A JavaScript function used to convert original relative x, y coordinates of the marker. @@ -22,8 +22,8 @@ Should return position data having the following structure: ```typescript { - x: number, - y: number + x: number; + y: number; } ``` diff --git a/ui-ngx/src/assets/help/en_US/widget/lib/map/tooltip_fn.md b/ui-ngx/src/assets/help/en_US/widget/lib/map/tooltip_fn.md index d4d97d99b6..61fb00e7fd 100644 --- a/ui-ngx/src/assets/help/en_US/widget/lib/map/tooltip_fn.md +++ b/ui-ngx/src/assets/help/en_US/widget/lib/map/tooltip_fn.md @@ -3,9 +3,9 @@

    -*function (data, dsData, dsIndex): string* +*function (data, dsData): string* -A JavaScript function used to compute text or HTML code to be displayed in the marker, point or polygon tooltip. +A JavaScript function used to compute text or HTML code to be displayed in the marker tooltip. **Parameters:** @@ -19,22 +19,4 @@ Should return string value presenting text or HTML for the tooltip.
    -##### Examples - -* Display details with corresponding telemetry data for `thermostat` device type: - -```javascript -var deviceType = data['Type']; -if (typeof deviceType !== undefined) { - if (deviceType == "energy meter") { - return '${entityName}
    Energy: ${energy:2} kWt
    '; - } else if (deviceType == "thermometer") { - return '${entityName}
    Temperature: ${temperature:2} °C
    '; - } -} -return data.entityName; -{:copy-code} -``` - -
    -
    +{% include widget/lib/map/tooltip_fn_examples %} diff --git a/ui-ngx/src/assets/help/en_US/widget/lib/map/tooltip_fn_examples.md b/ui-ngx/src/assets/help/en_US/widget/lib/map/tooltip_fn_examples.md new file mode 100644 index 0000000000..34f8e06e0f --- /dev/null +++ b/ui-ngx/src/assets/help/en_US/widget/lib/map/tooltip_fn_examples.md @@ -0,0 +1,19 @@ +##### Examples + +* Display details with corresponding telemetry data for `energy meter` or `thermometer` device types: + +```javascript +var deviceType = data.Type; +if (typeof deviceType !== undefined) { + if (deviceType == "energy meter") { + return '${entityName}
    Energy: ${energy:2} kWt
    '; + } else if (deviceType == "thermometer") { + return '${entityName}
    Temperature: ${temperature:2} °C
    '; + } +} +return data.entityName; +{:copy-code} +``` + +
    +
    diff --git a/ui-ngx/src/assets/help/en_US/widget/lib/map/trip_point_as_anchor_fn.md b/ui-ngx/src/assets/help/en_US/widget/lib/map/trip_point_as_anchor_fn.md index e938532b78..167fb0a123 100644 --- a/ui-ngx/src/assets/help/en_US/widget/lib/map/trip_point_as_anchor_fn.md +++ b/ui-ngx/src/assets/help/en_US/widget/lib/map/trip_point_as_anchor_fn.md @@ -1,9 +1,9 @@ -#### Point as anchor function +#### Location snap filter function

    -*function (data, dsData, dsIndex): boolean* +*function (data, dsData): boolean* A JavaScript function evaluating whether to use trip point as time anchor used in time selector. diff --git a/ui-ngx/src/typings/leaflet-extend-tb.d.ts b/ui-ngx/src/typings/leaflet-extend-tb.d.ts index de63139bd7..3ba1317436 100644 --- a/ui-ngx/src/typings/leaflet-extend-tb.d.ts +++ b/ui-ngx/src/typings/leaflet-extend-tb.d.ts @@ -15,8 +15,8 @@ /// import { FormattedData } from '@shared/models/widget.models'; -import L, { Control, ControlOptions } from 'leaflet'; -import { TbMapDatasource } from '@home/components/widget/lib/maps/models/map.models'; +import L from 'leaflet'; +import { TbMapDatasource } from '@shared/models/widget/maps/map.models'; import { MatIconRegistry } from '@angular/material/icon'; // redeclare module, maintains compatibility with @types/leaflet From b6b440fa2e374a3012e889db40651b2dee3f71d0 Mon Sep 17 00:00:00 2001 From: IrynaMatveieva Date: Thu, 6 Mar 2025 10:12:29 +0200 Subject: [PATCH 207/345] updated desscription --- .../DefaultCalculatedFieldQueueService.java | 18 +---- .../telemetry/TbCalculatedFieldsNode.java | 69 +++++++++---------- 2 files changed, 33 insertions(+), 54 deletions(-) diff --git a/application/src/main/java/org/thingsboard/server/service/cf/DefaultCalculatedFieldQueueService.java b/application/src/main/java/org/thingsboard/server/service/cf/DefaultCalculatedFieldQueueService.java index c1ffb10e79..fc92b70e07 100644 --- a/application/src/main/java/org/thingsboard/server/service/cf/DefaultCalculatedFieldQueueService.java +++ b/application/src/main/java/org/thingsboard/server/service/cf/DefaultCalculatedFieldQueueService.java @@ -93,10 +93,7 @@ public class DefaultCalculatedFieldQueueService implements CalculatedFieldQueueS @Override public void pushRequestToQueue(TimeseriesSaveRequest request, FutureCallback callback) { - var tenantId = request.getTenantId(); - var entityId = request.getEntityId(); - checkEntityAndPushToQueue(tenantId, entityId, cf -> cf.matches(request.getEntries()), cf -> cf.linkMatches(entityId, request.getEntries()), - () -> toCalculatedFieldTelemetryMsgProto(request), callback); + pushRequestToQueue(request, null, callback); } @Override @@ -109,10 +106,7 @@ public class DefaultCalculatedFieldQueueService implements CalculatedFieldQueueS @Override public void pushRequestToQueue(AttributesSaveRequest request, FutureCallback callback) { - var tenantId = request.getTenantId(); - var entityId = request.getEntityId(); - checkEntityAndPushToQueue(tenantId, entityId, cf -> cf.matches(request.getEntries(), request.getScope()), cf -> cf.linkMatches(entityId, request.getEntries(), request.getScope()), - () -> toCalculatedFieldTelemetryMsgProto(request), callback); + pushRequestToQueue(request, null, callback); } @Override @@ -170,10 +164,6 @@ public class DefaultCalculatedFieldQueueService implements CalculatedFieldQueueS }; } - private ToCalculatedFieldMsg toCalculatedFieldTelemetryMsgProto(TimeseriesSaveRequest request) { - return toCalculatedFieldTelemetryMsgProto(request, null); - } - private ToCalculatedFieldMsg toCalculatedFieldTelemetryMsgProto(TimeseriesSaveRequest request, TimeseriesSaveResult result) { ToCalculatedFieldMsg.Builder msg = ToCalculatedFieldMsg.newBuilder(); @@ -194,10 +184,6 @@ public class DefaultCalculatedFieldQueueService implements CalculatedFieldQueueS return msg.build(); } - private ToCalculatedFieldMsg toCalculatedFieldTelemetryMsgProto(AttributesSaveRequest request) { - return toCalculatedFieldTelemetryMsgProto(request, null); - } - private ToCalculatedFieldMsg toCalculatedFieldTelemetryMsgProto(AttributesSaveRequest request, List versions) { ToCalculatedFieldMsg.Builder msg = ToCalculatedFieldMsg.newBuilder(); diff --git a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/telemetry/TbCalculatedFieldsNode.java b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/telemetry/TbCalculatedFieldsNode.java index ac0e61d9be..1f2c7f135d 100644 --- a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/telemetry/TbCalculatedFieldsNode.java +++ b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/telemetry/TbCalculatedFieldsNode.java @@ -16,7 +16,6 @@ package org.thingsboard.rule.engine.telemetry; import com.fasterxml.jackson.core.type.TypeReference; -import com.fasterxml.jackson.databind.JsonNode; import com.google.common.util.concurrent.FutureCallback; import com.google.gson.JsonParser; import jakarta.annotation.Nullable; @@ -43,24 +42,23 @@ import org.thingsboard.server.common.data.plugin.ComponentType; import org.thingsboard.server.common.msg.TbMsg; import java.util.ArrayList; +import java.util.Collections; import java.util.List; import java.util.Map; +import java.util.Optional; import static org.thingsboard.server.common.data.DataConstants.SCOPE; -import static org.thingsboard.server.common.data.msg.TbMsgType.ATTRIBUTES_DELETED; -import static org.thingsboard.server.common.data.msg.TbMsgType.POST_ATTRIBUTES_REQUEST; -import static org.thingsboard.server.common.data.msg.TbMsgType.POST_TELEMETRY_REQUEST; -import static org.thingsboard.server.common.data.msg.TbMsgType.TIMESERIES_DELETED; @Slf4j @RuleNode( type = ComponentType.ACTION, - name = "push to calculated fields", + name = "apply to calculated fields", configClazz = EmptyNodeConfiguration.class, - nodeDescription = "Pushes messages to the calculated fields for further processing", - nodeDetails = "Forwards incoming messages to the calculated fields, where they will be processed to compute values based on predefined calculation rules without persisting any data in the database.", + nodeDescription = "Processes incoming messages for calculated fields", + nodeDetails = "This node processes incoming messages to update telemetry or attributes for predefined calculated fields without storing the original telemetry or attributes in the database. " + + "It ensures that calculated fields receive and process the necessary data without persisting the incoming values.", configDirective = "tbNodeEmptyConfig", - icon = "send" + icon = "call_made" ) public class TbCalculatedFieldsNode implements TbNode { @@ -73,29 +71,20 @@ public class TbCalculatedFieldsNode implements TbNode { @Override public void onMsg(TbContext ctx, TbMsg msg) { - if (!msg.isTypeOneOf(POST_ATTRIBUTES_REQUEST, POST_TELEMETRY_REQUEST, ATTRIBUTES_DELETED, TIMESERIES_DELETED)) { - ctx.tellFailure(msg, new IllegalArgumentException("Unsupported msg type: " + msg.getType())); - return; - } - - String src = msg.getData(); - - if (msg.isTypeOf(POST_TELEMETRY_REQUEST)) { - processPostTelemetryRequest(ctx, msg, src); - } else if (msg.isTypeOf(POST_ATTRIBUTES_REQUEST)) { - processPostAttributesRequest(ctx, msg, src); - } else if (msg.isTypeOf(TIMESERIES_DELETED)) { - processTimeSeriesDeleted(ctx, msg, src); - } else { - processAttributesDeleted(ctx, msg, src); + switch (msg.getInternalType()) { + case POST_TELEMETRY_REQUEST -> processPostTelemetryRequest(ctx, msg); + case POST_ATTRIBUTES_REQUEST -> processPostAttributesRequest(ctx, msg); + case TIMESERIES_DELETED -> processTimeSeriesDeleted(ctx, msg); + case ATTRIBUTES_DELETED -> processAttributesDeleted(ctx, msg); + default -> ctx.tellFailure(msg, new IllegalArgumentException("Unsupported msg type: " + msg.getType())); } - } - private void processPostTelemetryRequest(TbContext ctx, TbMsg msg, String src) { - Map> tsKvMap = JsonConverter.convertToTelemetry(JsonParser.parseString(src), System.currentTimeMillis()); + private void processPostTelemetryRequest(TbContext ctx, TbMsg msg) { + Map> tsKvMap = JsonConverter.convertToTelemetry(JsonParser.parseString(msg.getData()), System.currentTimeMillis()); + if (tsKvMap.isEmpty()) { - ctx.tellFailure(msg, new IllegalArgumentException("Msg body is empty: " + src)); + ctx.tellFailure(msg, new IllegalArgumentException("Msg body is empty: " + msg.getData())); return; } @@ -120,8 +109,8 @@ public class TbCalculatedFieldsNode implements TbNode { ctx.getCalculatedFieldQueueService().pushRequestToQueue(timeseriesSaveRequest, timeseriesSaveRequest.getCallback()); } - private void processPostAttributesRequest(TbContext ctx, TbMsg msg, String src) { - List newAttributes = new ArrayList<>(JsonConverter.convertToAttributes(JsonParser.parseString(src))); + private void processPostAttributesRequest(TbContext ctx, TbMsg msg) { + List newAttributes = new ArrayList<>(JsonConverter.convertToAttributes(JsonParser.parseString(msg.getData()))); if (newAttributes.isEmpty()) { ctx.tellSuccess(msg); @@ -141,10 +130,11 @@ public class TbCalculatedFieldsNode implements TbNode { ctx.getCalculatedFieldQueueService().pushRequestToQueue(attributesSaveRequest, attributesSaveRequest.getCallback()); } - private void processTimeSeriesDeleted(TbContext ctx, TbMsg msg, String src) { - JsonNode dataJson = JacksonUtil.toJsonNode(msg.getData()); - List keysToDelete = JacksonUtil.convertValue(dataJson.get("timeSeries"), new TypeReference<>() { - }); + private void processTimeSeriesDeleted(TbContext ctx, TbMsg msg) { + List keysToDelete = Optional.ofNullable( + JacksonUtil.convertValue(JacksonUtil.toJsonNode(msg.getData()).get("timeseries"), new TypeReference>() { + }) + ).orElse(Collections.emptyList()); if (keysToDelete.isEmpty()) { ctx.tellSuccess(msg); @@ -161,10 +151,12 @@ public class TbCalculatedFieldsNode implements TbNode { .callback(new FutureCallback>() { @Override public void onSuccess(@Nullable List tmp) { + ctx.tellSuccess(msg); } @Override public void onFailure(Throwable t) { + ctx.tellFailure(msg, t); } }) .build(); @@ -172,10 +164,11 @@ public class TbCalculatedFieldsNode implements TbNode { ctx.getCalculatedFieldQueueService().pushRequestToQueue(timeseriesDeleteRequest, keysToDelete, getCalculatedFieldCallback(timeseriesDeleteRequest.getCallback(), keysToDelete)); } - private void processAttributesDeleted(TbContext ctx, TbMsg msg, String src) { - JsonNode dataJson = JacksonUtil.toJsonNode(msg.getData()); - List keysToDelete = JacksonUtil.convertValue(dataJson.get("attributes"), new TypeReference<>() { - }); + private void processAttributesDeleted(TbContext ctx, TbMsg msg) { + List keysToDelete = Optional.ofNullable( + JacksonUtil.convertValue(JacksonUtil.toJsonNode(msg.getData()).get("attributes"), new TypeReference>() { + }) + ).orElse(Collections.emptyList()); if (keysToDelete.isEmpty()) { ctx.tellSuccess(msg); From 7600401b867e0d4420173707eb7a32e7713170cb Mon Sep 17 00:00:00 2001 From: IrynaMatveieva Date: Thu, 6 Mar 2025 10:53:25 +0200 Subject: [PATCH 208/345] fixed error when try to push telemtry and cf fails --- .../server/service/cf/DefaultCalculatedFieldCache.java | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) 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 2282124bd5..3e5d09e953 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 @@ -117,7 +117,11 @@ public class DefaultCalculatedFieldCache implements CalculatedFieldCache { CalculatedField calculatedField = getCalculatedField(calculatedFieldId); if (calculatedField != null) { ctx = new CalculatedFieldCtx(calculatedField, tbelInvokeService, apiLimitService); - ctx.init(); + try { + ctx.init(); + } catch (Exception e) { + log.warn("Failed to initialize CF context."); + } calculatedFieldsCtx.put(calculatedFieldId, ctx); log.debug("[{}] Put calculated field ctx into cache: {}", calculatedFieldId, ctx); } From 59d26eb2e861168591cbf8e984479d79fb34ac5c Mon Sep 17 00:00:00 2001 From: Andrii Landiak Date: Thu, 6 Mar 2025 11:52:22 +0200 Subject: [PATCH 209/345] Improvements after review --- .../DefaultEntitiesExportImportService.java | 6 ---- .../impl/BaseEntityImportService.java | 2 +- .../sync/vc/data/EntitiesImportCtx.java | 7 ---- .../sync/ie/ExportImportServiceSqlTest.java | 2 +- .../service/sync/vc/VersionControlTest.java | 35 +++++++++++++++++++ 5 files changed, 37 insertions(+), 15 deletions(-) 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 d45f32c918..06fe7f4036 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 @@ -23,7 +23,6 @@ import org.thingsboard.server.cache.limits.RateLimitService; import org.thingsboard.server.common.data.EntityType; import org.thingsboard.server.common.data.ExportableEntity; import org.thingsboard.server.common.data.audit.ActionType; -import org.thingsboard.server.common.data.cf.CalculatedField; import org.thingsboard.server.common.data.exception.ThingsboardErrorCode; import org.thingsboard.server.common.data.exception.ThingsboardException; import org.thingsboard.server.common.data.id.EntityId; @@ -129,11 +128,6 @@ public class DefaultEntitiesExportImportService implements EntitiesExportImportS logEntityActionService.logEntityRelationAction(ctx.getTenantId(), null, relation, ctx.getUser(), ActionType.RELATION_ADD_OR_UPDATE, null, relation); } - - for (CalculatedField calculatedField : ctx.getCalculatedFields()) { - var savedCalculatedField = calculatedFieldService.save(calculatedField); - logEntityActionService.logEntityAction(ctx.getTenantId(), savedCalculatedField.getId(), savedCalculatedField, ActionType.ADDED, ctx.getUser()); - } } @Override diff --git a/application/src/main/java/org/thingsboard/server/service/sync/ie/importing/impl/BaseEntityImportService.java b/application/src/main/java/org/thingsboard/server/service/sync/ie/importing/impl/BaseEntityImportService.java index 65f2b4f032..bfa95af83c 100644 --- a/application/src/main/java/org/thingsboard/server/service/sync/ie/importing/impl/BaseEntityImportService.java +++ b/application/src/main/java/org/thingsboard/server/service/sync/ie/importing/impl/BaseEntityImportService.java @@ -295,7 +295,7 @@ public abstract class BaseEntityImportService { if (argument.getRefEntityId() != null) { - argument.setRefEntityId(idProvider.getInternalId(argument.getRefEntityId(), false)); + argument.setRefEntityId(idProvider.getInternalId(argument.getRefEntityId(), ctx.isFinalImportAttempt())); } }); }).toList(); diff --git a/application/src/main/java/org/thingsboard/server/service/sync/vc/data/EntitiesImportCtx.java b/application/src/main/java/org/thingsboard/server/service/sync/vc/data/EntitiesImportCtx.java index 726dd12102..e24683cd5a 100644 --- a/application/src/main/java/org/thingsboard/server/service/sync/vc/data/EntitiesImportCtx.java +++ b/application/src/main/java/org/thingsboard/server/service/sync/vc/data/EntitiesImportCtx.java @@ -19,7 +19,6 @@ import lombok.Data; import lombok.extern.slf4j.Slf4j; import org.thingsboard.server.common.data.EntityType; import org.thingsboard.server.common.data.User; -import org.thingsboard.server.common.data.cf.CalculatedField; import org.thingsboard.server.common.data.id.EntityId; import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.common.data.relation.EntityRelation; @@ -32,7 +31,6 @@ import java.util.ArrayList; import java.util.Collection; import java.util.HashMap; import java.util.HashSet; -import java.util.LinkedHashMap; import java.util.LinkedHashSet; import java.util.List; import java.util.Map; @@ -56,7 +54,6 @@ public class EntitiesImportCtx { private final Set notFoundIds = new HashSet<>(); private final Set relations = new LinkedHashSet<>(); - private final Set calculatedFields = new LinkedHashSet<>(); private boolean finalImportAttempt = false; private EntityImportSettings settings; @@ -127,10 +124,6 @@ public class EntitiesImportCtx { relations.addAll(values); } - public void addCalculatedFields(Collection values) { - calculatedFields.addAll(values); - } - public void addReferenceCallback(EntityId externalId, ThrowingRunnable tr) { if (tr != null) { referenceCallbacks.put(externalId, tr); diff --git a/application/src/test/java/org/thingsboard/server/service/sync/ie/ExportImportServiceSqlTest.java b/application/src/test/java/org/thingsboard/server/service/sync/ie/ExportImportServiceSqlTest.java index a75fbba095..da3b214afb 100644 --- a/application/src/test/java/org/thingsboard/server/service/sync/ie/ExportImportServiceSqlTest.java +++ b/application/src/test/java/org/thingsboard/server/service/sync/ie/ExportImportServiceSqlTest.java @@ -60,7 +60,6 @@ import org.thingsboard.server.common.data.device.profile.DeviceProfileData; import org.thingsboard.server.common.data.edge.EdgeEventActionType; import org.thingsboard.server.common.data.id.AssetId; import org.thingsboard.server.common.data.id.AssetProfileId; -import org.thingsboard.server.common.data.id.CalculatedFieldId; import org.thingsboard.server.common.data.id.CustomerId; import org.thingsboard.server.common.data.id.DashboardId; import org.thingsboard.server.common.data.id.DeviceId; @@ -288,6 +287,7 @@ public class ExportImportServiceSqlTest extends AbstractControllerTest { assertThat(calculatedFields.size()).isOne(); var importedCalculatedField = calculatedFields.get(0); assertThat(importedCalculatedField.getName()).isEqualTo(calculatedField.getName()); + verify(tbClusterService).onCalculatedFieldUpdated(eq(importedCalculatedField), isNull(), any()); EntityExportData updatedDeviceEntity = getAndClone(entitiesExportData, EntityType.DEVICE); updatedDeviceEntity.getEntity().setLabel("t" + updatedDeviceEntity.getEntity().getLabel()); 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 bb0d490ce0..06f61ca4e5 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 @@ -597,6 +597,41 @@ public class VersionControlTest extends AbstractControllerTest { }); } + @Test + public void testVcWithReferencedCalculatedFields_betweenTenants() throws Exception { + Asset asset = createAsset(null, null, "Asset 1"); + Device device = createDevice(null, null, "Device 1", "test1"); + CalculatedField deviceCalculatedField = createCalculatedField("CalculatedField1", device.getId(), asset.getId()); + CalculatedField assetCalculatedField = createCalculatedField("CalculatedField2", asset.getId(), device.getId()); + String versionId = createVersion("calculated fields of asset and device", EntityType.ASSET, EntityType.DEVICE, EntityType.DEVICE_PROFILE, EntityType.ASSET_PROFILE); + + loginTenant2(); + loadVersion(versionId, config -> { + config.setLoadCredentials(false); + }, EntityType.ASSET, EntityType.DEVICE, EntityType.DEVICE_PROFILE, EntityType.ASSET_PROFILE); + + Asset importedAsset = findAsset(asset.getName()); + Device importedDevice = findDevice(device.getName()); + checkImportedEntity(tenantId1, device, tenantId2, importedDevice); + checkImportedEntity(tenantId1, asset, tenantId2, importedAsset); + + List importedDeviceCalculatedFields = findCalculatedFieldsByEntityId(importedDevice.getId()); + assertThat(importedDeviceCalculatedFields).size().isOne(); + assertThat(importedDeviceCalculatedFields.get(0)).satisfies(importedField -> { + assertThat(importedField.getName()).isEqualTo(deviceCalculatedField.getName()); + assertThat(importedField.getType()).isEqualTo(deviceCalculatedField.getType()); + assertThat(importedField.getId()).isNotEqualTo(deviceCalculatedField.getId()); + }); + + List importedAssetCalculatedFields = findCalculatedFieldsByEntityId(importedAsset.getId()); + assertThat(importedAssetCalculatedFields).size().isOne(); + assertThat(importedAssetCalculatedFields.get(0)).satisfies(importedField -> { + assertThat(importedField.getName()).isEqualTo(assetCalculatedField.getName()); + assertThat(importedField.getType()).isEqualTo(assetCalculatedField.getType()); + assertThat(importedField.getId()).isNotEqualTo(assetCalculatedField.getId()); + }); + } + @Test public void testVcWithCalculatedFields_sameTenant() throws Exception { Asset asset = createAsset(null, null, "Asset 1"); From dca80e71735fe39be844e59b170bb13a535532e3 Mon Sep 17 00:00:00 2001 From: Andrii Landiak Date: Thu, 6 Mar 2025 12:14:09 +0200 Subject: [PATCH 210/345] Remove duplicated call --- .../service/sync/ie/importing/impl/DeviceImportService.java | 1 - 1 file changed, 1 deletion(-) diff --git a/application/src/main/java/org/thingsboard/server/service/sync/ie/importing/impl/DeviceImportService.java b/application/src/main/java/org/thingsboard/server/service/sync/ie/importing/impl/DeviceImportService.java index 92bdb1e294..84e264efdd 100644 --- a/application/src/main/java/org/thingsboard/server/service/sync/ie/importing/impl/DeviceImportService.java +++ b/application/src/main/java/org/thingsboard/server/service/sync/ie/importing/impl/DeviceImportService.java @@ -91,7 +91,6 @@ public class DeviceImportService extends BaseEntityImportService Date: Thu, 6 Mar 2025 12:21:57 +0200 Subject: [PATCH 211/345] Handeled deepClone of Observable --- ui-ngx/src/app/core/utils.ts | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/ui-ngx/src/app/core/utils.ts b/ui-ngx/src/app/core/utils.ts index 3096054a67..ca9c17dbb5 100644 --- a/ui-ngx/src/app/core/utils.ts +++ b/ui-ngx/src/app/core/utils.ts @@ -15,7 +15,7 @@ /// import _ from 'lodash'; -import { from, Observable, of, ReplaySubject, Subject } from 'rxjs'; +import { from, isObservable, Observable, of, ReplaySubject, Subject } from 'rxjs'; import { catchError, finalize, share } from 'rxjs/operators'; import { Datasource, DatasourceData, FormattedData, ReplaceInfo } from '@app/shared/models/widget.models'; import { EntityId } from '@shared/models/id/entity-id'; @@ -331,6 +331,10 @@ export function deepClone(target: T, ignoreFields?: string[]): T { if (target === null) { return target; } + // Observables can't be cloned using the spread operator, because they have non-enumerable methods (like .pipe). + if (isObservable(target)) { + return target; + } if (target instanceof Date) { return new Date(target.getTime()) as any; } From 894f6db5184e39e2350b89c3dadd7dabdd06f57e Mon Sep 17 00:00:00 2001 From: kalytka Date: Thu, 6 Mar 2025 13:46:59 +0200 Subject: [PATCH 212/345] Fix not closing pop-up after click insight dashboard zone --- .../modules/home/components/dashboard/dashboard.component.ts | 2 +- .../home/components/widget/widget-container.component.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/ui-ngx/src/app/modules/home/components/dashboard/dashboard.component.ts b/ui-ngx/src/app/modules/home/components/dashboard/dashboard.component.ts index d25c6b77cf..cc013fe34d 100644 --- a/ui-ngx/src/app/modules/home/components/dashboard/dashboard.component.ts +++ b/ui-ngx/src/app/modules/home/components/dashboard/dashboard.component.ts @@ -395,7 +395,7 @@ export class DashboardComponent extends PageComponent implements IDashboardCompo onDashboardMouseDown($event: MouseEvent) { if (this.callbacks && this.callbacks.onDashboardMouseDown) { - if ($event) { + if ($event && this.isEdit) { $event.stopPropagation(); } this.callbacks.onDashboardMouseDown($event); diff --git a/ui-ngx/src/app/modules/home/components/widget/widget-container.component.ts b/ui-ngx/src/app/modules/home/components/widget/widget-container.component.ts index 80afff855f..ba59570ff5 100644 --- a/ui-ngx/src/app/modules/home/components/widget/widget-container.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/widget-container.component.ts @@ -204,7 +204,7 @@ export class WidgetContainerComponent extends PageComponent implements OnInit, O } onMouseDown(event: MouseEvent) { - if (event) { + if (event && this.isEdit) { event.stopPropagation(); } this.widgetComponentAction.emit({ From fe16d104116fed36e89053140025a06ad4c149c1 Mon Sep 17 00:00:00 2001 From: Dmytro Skarzhynets Date: Thu, 6 Mar 2025 14:25:04 +0200 Subject: [PATCH 213/345] Save time series strategies: always handle inactivity timeout as a server attribute --- .../rpc/sync/DefaultEdgeRequestsService.java | 5 +- .../state/DefaultDeviceStateService.java | 77 ++++---- .../DefaultSubscriptionManagerService.java | 3 - .../DefaultTelemetrySubscriptionService.java | 32 +--- .../state/DefaultDeviceStateServiceTest.java | 40 ---- ...faultTelemetrySubscriptionServiceTest.java | 176 +++--------------- 6 files changed, 67 insertions(+), 266 deletions(-) diff --git a/application/src/main/java/org/thingsboard/server/service/edge/rpc/sync/DefaultEdgeRequestsService.java b/application/src/main/java/org/thingsboard/server/service/edge/rpc/sync/DefaultEdgeRequestsService.java index a371582409..6fc6d5bad9 100644 --- a/application/src/main/java/org/thingsboard/server/service/edge/rpc/sync/DefaultEdgeRequestsService.java +++ b/application/src/main/java/org/thingsboard/server/service/edge/rpc/sync/DefaultEdgeRequestsService.java @@ -152,8 +152,7 @@ public class DefaultEdgeRequestsService implements EdgeRequestsService { entityData = new HashMap<>(); attributes = JacksonUtil.newObjectNode(); for (AttributeKvEntry attr : ssAttributes) { - if (DefaultDeviceStateService.PERSISTENT_ATTRIBUTES.contains(attr.getKey()) - && !DefaultDeviceStateService.INACTIVITY_TIMEOUT.equals(attr.getKey())) { + if (DefaultDeviceStateService.ACTIVITY_KEYS_WITHOUT_INACTIVITY_TIMEOUT.contains(attr.getKey())) { continue; } if (attr.getDataType() == DataType.BOOLEAN && attr.getBooleanValue().isPresent()) { @@ -200,7 +199,7 @@ public class DefaultEdgeRequestsService implements EdgeRequestsService { } Map> tsData = new HashMap<>(); for (TsKvEntry tsKvEntry : tsKvEntries) { - if (DefaultDeviceStateService.PERSISTENT_ATTRIBUTES.contains(tsKvEntry.getKey())) { + if (DefaultDeviceStateService.ACTIVITY_KEYS_WITH_INACTIVITY_TIMEOUT.contains(tsKvEntry.getKey())) { continue; } tsData.computeIfAbsent(tsKvEntry.getTs(), k -> new HashMap<>()).put(tsKvEntry.getKey(), tsKvEntry.getValue()); diff --git a/application/src/main/java/org/thingsboard/server/service/state/DefaultDeviceStateService.java b/application/src/main/java/org/thingsboard/server/service/state/DefaultDeviceStateService.java index 11819ce5d8..f8cb1a1059 100644 --- a/application/src/main/java/org/thingsboard/server/service/state/DefaultDeviceStateService.java +++ b/application/src/main/java/org/thingsboard/server/service/state/DefaultDeviceStateService.java @@ -96,6 +96,7 @@ import java.util.HashSet; import java.util.List; import java.util.Map; import java.util.Objects; +import java.util.Optional; import java.util.Random; import java.util.Set; import java.util.UUID; @@ -129,11 +130,10 @@ public class DefaultDeviceStateService extends AbstractPartitionBasedService PERSISTENT_TELEMETRY_KEYS = Arrays.asList( new EntityKey(EntityKeyType.TIME_SERIES, LAST_ACTIVITY_TIME), new EntityKey(EntityKeyType.TIME_SERIES, INACTIVITY_ALARM_TIME), - new EntityKey(EntityKeyType.TIME_SERIES, INACTIVITY_TIMEOUT), new EntityKey(EntityKeyType.TIME_SERIES, ACTIVITY_STATE), new EntityKey(EntityKeyType.TIME_SERIES, LAST_CONNECT_TIME), new EntityKey(EntityKeyType.TIME_SERIES, LAST_DISCONNECT_TIME), - new EntityKey(EntityKeyType.SERVER_ATTRIBUTE, INACTIVITY_TIMEOUT)); + new EntityKey(EntityKeyType.SERVER_ATTRIBUTE, INACTIVITY_TIMEOUT)); // inactivity timeout is always a server attribute, even when activity data is stored as time series private static final List PERSISTENT_ATTRIBUTE_KEYS = Arrays.asList( new EntityKey(EntityKeyType.SERVER_ATTRIBUTE, LAST_ACTIVITY_TIME), @@ -143,8 +143,14 @@ public class DefaultDeviceStateService extends AbstractPartitionBasedService PERSISTENT_ATTRIBUTES = Arrays.asList(ACTIVITY_STATE, LAST_CONNECT_TIME, - LAST_DISCONNECT_TIME, LAST_ACTIVITY_TIME, INACTIVITY_ALARM_TIME, INACTIVITY_TIMEOUT); + public static final List ACTIVITY_KEYS_WITHOUT_INACTIVITY_TIMEOUT = List.of( + ACTIVITY_STATE, LAST_CONNECT_TIME, LAST_DISCONNECT_TIME, LAST_ACTIVITY_TIME, INACTIVITY_ALARM_TIME + ); + + public static final List ACTIVITY_KEYS_WITH_INACTIVITY_TIMEOUT = List.of( + ACTIVITY_STATE, LAST_CONNECT_TIME, LAST_DISCONNECT_TIME, LAST_ACTIVITY_TIME, INACTIVITY_ALARM_TIME, INACTIVITY_TIMEOUT + ); + private static final List PERSISTENT_ENTITY_FIELDS = Arrays.asList( new EntityKey(EntityKeyType.ENTITY_FIELD, "name"), new EntityKey(EntityKeyType.ENTITY_FIELD, "type"), @@ -643,41 +649,45 @@ public class DefaultDeviceStateService extends AbstractPartitionBasedService fetchDeviceState(Device device) { ListenableFuture future; if (persistToTelemetry) { - ListenableFuture> tsData = tsService.findLatest(TenantId.SYS_TENANT_ID, device.getId(), PERSISTENT_ATTRIBUTES); - future = Futures.transform(tsData, extractDeviceStateData(device), MoreExecutors.directExecutor()); + ListenableFuture> timeseriesActivityDataFuture = tsService.findLatest(TenantId.SYS_TENANT_ID, device.getId(), ACTIVITY_KEYS_WITHOUT_INACTIVITY_TIMEOUT); + ListenableFuture> inactivityTimeoutAttributeFuture = attributesService.find( + TenantId.SYS_TENANT_ID, device.getId(), AttributeScope.SERVER_SCOPE, INACTIVITY_TIMEOUT + ); + + ListenableFuture> fullActivityDataFuture = Futures.whenAllSucceed(timeseriesActivityDataFuture, inactivityTimeoutAttributeFuture).call(() -> { + List activityTimeseries = Futures.getDone(timeseriesActivityDataFuture); + Optional inactivityTimeoutAttribute = Futures.getDone(inactivityTimeoutAttributeFuture); + + List result; + if (inactivityTimeoutAttribute.isPresent()) { + result = new ArrayList<>(activityTimeseries.size() + 1); + result.addAll(activityTimeseries); + inactivityTimeoutAttribute.ifPresent(result::add); + } else { + return activityTimeseries; + } + + return result; + }, deviceStateCallbackExecutor); + + future = Futures.transform(fullActivityDataFuture, extractDeviceStateData(device), MoreExecutors.directExecutor()); } else { - ListenableFuture> attrData = attributesService.find(TenantId.SYS_TENANT_ID, device.getId(), AttributeScope.SERVER_SCOPE, PERSISTENT_ATTRIBUTES); - future = Futures.transform(attrData, extractDeviceStateData(device), MoreExecutors.directExecutor()); + ListenableFuture> attributesActivityDataFuture = attributesService.find( + TenantId.SYS_TENANT_ID, device.getId(), AttributeScope.SERVER_SCOPE, ACTIVITY_KEYS_WITH_INACTIVITY_TIMEOUT + ); + future = Futures.transform(attributesActivityDataFuture, extractDeviceStateData(device), MoreExecutors.directExecutor()); } - return transformInactivityTimeout(future); + return future; } - private ListenableFuture transformInactivityTimeout(ListenableFuture future) { - return Futures.transformAsync(future, deviceStateData -> { - if (!persistToTelemetry || deviceStateData.getState().getInactivityTimeout() != defaultInactivityTimeoutMs) { - return future; //fail fast - } - var attributesFuture = attributesService.find(TenantId.SYS_TENANT_ID, deviceStateData.getDeviceId(), AttributeScope.SERVER_SCOPE, INACTIVITY_TIMEOUT); - return Futures.transform(attributesFuture, attributes -> { - attributes.flatMap(KvEntry::getLongValue).ifPresent((inactivityTimeout) -> { - if (inactivityTimeout > 0) { - deviceStateData.getState().setInactivityTimeout(inactivityTimeout); - } - }); - return deviceStateData; - }, MoreExecutors.directExecutor()); - }, deviceStateCallbackExecutor); - } - - private Function, DeviceStateData> extractDeviceStateData(Device device) { + private Function, DeviceStateData> extractDeviceStateData(Device device) { return new Function<>() { @Nonnull @Override - public DeviceStateData apply(@Nullable List data) { + public DeviceStateData apply(@Nullable List data) { try { long lastActivityTime = getEntryValue(data, LAST_ACTIVITY_TIME, 0L); long inactivityAlarmTime = getEntryValue(data, INACTIVITY_ALARM_TIME, 0L); @@ -690,7 +700,7 @@ public class DefaultDeviceStateService extends AbstractPartitionBasedService 0 ? inactivityTimeout : defaultInactivityTimeoutMs) .build(); TbMsgMetaData md = new TbMsgMetaData(); md.putValue("deviceName", device.getName()); @@ -761,12 +771,7 @@ public class DefaultDeviceStateService extends AbstractPartitionBasedService keys, TbCallback callback) { onTimeSeriesUpdate(entityId, keys.stream().map(key -> new BasicTsKvEntry(0, new StringDataEntry(key, ""))).collect(Collectors.toList())); - if (entityId.getEntityType() == EntityType.DEVICE) { - deleteDeviceInactivityTimeout(tenantId, entityId, keys); - } callback.onSuccess(); } diff --git a/application/src/main/java/org/thingsboard/server/service/telemetry/DefaultTelemetrySubscriptionService.java b/application/src/main/java/org/thingsboard/server/service/telemetry/DefaultTelemetrySubscriptionService.java index d0507d4fee..36cd5516a8 100644 --- a/application/src/main/java/org/thingsboard/server/service/telemetry/DefaultTelemetrySubscriptionService.java +++ b/application/src/main/java/org/thingsboard/server/service/telemetry/DefaultTelemetrySubscriptionService.java @@ -31,7 +31,6 @@ import org.thingsboard.common.util.DonAsynchron; import org.thingsboard.common.util.ThingsBoardThreadFactory; import org.thingsboard.rule.engine.api.AttributesDeleteRequest; import org.thingsboard.rule.engine.api.AttributesSaveRequest; -import org.thingsboard.rule.engine.api.DeviceStateManager; import org.thingsboard.rule.engine.api.RuleEngineTelemetryService; import org.thingsboard.rule.engine.api.TimeseriesDeleteRequest; import org.thingsboard.rule.engine.api.TimeseriesSaveRequest; @@ -39,11 +38,9 @@ import org.thingsboard.server.common.data.ApiUsageRecordKey; import org.thingsboard.server.common.data.EntityType; import org.thingsboard.server.common.data.EntityView; import org.thingsboard.server.common.data.id.CustomerId; -import org.thingsboard.server.common.data.id.DeviceId; import org.thingsboard.server.common.data.id.EntityId; import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.common.data.kv.AttributeKvEntry; -import org.thingsboard.server.common.data.kv.KvEntry; import org.thingsboard.server.common.data.kv.TimeseriesSaveResult; import org.thingsboard.server.common.data.kv.TsKvEntry; import org.thingsboard.server.common.data.kv.TsKvLatestRemovingResult; @@ -55,7 +52,6 @@ import org.thingsboard.server.dao.util.KvUtils; import org.thingsboard.server.service.apiusage.TbApiUsageStateService; import org.thingsboard.server.service.cf.CalculatedFieldQueueService; import org.thingsboard.server.service.entitiy.entityview.TbEntityViewService; -import org.thingsboard.server.service.state.DefaultDeviceStateService; import org.thingsboard.server.service.subscription.TbSubscriptionUtils; import java.util.ArrayList; @@ -82,7 +78,6 @@ public class DefaultTelemetrySubscriptionService extends AbstractSubscriptionSer private final TbApiUsageReportClient apiUsageClient; private final TbApiUsageStateService apiUsageStateService; private final CalculatedFieldQueueService calculatedFieldQueueService; - private final DeviceStateManager deviceStateManager; private ExecutorService tsCallBackExecutor; @@ -94,15 +89,13 @@ public class DefaultTelemetrySubscriptionService extends AbstractSubscriptionSer @Lazy TbEntityViewService tbEntityViewService, TbApiUsageReportClient apiUsageClient, TbApiUsageStateService apiUsageStateService, - CalculatedFieldQueueService calculatedFieldQueueService, - DeviceStateManager deviceStateManager) { + CalculatedFieldQueueService calculatedFieldQueueService) { this.attrService = attrService; this.tsService = tsService; this.tbEntityViewService = tbEntityViewService; this.apiUsageClient = apiUsageClient; this.apiUsageStateService = apiUsageStateService; this.calculatedFieldQueueService = calculatedFieldQueueService; - this.deviceStateManager = deviceStateManager; } @PostConstruct @@ -165,14 +158,6 @@ public class DefaultTelemetrySubscriptionService extends AbstractSubscriptionSer } }, t -> request.getCallback().onFailure(t)); - if (entityId.getEntityType() == EntityType.DEVICE && request.getStrategy().saveLatest() /* Device State Service reads from the latest values when initializing */) { - findNewInactivityTimeout(request.getEntries()).ifPresent(newInactivityTimeout -> - addMainCallback(resultFuture, __ -> deviceStateManager.onDeviceInactivityTimeoutUpdate( - tenantId, new DeviceId(entityId.getId()), newInactivityTimeout, TbCallback.EMPTY) - ) - ); - } - if (strategy.sendWsUpdate()) { addWsCallback(resultFuture, success -> onTimeSeriesUpdate(tenantId, entityId, request.getEntries())); } @@ -182,21 +167,6 @@ public class DefaultTelemetrySubscriptionService extends AbstractSubscriptionSer return resultFuture; } - private static Optional findNewInactivityTimeout(List entries) { - return entries.stream() - .filter(entry -> Objects.equals(DefaultDeviceStateService.INACTIVITY_TIMEOUT, entry.getKey())) - .findFirst() - .map(DefaultTelemetrySubscriptionService::parseAsLong); - } - - private static long parseAsLong(KvEntry kve) { - try { - return Long.parseLong(kve.getValueAsString()); - } catch (NumberFormatException e) { - return 0L; - } - } - @Override public void saveAttributes(AttributesSaveRequest request) { checkInternalEntity(request.getEntityId()); diff --git a/application/src/test/java/org/thingsboard/server/service/state/DefaultDeviceStateServiceTest.java b/application/src/test/java/org/thingsboard/server/service/state/DefaultDeviceStateServiceTest.java index 26c978bbd0..4e36a44957 100644 --- a/application/src/test/java/org/thingsboard/server/service/state/DefaultDeviceStateServiceTest.java +++ b/application/src/test/java/org/thingsboard/server/service/state/DefaultDeviceStateServiceTest.java @@ -38,9 +38,6 @@ import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.common.data.msg.TbMsgType; import org.thingsboard.server.common.data.notification.rule.trigger.DeviceActivityTrigger; import org.thingsboard.server.common.data.page.PageData; -import org.thingsboard.server.common.data.query.EntityData; -import org.thingsboard.server.common.data.query.EntityKeyType; -import org.thingsboard.server.common.data.query.TsValue; import org.thingsboard.server.common.msg.TbMsg; import org.thingsboard.server.common.msg.TbMsgMetaData; import org.thingsboard.server.common.msg.notification.NotificationRuleProcessor; @@ -88,7 +85,6 @@ import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; import static org.thingsboard.server.service.state.DefaultDeviceStateService.ACTIVITY_STATE; import static org.thingsboard.server.service.state.DefaultDeviceStateService.INACTIVITY_ALARM_TIME; -import static org.thingsboard.server.service.state.DefaultDeviceStateService.INACTIVITY_TIMEOUT; import static org.thingsboard.server.service.state.DefaultDeviceStateService.LAST_ACTIVITY_TIME; import static org.thingsboard.server.service.state.DefaultDeviceStateService.LAST_CONNECT_TIME; import static org.thingsboard.server.service.state.DefaultDeviceStateService.LAST_DISCONNECT_TIME; @@ -508,42 +504,6 @@ public class DefaultDeviceStateServiceTest { verify(service).fetchDeviceStateDataUsingSeparateRequests(deviceId); } - @Test - public void givenPersistToTelemetryAndDefaultInactivityTimeoutFetched_whenTransformingToDeviceStateData_thenTryGetInactivityFromAttribute() { - var defaultInactivityTimeoutInSec = 60L; - var latest = - Map.of( - EntityKeyType.TIME_SERIES, Map.of(INACTIVITY_TIMEOUT, new TsValue(0, Long.toString(defaultInactivityTimeoutInSec * 1000))), - EntityKeyType.SERVER_ATTRIBUTE, Map.of(INACTIVITY_TIMEOUT, new TsValue(0, Long.toString(5000L))) - ); - - process(latest, defaultInactivityTimeoutInSec); - } - - @Test - public void givenPersistToTelemetryAndNoInactivityTimeoutFetchedFromTimeSeries_whenTransformingToDeviceStateData_thenTryGetInactivityFromAttribute() { - var defaultInactivityTimeoutInSec = 60L; - var latest = - Map.of( - EntityKeyType.SERVER_ATTRIBUTE, Map.of(INACTIVITY_TIMEOUT, new TsValue(0, Long.toString(5000L))) - ); - - process(latest, defaultInactivityTimeoutInSec); - } - - private void process(Map> latest, long defaultInactivityTimeoutInSec) { - service.setDefaultInactivityTimeoutInSec(defaultInactivityTimeoutInSec); - service.setDefaultInactivityTimeoutMs(defaultInactivityTimeoutInSec * 1000); - service.setPersistToTelemetry(true); - - var deviceUuid = UUID.randomUUID(); - var deviceId = new DeviceId(deviceUuid); - - DeviceStateData deviceStateData = service.toDeviceStateData(new EntityData(deviceId, latest, Map.of()), new DeviceIdInfo(TenantId.SYS_TENANT_ID.getId(), UUID.randomUUID(), deviceUuid)); - - assertThat(deviceStateData.getState().getInactivityTimeout()).isEqualTo(5000L); - } - private void initStateService(long timeout) throws InterruptedException { service.stop(); reset(service, telemetrySubscriptionService); diff --git a/application/src/test/java/org/thingsboard/server/service/telemetry/DefaultTelemetrySubscriptionServiceTest.java b/application/src/test/java/org/thingsboard/server/service/telemetry/DefaultTelemetrySubscriptionServiceTest.java index b1c84184d2..6416fb4f98 100644 --- a/application/src/test/java/org/thingsboard/server/service/telemetry/DefaultTelemetrySubscriptionServiceTest.java +++ b/application/src/test/java/org/thingsboard/server/service/telemetry/DefaultTelemetrySubscriptionServiceTest.java @@ -24,30 +24,25 @@ import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; import org.junit.jupiter.params.ParameterizedTest; import org.junit.jupiter.params.provider.Arguments; -import org.junit.jupiter.params.provider.EnumSource; import org.junit.jupiter.params.provider.MethodSource; import org.mockito.Mock; import org.mockito.junit.jupiter.MockitoExtension; import org.springframework.test.util.ReflectionTestUtils; -import org.thingsboard.rule.engine.api.DeviceStateManager; import org.thingsboard.rule.engine.api.TimeseriesSaveRequest; import org.thingsboard.server.cluster.TbClusterService; import org.thingsboard.server.common.data.ApiUsageRecordKey; import org.thingsboard.server.common.data.ApiUsageState; import org.thingsboard.server.common.data.ApiUsageStateValue; -import org.thingsboard.server.common.data.EntityType; import org.thingsboard.server.common.data.EntityView; import org.thingsboard.server.common.data.id.ApiUsageStateId; import org.thingsboard.server.common.data.id.CustomerId; import org.thingsboard.server.common.data.id.DeviceId; import org.thingsboard.server.common.data.id.EntityId; -import org.thingsboard.server.common.data.id.EntityIdFactory; import org.thingsboard.server.common.data.id.EntityViewId; import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.common.data.kv.BasicTsKvEntry; import org.thingsboard.server.common.data.kv.DoubleDataEntry; import org.thingsboard.server.common.data.kv.KvEntry; -import org.thingsboard.server.common.data.kv.LongDataEntry; import org.thingsboard.server.common.data.kv.TimeseriesSaveResult; import org.thingsboard.server.common.data.kv.TsKvEntry; import org.thingsboard.server.common.data.objects.AttributesEntityView; @@ -78,17 +73,14 @@ import java.util.concurrent.ExecutorService; import java.util.stream.LongStream; import java.util.stream.Stream; -import static com.google.common.util.concurrent.Futures.immediateFailedFuture; import static com.google.common.util.concurrent.Futures.immediateFuture; import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatThrownBy; import static org.mockito.ArgumentMatchers.any; -import static org.mockito.ArgumentMatchers.anyLong; import static org.mockito.ArgumentMatchers.eq; import static org.mockito.BDDMockito.given; import static org.mockito.BDDMockito.then; import static org.mockito.Mockito.lenient; -import static org.mockito.Mockito.never; @ExtendWith(MockitoExtension.class) class DefaultTelemetrySubscriptionServiceTest { @@ -132,14 +124,12 @@ class DefaultTelemetrySubscriptionServiceTest { TbApiUsageStateService apiUsageStateService; @Mock CalculatedFieldQueueService calculatedFieldQueueService; - @Mock - DeviceStateManager deviceStateManager; DefaultTelemetrySubscriptionService telemetryService; @BeforeEach void setup() { - telemetryService = new DefaultTelemetrySubscriptionService(attrService, tsService, tbEntityViewService, apiUsageClient, apiUsageStateService, calculatedFieldQueueService, deviceStateManager); + telemetryService = new DefaultTelemetrySubscriptionService(attrService, tsService, tbEntityViewService, apiUsageClient, apiUsageStateService, calculatedFieldQueueService); ReflectionTestUtils.setField(telemetryService, "clusterService", clusterService); ReflectionTestUtils.setField(telemetryService, "partitionService", partitionService); ReflectionTestUtils.setField(telemetryService, "subscriptionManagerService", Optional.of(subscriptionManagerService)); @@ -180,6 +170,28 @@ class DefaultTelemetrySubscriptionServiceTest { tsCallBackExecutor.shutdownNow(); } + /* --- Save time series API --- */ + + @Test + void shouldThrowErrorWhenTryingToSaveTimeseriesForApiUsageState() { + // GIVEN + var request = TimeseriesSaveRequest.builder() + .tenantId(tenantId) + .customerId(customerId) + .entityId(new ApiUsageStateId(UUID.randomUUID())) + .entries(sampleTelemetry) + .strategy(TimeseriesSaveRequest.Strategy.PROCESS_ALL) + .build(); + + // WHEN + assertThatThrownBy(() -> telemetryService.saveTimeseries(request)) + .isInstanceOf(RuntimeException.class) + .hasMessage("Can't update API Usage State!"); + + // THEN + then(tsService).shouldHaveNoInteractions(); + } + @Test void shouldReportStorageDataPointsApiUsageWhenTimeSeriesIsSaved() { // GIVEN @@ -389,148 +401,6 @@ class DefaultTelemetrySubscriptionServiceTest { ); } - @Test - void shouldThrowErrorWhenTryingToSaveTimeseriesForApiUsageState() { - // GIVEN - var request = TimeseriesSaveRequest.builder() - .tenantId(tenantId) - .customerId(customerId) - .entityId(new ApiUsageStateId(UUID.randomUUID())) - .entries(sampleTelemetry) - .strategy(TimeseriesSaveRequest.Strategy.PROCESS_ALL) - .build(); - - // WHEN - assertThatThrownBy(() -> telemetryService.saveTimeseries(request)) - .isInstanceOf(RuntimeException.class) - .hasMessage("Can't update API Usage State!"); - - // THEN - then(tsService).shouldHaveNoInteractions(); - then(deviceStateManager).shouldHaveNoInteractions(); - } - - @Test - void shouldNotifyDeviceStateManagerWhenDeviceInactivityTimeoutTimeseriesWasSavedToLatest() { - // GIVEN - var deviceId = DeviceId.fromString("cc51e450-53e1-11ee-883e-e56b48fd2088"); - var inactivityTimeout = new BasicTsKvEntry(123L, new LongDataEntry("inactivityTimeout", 5000L)); - - var request = TimeseriesSaveRequest.builder() - .tenantId(tenantId) - .customerId(customerId) - .entityId(deviceId) - .entry(inactivityTimeout) - .strategy(new TimeseriesSaveRequest.Strategy(false, true, false, false)) - .build(); - - given(tsService.saveLatest(tenantId, deviceId, List.of(inactivityTimeout))).willReturn(immediateFuture(TimeseriesSaveResult.of(1, listOfNNumbers(1)))); - - // WHEN - telemetryService.saveTimeseries(request); - - // THEN - then(deviceStateManager).should().onDeviceInactivityTimeoutUpdate(tenantId, deviceId, 5000L, TbCallback.EMPTY); - } - - @ParameterizedTest - @EnumSource( - value = EntityType.class, - names = {"DEVICE", "API_USAGE_STATE"}, // API usage state excluded due to coverage in another test - mode = EnumSource.Mode.EXCLUDE - ) - void shouldNotNotifyDeviceStateManagerWhenInactivityTimeoutTimeseriesWasUpdatedButEntityTypeIsNotDevice(EntityType entityType) { - // GIVEN - var nonDeviceId = EntityIdFactory.getByTypeAndUuid(entityType, "cc51e450-53e1-11ee-883e-e56b48fd2088"); - var inactivityTimeout = new BasicTsKvEntry(123L, new LongDataEntry("inactivityTimeout", 5000L)); - - var request = TimeseriesSaveRequest.builder() - .tenantId(tenantId) - .customerId(customerId) - .entityId(nonDeviceId) - .entry(inactivityTimeout) - .strategy(new TimeseriesSaveRequest.Strategy(false, true, false, false)) - .build(); - - given(tsService.saveLatest(tenantId, nonDeviceId, List.of(inactivityTimeout))).willReturn(immediateFuture(TimeseriesSaveResult.of(1, listOfNNumbers(1)))); - lenient().when(tbEntityViewService.findEntityViewsByTenantIdAndEntityIdAsync(tenantId, nonDeviceId)).thenReturn(immediateFuture(Collections.emptyList())); - - // WHEN - telemetryService.saveTimeseries(request); - - // THEN - then(deviceStateManager).should(never()).onDeviceInactivityTimeoutUpdate(any(), any(), anyLong(), any()); - } - - @Test - void shouldNotNotifyDeviceStateManagerWhenDeviceInactivityTimeoutTimeseriesWasNotSavedToLatest() { - // GIVEN - var deviceId = DeviceId.fromString("cc51e450-53e1-11ee-883e-e56b48fd2088"); - var inactivityTimeout = new BasicTsKvEntry(123L, new LongDataEntry("inactivityTimeout", 5000L)); - - var request = TimeseriesSaveRequest.builder() - .tenantId(tenantId) - .customerId(customerId) - .entityId(deviceId) - .entry(inactivityTimeout) - .strategy(new TimeseriesSaveRequest.Strategy(true, false, true, true)) - .build(); - - given(tsService.saveWithoutLatest(tenantId, deviceId, List.of(inactivityTimeout), 0L)).willReturn(immediateFuture(TimeseriesSaveResult.of(1, null))); - - // WHEN - telemetryService.saveTimeseries(request); - - // THEN - then(deviceStateManager).should(never()).onDeviceInactivityTimeoutUpdate(any(), any(), anyLong(), any()); - } - - @Test - void shouldNotNotifyDeviceStateManagerWhenInactivityTimeoutTimeseriesWasNotUpdated() { - // GIVEN - var deviceId = DeviceId.fromString("cc51e450-53e1-11ee-883e-e56b48fd2088"); - var notInactivityTimeout = new BasicTsKvEntry(123L, new LongDataEntry("notInactivityTimeout", 5000L)); - - var request = TimeseriesSaveRequest.builder() - .tenantId(tenantId) - .customerId(customerId) - .entityId(deviceId) - .entry(notInactivityTimeout) - .strategy(new TimeseriesSaveRequest.Strategy(false, true, false, false)) - .build(); - - given(tsService.saveLatest(tenantId, deviceId, List.of(notInactivityTimeout))).willReturn(immediateFuture(TimeseriesSaveResult.of(1, listOfNNumbers(1)))); - - // WHEN - telemetryService.saveTimeseries(request); - - // THEN - then(deviceStateManager).should(never()).onDeviceInactivityTimeoutUpdate(any(), any(), anyLong(), any()); - } - - @Test - void shouldNotNotifyDeviceStateManagerWhenDeviceInactivityTimeoutTimeseriesSaveFailed() { - // GIVEN - var deviceId = DeviceId.fromString("cc51e450-53e1-11ee-883e-e56b48fd2088"); - var inactivityTimeout = new BasicTsKvEntry(123L, new LongDataEntry("inactivityTimeout", 5000L)); - - var request = TimeseriesSaveRequest.builder() - .tenantId(tenantId) - .customerId(customerId) - .entityId(deviceId) - .entry(inactivityTimeout) - .strategy(new TimeseriesSaveRequest.Strategy(false, true, false, false)) - .build(); - - given(tsService.saveLatest(tenantId, deviceId, List.of(inactivityTimeout))).willReturn(immediateFailedFuture(new RuntimeException("failed to save"))); - - // WHEN - telemetryService.saveTimeseries(request); - - // THEN - then(deviceStateManager).should(never()).onDeviceInactivityTimeoutUpdate(any(), any(), anyLong(), any()); - } - // used to emulate sequence numbers returned by save latest API private static List listOfNNumbers(int N) { return LongStream.range(0, N).boxed().toList(); From e6428a8e78bb0c043eb3cc8a569ee855ef411b71 Mon Sep 17 00:00:00 2001 From: dashevchenko Date: Thu, 6 Mar 2025 15:21:37 +0200 Subject: [PATCH 214/345] increased timeout for edqs tests --- .../server/service/entitiy/EdqsEntityServiceTest.java | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/application/src/test/java/org/thingsboard/server/service/entitiy/EdqsEntityServiceTest.java b/application/src/test/java/org/thingsboard/server/service/entitiy/EdqsEntityServiceTest.java index 4d444ad637..50c80d08c7 100644 --- a/application/src/test/java/org/thingsboard/server/service/entitiy/EdqsEntityServiceTest.java +++ b/application/src/test/java/org/thingsboard/server/service/entitiy/EdqsEntityServiceTest.java @@ -104,13 +104,13 @@ public class EdqsEntityServiceTest extends EntityServiceTest { @Override protected PageData findByQueryAndCheck(CustomerId customerId, EntityDataQuery query, long expectedResultSize) { - return await().atMost(15, TimeUnit.SECONDS).until(() -> findByQuery(customerId, query), + return await().atMost(TIMEOUT, TimeUnit.SECONDS).until(() -> findByQuery(customerId, query), result -> result.getTotalElements() == expectedResultSize); } @Override protected List findByQueryAndCheckTelemetry(EntityDataQuery query, EntityKeyType entityKeyType, String key, List expectedTelemetries) { - return await().atMost(15, TimeUnit.SECONDS).until(() -> findEntitiesTelemetry(query, entityKeyType, key, expectedTelemetries), + return await().atMost(TIMEOUT, TimeUnit.SECONDS).until(() -> findEntitiesTelemetry(query, entityKeyType, key, expectedTelemetries), loadedEntities -> loadedEntities.stream().map(entityData -> entityData.getLatest().get(entityKeyType).get(key).getValue()).toList().containsAll(expectedTelemetries)); } @@ -121,7 +121,7 @@ public class EdqsEntityServiceTest extends EntityServiceTest { @Override protected long countByQueryAndCheck(CustomerId customerId, EntityCountQuery query, int expectedResult) { - return await().atMost(15, TimeUnit.SECONDS).until(() -> countByQuery(customerId, query), + return await().atMost(TIMEOUT, TimeUnit.SECONDS).until(() -> countByQuery(customerId, query), result -> result == expectedResult); } From 5028b7b551d7113d895f977ed45e25b22f60d864 Mon Sep 17 00:00:00 2001 From: dashevchenko Date: Thu, 6 Mar 2025 15:43:23 +0200 Subject: [PATCH 215/345] added Lazy for edqsApiService --- .../org/thingsboard/server/dao/entity/BaseEntityService.java | 1 + 1 file changed, 1 insertion(+) diff --git a/dao/src/main/java/org/thingsboard/server/dao/entity/BaseEntityService.java b/dao/src/main/java/org/thingsboard/server/dao/entity/BaseEntityService.java index 6e32901517..a762aabf38 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/entity/BaseEntityService.java +++ b/dao/src/main/java/org/thingsboard/server/dao/entity/BaseEntityService.java @@ -86,6 +86,7 @@ public class BaseEntityService extends AbstractEntityService implements EntitySe EntityServiceRegistry entityServiceRegistry; @Autowired + @Lazy private EdqsApiService edqsApiService; @Override From 93090af2ee7e8406aab7cde134a66e4ced51214c Mon Sep 17 00:00:00 2001 From: IrynaMatveieva Date: Thu, 6 Mar 2025 16:35:54 +0200 Subject: [PATCH 216/345] updated check for single value argument size --- .../CalculatedFieldEntityMessageProcessor.java | 3 ++- .../CalculatedFieldManagerMessageProcessor.java | 3 --- .../server/service/cf/DefaultCalculatedFieldCache.java | 5 ----- .../service/cf/ctx/state/BaseCalculatedFieldState.java | 6 ++++-- 4 files changed, 6 insertions(+), 11 deletions(-) diff --git a/application/src/main/java/org/thingsboard/server/actors/calculatedField/CalculatedFieldEntityMessageProcessor.java b/application/src/main/java/org/thingsboard/server/actors/calculatedField/CalculatedFieldEntityMessageProcessor.java index c75b47c8a0..3911714ae0 100644 --- a/application/src/main/java/org/thingsboard/server/actors/calculatedField/CalculatedFieldEntityMessageProcessor.java +++ b/application/src/main/java/org/thingsboard/server/actors/calculatedField/CalculatedFieldEntityMessageProcessor.java @@ -301,7 +301,7 @@ public class CalculatedFieldEntityMessageProcessor extends AbstractContextAwareM } } - private void removeStateAndRaiseSizeException(CalculatedFieldEntityCtxId ctxId, CalculatedFieldException ex, TbCallback callback) { + private void removeStateAndRaiseSizeException(CalculatedFieldEntityCtxId ctxId, CalculatedFieldException ex, TbCallback callback) throws CalculatedFieldException { // We remove the state, but remember that it is over-sized in a local map. cfStateService.removeState(ctxId, new TbCallback() { @Override @@ -314,6 +314,7 @@ public class CalculatedFieldEntityMessageProcessor extends AbstractContextAwareM callback.onFailure(ex); } }); + throw ex; } private Map mapToArguments(CalculatedFieldCtx ctx, List data) { diff --git a/application/src/main/java/org/thingsboard/server/actors/calculatedField/CalculatedFieldManagerMessageProcessor.java b/application/src/main/java/org/thingsboard/server/actors/calculatedField/CalculatedFieldManagerMessageProcessor.java index d26d89626e..e7c963a4a7 100644 --- a/application/src/main/java/org/thingsboard/server/actors/calculatedField/CalculatedFieldManagerMessageProcessor.java +++ b/application/src/main/java/org/thingsboard/server/actors/calculatedField/CalculatedFieldManagerMessageProcessor.java @@ -16,7 +16,6 @@ package org.thingsboard.server.actors.calculatedField; import lombok.extern.slf4j.Slf4j; -import org.thingsboard.common.util.DebugModeUtil; import org.thingsboard.server.actors.ActorSystemContext; import org.thingsboard.server.actors.TbActorCtx; import org.thingsboard.server.actors.TbActorRef; @@ -51,8 +50,6 @@ import java.util.Collections; import java.util.HashMap; import java.util.List; import java.util.Map; -import java.util.concurrent.ConcurrentHashMap; -import java.util.concurrent.ConcurrentMap; import java.util.concurrent.CopyOnWriteArrayList; import static org.thingsboard.server.utils.CalculatedFieldUtils.fromProto; 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 3e5d09e953..64487d9b3e 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 @@ -117,11 +117,6 @@ public class DefaultCalculatedFieldCache implements CalculatedFieldCache { CalculatedField calculatedField = getCalculatedField(calculatedFieldId); if (calculatedField != null) { ctx = new CalculatedFieldCtx(calculatedField, tbelInvokeService, apiLimitService); - try { - ctx.init(); - } catch (Exception e) { - log.warn("Failed to initialize CF context."); - } calculatedFieldsCtx.put(calculatedFieldId, ctx); log.debug("[{}] Put calculated field ctx into cache: {}", calculatedFieldId, ctx); } diff --git a/application/src/main/java/org/thingsboard/server/service/cf/ctx/state/BaseCalculatedFieldState.java b/application/src/main/java/org/thingsboard/server/service/cf/ctx/state/BaseCalculatedFieldState.java index b2a4321961..624198e579 100644 --- a/application/src/main/java/org/thingsboard/server/service/cf/ctx/state/BaseCalculatedFieldState.java +++ b/application/src/main/java/org/thingsboard/server/service/cf/ctx/state/BaseCalculatedFieldState.java @@ -91,8 +91,10 @@ public abstract class BaseCalculatedFieldState implements CalculatedFieldState { if (entry instanceof TsRollingArgumentEntry) { return; } - if (ctx.getMaxSingleValueArgumentSize() > 0 && toSingleValueArgumentProto(name, (SingleValueArgumentEntry) entry).getSerializedSize() > ctx.getMaxSingleValueArgumentSize()) { - throw new IllegalArgumentException("Single value size exceeds the maximum allowed limit. The argument will not be used for calculation."); + if (entry instanceof SingleValueArgumentEntry singleValueArgumentEntry) { + if (ctx.getMaxSingleValueArgumentSize() > 0 && toSingleValueArgumentProto(name, singleValueArgumentEntry).getSerializedSize() > ctx.getMaxSingleValueArgumentSize()) { + throw new IllegalArgumentException("Single value size exceeds the maximum allowed limit. The argument will not be used for calculation."); + } } } From e59de20b35be540d0647431659557028bdeb5b73 Mon Sep 17 00:00:00 2001 From: Andrii Landiak <50847617+AndriiLandiak@users.noreply.github.com> Date: Thu, 6 Mar 2025 16:41:20 +0200 Subject: [PATCH 217/345] Fix TenantControllerTest (#12839) * Increase timeout for TenantControllerTest to 60 seconds to prevent failing on waiting for tenant deletion * Modify deleteTenant in AbstractWebTest --- .../java/org/thingsboard/server/controller/AbstractWebTest.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/application/src/test/java/org/thingsboard/server/controller/AbstractWebTest.java b/application/src/test/java/org/thingsboard/server/controller/AbstractWebTest.java index 975c3b426b..e90aecb41e 100644 --- a/application/src/test/java/org/thingsboard/server/controller/AbstractWebTest.java +++ b/application/src/test/java/org/thingsboard/server/controller/AbstractWebTest.java @@ -402,7 +402,7 @@ public abstract class AbstractWebTest extends AbstractInMemoryStorageTest { } catch (Exception e) { throw new RuntimeException(e); } - Awaitility.await("all tasks processed").atMost(60, TimeUnit.SECONDS).during(300, TimeUnit.MILLISECONDS) + Awaitility.await("all tasks processed").atMost(90, TimeUnit.SECONDS).during(300, TimeUnit.MILLISECONDS) .until(() -> storage.getLag("tb_housekeeper") == 0); } From 62e6c0948ca84eb177b9aae952b04a1e9e64346c Mon Sep 17 00:00:00 2001 From: IrynaMatveieva Date: Thu, 6 Mar 2025 16:59:12 +0200 Subject: [PATCH 218/345] removed handling for attr/ts deletion in the rule node --- .../server/actors/ActorSystemContext.java | 2 +- .../actors/ruleChain/DefaultTbContext.java | 4 +- .../cf}/CalculatedFieldQueueService.java | 13 +-- .../DefaultCalculatedFieldQueueService.java | 1 - .../DefaultTelemetrySubscriptionService.java | 2 +- ...faultTelemetrySubscriptionServiceTest.java | 2 +- ...RuleEngineCalculatedFieldQueueService.java | 26 +++++ .../rule/engine/api/TbContext.java | 2 +- .../telemetry/TbCalculatedFieldsNode.java | 96 ++----------------- 9 files changed, 47 insertions(+), 101 deletions(-) rename {rule-engine/rule-engine-api/src/main/java/org/thingsboard/rule/engine/api => application/src/main/java/org/thingsboard/server/service/cf}/CalculatedFieldQueueService.java (75%) create mode 100644 rule-engine/rule-engine-api/src/main/java/org/thingsboard/rule/engine/api/RuleEngineCalculatedFieldQueueService.java diff --git a/application/src/main/java/org/thingsboard/server/actors/ActorSystemContext.java b/application/src/main/java/org/thingsboard/server/actors/ActorSystemContext.java index 442ff1d11f..effca0e126 100644 --- a/application/src/main/java/org/thingsboard/server/actors/ActorSystemContext.java +++ b/application/src/main/java/org/thingsboard/server/actors/ActorSystemContext.java @@ -31,7 +31,6 @@ import org.springframework.data.redis.core.RedisTemplate; import org.springframework.scheduling.annotation.Scheduled; import org.springframework.stereotype.Component; import org.thingsboard.common.util.JacksonUtil; -import org.thingsboard.rule.engine.api.CalculatedFieldQueueService; import org.thingsboard.rule.engine.api.MailService; import org.thingsboard.rule.engine.api.NotificationCenter; import org.thingsboard.rule.engine.api.RuleEngineDeviceStateManager; @@ -110,6 +109,7 @@ import org.thingsboard.server.queue.discovery.PartitionService; import org.thingsboard.server.queue.discovery.TbServiceInfoProvider; import org.thingsboard.server.service.apiusage.TbApiUsageStateService; import org.thingsboard.server.service.cf.CalculatedFieldProcessingService; +import org.thingsboard.server.service.cf.CalculatedFieldQueueService; import org.thingsboard.server.service.cf.CalculatedFieldStateService; import org.thingsboard.server.service.cf.cache.CalculatedFieldEntityProfileCache; import org.thingsboard.server.service.cf.ctx.state.ArgumentEntry; diff --git a/application/src/main/java/org/thingsboard/server/actors/ruleChain/DefaultTbContext.java b/application/src/main/java/org/thingsboard/server/actors/ruleChain/DefaultTbContext.java index a5b43d3223..2943c44827 100644 --- a/application/src/main/java/org/thingsboard/server/actors/ruleChain/DefaultTbContext.java +++ b/application/src/main/java/org/thingsboard/server/actors/ruleChain/DefaultTbContext.java @@ -23,12 +23,12 @@ import org.bouncycastle.util.Arrays; import org.thingsboard.common.util.DebugModeUtil; import org.thingsboard.common.util.JacksonUtil; import org.thingsboard.common.util.ListeningExecutor; -import org.thingsboard.rule.engine.api.CalculatedFieldQueueService; import org.thingsboard.rule.engine.api.MailService; import org.thingsboard.rule.engine.api.NotificationCenter; import org.thingsboard.rule.engine.api.RuleEngineAlarmService; import org.thingsboard.rule.engine.api.RuleEngineApiUsageStateService; import org.thingsboard.rule.engine.api.RuleEngineAssetProfileCache; +import org.thingsboard.rule.engine.api.RuleEngineCalculatedFieldQueueService; import org.thingsboard.rule.engine.api.RuleEngineDeviceProfileCache; import org.thingsboard.rule.engine.api.RuleEngineDeviceStateManager; import org.thingsboard.rule.engine.api.RuleEngineRpcService; @@ -904,7 +904,7 @@ public class DefaultTbContext implements TbContext { } @Override - public CalculatedFieldQueueService getCalculatedFieldQueueService() { + public RuleEngineCalculatedFieldQueueService getCalculatedFieldQueueService() { return mainCtx.getCalculatedFieldQueueService(); } diff --git a/rule-engine/rule-engine-api/src/main/java/org/thingsboard/rule/engine/api/CalculatedFieldQueueService.java b/application/src/main/java/org/thingsboard/server/service/cf/CalculatedFieldQueueService.java similarity index 75% rename from rule-engine/rule-engine-api/src/main/java/org/thingsboard/rule/engine/api/CalculatedFieldQueueService.java rename to application/src/main/java/org/thingsboard/server/service/cf/CalculatedFieldQueueService.java index b4eb24baea..eb86220361 100644 --- a/rule-engine/rule-engine-api/src/main/java/org/thingsboard/rule/engine/api/CalculatedFieldQueueService.java +++ b/application/src/main/java/org/thingsboard/server/service/cf/CalculatedFieldQueueService.java @@ -13,14 +13,19 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package org.thingsboard.rule.engine.api; +package org.thingsboard.server.service.cf; import com.google.common.util.concurrent.FutureCallback; +import org.thingsboard.rule.engine.api.AttributesDeleteRequest; +import org.thingsboard.rule.engine.api.AttributesSaveRequest; +import org.thingsboard.rule.engine.api.RuleEngineCalculatedFieldQueueService; +import org.thingsboard.rule.engine.api.TimeseriesDeleteRequest; +import org.thingsboard.rule.engine.api.TimeseriesSaveRequest; import org.thingsboard.server.common.data.kv.TimeseriesSaveResult; import java.util.List; -public interface CalculatedFieldQueueService { +public interface CalculatedFieldQueueService extends RuleEngineCalculatedFieldQueueService { /** * Filter CFs based on the request entity. Push to the queue if any matching CF exist; @@ -30,12 +35,8 @@ public interface CalculatedFieldQueueService { */ void pushRequestToQueue(TimeseriesSaveRequest request, TimeseriesSaveResult result, FutureCallback callback); - void pushRequestToQueue(TimeseriesSaveRequest request, FutureCallback callback); - void pushRequestToQueue(AttributesSaveRequest request, List result, FutureCallback callback); - void pushRequestToQueue(AttributesSaveRequest request, FutureCallback callback); - void pushRequestToQueue(AttributesDeleteRequest request, List result, FutureCallback callback); void pushRequestToQueue(TimeseriesDeleteRequest request, List result, FutureCallback callback); diff --git a/application/src/main/java/org/thingsboard/server/service/cf/DefaultCalculatedFieldQueueService.java b/application/src/main/java/org/thingsboard/server/service/cf/DefaultCalculatedFieldQueueService.java index fc92b70e07..ba4be6ace6 100644 --- a/application/src/main/java/org/thingsboard/server/service/cf/DefaultCalculatedFieldQueueService.java +++ b/application/src/main/java/org/thingsboard/server/service/cf/DefaultCalculatedFieldQueueService.java @@ -21,7 +21,6 @@ import lombok.extern.slf4j.Slf4j; import org.springframework.stereotype.Service; import org.thingsboard.rule.engine.api.AttributesDeleteRequest; import org.thingsboard.rule.engine.api.AttributesSaveRequest; -import org.thingsboard.rule.engine.api.CalculatedFieldQueueService; import org.thingsboard.rule.engine.api.TimeseriesDeleteRequest; import org.thingsboard.rule.engine.api.TimeseriesSaveRequest; import org.thingsboard.server.cluster.TbClusterService; diff --git a/application/src/main/java/org/thingsboard/server/service/telemetry/DefaultTelemetrySubscriptionService.java b/application/src/main/java/org/thingsboard/server/service/telemetry/DefaultTelemetrySubscriptionService.java index 31ae9e8055..2bdbd917ab 100644 --- a/application/src/main/java/org/thingsboard/server/service/telemetry/DefaultTelemetrySubscriptionService.java +++ b/application/src/main/java/org/thingsboard/server/service/telemetry/DefaultTelemetrySubscriptionService.java @@ -31,7 +31,6 @@ import org.thingsboard.common.util.DonAsynchron; import org.thingsboard.common.util.ThingsBoardThreadFactory; import org.thingsboard.rule.engine.api.AttributesDeleteRequest; import org.thingsboard.rule.engine.api.AttributesSaveRequest; -import org.thingsboard.rule.engine.api.CalculatedFieldQueueService; import org.thingsboard.rule.engine.api.RuleEngineTelemetryService; import org.thingsboard.rule.engine.api.TimeseriesDeleteRequest; import org.thingsboard.rule.engine.api.TimeseriesSaveRequest; @@ -51,6 +50,7 @@ import org.thingsboard.server.dao.attributes.AttributesService; import org.thingsboard.server.dao.timeseries.TimeseriesService; import org.thingsboard.server.dao.util.KvUtils; import org.thingsboard.server.service.apiusage.TbApiUsageStateService; +import org.thingsboard.server.service.cf.CalculatedFieldQueueService; import org.thingsboard.server.service.entitiy.entityview.TbEntityViewService; import org.thingsboard.server.service.subscription.TbSubscriptionUtils; diff --git a/application/src/test/java/org/thingsboard/server/service/telemetry/DefaultTelemetrySubscriptionServiceTest.java b/application/src/test/java/org/thingsboard/server/service/telemetry/DefaultTelemetrySubscriptionServiceTest.java index b47bea18d8..b8bfa13e50 100644 --- a/application/src/test/java/org/thingsboard/server/service/telemetry/DefaultTelemetrySubscriptionServiceTest.java +++ b/application/src/test/java/org/thingsboard/server/service/telemetry/DefaultTelemetrySubscriptionServiceTest.java @@ -28,7 +28,6 @@ import org.junit.jupiter.params.provider.MethodSource; import org.mockito.Mock; import org.mockito.junit.jupiter.MockitoExtension; import org.springframework.test.util.ReflectionTestUtils; -import org.thingsboard.rule.engine.api.CalculatedFieldQueueService; import org.thingsboard.rule.engine.api.TimeseriesSaveRequest; import org.thingsboard.server.cluster.TbClusterService; import org.thingsboard.server.common.data.ApiUsageRecordKey; @@ -57,6 +56,7 @@ import org.thingsboard.server.queue.discovery.PartitionService; import org.thingsboard.server.queue.discovery.QueueKey; import org.thingsboard.server.queue.discovery.event.PartitionChangeEvent; import org.thingsboard.server.service.apiusage.TbApiUsageStateService; +import org.thingsboard.server.service.cf.CalculatedFieldQueueService; import org.thingsboard.server.service.entitiy.entityview.TbEntityViewService; import org.thingsboard.server.service.subscription.SubscriptionManagerService; diff --git a/rule-engine/rule-engine-api/src/main/java/org/thingsboard/rule/engine/api/RuleEngineCalculatedFieldQueueService.java b/rule-engine/rule-engine-api/src/main/java/org/thingsboard/rule/engine/api/RuleEngineCalculatedFieldQueueService.java new file mode 100644 index 0000000000..6ab40b79c2 --- /dev/null +++ b/rule-engine/rule-engine-api/src/main/java/org/thingsboard/rule/engine/api/RuleEngineCalculatedFieldQueueService.java @@ -0,0 +1,26 @@ +/** + * 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.rule.engine.api; + +import com.google.common.util.concurrent.FutureCallback; + +public interface RuleEngineCalculatedFieldQueueService { + + void pushRequestToQueue(TimeseriesSaveRequest request, FutureCallback callback); + + void pushRequestToQueue(AttributesSaveRequest request, FutureCallback callback); + +} diff --git a/rule-engine/rule-engine-api/src/main/java/org/thingsboard/rule/engine/api/TbContext.java b/rule-engine/rule-engine-api/src/main/java/org/thingsboard/rule/engine/api/TbContext.java index 1abad50a8d..9053d96dfe 100644 --- a/rule-engine/rule-engine-api/src/main/java/org/thingsboard/rule/engine/api/TbContext.java +++ b/rule-engine/rule-engine-api/src/main/java/org/thingsboard/rule/engine/api/TbContext.java @@ -360,7 +360,7 @@ public interface TbContext { CalculatedFieldService getCalculatedFieldService(); - CalculatedFieldQueueService getCalculatedFieldQueueService(); + RuleEngineCalculatedFieldQueueService getCalculatedFieldQueueService(); boolean isExternalNodeForceAck(); diff --git a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/telemetry/TbCalculatedFieldsNode.java b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/telemetry/TbCalculatedFieldsNode.java index 1f2c7f135d..b2cce52c87 100644 --- a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/telemetry/TbCalculatedFieldsNode.java +++ b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/telemetry/TbCalculatedFieldsNode.java @@ -15,13 +15,8 @@ */ package org.thingsboard.rule.engine.telemetry; -import com.fasterxml.jackson.core.type.TypeReference; -import com.google.common.util.concurrent.FutureCallback; import com.google.gson.JsonParser; -import jakarta.annotation.Nullable; import lombok.extern.slf4j.Slf4j; -import org.thingsboard.common.util.JacksonUtil; -import org.thingsboard.rule.engine.api.AttributesDeleteRequest; import org.thingsboard.rule.engine.api.AttributesSaveRequest; import org.thingsboard.rule.engine.api.EmptyNodeConfiguration; import org.thingsboard.rule.engine.api.RuleNode; @@ -29,7 +24,6 @@ import org.thingsboard.rule.engine.api.TbContext; import org.thingsboard.rule.engine.api.TbNode; import org.thingsboard.rule.engine.api.TbNodeConfiguration; import org.thingsboard.rule.engine.api.TbNodeException; -import org.thingsboard.rule.engine.api.TimeseriesDeleteRequest; import org.thingsboard.rule.engine.api.TimeseriesSaveRequest; import org.thingsboard.rule.engine.api.util.TbNodeUtils; import org.thingsboard.server.common.adaptor.JsonConverter; @@ -42,23 +36,23 @@ import org.thingsboard.server.common.data.plugin.ComponentType; import org.thingsboard.server.common.msg.TbMsg; import java.util.ArrayList; -import java.util.Collections; import java.util.List; import java.util.Map; -import java.util.Optional; import static org.thingsboard.server.common.data.DataConstants.SCOPE; @Slf4j @RuleNode( type = ComponentType.ACTION, - name = "apply to calculated fields", + name = "calculated fields", configClazz = EmptyNodeConfiguration.class, - nodeDescription = "Processes incoming messages for calculated fields", - nodeDetails = "This node processes incoming messages to update telemetry or attributes for predefined calculated fields without storing the original telemetry or attributes in the database. " + - "It ensures that calculated fields receive and process the necessary data without persisting the incoming values.", + nodeDescription = "Pushes incoming messages to calculated fields service", + nodeDetails = "Node enables the processing of calculated fields without persisting incoming messages to the database. " + + "By default, the processing of calculated fields is triggered by the save attributes and save time series nodes. " + + "This rule node accepts the same messages as these nodes but allows you to trigger the processing of calculated " + + "fields independently, ensuring that derived data can be computed and utilized in real time without storing the original message in the database.", configDirective = "tbNodeEmptyConfig", - icon = "call_made" + icon = "published_with_changes" ) public class TbCalculatedFieldsNode implements TbNode { @@ -74,8 +68,6 @@ public class TbCalculatedFieldsNode implements TbNode { switch (msg.getInternalType()) { case POST_TELEMETRY_REQUEST -> processPostTelemetryRequest(ctx, msg); case POST_ATTRIBUTES_REQUEST -> processPostAttributesRequest(ctx, msg); - case TIMESERIES_DELETED -> processTimeSeriesDeleted(ctx, msg); - case ATTRIBUTES_DELETED -> processAttributesDeleted(ctx, msg); default -> ctx.tellFailure(msg, new IllegalArgumentException("Unsupported msg type: " + msg.getType())); } } @@ -84,7 +76,7 @@ public class TbCalculatedFieldsNode implements TbNode { Map> tsKvMap = JsonConverter.convertToTelemetry(JsonParser.parseString(msg.getData()), System.currentTimeMillis()); if (tsKvMap.isEmpty()) { - ctx.tellFailure(msg, new IllegalArgumentException("Msg body is empty: " + msg.getData())); + ctx.tellSuccess(msg); return; } @@ -130,76 +122,4 @@ public class TbCalculatedFieldsNode implements TbNode { ctx.getCalculatedFieldQueueService().pushRequestToQueue(attributesSaveRequest, attributesSaveRequest.getCallback()); } - private void processTimeSeriesDeleted(TbContext ctx, TbMsg msg) { - List keysToDelete = Optional.ofNullable( - JacksonUtil.convertValue(JacksonUtil.toJsonNode(msg.getData()).get("timeseries"), new TypeReference>() { - }) - ).orElse(Collections.emptyList()); - - if (keysToDelete.isEmpty()) { - ctx.tellSuccess(msg); - return; - } - - TimeseriesDeleteRequest timeseriesDeleteRequest = TimeseriesDeleteRequest.builder() - .tenantId(ctx.getTenantId()) - .entityId(msg.getOriginator()) - .keys(keysToDelete) - .previousCalculatedFieldIds(msg.getPreviousCalculatedFieldIds()) - .tbMsgId(msg.getId()) - .tbMsgType(msg.getInternalType()) - .callback(new FutureCallback>() { - @Override - public void onSuccess(@Nullable List tmp) { - ctx.tellSuccess(msg); - } - - @Override - public void onFailure(Throwable t) { - ctx.tellFailure(msg, t); - } - }) - .build(); - - ctx.getCalculatedFieldQueueService().pushRequestToQueue(timeseriesDeleteRequest, keysToDelete, getCalculatedFieldCallback(timeseriesDeleteRequest.getCallback(), keysToDelete)); - } - - private void processAttributesDeleted(TbContext ctx, TbMsg msg) { - List keysToDelete = Optional.ofNullable( - JacksonUtil.convertValue(JacksonUtil.toJsonNode(msg.getData()).get("attributes"), new TypeReference>() { - }) - ).orElse(Collections.emptyList()); - - if (keysToDelete.isEmpty()) { - ctx.tellSuccess(msg); - return; - } - - AttributesDeleteRequest attributesDeleteRequest = AttributesDeleteRequest.builder() - .tenantId(ctx.getTenantId()) - .entityId(msg.getOriginator()) - .scope(AttributeScope.valueOf(msg.getMetaData().getValue(SCOPE))) - .keys(keysToDelete) - .previousCalculatedFieldIds(msg.getPreviousCalculatedFieldIds()) - .tbMsgId(msg.getId()) - .tbMsgType(msg.getInternalType()) - .callback(new TelemetryNodeCallback(ctx, msg)) - .build(); - ctx.getCalculatedFieldQueueService().pushRequestToQueue(attributesDeleteRequest, keysToDelete, attributesDeleteRequest.getCallback()); - } - - private FutureCallback getCalculatedFieldCallback(FutureCallback> originalCallback, List keys) { - return new FutureCallback() { - @Override - public void onSuccess(Void unused) { - originalCallback.onSuccess(keys); - } - - @Override - public void onFailure(Throwable t) { - originalCallback.onFailure(t); - } - }; - } - } From e12b0ea8ea74704d7ea2bc21ad0af111b2aceb92 Mon Sep 17 00:00:00 2001 From: Andrii Shvaika Date: Thu, 6 Mar 2025 17:04:18 +0200 Subject: [PATCH 219/345] Change signature for ctx to be first parameter --- .../CalculatedFieldEntityMessageProcessor.java | 2 +- .../service/cf/DefaultCalculatedFieldProcessingService.java | 4 ++-- .../service/cf/ctx/state/BaseCalculatedFieldState.java | 2 +- .../server/service/cf/ctx/state/CalculatedFieldState.java | 2 +- .../cf/ctx/state/ScriptCalculatedFieldStateTest.java | 4 ++-- .../cf/ctx/state/SimpleCalculatedFieldStateTest.java | 6 +++--- 6 files changed, 10 insertions(+), 10 deletions(-) diff --git a/application/src/main/java/org/thingsboard/server/actors/calculatedField/CalculatedFieldEntityMessageProcessor.java b/application/src/main/java/org/thingsboard/server/actors/calculatedField/CalculatedFieldEntityMessageProcessor.java index 3911714ae0..61f46b7901 100644 --- a/application/src/main/java/org/thingsboard/server/actors/calculatedField/CalculatedFieldEntityMessageProcessor.java +++ b/application/src/main/java/org/thingsboard/server/actors/calculatedField/CalculatedFieldEntityMessageProcessor.java @@ -242,7 +242,7 @@ public class CalculatedFieldEntityMessageProcessor extends AbstractContextAwareM justRestored = true; } if (state.isSizeOk()) { - if (state.updateState(newArgValues, ctx) || justRestored) { + if (state.updateState(ctx, newArgValues) || justRestored) { cfIdList = new ArrayList<>(cfIdList); cfIdList.add(ctx.getCfId()); processStateIfReady(ctx, cfIdList, state, tbMsgId, tbMsgType, callback); diff --git a/application/src/main/java/org/thingsboard/server/service/cf/DefaultCalculatedFieldProcessingService.java b/application/src/main/java/org/thingsboard/server/service/cf/DefaultCalculatedFieldProcessingService.java index 2f73788fad..4976045b84 100644 --- a/application/src/main/java/org/thingsboard/server/service/cf/DefaultCalculatedFieldProcessingService.java +++ b/application/src/main/java/org/thingsboard/server/service/cf/DefaultCalculatedFieldProcessingService.java @@ -125,7 +125,7 @@ public class DefaultCalculatedFieldProcessingService implements CalculatedFieldP } return Futures.whenAllComplete(argFutures.values()).call(() -> { var result = createStateByType(ctx); - result.updateState(argFutures.entrySet().stream() + result.updateState(ctx, argFutures.entrySet().stream() .collect(Collectors.toMap( Entry::getKey, // Keep the key as is entry -> { @@ -136,7 +136,7 @@ public class DefaultCalculatedFieldProcessingService implements CalculatedFieldP throw new RuntimeException("Error getting future result for key: " + entry.getKey(), e); } } - )), ctx); + ))); return result; }, calculatedFieldCallbackExecutor); } diff --git a/application/src/main/java/org/thingsboard/server/service/cf/ctx/state/BaseCalculatedFieldState.java b/application/src/main/java/org/thingsboard/server/service/cf/ctx/state/BaseCalculatedFieldState.java index 624198e579..80b003b3cc 100644 --- a/application/src/main/java/org/thingsboard/server/service/cf/ctx/state/BaseCalculatedFieldState.java +++ b/application/src/main/java/org/thingsboard/server/service/cf/ctx/state/BaseCalculatedFieldState.java @@ -45,7 +45,7 @@ public abstract class BaseCalculatedFieldState implements CalculatedFieldState { } @Override - public boolean updateState(Map argumentValues, CalculatedFieldCtx ctx) { + public boolean updateState(CalculatedFieldCtx ctx, Map argumentValues) { if (arguments == null) { arguments = new HashMap<>(); } diff --git a/application/src/main/java/org/thingsboard/server/service/cf/ctx/state/CalculatedFieldState.java b/application/src/main/java/org/thingsboard/server/service/cf/ctx/state/CalculatedFieldState.java index 501cb5f54c..fc4ba513d2 100644 --- a/application/src/main/java/org/thingsboard/server/service/cf/ctx/state/CalculatedFieldState.java +++ b/application/src/main/java/org/thingsboard/server/service/cf/ctx/state/CalculatedFieldState.java @@ -44,7 +44,7 @@ public interface CalculatedFieldState { void setRequiredArguments(List requiredArguments); - boolean updateState(Map argumentValues, CalculatedFieldCtx ctx); + boolean updateState(CalculatedFieldCtx ctx, Map argumentValues); ListenableFuture performCalculation(CalculatedFieldCtx ctx); diff --git a/application/src/test/java/org/thingsboard/server/service/cf/ctx/state/ScriptCalculatedFieldStateTest.java b/application/src/test/java/org/thingsboard/server/service/cf/ctx/state/ScriptCalculatedFieldStateTest.java index 78144a0150..dee32e7d1f 100644 --- a/application/src/test/java/org/thingsboard/server/service/cf/ctx/state/ScriptCalculatedFieldStateTest.java +++ b/application/src/test/java/org/thingsboard/server/service/cf/ctx/state/ScriptCalculatedFieldStateTest.java @@ -90,7 +90,7 @@ public class ScriptCalculatedFieldStateTest { state.arguments = new HashMap<>(Map.of("assetHumidity", assetHumidityArgEntry)); Map newArgs = Map.of("deviceTemperature", deviceTemperatureArgEntry); - boolean stateUpdated = state.updateState(newArgs, ctx); + boolean stateUpdated = state.updateState(ctx, newArgs); assertThat(stateUpdated).isTrue(); assertThat(state.getArguments()).containsExactlyInAnyOrderEntriesOf( @@ -107,7 +107,7 @@ public class ScriptCalculatedFieldStateTest { SingleValueArgumentEntry newArgEntry = new SingleValueArgumentEntry(ts, new LongDataEntry("assetHumidity", 41L), 349L); Map newArgs = Map.of("assetHumidity", newArgEntry); - boolean stateUpdated = state.updateState(newArgs, ctx); + boolean stateUpdated = state.updateState(ctx, newArgs); assertThat(stateUpdated).isTrue(); assertThat(state.getArguments()).containsExactlyInAnyOrderEntriesOf( diff --git a/application/src/test/java/org/thingsboard/server/service/cf/ctx/state/SimpleCalculatedFieldStateTest.java b/application/src/test/java/org/thingsboard/server/service/cf/ctx/state/SimpleCalculatedFieldStateTest.java index f146eb434f..f0059e4f6f 100644 --- a/application/src/test/java/org/thingsboard/server/service/cf/ctx/state/SimpleCalculatedFieldStateTest.java +++ b/application/src/test/java/org/thingsboard/server/service/cf/ctx/state/SimpleCalculatedFieldStateTest.java @@ -88,7 +88,7 @@ public class SimpleCalculatedFieldStateTest { )); Map newArgs = Map.of("key3", key3ArgEntry); - boolean stateUpdated = state.updateState(newArgs, ctx); + boolean stateUpdated = state.updateState(ctx, newArgs); assertThat(stateUpdated).isTrue(); assertThat(state.getArguments()).containsExactlyInAnyOrderEntriesOf( @@ -106,7 +106,7 @@ public class SimpleCalculatedFieldStateTest { SingleValueArgumentEntry newArgEntry = new SingleValueArgumentEntry(System.currentTimeMillis(), new LongDataEntry("key1", 18L), 190L); Map newArgs = Map.of("key1", newArgEntry); - boolean stateUpdated = state.updateState(newArgs, ctx); + boolean stateUpdated = state.updateState(ctx, newArgs); assertThat(stateUpdated).isTrue(); assertThat(state.getArguments()).containsExactlyInAnyOrderEntriesOf(Map.of("key1", newArgEntry)); @@ -120,7 +120,7 @@ public class SimpleCalculatedFieldStateTest { )); Map newArgs = Map.of("key3", new TsRollingArgumentEntry(10, 30000L)); - assertThatThrownBy(() -> state.updateState(newArgs, ctx)) + assertThatThrownBy(() -> state.updateState(ctx, newArgs)) .isInstanceOf(IllegalArgumentException.class) .hasMessage("Rolling argument entry is not supported for simple calculated fields."); } From 14de7726c2f17773bcb2a9c53719ee9c70f27fb8 Mon Sep 17 00:00:00 2001 From: Igor Kulikov Date: Thu, 6 Mar 2025 17:31:00 +0200 Subject: [PATCH 220/345] UI: Fix widget edit tooltip. Map - Improve marker icon appearance. --- ui-ngx/patches/tooltipster+4.2.8.patch | 223 ++++++++++++++++++ .../lib/maps/data-layer/markers-data-layer.ts | 10 +- .../home/components/widget/lib/maps/map.scss | 8 + .../map/map-data-layer-dialog.component.html | 74 +++--- .../map/marker-icon-shapes.component.html | 60 +++++ .../map/marker-icon-shapes.component.scss | 47 ++++ .../map/marker-icon-shapes.component.ts | 127 ++++++++++ .../map/marker-shape-settings.component.ts | 31 ++- .../common/widget-settings-common.module.ts | 4 + .../widget/widget-container.component.ts | 7 +- .../components/material-icons.component.html | 2 +- .../components/material-icons.component.ts | 6 +- .../shared/models/widget/maps/map.models.ts | 10 +- .../models/widget/maps/marker-shape.models.ts | 66 ++++-- .../assets/locale/locale.constant-en_US.json | 2 + ui-ngx/src/assets/markers/iconContainer1.svg | 22 ++ ui-ngx/src/assets/markers/shape1.svg | 4 +- ui-ngx/src/assets/markers/shape2.svg | 4 +- ui-ngx/src/assets/markers/shape6.svg | 7 +- .../src/assets/markers/tripIconContainer1.svg | 12 + 20 files changed, 648 insertions(+), 78 deletions(-) create mode 100644 ui-ngx/patches/tooltipster+4.2.8.patch create mode 100644 ui-ngx/src/app/modules/home/components/widget/lib/settings/common/map/marker-icon-shapes.component.html create mode 100644 ui-ngx/src/app/modules/home/components/widget/lib/settings/common/map/marker-icon-shapes.component.scss create mode 100644 ui-ngx/src/app/modules/home/components/widget/lib/settings/common/map/marker-icon-shapes.component.ts create mode 100644 ui-ngx/src/assets/markers/iconContainer1.svg create mode 100644 ui-ngx/src/assets/markers/tripIconContainer1.svg diff --git a/ui-ngx/patches/tooltipster+4.2.8.patch b/ui-ngx/patches/tooltipster+4.2.8.patch new file mode 100644 index 0000000000..d328300720 --- /dev/null +++ b/ui-ngx/patches/tooltipster+4.2.8.patch @@ -0,0 +1,223 @@ +diff --git a/node_modules/tooltipster/dist/js/plugins/tooltipster/SVG/tooltipster-SVG.js b/node_modules/tooltipster/dist/js/plugins/tooltipster/SVG/tooltipster-SVG.js +index 8d94dc7..8123bac 100644 +--- a/node_modules/tooltipster/dist/js/plugins/tooltipster/SVG/tooltipster-SVG.js ++++ b/node_modules/tooltipster/dist/js/plugins/tooltipster/SVG/tooltipster-SVG.js +@@ -1,32 +1,34 @@ + (function (root, factory) { ++ if (root === undefined && window !== undefined) root = window; + if (typeof define === 'function' && define.amd) { + // AMD. Register as an anonymous module unless amdModuleId is set + define(["tooltipster"], function (a0) { + return (factory(a0)); + }); +- } else if (typeof exports === 'object') { ++ } else if (typeof module === 'object' && module.exports) { + // Node. Does not work with strict CommonJS, but + // only CommonJS-like environments that support module.exports, + // like Node. + module.exports = factory(require("tooltipster")); + } else { +- factory(jQuery); ++ factory(root["jQuery"]); + } + }(this, function ($) { + + (function (root, factory) { ++ if (root === undefined && window !== undefined) root = window; + if (typeof define === 'function' && define.amd) { + // AMD. Register as an anonymous module unless amdModuleId is set + define(["jquery"], function (a0) { + return (factory(a0)); + }); +- } else if (typeof exports === 'object') { ++ } else if (typeof module === 'object' && module.exports) { + // Node. Does not work with strict CommonJS, but + // only CommonJS-like environments that support module.exports, + // like Node. + module.exports = factory(require("jquery")); + } else { +- factory(jQuery); ++ factory(root["jQuery"]); + } + }(this, function ($) { + +diff --git a/node_modules/tooltipster/dist/js/plugins/tooltipster/SVG/tooltipster-SVG.min.js b/node_modules/tooltipster/dist/js/plugins/tooltipster/SVG/tooltipster-SVG.min.js +index df77068..84bdfdd 100644 +--- a/node_modules/tooltipster/dist/js/plugins/tooltipster/SVG/tooltipster-SVG.min.js ++++ b/node_modules/tooltipster/dist/js/plugins/tooltipster/SVG/tooltipster-SVG.min.js +@@ -1 +1 @@ +-!function(a,b){"function"==typeof define&&define.amd?define(["tooltipster"],function(a){return b(a)}):"object"==typeof exports?module.exports=b(require("tooltipster")):b(jQuery)}(this,function(a){!function(a,b){"function"==typeof define&&define.amd?define(["jquery"],function(a){return b(a)}):"object"==typeof exports?module.exports=b(require("jquery")):b(jQuery)}(this,function(a){var b="tooltipster.SVG";return a.tooltipster._plugin({name:b,core:{__init:function(){a.tooltipster._on("init",function(c){var d=a.tooltipster._env.window;d.SVGElement&&c.origin instanceof d.SVGElement&&c.instance._plug(b)})}},instance:{__init:function(b){var c=this;if(c.__hadTitleTag=!1,c.__instance=b,!c.__instance._$origin.hasClass("tooltipstered")){var d=c.__instance._$origin.attr("class")||"";-1==d.indexOf("tooltipstered")&&c.__instance._$origin.attr("class",d+" tooltipstered")}if(null===c.__instance.content()){var e=c.__instance._$origin.find(">title");if(e[0]){var f=e.text();c.__hadTitleTag=!0,c.__instance._$origin.data("tooltipster-initialTitle",f),c.__instance.content(f),e.remove()}}c.__instance._on("geometry."+c.namespace,function(b){var c=a.tooltipster._env.window;if(c.SVG.svgjs){c.SVG.parser||c.SVG.prepare();var d=c.SVG.adopt(b.origin);if(d&&d.screenBBox){var e=d.screenBBox();b.edit({height:e.height,left:e.x,top:e.y,width:e.width})}}})._on("destroy."+c.namespace,function(){c.__destroy()})},__destroy:function(){var b=this;if(!b.__instance._$origin.hasClass("tooltipstered")){var c=b.__instance._$origin.attr("class").replace("tooltipstered","");b.__instance._$origin.attr("class",c)}b.__instance._off("."+b.namespace),b.__hadTitleTag&&b.__instance.one("destroyed",function(){var c=b.__instance._$origin.attr("title");c&&(a(document.createElementNS("http://www.w3.org/2000/svg","title")).text(c).appendTo(b.__instance._$origin),b.__instance._$origin.removeAttr("title"))})}}}),a})}); +\ No newline at end of file ++!function(a,b){void 0===a&&void 0!==window&&(a=window),"function"==typeof define&&define.amd?define(["tooltipster"],function(a){return b(a)}):"object"==typeof module&&module.exports?module.exports=b(require("tooltipster")):b(a.jQuery)}(this,function(a){!function(a,b){void 0===a&&void 0!==window&&(a=window),"function"==typeof define&&define.amd?define(["jquery"],function(a){return b(a)}):"object"==typeof module&&module.exports?module.exports=b(require("jquery")):b(a.jQuery)}(this,function(a){var b="tooltipster.SVG";return a.tooltipster._plugin({name:b,core:{__init:function(){a.tooltipster._on("init",function(c){var d=a.tooltipster._env.window;d.SVGElement&&c.origin instanceof d.SVGElement&&c.instance._plug(b)})}},instance:{__init:function(b){var c=this;if(c.__hadTitleTag=!1,c.__instance=b,!c.__instance._$origin.hasClass("tooltipstered")){var d=c.__instance._$origin.attr("class")||"";d.indexOf("tooltipstered")==-1&&c.__instance._$origin.attr("class",d+" tooltipstered")}if(null===c.__instance.content()){var e=c.__instance._$origin.find(">title");if(e[0]){var f=e.text();c.__hadTitleTag=!0,c.__instance._$origin.data("tooltipster-initialTitle",f),c.__instance.content(f),e.remove()}}c.__instance._on("geometry."+c.namespace,function(b){var c=a.tooltipster._env.window;if(c.SVG.svgjs){c.SVG.parser||c.SVG.prepare();var d=c.SVG.adopt(b.origin);if(d&&d.screenBBox){var e=d.screenBBox();b.edit({height:e.height,left:e.x,top:e.y,width:e.width})}}})._on("destroy."+c.namespace,function(){c.__destroy()})},__destroy:function(){var b=this;if(!b.__instance._$origin.hasClass("tooltipstered")){var c=b.__instance._$origin.attr("class").replace("tooltipstered","");b.__instance._$origin.attr("class",c)}b.__instance._off("."+b.namespace),b.__hadTitleTag&&b.__instance.one("destroyed",function(){var c=b.__instance._$origin.attr("title");c&&(a(document.createElementNS("http://www.w3.org/2000/svg","title")).text(c).appendTo(b.__instance._$origin),b.__instance._$origin.removeAttr("title"))})}}}),a})}); +\ No newline at end of file +diff --git a/node_modules/tooltipster/dist/js/tooltipster.bundle.js b/node_modules/tooltipster/dist/js/tooltipster.bundle.js +index ed97bdc..2b3381f 100644 +--- a/node_modules/tooltipster/dist/js/tooltipster.bundle.js ++++ b/node_modules/tooltipster/dist/js/tooltipster.bundle.js +@@ -5,18 +5,19 @@ + * MIT license + */ + (function (root, factory) { ++ if (root === undefined && window !== undefined) root = window; + if (typeof define === 'function' && define.amd) { + // AMD. Register as an anonymous module unless amdModuleId is set + define(["jquery"], function (a0) { + return (factory(a0)); + }); +- } else if (typeof exports === 'object') { ++ } else if (typeof module === 'object' && module.exports) { + // Node. Does not work with strict CommonJS, but + // only CommonJS-like environments that support module.exports, + // like Node. + module.exports = factory(require("jquery")); + } else { +- factory(jQuery); ++ factory(root["jQuery"]); + } + }(this, function ($) { + +@@ -1260,8 +1261,14 @@ $.Tooltipster.prototype = { + if ( geo.origin.windowOffset.top < bcr.top + || geo.origin.windowOffset.bottom > bcr.bottom + ) { +- overflows = true; +- return false; ++ if (self.__options.checkOverflowY) { ++ overflows = self.__options.checkOverflowY(geo, bcr); ++ } else { ++ overflows = true; ++ } ++ if (overflows) { ++ return false; ++ } + } + } + } +@@ -1412,7 +1419,7 @@ $.Tooltipster.prototype = { + + // close the tooltip when using the mouseleave close trigger + // (see https://github.com/iamceege/tooltipster/pull/253) +- if (self.__options.triggerClose.mouseleave) { ++ if (self.__options.triggerClose.mouseleave && !self.__options.ignoreCloseOnScroll) { + self._close(); + } + else { +@@ -3341,6 +3348,7 @@ function transitionSupport() { + // we'll return jQuery for plugins not to have to declare it as a dependency, + // but it's done by a build task since it should be included only once at the + // end when we concatenate the main file with a plugin ++ + // sideTip is Tooltipster's default plugin. + // This file will be UMDified by a build task. + +diff --git a/node_modules/tooltipster/dist/js/tooltipster.bundle.min.js b/node_modules/tooltipster/dist/js/tooltipster.bundle.min.js +index bbbd6ad..af0ffc0 100644 +--- a/node_modules/tooltipster/dist/js/tooltipster.bundle.min.js ++++ b/node_modules/tooltipster/dist/js/tooltipster.bundle.min.js +@@ -1,2 +1,2 @@ +-/*! tooltipster v4.2.8 */!function(a,b){"function"==typeof define&&define.amd?define(["jquery"],function(a){return b(a)}):"object"==typeof exports?module.exports=b(require("jquery")):b(jQuery)}(this,function(a){function b(a){this.$container,this.constraints=null,this.__$tooltip,this.__init(a)}function c(b,c){var d=!0;return a.each(b,function(a,e){return void 0===c[a]||b[a]!==c[a]?(d=!1,!1):void 0}),d}function d(b){var c=b.attr("id"),d=c?h.window.document.getElementById(c):null;return d?d===b[0]:a.contains(h.window.document.body,b[0])}function e(){if(!g)return!1;var a=g.document.body||g.document.documentElement,b=a.style,c="transition",d=["Moz","Webkit","Khtml","O","ms"];if("string"==typeof b[c])return!0;c=c.charAt(0).toUpperCase()+c.substr(1);for(var e=0;e0?e=c.__plugins[d]:a.each(c.__plugins,function(a,b){return b.name.substring(b.name.length-d.length-1)=="."+d?(e=b,!1):void 0}),e}if(b.name.indexOf(".")<0)throw new Error("Plugins must be namespaced");return c.__plugins[b.name]=b,b.core&&c.__bridge(b.core,c,b.name),this},_trigger:function(){var a=Array.prototype.slice.apply(arguments);return"string"==typeof a[0]&&(a[0]={type:a[0]}),this.__$emitterPrivate.trigger.apply(this.__$emitterPrivate,a),this.__$emitterPublic.trigger.apply(this.__$emitterPublic,a),this},instances:function(b){var c=[],d=b||".tooltipstered";return a(d).each(function(){var b=a(this),d=b.data("tooltipster-ns");d&&a.each(d,function(a,d){c.push(b.data(d))})}),c},instancesLatest:function(){return this.__instancesLatestArr},off:function(){return this.__$emitterPublic.off.apply(this.__$emitterPublic,Array.prototype.slice.apply(arguments)),this},on:function(){return this.__$emitterPublic.on.apply(this.__$emitterPublic,Array.prototype.slice.apply(arguments)),this},one:function(){return this.__$emitterPublic.one.apply(this.__$emitterPublic,Array.prototype.slice.apply(arguments)),this},origins:function(b){var c=b?b+" ":"";return a(c+".tooltipstered").toArray()},setDefaults:function(b){return a.extend(f,b),this},triggerHandler:function(){return this.__$emitterPublic.triggerHandler.apply(this.__$emitterPublic,Array.prototype.slice.apply(arguments)),this}},a.tooltipster=new i,a.Tooltipster=function(b,c){this.__callbacks={close:[],open:[]},this.__closingTime,this.__Content,this.__contentBcr,this.__destroyed=!1,this.__$emitterPrivate=a({}),this.__$emitterPublic=a({}),this.__enabled=!0,this.__garbageCollector,this.__Geometry,this.__lastPosition,this.__namespace="tooltipster-"+Math.round(1e6*Math.random()),this.__options,this.__$originParents,this.__pointerIsOverOrigin=!1,this.__previousThemes=[],this.__state="closed",this.__timeouts={close:[],open:null},this.__touchEvents=[],this.__tracker=null,this._$origin,this._$tooltip,this.__init(b,c)},a.Tooltipster.prototype={__init:function(b,c){var d=this;if(d._$origin=a(b),d.__options=a.extend(!0,{},f,c),d.__optionsFormat(),!h.IE||h.IE>=d.__options.IEmin){var e=null;if(void 0===d._$origin.data("tooltipster-initialTitle")&&(e=d._$origin.attr("title"),void 0===e&&(e=null),d._$origin.data("tooltipster-initialTitle",e)),null!==d.__options.content)d.__contentSet(d.__options.content);else{var g,i=d._$origin.attr("data-tooltip-content");i&&(g=a(i)),g&&g[0]?d.__contentSet(g.first()):d.__contentSet(e)}d._$origin.removeAttr("title").addClass("tooltipstered"),d.__prepareOrigin(),d.__prepareGC(),a.each(d.__options.plugins,function(a,b){d._plug(b)}),h.hasTouchCapability&&a(h.window.document.body).on("touchmove."+d.__namespace+"-triggerOpen",function(a){d._touchRecordEvent(a)}),d._on("created",function(){d.__prepareTooltip()})._on("repositioned",function(a){d.__lastPosition=a.position})}else d.__options.disabled=!0},__contentInsert:function(){var a=this,b=a._$tooltip.find(".tooltipster-content"),c=a.__Content,d=function(a){c=a};return a._trigger({type:"format",content:a.__Content,format:d}),a.__options.functionFormat&&(c=a.__options.functionFormat.call(a,a,{origin:a._$origin[0]},a.__Content)),"string"!=typeof c||a.__options.contentAsHTML?b.empty().append(c):b.text(c),a},__contentSet:function(b){return b instanceof a&&this.__options.contentCloning&&(b=b.clone(!0)),this.__Content=b,this._trigger({type:"updated",content:b}),this},__destroyError:function(){throw new Error("This tooltip has been destroyed and cannot execute your method call.")},__geometry:function(){var b=this,c=b._$origin,d=b._$origin.is("area");if(d){var e=b._$origin.parent().attr("name");c=a('img[usemap="#'+e+'"]')}var f=c[0].getBoundingClientRect(),g=a(h.window.document),i=a(h.window),j=c,k={available:{document:null,window:null},document:{size:{height:g.height(),width:g.width()}},window:{scroll:{left:h.window.scrollX||h.window.document.documentElement.scrollLeft,top:h.window.scrollY||h.window.document.documentElement.scrollTop},size:{height:i.height(),width:i.width()}},origin:{fixedLineage:!1,offset:{},size:{height:f.bottom-f.top,width:f.right-f.left},usemapImage:d?c[0]:null,windowOffset:{bottom:f.bottom,left:f.left,right:f.right,top:f.top}}};if(d){var l=b._$origin.attr("shape"),m=b._$origin.attr("coords");if(m&&(m=m.split(","),a.map(m,function(a,b){m[b]=parseInt(a)})),"default"!=l)switch(l){case"circle":var n=m[0],o=m[1],p=m[2],q=o-p,r=n-p;k.origin.size.height=2*p,k.origin.size.width=k.origin.size.height,k.origin.windowOffset.left+=r,k.origin.windowOffset.top+=q;break;case"rect":var s=m[0],t=m[1],u=m[2],v=m[3];k.origin.size.height=v-t,k.origin.size.width=u-s,k.origin.windowOffset.left+=s,k.origin.windowOffset.top+=t;break;case"poly":for(var w=0,x=0,y=0,z=0,A="even",B=0;By&&(y=C,0===B&&(w=y)),w>C&&(w=C),A="odd"):(C>z&&(z=C,1==B&&(x=z)),x>C&&(x=C),A="even")}k.origin.size.height=z-x,k.origin.size.width=y-w,k.origin.windowOffset.left+=w,k.origin.windowOffset.top+=x}}var D=function(a){k.origin.size.height=a.height,k.origin.windowOffset.left=a.left,k.origin.windowOffset.top=a.top,k.origin.size.width=a.width};for(b._trigger({type:"geometry",edit:D,geometry:{height:k.origin.size.height,left:k.origin.windowOffset.left,top:k.origin.windowOffset.top,width:k.origin.size.width}}),k.origin.windowOffset.right=k.origin.windowOffset.left+k.origin.size.width,k.origin.windowOffset.bottom=k.origin.windowOffset.top+k.origin.size.height,k.origin.offset.left=k.origin.windowOffset.left+k.window.scroll.left,k.origin.offset.top=k.origin.windowOffset.top+k.window.scroll.top,k.origin.offset.bottom=k.origin.offset.top+k.origin.size.height,k.origin.offset.right=k.origin.offset.left+k.origin.size.width,k.available.document={bottom:{height:k.document.size.height-k.origin.offset.bottom,width:k.document.size.width},left:{height:k.document.size.height,width:k.origin.offset.left},right:{height:k.document.size.height,width:k.document.size.width-k.origin.offset.right},top:{height:k.origin.offset.top,width:k.document.size.width}},k.available.window={bottom:{height:Math.max(k.window.size.height-Math.max(k.origin.windowOffset.bottom,0),0),width:k.window.size.width},left:{height:k.window.size.height,width:Math.max(k.origin.windowOffset.left,0)},right:{height:k.window.size.height,width:Math.max(k.window.size.width-Math.max(k.origin.windowOffset.right,0),0)},top:{height:Math.max(k.origin.windowOffset.top,0),width:k.window.size.width}};"html"!=j[0].tagName.toLowerCase();){if("fixed"==j.css("position")){k.origin.fixedLineage=!0;break}j=j.parent()}return k},__optionsFormat:function(){return"number"==typeof this.__options.animationDuration&&(this.__options.animationDuration=[this.__options.animationDuration,this.__options.animationDuration]),"number"==typeof this.__options.delay&&(this.__options.delay=[this.__options.delay,this.__options.delay]),"number"==typeof this.__options.delayTouch&&(this.__options.delayTouch=[this.__options.delayTouch,this.__options.delayTouch]),"string"==typeof this.__options.theme&&(this.__options.theme=[this.__options.theme]),null===this.__options.parent?this.__options.parent=a(h.window.document.body):"string"==typeof this.__options.parent&&(this.__options.parent=a(this.__options.parent)),"hover"==this.__options.trigger?(this.__options.triggerOpen={mouseenter:!0,touchstart:!0},this.__options.triggerClose={mouseleave:!0,originClick:!0,touchleave:!0}):"click"==this.__options.trigger&&(this.__options.triggerOpen={click:!0,tap:!0},this.__options.triggerClose={click:!0,tap:!0}),this._trigger("options"),this},__prepareGC:function(){var b=this;return b.__options.selfDestruction?b.__garbageCollector=setInterval(function(){var c=(new Date).getTime();b.__touchEvents=a.grep(b.__touchEvents,function(a,b){return c-a.time>6e4}),d(b._$origin)||b.close(function(){b.destroy()})},2e4):clearInterval(b.__garbageCollector),b},__prepareOrigin:function(){var a=this;if(a._$origin.off("."+a.__namespace+"-triggerOpen"),h.hasTouchCapability&&a._$origin.on("touchstart."+a.__namespace+"-triggerOpen touchend."+a.__namespace+"-triggerOpen touchcancel."+a.__namespace+"-triggerOpen",function(b){a._touchRecordEvent(b)}),a.__options.triggerOpen.click||a.__options.triggerOpen.tap&&h.hasTouchCapability){var b="";a.__options.triggerOpen.click&&(b+="click."+a.__namespace+"-triggerOpen "),a.__options.triggerOpen.tap&&h.hasTouchCapability&&(b+="touchend."+a.__namespace+"-triggerOpen"),a._$origin.on(b,function(b){a._touchIsMeaningfulEvent(b)&&a._open(b)})}if(a.__options.triggerOpen.mouseenter||a.__options.triggerOpen.touchstart&&h.hasTouchCapability){var b="";a.__options.triggerOpen.mouseenter&&(b+="mouseenter."+a.__namespace+"-triggerOpen "),a.__options.triggerOpen.touchstart&&h.hasTouchCapability&&(b+="touchstart."+a.__namespace+"-triggerOpen"),a._$origin.on(b,function(b){!a._touchIsTouchEvent(b)&&a._touchIsEmulatedEvent(b)||(a.__pointerIsOverOrigin=!0,a._openShortly(b))})}if(a.__options.triggerClose.mouseleave||a.__options.triggerClose.touchleave&&h.hasTouchCapability){var b="";a.__options.triggerClose.mouseleave&&(b+="mouseleave."+a.__namespace+"-triggerOpen "),a.__options.triggerClose.touchleave&&h.hasTouchCapability&&(b+="touchend."+a.__namespace+"-triggerOpen touchcancel."+a.__namespace+"-triggerOpen"),a._$origin.on(b,function(b){a._touchIsMeaningfulEvent(b)&&(a.__pointerIsOverOrigin=!1)})}return a},__prepareTooltip:function(){var b=this,c=b.__options.interactive?"auto":"";return b._$tooltip.attr("id",b.__namespace).css({"pointer-events":c,zIndex:b.__options.zIndex}),a.each(b.__previousThemes,function(a,c){b._$tooltip.removeClass(c)}),a.each(b.__options.theme,function(a,c){b._$tooltip.addClass(c)}),b.__previousThemes=a.merge([],b.__options.theme),b},__scrollHandler:function(b){var c=this;if(c.__options.triggerClose.scroll)c._close(b);else if(d(c._$origin)&&d(c._$tooltip)){var e=null;if(b.target===h.window.document)c.__Geometry.origin.fixedLineage||c.__options.repositionOnScroll&&c.reposition(b);else{e=c.__geometry();var f=!1;if("fixed"!=c._$origin.css("position")&&c.__$originParents.each(function(b,c){var d=a(c),g=d.css("overflow-x"),h=d.css("overflow-y");if("visible"!=g||"visible"!=h){var i=c.getBoundingClientRect();if("visible"!=g&&(e.origin.windowOffset.lefti.right))return f=!0,!1;if("visible"!=h&&(e.origin.windowOffset.topi.bottom))return f=!0,!1}return"fixed"==d.css("position")?!1:void 0}),f)c._$tooltip.css("visibility","hidden");else if(c._$tooltip.css("visibility","visible"),c.__options.repositionOnScroll)c.reposition(b);else{var g=e.origin.offset.left-c.__Geometry.origin.offset.left,i=e.origin.offset.top-c.__Geometry.origin.offset.top;c._$tooltip.css({left:c.__lastPosition.coord.left+g,top:c.__lastPosition.coord.top+i})}}c._trigger({type:"scroll",event:b,geo:e})}return c},__stateSet:function(a){return this.__state=a,this._trigger({type:"state",state:a}),this},__timeoutsClear:function(){return clearTimeout(this.__timeouts.open),this.__timeouts.open=null,a.each(this.__timeouts.close,function(a,b){clearTimeout(b)}),this.__timeouts.close=[],this},__trackerStart:function(){var a=this,b=a._$tooltip.find(".tooltipster-content");return a.__options.trackTooltip&&(a.__contentBcr=b[0].getBoundingClientRect()),a.__tracker=setInterval(function(){if(d(a._$origin)&&d(a._$tooltip)){if(a.__options.trackOrigin){var e=a.__geometry(),f=!1;c(e.origin.size,a.__Geometry.origin.size)&&(a.__Geometry.origin.fixedLineage?c(e.origin.windowOffset,a.__Geometry.origin.windowOffset)&&(f=!0):c(e.origin.offset,a.__Geometry.origin.offset)&&(f=!0)),f||(a.__options.triggerClose.mouseleave?a._close():a.reposition())}if(a.__options.trackTooltip){var g=b[0].getBoundingClientRect();g.height===a.__contentBcr.height&&g.width===a.__contentBcr.width||(a.reposition(),a.__contentBcr=g)}}else a._close()},a.__options.trackerInterval),a},_close:function(b,c,d){var e=this,f=!0;if(e._trigger({type:"close",event:b,stop:function(){f=!1}}),f||d){c&&e.__callbacks.close.push(c),e.__callbacks.open=[],e.__timeoutsClear();var g=function(){a.each(e.__callbacks.close,function(a,c){c.call(e,e,{event:b,origin:e._$origin[0]})}),e.__callbacks.close=[]};if("closed"!=e.__state){var i=!0,j=new Date,k=j.getTime(),l=k+e.__options.animationDuration[1];if("disappearing"==e.__state&&l>e.__closingTime&&e.__options.animationDuration[1]>0&&(i=!1),i){e.__closingTime=l,"disappearing"!=e.__state&&e.__stateSet("disappearing");var m=function(){clearInterval(e.__tracker),e._trigger({type:"closing",event:b}),e._$tooltip.off("."+e.__namespace+"-triggerClose").removeClass("tooltipster-dying"),a(h.window).off("."+e.__namespace+"-triggerClose"),e.__$originParents.each(function(b,c){a(c).off("scroll."+e.__namespace+"-triggerClose")}),e.__$originParents=null,a(h.window.document.body).off("."+e.__namespace+"-triggerClose"),e._$origin.off("."+e.__namespace+"-triggerClose"),e._off("dismissable"),e.__stateSet("closed"),e._trigger({type:"after",event:b}),e.__options.functionAfter&&e.__options.functionAfter.call(e,e,{event:b,origin:e._$origin[0]}),g()};h.hasTransitions?(e._$tooltip.css({"-moz-animation-duration":e.__options.animationDuration[1]+"ms","-ms-animation-duration":e.__options.animationDuration[1]+"ms","-o-animation-duration":e.__options.animationDuration[1]+"ms","-webkit-animation-duration":e.__options.animationDuration[1]+"ms","animation-duration":e.__options.animationDuration[1]+"ms","transition-duration":e.__options.animationDuration[1]+"ms"}),e._$tooltip.clearQueue().removeClass("tooltipster-show").addClass("tooltipster-dying"),e.__options.animationDuration[1]>0&&e._$tooltip.delay(e.__options.animationDuration[1]),e._$tooltip.queue(m)):e._$tooltip.stop().fadeOut(e.__options.animationDuration[1],m)}}else g()}return e},_off:function(){return this.__$emitterPrivate.off.apply(this.__$emitterPrivate,Array.prototype.slice.apply(arguments)),this},_on:function(){return this.__$emitterPrivate.on.apply(this.__$emitterPrivate,Array.prototype.slice.apply(arguments)),this},_one:function(){return this.__$emitterPrivate.one.apply(this.__$emitterPrivate,Array.prototype.slice.apply(arguments)),this},_open:function(b,c){var e=this;if(!e.__destroying&&d(e._$origin)&&e.__enabled){var f=!0;if("closed"==e.__state&&(e._trigger({type:"before",event:b,stop:function(){f=!1}}),f&&e.__options.functionBefore&&(f=e.__options.functionBefore.call(e,e,{event:b,origin:e._$origin[0]}))),f!==!1&&null!==e.__Content){c&&e.__callbacks.open.push(c),e.__callbacks.close=[],e.__timeoutsClear();var g,i=function(){"stable"!=e.__state&&e.__stateSet("stable"),a.each(e.__callbacks.open,function(a,b){b.call(e,e,{origin:e._$origin[0],tooltip:e._$tooltip[0]})}),e.__callbacks.open=[]};if("closed"!==e.__state)g=0,"disappearing"===e.__state?(e.__stateSet("appearing"),h.hasTransitions?(e._$tooltip.clearQueue().removeClass("tooltipster-dying").addClass("tooltipster-show"),e.__options.animationDuration[0]>0&&e._$tooltip.delay(e.__options.animationDuration[0]),e._$tooltip.queue(i)):e._$tooltip.stop().fadeIn(i)):"stable"==e.__state&&i();else{if(e.__stateSet("appearing"),g=e.__options.animationDuration[0],e.__contentInsert(),e.reposition(b,!0),h.hasTransitions?(e._$tooltip.addClass("tooltipster-"+e.__options.animation).addClass("tooltipster-initial").css({"-moz-animation-duration":e.__options.animationDuration[0]+"ms","-ms-animation-duration":e.__options.animationDuration[0]+"ms","-o-animation-duration":e.__options.animationDuration[0]+"ms","-webkit-animation-duration":e.__options.animationDuration[0]+"ms","animation-duration":e.__options.animationDuration[0]+"ms","transition-duration":e.__options.animationDuration[0]+"ms"}),setTimeout(function(){"closed"!=e.__state&&(e._$tooltip.addClass("tooltipster-show").removeClass("tooltipster-initial"),e.__options.animationDuration[0]>0&&e._$tooltip.delay(e.__options.animationDuration[0]),e._$tooltip.queue(i))},0)):e._$tooltip.css("display","none").fadeIn(e.__options.animationDuration[0],i),e.__trackerStart(),a(h.window).on("resize."+e.__namespace+"-triggerClose",function(b){var c=a(document.activeElement);(c.is("input")||c.is("textarea"))&&a.contains(e._$tooltip[0],c[0])||e.reposition(b)}).on("scroll."+e.__namespace+"-triggerClose",function(a){e.__scrollHandler(a)}),e.__$originParents=e._$origin.parents(),e.__$originParents.each(function(b,c){a(c).on("scroll."+e.__namespace+"-triggerClose",function(a){e.__scrollHandler(a)})}),e.__options.triggerClose.mouseleave||e.__options.triggerClose.touchleave&&h.hasTouchCapability){e._on("dismissable",function(a){a.dismissable?a.delay?(m=setTimeout(function(){e._close(a.event)},a.delay),e.__timeouts.close.push(m)):e._close(a):clearTimeout(m)});var j=e._$origin,k="",l="",m=null;e.__options.interactive&&(j=j.add(e._$tooltip)),e.__options.triggerClose.mouseleave&&(k+="mouseenter."+e.__namespace+"-triggerClose ",l+="mouseleave."+e.__namespace+"-triggerClose "),e.__options.triggerClose.touchleave&&h.hasTouchCapability&&(k+="touchstart."+e.__namespace+"-triggerClose",l+="touchend."+e.__namespace+"-triggerClose touchcancel."+e.__namespace+"-triggerClose"),j.on(l,function(a){if(e._touchIsTouchEvent(a)||!e._touchIsEmulatedEvent(a)){var b="mouseleave"==a.type?e.__options.delay:e.__options.delayTouch;e._trigger({delay:b[1],dismissable:!0,event:a,type:"dismissable"})}}).on(k,function(a){!e._touchIsTouchEvent(a)&&e._touchIsEmulatedEvent(a)||e._trigger({dismissable:!1,event:a,type:"dismissable"})})}e.__options.triggerClose.originClick&&e._$origin.on("click."+e.__namespace+"-triggerClose",function(a){e._touchIsTouchEvent(a)||e._touchIsEmulatedEvent(a)||e._close(a)}),(e.__options.triggerClose.click||e.__options.triggerClose.tap&&h.hasTouchCapability)&&setTimeout(function(){if("closed"!=e.__state){var b="",c=a(h.window.document.body);e.__options.triggerClose.click&&(b+="click."+e.__namespace+"-triggerClose "),e.__options.triggerClose.tap&&h.hasTouchCapability&&(b+="touchend."+e.__namespace+"-triggerClose"),c.on(b,function(b){e._touchIsMeaningfulEvent(b)&&(e._touchRecordEvent(b),e.__options.interactive&&a.contains(e._$tooltip[0],b.target)||e._close(b))}),e.__options.triggerClose.tap&&h.hasTouchCapability&&c.on("touchstart."+e.__namespace+"-triggerClose",function(a){e._touchRecordEvent(a)})}},0),e._trigger("ready"),e.__options.functionReady&&e.__options.functionReady.call(e,e,{origin:e._$origin[0],tooltip:e._$tooltip[0]})}if(e.__options.timer>0){var m=setTimeout(function(){e._close()},e.__options.timer+g);e.__timeouts.close.push(m)}}}return e},_openShortly:function(a){var b=this,c=!0;if("stable"!=b.__state&&"appearing"!=b.__state&&!b.__timeouts.open&&(b._trigger({type:"start",event:a,stop:function(){c=!1}}),c)){var d=0==a.type.indexOf("touch")?b.__options.delayTouch:b.__options.delay;d[0]?b.__timeouts.open=setTimeout(function(){b.__timeouts.open=null,b.__pointerIsOverOrigin&&b._touchIsMeaningfulEvent(a)?(b._trigger("startend"),b._open(a)):b._trigger("startcancel")},d[0]):(b._trigger("startend"),b._open(a))}return b},_optionsExtract:function(b,c){var d=this,e=a.extend(!0,{},c),f=d.__options[b];return f||(f={},a.each(c,function(a,b){var c=d.__options[a];void 0!==c&&(f[a]=c)})),a.each(e,function(b,c){void 0!==f[b]&&("object"!=typeof c||c instanceof Array||null==c||"object"!=typeof f[b]||f[b]instanceof Array||null==f[b]?e[b]=f[b]:a.extend(e[b],f[b]))}),e},_plug:function(b){var c=a.tooltipster._plugin(b);if(!c)throw new Error('The "'+b+'" plugin is not defined');return c.instance&&a.tooltipster.__bridge(c.instance,this,c.name),this},_touchIsEmulatedEvent:function(a){for(var b=!1,c=(new Date).getTime(),d=this.__touchEvents.length-1;d>=0;d--){var e=this.__touchEvents[d];if(!(c-e.time<500))break;e.target===a.target&&(b=!0)}return b},_touchIsMeaningfulEvent:function(a){return this._touchIsTouchEvent(a)&&!this._touchSwiped(a.target)||!this._touchIsTouchEvent(a)&&!this._touchIsEmulatedEvent(a)},_touchIsTouchEvent:function(a){return 0==a.type.indexOf("touch")},_touchRecordEvent:function(a){return this._touchIsTouchEvent(a)&&(a.time=(new Date).getTime(),this.__touchEvents.push(a)),this},_touchSwiped:function(a){for(var b=!1,c=this.__touchEvents.length-1;c>=0;c--){var d=this.__touchEvents[c];if("touchmove"==d.type){b=!0;break}if("touchstart"==d.type&&a===d.target)break}return b},_trigger:function(){var b=Array.prototype.slice.apply(arguments);return"string"==typeof b[0]&&(b[0]={type:b[0]}),b[0].instance=this,b[0].origin=this._$origin?this._$origin[0]:null,b[0].tooltip=this._$tooltip?this._$tooltip[0]:null,this.__$emitterPrivate.trigger.apply(this.__$emitterPrivate,b),a.tooltipster._trigger.apply(a.tooltipster,b),this.__$emitterPublic.trigger.apply(this.__$emitterPublic,b),this},_unplug:function(b){var c=this;if(c[b]){var d=a.tooltipster._plugin(b);d.instance&&a.each(d.instance,function(a,d){c[a]&&c[a].bridged===c[b]&&delete c[a]}),c[b].__destroy&&c[b].__destroy(),delete c[b]}return c},close:function(a){return this.__destroyed?this.__destroyError():this._close(null,a),this},content:function(a){var b=this;if(void 0===a)return b.__Content;if(b.__destroyed)b.__destroyError();else if(b.__contentSet(a),null!==b.__Content){if("closed"!==b.__state&&(b.__contentInsert(),b.reposition(),b.__options.updateAnimation))if(h.hasTransitions){var c=b.__options.updateAnimation;b._$tooltip.addClass("tooltipster-update-"+c),setTimeout(function(){"closed"!=b.__state&&b._$tooltip.removeClass("tooltipster-update-"+c)},1e3)}else b._$tooltip.fadeTo(200,.5,function(){"closed"!=b.__state&&b._$tooltip.fadeTo(200,1)})}else b._close();return b},destroy:function(){var b=this;if(b.__destroyed)b.__destroyError();else{"closed"!=b.__state?b.option("animationDuration",0)._close(null,null,!0):b.__timeoutsClear(),b._trigger("destroy"),b.__destroyed=!0,b._$origin.removeData(b.__namespace).off("."+b.__namespace+"-triggerOpen"),a(h.window.document.body).off("."+b.__namespace+"-triggerOpen");var c=b._$origin.data("tooltipster-ns");if(c)if(1===c.length){var d=null;"previous"==b.__options.restoration?d=b._$origin.data("tooltipster-initialTitle"):"current"==b.__options.restoration&&(d="string"==typeof b.__Content?b.__Content:a("
    ").append(b.__Content).html()),d&&b._$origin.attr("title",d),b._$origin.removeClass("tooltipstered"),b._$origin.removeData("tooltipster-ns").removeData("tooltipster-initialTitle")}else c=a.grep(c,function(a,c){return a!==b.__namespace}),b._$origin.data("tooltipster-ns",c);b._trigger("destroyed"),b._off(),b.off(),b.__Content=null,b.__$emitterPrivate=null,b.__$emitterPublic=null,b.__options.parent=null,b._$origin=null,b._$tooltip=null,a.tooltipster.__instancesLatestArr=a.grep(a.tooltipster.__instancesLatestArr,function(a,c){return b!==a}),clearInterval(b.__garbageCollector)}return b},disable:function(){return this.__destroyed?(this.__destroyError(),this):(this._close(),this.__enabled=!1,this)},elementOrigin:function(){return this.__destroyed?void this.__destroyError():this._$origin[0]},elementTooltip:function(){return this._$tooltip?this._$tooltip[0]:null},enable:function(){return this.__enabled=!0,this},hide:function(a){return this.close(a)},instance:function(){return this},off:function(){return this.__destroyed||this.__$emitterPublic.off.apply(this.__$emitterPublic,Array.prototype.slice.apply(arguments)),this},on:function(){return this.__destroyed?this.__destroyError():this.__$emitterPublic.on.apply(this.__$emitterPublic,Array.prototype.slice.apply(arguments)),this},one:function(){return this.__destroyed?this.__destroyError():this.__$emitterPublic.one.apply(this.__$emitterPublic,Array.prototype.slice.apply(arguments)),this},open:function(a){return this.__destroyed?this.__destroyError():this._open(null,a),this},option:function(b,c){return void 0===c?this.__options[b]:(this.__destroyed?this.__destroyError():(this.__options[b]=c,this.__optionsFormat(),a.inArray(b,["trigger","triggerClose","triggerOpen"])>=0&&this.__prepareOrigin(),"selfDestruction"===b&&this.__prepareGC()),this)},reposition:function(a,b){var c=this;return c.__destroyed?c.__destroyError():"closed"!=c.__state&&d(c._$origin)&&(b||d(c._$tooltip))&&(b||c._$tooltip.detach(),c.__Geometry=c.__geometry(),c._trigger({type:"reposition",event:a,helper:{geo:c.__Geometry}})),c},show:function(a){return this.open(a)},status:function(){return{destroyed:this.__destroyed,enabled:this.__enabled,open:"closed"!==this.__state,state:this.__state}},triggerHandler:function(){return this.__destroyed?this.__destroyError():this.__$emitterPublic.triggerHandler.apply(this.__$emitterPublic,Array.prototype.slice.apply(arguments)),this}},a.fn.tooltipster=function(){var b=Array.prototype.slice.apply(arguments),c="You are using a single HTML element as content for several tooltips. You probably want to set the contentCloning option to TRUE.";if(0===this.length)return this;if("string"==typeof b[0]){var d="#*$~&";return this.each(function(){var e=a(this).data("tooltipster-ns"),f=e?a(this).data(e[0]):null;if(!f)throw new Error("You called Tooltipster's \""+b[0]+'" method on an uninitialized element');if("function"!=typeof f[b[0]])throw new Error('Unknown method "'+b[0]+'"');this.length>1&&"content"==b[0]&&(b[1]instanceof a||"object"==typeof b[1]&&null!=b[1]&&b[1].tagName)&&!f.__options.contentCloning&&f.__options.debug&&console.log(c);var g=f[b[0]](b[1],b[2]);return g!==f||"instance"===b[0]?(d=g,!1):void 0}),"#*$~&"!==d?d:this}a.tooltipster.__instancesLatestArr=[];var e=b[0]&&void 0!==b[0].multiple,g=e&&b[0].multiple||!e&&f.multiple,h=b[0]&&void 0!==b[0].content,i=h&&b[0].content||!h&&f.content,j=b[0]&&void 0!==b[0].contentCloning,k=j&&b[0].contentCloning||!j&&f.contentCloning,l=b[0]&&void 0!==b[0].debug,m=l&&b[0].debug||!l&&f.debug;return this.length>1&&(i instanceof a||"object"==typeof i&&null!=i&&i.tagName)&&!k&&m&&console.log(c),this.each(function(){var c=!1,d=a(this),e=d.data("tooltipster-ns"),f=null;e?g?c=!0:m&&(console.log("Tooltipster: one or more tooltips are already attached to the element below. Ignoring."),console.log(this)):c=!0,c&&(f=new a.Tooltipster(this,b[0]),e||(e=[]),e.push(f.__namespace),d.data("tooltipster-ns",e),d.data(f.__namespace,f),f.__options.functionInit&&f.__options.functionInit.call(f,f,{origin:this}),f._trigger("init")),a.tooltipster.__instancesLatestArr.push(f)}),this},b.prototype={__init:function(b){this.__$tooltip=b,this.__$tooltip.css({left:0,overflow:"hidden",position:"absolute",top:0}).find(".tooltipster-content").css("overflow","auto"),this.$container=a('
    ').append(this.__$tooltip).appendTo(h.window.document.body)},__forceRedraw:function(){var a=this.__$tooltip.parent();this.__$tooltip.detach(),this.__$tooltip.appendTo(a)},constrain:function(a,b){return this.constraints={width:a,height:b},this.__$tooltip.css({display:"block",height:"",overflow:"auto",width:a}),this},destroy:function(){this.__$tooltip.detach().find(".tooltipster-content").css({display:"",overflow:""}),this.$container.remove()},free:function(){return this.constraints=null,this.__$tooltip.css({display:"",height:"",overflow:"visible",width:""}),this},measure:function(){this.__forceRedraw();var a=this.__$tooltip[0].getBoundingClientRect(),b={size:{height:a.height||a.bottom-a.top,width:a.width||a.right-a.left}};if(this.constraints){var c=this.__$tooltip.find(".tooltipster-content"),d=this.__$tooltip.outerHeight(),e=c[0].getBoundingClientRect(),f={height:d<=this.constraints.height,width:a.width<=this.constraints.width&&e.width>=c[0].scrollWidth-1};b.fits=f.height&&f.width}return h.IE&&h.IE<=11&&b.size.width!==h.window.document.documentElement.clientWidth&&(b.size.width=Math.ceil(b.size.width)+1),b}};var j=navigator.userAgent.toLowerCase();-1!=j.indexOf("msie")?h.IE=parseInt(j.split("msie")[1]):-1!==j.toLowerCase().indexOf("trident")&&-1!==j.indexOf(" rv:11")?h.IE=11:-1!=j.toLowerCase().indexOf("edge/")&&(h.IE=parseInt(j.toLowerCase().split("edge/")[1]));var k="tooltipster.sideTip";return a.tooltipster._plugin({name:k,instance:{__defaults:function(){return{arrow:!0,distance:6,functionPosition:null,maxWidth:null,minIntersection:16,minWidth:0,position:null,side:"top",viewportAware:!0}},__init:function(a){var b=this;b.__instance=a,b.__namespace="tooltipster-sideTip-"+Math.round(1e6*Math.random()),b.__previousState="closed",b.__options,b.__optionsFormat(),b.__instance._on("state."+b.__namespace,function(a){"closed"==a.state?b.__close():"appearing"==a.state&&"closed"==b.__previousState&&b.__create(),b.__previousState=a.state}),b.__instance._on("options."+b.__namespace,function(){b.__optionsFormat()}),b.__instance._on("reposition."+b.__namespace,function(a){b.__reposition(a.event,a.helper)})},__close:function(){this.__instance.content()instanceof a&&this.__instance.content().detach(),this.__instance._$tooltip.remove(),this.__instance._$tooltip=null},__create:function(){var b=a('
    ');this.__options.arrow||b.find(".tooltipster-box").css("margin",0).end().find(".tooltipster-arrow").hide(),this.__options.minWidth&&b.css("min-width",this.__options.minWidth+"px"),this.__options.maxWidth&&b.css("max-width",this.__options.maxWidth+"px"), +-this.__instance._$tooltip=b,this.__instance._trigger("created")},__destroy:function(){this.__instance._off("."+self.__namespace)},__optionsFormat:function(){var b=this;if(b.__options=b.__instance._optionsExtract(k,b.__defaults()),b.__options.position&&(b.__options.side=b.__options.position),"object"!=typeof b.__options.distance&&(b.__options.distance=[b.__options.distance]),b.__options.distance.length<4&&(void 0===b.__options.distance[1]&&(b.__options.distance[1]=b.__options.distance[0]),void 0===b.__options.distance[2]&&(b.__options.distance[2]=b.__options.distance[0]),void 0===b.__options.distance[3]&&(b.__options.distance[3]=b.__options.distance[1])),b.__options.distance={top:b.__options.distance[0],right:b.__options.distance[1],bottom:b.__options.distance[2],left:b.__options.distance[3]},"string"==typeof b.__options.side){var c={top:"bottom",right:"left",bottom:"top",left:"right"};b.__options.side=[b.__options.side,c[b.__options.side]],"left"==b.__options.side[0]||"right"==b.__options.side[0]?b.__options.side.push("top","bottom"):b.__options.side.push("right","left")}6===a.tooltipster._env.IE&&b.__options.arrow!==!0&&(b.__options.arrow=!1)},__reposition:function(b,c){var d,e=this,f=e.__targetFind(c),g=[];e.__instance._$tooltip.detach();var h=e.__instance._$tooltip.clone(),i=a.tooltipster._getRuler(h),j=!1,k=e.__instance.option("animation");switch(k&&h.removeClass("tooltipster-"+k),a.each(["window","document"],function(d,k){var l=null;if(e.__instance._trigger({container:k,helper:c,satisfied:j,takeTest:function(a){l=a},results:g,type:"positionTest"}),1==l||0!=l&&0==j&&("window"!=k||e.__options.viewportAware))for(var d=0;d=h.outerSize.width&&c.geo.available[k][n].height>=h.outerSize.height?h.fits=!0:h.fits=!1:h.fits=p.fits,"window"==k&&(h.fits?"top"==n||"bottom"==n?h.whole=c.geo.origin.windowOffset.right>=e.__options.minIntersection&&c.geo.window.size.width-c.geo.origin.windowOffset.left>=e.__options.minIntersection:h.whole=c.geo.origin.windowOffset.bottom>=e.__options.minIntersection&&c.geo.window.size.height-c.geo.origin.windowOffset.top>=e.__options.minIntersection:h.whole=!1),g.push(h),h.whole)j=!0;else if("natural"==h.mode&&(h.fits||h.size.width<=c.geo.available[k][n].width))return!1}})}}),e.__instance._trigger({edit:function(a){g=a},event:b,helper:c,results:g,type:"positionTested"}),g.sort(function(a,b){if(a.whole&&!b.whole)return-1;if(!a.whole&&b.whole)return 1;if(a.whole&&b.whole){var c=e.__options.side.indexOf(a.side),d=e.__options.side.indexOf(b.side);return d>c?-1:c>d?1:"natural"==a.mode?-1:1}if(a.fits&&!b.fits)return-1;if(!a.fits&&b.fits)return 1;if(a.fits&&b.fits){var c=e.__options.side.indexOf(a.side),d=e.__options.side.indexOf(b.side);return d>c?-1:c>d?1:"natural"==a.mode?-1:1}return"document"==a.container&&"bottom"==a.side&&"natural"==a.mode?-1:1}),d=g[0],d.coord={},d.side){case"left":case"right":d.coord.top=Math.floor(d.target-d.size.height/2);break;case"bottom":case"top":d.coord.left=Math.floor(d.target-d.size.width/2)}switch(d.side){case"left":d.coord.left=c.geo.origin.windowOffset.left-d.outerSize.width;break;case"right":d.coord.left=c.geo.origin.windowOffset.right+d.distance.horizontal;break;case"top":d.coord.top=c.geo.origin.windowOffset.top-d.outerSize.height;break;case"bottom":d.coord.top=c.geo.origin.windowOffset.bottom+d.distance.vertical}"window"==d.container?"top"==d.side||"bottom"==d.side?d.coord.left<0?c.geo.origin.windowOffset.right-this.__options.minIntersection>=0?d.coord.left=0:d.coord.left=c.geo.origin.windowOffset.right-this.__options.minIntersection-1:d.coord.left>c.geo.window.size.width-d.size.width&&(c.geo.origin.windowOffset.left+this.__options.minIntersection<=c.geo.window.size.width?d.coord.left=c.geo.window.size.width-d.size.width:d.coord.left=c.geo.origin.windowOffset.left+this.__options.minIntersection+1-d.size.width):d.coord.top<0?c.geo.origin.windowOffset.bottom-this.__options.minIntersection>=0?d.coord.top=0:d.coord.top=c.geo.origin.windowOffset.bottom-this.__options.minIntersection-1:d.coord.top>c.geo.window.size.height-d.size.height&&(c.geo.origin.windowOffset.top+this.__options.minIntersection<=c.geo.window.size.height?d.coord.top=c.geo.window.size.height-d.size.height:d.coord.top=c.geo.origin.windowOffset.top+this.__options.minIntersection+1-d.size.height):(d.coord.left>c.geo.window.size.width-d.size.width&&(d.coord.left=c.geo.window.size.width-d.size.width),d.coord.left<0&&(d.coord.left=0)),e.__sideChange(h,d.side),c.tooltipClone=h[0],c.tooltipParent=e.__instance.option("parent").parent[0],c.mode=d.mode,c.whole=d.whole,c.origin=e.__instance._$origin[0],c.tooltip=e.__instance._$tooltip[0],delete d.container,delete d.fits,delete d.mode,delete d.outerSize,delete d.whole,d.distance=d.distance.horizontal||d.distance.vertical;var l=a.extend(!0,{},d);if(e.__instance._trigger({edit:function(a){d=a},event:b,helper:c,position:l,type:"position"}),e.__options.functionPosition){var m=e.__options.functionPosition.call(e,e.__instance,c,l);m&&(d=m)}i.destroy();var n,o;"top"==d.side||"bottom"==d.side?(n={prop:"left",val:d.target-d.coord.left},o=d.size.width-this.__options.minIntersection):(n={prop:"top",val:d.target-d.coord.top},o=d.size.height-this.__options.minIntersection),n.valo&&(n.val=o);var p;p=c.geo.origin.fixedLineage?c.geo.origin.windowOffset:{left:c.geo.origin.windowOffset.left+c.geo.window.scroll.left,top:c.geo.origin.windowOffset.top+c.geo.window.scroll.top},d.coord={left:p.left+(d.coord.left-c.geo.origin.windowOffset.left),top:p.top+(d.coord.top-c.geo.origin.windowOffset.top)},e.__sideChange(e.__instance._$tooltip,d.side),c.geo.origin.fixedLineage?e.__instance._$tooltip.css("position","fixed"):e.__instance._$tooltip.css("position",""),e.__instance._$tooltip.css({left:d.coord.left,top:d.coord.top,height:d.size.height,width:d.size.width}).find(".tooltipster-arrow").css({left:"",top:""}).css(n.prop,n.val),e.__instance._$tooltip.appendTo(e.__instance.option("parent")),e.__instance._trigger({type:"repositioned",event:b,position:d})},__sideChange:function(a,b){a.removeClass("tooltipster-bottom").removeClass("tooltipster-left").removeClass("tooltipster-right").removeClass("tooltipster-top").addClass("tooltipster-"+b)},__targetFind:function(a){var b={},c=this.__instance._$origin[0].getClientRects();if(c.length>1){var d=this.__instance._$origin.css("opacity");1==d&&(this.__instance._$origin.css("opacity",.99),c=this.__instance._$origin[0].getClientRects(),this.__instance._$origin.css("opacity",1))}if(c.length<2)b.top=Math.floor(a.geo.origin.windowOffset.left+a.geo.origin.size.width/2),b.bottom=b.top,b.left=Math.floor(a.geo.origin.windowOffset.top+a.geo.origin.size.height/2),b.right=b.left;else{var e=c[0];b.top=Math.floor(e.left+(e.right-e.left)/2),e=c.length>2?c[Math.ceil(c.length/2)-1]:c[0],b.right=Math.floor(e.top+(e.bottom-e.top)/2),e=c[c.length-1],b.bottom=Math.floor(e.left+(e.right-e.left)/2),e=c.length>2?c[Math.ceil((c.length+1)/2)-1]:c[c.length-1],b.left=Math.floor(e.top+(e.bottom-e.top)/2)}return b}}}),a}); +\ No newline at end of file ++/*! tooltipster v4.2.8 */!function(a,b){void 0===a&&void 0!==window&&(a=window),"function"==typeof define&&define.amd?define(["jquery"],function(a){return b(a)}):"object"==typeof module&&module.exports?module.exports=b(require("jquery")):b(a.jQuery)}(this,function(a){function b(a){this.$container,this.constraints=null,this.__$tooltip,this.__init(a)}function c(b,c){var d=!0;return a.each(b,function(a,e){if(void 0===c[a]||b[a]!==c[a])return d=!1,!1}),d}function d(b){var c=b.attr("id"),d=c?h.window.document.getElementById(c):null;return d?d===b[0]:a.contains(h.window.document.body,b[0])}function e(){if(!g)return!1;var a=g.document.body||g.document.documentElement,b=a.style,c="transition",d=["Moz","Webkit","Khtml","O","ms"];if("string"==typeof b[c])return!0;c=c.charAt(0).toUpperCase()+c.substr(1);for(var e=0;e0?e=c.__plugins[d]:a.each(c.__plugins,function(a,b){if(b.name.substring(b.name.length-d.length-1)=="."+d)return e=b,!1}),e}if(b.name.indexOf(".")<0)throw new Error("Plugins must be namespaced");return c.__plugins[b.name]=b,b.core&&c.__bridge(b.core,c,b.name),this},_trigger:function(){var a=Array.prototype.slice.apply(arguments);return"string"==typeof a[0]&&(a[0]={type:a[0]}),this.__$emitterPrivate.trigger.apply(this.__$emitterPrivate,a),this.__$emitterPublic.trigger.apply(this.__$emitterPublic,a),this},instances:function(b){var c=[],d=b||".tooltipstered";return a(d).each(function(){var b=a(this),d=b.data("tooltipster-ns");d&&a.each(d,function(a,d){c.push(b.data(d))})}),c},instancesLatest:function(){return this.__instancesLatestArr},off:function(){return this.__$emitterPublic.off.apply(this.__$emitterPublic,Array.prototype.slice.apply(arguments)),this},on:function(){return this.__$emitterPublic.on.apply(this.__$emitterPublic,Array.prototype.slice.apply(arguments)),this},one:function(){return this.__$emitterPublic.one.apply(this.__$emitterPublic,Array.prototype.slice.apply(arguments)),this},origins:function(b){var c=b?b+" ":"";return a(c+".tooltipstered").toArray()},setDefaults:function(b){return a.extend(f,b),this},triggerHandler:function(){return this.__$emitterPublic.triggerHandler.apply(this.__$emitterPublic,Array.prototype.slice.apply(arguments)),this}},a.tooltipster=new i,a.Tooltipster=function(b,c){this.__callbacks={close:[],open:[]},this.__closingTime,this.__Content,this.__contentBcr,this.__destroyed=!1,this.__$emitterPrivate=a({}),this.__$emitterPublic=a({}),this.__enabled=!0,this.__garbageCollector,this.__Geometry,this.__lastPosition,this.__namespace="tooltipster-"+Math.round(1e6*Math.random()),this.__options,this.__$originParents,this.__pointerIsOverOrigin=!1,this.__previousThemes=[],this.__state="closed",this.__timeouts={close:[],open:null},this.__touchEvents=[],this.__tracker=null,this._$origin,this._$tooltip,this.__init(b,c)},a.Tooltipster.prototype={__init:function(b,c){var d=this;if(d._$origin=a(b),d.__options=a.extend(!0,{},f,c),d.__optionsFormat(),!h.IE||h.IE>=d.__options.IEmin){var e=null;if(void 0===d._$origin.data("tooltipster-initialTitle")&&(e=d._$origin.attr("title"),void 0===e&&(e=null),d._$origin.data("tooltipster-initialTitle",e)),null!==d.__options.content)d.__contentSet(d.__options.content);else{var g,i=d._$origin.attr("data-tooltip-content");i&&(g=a(i)),g&&g[0]?d.__contentSet(g.first()):d.__contentSet(e)}d._$origin.removeAttr("title").addClass("tooltipstered"),d.__prepareOrigin(),d.__prepareGC(),a.each(d.__options.plugins,function(a,b){d._plug(b)}),h.hasTouchCapability&&a(h.window.document.body).on("touchmove."+d.__namespace+"-triggerOpen",function(a){d._touchRecordEvent(a)}),d._on("created",function(){d.__prepareTooltip()})._on("repositioned",function(a){d.__lastPosition=a.position})}else d.__options.disabled=!0},__contentInsert:function(){var a=this,b=a._$tooltip.find(".tooltipster-content"),c=a.__Content,d=function(a){c=a};return a._trigger({type:"format",content:a.__Content,format:d}),a.__options.functionFormat&&(c=a.__options.functionFormat.call(a,a,{origin:a._$origin[0]},a.__Content)),"string"!=typeof c||a.__options.contentAsHTML?b.empty().append(c):b.text(c),a},__contentSet:function(b){return b instanceof a&&this.__options.contentCloning&&(b=b.clone(!0)),this.__Content=b,this._trigger({type:"updated",content:b}),this},__destroyError:function(){throw new Error("This tooltip has been destroyed and cannot execute your method call.")},__geometry:function(){var b=this,c=b._$origin,d=b._$origin.is("area");if(d){var e=b._$origin.parent().attr("name");c=a('img[usemap="#'+e+'"]')}var f=c[0].getBoundingClientRect(),g=a(h.window.document),i=a(h.window),j=c,k={available:{document:null,window:null},document:{size:{height:g.height(),width:g.width()}},window:{scroll:{left:h.window.scrollX||h.window.document.documentElement.scrollLeft,top:h.window.scrollY||h.window.document.documentElement.scrollTop},size:{height:i.height(),width:i.width()}},origin:{fixedLineage:!1,offset:{},size:{height:f.bottom-f.top,width:f.right-f.left},usemapImage:d?c[0]:null,windowOffset:{bottom:f.bottom,left:f.left,right:f.right,top:f.top}}};if(d){var l=b._$origin.attr("shape"),m=b._$origin.attr("coords");if(m&&(m=m.split(","),a.map(m,function(a,b){m[b]=parseInt(a)})),"default"!=l)switch(l){case"circle":var n=m[0],o=m[1],p=m[2],q=o-p,r=n-p;k.origin.size.height=2*p,k.origin.size.width=k.origin.size.height,k.origin.windowOffset.left+=r,k.origin.windowOffset.top+=q;break;case"rect":var s=m[0],t=m[1],u=m[2],v=m[3];k.origin.size.height=v-t,k.origin.size.width=u-s,k.origin.windowOffset.left+=s,k.origin.windowOffset.top+=t;break;case"poly":for(var w=0,x=0,y=0,z=0,A="even",B=0;By&&(y=C,0===B&&(w=y)),Cz&&(z=C,1==B&&(x=z)),C6e4}),d(b._$origin)||b.close(function(){b.destroy()})},2e4):clearInterval(b.__garbageCollector),b},__prepareOrigin:function(){var a=this;if(a._$origin.off("."+a.__namespace+"-triggerOpen"),h.hasTouchCapability&&a._$origin.on("touchstart."+a.__namespace+"-triggerOpen touchend."+a.__namespace+"-triggerOpen touchcancel."+a.__namespace+"-triggerOpen",function(b){a._touchRecordEvent(b)}),a.__options.triggerOpen.click||a.__options.triggerOpen.tap&&h.hasTouchCapability){var b="";a.__options.triggerOpen.click&&(b+="click."+a.__namespace+"-triggerOpen "),a.__options.triggerOpen.tap&&h.hasTouchCapability&&(b+="touchend."+a.__namespace+"-triggerOpen"),a._$origin.on(b,function(b){a._touchIsMeaningfulEvent(b)&&a._open(b)})}if(a.__options.triggerOpen.mouseenter||a.__options.triggerOpen.touchstart&&h.hasTouchCapability){var b="";a.__options.triggerOpen.mouseenter&&(b+="mouseenter."+a.__namespace+"-triggerOpen "),a.__options.triggerOpen.touchstart&&h.hasTouchCapability&&(b+="touchstart."+a.__namespace+"-triggerOpen"),a._$origin.on(b,function(b){!a._touchIsTouchEvent(b)&&a._touchIsEmulatedEvent(b)||(a.__pointerIsOverOrigin=!0,a._openShortly(b))})}if(a.__options.triggerClose.mouseleave||a.__options.triggerClose.touchleave&&h.hasTouchCapability){var b="";a.__options.triggerClose.mouseleave&&(b+="mouseleave."+a.__namespace+"-triggerOpen "),a.__options.triggerClose.touchleave&&h.hasTouchCapability&&(b+="touchend."+a.__namespace+"-triggerOpen touchcancel."+a.__namespace+"-triggerOpen"),a._$origin.on(b,function(b){a._touchIsMeaningfulEvent(b)&&(a.__pointerIsOverOrigin=!1)})}return a},__prepareTooltip:function(){var b=this,c=b.__options.interactive?"auto":"";return b._$tooltip.attr("id",b.__namespace).css({"pointer-events":c,zIndex:b.__options.zIndex}),a.each(b.__previousThemes,function(a,c){b._$tooltip.removeClass(c)}),a.each(b.__options.theme,function(a,c){b._$tooltip.addClass(c)}),b.__previousThemes=a.merge([],b.__options.theme),b},__scrollHandler:function(b){var c=this;if(c.__options.triggerClose.scroll)c._close(b);else if(d(c._$origin)&&d(c._$tooltip)){var e=null;if(b.target===h.window.document)c.__Geometry.origin.fixedLineage||c.__options.repositionOnScroll&&c.reposition(b);else{e=c.__geometry();var f=!1;if("fixed"!=c._$origin.css("position")&&c.__$originParents.each(function(b,d){var g=a(d),h=g.css("overflow-x"),i=g.css("overflow-y");if("visible"!=h||"visible"!=i){var j=d.getBoundingClientRect();if("visible"!=h&&(e.origin.windowOffset.leftj.right))return f=!0,!1;if("visible"!=i&&(e.origin.windowOffset.topj.bottom)&&(f=!c.__options.checkOverflowY||c.__options.checkOverflowY(e,j)))return!1}if("fixed"==g.css("position"))return!1}),f)c._$tooltip.css("visibility","hidden");else if(c._$tooltip.css("visibility","visible"),c.__options.repositionOnScroll)c.reposition(b);else{var g=e.origin.offset.left-c.__Geometry.origin.offset.left,i=e.origin.offset.top-c.__Geometry.origin.offset.top;c._$tooltip.css({left:c.__lastPosition.coord.left+g,top:c.__lastPosition.coord.top+i})}}c._trigger({type:"scroll",event:b,geo:e})}return c},__stateSet:function(a){return this.__state=a,this._trigger({type:"state",state:a}),this},__timeoutsClear:function(){return clearTimeout(this.__timeouts.open),this.__timeouts.open=null,a.each(this.__timeouts.close,function(a,b){clearTimeout(b)}),this.__timeouts.close=[],this},__trackerStart:function(){var a=this,b=a._$tooltip.find(".tooltipster-content");return a.__options.trackTooltip&&(a.__contentBcr=b[0].getBoundingClientRect()),a.__tracker=setInterval(function(){if(d(a._$origin)&&d(a._$tooltip)){if(a.__options.trackOrigin){var e=a.__geometry(),f=!1;c(e.origin.size,a.__Geometry.origin.size)&&(a.__Geometry.origin.fixedLineage?c(e.origin.windowOffset,a.__Geometry.origin.windowOffset)&&(f=!0):c(e.origin.offset,a.__Geometry.origin.offset)&&(f=!0)),f||(a.__options.triggerClose.mouseleave&&!a.__options.ignoreCloseOnScroll?a._close():a.reposition())}if(a.__options.trackTooltip){var g=b[0].getBoundingClientRect();g.height===a.__contentBcr.height&&g.width===a.__contentBcr.width||(a.reposition(),a.__contentBcr=g)}}else a._close()},a.__options.trackerInterval),a},_close:function(b,c,d){var e=this,f=!0;if(e._trigger({type:"close",event:b,stop:function(){f=!1}}),f||d){c&&e.__callbacks.close.push(c),e.__callbacks.open=[],e.__timeoutsClear();var g=function(){a.each(e.__callbacks.close,function(a,c){c.call(e,e,{event:b,origin:e._$origin[0]})}),e.__callbacks.close=[]};if("closed"!=e.__state){var i=!0,j=new Date,k=j.getTime(),l=k+e.__options.animationDuration[1];if("disappearing"==e.__state&&l>e.__closingTime&&e.__options.animationDuration[1]>0&&(i=!1),i){e.__closingTime=l,"disappearing"!=e.__state&&e.__stateSet("disappearing");var m=function(){clearInterval(e.__tracker),e._trigger({type:"closing",event:b}),e._$tooltip.off("."+e.__namespace+"-triggerClose").removeClass("tooltipster-dying"),a(h.window).off("."+e.__namespace+"-triggerClose"),e.__$originParents.each(function(b,c){a(c).off("scroll."+e.__namespace+"-triggerClose")}),e.__$originParents=null,a(h.window.document.body).off("."+e.__namespace+"-triggerClose"),e._$origin.off("."+e.__namespace+"-triggerClose"),e._off("dismissable"),e.__stateSet("closed"),e._trigger({type:"after",event:b}),e.__options.functionAfter&&e.__options.functionAfter.call(e,e,{event:b,origin:e._$origin[0]}),g()};h.hasTransitions?(e._$tooltip.css({"-moz-animation-duration":e.__options.animationDuration[1]+"ms","-ms-animation-duration":e.__options.animationDuration[1]+"ms","-o-animation-duration":e.__options.animationDuration[1]+"ms","-webkit-animation-duration":e.__options.animationDuration[1]+"ms","animation-duration":e.__options.animationDuration[1]+"ms","transition-duration":e.__options.animationDuration[1]+"ms"}),e._$tooltip.clearQueue().removeClass("tooltipster-show").addClass("tooltipster-dying"),e.__options.animationDuration[1]>0&&e._$tooltip.delay(e.__options.animationDuration[1]),e._$tooltip.queue(m)):e._$tooltip.stop().fadeOut(e.__options.animationDuration[1],m)}}else g()}return e},_off:function(){return this.__$emitterPrivate.off.apply(this.__$emitterPrivate,Array.prototype.slice.apply(arguments)),this},_on:function(){return this.__$emitterPrivate.on.apply(this.__$emitterPrivate,Array.prototype.slice.apply(arguments)),this},_one:function(){return this.__$emitterPrivate.one.apply(this.__$emitterPrivate,Array.prototype.slice.apply(arguments)),this},_open:function(b,c){var e=this;if(!e.__destroying&&d(e._$origin)&&e.__enabled){var f=!0;if("closed"==e.__state&&(e._trigger({type:"before",event:b,stop:function(){f=!1}}),f&&e.__options.functionBefore&&(f=e.__options.functionBefore.call(e,e,{event:b,origin:e._$origin[0]}))),f!==!1&&null!==e.__Content){c&&e.__callbacks.open.push(c),e.__callbacks.close=[],e.__timeoutsClear();var g,i=function(){"stable"!=e.__state&&e.__stateSet("stable"),a.each(e.__callbacks.open,function(a,b){b.call(e,e,{origin:e._$origin[0],tooltip:e._$tooltip[0]})}),e.__callbacks.open=[]};if("closed"!==e.__state)g=0,"disappearing"===e.__state?(e.__stateSet("appearing"),h.hasTransitions?(e._$tooltip.clearQueue().removeClass("tooltipster-dying").addClass("tooltipster-show"),e.__options.animationDuration[0]>0&&e._$tooltip.delay(e.__options.animationDuration[0]),e._$tooltip.queue(i)):e._$tooltip.stop().fadeIn(i)):"stable"==e.__state&&i();else{if(e.__stateSet("appearing"),g=e.__options.animationDuration[0],e.__contentInsert(),e.reposition(b,!0),h.hasTransitions?(e._$tooltip.addClass("tooltipster-"+e.__options.animation).addClass("tooltipster-initial").css({"-moz-animation-duration":e.__options.animationDuration[0]+"ms","-ms-animation-duration":e.__options.animationDuration[0]+"ms","-o-animation-duration":e.__options.animationDuration[0]+"ms","-webkit-animation-duration":e.__options.animationDuration[0]+"ms","animation-duration":e.__options.animationDuration[0]+"ms","transition-duration":e.__options.animationDuration[0]+"ms"}),setTimeout(function(){"closed"!=e.__state&&(e._$tooltip.addClass("tooltipster-show").removeClass("tooltipster-initial"),e.__options.animationDuration[0]>0&&e._$tooltip.delay(e.__options.animationDuration[0]),e._$tooltip.queue(i))},0)):e._$tooltip.css("display","none").fadeIn(e.__options.animationDuration[0],i),e.__trackerStart(),a(h.window).on("resize."+e.__namespace+"-triggerClose",function(b){var c=a(document.activeElement);(c.is("input")||c.is("textarea"))&&a.contains(e._$tooltip[0],c[0])||e.reposition(b)}).on("scroll."+e.__namespace+"-triggerClose",function(a){e.__scrollHandler(a)}),e.__$originParents=e._$origin.parents(),e.__$originParents.each(function(b,c){a(c).on("scroll."+e.__namespace+"-triggerClose",function(a){e.__scrollHandler(a)})}),e.__options.triggerClose.mouseleave||e.__options.triggerClose.touchleave&&h.hasTouchCapability){e._on("dismissable",function(a){a.dismissable?a.delay?(m=setTimeout(function(){e._close(a.event)},a.delay),e.__timeouts.close.push(m)):e._close(a):clearTimeout(m)});var j=e._$origin,k="",l="",m=null;e.__options.interactive&&(j=j.add(e._$tooltip)),e.__options.triggerClose.mouseleave&&(k+="mouseenter."+e.__namespace+"-triggerClose ",l+="mouseleave."+e.__namespace+"-triggerClose "),e.__options.triggerClose.touchleave&&h.hasTouchCapability&&(k+="touchstart."+e.__namespace+"-triggerClose",l+="touchend."+e.__namespace+"-triggerClose touchcancel."+e.__namespace+"-triggerClose"),j.on(l,function(a){if(e._touchIsTouchEvent(a)||!e._touchIsEmulatedEvent(a)){var b="mouseleave"==a.type?e.__options.delay:e.__options.delayTouch;e._trigger({delay:b[1],dismissable:!0,event:a,type:"dismissable"})}}).on(k,function(a){!e._touchIsTouchEvent(a)&&e._touchIsEmulatedEvent(a)||e._trigger({dismissable:!1,event:a,type:"dismissable"})})}e.__options.triggerClose.originClick&&e._$origin.on("click."+e.__namespace+"-triggerClose",function(a){e._touchIsTouchEvent(a)||e._touchIsEmulatedEvent(a)||e._close(a)}),(e.__options.triggerClose.click||e.__options.triggerClose.tap&&h.hasTouchCapability)&&setTimeout(function(){if("closed"!=e.__state){var b="",c=a(h.window.document.body);e.__options.triggerClose.click&&(b+="click."+e.__namespace+"-triggerClose "),e.__options.triggerClose.tap&&h.hasTouchCapability&&(b+="touchend."+e.__namespace+"-triggerClose"),c.on(b,function(b){e._touchIsMeaningfulEvent(b)&&(e._touchRecordEvent(b),e.__options.interactive&&a.contains(e._$tooltip[0],b.target)||e._close(b))}),e.__options.triggerClose.tap&&h.hasTouchCapability&&c.on("touchstart."+e.__namespace+"-triggerClose",function(a){e._touchRecordEvent(a)})}},0),e._trigger("ready"),e.__options.functionReady&&e.__options.functionReady.call(e,e,{origin:e._$origin[0],tooltip:e._$tooltip[0]})}if(e.__options.timer>0){var m=setTimeout(function(){e._close()},e.__options.timer+g);e.__timeouts.close.push(m)}}}return e},_openShortly:function(a){var b=this,c=!0;if("stable"!=b.__state&&"appearing"!=b.__state&&!b.__timeouts.open&&(b._trigger({type:"start",event:a,stop:function(){c=!1}}),c)){var d=0==a.type.indexOf("touch")?b.__options.delayTouch:b.__options.delay;d[0]?b.__timeouts.open=setTimeout(function(){b.__timeouts.open=null,b.__pointerIsOverOrigin&&b._touchIsMeaningfulEvent(a)?(b._trigger("startend"),b._open(a)):b._trigger("startcancel")},d[0]):(b._trigger("startend"),b._open(a))}return b},_optionsExtract:function(b,c){var d=this,e=a.extend(!0,{},c),f=d.__options[b];return f||(f={},a.each(c,function(a,b){var c=d.__options[a];void 0!==c&&(f[a]=c)})),a.each(e,function(b,c){void 0!==f[b]&&("object"!=typeof c||c instanceof Array||null==c||"object"!=typeof f[b]||f[b]instanceof Array||null==f[b]?e[b]=f[b]:a.extend(e[b],f[b]))}),e},_plug:function(b){var c=a.tooltipster._plugin(b);if(!c)throw new Error('The "'+b+'" plugin is not defined');return c.instance&&a.tooltipster.__bridge(c.instance,this,c.name),this},_touchIsEmulatedEvent:function(a){for(var b=!1,c=(new Date).getTime(),d=this.__touchEvents.length-1;d>=0;d--){var e=this.__touchEvents[d];if(!(c-e.time<500))break;e.target===a.target&&(b=!0)}return b},_touchIsMeaningfulEvent:function(a){return this._touchIsTouchEvent(a)&&!this._touchSwiped(a.target)||!this._touchIsTouchEvent(a)&&!this._touchIsEmulatedEvent(a)},_touchIsTouchEvent:function(a){return 0==a.type.indexOf("touch")},_touchRecordEvent:function(a){return this._touchIsTouchEvent(a)&&(a.time=(new Date).getTime(),this.__touchEvents.push(a)),this},_touchSwiped:function(a){for(var b=!1,c=this.__touchEvents.length-1;c>=0;c--){var d=this.__touchEvents[c];if("touchmove"==d.type){b=!0;break}if("touchstart"==d.type&&a===d.target)break}return b},_trigger:function(){var b=Array.prototype.slice.apply(arguments);return"string"==typeof b[0]&&(b[0]={type:b[0]}),b[0].instance=this,b[0].origin=this._$origin?this._$origin[0]:null,b[0].tooltip=this._$tooltip?this._$tooltip[0]:null,this.__$emitterPrivate.trigger.apply(this.__$emitterPrivate,b),a.tooltipster._trigger.apply(a.tooltipster,b),this.__$emitterPublic.trigger.apply(this.__$emitterPublic,b),this},_unplug:function(b){var c=this;if(c[b]){var d=a.tooltipster._plugin(b);d.instance&&a.each(d.instance,function(a,d){c[a]&&c[a].bridged===c[b]&&delete c[a]}),c[b].__destroy&&c[b].__destroy(),delete c[b]}return c},close:function(a){return this.__destroyed?this.__destroyError():this._close(null,a),this},content:function(a){var b=this;if(void 0===a)return b.__Content;if(b.__destroyed)b.__destroyError();else if(b.__contentSet(a),null!==b.__Content){if("closed"!==b.__state&&(b.__contentInsert(),b.reposition(),b.__options.updateAnimation))if(h.hasTransitions){var c=b.__options.updateAnimation;b._$tooltip.addClass("tooltipster-update-"+c),setTimeout(function(){"closed"!=b.__state&&b._$tooltip.removeClass("tooltipster-update-"+c)},1e3)}else b._$tooltip.fadeTo(200,.5,function(){"closed"!=b.__state&&b._$tooltip.fadeTo(200,1)})}else b._close();return b},destroy:function(){var b=this;if(b.__destroyed)b.__destroyError();else{"closed"!=b.__state?b.option("animationDuration",0)._close(null,null,!0):b.__timeoutsClear(),b._trigger("destroy"),b.__destroyed=!0,b._$origin.removeData(b.__namespace).off("."+b.__namespace+"-triggerOpen"),a(h.window.document.body).off("."+b.__namespace+"-triggerOpen");var c=b._$origin.data("tooltipster-ns");if(c)if(1===c.length){var d=null;"previous"==b.__options.restoration?d=b._$origin.data("tooltipster-initialTitle"):"current"==b.__options.restoration&&(d="string"==typeof b.__Content?b.__Content:a("
    ").append(b.__Content).html()),d&&b._$origin.attr("title",d),b._$origin.removeClass("tooltipstered"),b._$origin.removeData("tooltipster-ns").removeData("tooltipster-initialTitle")}else c=a.grep(c,function(a,c){return a!==b.__namespace}),b._$origin.data("tooltipster-ns",c);b._trigger("destroyed"),b._off(),b.off(),b.__Content=null,b.__$emitterPrivate=null,b.__$emitterPublic=null,b.__options.parent=null,b._$origin=null,b._$tooltip=null,a.tooltipster.__instancesLatestArr=a.grep(a.tooltipster.__instancesLatestArr,function(a,c){return b!==a}),clearInterval(b.__garbageCollector)}return b},disable:function(){return this.__destroyed?(this.__destroyError(),this):(this._close(),this.__enabled=!1,this)},elementOrigin:function(){return this.__destroyed?void this.__destroyError():this._$origin[0]},elementTooltip:function(){return this._$tooltip?this._$tooltip[0]:null},enable:function(){return this.__enabled=!0,this},hide:function(a){return this.close(a)},instance:function(){return this},off:function(){return this.__destroyed||this.__$emitterPublic.off.apply(this.__$emitterPublic,Array.prototype.slice.apply(arguments)),this},on:function(){return this.__destroyed?this.__destroyError():this.__$emitterPublic.on.apply(this.__$emitterPublic,Array.prototype.slice.apply(arguments)),this},one:function(){return this.__destroyed?this.__destroyError():this.__$emitterPublic.one.apply(this.__$emitterPublic,Array.prototype.slice.apply(arguments)),this},open:function(a){return this.__destroyed?this.__destroyError():this._open(null,a),this},option:function(b,c){return void 0===c?this.__options[b]:(this.__destroyed?this.__destroyError():(this.__options[b]=c,this.__optionsFormat(),a.inArray(b,["trigger","triggerClose","triggerOpen"])>=0&&this.__prepareOrigin(),"selfDestruction"===b&&this.__prepareGC()),this)},reposition:function(a,b){var c=this;return c.__destroyed?c.__destroyError():"closed"!=c.__state&&d(c._$origin)&&(b||d(c._$tooltip))&&(b||c._$tooltip.detach(),c.__Geometry=c.__geometry(),c._trigger({type:"reposition",event:a,helper:{geo:c.__Geometry}})),c},show:function(a){return this.open(a)},status:function(){return{destroyed:this.__destroyed,enabled:this.__enabled,open:"closed"!==this.__state,state:this.__state}},triggerHandler:function(){return this.__destroyed?this.__destroyError():this.__$emitterPublic.triggerHandler.apply(this.__$emitterPublic,Array.prototype.slice.apply(arguments)),this}},a.fn.tooltipster=function(){var b=Array.prototype.slice.apply(arguments),c="You are using a single HTML element as content for several tooltips. You probably want to set the contentCloning option to TRUE.";if(0===this.length)return this;if("string"==typeof b[0]){var d="#*$~&";return this.each(function(){var e=a(this).data("tooltipster-ns"),f=e?a(this).data(e[0]):null;if(!f)throw new Error("You called Tooltipster's \""+b[0]+'" method on an uninitialized element');if("function"!=typeof f[b[0]])throw new Error('Unknown method "'+b[0]+'"');this.length>1&&"content"==b[0]&&(b[1]instanceof a||"object"==typeof b[1]&&null!=b[1]&&b[1].tagName)&&!f.__options.contentCloning&&f.__options.debug&&console.log(c);var g=f[b[0]](b[1],b[2]);if(g!==f||"instance"===b[0])return d=g,!1}),"#*$~&"!==d?d:this}a.tooltipster.__instancesLatestArr=[];var e=b[0]&&void 0!==b[0].multiple,g=e&&b[0].multiple||!e&&f.multiple,h=b[0]&&void 0!==b[0].content,i=h&&b[0].content||!h&&f.content,j=b[0]&&void 0!==b[0].contentCloning,k=j&&b[0].contentCloning||!j&&f.contentCloning,l=b[0]&&void 0!==b[0].debug,m=l&&b[0].debug||!l&&f.debug;return this.length>1&&(i instanceof a||"object"==typeof i&&null!=i&&i.tagName)&&!k&&m&&console.log(c),this.each(function(){var c=!1,d=a(this),e=d.data("tooltipster-ns"),f=null;e?g?c=!0:m&&(console.log("Tooltipster: one or more tooltips are already attached to the element below. Ignoring."),console.log(this)):c=!0,c&&(f=new a.Tooltipster(this,b[0]),e||(e=[]),e.push(f.__namespace),d.data("tooltipster-ns",e),d.data(f.__namespace,f),f.__options.functionInit&&f.__options.functionInit.call(f,f,{origin:this}),f._trigger("init")),a.tooltipster.__instancesLatestArr.push(f)}),this},b.prototype={__init:function(b){this.__$tooltip=b,this.__$tooltip.css({left:0,overflow:"hidden",position:"absolute",top:0}).find(".tooltipster-content").css("overflow","auto"),this.$container=a('
    ').append(this.__$tooltip).appendTo(h.window.document.body)},__forceRedraw:function(){var a=this.__$tooltip.parent();this.__$tooltip.detach(),this.__$tooltip.appendTo(a)},constrain:function(a,b){return this.constraints={width:a,height:b},this.__$tooltip.css({display:"block",height:"",overflow:"auto",width:a}),this},destroy:function(){this.__$tooltip.detach().find(".tooltipster-content").css({display:"",overflow:""}),this.$container.remove()},free:function(){return this.constraints=null,this.__$tooltip.css({display:"",height:"",overflow:"visible",width:""}),this},measure:function(){this.__forceRedraw();var a=this.__$tooltip[0].getBoundingClientRect(),b={size:{height:a.height||a.bottom-a.top,width:a.width||a.right-a.left}};if(this.constraints){var c=this.__$tooltip.find(".tooltipster-content"),d=this.__$tooltip.outerHeight(),e=c[0].getBoundingClientRect(),f={height:d<=this.constraints.height,width:a.width<=this.constraints.width&&e.width>=c[0].scrollWidth-1};b.fits=f.height&&f.width}return h.IE&&h.IE<=11&&b.size.width!==h.window.document.documentElement.clientWidth&&(b.size.width=Math.ceil(b.size.width)+1),b}};var j=navigator.userAgent.toLowerCase();j.indexOf("msie")!=-1?h.IE=parseInt(j.split("msie")[1]):j.toLowerCase().indexOf("trident")!==-1&&j.indexOf(" rv:11")!==-1?h.IE=11:j.toLowerCase().indexOf("edge/")!=-1&&(h.IE=parseInt(j.toLowerCase().split("edge/")[1]));var k="tooltipster.sideTip";return a.tooltipster._plugin({name:k,instance:{__defaults:function(){return{arrow:!0,distance:6,functionPosition:null,maxWidth:null,minIntersection:16,minWidth:0,position:null,side:"top",viewportAware:!0}},__init:function(a){var b=this;b.__instance=a,b.__namespace="tooltipster-sideTip-"+Math.round(1e6*Math.random()),b.__previousState="closed",b.__options,b.__optionsFormat(),b.__instance._on("state."+b.__namespace,function(a){"closed"==a.state?b.__close():"appearing"==a.state&&"closed"==b.__previousState&&b.__create(),b.__previousState=a.state}),b.__instance._on("options."+b.__namespace,function(){b.__optionsFormat()}),b.__instance._on("reposition."+b.__namespace,function(a){b.__reposition(a.event,a.helper)})},__close:function(){this.__instance.content()instanceof a&&this.__instance.content().detach(),this.__instance._$tooltip.remove(),this.__instance._$tooltip=null},__create:function(){var b=a('
    ');this.__options.arrow||b.find(".tooltipster-box").css("margin",0).end().find(".tooltipster-arrow").hide(), ++this.__options.minWidth&&b.css("min-width",this.__options.minWidth+"px"),this.__options.maxWidth&&b.css("max-width",this.__options.maxWidth+"px"),this.__instance._$tooltip=b,this.__instance._trigger("created")},__destroy:function(){this.__instance._off("."+self.__namespace)},__optionsFormat:function(){var b=this;if(b.__options=b.__instance._optionsExtract(k,b.__defaults()),b.__options.position&&(b.__options.side=b.__options.position),"object"!=typeof b.__options.distance&&(b.__options.distance=[b.__options.distance]),b.__options.distance.length<4&&(void 0===b.__options.distance[1]&&(b.__options.distance[1]=b.__options.distance[0]),void 0===b.__options.distance[2]&&(b.__options.distance[2]=b.__options.distance[0]),void 0===b.__options.distance[3]&&(b.__options.distance[3]=b.__options.distance[1])),b.__options.distance={top:b.__options.distance[0],right:b.__options.distance[1],bottom:b.__options.distance[2],left:b.__options.distance[3]},"string"==typeof b.__options.side){var c={top:"bottom",right:"left",bottom:"top",left:"right"};b.__options.side=[b.__options.side,c[b.__options.side]],"left"==b.__options.side[0]||"right"==b.__options.side[0]?b.__options.side.push("top","bottom"):b.__options.side.push("right","left")}6===a.tooltipster._env.IE&&b.__options.arrow!==!0&&(b.__options.arrow=!1)},__reposition:function(b,c){var d,e=this,f=e.__targetFind(c),g=[];e.__instance._$tooltip.detach();var h=e.__instance._$tooltip.clone(),i=a.tooltipster._getRuler(h),j=!1,k=e.__instance.option("animation");switch(k&&h.removeClass("tooltipster-"+k),a.each(["window","document"],function(d,k){var l=null;if(e.__instance._trigger({container:k,helper:c,satisfied:j,takeTest:function(a){l=a},results:g,type:"positionTest"}),1==l||0!=l&&0==j&&("window"!=k||e.__options.viewportAware))for(var d=0;d=h.outerSize.width&&c.geo.available[k][n].height>=h.outerSize.height?h.fits=!0:h.fits=!1:h.fits=p.fits,"window"==k&&(h.fits?"top"==n||"bottom"==n?h.whole=c.geo.origin.windowOffset.right>=e.__options.minIntersection&&c.geo.window.size.width-c.geo.origin.windowOffset.left>=e.__options.minIntersection:h.whole=c.geo.origin.windowOffset.bottom>=e.__options.minIntersection&&c.geo.window.size.height-c.geo.origin.windowOffset.top>=e.__options.minIntersection:h.whole=!1),g.push(h),h.whole)j=!0;else if("natural"==h.mode&&(h.fits||h.size.width<=c.geo.available[k][n].width))return!1}})}}),e.__instance._trigger({edit:function(a){g=a},event:b,helper:c,results:g,type:"positionTested"}),g.sort(function(a,b){if(a.whole&&!b.whole)return-1;if(!a.whole&&b.whole)return 1;if(a.whole&&b.whole){var c=e.__options.side.indexOf(a.side),d=e.__options.side.indexOf(b.side);return cd?1:"natural"==a.mode?-1:1}if(a.fits&&!b.fits)return-1;if(!a.fits&&b.fits)return 1;if(a.fits&&b.fits){var c=e.__options.side.indexOf(a.side),d=e.__options.side.indexOf(b.side);return cd?1:"natural"==a.mode?-1:1}return"document"==a.container&&"bottom"==a.side&&"natural"==a.mode?-1:1}),d=g[0],d.coord={},d.side){case"left":case"right":d.coord.top=Math.floor(d.target-d.size.height/2);break;case"bottom":case"top":d.coord.left=Math.floor(d.target-d.size.width/2)}switch(d.side){case"left":d.coord.left=c.geo.origin.windowOffset.left-d.outerSize.width;break;case"right":d.coord.left=c.geo.origin.windowOffset.right+d.distance.horizontal;break;case"top":d.coord.top=c.geo.origin.windowOffset.top-d.outerSize.height;break;case"bottom":d.coord.top=c.geo.origin.windowOffset.bottom+d.distance.vertical}"window"==d.container?"top"==d.side||"bottom"==d.side?d.coord.left<0?c.geo.origin.windowOffset.right-this.__options.minIntersection>=0?d.coord.left=0:d.coord.left=c.geo.origin.windowOffset.right-this.__options.minIntersection-1:d.coord.left>c.geo.window.size.width-d.size.width&&(c.geo.origin.windowOffset.left+this.__options.minIntersection<=c.geo.window.size.width?d.coord.left=c.geo.window.size.width-d.size.width:d.coord.left=c.geo.origin.windowOffset.left+this.__options.minIntersection+1-d.size.width):d.coord.top<0?c.geo.origin.windowOffset.bottom-this.__options.minIntersection>=0?d.coord.top=0:d.coord.top=c.geo.origin.windowOffset.bottom-this.__options.minIntersection-1:d.coord.top>c.geo.window.size.height-d.size.height&&(c.geo.origin.windowOffset.top+this.__options.minIntersection<=c.geo.window.size.height?d.coord.top=c.geo.window.size.height-d.size.height:d.coord.top=c.geo.origin.windowOffset.top+this.__options.minIntersection+1-d.size.height):(d.coord.left>c.geo.window.size.width-d.size.width&&(d.coord.left=c.geo.window.size.width-d.size.width),d.coord.left<0&&(d.coord.left=0)),e.__sideChange(h,d.side),c.tooltipClone=h[0],c.tooltipParent=e.__instance.option("parent").parent[0],c.mode=d.mode,c.whole=d.whole,c.origin=e.__instance._$origin[0],c.tooltip=e.__instance._$tooltip[0],delete d.container,delete d.fits,delete d.mode,delete d.outerSize,delete d.whole,d.distance=d.distance.horizontal||d.distance.vertical;var l=a.extend(!0,{},d);if(e.__instance._trigger({edit:function(a){d=a},event:b,helper:c,position:l,type:"position"}),e.__options.functionPosition){var m=e.__options.functionPosition.call(e,e.__instance,c,l);m&&(d=m)}i.destroy();var n,o;"top"==d.side||"bottom"==d.side?(n={prop:"left",val:d.target-d.coord.left},o=d.size.width-this.__options.minIntersection):(n={prop:"top",val:d.target-d.coord.top},o=d.size.height-this.__options.minIntersection),n.valo&&(n.val=o);var p;p=c.geo.origin.fixedLineage?c.geo.origin.windowOffset:{left:c.geo.origin.windowOffset.left+c.geo.window.scroll.left,top:c.geo.origin.windowOffset.top+c.geo.window.scroll.top},d.coord={left:p.left+(d.coord.left-c.geo.origin.windowOffset.left),top:p.top+(d.coord.top-c.geo.origin.windowOffset.top)},e.__sideChange(e.__instance._$tooltip,d.side),c.geo.origin.fixedLineage?e.__instance._$tooltip.css("position","fixed"):e.__instance._$tooltip.css("position",""),e.__instance._$tooltip.css({left:d.coord.left,top:d.coord.top,height:d.size.height,width:d.size.width}).find(".tooltipster-arrow").css({left:"",top:""}).css(n.prop,n.val),e.__instance._$tooltip.appendTo(e.__instance.option("parent")),e.__instance._trigger({type:"repositioned",event:b,position:d})},__sideChange:function(a,b){a.removeClass("tooltipster-bottom").removeClass("tooltipster-left").removeClass("tooltipster-right").removeClass("tooltipster-top").addClass("tooltipster-"+b)},__targetFind:function(a){var b={},c=this.__instance._$origin[0].getClientRects();if(c.length>1){var d=this.__instance._$origin.css("opacity");1==d&&(this.__instance._$origin.css("opacity",.99),c=this.__instance._$origin[0].getClientRects(),this.__instance._$origin.css("opacity",1))}if(c.length<2)b.top=Math.floor(a.geo.origin.windowOffset.left+a.geo.origin.size.width/2),b.bottom=b.top,b.left=Math.floor(a.geo.origin.windowOffset.top+a.geo.origin.size.height/2),b.right=b.left;else{var e=c[0];b.top=Math.floor(e.left+(e.right-e.left)/2),e=c.length>2?c[Math.ceil(c.length/2)-1]:c[0],b.right=Math.floor(e.top+(e.bottom-e.top)/2),e=c[c.length-1],b.bottom=Math.floor(e.left+(e.right-e.left)/2),e=c.length>2?c[Math.ceil((c.length+1)/2)-1]:c[c.length-1],b.left=Math.floor(e.top+(e.bottom-e.top)/2)}return b}}}),a}); +\ No newline at end of file +diff --git a/node_modules/tooltipster/dist/js/tooltipster.main.js b/node_modules/tooltipster/dist/js/tooltipster.main.js +index 34c11d8..44efcd5 100644 +--- a/node_modules/tooltipster/dist/js/tooltipster.main.js ++++ b/node_modules/tooltipster/dist/js/tooltipster.main.js +@@ -5,18 +5,19 @@ + * MIT license + */ + (function (root, factory) { ++ if (root === undefined && window !== undefined) root = window; + if (typeof define === 'function' && define.amd) { + // AMD. Register as an anonymous module unless amdModuleId is set + define(["jquery"], function (a0) { + return (factory(a0)); + }); +- } else if (typeof exports === 'object') { ++ } else if (typeof module === 'object' && module.exports) { + // Node. Does not work with strict CommonJS, but + // only CommonJS-like environments that support module.exports, + // like Node. + module.exports = factory(require("jquery")); + } else { +- factory(jQuery); ++ factory(root["jQuery"]); + } + }(this, function ($) { + +@@ -1260,8 +1261,14 @@ $.Tooltipster.prototype = { + if ( geo.origin.windowOffset.top < bcr.top + || geo.origin.windowOffset.bottom > bcr.bottom + ) { +- overflows = true; +- return false; ++ if (self.__options.checkOverflowY) { ++ overflows = self.__options.checkOverflowY(geo, bcr); ++ } else { ++ overflows = true; ++ } ++ if (overflows) { ++ return false; ++ } + } + } + } +@@ -1412,7 +1419,7 @@ $.Tooltipster.prototype = { + + // close the tooltip when using the mouseleave close trigger + // (see https://github.com/iamceege/tooltipster/pull/253) +- if (self.__options.triggerClose.mouseleave) { ++ if (self.__options.triggerClose.mouseleave && !self.__options.ignoreCloseOnScroll) { + self._close(); + } + else { +@@ -3340,6 +3347,7 @@ function transitionSupport() { + + // we'll return jQuery for plugins not to have to declare it as a dependency, + // but it's done by a build task since it should be included only once at the +-// end when we concatenate the main file with a pluginreturn $; ++// end when we concatenate the main file with a plugin ++return $; + + })); +diff --git a/node_modules/tooltipster/dist/js/tooltipster.main.min.js b/node_modules/tooltipster/dist/js/tooltipster.main.min.js +index 1221ae9..b2b3ac9 100644 +--- a/node_modules/tooltipster/dist/js/tooltipster.main.min.js ++++ b/node_modules/tooltipster/dist/js/tooltipster.main.min.js +@@ -1 +1 @@ +-/*! tooltipster v4.2.8 */!function(a,b){"function"==typeof define&&define.amd?define(["jquery"],function(a){return b(a)}):"object"==typeof exports?module.exports=b(require("jquery")):b(jQuery)}(this,function(a){function b(a){this.$container,this.constraints=null,this.__$tooltip,this.__init(a)}function c(b,c){var d=!0;return a.each(b,function(a,e){return void 0===c[a]||b[a]!==c[a]?(d=!1,!1):void 0}),d}function d(b){var c=b.attr("id"),d=c?h.window.document.getElementById(c):null;return d?d===b[0]:a.contains(h.window.document.body,b[0])}function e(){if(!g)return!1;var a=g.document.body||g.document.documentElement,b=a.style,c="transition",d=["Moz","Webkit","Khtml","O","ms"];if("string"==typeof b[c])return!0;c=c.charAt(0).toUpperCase()+c.substr(1);for(var e=0;e0?e=c.__plugins[d]:a.each(c.__plugins,function(a,b){return b.name.substring(b.name.length-d.length-1)=="."+d?(e=b,!1):void 0}),e}if(b.name.indexOf(".")<0)throw new Error("Plugins must be namespaced");return c.__plugins[b.name]=b,b.core&&c.__bridge(b.core,c,b.name),this},_trigger:function(){var a=Array.prototype.slice.apply(arguments);return"string"==typeof a[0]&&(a[0]={type:a[0]}),this.__$emitterPrivate.trigger.apply(this.__$emitterPrivate,a),this.__$emitterPublic.trigger.apply(this.__$emitterPublic,a),this},instances:function(b){var c=[],d=b||".tooltipstered";return a(d).each(function(){var b=a(this),d=b.data("tooltipster-ns");d&&a.each(d,function(a,d){c.push(b.data(d))})}),c},instancesLatest:function(){return this.__instancesLatestArr},off:function(){return this.__$emitterPublic.off.apply(this.__$emitterPublic,Array.prototype.slice.apply(arguments)),this},on:function(){return this.__$emitterPublic.on.apply(this.__$emitterPublic,Array.prototype.slice.apply(arguments)),this},one:function(){return this.__$emitterPublic.one.apply(this.__$emitterPublic,Array.prototype.slice.apply(arguments)),this},origins:function(b){var c=b?b+" ":"";return a(c+".tooltipstered").toArray()},setDefaults:function(b){return a.extend(f,b),this},triggerHandler:function(){return this.__$emitterPublic.triggerHandler.apply(this.__$emitterPublic,Array.prototype.slice.apply(arguments)),this}},a.tooltipster=new i,a.Tooltipster=function(b,c){this.__callbacks={close:[],open:[]},this.__closingTime,this.__Content,this.__contentBcr,this.__destroyed=!1,this.__$emitterPrivate=a({}),this.__$emitterPublic=a({}),this.__enabled=!0,this.__garbageCollector,this.__Geometry,this.__lastPosition,this.__namespace="tooltipster-"+Math.round(1e6*Math.random()),this.__options,this.__$originParents,this.__pointerIsOverOrigin=!1,this.__previousThemes=[],this.__state="closed",this.__timeouts={close:[],open:null},this.__touchEvents=[],this.__tracker=null,this._$origin,this._$tooltip,this.__init(b,c)},a.Tooltipster.prototype={__init:function(b,c){var d=this;if(d._$origin=a(b),d.__options=a.extend(!0,{},f,c),d.__optionsFormat(),!h.IE||h.IE>=d.__options.IEmin){var e=null;if(void 0===d._$origin.data("tooltipster-initialTitle")&&(e=d._$origin.attr("title"),void 0===e&&(e=null),d._$origin.data("tooltipster-initialTitle",e)),null!==d.__options.content)d.__contentSet(d.__options.content);else{var g,i=d._$origin.attr("data-tooltip-content");i&&(g=a(i)),g&&g[0]?d.__contentSet(g.first()):d.__contentSet(e)}d._$origin.removeAttr("title").addClass("tooltipstered"),d.__prepareOrigin(),d.__prepareGC(),a.each(d.__options.plugins,function(a,b){d._plug(b)}),h.hasTouchCapability&&a(h.window.document.body).on("touchmove."+d.__namespace+"-triggerOpen",function(a){d._touchRecordEvent(a)}),d._on("created",function(){d.__prepareTooltip()})._on("repositioned",function(a){d.__lastPosition=a.position})}else d.__options.disabled=!0},__contentInsert:function(){var a=this,b=a._$tooltip.find(".tooltipster-content"),c=a.__Content,d=function(a){c=a};return a._trigger({type:"format",content:a.__Content,format:d}),a.__options.functionFormat&&(c=a.__options.functionFormat.call(a,a,{origin:a._$origin[0]},a.__Content)),"string"!=typeof c||a.__options.contentAsHTML?b.empty().append(c):b.text(c),a},__contentSet:function(b){return b instanceof a&&this.__options.contentCloning&&(b=b.clone(!0)),this.__Content=b,this._trigger({type:"updated",content:b}),this},__destroyError:function(){throw new Error("This tooltip has been destroyed and cannot execute your method call.")},__geometry:function(){var b=this,c=b._$origin,d=b._$origin.is("area");if(d){var e=b._$origin.parent().attr("name");c=a('img[usemap="#'+e+'"]')}var f=c[0].getBoundingClientRect(),g=a(h.window.document),i=a(h.window),j=c,k={available:{document:null,window:null},document:{size:{height:g.height(),width:g.width()}},window:{scroll:{left:h.window.scrollX||h.window.document.documentElement.scrollLeft,top:h.window.scrollY||h.window.document.documentElement.scrollTop},size:{height:i.height(),width:i.width()}},origin:{fixedLineage:!1,offset:{},size:{height:f.bottom-f.top,width:f.right-f.left},usemapImage:d?c[0]:null,windowOffset:{bottom:f.bottom,left:f.left,right:f.right,top:f.top}}};if(d){var l=b._$origin.attr("shape"),m=b._$origin.attr("coords");if(m&&(m=m.split(","),a.map(m,function(a,b){m[b]=parseInt(a)})),"default"!=l)switch(l){case"circle":var n=m[0],o=m[1],p=m[2],q=o-p,r=n-p;k.origin.size.height=2*p,k.origin.size.width=k.origin.size.height,k.origin.windowOffset.left+=r,k.origin.windowOffset.top+=q;break;case"rect":var s=m[0],t=m[1],u=m[2],v=m[3];k.origin.size.height=v-t,k.origin.size.width=u-s,k.origin.windowOffset.left+=s,k.origin.windowOffset.top+=t;break;case"poly":for(var w=0,x=0,y=0,z=0,A="even",B=0;By&&(y=C,0===B&&(w=y)),w>C&&(w=C),A="odd"):(C>z&&(z=C,1==B&&(x=z)),x>C&&(x=C),A="even")}k.origin.size.height=z-x,k.origin.size.width=y-w,k.origin.windowOffset.left+=w,k.origin.windowOffset.top+=x}}var D=function(a){k.origin.size.height=a.height,k.origin.windowOffset.left=a.left,k.origin.windowOffset.top=a.top,k.origin.size.width=a.width};for(b._trigger({type:"geometry",edit:D,geometry:{height:k.origin.size.height,left:k.origin.windowOffset.left,top:k.origin.windowOffset.top,width:k.origin.size.width}}),k.origin.windowOffset.right=k.origin.windowOffset.left+k.origin.size.width,k.origin.windowOffset.bottom=k.origin.windowOffset.top+k.origin.size.height,k.origin.offset.left=k.origin.windowOffset.left+k.window.scroll.left,k.origin.offset.top=k.origin.windowOffset.top+k.window.scroll.top,k.origin.offset.bottom=k.origin.offset.top+k.origin.size.height,k.origin.offset.right=k.origin.offset.left+k.origin.size.width,k.available.document={bottom:{height:k.document.size.height-k.origin.offset.bottom,width:k.document.size.width},left:{height:k.document.size.height,width:k.origin.offset.left},right:{height:k.document.size.height,width:k.document.size.width-k.origin.offset.right},top:{height:k.origin.offset.top,width:k.document.size.width}},k.available.window={bottom:{height:Math.max(k.window.size.height-Math.max(k.origin.windowOffset.bottom,0),0),width:k.window.size.width},left:{height:k.window.size.height,width:Math.max(k.origin.windowOffset.left,0)},right:{height:k.window.size.height,width:Math.max(k.window.size.width-Math.max(k.origin.windowOffset.right,0),0)},top:{height:Math.max(k.origin.windowOffset.top,0),width:k.window.size.width}};"html"!=j[0].tagName.toLowerCase();){if("fixed"==j.css("position")){k.origin.fixedLineage=!0;break}j=j.parent()}return k},__optionsFormat:function(){return"number"==typeof this.__options.animationDuration&&(this.__options.animationDuration=[this.__options.animationDuration,this.__options.animationDuration]),"number"==typeof this.__options.delay&&(this.__options.delay=[this.__options.delay,this.__options.delay]),"number"==typeof this.__options.delayTouch&&(this.__options.delayTouch=[this.__options.delayTouch,this.__options.delayTouch]),"string"==typeof this.__options.theme&&(this.__options.theme=[this.__options.theme]),null===this.__options.parent?this.__options.parent=a(h.window.document.body):"string"==typeof this.__options.parent&&(this.__options.parent=a(this.__options.parent)),"hover"==this.__options.trigger?(this.__options.triggerOpen={mouseenter:!0,touchstart:!0},this.__options.triggerClose={mouseleave:!0,originClick:!0,touchleave:!0}):"click"==this.__options.trigger&&(this.__options.triggerOpen={click:!0,tap:!0},this.__options.triggerClose={click:!0,tap:!0}),this._trigger("options"),this},__prepareGC:function(){var b=this;return b.__options.selfDestruction?b.__garbageCollector=setInterval(function(){var c=(new Date).getTime();b.__touchEvents=a.grep(b.__touchEvents,function(a,b){return c-a.time>6e4}),d(b._$origin)||b.close(function(){b.destroy()})},2e4):clearInterval(b.__garbageCollector),b},__prepareOrigin:function(){var a=this;if(a._$origin.off("."+a.__namespace+"-triggerOpen"),h.hasTouchCapability&&a._$origin.on("touchstart."+a.__namespace+"-triggerOpen touchend."+a.__namespace+"-triggerOpen touchcancel."+a.__namespace+"-triggerOpen",function(b){a._touchRecordEvent(b)}),a.__options.triggerOpen.click||a.__options.triggerOpen.tap&&h.hasTouchCapability){var b="";a.__options.triggerOpen.click&&(b+="click."+a.__namespace+"-triggerOpen "),a.__options.triggerOpen.tap&&h.hasTouchCapability&&(b+="touchend."+a.__namespace+"-triggerOpen"),a._$origin.on(b,function(b){a._touchIsMeaningfulEvent(b)&&a._open(b)})}if(a.__options.triggerOpen.mouseenter||a.__options.triggerOpen.touchstart&&h.hasTouchCapability){var b="";a.__options.triggerOpen.mouseenter&&(b+="mouseenter."+a.__namespace+"-triggerOpen "),a.__options.triggerOpen.touchstart&&h.hasTouchCapability&&(b+="touchstart."+a.__namespace+"-triggerOpen"),a._$origin.on(b,function(b){!a._touchIsTouchEvent(b)&&a._touchIsEmulatedEvent(b)||(a.__pointerIsOverOrigin=!0,a._openShortly(b))})}if(a.__options.triggerClose.mouseleave||a.__options.triggerClose.touchleave&&h.hasTouchCapability){var b="";a.__options.triggerClose.mouseleave&&(b+="mouseleave."+a.__namespace+"-triggerOpen "),a.__options.triggerClose.touchleave&&h.hasTouchCapability&&(b+="touchend."+a.__namespace+"-triggerOpen touchcancel."+a.__namespace+"-triggerOpen"),a._$origin.on(b,function(b){a._touchIsMeaningfulEvent(b)&&(a.__pointerIsOverOrigin=!1)})}return a},__prepareTooltip:function(){var b=this,c=b.__options.interactive?"auto":"";return b._$tooltip.attr("id",b.__namespace).css({"pointer-events":c,zIndex:b.__options.zIndex}),a.each(b.__previousThemes,function(a,c){b._$tooltip.removeClass(c)}),a.each(b.__options.theme,function(a,c){b._$tooltip.addClass(c)}),b.__previousThemes=a.merge([],b.__options.theme),b},__scrollHandler:function(b){var c=this;if(c.__options.triggerClose.scroll)c._close(b);else if(d(c._$origin)&&d(c._$tooltip)){var e=null;if(b.target===h.window.document)c.__Geometry.origin.fixedLineage||c.__options.repositionOnScroll&&c.reposition(b);else{e=c.__geometry();var f=!1;if("fixed"!=c._$origin.css("position")&&c.__$originParents.each(function(b,c){var d=a(c),g=d.css("overflow-x"),h=d.css("overflow-y");if("visible"!=g||"visible"!=h){var i=c.getBoundingClientRect();if("visible"!=g&&(e.origin.windowOffset.lefti.right))return f=!0,!1;if("visible"!=h&&(e.origin.windowOffset.topi.bottom))return f=!0,!1}return"fixed"==d.css("position")?!1:void 0}),f)c._$tooltip.css("visibility","hidden");else if(c._$tooltip.css("visibility","visible"),c.__options.repositionOnScroll)c.reposition(b);else{var g=e.origin.offset.left-c.__Geometry.origin.offset.left,i=e.origin.offset.top-c.__Geometry.origin.offset.top;c._$tooltip.css({left:c.__lastPosition.coord.left+g,top:c.__lastPosition.coord.top+i})}}c._trigger({type:"scroll",event:b,geo:e})}return c},__stateSet:function(a){return this.__state=a,this._trigger({type:"state",state:a}),this},__timeoutsClear:function(){return clearTimeout(this.__timeouts.open),this.__timeouts.open=null,a.each(this.__timeouts.close,function(a,b){clearTimeout(b)}),this.__timeouts.close=[],this},__trackerStart:function(){var a=this,b=a._$tooltip.find(".tooltipster-content");return a.__options.trackTooltip&&(a.__contentBcr=b[0].getBoundingClientRect()),a.__tracker=setInterval(function(){if(d(a._$origin)&&d(a._$tooltip)){if(a.__options.trackOrigin){var e=a.__geometry(),f=!1;c(e.origin.size,a.__Geometry.origin.size)&&(a.__Geometry.origin.fixedLineage?c(e.origin.windowOffset,a.__Geometry.origin.windowOffset)&&(f=!0):c(e.origin.offset,a.__Geometry.origin.offset)&&(f=!0)),f||(a.__options.triggerClose.mouseleave?a._close():a.reposition())}if(a.__options.trackTooltip){var g=b[0].getBoundingClientRect();g.height===a.__contentBcr.height&&g.width===a.__contentBcr.width||(a.reposition(),a.__contentBcr=g)}}else a._close()},a.__options.trackerInterval),a},_close:function(b,c,d){var e=this,f=!0;if(e._trigger({type:"close",event:b,stop:function(){f=!1}}),f||d){c&&e.__callbacks.close.push(c),e.__callbacks.open=[],e.__timeoutsClear();var g=function(){a.each(e.__callbacks.close,function(a,c){c.call(e,e,{event:b,origin:e._$origin[0]})}),e.__callbacks.close=[]};if("closed"!=e.__state){var i=!0,j=new Date,k=j.getTime(),l=k+e.__options.animationDuration[1];if("disappearing"==e.__state&&l>e.__closingTime&&e.__options.animationDuration[1]>0&&(i=!1),i){e.__closingTime=l,"disappearing"!=e.__state&&e.__stateSet("disappearing");var m=function(){clearInterval(e.__tracker),e._trigger({type:"closing",event:b}),e._$tooltip.off("."+e.__namespace+"-triggerClose").removeClass("tooltipster-dying"),a(h.window).off("."+e.__namespace+"-triggerClose"),e.__$originParents.each(function(b,c){a(c).off("scroll."+e.__namespace+"-triggerClose")}),e.__$originParents=null,a(h.window.document.body).off("."+e.__namespace+"-triggerClose"),e._$origin.off("."+e.__namespace+"-triggerClose"),e._off("dismissable"),e.__stateSet("closed"),e._trigger({type:"after",event:b}),e.__options.functionAfter&&e.__options.functionAfter.call(e,e,{event:b,origin:e._$origin[0]}),g()};h.hasTransitions?(e._$tooltip.css({"-moz-animation-duration":e.__options.animationDuration[1]+"ms","-ms-animation-duration":e.__options.animationDuration[1]+"ms","-o-animation-duration":e.__options.animationDuration[1]+"ms","-webkit-animation-duration":e.__options.animationDuration[1]+"ms","animation-duration":e.__options.animationDuration[1]+"ms","transition-duration":e.__options.animationDuration[1]+"ms"}),e._$tooltip.clearQueue().removeClass("tooltipster-show").addClass("tooltipster-dying"),e.__options.animationDuration[1]>0&&e._$tooltip.delay(e.__options.animationDuration[1]),e._$tooltip.queue(m)):e._$tooltip.stop().fadeOut(e.__options.animationDuration[1],m)}}else g()}return e},_off:function(){return this.__$emitterPrivate.off.apply(this.__$emitterPrivate,Array.prototype.slice.apply(arguments)),this},_on:function(){return this.__$emitterPrivate.on.apply(this.__$emitterPrivate,Array.prototype.slice.apply(arguments)),this},_one:function(){return this.__$emitterPrivate.one.apply(this.__$emitterPrivate,Array.prototype.slice.apply(arguments)),this},_open:function(b,c){var e=this;if(!e.__destroying&&d(e._$origin)&&e.__enabled){var f=!0;if("closed"==e.__state&&(e._trigger({type:"before",event:b,stop:function(){f=!1}}),f&&e.__options.functionBefore&&(f=e.__options.functionBefore.call(e,e,{event:b,origin:e._$origin[0]}))),f!==!1&&null!==e.__Content){c&&e.__callbacks.open.push(c),e.__callbacks.close=[],e.__timeoutsClear();var g,i=function(){"stable"!=e.__state&&e.__stateSet("stable"),a.each(e.__callbacks.open,function(a,b){b.call(e,e,{origin:e._$origin[0],tooltip:e._$tooltip[0]})}),e.__callbacks.open=[]};if("closed"!==e.__state)g=0,"disappearing"===e.__state?(e.__stateSet("appearing"),h.hasTransitions?(e._$tooltip.clearQueue().removeClass("tooltipster-dying").addClass("tooltipster-show"),e.__options.animationDuration[0]>0&&e._$tooltip.delay(e.__options.animationDuration[0]),e._$tooltip.queue(i)):e._$tooltip.stop().fadeIn(i)):"stable"==e.__state&&i();else{if(e.__stateSet("appearing"),g=e.__options.animationDuration[0],e.__contentInsert(),e.reposition(b,!0),h.hasTransitions?(e._$tooltip.addClass("tooltipster-"+e.__options.animation).addClass("tooltipster-initial").css({"-moz-animation-duration":e.__options.animationDuration[0]+"ms","-ms-animation-duration":e.__options.animationDuration[0]+"ms","-o-animation-duration":e.__options.animationDuration[0]+"ms","-webkit-animation-duration":e.__options.animationDuration[0]+"ms","animation-duration":e.__options.animationDuration[0]+"ms","transition-duration":e.__options.animationDuration[0]+"ms"}),setTimeout(function(){"closed"!=e.__state&&(e._$tooltip.addClass("tooltipster-show").removeClass("tooltipster-initial"),e.__options.animationDuration[0]>0&&e._$tooltip.delay(e.__options.animationDuration[0]),e._$tooltip.queue(i))},0)):e._$tooltip.css("display","none").fadeIn(e.__options.animationDuration[0],i),e.__trackerStart(),a(h.window).on("resize."+e.__namespace+"-triggerClose",function(b){var c=a(document.activeElement);(c.is("input")||c.is("textarea"))&&a.contains(e._$tooltip[0],c[0])||e.reposition(b)}).on("scroll."+e.__namespace+"-triggerClose",function(a){e.__scrollHandler(a)}),e.__$originParents=e._$origin.parents(),e.__$originParents.each(function(b,c){a(c).on("scroll."+e.__namespace+"-triggerClose",function(a){e.__scrollHandler(a)})}),e.__options.triggerClose.mouseleave||e.__options.triggerClose.touchleave&&h.hasTouchCapability){e._on("dismissable",function(a){a.dismissable?a.delay?(m=setTimeout(function(){e._close(a.event)},a.delay),e.__timeouts.close.push(m)):e._close(a):clearTimeout(m)});var j=e._$origin,k="",l="",m=null;e.__options.interactive&&(j=j.add(e._$tooltip)),e.__options.triggerClose.mouseleave&&(k+="mouseenter."+e.__namespace+"-triggerClose ",l+="mouseleave."+e.__namespace+"-triggerClose "),e.__options.triggerClose.touchleave&&h.hasTouchCapability&&(k+="touchstart."+e.__namespace+"-triggerClose",l+="touchend."+e.__namespace+"-triggerClose touchcancel."+e.__namespace+"-triggerClose"),j.on(l,function(a){if(e._touchIsTouchEvent(a)||!e._touchIsEmulatedEvent(a)){var b="mouseleave"==a.type?e.__options.delay:e.__options.delayTouch;e._trigger({delay:b[1],dismissable:!0,event:a,type:"dismissable"})}}).on(k,function(a){!e._touchIsTouchEvent(a)&&e._touchIsEmulatedEvent(a)||e._trigger({dismissable:!1,event:a,type:"dismissable"})})}e.__options.triggerClose.originClick&&e._$origin.on("click."+e.__namespace+"-triggerClose",function(a){e._touchIsTouchEvent(a)||e._touchIsEmulatedEvent(a)||e._close(a)}),(e.__options.triggerClose.click||e.__options.triggerClose.tap&&h.hasTouchCapability)&&setTimeout(function(){if("closed"!=e.__state){var b="",c=a(h.window.document.body);e.__options.triggerClose.click&&(b+="click."+e.__namespace+"-triggerClose "),e.__options.triggerClose.tap&&h.hasTouchCapability&&(b+="touchend."+e.__namespace+"-triggerClose"),c.on(b,function(b){e._touchIsMeaningfulEvent(b)&&(e._touchRecordEvent(b),e.__options.interactive&&a.contains(e._$tooltip[0],b.target)||e._close(b))}),e.__options.triggerClose.tap&&h.hasTouchCapability&&c.on("touchstart."+e.__namespace+"-triggerClose",function(a){e._touchRecordEvent(a)})}},0),e._trigger("ready"),e.__options.functionReady&&e.__options.functionReady.call(e,e,{origin:e._$origin[0],tooltip:e._$tooltip[0]})}if(e.__options.timer>0){var m=setTimeout(function(){e._close()},e.__options.timer+g);e.__timeouts.close.push(m)}}}return e},_openShortly:function(a){var b=this,c=!0;if("stable"!=b.__state&&"appearing"!=b.__state&&!b.__timeouts.open&&(b._trigger({type:"start",event:a,stop:function(){c=!1}}),c)){var d=0==a.type.indexOf("touch")?b.__options.delayTouch:b.__options.delay;d[0]?b.__timeouts.open=setTimeout(function(){b.__timeouts.open=null,b.__pointerIsOverOrigin&&b._touchIsMeaningfulEvent(a)?(b._trigger("startend"),b._open(a)):b._trigger("startcancel")},d[0]):(b._trigger("startend"),b._open(a))}return b},_optionsExtract:function(b,c){var d=this,e=a.extend(!0,{},c),f=d.__options[b];return f||(f={},a.each(c,function(a,b){var c=d.__options[a];void 0!==c&&(f[a]=c)})),a.each(e,function(b,c){void 0!==f[b]&&("object"!=typeof c||c instanceof Array||null==c||"object"!=typeof f[b]||f[b]instanceof Array||null==f[b]?e[b]=f[b]:a.extend(e[b],f[b]))}),e},_plug:function(b){var c=a.tooltipster._plugin(b);if(!c)throw new Error('The "'+b+'" plugin is not defined');return c.instance&&a.tooltipster.__bridge(c.instance,this,c.name),this},_touchIsEmulatedEvent:function(a){for(var b=!1,c=(new Date).getTime(),d=this.__touchEvents.length-1;d>=0;d--){var e=this.__touchEvents[d];if(!(c-e.time<500))break;e.target===a.target&&(b=!0)}return b},_touchIsMeaningfulEvent:function(a){return this._touchIsTouchEvent(a)&&!this._touchSwiped(a.target)||!this._touchIsTouchEvent(a)&&!this._touchIsEmulatedEvent(a)},_touchIsTouchEvent:function(a){return 0==a.type.indexOf("touch")},_touchRecordEvent:function(a){return this._touchIsTouchEvent(a)&&(a.time=(new Date).getTime(),this.__touchEvents.push(a)),this},_touchSwiped:function(a){for(var b=!1,c=this.__touchEvents.length-1;c>=0;c--){var d=this.__touchEvents[c];if("touchmove"==d.type){b=!0;break}if("touchstart"==d.type&&a===d.target)break}return b},_trigger:function(){var b=Array.prototype.slice.apply(arguments);return"string"==typeof b[0]&&(b[0]={type:b[0]}),b[0].instance=this,b[0].origin=this._$origin?this._$origin[0]:null,b[0].tooltip=this._$tooltip?this._$tooltip[0]:null,this.__$emitterPrivate.trigger.apply(this.__$emitterPrivate,b),a.tooltipster._trigger.apply(a.tooltipster,b),this.__$emitterPublic.trigger.apply(this.__$emitterPublic,b),this},_unplug:function(b){var c=this;if(c[b]){var d=a.tooltipster._plugin(b);d.instance&&a.each(d.instance,function(a,d){c[a]&&c[a].bridged===c[b]&&delete c[a]}),c[b].__destroy&&c[b].__destroy(),delete c[b]}return c},close:function(a){return this.__destroyed?this.__destroyError():this._close(null,a),this},content:function(a){var b=this;if(void 0===a)return b.__Content;if(b.__destroyed)b.__destroyError();else if(b.__contentSet(a),null!==b.__Content){if("closed"!==b.__state&&(b.__contentInsert(),b.reposition(),b.__options.updateAnimation))if(h.hasTransitions){var c=b.__options.updateAnimation;b._$tooltip.addClass("tooltipster-update-"+c),setTimeout(function(){"closed"!=b.__state&&b._$tooltip.removeClass("tooltipster-update-"+c)},1e3)}else b._$tooltip.fadeTo(200,.5,function(){"closed"!=b.__state&&b._$tooltip.fadeTo(200,1)})}else b._close();return b},destroy:function(){var b=this;if(b.__destroyed)b.__destroyError();else{"closed"!=b.__state?b.option("animationDuration",0)._close(null,null,!0):b.__timeoutsClear(),b._trigger("destroy"),b.__destroyed=!0,b._$origin.removeData(b.__namespace).off("."+b.__namespace+"-triggerOpen"),a(h.window.document.body).off("."+b.__namespace+"-triggerOpen");var c=b._$origin.data("tooltipster-ns");if(c)if(1===c.length){var d=null;"previous"==b.__options.restoration?d=b._$origin.data("tooltipster-initialTitle"):"current"==b.__options.restoration&&(d="string"==typeof b.__Content?b.__Content:a("
    ").append(b.__Content).html()),d&&b._$origin.attr("title",d),b._$origin.removeClass("tooltipstered"),b._$origin.removeData("tooltipster-ns").removeData("tooltipster-initialTitle")}else c=a.grep(c,function(a,c){return a!==b.__namespace}),b._$origin.data("tooltipster-ns",c);b._trigger("destroyed"),b._off(),b.off(),b.__Content=null,b.__$emitterPrivate=null,b.__$emitterPublic=null,b.__options.parent=null,b._$origin=null,b._$tooltip=null,a.tooltipster.__instancesLatestArr=a.grep(a.tooltipster.__instancesLatestArr,function(a,c){return b!==a}),clearInterval(b.__garbageCollector)}return b},disable:function(){return this.__destroyed?(this.__destroyError(),this):(this._close(),this.__enabled=!1,this)},elementOrigin:function(){return this.__destroyed?void this.__destroyError():this._$origin[0]},elementTooltip:function(){return this._$tooltip?this._$tooltip[0]:null},enable:function(){return this.__enabled=!0,this},hide:function(a){return this.close(a)},instance:function(){return this},off:function(){return this.__destroyed||this.__$emitterPublic.off.apply(this.__$emitterPublic,Array.prototype.slice.apply(arguments)),this},on:function(){return this.__destroyed?this.__destroyError():this.__$emitterPublic.on.apply(this.__$emitterPublic,Array.prototype.slice.apply(arguments)),this},one:function(){return this.__destroyed?this.__destroyError():this.__$emitterPublic.one.apply(this.__$emitterPublic,Array.prototype.slice.apply(arguments)),this},open:function(a){return this.__destroyed?this.__destroyError():this._open(null,a),this},option:function(b,c){return void 0===c?this.__options[b]:(this.__destroyed?this.__destroyError():(this.__options[b]=c,this.__optionsFormat(),a.inArray(b,["trigger","triggerClose","triggerOpen"])>=0&&this.__prepareOrigin(),"selfDestruction"===b&&this.__prepareGC()),this)},reposition:function(a,b){var c=this;return c.__destroyed?c.__destroyError():"closed"!=c.__state&&d(c._$origin)&&(b||d(c._$tooltip))&&(b||c._$tooltip.detach(),c.__Geometry=c.__geometry(),c._trigger({type:"reposition",event:a,helper:{geo:c.__Geometry}})),c},show:function(a){return this.open(a)},status:function(){return{destroyed:this.__destroyed,enabled:this.__enabled,open:"closed"!==this.__state,state:this.__state}},triggerHandler:function(){return this.__destroyed?this.__destroyError():this.__$emitterPublic.triggerHandler.apply(this.__$emitterPublic,Array.prototype.slice.apply(arguments)),this}},a.fn.tooltipster=function(){var b=Array.prototype.slice.apply(arguments),c="You are using a single HTML element as content for several tooltips. You probably want to set the contentCloning option to TRUE.";if(0===this.length)return this;if("string"==typeof b[0]){var d="#*$~&";return this.each(function(){var e=a(this).data("tooltipster-ns"),f=e?a(this).data(e[0]):null;if(!f)throw new Error("You called Tooltipster's \""+b[0]+'" method on an uninitialized element');if("function"!=typeof f[b[0]])throw new Error('Unknown method "'+b[0]+'"');this.length>1&&"content"==b[0]&&(b[1]instanceof a||"object"==typeof b[1]&&null!=b[1]&&b[1].tagName)&&!f.__options.contentCloning&&f.__options.debug&&console.log(c);var g=f[b[0]](b[1],b[2]);return g!==f||"instance"===b[0]?(d=g,!1):void 0}),"#*$~&"!==d?d:this}a.tooltipster.__instancesLatestArr=[];var e=b[0]&&void 0!==b[0].multiple,g=e&&b[0].multiple||!e&&f.multiple,h=b[0]&&void 0!==b[0].content,i=h&&b[0].content||!h&&f.content,j=b[0]&&void 0!==b[0].contentCloning,k=j&&b[0].contentCloning||!j&&f.contentCloning,l=b[0]&&void 0!==b[0].debug,m=l&&b[0].debug||!l&&f.debug;return this.length>1&&(i instanceof a||"object"==typeof i&&null!=i&&i.tagName)&&!k&&m&&console.log(c),this.each(function(){var c=!1,d=a(this),e=d.data("tooltipster-ns"),f=null;e?g?c=!0:m&&(console.log("Tooltipster: one or more tooltips are already attached to the element below. Ignoring."),console.log(this)):c=!0,c&&(f=new a.Tooltipster(this,b[0]),e||(e=[]),e.push(f.__namespace),d.data("tooltipster-ns",e),d.data(f.__namespace,f),f.__options.functionInit&&f.__options.functionInit.call(f,f,{origin:this}),f._trigger("init")),a.tooltipster.__instancesLatestArr.push(f)}),this},b.prototype={__init:function(b){this.__$tooltip=b,this.__$tooltip.css({left:0,overflow:"hidden",position:"absolute",top:0}).find(".tooltipster-content").css("overflow","auto"),this.$container=a('
    ').append(this.__$tooltip).appendTo(h.window.document.body)},__forceRedraw:function(){var a=this.__$tooltip.parent();this.__$tooltip.detach(),this.__$tooltip.appendTo(a)},constrain:function(a,b){return this.constraints={width:a,height:b},this.__$tooltip.css({display:"block",height:"",overflow:"auto",width:a}),this},destroy:function(){this.__$tooltip.detach().find(".tooltipster-content").css({display:"",overflow:""}),this.$container.remove()},free:function(){return this.constraints=null,this.__$tooltip.css({display:"",height:"",overflow:"visible",width:""}),this},measure:function(){this.__forceRedraw();var a=this.__$tooltip[0].getBoundingClientRect(),b={size:{height:a.height||a.bottom-a.top,width:a.width||a.right-a.left}};if(this.constraints){var c=this.__$tooltip.find(".tooltipster-content"),d=this.__$tooltip.outerHeight(),e=c[0].getBoundingClientRect(),f={height:d<=this.constraints.height,width:a.width<=this.constraints.width&&e.width>=c[0].scrollWidth-1};b.fits=f.height&&f.width}return h.IE&&h.IE<=11&&b.size.width!==h.window.document.documentElement.clientWidth&&(b.size.width=Math.ceil(b.size.width)+1),b}};var j=navigator.userAgent.toLowerCase();-1!=j.indexOf("msie")?h.IE=parseInt(j.split("msie")[1]):-1!==j.toLowerCase().indexOf("trident")&&-1!==j.indexOf(" rv:11")?h.IE=11:-1!=j.toLowerCase().indexOf("edge/")&&(h.IE=parseInt(j.toLowerCase().split("edge/")[1]))}); +\ No newline at end of file ++/*! tooltipster v4.2.8 */!function(a,b){void 0===a&&void 0!==window&&(a=window),"function"==typeof define&&define.amd?define(["jquery"],function(a){return b(a)}):"object"==typeof module&&module.exports?module.exports=b(require("jquery")):b(a.jQuery)}(this,function(a){function b(a){this.$container,this.constraints=null,this.__$tooltip,this.__init(a)}function c(b,c){var d=!0;return a.each(b,function(a,e){if(void 0===c[a]||b[a]!==c[a])return d=!1,!1}),d}function d(b){var c=b.attr("id"),d=c?h.window.document.getElementById(c):null;return d?d===b[0]:a.contains(h.window.document.body,b[0])}function e(){if(!g)return!1;var a=g.document.body||g.document.documentElement,b=a.style,c="transition",d=["Moz","Webkit","Khtml","O","ms"];if("string"==typeof b[c])return!0;c=c.charAt(0).toUpperCase()+c.substr(1);for(var e=0;e0?e=c.__plugins[d]:a.each(c.__plugins,function(a,b){if(b.name.substring(b.name.length-d.length-1)=="."+d)return e=b,!1}),e}if(b.name.indexOf(".")<0)throw new Error("Plugins must be namespaced");return c.__plugins[b.name]=b,b.core&&c.__bridge(b.core,c,b.name),this},_trigger:function(){var a=Array.prototype.slice.apply(arguments);return"string"==typeof a[0]&&(a[0]={type:a[0]}),this.__$emitterPrivate.trigger.apply(this.__$emitterPrivate,a),this.__$emitterPublic.trigger.apply(this.__$emitterPublic,a),this},instances:function(b){var c=[],d=b||".tooltipstered";return a(d).each(function(){var b=a(this),d=b.data("tooltipster-ns");d&&a.each(d,function(a,d){c.push(b.data(d))})}),c},instancesLatest:function(){return this.__instancesLatestArr},off:function(){return this.__$emitterPublic.off.apply(this.__$emitterPublic,Array.prototype.slice.apply(arguments)),this},on:function(){return this.__$emitterPublic.on.apply(this.__$emitterPublic,Array.prototype.slice.apply(arguments)),this},one:function(){return this.__$emitterPublic.one.apply(this.__$emitterPublic,Array.prototype.slice.apply(arguments)),this},origins:function(b){var c=b?b+" ":"";return a(c+".tooltipstered").toArray()},setDefaults:function(b){return a.extend(f,b),this},triggerHandler:function(){return this.__$emitterPublic.triggerHandler.apply(this.__$emitterPublic,Array.prototype.slice.apply(arguments)),this}},a.tooltipster=new i,a.Tooltipster=function(b,c){this.__callbacks={close:[],open:[]},this.__closingTime,this.__Content,this.__contentBcr,this.__destroyed=!1,this.__$emitterPrivate=a({}),this.__$emitterPublic=a({}),this.__enabled=!0,this.__garbageCollector,this.__Geometry,this.__lastPosition,this.__namespace="tooltipster-"+Math.round(1e6*Math.random()),this.__options,this.__$originParents,this.__pointerIsOverOrigin=!1,this.__previousThemes=[],this.__state="closed",this.__timeouts={close:[],open:null},this.__touchEvents=[],this.__tracker=null,this._$origin,this._$tooltip,this.__init(b,c)},a.Tooltipster.prototype={__init:function(b,c){var d=this;if(d._$origin=a(b),d.__options=a.extend(!0,{},f,c),d.__optionsFormat(),!h.IE||h.IE>=d.__options.IEmin){var e=null;if(void 0===d._$origin.data("tooltipster-initialTitle")&&(e=d._$origin.attr("title"),void 0===e&&(e=null),d._$origin.data("tooltipster-initialTitle",e)),null!==d.__options.content)d.__contentSet(d.__options.content);else{var g,i=d._$origin.attr("data-tooltip-content");i&&(g=a(i)),g&&g[0]?d.__contentSet(g.first()):d.__contentSet(e)}d._$origin.removeAttr("title").addClass("tooltipstered"),d.__prepareOrigin(),d.__prepareGC(),a.each(d.__options.plugins,function(a,b){d._plug(b)}),h.hasTouchCapability&&a(h.window.document.body).on("touchmove."+d.__namespace+"-triggerOpen",function(a){d._touchRecordEvent(a)}),d._on("created",function(){d.__prepareTooltip()})._on("repositioned",function(a){d.__lastPosition=a.position})}else d.__options.disabled=!0},__contentInsert:function(){var a=this,b=a._$tooltip.find(".tooltipster-content"),c=a.__Content,d=function(a){c=a};return a._trigger({type:"format",content:a.__Content,format:d}),a.__options.functionFormat&&(c=a.__options.functionFormat.call(a,a,{origin:a._$origin[0]},a.__Content)),"string"!=typeof c||a.__options.contentAsHTML?b.empty().append(c):b.text(c),a},__contentSet:function(b){return b instanceof a&&this.__options.contentCloning&&(b=b.clone(!0)),this.__Content=b,this._trigger({type:"updated",content:b}),this},__destroyError:function(){throw new Error("This tooltip has been destroyed and cannot execute your method call.")},__geometry:function(){var b=this,c=b._$origin,d=b._$origin.is("area");if(d){var e=b._$origin.parent().attr("name");c=a('img[usemap="#'+e+'"]')}var f=c[0].getBoundingClientRect(),g=a(h.window.document),i=a(h.window),j=c,k={available:{document:null,window:null},document:{size:{height:g.height(),width:g.width()}},window:{scroll:{left:h.window.scrollX||h.window.document.documentElement.scrollLeft,top:h.window.scrollY||h.window.document.documentElement.scrollTop},size:{height:i.height(),width:i.width()}},origin:{fixedLineage:!1,offset:{},size:{height:f.bottom-f.top,width:f.right-f.left},usemapImage:d?c[0]:null,windowOffset:{bottom:f.bottom,left:f.left,right:f.right,top:f.top}}};if(d){var l=b._$origin.attr("shape"),m=b._$origin.attr("coords");if(m&&(m=m.split(","),a.map(m,function(a,b){m[b]=parseInt(a)})),"default"!=l)switch(l){case"circle":var n=m[0],o=m[1],p=m[2],q=o-p,r=n-p;k.origin.size.height=2*p,k.origin.size.width=k.origin.size.height,k.origin.windowOffset.left+=r,k.origin.windowOffset.top+=q;break;case"rect":var s=m[0],t=m[1],u=m[2],v=m[3];k.origin.size.height=v-t,k.origin.size.width=u-s,k.origin.windowOffset.left+=s,k.origin.windowOffset.top+=t;break;case"poly":for(var w=0,x=0,y=0,z=0,A="even",B=0;By&&(y=C,0===B&&(w=y)),Cz&&(z=C,1==B&&(x=z)),C6e4}),d(b._$origin)||b.close(function(){b.destroy()})},2e4):clearInterval(b.__garbageCollector),b},__prepareOrigin:function(){var a=this;if(a._$origin.off("."+a.__namespace+"-triggerOpen"),h.hasTouchCapability&&a._$origin.on("touchstart."+a.__namespace+"-triggerOpen touchend."+a.__namespace+"-triggerOpen touchcancel."+a.__namespace+"-triggerOpen",function(b){a._touchRecordEvent(b)}),a.__options.triggerOpen.click||a.__options.triggerOpen.tap&&h.hasTouchCapability){var b="";a.__options.triggerOpen.click&&(b+="click."+a.__namespace+"-triggerOpen "),a.__options.triggerOpen.tap&&h.hasTouchCapability&&(b+="touchend."+a.__namespace+"-triggerOpen"),a._$origin.on(b,function(b){a._touchIsMeaningfulEvent(b)&&a._open(b)})}if(a.__options.triggerOpen.mouseenter||a.__options.triggerOpen.touchstart&&h.hasTouchCapability){var b="";a.__options.triggerOpen.mouseenter&&(b+="mouseenter."+a.__namespace+"-triggerOpen "),a.__options.triggerOpen.touchstart&&h.hasTouchCapability&&(b+="touchstart."+a.__namespace+"-triggerOpen"),a._$origin.on(b,function(b){!a._touchIsTouchEvent(b)&&a._touchIsEmulatedEvent(b)||(a.__pointerIsOverOrigin=!0,a._openShortly(b))})}if(a.__options.triggerClose.mouseleave||a.__options.triggerClose.touchleave&&h.hasTouchCapability){var b="";a.__options.triggerClose.mouseleave&&(b+="mouseleave."+a.__namespace+"-triggerOpen "),a.__options.triggerClose.touchleave&&h.hasTouchCapability&&(b+="touchend."+a.__namespace+"-triggerOpen touchcancel."+a.__namespace+"-triggerOpen"),a._$origin.on(b,function(b){a._touchIsMeaningfulEvent(b)&&(a.__pointerIsOverOrigin=!1)})}return a},__prepareTooltip:function(){var b=this,c=b.__options.interactive?"auto":"";return b._$tooltip.attr("id",b.__namespace).css({"pointer-events":c,zIndex:b.__options.zIndex}),a.each(b.__previousThemes,function(a,c){b._$tooltip.removeClass(c)}),a.each(b.__options.theme,function(a,c){b._$tooltip.addClass(c)}),b.__previousThemes=a.merge([],b.__options.theme),b},__scrollHandler:function(b){var c=this;if(c.__options.triggerClose.scroll)c._close(b);else if(d(c._$origin)&&d(c._$tooltip)){var e=null;if(b.target===h.window.document)c.__Geometry.origin.fixedLineage||c.__options.repositionOnScroll&&c.reposition(b);else{e=c.__geometry();var f=!1;if("fixed"!=c._$origin.css("position")&&c.__$originParents.each(function(b,d){var g=a(d),h=g.css("overflow-x"),i=g.css("overflow-y");if("visible"!=h||"visible"!=i){var j=d.getBoundingClientRect();if("visible"!=h&&(e.origin.windowOffset.leftj.right))return f=!0,!1;if("visible"!=i&&(e.origin.windowOffset.topj.bottom)&&(f=!c.__options.checkOverflowY||c.__options.checkOverflowY(e,j)))return!1}if("fixed"==g.css("position"))return!1}),f)c._$tooltip.css("visibility","hidden");else if(c._$tooltip.css("visibility","visible"),c.__options.repositionOnScroll)c.reposition(b);else{var g=e.origin.offset.left-c.__Geometry.origin.offset.left,i=e.origin.offset.top-c.__Geometry.origin.offset.top;c._$tooltip.css({left:c.__lastPosition.coord.left+g,top:c.__lastPosition.coord.top+i})}}c._trigger({type:"scroll",event:b,geo:e})}return c},__stateSet:function(a){return this.__state=a,this._trigger({type:"state",state:a}),this},__timeoutsClear:function(){return clearTimeout(this.__timeouts.open),this.__timeouts.open=null,a.each(this.__timeouts.close,function(a,b){clearTimeout(b)}),this.__timeouts.close=[],this},__trackerStart:function(){var a=this,b=a._$tooltip.find(".tooltipster-content");return a.__options.trackTooltip&&(a.__contentBcr=b[0].getBoundingClientRect()),a.__tracker=setInterval(function(){if(d(a._$origin)&&d(a._$tooltip)){if(a.__options.trackOrigin){var e=a.__geometry(),f=!1;c(e.origin.size,a.__Geometry.origin.size)&&(a.__Geometry.origin.fixedLineage?c(e.origin.windowOffset,a.__Geometry.origin.windowOffset)&&(f=!0):c(e.origin.offset,a.__Geometry.origin.offset)&&(f=!0)),f||(a.__options.triggerClose.mouseleave&&!a.__options.ignoreCloseOnScroll?a._close():a.reposition())}if(a.__options.trackTooltip){var g=b[0].getBoundingClientRect();g.height===a.__contentBcr.height&&g.width===a.__contentBcr.width||(a.reposition(),a.__contentBcr=g)}}else a._close()},a.__options.trackerInterval),a},_close:function(b,c,d){var e=this,f=!0;if(e._trigger({type:"close",event:b,stop:function(){f=!1}}),f||d){c&&e.__callbacks.close.push(c),e.__callbacks.open=[],e.__timeoutsClear();var g=function(){a.each(e.__callbacks.close,function(a,c){c.call(e,e,{event:b,origin:e._$origin[0]})}),e.__callbacks.close=[]};if("closed"!=e.__state){var i=!0,j=new Date,k=j.getTime(),l=k+e.__options.animationDuration[1];if("disappearing"==e.__state&&l>e.__closingTime&&e.__options.animationDuration[1]>0&&(i=!1),i){e.__closingTime=l,"disappearing"!=e.__state&&e.__stateSet("disappearing");var m=function(){clearInterval(e.__tracker),e._trigger({type:"closing",event:b}),e._$tooltip.off("."+e.__namespace+"-triggerClose").removeClass("tooltipster-dying"),a(h.window).off("."+e.__namespace+"-triggerClose"),e.__$originParents.each(function(b,c){a(c).off("scroll."+e.__namespace+"-triggerClose")}),e.__$originParents=null,a(h.window.document.body).off("."+e.__namespace+"-triggerClose"),e._$origin.off("."+e.__namespace+"-triggerClose"),e._off("dismissable"),e.__stateSet("closed"),e._trigger({type:"after",event:b}),e.__options.functionAfter&&e.__options.functionAfter.call(e,e,{event:b,origin:e._$origin[0]}),g()};h.hasTransitions?(e._$tooltip.css({"-moz-animation-duration":e.__options.animationDuration[1]+"ms","-ms-animation-duration":e.__options.animationDuration[1]+"ms","-o-animation-duration":e.__options.animationDuration[1]+"ms","-webkit-animation-duration":e.__options.animationDuration[1]+"ms","animation-duration":e.__options.animationDuration[1]+"ms","transition-duration":e.__options.animationDuration[1]+"ms"}),e._$tooltip.clearQueue().removeClass("tooltipster-show").addClass("tooltipster-dying"),e.__options.animationDuration[1]>0&&e._$tooltip.delay(e.__options.animationDuration[1]),e._$tooltip.queue(m)):e._$tooltip.stop().fadeOut(e.__options.animationDuration[1],m)}}else g()}return e},_off:function(){return this.__$emitterPrivate.off.apply(this.__$emitterPrivate,Array.prototype.slice.apply(arguments)),this},_on:function(){return this.__$emitterPrivate.on.apply(this.__$emitterPrivate,Array.prototype.slice.apply(arguments)),this},_one:function(){return this.__$emitterPrivate.one.apply(this.__$emitterPrivate,Array.prototype.slice.apply(arguments)),this},_open:function(b,c){var e=this;if(!e.__destroying&&d(e._$origin)&&e.__enabled){var f=!0;if("closed"==e.__state&&(e._trigger({type:"before",event:b,stop:function(){f=!1}}),f&&e.__options.functionBefore&&(f=e.__options.functionBefore.call(e,e,{event:b,origin:e._$origin[0]}))),f!==!1&&null!==e.__Content){c&&e.__callbacks.open.push(c),e.__callbacks.close=[],e.__timeoutsClear();var g,i=function(){"stable"!=e.__state&&e.__stateSet("stable"),a.each(e.__callbacks.open,function(a,b){b.call(e,e,{origin:e._$origin[0],tooltip:e._$tooltip[0]})}),e.__callbacks.open=[]};if("closed"!==e.__state)g=0,"disappearing"===e.__state?(e.__stateSet("appearing"),h.hasTransitions?(e._$tooltip.clearQueue().removeClass("tooltipster-dying").addClass("tooltipster-show"),e.__options.animationDuration[0]>0&&e._$tooltip.delay(e.__options.animationDuration[0]),e._$tooltip.queue(i)):e._$tooltip.stop().fadeIn(i)):"stable"==e.__state&&i();else{if(e.__stateSet("appearing"),g=e.__options.animationDuration[0],e.__contentInsert(),e.reposition(b,!0),h.hasTransitions?(e._$tooltip.addClass("tooltipster-"+e.__options.animation).addClass("tooltipster-initial").css({"-moz-animation-duration":e.__options.animationDuration[0]+"ms","-ms-animation-duration":e.__options.animationDuration[0]+"ms","-o-animation-duration":e.__options.animationDuration[0]+"ms","-webkit-animation-duration":e.__options.animationDuration[0]+"ms","animation-duration":e.__options.animationDuration[0]+"ms","transition-duration":e.__options.animationDuration[0]+"ms"}),setTimeout(function(){"closed"!=e.__state&&(e._$tooltip.addClass("tooltipster-show").removeClass("tooltipster-initial"),e.__options.animationDuration[0]>0&&e._$tooltip.delay(e.__options.animationDuration[0]),e._$tooltip.queue(i))},0)):e._$tooltip.css("display","none").fadeIn(e.__options.animationDuration[0],i),e.__trackerStart(),a(h.window).on("resize."+e.__namespace+"-triggerClose",function(b){var c=a(document.activeElement);(c.is("input")||c.is("textarea"))&&a.contains(e._$tooltip[0],c[0])||e.reposition(b)}).on("scroll."+e.__namespace+"-triggerClose",function(a){e.__scrollHandler(a)}),e.__$originParents=e._$origin.parents(),e.__$originParents.each(function(b,c){a(c).on("scroll."+e.__namespace+"-triggerClose",function(a){e.__scrollHandler(a)})}),e.__options.triggerClose.mouseleave||e.__options.triggerClose.touchleave&&h.hasTouchCapability){e._on("dismissable",function(a){a.dismissable?a.delay?(m=setTimeout(function(){e._close(a.event)},a.delay),e.__timeouts.close.push(m)):e._close(a):clearTimeout(m)});var j=e._$origin,k="",l="",m=null;e.__options.interactive&&(j=j.add(e._$tooltip)),e.__options.triggerClose.mouseleave&&(k+="mouseenter."+e.__namespace+"-triggerClose ",l+="mouseleave."+e.__namespace+"-triggerClose "),e.__options.triggerClose.touchleave&&h.hasTouchCapability&&(k+="touchstart."+e.__namespace+"-triggerClose",l+="touchend."+e.__namespace+"-triggerClose touchcancel."+e.__namespace+"-triggerClose"),j.on(l,function(a){if(e._touchIsTouchEvent(a)||!e._touchIsEmulatedEvent(a)){var b="mouseleave"==a.type?e.__options.delay:e.__options.delayTouch;e._trigger({delay:b[1],dismissable:!0,event:a,type:"dismissable"})}}).on(k,function(a){!e._touchIsTouchEvent(a)&&e._touchIsEmulatedEvent(a)||e._trigger({dismissable:!1,event:a,type:"dismissable"})})}e.__options.triggerClose.originClick&&e._$origin.on("click."+e.__namespace+"-triggerClose",function(a){e._touchIsTouchEvent(a)||e._touchIsEmulatedEvent(a)||e._close(a)}),(e.__options.triggerClose.click||e.__options.triggerClose.tap&&h.hasTouchCapability)&&setTimeout(function(){if("closed"!=e.__state){var b="",c=a(h.window.document.body);e.__options.triggerClose.click&&(b+="click."+e.__namespace+"-triggerClose "),e.__options.triggerClose.tap&&h.hasTouchCapability&&(b+="touchend."+e.__namespace+"-triggerClose"),c.on(b,function(b){e._touchIsMeaningfulEvent(b)&&(e._touchRecordEvent(b),e.__options.interactive&&a.contains(e._$tooltip[0],b.target)||e._close(b))}),e.__options.triggerClose.tap&&h.hasTouchCapability&&c.on("touchstart."+e.__namespace+"-triggerClose",function(a){e._touchRecordEvent(a)})}},0),e._trigger("ready"),e.__options.functionReady&&e.__options.functionReady.call(e,e,{origin:e._$origin[0],tooltip:e._$tooltip[0]})}if(e.__options.timer>0){var m=setTimeout(function(){e._close()},e.__options.timer+g);e.__timeouts.close.push(m)}}}return e},_openShortly:function(a){var b=this,c=!0;if("stable"!=b.__state&&"appearing"!=b.__state&&!b.__timeouts.open&&(b._trigger({type:"start",event:a,stop:function(){c=!1}}),c)){var d=0==a.type.indexOf("touch")?b.__options.delayTouch:b.__options.delay;d[0]?b.__timeouts.open=setTimeout(function(){b.__timeouts.open=null,b.__pointerIsOverOrigin&&b._touchIsMeaningfulEvent(a)?(b._trigger("startend"),b._open(a)):b._trigger("startcancel")},d[0]):(b._trigger("startend"),b._open(a))}return b},_optionsExtract:function(b,c){var d=this,e=a.extend(!0,{},c),f=d.__options[b];return f||(f={},a.each(c,function(a,b){var c=d.__options[a];void 0!==c&&(f[a]=c)})),a.each(e,function(b,c){void 0!==f[b]&&("object"!=typeof c||c instanceof Array||null==c||"object"!=typeof f[b]||f[b]instanceof Array||null==f[b]?e[b]=f[b]:a.extend(e[b],f[b]))}),e},_plug:function(b){var c=a.tooltipster._plugin(b);if(!c)throw new Error('The "'+b+'" plugin is not defined');return c.instance&&a.tooltipster.__bridge(c.instance,this,c.name),this},_touchIsEmulatedEvent:function(a){for(var b=!1,c=(new Date).getTime(),d=this.__touchEvents.length-1;d>=0;d--){var e=this.__touchEvents[d];if(!(c-e.time<500))break;e.target===a.target&&(b=!0)}return b},_touchIsMeaningfulEvent:function(a){return this._touchIsTouchEvent(a)&&!this._touchSwiped(a.target)||!this._touchIsTouchEvent(a)&&!this._touchIsEmulatedEvent(a)},_touchIsTouchEvent:function(a){return 0==a.type.indexOf("touch")},_touchRecordEvent:function(a){return this._touchIsTouchEvent(a)&&(a.time=(new Date).getTime(),this.__touchEvents.push(a)),this},_touchSwiped:function(a){for(var b=!1,c=this.__touchEvents.length-1;c>=0;c--){var d=this.__touchEvents[c];if("touchmove"==d.type){b=!0;break}if("touchstart"==d.type&&a===d.target)break}return b},_trigger:function(){var b=Array.prototype.slice.apply(arguments);return"string"==typeof b[0]&&(b[0]={type:b[0]}),b[0].instance=this,b[0].origin=this._$origin?this._$origin[0]:null,b[0].tooltip=this._$tooltip?this._$tooltip[0]:null,this.__$emitterPrivate.trigger.apply(this.__$emitterPrivate,b),a.tooltipster._trigger.apply(a.tooltipster,b),this.__$emitterPublic.trigger.apply(this.__$emitterPublic,b),this},_unplug:function(b){var c=this;if(c[b]){var d=a.tooltipster._plugin(b);d.instance&&a.each(d.instance,function(a,d){c[a]&&c[a].bridged===c[b]&&delete c[a]}),c[b].__destroy&&c[b].__destroy(),delete c[b]}return c},close:function(a){return this.__destroyed?this.__destroyError():this._close(null,a),this},content:function(a){var b=this;if(void 0===a)return b.__Content;if(b.__destroyed)b.__destroyError();else if(b.__contentSet(a),null!==b.__Content){if("closed"!==b.__state&&(b.__contentInsert(),b.reposition(),b.__options.updateAnimation))if(h.hasTransitions){var c=b.__options.updateAnimation;b._$tooltip.addClass("tooltipster-update-"+c),setTimeout(function(){"closed"!=b.__state&&b._$tooltip.removeClass("tooltipster-update-"+c)},1e3)}else b._$tooltip.fadeTo(200,.5,function(){"closed"!=b.__state&&b._$tooltip.fadeTo(200,1)})}else b._close();return b},destroy:function(){var b=this;if(b.__destroyed)b.__destroyError();else{"closed"!=b.__state?b.option("animationDuration",0)._close(null,null,!0):b.__timeoutsClear(),b._trigger("destroy"),b.__destroyed=!0,b._$origin.removeData(b.__namespace).off("."+b.__namespace+"-triggerOpen"),a(h.window.document.body).off("."+b.__namespace+"-triggerOpen");var c=b._$origin.data("tooltipster-ns");if(c)if(1===c.length){var d=null;"previous"==b.__options.restoration?d=b._$origin.data("tooltipster-initialTitle"):"current"==b.__options.restoration&&(d="string"==typeof b.__Content?b.__Content:a("
    ").append(b.__Content).html()),d&&b._$origin.attr("title",d),b._$origin.removeClass("tooltipstered"),b._$origin.removeData("tooltipster-ns").removeData("tooltipster-initialTitle")}else c=a.grep(c,function(a,c){return a!==b.__namespace}),b._$origin.data("tooltipster-ns",c);b._trigger("destroyed"),b._off(),b.off(),b.__Content=null,b.__$emitterPrivate=null,b.__$emitterPublic=null,b.__options.parent=null,b._$origin=null,b._$tooltip=null,a.tooltipster.__instancesLatestArr=a.grep(a.tooltipster.__instancesLatestArr,function(a,c){return b!==a}),clearInterval(b.__garbageCollector)}return b},disable:function(){return this.__destroyed?(this.__destroyError(),this):(this._close(),this.__enabled=!1,this)},elementOrigin:function(){return this.__destroyed?void this.__destroyError():this._$origin[0]},elementTooltip:function(){return this._$tooltip?this._$tooltip[0]:null},enable:function(){return this.__enabled=!0,this},hide:function(a){return this.close(a)},instance:function(){return this},off:function(){return this.__destroyed||this.__$emitterPublic.off.apply(this.__$emitterPublic,Array.prototype.slice.apply(arguments)),this},on:function(){return this.__destroyed?this.__destroyError():this.__$emitterPublic.on.apply(this.__$emitterPublic,Array.prototype.slice.apply(arguments)),this},one:function(){return this.__destroyed?this.__destroyError():this.__$emitterPublic.one.apply(this.__$emitterPublic,Array.prototype.slice.apply(arguments)),this},open:function(a){return this.__destroyed?this.__destroyError():this._open(null,a),this},option:function(b,c){return void 0===c?this.__options[b]:(this.__destroyed?this.__destroyError():(this.__options[b]=c,this.__optionsFormat(),a.inArray(b,["trigger","triggerClose","triggerOpen"])>=0&&this.__prepareOrigin(),"selfDestruction"===b&&this.__prepareGC()),this)},reposition:function(a,b){var c=this;return c.__destroyed?c.__destroyError():"closed"!=c.__state&&d(c._$origin)&&(b||d(c._$tooltip))&&(b||c._$tooltip.detach(),c.__Geometry=c.__geometry(),c._trigger({type:"reposition",event:a,helper:{geo:c.__Geometry}})),c},show:function(a){return this.open(a)},status:function(){return{destroyed:this.__destroyed,enabled:this.__enabled,open:"closed"!==this.__state,state:this.__state}},triggerHandler:function(){return this.__destroyed?this.__destroyError():this.__$emitterPublic.triggerHandler.apply(this.__$emitterPublic,Array.prototype.slice.apply(arguments)),this}},a.fn.tooltipster=function(){var b=Array.prototype.slice.apply(arguments),c="You are using a single HTML element as content for several tooltips. You probably want to set the contentCloning option to TRUE.";if(0===this.length)return this;if("string"==typeof b[0]){var d="#*$~&";return this.each(function(){var e=a(this).data("tooltipster-ns"),f=e?a(this).data(e[0]):null;if(!f)throw new Error("You called Tooltipster's \""+b[0]+'" method on an uninitialized element');if("function"!=typeof f[b[0]])throw new Error('Unknown method "'+b[0]+'"');this.length>1&&"content"==b[0]&&(b[1]instanceof a||"object"==typeof b[1]&&null!=b[1]&&b[1].tagName)&&!f.__options.contentCloning&&f.__options.debug&&console.log(c);var g=f[b[0]](b[1],b[2]);if(g!==f||"instance"===b[0])return d=g,!1}),"#*$~&"!==d?d:this}a.tooltipster.__instancesLatestArr=[];var e=b[0]&&void 0!==b[0].multiple,g=e&&b[0].multiple||!e&&f.multiple,h=b[0]&&void 0!==b[0].content,i=h&&b[0].content||!h&&f.content,j=b[0]&&void 0!==b[0].contentCloning,k=j&&b[0].contentCloning||!j&&f.contentCloning,l=b[0]&&void 0!==b[0].debug,m=l&&b[0].debug||!l&&f.debug;return this.length>1&&(i instanceof a||"object"==typeof i&&null!=i&&i.tagName)&&!k&&m&&console.log(c),this.each(function(){var c=!1,d=a(this),e=d.data("tooltipster-ns"),f=null;e?g?c=!0:m&&(console.log("Tooltipster: one or more tooltips are already attached to the element below. Ignoring."),console.log(this)):c=!0,c&&(f=new a.Tooltipster(this,b[0]),e||(e=[]),e.push(f.__namespace),d.data("tooltipster-ns",e),d.data(f.__namespace,f),f.__options.functionInit&&f.__options.functionInit.call(f,f,{origin:this}),f._trigger("init")),a.tooltipster.__instancesLatestArr.push(f)}),this},b.prototype={__init:function(b){this.__$tooltip=b,this.__$tooltip.css({left:0,overflow:"hidden",position:"absolute",top:0}).find(".tooltipster-content").css("overflow","auto"),this.$container=a('
    ').append(this.__$tooltip).appendTo(h.window.document.body)},__forceRedraw:function(){var a=this.__$tooltip.parent();this.__$tooltip.detach(),this.__$tooltip.appendTo(a)},constrain:function(a,b){return this.constraints={width:a,height:b},this.__$tooltip.css({display:"block",height:"",overflow:"auto",width:a}),this},destroy:function(){this.__$tooltip.detach().find(".tooltipster-content").css({display:"",overflow:""}),this.$container.remove()},free:function(){return this.constraints=null,this.__$tooltip.css({display:"",height:"",overflow:"visible",width:""}),this},measure:function(){this.__forceRedraw();var a=this.__$tooltip[0].getBoundingClientRect(),b={size:{height:a.height||a.bottom-a.top,width:a.width||a.right-a.left}};if(this.constraints){var c=this.__$tooltip.find(".tooltipster-content"),d=this.__$tooltip.outerHeight(),e=c[0].getBoundingClientRect(),f={height:d<=this.constraints.height,width:a.width<=this.constraints.width&&e.width>=c[0].scrollWidth-1};b.fits=f.height&&f.width}return h.IE&&h.IE<=11&&b.size.width!==h.window.document.documentElement.clientWidth&&(b.size.width=Math.ceil(b.size.width)+1),b}};var j=navigator.userAgent.toLowerCase();return j.indexOf("msie")!=-1?h.IE=parseInt(j.split("msie")[1]):j.toLowerCase().indexOf("trident")!==-1&&j.indexOf(" rv:11")!==-1?h.IE=11:j.toLowerCase().indexOf("edge/")!=-1&&(h.IE=parseInt(j.toLowerCase().split("edge/")[1])),a}); +\ No newline at end of file +diff --git a/node_modules/tooltipster/src/js/tooltipster.js b/node_modules/tooltipster/src/js/tooltipster.js +index bcf5abd..d22ed9e 100644 +--- a/node_modules/tooltipster/src/js/tooltipster.js ++++ b/node_modules/tooltipster/src/js/tooltipster.js +@@ -1238,8 +1238,14 @@ $.Tooltipster.prototype = { + if ( geo.origin.windowOffset.top < bcr.top + || geo.origin.windowOffset.bottom > bcr.bottom + ) { +- overflows = true; +- return false; ++ if (self.__options.checkOverflowY) { ++ overflows = self.__options.checkOverflowY(geo, bcr); ++ } else { ++ overflows = true; ++ } ++ if (overflows) { ++ return false; ++ } + } + } + } +@@ -1390,7 +1396,7 @@ $.Tooltipster.prototype = { + + // close the tooltip when using the mouseleave close trigger + // (see https://github.com/iamceege/tooltipster/pull/253) +- if (self.__options.triggerClose.mouseleave) { ++ if (self.__options.triggerClose.mouseleave && !self.__options.ignoreCloseOnScroll) { + self._close(); + } + else { 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 106c33a530..f835c0578b 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 @@ -53,7 +53,7 @@ import { ImagePipe } from '@shared/pipe/image.pipe'; import { TbMap } from '@home/components/widget/lib/maps/map'; import { createColorMarkerIconElement, - createColorMarkerShapeURI, + createColorMarkerShapeURI, MarkerIconContainer, MarkerShape } from '@shared/models/widget/maps/marker-shape.models'; import { MatIconRegistry } from '@angular/material/icon'; @@ -168,8 +168,10 @@ export class MarkerDataProcessor { - return createColorMarkerIconElement(this.dataLayer.getCtx().$injector.get(MatIconRegistry), this.dataLayer.getCtx().$injector.get(DomSanitizer), icon, color, this.trip).pipe( + public createColoredMarkerIcon(iconContainer: MarkerIconContainer, + icon: string, color: tinycolor.Instance, rotationAngle = 0, size = 34): Observable { + return createColorMarkerIconElement(this.dataLayer.getCtx().$injector.get(MatIconRegistry), this.dataLayer.getCtx().$injector.get(DomSanitizer), + iconContainer, icon, color, this.trip).pipe( map((element) => { if (rotationAngle !== 0) { element.style.transform = `rotate(${rotationAngle}deg)`; @@ -289,7 +291,7 @@ class IconMarkerIconProcessor extends BaseColorMarkerShapeProcessor { - return this.dataProcessor.createColoredMarkerIcon(this.settings.icon, color, rotationAngle, size); + return this.dataProcessor.createColoredMarkerIcon(this.settings.iconContainer, this.settings.icon, color, rotationAngle, size); } } diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/maps/map.scss b/ui-ngx/src/app/modules/home/components/widget/lib/maps/map.scss index fc60862be4..592610f8b2 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/maps/map.scss +++ b/ui-ngx/src/app/modules/home/components/widget/lib/maps/map.scss @@ -15,6 +15,14 @@ */ @import '../../../../../../../scss/constants'; +div.tb-widget .tb-widget-content.tb-no-interaction { + .tb-map { + .leaflet-interactive, .leaflet-control { + pointer-events: none; + } + } +} + .tb-map-container, .tb-map-layout { position: relative; display: flex; diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/map/map-data-layer-dialog.component.html b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/map/map-data-layer-dialog.component.html index ecabc191e2..7cf5ba9a7f 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/map/map-data-layer-dialog.component.html +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/map/map-data-layer-dialog.component.html @@ -224,6 +224,10 @@
    + @if (dataLayerType === 'trips') { + + + }
    widgets.maps.data-layer.marker.position-conversion
    @@ -244,7 +248,7 @@ - {{ 'widgets.maps.data-layer.path.path' | translate }} +
    {{ 'widgets.maps.data-layer.path.path' | translate }}
    @@ -322,7 +326,7 @@ - {{ 'widgets.maps.data-layer.points.points' | translate }} +
    {{ 'widgets.maps.data-layer.points.points' | translate }}
    @@ -364,32 +368,13 @@
    - - - - - -
    -
    widgets.maps.data-layer.behavior
    -
    -
    widgets.maps.data-layer.on-click
    - - -
    + @if (dataLayerType !== 'trips') { + + }
    + @if (dataLayerType !== 'trips') { + + }
    {{ 'widgets.maps.data-layer.groups' | translate }}
    + + + + + + + + + + + + +
    +
    widgets.maps.data-layer.behavior
    +
    +
    widgets.maps.data-layer.on-click
    + + +
    +
    +
    +
    diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/map/marker-icon-shapes.component.html b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/map/marker-icon-shapes.component.html new file mode 100644 index 0000000000..a7e62694df --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/map/marker-icon-shapes.component.html @@ -0,0 +1,60 @@ + +
    +
    widgets.maps.data-layer.marker.marker-icon
    + +
    widgets.maps.data-layer.marker.marker-appearance
    +
    + + @if (containerInfo.iconContainer === iconContainer) { + + } @else { + + } + +
    +
    + + + +
    +
    diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/map/marker-icon-shapes.component.scss b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/map/marker-icon-shapes.component.scss new file mode 100644 index 0000000000..9022db881d --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/map/marker-icon-shapes.component.scss @@ -0,0 +1,47 @@ +/** + * 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. + */ +.tb-marker-icon-shapes-panel { + width: 100%; + display: flex; + flex-direction: column; + gap: 16px; + .tb-marker-icon-shapes-title { + font-size: 16px; + font-weight: 500; + line-height: 24px; + letter-spacing: 0.25px; + color: rgba(0, 0, 0, 0.87); + } + button.mat-mdc-button-base.tb-select-shape-button { + width: 42px; + min-width: 42px; + height: 42px; + padding: 4px; + div.tb-marker-shape { + width: 34px; + height: 34px; + object-fit: contain; + } + } + .tb-marker-icon-shapes-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/marker-icon-shapes.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/map/marker-icon-shapes.component.ts new file mode 100644 index 0000000000..b4cfa7ae00 --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/map/marker-icon-shapes.component.ts @@ -0,0 +1,127 @@ +/// +/// 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, EventEmitter, Input, OnInit, Output, ViewEncapsulation } from '@angular/core'; +import { PageComponent } from '@shared/components/page.component'; +import { TbPopoverComponent } from '@shared/components/popover.component'; +import { Store } from '@ngrx/store'; +import { AppState } from '@core/core.state'; +import { + createColorMarkerIconElement, + MarkerIconContainer, markerIconContainers, + tripMarkerIconContainers +} from '@shared/models/widget/maps/marker-shape.models'; +import { Observable } from 'rxjs'; +import { DomSanitizer, SafeHtml } from '@angular/platform-browser'; +import { MatIconRegistry } from '@angular/material/icon'; +import tinycolor from 'tinycolor2'; +import { map, share } from 'rxjs/operators'; +import { coerceBoolean } from '@shared/decorators/coercion'; + +export interface MarkerIconInfo { + iconContainer?: MarkerIconContainer; + icon: string; +} + +interface MarkerIconContainerInfo { + iconContainer: MarkerIconContainer; + html$: Observable; +} + +@Component({ + selector: 'tb-marker-icon-shapes', + templateUrl: './marker-icon-shapes.component.html', + providers: [], + styleUrls: ['./marker-icon-shapes.component.scss'], + encapsulation: ViewEncapsulation.None +}) +export class MarkerIconShapesComponent extends PageComponent implements OnInit { + + @Input() + icon: string; + + @Input() + iconContainer: MarkerIconContainer; + + @Input() + color: string; + + @Input() + @coerceBoolean() + trip = false; + + @Input() + popover: TbPopoverComponent; + + @Output() + markerIconSelected = new EventEmitter(); + + dirty = false; + + iconContainers: MarkerIconContainerInfo[]; + + constructor(protected store: Store, + private iconRegistry: MatIconRegistry, + private domSanitizer: DomSanitizer) { + super(store); + } + + ngOnInit(): void { + this.updateIconContainers(); + } + + cancel() { + this.popover?.hide(); + } + + selectIcon(icon: string) { + if (this.icon !== icon) { + this.icon = icon; + this.dirty = true; + this.updateIconContainers(); + } + } + + selectIconContainer(iconContainer: MarkerIconContainer) { + if (this.iconContainer !== iconContainer) { + this.iconContainer = iconContainer; + this.dirty = true; + } + } + + apply() { + const iconInfo: MarkerIconInfo = { + iconContainer: this.iconContainer, + icon: this.icon + }; + this.markerIconSelected.emit(iconInfo); + } + + private updateIconContainers() { + const containersList = [...(this.trip ? tripMarkerIconContainers : markerIconContainers),null]; + this.iconContainers = containersList.map((iconContainer) => { + return { + iconContainer, + html$: createColorMarkerIconElement(this.iconRegistry, this.domSanitizer, iconContainer, this.icon, tinycolor(this.color), this.trip).pipe( + map((element) => { + return this.domSanitizer.bypassSecurityTrustHtml(element.outerHTML); + }), + share() + ) + }; + }); + } +} diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/map/marker-shape-settings.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/map/marker-shape-settings.component.ts index 98651a397d..ef6a85ac8c 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/map/marker-shape-settings.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/map/marker-shape-settings.component.ts @@ -45,8 +45,10 @@ import { import tinycolor from 'tinycolor2'; import { map, share } from 'rxjs/operators'; import { MarkerShapesComponent } from '@home/components/widget/lib/settings/common/map/marker-shapes.component'; -import { MaterialIconsComponent } from '@shared/components/material-icons.component'; import { coerceBoolean } from '@shared/decorators/coercion'; +import { + MarkerIconShapesComponent +} from '@home/components/widget/lib/settings/common/map/marker-icon-shapes.component'; @Component({ selector: 'tb-marker-shape-settings', @@ -89,7 +91,6 @@ export class MarkerShapeSettingsComponent implements ControlValueAccessor, OnIni private iconRegistry: MatIconRegistry, private domSanitizer: DomSanitizer, private renderer: Renderer2, - private cd: ChangeDetectorRef, private viewContainerRef: ViewContainerRef) {} ngOnInit(): void { @@ -101,6 +102,7 @@ export class MarkerShapeSettingsComponent implements ControlValueAccessor, OnIni this.shapeSettingsFormGroup.addControl('shape', this.fb.control(null, [Validators.required])); } if (this.markerType === MarkerType.icon) { + this.shapeSettingsFormGroup.addControl('iconContainer', this.fb.control(null, [])); this.shapeSettingsFormGroup.addControl('icon', this.fb.control(null, [Validators.required])); } this.shapeSettingsFormGroup.valueChanges.pipe( @@ -162,20 +164,26 @@ export class MarkerShapeSettingsComponent implements ControlValueAccessor, OnIni }); } else if (this.markerType === MarkerType.icon) { const ctx: any = { - selectedIcon: (this.modelValue as MarkerIconSettings).icon, - iconClearButton: false + iconContainer: (this.modelValue as MarkerIconSettings).iconContainer, + icon: (this.modelValue as MarkerIconSettings).icon, + color: this.modelValue.color.color, + trip: this.trip }; - const materialIconsPopover = this.popoverService.displayPopover(trigger, this.renderer, - this.viewContainerRef, MaterialIconsComponent, 'left', true, null, + const markerIconShapesPopover = this.popoverService.displayPopover(trigger, this.renderer, + this.viewContainerRef, MarkerIconShapesComponent, 'left', true, null, ctx, {}, {}, {}, true); - materialIconsPopover.tbComponentRef.instance.popover = materialIconsPopover; - materialIconsPopover.tbComponentRef.instance.iconSelected.subscribe((icon) => { - materialIconsPopover.hide(); + markerIconShapesPopover.tbComponentRef.instance.popover = markerIconShapesPopover; + markerIconShapesPopover.tbComponentRef.instance.markerIconSelected.subscribe((iconInfo) => { + markerIconShapesPopover.hide(); + this.shapeSettingsFormGroup.get('iconContainer').patchValue( + iconInfo.iconContainer, {emitEvent: false} + ); this.shapeSettingsFormGroup.get('icon').patchValue( - icon + iconInfo.icon, {emitEvent: false} ); + this.updateModel(); }); } } @@ -198,8 +206,9 @@ export class MarkerShapeSettingsComponent implements ControlValueAccessor, OnIni share() ); } else if (this.markerType === MarkerType.icon) { + const iconContainer = (this.modelValue as MarkerIconSettings).iconContainer; const icon = (this.modelValue as MarkerIconSettings).icon; - this.iconPreview$ = createColorMarkerIconElement(this.iconRegistry, this.domSanitizer, icon, tinycolor(color), this.trip).pipe( + this.iconPreview$ = createColorMarkerIconElement(this.iconRegistry, this.domSanitizer, iconContainer, icon, tinycolor(color), this.trip).pipe( map((element) => { return this.domSanitizer.bypassSecurityTrustHtml(element.outerHTML); }), 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 ef7dca9a11..045ce9a801 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 @@ -245,6 +245,9 @@ import { import { TripTimelineSettingsComponent } from '@home/components/widget/lib/settings/common/map/trip-timeline-settings.component'; +import { + MarkerIconShapesComponent +} from '@home/components/widget/lib/settings/common/map/marker-icon-shapes.component'; @NgModule({ declarations: [ @@ -327,6 +330,7 @@ import { DataLayerPatternSettingsComponent, MarkerShapeSettingsComponent, MarkerShapesComponent, + MarkerIconShapesComponent, MarkerImageSettingsComponent, MarkerImageSettingsPanelComponent, MarkerClusteringSettingsComponent, diff --git a/ui-ngx/src/app/modules/home/components/widget/widget-container.component.ts b/ui-ngx/src/app/modules/home/components/widget/widget-container.component.ts index 80afff855f..47a0936b90 100644 --- a/ui-ngx/src/app/modules/home/components/widget/widget-container.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/widget-container.component.ts @@ -45,6 +45,7 @@ import { UtilsService } from '@core/services/utils.service'; import { from } from 'rxjs'; import { DashboardUtilsService } from '@core/services/dashboard-utils.service'; import ITooltipsterInstance = JQueryTooltipster.ITooltipsterInstance; +import ITooltipsterGeoHelper = JQueryTooltipster.ITooltipsterGeoHelper; import { TbContextMenuEvent } from '@shared/models/jquery-event.models'; export enum WidgetComponentActionType { @@ -124,7 +125,6 @@ export class WidgetContainerComponent extends PageComponent implements OnInit, O widgetComponentAction: EventEmitter = new EventEmitter(); hovered = false; - isReferenceWidget = false; get widgetEditActionsEnabled(): boolean { return (this.isEditActionEnabled || this.isRemoveActionEnabled || this.isExportActionEnabled) && !this.widget?.isFullscreen; @@ -295,6 +295,7 @@ export class WidgetContainerComponent extends PageComponent implements OnInit, O theme: ['tb-widget-edit-actions-tooltip'], interactive: true, trigger: 'custom', + ignoreCloseOnScroll: true, triggerOpen: { mouseenter: true }, @@ -305,6 +306,9 @@ export class WidgetContainerComponent extends PageComponent implements OnInit, O trackOrigin: true, trackerInterval: 25, content: '', + checkOverflowY: (geo: ITooltipsterGeoHelper, bcr: DOMRect) => { + return geo.origin.windowOffset.top < bcr.top || geo.origin.windowOffset.bottom < bcr.bottom; + }, functionPosition: (instance, helper, position) => { const clientRect = helper.origin.getBoundingClientRect(); const container = parent.getBoundingClientRect(); @@ -314,6 +318,7 @@ export class WidgetContainerComponent extends PageComponent implements OnInit, O return position; }, functionReady: (_instance, helper) => { + this.editWidgetActionsTooltip.__scrollHandler({}); const tooltipEl = $(helper.tooltip); tooltipEl.on('mouseenter', () => { this.hovered = true; diff --git a/ui-ngx/src/app/shared/components/material-icons.component.html b/ui-ngx/src/app/shared/components/material-icons.component.html index 1df91fc9be..d4d9fe93a6 100644 --- a/ui-ngx/src/app/shared/components/material-icons.component.html +++ b/ui-ngx/src/app/shared/components/material-icons.component.html @@ -16,7 +16,7 @@ -->
    -
    icon.icons
    +
    icon.icons
    search diff --git a/ui-ngx/src/app/shared/components/material-icons.component.ts b/ui-ngx/src/app/shared/components/material-icons.component.ts index 22360affa7..ece1a99108 100644 --- a/ui-ngx/src/app/shared/components/material-icons.component.ts +++ b/ui-ngx/src/app/shared/components/material-icons.component.ts @@ -58,7 +58,11 @@ export class MaterialIconsComponent extends PageComponent implements OnInit { iconClearButton = false; @Input() - popover: TbPopoverComponent; + @coerceBoolean() + showTitle = true; + + @Input() + popover: TbPopoverComponent; @Output() iconSelected = new EventEmitter(); 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 8b44534c68..b7755c00d5 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 @@ -28,18 +28,19 @@ import { hashCode, isDefinedAndNotNull, isNotEmptyStr, + isNumber, isString, isUndefinedOrNull, mergeDeep } from '@core/utils'; import { AbstractControl, ValidationErrors, ValidatorFn } from '@angular/forms'; import { materialColors } from '@shared/models/material.models'; -import L from 'leaflet'; +import type L from 'leaflet'; import { TbFunction } from '@shared/models/js-function.models'; import { Observable, Observer, of, switchMap } from 'rxjs'; import { map } from 'rxjs/operators'; import { ImagePipe } from '@shared/pipe/image.pipe'; -import { MarkerShape } from '@shared/models/widget/maps/marker-shape.models'; +import { MarkerIconContainer, MarkerShape } from '@shared/models/widget/maps/marker-shape.models'; import { DateFormatSettings, simpleDateFormat } from '@shared/models/widget-settings.models'; export enum MapType { @@ -262,6 +263,7 @@ export interface MarkerShapeSettings extends BaseMarkerShapeSettings { } export interface MarkerIconSettings extends BaseMarkerShapeSettings { + iconContainer?: MarkerIconContainer; icon: string; } export interface MarkerClusteringSettings { @@ -352,6 +354,7 @@ export const defaultBaseMarkersDataLayerSettings = (mapType: MapType): Partial isValidLatitude(latitude) && isValidLongitude(longitude); export const isCutPolygon = (data: TbPolygonCoordinates | TbPolygonRawCoordinates): boolean => { - return data.length > 1 && Array.isArray(data[0]) && (Array.isArray(data[0][0]) || data[0][0] instanceof L.LatLng); + return data.length > 1 && Array.isArray(data[0]) && (Array.isArray(data[0][0]) || (isNumber((data[0][0] as any).lat) && isNumber((data[0][0] as any).lng)) ); } export const parseCenterPosition = (position: string | [number, number]): [number, number] => { diff --git a/ui-ngx/src/app/shared/models/widget/maps/marker-shape.models.ts b/ui-ngx/src/app/shared/models/widget/maps/marker-shape.models.ts index 7a4c25d879..76a8af733f 100644 --- a/ui-ngx/src/app/shared/models/widget/maps/marker-shape.models.ts +++ b/ui-ngx/src/app/shared/models/widget/maps/marker-shape.models.ts @@ -20,7 +20,7 @@ import { DomSanitizer } from '@angular/platform-browser'; import { Observable, of, shareReplay, switchMap } from 'rxjs'; import { catchError, map, take } from 'rxjs/operators'; import { isSvgIcon, splitIconName } from '@shared/models/icon.models'; -import { Element, G, Text } from '@svgdotjs/svg.js'; +import { Element, G, SVG, Text } from '@svgdotjs/svg.js'; export enum MarkerShape { markerShape1 = 'markerShape1', @@ -45,6 +45,11 @@ export enum MarkerShape { tripMarkerShape10 = 'tripMarkerShape10' } +export enum MarkerIconContainer { + iconContainer1 = 'iconContainer1', + tripIconContainer1 = 'tripIconContainer1' +} + export const markerShapeMap = new Map( [ [MarkerShape.markerShape1, '/assets/markers/shape1.svg'], @@ -70,6 +75,13 @@ export const markerShapeMap = new Map( ] ); +export const markerIconContainerMap = new Map( + [ + [MarkerIconContainer.iconContainer1, '/assets/markers/iconContainer1.svg'], + [MarkerIconContainer.tripIconContainer1, '/assets/markers/tripIconContainer1.svg'] + ] +); + export const markerShapes = [ MarkerShape.markerShape1, MarkerShape.markerShape2, @@ -96,9 +108,11 @@ export const tripMarkerShapes = [ MarkerShape.tripMarkerShape10 ]; -const createColorMarkerShape = (iconRegistry: MatIconRegistry, domSanitizer: DomSanitizer, shape: MarkerShape, color: tinycolor.Instance): Observable => { - const markerAssetUrl = markerShapeMap.get(shape); - const safeUrl = domSanitizer.bypassSecurityTrustResourceUrl(markerAssetUrl); +export const markerIconContainers = [ MarkerIconContainer.iconContainer1 ]; +export const tripMarkerIconContainers = [ MarkerIconContainer.tripIconContainer1 ]; + +const createColorMarkerShape = (iconRegistry: MatIconRegistry, domSanitizer: DomSanitizer, assetUrl: string, color: tinycolor.Instance): Observable => { + const safeUrl = domSanitizer.bypassSecurityTrustResourceUrl(assetUrl); return iconRegistry.getSvgIconFromUrl(safeUrl).pipe( map((svgElement) => { const colorElements = Array.from(svgElement.getElementsByClassName('marker-color')); @@ -124,7 +138,8 @@ const createColorMarkerShape = (iconRegistry: MatIconRegistry, domSanitizer: Dom export const createColorMarkerShapeURI = (iconRegistry: MatIconRegistry, domSanitizer: DomSanitizer, shape: MarkerShape, color: tinycolor.Instance): Observable => { - return createColorMarkerShape(iconRegistry, domSanitizer, shape, color).pipe( + const assetUrl = markerShapeMap.get(shape); + return createColorMarkerShape(iconRegistry, domSanitizer, assetUrl, color).pipe( map((svgElement) => { const svg = svgElement.outerHTML; return 'data:image/svg+xml;base64,' + btoa(svg); @@ -174,29 +189,38 @@ const createIconElement = (iconRegistry: MatIconRegistry, icon: string, size: nu } } -const markerIconShape = MarkerShape.markerShape6; -const tripMarkerIconShape = MarkerShape.tripMarkerShape2; - -export const createColorMarkerIconElement = (iconRegistry: MatIconRegistry, domSanitizer: DomSanitizer, icon: string, color: tinycolor.Instance, +export const createColorMarkerIconElement = (iconRegistry: MatIconRegistry, domSanitizer: DomSanitizer, + iconContainer: MarkerIconContainer, icon: string, color: tinycolor.Instance, trip = false): Observable => { - return createColorMarkerShape(iconRegistry, domSanitizer, trip ? tripMarkerIconShape : markerIconShape, color).pipe( + const markerShape$: Observable = iconContainer ? + createColorMarkerShape(iconRegistry, domSanitizer, markerIconContainerMap.get(iconContainer), color) : of(null); + return markerShape$.pipe( switchMap((svgElement) => { - return createIconElement(iconRegistry, icon, trip ? 24 : 12, trip ? tinycolor('#fff') : color, trip).pipe( + const iconSize = svgElement ? (trip ? 24 : 12) : 24; + const iconColor = svgElement ? (trip ? tinycolor('#fff') : color) : color; + return createIconElement(iconRegistry, icon, iconSize, iconColor, trip || !svgElement).pipe( map((iconElement) => { - let elements = svgElement.getElementsByClassName('marker-icon-container'); - if (iconElement && elements.length) { - const iconContainer = new G(elements[0] as SVGGElement); - iconContainer.clear(); + if (svgElement) { + const elements = svgElement.getElementsByClassName('marker-icon-container'); + if (iconElement && elements.length) { + const iconContainer = new G(elements[0] as SVGGElement); + iconContainer.clear(); + iconContainer.add(iconElement); + const box = iconElement.bbox(); + iconElement.translate(-box.cx, -box.cy); + } + return svgElement; + } else { + const svg = SVG(); + svg.viewbox(0,0,iconSize,iconSize); + const iconContainer = new G(); + iconContainer.translate(iconSize/2,iconSize/2); iconContainer.add(iconElement); const box = iconElement.bbox(); iconElement.translate(-box.cx, -box.cy); + svg.add(iconContainer); + return svg.node; } - elements = svgElement.getElementsByClassName('marker-icon-background'); - if (elements.length) { - (elements[0] as SVGGElement).style.display = ''; - (elements[0] as SVGGElement).setAttribute('fill-opacity', `${color.getAlpha()}`); - } - return svgElement; }) ); }) 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 4c336831fa..48272ada9e 100644 --- a/ui-ngx/src/assets/locale/locale.constant-en_US.json +++ b/ui-ngx/src/assets/locale/locale.constant-en_US.json @@ -7869,6 +7869,8 @@ "icon": "Icon", "image": "Image", "marker-shapes": "Marker shapes", + "marker-icon": "Marker icon", + "marker-appearance": "Marker appearance", "marker-image": "Marker image", "marker-image-type-image": "Image", "marker-image-type-function": "Function", diff --git a/ui-ngx/src/assets/markers/iconContainer1.svg b/ui-ngx/src/assets/markers/iconContainer1.svg new file mode 100644 index 0000000000..bf92e4d90f --- /dev/null +++ b/ui-ngx/src/assets/markers/iconContainer1.svg @@ -0,0 +1,22 @@ + + + + + + + + + + + + + + + + + + + + + + diff --git a/ui-ngx/src/assets/markers/shape1.svg b/ui-ngx/src/assets/markers/shape1.svg index fafd5638e7..0a29bb7dbe 100644 --- a/ui-ngx/src/assets/markers/shape1.svg +++ b/ui-ngx/src/assets/markers/shape1.svg @@ -1,11 +1,11 @@ - + - + diff --git a/ui-ngx/src/assets/markers/shape2.svg b/ui-ngx/src/assets/markers/shape2.svg index 6777c1810e..1c59215800 100644 --- a/ui-ngx/src/assets/markers/shape2.svg +++ b/ui-ngx/src/assets/markers/shape2.svg @@ -1,8 +1,8 @@ - + - + diff --git a/ui-ngx/src/assets/markers/shape6.svg b/ui-ngx/src/assets/markers/shape6.svg index f4de6d140d..19002cec71 100644 --- a/ui-ngx/src/assets/markers/shape6.svg +++ b/ui-ngx/src/assets/markers/shape6.svg @@ -1,15 +1,12 @@ - + - + - - - diff --git a/ui-ngx/src/assets/markers/tripIconContainer1.svg b/ui-ngx/src/assets/markers/tripIconContainer1.svg new file mode 100644 index 0000000000..df33957809 --- /dev/null +++ b/ui-ngx/src/assets/markers/tripIconContainer1.svg @@ -0,0 +1,12 @@ + + + + + + + + + + + From ac03bf27a38a92fd59087189717d8240514c6f99 Mon Sep 17 00:00:00 2001 From: Igor Kulikov Date: Thu, 6 Mar 2025 19:08:39 +0200 Subject: [PATCH 221/345] UI: Map - add show marker option for trips. --- .../lib/maps/data-layer/trips-data-layer.ts | 84 ++++++----- .../home/components/widget/lib/maps/map.ts | 38 ++--- .../map/map-data-layer-dialog.component.html | 119 +++++++++------- .../map/map-data-layer-dialog.component.ts | 131 +++++++++++------- .../shared/models/widget/maps/map.models.ts | 2 + 5 files changed, 217 insertions(+), 157 deletions(-) diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/maps/data-layer/trips-data-layer.ts b/ui-ngx/src/app/modules/home/components/widget/lib/maps/data-layer/trips-data-layer.ts index 368ca42be8..1bc1e8a395 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/maps/data-layer/trips-data-layer.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/maps/data-layer/trips-data-layer.ts @@ -149,37 +149,41 @@ class TbTripDataItem extends TbDataLayerItem true); - updateTooltip(this.dataLayer.getMap(), this.markerTooltip, - this.settings.tooltip, this.dataLayer.dataLayerTooltipProcessor, this.pointData, dsData); - } - const clickAction = this.settings.click; - if (clickAction && clickAction.type !== WidgetActionType.doNothing) { - this.marker.on('click', (event) => { - this.dataLayer.getMap().dataItemClick(event.originalEvent, clickAction, this.pointData); + if (this.settings.showMarker) { + const dsData = this.dataLayer.getMap().getData(); + const location = this.dataLayer.dataProcessor.extractLocation(this.pointData, dsData); + this.marker = L.marker(location, { + tbMarkerData: this.pointData, + snapIgnore: true }); + this.marker.addTo(this.layer); + this.updateMarkerIcon(this.pointData, dsData); + if (this.settings.tooltip?.show) { + this.markerTooltip = createTooltip(this.dataLayer.getMap(), + this.marker, this.settings.tooltip, this.pointData, () => true); + updateTooltip(this.dataLayer.getMap(), this.markerTooltip, + this.settings.tooltip, this.dataLayer.dataLayerTooltipProcessor, this.pointData, dsData); + } + const clickAction = this.settings.click; + if (clickAction && clickAction.type !== WidgetActionType.doNothing) { + this.marker.on('click', (event) => { + this.dataLayer.getMap().dataItemClick(event.originalEvent, clickAction, this.pointData); + }); + } } } private updateMarker() { - const dsData = this.dataLayer.getMap().getData(); - this.marker.options.tbMarkerData = this.pointData; - this.updateMarkerLocation(this.pointData, dsData); - if (this.settings.tooltip.show) { - updateTooltip(this.dataLayer.getMap(), this.markerTooltip, - this.settings.tooltip, this.dataLayer.dataLayerTooltipProcessor, this.pointData, dsData); + if (this.settings.showMarker) { + const dsData = this.dataLayer.getMap().getData(); + this.marker.options.tbMarkerData = this.pointData; + this.updateMarkerLocation(this.pointData, dsData); + if (this.settings.tooltip.show) { + updateTooltip(this.dataLayer.getMap(), this.markerTooltip, + this.settings.tooltip, this.dataLayer.dataLayerTooltipProcessor, this.pointData, dsData); + } + this.updateMarkerIcon(this.pointData, dsData); } - this.updateMarkerIcon(this.pointData, dsData); } private createPath() { @@ -262,19 +266,21 @@ class TbTripDataItem extends TbDataLayerItem, dsData: FormattedData[]) { - this.dataLayer.dataProcessor.createMarkerIcon(data, dsData, data.rotationAngle).subscribe( - (iconInfo) => { - const options = deepClone(iconInfo.icon.options); - this.marker.setIcon(iconInfo.icon); - const anchor = options.iconAnchor; - if (anchor && Array.isArray(anchor)) { - this.labelOffset = [iconInfo.size[0] / 2 - anchor[0], 10 - anchor[1]]; - } else { - this.labelOffset = [0, -iconInfo.size[1] * this.dataLayer.markerOffset[1] + 10]; + if (this.settings.showMarker) { + this.dataLayer.dataProcessor.createMarkerIcon(data, dsData, data.rotationAngle).subscribe( + (iconInfo) => { + const options = deepClone(iconInfo.icon.options); + this.marker.setIcon(iconInfo.icon); + const anchor = options.iconAnchor; + if (anchor && Array.isArray(anchor)) { + this.labelOffset = [iconInfo.size[0] / 2 - anchor[0], 10 - anchor[1]]; + } else { + this.labelOffset = [0, -iconInfo.size[1] * this.dataLayer.markerOffset[1] + 10]; + } + this.updateMarkerLabel(data, dsData); } - this.updateMarkerLabel(data, dsData); - } - ); + ); + } } private updateMarkerLabel(data: FormattedData, dsData: FormattedData[]) { @@ -384,6 +390,10 @@ export class TbTripsDataLayer extends TbMapDataLayer[][], tripsLatestData: FormattedData[]): {minTime: number; maxTime: number} { let minTime = Infinity; let maxTime = -Infinity; 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 64b02d83bd..7fc4cf5e54 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 @@ -148,23 +148,6 @@ export abstract class TbMap { this.mapLayoutElement = mapLayoutElement[0]; $(containerElement).append(mapLayoutElement); - if (this.settings.tripTimeline?.showTimelineControl) { - this.timeline = true; - this.timeStep = this.settings.tripTimeline.timeStep; - this.timeLineComponentRef = this.ctx.widgetContentContainer.createComponent(MapTimelinePanelComponent); - this.timeLineComponent = this.timeLineComponentRef.instance; - this.timeLineComponent.settings = this.settings.tripTimeline; - this.timeLineComponent.timeChanged.subscribe((time) => { - this.currentTime = time; - this.updateTripsTime(); - }); - const parentElement = this.timeLineComponentRef.instance.element.nativeElement; - const content = parentElement.firstChild; - parentElement.removeChild(content); - parentElement.style.display = 'none'; - containerElement.append(content); - } - const mapElement = $('
    '); mapLayoutElement.append(mapElement); @@ -380,6 +363,27 @@ export abstract class TbMap { } private setupEditMode() { + + const tripsWithMarkers = this.tripDataLayers.filter(dl => dl.showMarker()); + const showTimeline = this.settings.tripTimeline?.showTimelineControl && tripsWithMarkers.length; + + if (showTimeline) { + this.timeline = true; + this.timeStep = this.settings.tripTimeline.timeStep; + this.timeLineComponentRef = this.ctx.widgetContentContainer.createComponent(MapTimelinePanelComponent); + this.timeLineComponent = this.timeLineComponentRef.instance; + this.timeLineComponent.settings = this.settings.tripTimeline; + this.timeLineComponent.timeChanged.subscribe((time) => { + this.currentTime = time; + this.updateTripsTime(); + }); + const parentElement = this.timeLineComponentRef.instance.element.nativeElement; + const content = parentElement.firstChild; + parentElement.removeChild(content); + parentElement.style.display = 'none'; + this.containerElement.append(content); + } + this.editToolbar = L.TB.bottomToolbar({ mapElement: $(this.mapElement), closeTitle: this.ctx.translate.instant('action.cancel'), diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/map/map-data-layer-dialog.component.html b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/map/map-data-layer-dialog.component.html index 7cf5ba9a7f..8e634e178b 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/map/map-data-layer-dialog.component.html +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/map/map-data-layer-dialog.component.html @@ -176,58 +176,73 @@
    {{ 'widget-config.appearance' | translate }}
    -
    -
    -
    {{ 'widgets.maps.data-layer.marker.marker' | translate }}
    - - {{ 'widgets.maps.data-layer.marker.marker-type-shape' | translate }} - {{ 'widgets.maps.data-layer.marker.marker-type-icon' | translate }} - {{ 'widgets.maps.data-layer.marker.marker-type-image' | translate }} - -
    -
    -
    widgets.maps.data-layer.marker.shape
    - -
    -
    -
    widgets.maps.data-layer.marker.icon
    - -
    -
    -
    widgets.maps.data-layer.marker.image
    - -
    -
    -
    widgets.maps.data-layer.marker.marker-offset
    -
    -
    widgets.maps.data-layer.marker.offset-horizontal
    - - - -
    widgets.maps.data-layer.marker.offset-vertical
    - - - -
    -
    -
    - - {{ 'widgets.maps.data-layer.marker.rotate-marker' | translate }} - -
    -
    widgets.maps.data-layer.marker.offset-angle
    - - -
    deg
    -
    -
    -
    - @if (dataLayerType === 'trips') { - - - } +
    + + + +
    + @if (dataLayerType === 'markers') { +
    {{ 'widgets.maps.data-layer.marker.marker' | translate }}
    + } @else { + +
    {{ 'widgets.maps.data-layer.marker.marker' | translate }}
    +
    + } + + {{ 'widgets.maps.data-layer.marker.marker-type-shape' | translate }} + {{ 'widgets.maps.data-layer.marker.marker-type-icon' | translate }} + {{ 'widgets.maps.data-layer.marker.marker-type-image' | translate }} + +
    +
    +
    + +
    +
    widgets.maps.data-layer.marker.shape
    + +
    +
    +
    widgets.maps.data-layer.marker.icon
    + +
    +
    +
    widgets.maps.data-layer.marker.image
    + +
    +
    +
    widgets.maps.data-layer.marker.marker-offset
    +
    +
    widgets.maps.data-layer.marker.offset-horizontal
    + + + +
    widgets.maps.data-layer.marker.offset-vertical
    + + + +
    +
    +
    + + {{ 'widgets.maps.data-layer.marker.rotate-marker' | translate }} + +
    +
    widgets.maps.data-layer.marker.offset-angle
    + + +
    deg
    +
    +
    +
    + @if (dataLayerType === 'trips') { + + + } +
    +
    widgets.maps.data-layer.marker.position-conversion
    diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/map/map-data-layer-dialog.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/map/map-data-layer-dialog.component.ts index 6771bdb226..b52e7b9013 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/map/map-data-layer-dialog.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/map/map-data-layer-dialog.component.ts @@ -168,6 +168,7 @@ export class MapDataLayerDialogComponent extends DialogComponent => mergeDeep( defaultBaseMarkersDataLayerSettings(mapType), { + showMarker: true, tooltip: { offsetY: -0.5, pattern: mapType === MapType.geoMap ? From e4a7046e24a30dab5f8b382d2f06561c51ff54bb Mon Sep 17 00:00:00 2001 From: Dmytro Skarzhynets Date: Fri, 7 Mar 2025 10:04:35 +0200 Subject: [PATCH 222/345] Save time series strategies: fix broken tests for device state service --- .../server/service/state/DefaultDeviceStateService.java | 8 +++----- .../service/state/DefaultDeviceStateServiceTest.java | 6 +++--- 2 files changed, 6 insertions(+), 8 deletions(-) diff --git a/application/src/main/java/org/thingsboard/server/service/state/DefaultDeviceStateService.java b/application/src/main/java/org/thingsboard/server/service/state/DefaultDeviceStateService.java index a57debf012..cc476d377d 100644 --- a/application/src/main/java/org/thingsboard/server/service/state/DefaultDeviceStateService.java +++ b/application/src/main/java/org/thingsboard/server/service/state/DefaultDeviceStateService.java @@ -661,16 +661,14 @@ public class DefaultDeviceStateService extends AbstractPartitionBasedService activityTimeseries = Futures.getDone(timeseriesActivityDataFuture); Optional inactivityTimeoutAttribute = Futures.getDone(inactivityTimeoutAttributeFuture); - List result; if (inactivityTimeoutAttribute.isPresent()) { - result = new ArrayList<>(activityTimeseries.size() + 1); + List result = new ArrayList<>(activityTimeseries.size() + 1); result.addAll(activityTimeseries); - inactivityTimeoutAttribute.ifPresent(result::add); + result.add(inactivityTimeoutAttribute.get()); + return result; } else { return activityTimeseries; } - - return result; }, deviceStateCallbackExecutor); future = Futures.transform(fullActivityDataFuture, extractDeviceStateData(device), MoreExecutors.directExecutor()); diff --git a/application/src/test/java/org/thingsboard/server/service/state/DefaultDeviceStateServiceTest.java b/application/src/test/java/org/thingsboard/server/service/state/DefaultDeviceStateServiceTest.java index f0276d2b62..26e913eacc 100644 --- a/application/src/test/java/org/thingsboard/server/service/state/DefaultDeviceStateServiceTest.java +++ b/application/src/test/java/org/thingsboard/server/service/state/DefaultDeviceStateServiceTest.java @@ -69,7 +69,7 @@ import java.util.stream.Stream; import static org.assertj.core.api.Assertions.assertThat; import static org.awaitility.Awaitility.await; import static org.mockito.ArgumentMatchers.any; -import static org.mockito.ArgumentMatchers.anyList; +import static org.mockito.ArgumentMatchers.anyCollection; import static org.mockito.ArgumentMatchers.argThat; import static org.mockito.ArgumentMatchers.eq; import static org.mockito.BDDMockito.given; @@ -1074,7 +1074,7 @@ public class DefaultDeviceStateServiceTest { final long defaultTimeout = 1000; initStateService(defaultTimeout); given(deviceService.findDeviceById(any(TenantId.class), any(DeviceId.class))).willReturn(new Device(deviceId)); - given(attributesService.find(any(TenantId.class), any(EntityId.class), any(AttributeScope.class), anyList())).willReturn(Futures.immediateFuture(Collections.emptyList())); + given(attributesService.find(any(TenantId.class), any(EntityId.class), any(AttributeScope.class), anyCollection())).willReturn(Futures.immediateFuture(Collections.emptyList())); TransportProtos.DeviceStateServiceMsgProto proto = TransportProtos.DeviceStateServiceMsgProto.newBuilder() .setTenantIdMSB(tenantId.getId().getMostSignificantBits()) @@ -1156,7 +1156,7 @@ public class DefaultDeviceStateServiceTest { final long defaultTimeout = 1000; initStateService(defaultTimeout); given(deviceService.findDeviceById(any(TenantId.class), any(DeviceId.class))).willReturn(new Device(deviceId)); - given(attributesService.find(any(TenantId.class), any(EntityId.class), any(AttributeScope.class), anyList())).willReturn(Futures.immediateFuture(Collections.emptyList())); + given(attributesService.find(any(TenantId.class), any(EntityId.class), any(AttributeScope.class), anyCollection())).willReturn(Futures.immediateFuture(Collections.emptyList())); long currentTime = System.currentTimeMillis(); DeviceState deviceState = DeviceState.builder() From d7f61f07892810a438d2490fdcebb02dcc6a1e3a Mon Sep 17 00:00:00 2001 From: IrynaMatveieva Date: Fri, 7 Mar 2025 10:07:59 +0200 Subject: [PATCH 223/345] added validation and fixed timeouts --- .../CalculatedFieldEntityMessageProcessor.java | 12 +++++++++--- .../CalculatedFieldManagerMessageProcessor.java | 4 ++-- .../server/controller/TenantProfileController.java | 10 ++++++++-- .../cf/DefaultCalculatedFieldProcessingService.java | 3 ++- .../server/common/data/TenantProfile.java | 2 ++ .../profile/DefaultTenantProfileConfiguration.java | 7 +++++++ .../data/tenant/profile/TenantProfileData.java | 2 ++ 7 files changed, 32 insertions(+), 8 deletions(-) diff --git a/application/src/main/java/org/thingsboard/server/actors/calculatedField/CalculatedFieldEntityMessageProcessor.java b/application/src/main/java/org/thingsboard/server/actors/calculatedField/CalculatedFieldEntityMessageProcessor.java index 61f46b7901..9ab019097b 100644 --- a/application/src/main/java/org/thingsboard/server/actors/calculatedField/CalculatedFieldEntityMessageProcessor.java +++ b/application/src/main/java/org/thingsboard/server/actors/calculatedField/CalculatedFieldEntityMessageProcessor.java @@ -134,14 +134,20 @@ public class CalculatedFieldEntityMessageProcessor extends AbstractContextAwareM public void process(CalculatedFieldEntityDeleteMsg msg) { log.info("[{}] Processing CF entity delete msg.", msg.getEntityId()); if (this.entityId.equals(msg.getEntityId())) { - MultipleTbCallback multipleTbCallback = new MultipleTbCallback(states.size(), msg.getCallback()); - states.forEach((cfId, state) -> cfStateService.removeState(new CalculatedFieldEntityCtxId(tenantId, cfId, entityId), multipleTbCallback)); - ctx.stop(ctx.getSelf()); + if (states.isEmpty()) { + msg.getCallback().onSuccess(); + } else { + MultipleTbCallback multipleTbCallback = new MultipleTbCallback(states.size(), msg.getCallback()); + states.forEach((cfId, state) -> cfStateService.removeState(new CalculatedFieldEntityCtxId(tenantId, cfId, entityId), multipleTbCallback)); + ctx.stop(ctx.getSelf()); + } } else { var cfId = new CalculatedFieldId(msg.getEntityId().getId()); var state = states.remove(cfId); if (state != null) { cfStateService.removeState(new CalculatedFieldEntityCtxId(tenantId, cfId, entityId), msg.getCallback()); + } else { + msg.getCallback().onSuccess(); } } } diff --git a/application/src/main/java/org/thingsboard/server/actors/calculatedField/CalculatedFieldManagerMessageProcessor.java b/application/src/main/java/org/thingsboard/server/actors/calculatedField/CalculatedFieldManagerMessageProcessor.java index e7c963a4a7..1418821081 100644 --- a/application/src/main/java/org/thingsboard/server/actors/calculatedField/CalculatedFieldManagerMessageProcessor.java +++ b/application/src/main/java/org/thingsboard/server/actors/calculatedField/CalculatedFieldManagerMessageProcessor.java @@ -199,7 +199,7 @@ public class CalculatedFieldManagerMessageProcessor extends AbstractContextAware if (fieldsCount > 0) { MultipleTbCallback multiCallback = new MultipleTbCallback(fieldsCount, callback); var entityId = msg.getEntityId(); - oldProfileCfs.forEach(ctx -> deleteCfForEntity(entityId, ctx.getCfId(), callback)); + oldProfileCfs.forEach(ctx -> deleteCfForEntity(entityId, ctx.getCfId(), multiCallback)); newProfileCfs.forEach(ctx -> initCfForEntity(entityId, ctx, true, multiCallback)); } else { callback.onSuccess(); @@ -306,7 +306,7 @@ public class CalculatedFieldManagerMessageProcessor extends AbstractContextAware if (!entityIds.isEmpty()) { //TODO: no need to do this if we cache all created actors and know which one belong to us; var multiCallback = new MultipleTbCallback(entityIds.size(), callback); - entityIds.forEach(id -> deleteCfForEntity(entityId, cfId, multiCallback)); + entityIds.forEach(id -> deleteCfForEntity(id, cfId, multiCallback)); } else { callback.onSuccess(); } diff --git a/application/src/main/java/org/thingsboard/server/controller/TenantProfileController.java b/application/src/main/java/org/thingsboard/server/controller/TenantProfileController.java index ff4de0ce0c..1a3bdce1f6 100644 --- a/application/src/main/java/org/thingsboard/server/controller/TenantProfileController.java +++ b/application/src/main/java/org/thingsboard/server/controller/TenantProfileController.java @@ -18,6 +18,7 @@ package org.thingsboard.server.controller; import io.swagger.v3.oas.annotations.Parameter; import io.swagger.v3.oas.annotations.media.ArraySchema; import io.swagger.v3.oas.annotations.media.Schema; +import jakarta.validation.Valid; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.springframework.http.HttpStatus; @@ -160,7 +161,12 @@ public class TenantProfileController extends BaseController { " \"rpcTtlDays\": 0,\n" + " \"queueStatsTtlDays\": 0,\n" + " \"ruleEngineExceptionsTtlDays\": 0,\n" + - " \"warnThreshold\": 0\n" + + " \"warnThreshold\": 0,\n" + + " \"maxCalculatedFieldsPerEntity\": 5,\n" + + " \"maxArgumentsPerCF\": 10,\n" + + " \"maxDataPointsPerRollingArg\": 1000,\n" + + " \"maxStateSizeInKBytes\": 32,\n" + + " \"maxSingleValueArgumentSizeInKBytes\": 2" + " }\n" + " },\n" + " \"default\": false\n" + @@ -172,7 +178,7 @@ public class TenantProfileController extends BaseController { @RequestMapping(value = "/tenantProfile", method = RequestMethod.POST) @ResponseBody public TenantProfile saveTenantProfile(@Parameter(description = "A JSON value representing the tenant profile.") - @RequestBody TenantProfile tenantProfile) throws ThingsboardException { + @Valid @RequestBody TenantProfile tenantProfile) throws ThingsboardException { TenantProfile oldProfile; if (tenantProfile.getId() == null) { accessControlService.checkPermission(getCurrentUser(), Resource.TENANT_PROFILE, Operation.CREATE); diff --git a/application/src/main/java/org/thingsboard/server/service/cf/DefaultCalculatedFieldProcessingService.java b/application/src/main/java/org/thingsboard/server/service/cf/DefaultCalculatedFieldProcessingService.java index 4976045b84..27bc0120c4 100644 --- a/application/src/main/java/org/thingsboard/server/service/cf/DefaultCalculatedFieldProcessingService.java +++ b/application/src/main/java/org/thingsboard/server/service/cf/DefaultCalculatedFieldProcessingService.java @@ -273,7 +273,8 @@ public class DefaultCalculatedFieldProcessingService implements CalculatedFieldP long timeWindow = argument.getTimeWindow() == 0 ? System.currentTimeMillis() : argument.getTimeWindow(); long startTs = currentTime - timeWindow; long maxDataPoints = apiLimitService.getLimit(tenantId, DefaultTenantProfileConfiguration::getMaxDataPointsPerRollingArg); - int limit = argument.getLimit() == 0 ? (int) maxDataPoints : argument.getLimit(); + int argumentLimit = argument.getLimit(); + int limit = argumentLimit == 0 || argumentLimit > maxDataPoints ? (int) maxDataPoints : argument.getLimit(); ReadTsKvQuery query = new BaseReadTsKvQuery(argument.getRefEntityKey().getKey(), startTs, currentTime, 0, limit, Aggregation.NONE); ListenableFuture> tsRollingFuture = timeseriesService.findAll(tenantId, entityId, List.of(query)); diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/TenantProfile.java b/common/data/src/main/java/org/thingsboard/server/common/data/TenantProfile.java index b7c7584931..5c7f0bb53d 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/TenantProfile.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/TenantProfile.java @@ -20,6 +20,7 @@ import com.fasterxml.jackson.annotation.JsonProperty; import com.fasterxml.jackson.core.JsonProcessingException; import com.fasterxml.jackson.databind.ObjectMapper; import io.swagger.v3.oas.annotations.media.Schema; +import jakarta.validation.Valid; import lombok.Data; import lombok.EqualsAndHashCode; import lombok.ToString; @@ -58,6 +59,7 @@ public class TenantProfile extends BaseData implements HasName @Schema(description = "If enabled, will push all messages related to this tenant and processed by the rule engine into separate queue. " + "Useful for complex microservices deployments, to isolate processing of the data for specific tenants", example = "false") private boolean isolatedTbRuleEngine; + @Valid @Schema(description = "Complex JSON object that contains profile settings: queue configs, max devices, max assets, rate limits, etc.") private transient TenantProfileData profileData; @JsonIgnore diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/tenant/profile/DefaultTenantProfileConfiguration.java b/common/data/src/main/java/org/thingsboard/server/common/data/tenant/profile/DefaultTenantProfileConfiguration.java index 9c86785a5e..0568218d9c 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/tenant/profile/DefaultTenantProfileConfiguration.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/tenant/profile/DefaultTenantProfileConfiguration.java @@ -16,6 +16,7 @@ package org.thingsboard.server.common.data.tenant.profile; import io.swagger.v3.oas.annotations.media.Schema; +import jakarta.validation.constraints.Min; import lombok.AllArgsConstructor; import lombok.Builder; import lombok.Data; @@ -135,10 +136,16 @@ public class DefaultTenantProfileConfiguration implements TenantProfileConfigura private double warnThreshold; + @Schema(example = "5") private long maxCalculatedFieldsPerEntity = 5; + @Schema(example = "10") private long maxArgumentsPerCF = 10; + @Min(value = 0, message = "must be at least 0") + @Schema(example = "1000") private long maxDataPointsPerRollingArg = 1000; + @Schema(example = "32") private long maxStateSizeInKBytes = 32; + @Schema(example = "2") private long maxSingleValueArgumentSizeInKBytes = 2; @Override diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/tenant/profile/TenantProfileData.java b/common/data/src/main/java/org/thingsboard/server/common/data/tenant/profile/TenantProfileData.java index b1f6c27fd8..44ca79cabb 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/tenant/profile/TenantProfileData.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/tenant/profile/TenantProfileData.java @@ -16,6 +16,7 @@ package org.thingsboard.server.common.data.tenant.profile; import io.swagger.v3.oas.annotations.media.Schema; +import jakarta.validation.Valid; import lombok.Data; import java.io.Serializable; @@ -27,6 +28,7 @@ public class TenantProfileData implements Serializable { private static final long serialVersionUID = -3642550257035920976L; + @Valid @Schema(description = "Complex JSON object that contains profile settings: max devices, max assets, rate limits, etc.") private TenantProfileConfiguration configuration; From 0fa32587e3525a90acab04ebcf5d0af6f74c27f2 Mon Sep 17 00:00:00 2001 From: IrynaMatveieva Date: Fri, 7 Mar 2025 10:38:48 +0200 Subject: [PATCH 224/345] added audit log mask --- application/src/main/resources/thingsboard.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/application/src/main/resources/thingsboard.yml b/application/src/main/resources/thingsboard.yml index b01a0cf55e..01d7e69ba7 100644 --- a/application/src/main/resources/thingsboard.yml +++ b/application/src/main/resources/thingsboard.yml @@ -858,6 +858,7 @@ audit-log: "edge": "${AUDIT_LOG_MASK_EDGE:W}" # Edge logging levels. Allowed values: OFF (disable), W (log write operations), RW (log read and write operation "tb_resource": "${AUDIT_LOG_MASK_RESOURCE:W}" # TB resource logging levels. Allowed values: OFF (disable), W (log write operations), RW (log read and write operation "ota_package": "${AUDIT_LOG_MASK_OTA_PACKAGE:W}" # Ota package logging levels. Allowed values: OFF (disable), W (log write operations), RW (log read and write operation + "calculated_field": "${AUDIT_LOG_MASK_CALCULATED_FIELD:W}" # Calculated field logging levels. Allowed values: OFF (disable), W (log write operations), RW (log read and write operation sink: # Type of external sink. possible options: none, elasticsearch type: "${AUDIT_LOG_SINK_TYPE:none}" From bbedb8797b6234e3bf0ef146dd8b48e277e9cdeb Mon Sep 17 00:00:00 2001 From: Artem Dzhereleiko Date: Fri, 7 Mar 2025 11:09:39 +0200 Subject: [PATCH 225/345] UI: Widget header button action --- .../widget-action-dialog.component.html | 74 +++++++++++++++-- .../action/widget-action-dialog.component.ts | 44 ++++++++++ .../widget/widget-container.component.html | 81 +++++++++++++++++-- .../widget/widget-container.component.scss | 2 +- .../widget/widget-container.component.ts | 5 +- .../components/widget/widget.component.ts | 6 ++ .../home/models/widget-component.models.ts | 7 ++ ui-ngx/src/app/shared/models/widget.models.ts | 41 ++++++++++ .../assets/locale/locale.constant-en_US.json | 16 ++++ 9 files changed, 260 insertions(+), 16 deletions(-) diff --git a/ui-ngx/src/app/modules/home/components/widget/action/widget-action-dialog.component.html b/ui-ngx/src/app/modules/home/components/widget/action/widget-action-dialog.component.html index ec1b165401..3e9bc581e1 100644 --- a/ui-ngx/src/app/modules/home/components/widget/action/widget-action-dialog.component.html +++ b/ui-ngx/src/app/modules/home/components/widget/action/widget-action-dialog.component.html @@ -88,12 +88,74 @@
    -
    -
    {{'widget-config.icon' | translate}}
    - - -
    + @if (widgetActionFormGroup.get('actionSourceId').value === 'headerButton') { +
    +
    widget-config.header-button.button-settings
    +
    +
    widget-config.header-button.button-type
    + + + + {{ widgetHeaderActionButtonTypeTranslationMap.get(widgetHeaderActionButtonType[button]) | translate }} + + + +
    +
    +
    + + +
    widget-config.icon
    +
    + + +
    +
    +
    {{ 'widget-config.header-button.colors' | translate }}
    +
    +
    widget-config.header-button.color
    + + + +
    widget-config.header-button.background
    + + + +
    widget-config.header-button.border
    + + +
    +
    +
    + + + + widget-config.header-button.advanced-button-style + + + + + + +
    +
    + } @else { +
    +
    {{'widget-config.icon' | translate}}
    + + +
    + } +
    diff --git a/ui-ngx/src/app/modules/home/components/widget/action/widget-action-dialog.component.ts b/ui-ngx/src/app/modules/home/components/widget/action/widget-action-dialog.component.ts index df8df4a882..a04609b241 100644 --- a/ui-ngx/src/app/modules/home/components/widget/action/widget-action-dialog.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/action/widget-action-dialog.component.ts @@ -43,6 +43,10 @@ import { CellClickColumnInfo, defaultWidgetAction, WidgetActionSource, + WidgetHeaderActionButtonType, + widgetHeaderActionButtonTypeMap, + WidgetHeaderActionButtonTypes, + widgetHeaderActionButtonTypeTranslationMap, widgetType } from '@shared/models/widget.models'; import { takeUntil } from 'rxjs/operators'; @@ -85,6 +89,11 @@ export class WidgetActionDialogComponent extends DialogComponent = []; usedCellClickColumns: Array = []; + widgetHeaderActionButtonType = WidgetHeaderActionButtonType + widgetHeaderActionButtonTypes = WidgetHeaderActionButtonTypes; + widgetHeaderActionButtonTypeTranslationMap = widgetHeaderActionButtonTypeTranslationMap; + widgetHeaderActionButtonTypeMap = widgetHeaderActionButtonTypeMap; + @ViewChild('columnIndexSelect') columnIndexSelect: MatSelect; columnIndexPlaceholderText = this.translate.instant('widget-config.select-column-index'); @@ -120,12 +129,19 @@ export class WidgetActionDialogComponent extends DialogComponent { @@ -143,6 +159,9 @@ export class WidgetActionDialogComponent extends DialogComponent { this.updateShowWidgetActionForm(); }); + this.widgetActionFormGroup.get('buttonType').valueChanges.pipe( + takeUntil(this.destroy$) + ).subscribe(() => this.widgetHeaderButtonValidators()); setTimeout(() => { if (this.action?.actionSourceId === 'cellClick') { this.widgetActionFormGroup.get('columnIndex').enable(); @@ -160,6 +179,31 @@ export class WidgetActionDialogComponent extends DialogComponent - +
    + + @switch (action.buttonType) { + @case (widgetHeaderActionButtonType.icon) { + + } + @case (widgetHeaderActionButtonType.miniFab) { + + } + @case (widgetHeaderActionButtonType.basic) { + + } + @case (widgetHeaderActionButtonType.raised) { + + } + @case (widgetHeaderActionButtonType.stroked) { + + } + @case (widgetHeaderActionButtonType.flat) { + + } + } + +
    +
    @if (dataLayerType === 'trips') { - + } @@ -360,6 +360,7 @@ patternType="tooltip" patternTitle="{{ 'widgets.maps.data-layer.points.point-tooltip' | translate }}" [context]="context" + [expand]="false" helpId="widget/lib/map/path_point_tooltip_fn" formControlName="pointTooltip"> @@ -384,7 +385,7 @@
    @if (dataLayerType !== 'trips') { - + }
    @if (dataLayerType !== 'trips') { @@ -436,18 +437,20 @@ - + diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/map/map-data-layer-dialog.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/map/map-data-layer-dialog.component.ts index b52e7b9013..64d75eb78f 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/map/map-data-layer-dialog.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/map/map-data-layer-dialog.component.ts @@ -428,6 +428,7 @@ export class MapDataLayerDialogComponent extends DialogComponent { if (updatedDataKey) { this.dataLayerFormGroup.get(keyType).patchValue(updatedDataKey); + this.dataLayerFormGroup.markAsDirty(); } } ); From ee3d405ed87aae8c229cda5834be512ae6d32d7d Mon Sep 17 00:00:00 2001 From: Andrii Shvaika Date: Fri, 7 Mar 2025 11:44:49 +0200 Subject: [PATCH 229/345] Adding ctx as first argument in CF --- .../controller/CalculatedFieldController.java | 28 +++++++++++---- .../cf/ctx/state/CalculatedFieldCtx.java | 5 ++- .../ctx/state/ScriptCalculatedFieldState.java | 25 ++++++++++--- .../state/ScriptCalculatedFieldStateTest.java | 2 +- .../DefaultTenantProfileConfiguration.java | 2 +- .../api/tbel/DefaultTbelInvokeService.java | 1 + .../thingsboard/script/api/tbel/TbUtils.java | 2 ++ .../script/api/tbel/TbelCfCtx.java | 36 +++++++++++++++++++ .../script/api/tbel/TbelCfObject.java | 2 ++ .../script/api/tbel/TbelCfSingleValueArg.java | 2 -- 10 files changed, 88 insertions(+), 17 deletions(-) create mode 100644 common/script/script-api/src/main/java/org/thingsboard/script/api/tbel/TbelCfCtx.java diff --git a/application/src/main/java/org/thingsboard/server/controller/CalculatedFieldController.java b/application/src/main/java/org/thingsboard/server/controller/CalculatedFieldController.java index fe85cf3f87..f899d0f480 100644 --- a/application/src/main/java/org/thingsboard/server/controller/CalculatedFieldController.java +++ b/application/src/main/java/org/thingsboard/server/controller/CalculatedFieldController.java @@ -34,6 +34,8 @@ import org.springframework.web.bind.annotation.ResponseStatus; import org.springframework.web.bind.annotation.RestController; import org.thingsboard.common.util.JacksonUtil; import org.thingsboard.script.api.tbel.TbelCfArg; +import org.thingsboard.script.api.tbel.TbelCfCtx; +import org.thingsboard.script.api.tbel.TbelCfSingleValueArg; import org.thingsboard.script.api.tbel.TbelInvokeService; import org.thingsboard.server.common.data.EntityType; import org.thingsboard.server.common.data.EventInfo; @@ -216,11 +218,14 @@ public class CalculatedFieldController extends BaseController { @RequestBody JsonNode inputParams) { String expression = inputParams.get("expression").asText(); Map arguments = Objects.requireNonNullElse( - JacksonUtil.convertValue(inputParams.get("arguments"), new TypeReference>() { + JacksonUtil.convertValue(inputParams.get("arguments"), new TypeReference<>() { }), Collections.emptyMap() ); - ArrayList argNames = new ArrayList<>(arguments.keySet()); + + ArrayList ctxAndArgNames = new ArrayList<>(arguments.size() + 1); + ctxAndArgNames.add("ctx"); + ctxAndArgNames.addAll(arguments.keySet()); String output = ""; String errorText = ""; @@ -234,12 +239,20 @@ public class CalculatedFieldController extends BaseController { getTenantId(), tbelInvokeService, expression, - argNames.toArray(String[]::new) + ctxAndArgNames.toArray(String[]::new) ); - Object[] args = argNames.stream() - .map(arguments::get) - .toArray(); + + Object[] args = new Object[ctxAndArgNames.size()]; + args[0] = new TbelCfCtx(arguments); + for (int i = 1; i < ctxAndArgNames.size(); i++) { + var arg = arguments.get(ctxAndArgNames.get(i)); + if (arg instanceof TbelCfSingleValueArg svArg) { + args[i] = svArg.getValue(); + } else { + args[i] = arg; + } + } JsonNode json = calculatedFieldScriptEngine.executeJsonAsync(args).get(TIMEOUT, TimeUnit.SECONDS); output = JacksonUtil.toString(json); @@ -260,7 +273,8 @@ public class CalculatedFieldController extends BaseController { EntityType entityType = referencedEntityId.getEntityType(); switch (entityType) { case TENANT, CUSTOMER, ASSET, DEVICE -> checkEntityId(referencedEntityId, Operation.READ); - default -> throw new IllegalArgumentException("Calculated fields do not support '" + entityType + "' for referenced entities."); + default -> + throw new IllegalArgumentException("Calculated fields do not support '" + entityType + "' for referenced entities."); } } diff --git a/application/src/main/java/org/thingsboard/server/service/cf/ctx/state/CalculatedFieldCtx.java b/application/src/main/java/org/thingsboard/server/service/cf/ctx/state/CalculatedFieldCtx.java index 399e0d9f84..e5a3d0e05e 100644 --- a/application/src/main/java/org/thingsboard/server/service/cf/ctx/state/CalculatedFieldCtx.java +++ b/application/src/main/java/org/thingsboard/server/service/cf/ctx/state/CalculatedFieldCtx.java @@ -136,11 +136,14 @@ public class CalculatedFieldCtx { throw new IllegalArgumentException("TBEL script engine is disabled!"); } + List ctxAndArgNames = new ArrayList<>(argNames.size() + 1); + ctxAndArgNames.add("ctx"); + ctxAndArgNames.addAll(argNames); return new CalculatedFieldTbelScriptEngine( tenantId, tbelInvokeService, expression, - argNames.toArray(String[]::new) + ctxAndArgNames.toArray(String[]::new) ); } diff --git a/application/src/main/java/org/thingsboard/server/service/cf/ctx/state/ScriptCalculatedFieldState.java b/application/src/main/java/org/thingsboard/server/service/cf/ctx/state/ScriptCalculatedFieldState.java index 00d9ccbe5a..bf00f1b0b1 100644 --- a/application/src/main/java/org/thingsboard/server/service/cf/ctx/state/ScriptCalculatedFieldState.java +++ b/application/src/main/java/org/thingsboard/server/service/cf/ctx/state/ScriptCalculatedFieldState.java @@ -23,11 +23,17 @@ import lombok.Data; import lombok.NoArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.thingsboard.script.api.tbel.TbelCfArg; +import org.thingsboard.script.api.tbel.TbelCfCtx; +import org.thingsboard.script.api.tbel.TbelCfSingleValueArg; import org.thingsboard.server.common.data.cf.CalculatedFieldType; import org.thingsboard.server.common.data.cf.configuration.Output; import org.thingsboard.server.service.cf.CalculatedFieldResult; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.LinkedHashMap; import java.util.List; +import java.util.Map; @Data @Slf4j @@ -49,11 +55,20 @@ public class ScriptCalculatedFieldState extends BaseCalculatedFieldState { @Override public ListenableFuture performCalculation(CalculatedFieldCtx ctx) { - Object[] args = ctx.getArgNames().stream() - .map(this::toTbelArgument) - .toArray(); - - ListenableFuture resultFuture = ctx.getCalculatedFieldScriptEngine().executeJsonAsync(args); + Map arguments = new LinkedHashMap<>(); + List args = new ArrayList<>(ctx.getArgNames().size() + 1); + args.add(new Object()); // first element is a ctx, but we will set it later; + for (String argName : ctx.getArgNames()) { + var arg = toTbelArgument(argName); + arguments.put(argName, arg); + if (arg instanceof TbelCfSingleValueArg svArg) { + args.add(svArg.getValue()); + } else { + args.add(arg); + } + } + args.set(0, new TbelCfCtx(arguments)); + ListenableFuture resultFuture = ctx.getCalculatedFieldScriptEngine().executeJsonAsync(args.toArray()); Output output = ctx.getOutput(); return Futures.transform(resultFuture, result -> new CalculatedFieldResult(output.getType(), output.getScope(), result), diff --git a/application/src/test/java/org/thingsboard/server/service/cf/ctx/state/ScriptCalculatedFieldStateTest.java b/application/src/test/java/org/thingsboard/server/service/cf/ctx/state/ScriptCalculatedFieldStateTest.java index dee32e7d1f..320093e8f4 100644 --- a/application/src/test/java/org/thingsboard/server/service/cf/ctx/state/ScriptCalculatedFieldStateTest.java +++ b/application/src/test/java/org/thingsboard/server/service/cf/ctx/state/ScriptCalculatedFieldStateTest.java @@ -191,7 +191,7 @@ public class ScriptCalculatedFieldStateTest { config.setArguments(Map.of("deviceTemperature", argument1, "assetHumidity", argument2)); - config.setExpression("return {\"maxDeviceTemperature\": deviceTemperature.max(), \"assetHumidity\": assetHumidity.value}"); + config.setExpression("return {\"maxDeviceTemperature\": deviceTemperature.max(), \"assetHumidity\": assetHumidity}"); Output output = new Output(); output.setType(OutputType.ATTRIBUTES); diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/tenant/profile/DefaultTenantProfileConfiguration.java b/common/data/src/main/java/org/thingsboard/server/common/data/tenant/profile/DefaultTenantProfileConfiguration.java index 0568218d9c..06a0489037 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/tenant/profile/DefaultTenantProfileConfiguration.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/tenant/profile/DefaultTenantProfileConfiguration.java @@ -140,7 +140,7 @@ public class DefaultTenantProfileConfiguration implements TenantProfileConfigura private long maxCalculatedFieldsPerEntity = 5; @Schema(example = "10") private long maxArgumentsPerCF = 10; - @Min(value = 0, message = "must be at least 0") + @Min(value = 1, message = "must be at least 1") @Schema(example = "1000") private long maxDataPointsPerRollingArg = 1000; @Schema(example = "32") diff --git a/common/script/script-api/src/main/java/org/thingsboard/script/api/tbel/DefaultTbelInvokeService.java b/common/script/script-api/src/main/java/org/thingsboard/script/api/tbel/DefaultTbelInvokeService.java index 38fed7a2aa..3680e1bfb2 100644 --- a/common/script/script-api/src/main/java/org/thingsboard/script/api/tbel/DefaultTbelInvokeService.java +++ b/common/script/script-api/src/main/java/org/thingsboard/script/api/tbel/DefaultTbelInvokeService.java @@ -139,6 +139,7 @@ public class DefaultTbelInvokeService extends AbstractScriptInvokeService implem parserConfig.registerDataType("TbelCfTsRollingData", TbelCfTsRollingData.class, TbelCfTsRollingData::memorySize); parserConfig.registerDataType("TbTimeWindow", TbTimeWindow.class, TbTimeWindow::memorySize); parserConfig.registerDataType("TbelCfTsDoubleVal", TbelCfTsMultiDoubleVal.class, TbelCfTsMultiDoubleVal::memorySize); + parserConfig.registerDataType("TbelCfCtx", TbelCfCtx.class, TbelCfCtx::memorySize); TbUtils.register(parserConfig); executor = MoreExecutors.listeningDecorator(ThingsBoardExecutors.newWorkStealingPool(threadPoolSize, "tbel-executor")); diff --git a/common/script/script-api/src/main/java/org/thingsboard/script/api/tbel/TbUtils.java b/common/script/script-api/src/main/java/org/thingsboard/script/api/tbel/TbUtils.java index 20a753da14..ca3a8db02a 100644 --- a/common/script/script-api/src/main/java/org/thingsboard/script/api/tbel/TbUtils.java +++ b/common/script/script-api/src/main/java/org/thingsboard/script/api/tbel/TbUtils.java @@ -18,6 +18,8 @@ package org.thingsboard.script.api.tbel; import com.google.common.primitives.Bytes; import lombok.extern.slf4j.Slf4j; import org.apache.commons.lang3.ArrayUtils; +import org.mvel2.ConversionHandler; +import org.mvel2.DataConversion; import org.mvel2.ExecutionContext; import org.mvel2.ParserConfiguration; import org.mvel2.execution.ExecutionArrayList; diff --git a/common/script/script-api/src/main/java/org/thingsboard/script/api/tbel/TbelCfCtx.java b/common/script/script-api/src/main/java/org/thingsboard/script/api/tbel/TbelCfCtx.java new file mode 100644 index 0000000000..ce42e2cf3b --- /dev/null +++ b/common/script/script-api/src/main/java/org/thingsboard/script/api/tbel/TbelCfCtx.java @@ -0,0 +1,36 @@ +/** + * 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.script.api.tbel; + +import lombok.Getter; + +import java.util.Collections; +import java.util.Map; + +public class TbelCfCtx implements TbelCfObject { + + @Getter + private final Map args; + + public TbelCfCtx(Map args) { + this.args = Collections.unmodifiableMap(args); + } + + @Override + public long memorySize() { + return OBJ_SIZE; + } +} diff --git a/common/script/script-api/src/main/java/org/thingsboard/script/api/tbel/TbelCfObject.java b/common/script/script-api/src/main/java/org/thingsboard/script/api/tbel/TbelCfObject.java index 3af6198a22..cf575bd9d0 100644 --- a/common/script/script-api/src/main/java/org/thingsboard/script/api/tbel/TbelCfObject.java +++ b/common/script/script-api/src/main/java/org/thingsboard/script/api/tbel/TbelCfObject.java @@ -17,6 +17,8 @@ package org.thingsboard.script.api.tbel; public interface TbelCfObject { + long OBJ_SIZE = 32L; // Approximate calculation; + long memorySize(); } diff --git a/common/script/script-api/src/main/java/org/thingsboard/script/api/tbel/TbelCfSingleValueArg.java b/common/script/script-api/src/main/java/org/thingsboard/script/api/tbel/TbelCfSingleValueArg.java index 193b6ea1ae..84227a8d80 100644 --- a/common/script/script-api/src/main/java/org/thingsboard/script/api/tbel/TbelCfSingleValueArg.java +++ b/common/script/script-api/src/main/java/org/thingsboard/script/api/tbel/TbelCfSingleValueArg.java @@ -22,8 +22,6 @@ import lombok.Data; @Data public class TbelCfSingleValueArg implements TbelCfArg { - public static final long OBJ_SIZE = 32L; // Approximate calculation; - private final long ts; private final Object value; From 4495a2fa4b25f4b65c11ca08ecd9997924244f6d Mon Sep 17 00:00:00 2001 From: Dmytro Skarzhynets Date: Fri, 7 Mar 2025 11:53:25 +0200 Subject: [PATCH 230/345] Save attributes strategies: ensure device state service is notified when inactivity timeout is updated --- .../DefaultSubscriptionManagerService.java | 61 +--- .../DefaultTelemetrySubscriptionService.java | 83 ++++- ...faultTelemetrySubscriptionServiceTest.java | 299 +++++++++++++++++- 3 files changed, 374 insertions(+), 69 deletions(-) diff --git a/application/src/main/java/org/thingsboard/server/service/subscription/DefaultSubscriptionManagerService.java b/application/src/main/java/org/thingsboard/server/service/subscription/DefaultSubscriptionManagerService.java index 3e2c873030..1fafb562c1 100644 --- a/application/src/main/java/org/thingsboard/server/service/subscription/DefaultSubscriptionManagerService.java +++ b/application/src/main/java/org/thingsboard/server/service/subscription/DefaultSubscriptionManagerService.java @@ -48,8 +48,6 @@ import org.thingsboard.server.queue.discovery.event.OtherServiceShutdownEvent; import org.thingsboard.server.queue.discovery.event.PartitionChangeEvent; import org.thingsboard.server.queue.provider.TbQueueProducerProvider; import org.thingsboard.server.queue.util.TbCoreComponent; -import org.thingsboard.server.service.state.DefaultDeviceStateService; -import org.thingsboard.server.service.state.DeviceStateService; import org.thingsboard.server.service.ws.notification.sub.NotificationUpdate; import org.thingsboard.server.service.ws.notification.sub.NotificationsSubscriptionUpdate; @@ -75,7 +73,6 @@ public class DefaultSubscriptionManagerService extends TbApplicationEventListene private final TbServiceInfoProvider serviceInfoProvider; private final TbQueueProducerProvider producerProvider; private final TbLocalSubscriptionService localSubscriptionService; - private final DeviceStateService deviceStateService; private final TbClusterService clusterService; private final SubscriptionSchedulerComponent scheduler; @@ -170,7 +167,7 @@ public class DefaultSubscriptionManagerService extends TbApplicationEventListene callback.onSuccess(); } - public void onTimeSeriesUpdate(EntityId entityId, List update) { + private void onTimeSeriesUpdate(EntityId entityId, List update) { getEntityUpdatesInfo(entityId).timeSeriesUpdateTs = System.currentTimeMillis(); TbEntityRemoteSubsInfo subInfo = entitySubscriptions.get(entityId); if (subInfo != null) { @@ -202,11 +199,6 @@ public class DefaultSubscriptionManagerService extends TbApplicationEventListene public void onAttributesUpdate(TenantId tenantId, EntityId entityId, String scope, List attributes, TbCallback callback) { getEntityUpdatesInfo(entityId).attributesUpdateTs = System.currentTimeMillis(); processAttributesUpdate(entityId, scope, attributes); - if (entityId.getEntityType() == EntityType.DEVICE) { - if (TbAttributeSubscriptionScope.SERVER_SCOPE.name().equalsIgnoreCase(scope)) { - updateDeviceInactivityTimeout(tenantId, entityId, attributes); - } - } callback.onSuccess(); } @@ -219,19 +211,13 @@ public class DefaultSubscriptionManagerService extends TbApplicationEventListene public void onAttributesDelete(TenantId tenantId, EntityId entityId, String scope, List keys, boolean notifyDevice, TbCallback callback) { processAttributesUpdate(entityId, scope, keys.stream().map(key -> new BaseAttributeKvEntry(0, new StringDataEntry(key, ""))).collect(Collectors.toList())); - if (entityId.getEntityType() == EntityType.DEVICE) { - if (TbAttributeSubscriptionScope.SERVER_SCOPE.name().equalsIgnoreCase(scope) - || TbAttributeSubscriptionScope.ANY_SCOPE.name().equalsIgnoreCase(scope)) { - deleteDeviceInactivityTimeout(tenantId, entityId, keys); - } else if (TbAttributeSubscriptionScope.SHARED_SCOPE.name().equalsIgnoreCase(scope) && notifyDevice) { - clusterService.pushMsgToCore(DeviceAttributesEventNotificationMsg.onDelete(tenantId, - new DeviceId(entityId.getId()), scope, keys), null); - } + if (entityId.getEntityType() == EntityType.DEVICE && TbAttributeSubscriptionScope.SHARED_SCOPE.name().equalsIgnoreCase(scope) && notifyDevice) { + clusterService.pushMsgToCore(DeviceAttributesEventNotificationMsg.onDelete(tenantId, new DeviceId(entityId.getId()), scope, keys), null); } callback.onSuccess(); } - public void processAttributesUpdate(EntityId entityId, String scope, List update) { + private void processAttributesUpdate(EntityId entityId, String scope, List update) { TbEntityRemoteSubsInfo subInfo = entitySubscriptions.get(entityId); if (subInfo != null) { log.trace("[{}] Handling attributes update: {}", entityId, update); @@ -259,22 +245,6 @@ public class DefaultSubscriptionManagerService extends TbApplicationEventListene } } - private void updateDeviceInactivityTimeout(TenantId tenantId, EntityId entityId, List kvEntries) { - for (KvEntry kvEntry : kvEntries) { - if (kvEntry.getKey().equals(DefaultDeviceStateService.INACTIVITY_TIMEOUT)) { - deviceStateService.onDeviceInactivityTimeoutUpdate(tenantId, new DeviceId(entityId.getId()), getLongValue(kvEntry)); - } - } - } - - private void deleteDeviceInactivityTimeout(TenantId tenantId, EntityId entityId, List keys) { - for (String key : keys) { - if (key.equals(DefaultDeviceStateService.INACTIVITY_TIMEOUT)) { - deviceStateService.onDeviceInactivityTimeoutUpdate(tenantId, new DeviceId(entityId.getId()), 0); - } - } - } - @Override public void onAlarmUpdate(TenantId tenantId, EntityId entityId, AlarmInfo alarm, TbCallback callback) { onAlarmSubUpdate(tenantId, entityId, alarm, false, callback); @@ -344,29 +314,6 @@ public class DefaultSubscriptionManagerService extends TbApplicationEventListene } } - private static long getLongValue(KvEntry kve) { - switch (kve.getDataType()) { - case LONG: - return kve.getLongValue().orElse(0L); - case DOUBLE: - return kve.getDoubleValue().orElse(0.0).longValue(); - case STRING: - try { - return Long.parseLong(kve.getStrValue().orElse("0")); - } catch (NumberFormatException e) { - return 0L; - } - case JSON: - try { - return Long.parseLong(kve.getJsonValue().orElse("0")); - } catch (NumberFormatException e) { - return 0L; - } - default: - return 0L; - } - } - private static List getSubList(List ts, Set keys) { List update = null; for (T entry : ts) { diff --git a/application/src/main/java/org/thingsboard/server/service/telemetry/DefaultTelemetrySubscriptionService.java b/application/src/main/java/org/thingsboard/server/service/telemetry/DefaultTelemetrySubscriptionService.java index 0201bb4942..2302446b6b 100644 --- a/application/src/main/java/org/thingsboard/server/service/telemetry/DefaultTelemetrySubscriptionService.java +++ b/application/src/main/java/org/thingsboard/server/service/telemetry/DefaultTelemetrySubscriptionService.java @@ -31,10 +31,12 @@ import org.thingsboard.common.util.DonAsynchron; import org.thingsboard.common.util.ThingsBoardThreadFactory; import org.thingsboard.rule.engine.api.AttributesDeleteRequest; import org.thingsboard.rule.engine.api.AttributesSaveRequest; +import org.thingsboard.rule.engine.api.DeviceStateManager; import org.thingsboard.rule.engine.api.RuleEngineTelemetryService; import org.thingsboard.rule.engine.api.TimeseriesDeleteRequest; import org.thingsboard.rule.engine.api.TimeseriesSaveRequest; import org.thingsboard.server.common.data.ApiUsageRecordKey; +import org.thingsboard.server.common.data.AttributeScope; import org.thingsboard.server.common.data.DataConstants; import org.thingsboard.server.common.data.EntityType; import org.thingsboard.server.common.data.EntityView; @@ -43,6 +45,7 @@ import org.thingsboard.server.common.data.id.DeviceId; import org.thingsboard.server.common.data.id.EntityId; import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.common.data.kv.AttributeKvEntry; +import org.thingsboard.server.common.data.kv.KvEntry; import org.thingsboard.server.common.data.kv.TimeseriesSaveResult; import org.thingsboard.server.common.data.kv.TsKvEntry; import org.thingsboard.server.common.data.kv.TsKvLatestRemovingResult; @@ -55,12 +58,11 @@ import org.thingsboard.server.dao.util.KvUtils; import org.thingsboard.server.service.apiusage.TbApiUsageStateService; import org.thingsboard.server.service.cf.CalculatedFieldQueueService; import org.thingsboard.server.service.entitiy.entityview.TbEntityViewService; -import org.thingsboard.server.service.subscription.TbAttributeSubscriptionScope; +import org.thingsboard.server.service.state.DefaultDeviceStateService; import org.thingsboard.server.service.subscription.TbSubscriptionUtils; import java.util.ArrayList; import java.util.Collections; -import java.util.Comparator; import java.util.HashMap; import java.util.List; import java.util.Map; @@ -70,6 +72,11 @@ import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; import java.util.function.Consumer; +import static java.util.Comparator.comparing; +import static java.util.Comparator.comparingLong; +import static java.util.Comparator.naturalOrder; +import static java.util.Comparator.nullsFirst; + /** * Created by ashvayka on 27.03.18. */ @@ -83,6 +90,7 @@ public class DefaultTelemetrySubscriptionService extends AbstractSubscriptionSer private final TbApiUsageReportClient apiUsageClient; private final TbApiUsageStateService apiUsageStateService; private final CalculatedFieldQueueService calculatedFieldQueueService; + private final DeviceStateManager deviceStateManager; private ExecutorService tsCallBackExecutor; @@ -94,13 +102,15 @@ public class DefaultTelemetrySubscriptionService extends AbstractSubscriptionSer @Lazy TbEntityViewService tbEntityViewService, TbApiUsageReportClient apiUsageClient, TbApiUsageStateService apiUsageStateService, - CalculatedFieldQueueService calculatedFieldQueueService) { + CalculatedFieldQueueService calculatedFieldQueueService, + DeviceStateManager deviceStateManager) { this.attrService = attrService; this.tsService = tsService; this.tbEntityViewService = tbEntityViewService; this.apiUsageClient = apiUsageClient; this.apiUsageStateService = apiUsageStateService; this.calculatedFieldQueueService = calculatedFieldQueueService; + this.deviceStateManager = deviceStateManager; } @PostConstruct @@ -201,20 +211,51 @@ public class DefaultTelemetrySubscriptionService extends AbstractSubscriptionSer } }, t -> request.getCallback().onFailure(t)); - if (strategy.saveAttributes() - && entityId.getEntityType() == EntityType.DEVICE - && TbAttributeSubscriptionScope.SHARED_SCOPE.name().equalsIgnoreCase(request.getScope().name()) - && request.isNotifyDevice()) { + if (shouldSendSharedAttributesUpdatedNotification(request)) { addMainCallback(resultFuture, success -> clusterService.pushMsgToCore( DeviceAttributesEventNotificationMsg.onUpdate(tenantId, new DeviceId(entityId.getId()), DataConstants.SHARED_SCOPE, request.getEntries()), null )); } + if (shouldCheckForInactivityTimeoutUpdates(request)) { + findNewInactivityTimeout(request.getEntries()).ifPresent(newInactivityTimeout -> + addMainCallback(resultFuture, success -> deviceStateManager.onDeviceInactivityTimeoutUpdate( + tenantId, new DeviceId(entityId.getId()), newInactivityTimeout, TbCallback.EMPTY) + ) + ); + } + if (strategy.sendWsUpdate()) { addWsCallback(resultFuture, success -> onAttributesUpdate(tenantId, entityId, request.getScope().name(), request.getEntries())); } } + private static boolean shouldSendSharedAttributesUpdatedNotification(AttributesSaveRequest request) { + return request.getStrategy().saveAttributes() && shouldSendSharedAttributesNotification(request.getEntityId(), request.getScope(), request.isNotifyDevice()); + } + + private static boolean shouldCheckForInactivityTimeoutUpdates(AttributesSaveRequest request) { + return request.getStrategy().saveAttributes() + && request.getEntityId().getEntityType() == EntityType.DEVICE + && request.getScope() == AttributeScope.SERVER_SCOPE; + } + + private static Optional findNewInactivityTimeout(List entries) { + return entries.stream() + .filter(entry -> Objects.equals(DefaultDeviceStateService.INACTIVITY_TIMEOUT, entry.getKey())) + // Select the entry with the highest version, or if the versions are equal, the one with the most recent update timestamp + .max(comparing(AttributeKvEntry::getVersion, nullsFirst(naturalOrder())).thenComparingLong(AttributeKvEntry::getLastUpdateTs)) + .map(DefaultTelemetrySubscriptionService::parseAsLong); + } + + private static long parseAsLong(KvEntry kve) { + try { + return Long.parseLong(kve.getValueAsString()); + } catch (NumberFormatException e) { + return 0L; + } + } + @Override public void deleteAttributes(AttributesDeleteRequest request) { checkInternalEntity(request.getEntityId()); @@ -233,17 +274,37 @@ public class DefaultTelemetrySubscriptionService extends AbstractSubscriptionSer t -> request.getCallback().onFailure(t) ); - if (entityId.getEntityType() == EntityType.DEVICE - && TbAttributeSubscriptionScope.SHARED_SCOPE.name().equalsIgnoreCase(request.getScope().name()) - && request.isNotifyDevice()) { + if (shouldSendSharedAttributesDeletedNotification(request)) { addMainCallback(deleteFuture, success -> clusterService.pushMsgToCore( DeviceAttributesEventNotificationMsg.onDelete(tenantId, new DeviceId(entityId.getId()), DataConstants.SHARED_SCOPE, request.getKeys()), null )); } + if (inactivityTimeoutDeleted(request)) { + addMainCallback(deleteFuture, success -> deviceStateManager.onDeviceInactivityTimeoutUpdate( + tenantId, new DeviceId(entityId.getId()), 0L, TbCallback.EMPTY) + ); + } + addWsCallback(deleteFuture, success -> onAttributesDelete(tenantId, entityId, request.getScope().name(), request.getKeys())); } + private static boolean shouldSendSharedAttributesDeletedNotification(AttributesDeleteRequest request) { + return shouldSendSharedAttributesNotification(request.getEntityId(), request.getScope(), request.isNotifyDevice()); + } + + private static boolean shouldSendSharedAttributesNotification(EntityId entityId, AttributeScope scope, boolean notifyDevice) { + return entityId.getEntityType() == EntityType.DEVICE + && scope == AttributeScope.SHARED_SCOPE + && notifyDevice; + } + + private static boolean inactivityTimeoutDeleted(AttributesDeleteRequest request) { + return request.getEntityId().getEntityType() == EntityType.DEVICE + && request.getScope() == AttributeScope.SERVER_SCOPE + && request.getKeys().stream().anyMatch(key -> Objects.equals(DefaultDeviceStateService.INACTIVITY_TIMEOUT, key)); + } + @Override public void deleteTimeseries(TimeseriesDeleteRequest request) { checkInternalEntity(request.getEntityId()); @@ -293,7 +354,7 @@ public class DefaultTelemetrySubscriptionService extends AbstractSubscriptionSer if (entries != null) { Optional tsKvEntry = entries.stream() .filter(entry -> entry.getTs() > startTs && entry.getTs() <= endTs) - .max(Comparator.comparingLong(TsKvEntry::getTs)); + .max(comparingLong(TsKvEntry::getTs)); tsKvEntry.ifPresent(entityViewLatest::add); } } diff --git a/application/src/test/java/org/thingsboard/server/service/telemetry/DefaultTelemetrySubscriptionServiceTest.java b/application/src/test/java/org/thingsboard/server/service/telemetry/DefaultTelemetrySubscriptionServiceTest.java index e96566c116..64845a12cd 100644 --- a/application/src/test/java/org/thingsboard/server/service/telemetry/DefaultTelemetrySubscriptionServiceTest.java +++ b/application/src/test/java/org/thingsboard/server/service/telemetry/DefaultTelemetrySubscriptionServiceTest.java @@ -31,6 +31,7 @@ import org.mockito.junit.jupiter.MockitoExtension; import org.springframework.test.util.ReflectionTestUtils; import org.thingsboard.rule.engine.api.AttributesDeleteRequest; import org.thingsboard.rule.engine.api.AttributesSaveRequest; +import org.thingsboard.rule.engine.api.DeviceStateManager; import org.thingsboard.rule.engine.api.TimeseriesSaveRequest; import org.thingsboard.server.cluster.TbClusterService; import org.thingsboard.server.common.data.ApiUsageRecordKey; @@ -51,6 +52,7 @@ import org.thingsboard.server.common.data.kv.BaseAttributeKvEntry; import org.thingsboard.server.common.data.kv.BasicTsKvEntry; import org.thingsboard.server.common.data.kv.DoubleDataEntry; import org.thingsboard.server.common.data.kv.KvEntry; +import org.thingsboard.server.common.data.kv.LongDataEntry; import org.thingsboard.server.common.data.kv.StringDataEntry; import org.thingsboard.server.common.data.kv.TimeseriesSaveResult; import org.thingsboard.server.common.data.kv.TsKvEntry; @@ -137,12 +139,14 @@ class DefaultTelemetrySubscriptionServiceTest { TbApiUsageStateService apiUsageStateService; @Mock CalculatedFieldQueueService calculatedFieldQueueService; + @Mock + DeviceStateManager deviceStateManager; DefaultTelemetrySubscriptionService telemetryService; @BeforeEach void setup() { - telemetryService = new DefaultTelemetrySubscriptionService(attrService, tsService, tbEntityViewService, apiUsageClient, apiUsageStateService, calculatedFieldQueueService); + telemetryService = new DefaultTelemetrySubscriptionService(attrService, tsService, tbEntityViewService, apiUsageClient, apiUsageStateService, calculatedFieldQueueService, deviceStateManager); ReflectionTestUtils.setField(telemetryService, "clusterService", clusterService); ReflectionTestUtils.setField(telemetryService, "partitionService", partitionService); ReflectionTestUtils.setField(telemetryService, "subscriptionManagerService", Optional.of(subscriptionManagerService)); @@ -658,6 +662,184 @@ class DefaultTelemetrySubscriptionServiceTest { then(clusterService).should(never()).pushMsgToCore(any(), any()); } + @Test + void shouldNotifyDeviceStateManagerWhenDeviceInactivityTimeoutWasUpdated() { + // GIVEN + var deviceId = DeviceId.fromString("cc51e450-53e1-11ee-883e-e56b48fd2088"); + var inactivityTimeout = new BaseAttributeKvEntry(123L, new LongDataEntry("inactivityTimeout", 5000L)); + + var request = AttributesSaveRequest.builder() + .tenantId(tenantId) + .entityId(deviceId) + .scope(AttributeScope.SERVER_SCOPE) + .entry(inactivityTimeout) + .strategy(new AttributesSaveRequest.Strategy(true, false, false)) + .build(); + + given(attrService.save(tenantId, deviceId, request.getScope(), request.getEntries())).willReturn(immediateFuture(listOfNNumbers(request.getEntries().size()))); + + // WHEN + telemetryService.saveAttributes(request); + + // THEN + then(deviceStateManager).should().onDeviceInactivityTimeoutUpdate(tenantId, deviceId, 5000L, TbCallback.EMPTY); + } + + @Test + void shouldNotNotifyDeviceStateManagerWhenDeviceInactivityTimeoutSaveWasSkipped() { + // GIVEN + var deviceId = DeviceId.fromString("cc51e450-53e1-11ee-883e-e56b48fd2088"); + var inactivityTimeout = new BaseAttributeKvEntry(123L, new LongDataEntry("inactivityTimeout", 5000L)); + + var request = AttributesSaveRequest.builder() + .tenantId(tenantId) + .entityId(deviceId) + .scope(AttributeScope.SERVER_SCOPE) + .entry(inactivityTimeout) + .strategy(new AttributesSaveRequest.Strategy(false, true, true)) + .build(); + + // WHEN + telemetryService.saveAttributes(request); + + // THEN + then(deviceStateManager).shouldHaveNoInteractions(); + } + + @ParameterizedTest + @EnumSource( + value = EntityType.class, + names = {"DEVICE", "API_USAGE_STATE"}, // API usage state excluded due to coverage in another test + mode = EnumSource.Mode.EXCLUDE + ) + void shouldNotNotifyDeviceStateManagerWhenInactivityTimeoutWasUpdatedButEntityTypeIsNotDevice(EntityType entityType) { + // GIVEN + var nonDeviceId = EntityIdFactory.getByTypeAndUuid(entityType, "cc51e450-53e1-11ee-883e-e56b48fd2088"); + var inactivityTimeout = new BaseAttributeKvEntry(123L, new LongDataEntry("inactivityTimeout", 5000L)); + + var request = AttributesSaveRequest.builder() + .tenantId(tenantId) + .entityId(nonDeviceId) + .scope(AttributeScope.SERVER_SCOPE) + .entry(inactivityTimeout) + .strategy(new AttributesSaveRequest.Strategy(true, false, false)) + .build(); + + given(attrService.save(tenantId, nonDeviceId, request.getScope(), request.getEntries())).willReturn(immediateFuture(listOfNNumbers(request.getEntries().size()))); + + // WHEN + telemetryService.saveAttributes(request); + + // THEN + then(deviceStateManager).shouldHaveNoInteractions(); + } + + @ParameterizedTest + @EnumSource( + value = AttributeScope.class, + names = {"SERVER_SCOPE"}, + mode = EnumSource.Mode.EXCLUDE + ) + void shouldNotNotifyDeviceStateManagerWhenInactivityTimeoutWasUpdatedButAttributeScopeIsNotServer(AttributeScope nonServerScope) { + // GIVEN + var deviceId = DeviceId.fromString("cc51e450-53e1-11ee-883e-e56b48fd2088"); + var inactivityTimeout = new BaseAttributeKvEntry(123L, new LongDataEntry("inactivityTimeout", 5000L)); + + var request = AttributesSaveRequest.builder() + .tenantId(tenantId) + .entityId(deviceId) + .scope(nonServerScope) + .entry(inactivityTimeout) + .strategy(new AttributesSaveRequest.Strategy(true, false, false)) + .build(); + + given(attrService.save(tenantId, deviceId, request.getScope(), request.getEntries())).willReturn(immediateFuture(listOfNNumbers(request.getEntries().size()))); + + // WHEN + telemetryService.saveAttributes(request); + + // THEN + then(deviceStateManager).shouldHaveNoInteractions(); + } + + @Test + void shouldNotNotifyDeviceStateManagerWhenUpdatedAttributesDoNotContainInactivityTimeout() { + // GIVEN + var deviceId = DeviceId.fromString("cc51e450-53e1-11ee-883e-e56b48fd2088"); + var inactivityTimeout = new BaseAttributeKvEntry(123L, new LongDataEntry("notInactivityTimeout", 5000L)); + + var request = AttributesSaveRequest.builder() + .tenantId(tenantId) + .entityId(deviceId) + .scope(AttributeScope.SERVER_SCOPE) + .entry(inactivityTimeout) + .strategy(new AttributesSaveRequest.Strategy(true, false, false)) + .build(); + + given(attrService.save(tenantId, deviceId, request.getScope(), request.getEntries())).willReturn(immediateFuture(listOfNNumbers(request.getEntries().size()))); + + // WHEN + telemetryService.saveAttributes(request); + + // THEN + then(deviceStateManager).shouldHaveNoInteractions(); + } + + @Test + void shouldUseInactivityTimeoutEntryWithTheGreatestVersion() { + // GIVEN + var deviceId = DeviceId.fromString("cc51e450-53e1-11ee-883e-e56b48fd2088"); + List entries = List.of( + new BaseAttributeKvEntry(new LongDataEntry("inactivityTimeout", 0L), 0L, null), + new BaseAttributeKvEntry(new LongDataEntry("inactivityTimeout", 1000L), 3L, 1L), + new BaseAttributeKvEntry(new LongDataEntry("inactivityTimeout", 2000L), 2L, 2L), + new BaseAttributeKvEntry(new LongDataEntry("inactivityTimeout", 3000L), 1L, 3L) + ); + + var request = AttributesSaveRequest.builder() + .tenantId(tenantId) + .entityId(deviceId) + .scope(AttributeScope.SERVER_SCOPE) + .entries(entries) + .strategy(new AttributesSaveRequest.Strategy(true, false, false)) + .build(); + + given(attrService.save(tenantId, deviceId, request.getScope(), request.getEntries())).willReturn(immediateFuture(listOfNNumbers(request.getEntries().size()))); + + // WHEN + telemetryService.saveAttributes(request); + + // THEN + then(deviceStateManager).should().onDeviceInactivityTimeoutUpdate(tenantId, deviceId, 3000L, TbCallback.EMPTY); + } + + @Test + void shouldUseInactivityTimeoutEntryWithTheGreatestLastUpdateTsWhenVersionsAreTheSame() { + // GIVEN + var deviceId = DeviceId.fromString("cc51e450-53e1-11ee-883e-e56b48fd2088"); + List entries = List.of( + new BaseAttributeKvEntry(new LongDataEntry("inactivityTimeout", 1000L), 1L, 1L), + new BaseAttributeKvEntry(new LongDataEntry("inactivityTimeout", 2000L), 2L, 1L), + new BaseAttributeKvEntry(new LongDataEntry("inactivityTimeout", 3000L), 3L, 1L) + ); + + var request = AttributesSaveRequest.builder() + .tenantId(tenantId) + .entityId(deviceId) + .scope(AttributeScope.SERVER_SCOPE) + .entries(entries) + .strategy(new AttributesSaveRequest.Strategy(true, false, false)) + .build(); + + given(attrService.save(tenantId, deviceId, request.getScope(), request.getEntries())).willReturn(immediateFuture(listOfNNumbers(request.getEntries().size()))); + + // WHEN + telemetryService.saveAttributes(request); + + // THEN + then(deviceStateManager).should().onDeviceInactivityTimeoutUpdate(tenantId, deviceId, 3000L, TbCallback.EMPTY); + } + /* --- Delete attributes API --- */ @Test @@ -807,6 +989,121 @@ class DefaultTelemetrySubscriptionServiceTest { then(clusterService).should(never()).pushMsgToCore(any(), any()); } + @Test + void shouldNotifyDeviceStateManagerWhenDeviceInactivityTimeoutWasDeleted() { + // GIVEN + var deviceId = DeviceId.fromString("cc51e450-53e1-11ee-883e-e56b48fd2088"); + + var request = AttributesDeleteRequest.builder() + .tenantId(tenantId) + .entityId(deviceId) + .scope(AttributeScope.SERVER_SCOPE) + .keys(List.of("inactivityTimeout", "someOtherDeletedAttribute")) + .build(); + + given(attrService.removeAll(tenantId, deviceId, request.getScope(), request.getKeys())).willReturn(immediateFuture(request.getKeys())); + + // WHEN + telemetryService.deleteAttributes(request); + + // THEN + then(deviceStateManager).should().onDeviceInactivityTimeoutUpdate(tenantId, deviceId, 0L, TbCallback.EMPTY); + } + + @ParameterizedTest + @EnumSource( + value = EntityType.class, + names = {"DEVICE", "API_USAGE_STATE"}, // API usage state excluded due to coverage in another test + mode = EnumSource.Mode.EXCLUDE + ) + void shouldNotNotifyDeviceStateManagerWhenInactivityTimeoutWasDeletedButEntityTypeIsNotDevice(EntityType entityType) { + // GIVEN + var nonDeviceId = EntityIdFactory.getByTypeAndUuid(entityType, "cc51e450-53e1-11ee-883e-e56b48fd2088"); + + var request = AttributesDeleteRequest.builder() + .tenantId(tenantId) + .entityId(nonDeviceId) + .scope(AttributeScope.SERVER_SCOPE) + .keys(List.of("inactivityTimeout", "someOtherDeletedAttribute")) + .build(); + + given(attrService.removeAll(tenantId, nonDeviceId, request.getScope(), request.getKeys())).willReturn(immediateFuture(request.getKeys())); + + // WHEN + telemetryService.deleteAttributes(request); + + // THEN + then(deviceStateManager).shouldHaveNoInteractions(); + } + + @ParameterizedTest + @EnumSource( + value = AttributeScope.class, + names = {"SERVER_SCOPE"}, + mode = EnumSource.Mode.EXCLUDE + ) + void shouldNotNotifyDeviceStateManagerWhenInactivityTimeoutWasDeletedButAttributeScopeIsNotServer(AttributeScope nonServerScope) { + // GIVEN + var deviceId = DeviceId.fromString("cc51e450-53e1-11ee-883e-e56b48fd2088"); + + var request = AttributesDeleteRequest.builder() + .tenantId(tenantId) + .entityId(deviceId) + .scope(nonServerScope) + .keys(List.of("inactivityTimeout", "someOtherDeletedAttribute")) + .build(); + + given(attrService.removeAll(tenantId, deviceId, request.getScope(), request.getKeys())).willReturn(immediateFuture(request.getKeys())); + + // WHEN + telemetryService.deleteAttributes(request); + + // THEN + then(deviceStateManager).shouldHaveNoInteractions(); + } + + @Test + void shouldNotNotifyDeviceStateManagerWhenInactivityTimeoutWasNotDeleted() { + // GIVEN + var deviceId = DeviceId.fromString("cc51e450-53e1-11ee-883e-e56b48fd2088"); + + var request = AttributesDeleteRequest.builder() + .tenantId(tenantId) + .entityId(deviceId) + .scope(AttributeScope.SERVER_SCOPE) + .keys(List.of("someOtherDeletedAttribute")) + .build(); + + given(attrService.removeAll(tenantId, deviceId, request.getScope(), request.getKeys())).willReturn(immediateFuture(request.getKeys())); + + // WHEN + telemetryService.deleteAttributes(request); + + // THEN + then(deviceStateManager).shouldHaveNoInteractions(); + } + + @Test + void shouldNotNotifyDeviceStateManagerWhenDeviceInactivityTimeoutDeleteFailed() { + // GIVEN + var deviceId = DeviceId.fromString("cc51e450-53e1-11ee-883e-e56b48fd2088"); + + var request = AttributesDeleteRequest.builder() + .tenantId(tenantId) + .entityId(deviceId) + .scope(AttributeScope.SERVER_SCOPE) + .keys(List.of("inactivityTimeout", "someOtherDeletedAttribute")) + .build(); + + given(attrService.removeAll(tenantId, deviceId, request.getScope(), request.getKeys())).willReturn(immediateFailedFuture(new RuntimeException("failed to delete"))); + + // WHEN + telemetryService.deleteAttributes(request); + + // THEN + then(deviceStateManager).shouldHaveNoInteractions(); + } + // used to emulate versions returned by save APIs private static List listOfNNumbers(int N) { return LongStream.range(0, N).boxed().toList(); From 7ca43d386c8aa26ec4f40660df547af3c42092cc Mon Sep 17 00:00:00 2001 From: ViacheslavKlimov Date: Fri, 7 Mar 2025 12:11:57 +0200 Subject: [PATCH 231/345] Add 3.9.1 to SUPPORTED_VERSIONS_FOR_UPGRADE --- .../service/install/DefaultDatabaseSchemaSettingsService.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/application/src/main/java/org/thingsboard/server/service/install/DefaultDatabaseSchemaSettingsService.java b/application/src/main/java/org/thingsboard/server/service/install/DefaultDatabaseSchemaSettingsService.java index 0f4cf30fac..a0012a6fb3 100644 --- a/application/src/main/java/org/thingsboard/server/service/install/DefaultDatabaseSchemaSettingsService.java +++ b/application/src/main/java/org/thingsboard/server/service/install/DefaultDatabaseSchemaSettingsService.java @@ -32,7 +32,7 @@ public class DefaultDatabaseSchemaSettingsService implements DatabaseSchemaSetti // This list should include all versions which are compatible for the upgrade. // The compatibility cycle usually breaks when we have some scripts written in Java that may not work after new release. - private static final List SUPPORTED_VERSIONS_FOR_UPGRADE = List.of("3.9.0"); + private static final List SUPPORTED_VERSIONS_FOR_UPGRADE = List.of("3.9.0", "3.9.1"); private final ProjectInfo projectInfo; private final JdbcTemplate jdbcTemplate; From a76149d60da8d8d0c40c72ac7dc6fae513842d81 Mon Sep 17 00:00:00 2001 From: mpetrov Date: Fri, 7 Mar 2025 12:27:58 +0200 Subject: [PATCH 232/345] Changed Calculated fields highlights and autocompletes --- .../calculated-field-dialog.component.scss | 14 ++- .../calculated-field-dialog.component.ts | 2 +- ...lculated-field-argument-panel.component.ts | 2 +- .../shared/models/calculated-field.models.ts | 95 +++++++++++++++---- 4 files changed, 88 insertions(+), 25 deletions(-) diff --git a/ui-ngx/src/app/modules/home/components/calculated-fields/components/dialog/calculated-field-dialog.component.scss b/ui-ngx/src/app/modules/home/components/calculated-fields/components/dialog/calculated-field-dialog.component.scss index 3bb2759274..8bc422eed1 100644 --- a/ui-ngx/src/app/modules/home/components/calculated-fields/components/dialog/calculated-field-dialog.component.scss +++ b/ui-ngx/src/app/modules/home/components/calculated-fields/components/dialog/calculated-field-dialog.component.scss @@ -36,14 +36,20 @@ .tb-js-func { .ace_tb { &.ace_calculated-field { - &-key { + &-ctx { color: #C52F00; } - &-ts, &-time-window, &-values, &-value, &-func { + &-args { + color: #185F2A; + } + &-key { + color: #c24c1a; + } + &-time-window, &-values, &-func, &-value, &-ts { color: #7214D0; } - &-start-ts, &-end-ts, &-limit { - color: #185F2A; + &-start-ts, &-end-ts { + color: #2CAA00; } } } diff --git a/ui-ngx/src/app/modules/home/components/calculated-fields/components/dialog/calculated-field-dialog.component.ts b/ui-ngx/src/app/modules/home/components/calculated-fields/components/dialog/calculated-field-dialog.component.ts index 0c3e7464cd..052b660c3c 100644 --- a/ui-ngx/src/app/modules/home/components/calculated-fields/components/dialog/calculated-field-dialog.component.ts +++ b/ui-ngx/src/app/modules/home/components/calculated-fields/components/dialog/calculated-field-dialog.component.ts @@ -70,7 +70,7 @@ export class CalculatedFieldDialogComponent extends DialogComponent Object.keys(argumentsObj)) + map(argumentsObj => ['ctx', ...Object.keys(argumentsObj)]) ); argumentsEditorCompleter$ = this.configFormGroup.get('arguments').valueChanges diff --git a/ui-ngx/src/app/modules/home/components/calculated-fields/components/panel/calculated-field-argument-panel.component.ts b/ui-ngx/src/app/modules/home/components/calculated-fields/components/panel/calculated-field-argument-panel.component.ts index 7e41f60968..1058f70b49 100644 --- a/ui-ngx/src/app/modules/home/components/calculated-fields/components/panel/calculated-field-argument-panel.component.ts +++ b/ui-ngx/src/app/modules/home/components/calculated-fields/components/panel/calculated-field-argument-panel.component.ts @@ -64,7 +64,7 @@ export class CalculatedFieldArgumentPanelComponent implements OnInit { readonly defaultLimit = Math.floor(this.maxDataPointsPerRollingArg / 10); argumentFormGroup = this.fb.group({ - argumentName: ['', [Validators.required, this.uniqNameRequired(), Validators.pattern(charsWithNumRegex), Validators.maxLength(255)]], + argumentName: ['', [Validators.required, this.uniqNameRequired(), Validators.pattern(/^(?!ctx$).+$/), Validators.pattern(charsWithNumRegex), Validators.maxLength(255)]], refEntityId: this.fb.group({ entityType: [ArgumentEntityType.Current], id: [''] diff --git a/ui-ngx/src/app/shared/models/calculated-field.models.ts b/ui-ngx/src/app/shared/models/calculated-field.models.ts index 7083ce7ae0..d20a9d8924 100644 --- a/ui-ngx/src/app/shared/models/calculated-field.models.ts +++ b/ui-ngx/src/app/shared/models/calculated-field.models.ts @@ -235,10 +235,10 @@ export type CalculatedFieldArgumentEventValue = CalculatedF export type CalculatedFieldEventArguments = Record>; -export const CalculatedFieldLatestTelemetryArgumentAutocomplete = { +export const CalculatedFieldCtxLatestTelemetryArgumentAutocomplete = { meta: 'object', type: '{ ts: number; value: any; }', - description: 'Calculated field latest telemetry value argument.', + description: 'Calculated field context latest telemetry value argument.', children: { ts: { meta: 'number', @@ -253,10 +253,10 @@ export const CalculatedFieldLatestTelemetryArgumentAutocomplete = { }, }; -export const CalculatedFieldAttributeValueArgumentAutocomplete = { +export const CalculatedFieldCtxAttributeValueArgumentAutocomplete = { meta: 'object', type: '{ ts: number; value: any; }', - description: 'Calculated field attribute value argument.', + description: 'Calculated field context attribute value argument.', children: { ts: { meta: 'number', @@ -270,6 +270,19 @@ export const CalculatedFieldAttributeValueArgumentAutocomplete = { } }, }; + +export const CalculatedFieldLatestTelemetryArgumentAutocomplete = { + meta: 'any', + type: 'any', + description: 'Calculated field latest telemetry argument value.', +}; + +export const CalculatedFieldAttributeValueArgumentAutocomplete = { + meta: 'any', + type: 'any', + description: 'Calculated field attribute argument value.', +}; + export const CalculatedFieldRollingValueArgumentFunctionsAutocomplete = { max: { meta: 'function', @@ -513,35 +526,84 @@ export const getCalculatedFieldArgumentsEditorCompleter = (argumentsObj: Record< switch (argumentsObj[key].refEntityKey.type) { case ArgumentType.Attribute: acc[key] = CalculatedFieldAttributeValueArgumentAutocomplete; + acc.ctx.children.args.children[key] = CalculatedFieldCtxAttributeValueArgumentAutocomplete; break; case ArgumentType.LatestTelemetry: - acc[key] = CalculatedFieldLatestTelemetryArgumentAutocomplete; + acc[key] = { ...CalculatedFieldLatestTelemetryArgumentAutocomplete, children: {} }; + acc.ctx.children.args.children[key] = CalculatedFieldCtxLatestTelemetryArgumentAutocomplete; break; case ArgumentType.Rolling: acc[key] = CalculatedFieldRollingValueArgumentAutocomplete; + acc.ctx.children.args.children[key] = CalculatedFieldRollingValueArgumentAutocomplete; break; } return acc; - }, {})); + }, { + ctx: { + meta: 'object', + type: '{ args: { [key: string]: object } }', + description: 'Calculated field context.', + children: { + args: { + meta: 'object', + type: '{ [key: string]: object }', + description: 'Calculated field context arguments.', + children: {} + } + } + } + })); } export const getCalculatedFieldArgumentsHighlights = ( argumentsObj: Record ): AceHighlightRules => { + const calculatedFieldArgumentsKeys = Object.keys(argumentsObj).map(key => ({ + token: 'tb.calculated-field-key', + regex: `\\b${key}\\b`, + next: argumentsObj[key].refEntityKey.type === ArgumentType.Rolling + ? 'calculatedFieldRollingArgumentValue' + : 'start' + })); + const calculatedFieldCtxArgumentsHighlightRules = { + calculatedFieldCtxArgs: [ + dotOperatorHighlightRule, + ...calculatedFieldArgumentsKeys.map(argumentRule => argumentRule.next === 'start' ? {...argumentRule, next: 'calculatedFieldSingleArgumentValue' } : argumentRule), + endGroupHighlightRule + ] + }; + return { - start: Object.keys(argumentsObj).map(key => ({ - token: 'tb.calculated-field-key', - regex: `\\b${key}\\b`, - next: argumentsObj[key].refEntityKey.type === ArgumentType.Rolling - ? 'calculatedFieldRollingArgumentValue' - : 'calculatedFieldSingleArgumentValue' - })), + start: [ + calculatedFieldArgumentsContextHighlightRules, + ...calculatedFieldArgumentsKeys, + ], + ...calculatedFieldArgumentsContextValueHighlightRules, + ...calculatedFieldCtxArgumentsHighlightRules, ...calculatedFieldSingleArgumentValueHighlightRules, ...calculatedFieldRollingArgumentValueHighlightRules, ...calculatedFieldTimeWindowArgumentValueHighlightRules }; }; +const calculatedFieldArgumentsContextHighlightRules: AceHighlightRule = { + token: 'tb.calculated-field-ctx', + regex: /ctx/, + next: 'calculatedFieldCtxValue' +} + +const calculatedFieldArgumentsContextValueHighlightRules: AceHighlightRules = { + calculatedFieldCtxValue: [ + dotOperatorHighlightRule, + { + token: 'tb.calculated-field-args', + regex: /args/, + next: 'calculatedFieldCtxArgs' + }, + endGroupHighlightRule + ] +} + const calculatedFieldSingleArgumentValueHighlightRules: AceHighlightRules = { calculatedFieldSingleArgumentValue: [ dotOperatorHighlightRule, @@ -597,16 +659,11 @@ const calculatedFieldTimeWindowArgumentValueHighlightRules: AceHighlightRules = regex: /endTs/, next: 'no_regex' }, - { - token: 'tb.calculated-field-limit', - regex: /limit/, - next: 'no_regex' - }, endGroupHighlightRule ] } export const calculatedFieldDefaultScript = 'return {\n' + ' // Convert Fahrenheit to Celsius\n' + - ' "temperatureCelsius": (temperature.value - 32) / 1.8\n' + + ' "temperatureCelsius": (temperature - 32) / 1.8\n' + '};' From dc648e0d8ca5b14d4cffda5c8d6f1c6c883c7baf Mon Sep 17 00:00:00 2001 From: mpetrov Date: Fri, 7 Mar 2025 12:31:24 +0200 Subject: [PATCH 233/345] refactoring --- ui-ngx/src/app/shared/models/calculated-field.models.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/ui-ngx/src/app/shared/models/calculated-field.models.ts b/ui-ngx/src/app/shared/models/calculated-field.models.ts index d20a9d8924..c898342c68 100644 --- a/ui-ngx/src/app/shared/models/calculated-field.models.ts +++ b/ui-ngx/src/app/shared/models/calculated-field.models.ts @@ -225,7 +225,7 @@ export interface CalculatedFieldLatestTelemetryArgumentValue extends CalculatedFieldArgumentValueBase { - timeWindow: { startTs: number; endTs: number; limit: number }; + timeWindow: { startTs: number; endTs: number; }; values: CalculatedFieldSingleArgumentValue[]; } @@ -529,7 +529,7 @@ export const getCalculatedFieldArgumentsEditorCompleter = (argumentsObj: Record< acc.ctx.children.args.children[key] = CalculatedFieldCtxAttributeValueArgumentAutocomplete; break; case ArgumentType.LatestTelemetry: - acc[key] = { ...CalculatedFieldLatestTelemetryArgumentAutocomplete, children: {} }; + acc[key] = CalculatedFieldLatestTelemetryArgumentAutocomplete; acc.ctx.children.args.children[key] = CalculatedFieldCtxLatestTelemetryArgumentAutocomplete; break; case ArgumentType.Rolling: From 7e49cd9ef06f436d3324119253b3f13243584d14 Mon Sep 17 00:00:00 2001 From: mpetrov Date: Fri, 7 Mar 2025 12:49:50 +0200 Subject: [PATCH 234/345] Fixed cf test script dialog styles --- ...ed-field-script-test-dialog.component.html | 2 +- ...ed-field-script-test-dialog.component.scss | 107 ++++++++++-------- ...ated-field-script-test-dialog.component.ts | 4 +- 3 files changed, 66 insertions(+), 47 deletions(-) diff --git a/ui-ngx/src/app/modules/home/components/calculated-fields/components/test-dialog/calculated-field-script-test-dialog.component.html b/ui-ngx/src/app/modules/home/components/calculated-fields/components/test-dialog/calculated-field-script-test-dialog.component.html index cda99f6c09..6859ff6dab 100644 --- a/ui-ngx/src/app/modules/home/components/calculated-fields/components/test-dialog/calculated-field-script-test-dialog.component.html +++ b/ui-ngx/src/app/modules/home/components/calculated-fields/components/test-dialog/calculated-field-script-test-dialog.component.html @@ -15,7 +15,7 @@ limitations under the License. --> -
    +

    {{ 'calculated-fields.test-script-function' | translate }} ({{ 'api-usage.tbel' | translate }})

    - } - @case (widgetHeaderActionButtonType.miniFab) { - - } - @case (widgetHeaderActionButtonType.basic) { - - } - @case (widgetHeaderActionButtonType.raised) { - - } - @case (widgetHeaderActionButtonType.stroked) { - - } - @case (widgetHeaderActionButtonType.flat) { - - } - } - +
    + @for (action of widget.customHeaderActions; track action.name; let last = $last) { + + }
    + } + @case (widgetHeaderActionButtonType.miniFab) { + + } + @case (widgetHeaderActionButtonType.basic) { + + } + @case (widgetHeaderActionButtonType.raised) { + + } + @case (widgetHeaderActionButtonType.stroked) { + + } + @case (widgetHeaderActionButtonType.flat) { + + } +} + diff --git a/ui-ngx/src/app/modules/home/components/widget/widget-container.component.scss b/ui-ngx/src/app/modules/home/components/widget/widget-container.component.scss index d763c11c9e..b246bc1662 100644 --- a/ui-ngx/src/app/modules/home/components/widget/widget-container.component.scss +++ b/ui-ngx/src/app/modules/home/components/widget/widget-container.component.scss @@ -86,8 +86,8 @@ div.tb-widget { flex-direction: row; place-content: center flex-start; align-items: center; - z-index: 19; - margin: 5px 0 5px; + z-index: 101; + margin: 5px 0 0; &-absolute { position: absolute; From 8df75df8587e31db3c5c5d4c14ce1c92c5ccae3f Mon Sep 17 00:00:00 2001 From: dashevchenko Date: Fri, 7 Mar 2025 14:11:10 +0200 Subject: [PATCH 238/345] fixed repository settings deletion --- .../DefaultEntitiesVersionControlService.java | 2 + .../controller/RepositorySettingsTest.java | 79 +++++++++++++++++++ 2 files changed, 81 insertions(+) create mode 100644 application/src/test/java/org/thingsboard/server/controller/RepositorySettingsTest.java diff --git a/application/src/main/java/org/thingsboard/server/service/sync/vc/DefaultEntitiesVersionControlService.java b/application/src/main/java/org/thingsboard/server/service/sync/vc/DefaultEntitiesVersionControlService.java index 89b51ce84a..8ef6465d4b 100644 --- a/application/src/main/java/org/thingsboard/server/service/sync/vc/DefaultEntitiesVersionControlService.java +++ b/application/src/main/java/org/thingsboard/server/service/sync/vc/DefaultEntitiesVersionControlService.java @@ -529,6 +529,8 @@ public class DefaultEntitiesVersionControlService implements EntitiesVersionCont @Override public ListenableFuture deleteVersionControlSettings(TenantId tenantId) { + log.debug("[{}] Deleting version control settings", tenantId); + repositorySettingsService.delete(tenantId); return gitServiceQueue.clearRepository(tenantId); } diff --git a/application/src/test/java/org/thingsboard/server/controller/RepositorySettingsTest.java b/application/src/test/java/org/thingsboard/server/controller/RepositorySettingsTest.java new file mode 100644 index 0000000000..9cd75e9a3c --- /dev/null +++ b/application/src/test/java/org/thingsboard/server/controller/RepositorySettingsTest.java @@ -0,0 +1,79 @@ +/** + * 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.controller; + +import com.google.common.util.concurrent.SettableFuture; +import lombok.extern.slf4j.Slf4j; +import org.junit.Test; +import org.springframework.boot.test.mock.mockito.MockBean; +import org.thingsboard.server.common.data.sync.vc.RepositoryAuthMethod; +import org.thingsboard.server.common.data.sync.vc.RepositorySettings; +import org.thingsboard.server.dao.service.DaoSqlTest; +import org.thingsboard.server.service.sync.vc.GitVersionControlQueueService; + +import static org.hamcrest.Matchers.is; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.when; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.content; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; + +@Slf4j +@DaoSqlTest +public class RepositorySettingsTest extends AbstractControllerTest { + + @MockBean + private GitVersionControlQueueService gitVersionControlQueueService; + + @Test + public void testFindRepositorySettings() throws Exception { + loginTenantAdmin(); + doGet("/api/admin/repositorySettings") + .andExpect(status().isNotFound()); + + String testRepositoryUri = "https://github.com/test/version-control-test-repository.git"; + + SettableFuture successFuture = SettableFuture.create(); + successFuture.set(null); + when(gitVersionControlQueueService.initRepository(any(), any())) + .thenReturn(successFuture); + + RepositorySettings repositorySettings = new RepositorySettings(); + repositorySettings.setPassword("test"); + repositorySettings.setAuthMethod(RepositoryAuthMethod.USERNAME_PASSWORD); + repositorySettings.setRepositoryUri(testRepositoryUri); + repositorySettings.setDefaultBranch("main"); + doPost("/api/admin/repositorySettings", repositorySettings) + .andExpect(status().isOk()); + + // check repository settings + doGet("/api/admin/repositorySettings") + .andExpect(status().isOk()) + .andExpect(content().contentType(contentType)) + .andExpect(jsonPath("$.repositoryUri", is(testRepositoryUri))); + + // delete settings + when(gitVersionControlQueueService.clearRepository(any())) + .thenReturn(successFuture); + doDelete("/api/admin/repositorySettings") + .andExpect(status().isOk()); + + // check repository settings + doGet("/api/admin/repositorySettings") + .andExpect(status().isNotFound()); + } + +} From 2b50d3159cd4d3c45bd83ab38a27ce4a722bf9c5 Mon Sep 17 00:00:00 2001 From: IrynaMatveieva Date: Fri, 7 Mar 2025 14:15:31 +0200 Subject: [PATCH 239/345] added index for cf debug event --- .../data/tenant/profile/DefaultTenantProfileConfiguration.java | 1 + dao/src/main/resources/sql/schema-entities-idx-psql-addon.sql | 2 ++ 2 files changed, 3 insertions(+) diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/tenant/profile/DefaultTenantProfileConfiguration.java b/common/data/src/main/java/org/thingsboard/server/common/data/tenant/profile/DefaultTenantProfileConfiguration.java index 06a0489037..f256b02d9a 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/tenant/profile/DefaultTenantProfileConfiguration.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/tenant/profile/DefaultTenantProfileConfiguration.java @@ -140,6 +140,7 @@ public class DefaultTenantProfileConfiguration implements TenantProfileConfigura private long maxCalculatedFieldsPerEntity = 5; @Schema(example = "10") private long maxArgumentsPerCF = 10; + @Builder.Default @Min(value = 1, message = "must be at least 1") @Schema(example = "1000") private long maxDataPointsPerRollingArg = 1000; diff --git a/dao/src/main/resources/sql/schema-entities-idx-psql-addon.sql b/dao/src/main/resources/sql/schema-entities-idx-psql-addon.sql index b0c69ef6f3..b756d282f8 100644 --- a/dao/src/main/resources/sql/schema-entities-idx-psql-addon.sql +++ b/dao/src/main/resources/sql/schema-entities-idx-psql-addon.sql @@ -36,3 +36,5 @@ CREATE INDEX IF NOT EXISTS idx_lc_event_main CREATE INDEX IF NOT EXISTS idx_error_event_main ON error_event (tenant_id ASC, entity_id ASC, ts DESC NULLS LAST) WITH (FILLFACTOR=95); +CREATE INDEX IF NOT EXISTS idx_cf_debug_event_main + ON cf_debug_event (tenant_id ASC, entity_id ASC, ts DESC NULLS LAST) WITH (FILLFACTOR=95); From ca350601bb47853738d9280fb21bd12e821e08ed Mon Sep 17 00:00:00 2001 From: mpetrov Date: Fri, 7 Mar 2025 14:46:25 +0200 Subject: [PATCH 240/345] Resolved conflicts --- .../calculated-fields/calculated-fields-table.component.ts | 2 -- 1 file changed, 2 deletions(-) diff --git a/ui-ngx/src/app/modules/home/components/calculated-fields/calculated-fields-table.component.ts b/ui-ngx/src/app/modules/home/components/calculated-fields/calculated-fields-table.component.ts index 91a4d69252..e10c4b301e 100644 --- a/ui-ngx/src/app/modules/home/components/calculated-fields/calculated-fields-table.component.ts +++ b/ui-ngx/src/app/modules/home/components/calculated-fields/calculated-fields-table.component.ts @@ -58,8 +58,6 @@ export class CalculatedFieldsTableComponent { private dialog: MatDialog, private store: Store, private datePipe: DatePipe, - private durationLeft: DurationLeftPipe, - private popoverService: TbPopoverService, private cd: ChangeDetectorRef, private renderer: Renderer2, private importExportService: ImportExportService, From 04c7160e73223e64897b2d93c19f98393ad0d3ab Mon Sep 17 00:00:00 2001 From: Artem Dzhereleiko Date: Fri, 7 Mar 2025 15:01:40 +0200 Subject: [PATCH 241/345] UI: Refactoring --- .../widget/action/widget-action-dialog.component.ts | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/ui-ngx/src/app/modules/home/components/widget/action/widget-action-dialog.component.ts b/ui-ngx/src/app/modules/home/components/widget/action/widget-action-dialog.component.ts index b921c89c52..b439a5771f 100644 --- a/ui-ngx/src/app/modules/home/components/widget/action/widget-action-dialog.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/action/widget-action-dialog.component.ts @@ -14,7 +14,7 @@ /// limitations under the License. /// -import { Component, DestroyRef, Inject, OnDestroy, OnInit, SkipSelf, ViewChild } from '@angular/core'; +import { Component, DestroyRef, Inject, OnInit, SkipSelf, ViewChild } from '@angular/core'; import { ErrorStateMatcher } from '@angular/material/core'; import { MAT_DIALOG_DATA, MatDialogRef } from '@angular/material/dialog'; import { Store } from '@ngrx/store'; @@ -69,7 +69,7 @@ export interface WidgetActionDialogData { styleUrls: [] }) export class WidgetActionDialogComponent extends DialogComponent implements OnInit, OnDestroy, ErrorStateMatcher { + WidgetActionDescriptorInfo> implements OnInit, ErrorStateMatcher { widgetActionFormGroup: FormGroup; @@ -169,10 +169,6 @@ export class WidgetActionDialogComponent extends DialogComponent Date: Fri, 7 Mar 2025 15:12:24 +0200 Subject: [PATCH 242/345] Implemented tbel utils autocompletes and highlights --- .../shared/components/js-func.component.scss | 6 + .../shared/components/js-func.component.ts | 7 + .../shared/models/ace/tbel-utils.models.ts | 1287 +++++++++++++++++ 3 files changed, 1300 insertions(+) create mode 100644 ui-ngx/src/app/shared/models/ace/tbel-utils.models.ts diff --git a/ui-ngx/src/app/shared/components/js-func.component.scss b/ui-ngx/src/app/shared/components/js-func.component.scss index eecfe72c3e..12b1d57ccc 100644 --- a/ui-ngx/src/app/shared/components/js-func.component.scss +++ b/ui-ngx/src/app/shared/components/js-func.component.scss @@ -79,4 +79,10 @@ background: #f3f3f3; } } + + .ace_tb { + &.ace_tbel-utils-func { + color: #0000A2; + } + } } diff --git a/ui-ngx/src/app/shared/components/js-func.component.ts b/ui-ngx/src/app/shared/components/js-func.component.ts index eccb5cb908..682cc872e6 100644 --- a/ui-ngx/src/app/shared/components/js-func.component.ts +++ b/ui-ngx/src/app/shared/components/js-func.component.ts @@ -50,6 +50,7 @@ import { JsFuncModulesComponent } from '@shared/components/js-func-modules.compo import { HttpClient } from '@angular/common/http'; import { map, Observable, of } from 'rxjs'; import { catchError } from 'rxjs/operators'; +import { tbelUtilsAutocompletes, tbelUtilsFuncHighlightRules } from '@shared/models/ace/tbel-utils.models'; @Component({ selector: 'tb-js-func', @@ -587,6 +588,9 @@ export class JsFuncComponent implements OnInit, OnChanges, OnDestroy, ControlVal newMode.$highlightRules.$rules[group] = this.highlightRules[group]; } } + if (this.scriptLanguage === ScriptLanguage.TBEL) { + newMode.$highlightRules.$rules.start = [...tbelUtilsFuncHighlightRules, ...newMode.$highlightRules.$rules.start]; + } const identifierRule = newMode.$highlightRules.$rules.no_regex.find(rule => rule.token?.includes('identifier')); if (identifierRule) { identifierRule.next = 'start'; @@ -641,6 +645,9 @@ export class JsFuncComponent implements OnInit, OnChanges, OnDestroy, ControlVal if (modulesCompleter) { completers.push(modulesCompleter); } + if (this.scriptLanguage === ScriptLanguage.TBEL) { + completers.push(tbelUtilsAutocompletes); + } completers.push(...this.initialCompleters); this.jsEditor.completers = completers; }); diff --git a/ui-ngx/src/app/shared/models/ace/tbel-utils.models.ts b/ui-ngx/src/app/shared/models/ace/tbel-utils.models.ts new file mode 100644 index 0000000000..24b6d9eaaf --- /dev/null +++ b/ui-ngx/src/app/shared/models/ace/tbel-utils.models.ts @@ -0,0 +1,1287 @@ +import { AceHighlightRule } from '@shared/models/ace/ace.models'; +import { TbEditorCompleter } from '@shared/models/ace/completion.models'; + +export const tbelUtilsAutocompletes = new TbEditorCompleter({ + btoa: { + meta: 'function', + description: 'Encodes a string to Base64.', + args: [ + { + name: 'input', + description: 'The string to encode', + type: 'string' + } + ], + return: { + description: 'The Base64 encoded string', + type: 'string' + } + }, + atob: { + meta: 'function', + description: 'Decodes a Base64 encoded string.', + args: [ + { + name: 'encoded', + description: 'The Base64 encoded string to decode', + type: 'string' + } + ], + return: { + description: 'The decoded string', + type: 'string' + } + }, + bytesToString: { + meta: 'function', + description: 'Converts a list of bytes to a string, optionally specifying the charset.', + args: [ + { + name: 'bytesList', + description: 'The list of bytes to convert', + type: 'array' + }, + { + name: 'charsetName', + description: 'The charset to use for conversion (e.g., "UTF-8")', + type: 'string', + optional: true + } + ], + return: { + description: 'The string representation of the bytes', + type: 'string' + } + }, + decodeToString: { + meta: 'function', + description: 'Converts a list of bytes to a string using the default charset.', + args: [ + { + name: 'bytesList', + description: 'The list of bytes to convert', + type: 'array' + } + ], + return: { + description: 'The string representation of the bytes', + type: 'string' + } + }, + decodeToJson: { + meta: 'function', + description: 'Parses a JSON string or converts a list of bytes to a string and parses it as JSON.', + args: [ + { + name: 'input', + description: 'The JSON string or list of bytes to parse', + type: 'string | array' + } + ], + return: { + description: 'The parsed JSON object (e.g., object, array, or primitive)', + type: 'object' + } + }, + stringToBytes: { + meta: 'function', + description: 'Converts a string to a list of bytes, optionally specifying the charset.', + args: [ + { + name: 'str', + description: 'The string to convert', + type: 'string' + }, + { + name: 'charsetName', + description: 'The charset to use for conversion (e.g., "UTF-8")', + type: 'string', + optional: true + } + ], + return: { + description: 'The list of bytes representing the string', + type: 'array' + } + }, + parseInt: { + meta: 'function', + description: 'Parses a string to an integer, optionally specifying the radix.', + args: [ + { + name: 'value', + description: 'The string to parse', + type: 'string' + }, + { + name: 'radix', + description: 'The radix for parsing (e.g., 2 for binary, 16 for hex, defaults to auto-detection if 0)', + type: 'number', + optional: true + } + ], + return: { + description: 'The parsed integer, or null if invalid', + type: 'number' + } + }, + parseLong: { + meta: 'function', + description: 'Parses a string to a long integer, optionally specifying the radix.', + args: [ + { + name: 'value', + description: 'The string to parse', + type: 'string' + }, + { + name: 'radix', + description: 'The radix for parsing (e.g., 2 for binary, 16 for hex, defaults to auto-detection if 0)', + type: 'number', + optional: true + } + ], + return: { + description: 'The parsed long integer, or null if invalid', + type: 'number' + } + }, + parseFloat: { + meta: 'function', + description: 'Parses a string to a float. If radix is 16, interprets the string as hexadecimal IEEE 754 float bits.', + args: [ + { + name: 'value', + description: 'The string to parse', + type: 'string' + }, + { + name: 'radix', + description: 'The radix for parsing (e.g., 16 for hex, defaults to 10 if 0)', + type: 'number', + optional: true + } + ], + return: { + description: 'The parsed float, or null if invalid', + type: 'number' + } + }, + parseHexIntLongToFloat: { + meta: 'function', + description: 'Parses a hexadecimal string to a float, treating it as an integer value.', + args: [ + { + name: 'value', + description: 'The hexadecimal string to parse (e.g., "0x0A" for 10.0)', + type: 'string' + }, + { + name: 'bigEndian', + description: 'Whether to interpret the string in big-endian order', + type: 'boolean' + } + ], + return: { + description: 'The parsed float, or null if invalid', + type: 'number' + } + }, + parseDouble: { + meta: 'function', + description: 'Parses a string to a double. If radix is 16, interprets the string as hexadecimal IEEE 754 double bits.', + args: [ + { + name: 'value', + description: 'The string to parse', + type: 'string' + }, + { + name: 'radix', + description: 'The radix for parsing (e.g., 16 for hex, defaults to 10 if unspecified)', + type: 'number', + optional: true + } + ], + return: { + description: 'The parsed double, or null if invalid', + type: 'number' + } + }, + parseLittleEndianHexToInt: { + meta: 'function', + description: 'Parses a little-endian hexadecimal string to an integer.', + args: [ + { + name: 'hex', + description: 'The hexadecimal string to parse', + type: 'string' + } + ], + return: { + description: 'The parsed integer', + type: 'number' + } + }, + parseBigEndianHexToInt: { + meta: 'function', + description: 'Parses a big-endian hexadecimal string to an integer.', + args: [ + { + name: 'hex', + description: 'The hexadecimal string to parse', + type: 'string' + } + ], + return: { + description: 'The parsed integer', + type: 'number' + } + }, + parseHexToInt: { + meta: 'function', + description: 'Parses a hexadecimal string to an integer, optionally specifying endianness.', + args: [ + { + name: 'value', + description: 'The hexadecimal string to parse', + type: 'string' + }, + { + name: 'bigEndian', + description: 'Whether to interpret the string in big-endian order (defaults to true)', + type: 'boolean', + optional: true + } + ], + return: { + description: 'The parsed integer', + type: 'number' + } + }, + parseBytesToInt: { + meta: 'function', + description: 'Parses a list or array of bytes to an integer.', + args: [ + { + name: 'data', + description: 'The bytes to parse', + type: 'array' + }, + { + name: 'offset', + description: 'The starting index in the byte array', + type: 'number', + optional: true + }, + { + name: 'length', + description: 'The number of bytes to parse (max 4)', + type: 'number', + optional: true + }, + { + name: 'bigEndian', + description: 'Whether to interpret bytes in big-endian order (defaults to true)', + type: 'boolean', + optional: true + } + ], + return: { + description: 'The parsed integer', + type: 'number' + } + }, + parseLittleEndianHexToLong: { + meta: 'function', + description: 'Parses a little-endian hexadecimal string to a long integer.', + args: [ + { + name: 'hex', + description: 'The hexadecimal string to parse', + type: 'string' + } + ], + return: { + description: 'The parsed long integer', + type: 'number' + } + }, + parseBigEndianHexToLong: { + meta: 'function', + description: 'Parses a big-endian hexadecimal string to a long integer.', + args: [ + { + name: 'hex', + description: 'The hexadecimal string to parse', + type: 'string' + } + ], + return: { + description: 'The parsed long integer', + type: 'number' + } + }, + parseHexToLong: { + meta: 'function', + description: 'Parses a hexadecimal string to a long integer, optionally specifying endianness.', + args: [ + { + name: 'value', + description: 'The hexadecimal string to parse', + type: 'string' + }, + { + name: 'bigEndian', + description: 'Whether to interpret the string in big-endian order (defaults to true)', + type: 'boolean', + optional: true + } + ], + return: { + description: 'The parsed long integer', + type: 'number' + } + }, + parseBytesToLong: { + meta: 'function', + description: 'Parses a list or array of bytes to a long integer.', + args: [ + { + name: 'data', + description: 'The bytes to parse', + type: 'array' + }, + { + name: 'offset', + description: 'The starting index in the byte array', + type: 'number', + optional: true + }, + { + name: 'length', + description: 'The number of bytes to parse (max 8)', + type: 'number', + optional: true + }, + { + name: 'bigEndian', + description: 'Whether to interpret bytes in big-endian order (defaults to true)', + type: 'boolean', + optional: true + } + ], + return: { + description: 'The parsed long integer', + type: 'number' + } + }, + parseLittleEndianHexToFloat: { + meta: 'function', + description: 'Parses a little-endian hexadecimal string to a float using IEEE 754 format.', + args: [ + { + name: 'hex', + description: 'The hexadecimal string to parse', + type: 'string' + } + ], + return: { + description: 'The parsed float', + type: 'number' + } + }, + parseBigEndianHexToFloat: { + meta: 'function', + description: 'Parses a big-endian hexadecimal string to a float using IEEE 754 format.', + args: [ + { + name: 'hex', + description: 'The hexadecimal string to parse', + type: 'string' + } + ], + return: { + description: 'The parsed float', + type: 'number' + } + }, + parseHexToFloat: { + meta: 'function', + description: 'Parses a hexadecimal string to a float using IEEE 754 format, optionally specifying endianness.', + args: [ + { + name: 'value', + description: 'The hexadecimal string to parse', + type: 'string' + }, + { + name: 'bigEndian', + description: 'Whether to interpret the string in big-endian order (defaults to true)', + type: 'boolean', + optional: true + } + ], + return: { + description: 'The parsed float', + type: 'number' + } + }, + parseBytesToFloat: { + meta: 'function', + description: 'Parses a list or array of bytes to a float using IEEE 754 format.', + args: [ + { + name: 'data', + description: 'The bytes to parse', + type: 'array' + }, + { + name: 'offset', + description: 'The starting index in the byte array', + type: 'number', + optional: true + }, + { + name: 'length', + description: 'The number of bytes to parse (max 4)', + type: 'number', + optional: true + }, + { + name: 'bigEndian', + description: 'Whether to interpret bytes in big-endian order (defaults to true)', + type: 'boolean', + optional: true + } + ], + return: { + description: 'The parsed float', + type: 'number' + } + }, + parseBytesIntToFloat: { + meta: 'function', + description: 'Parses a list or array of bytes to a float by first interpreting them as an integer.', + args: [ + { + name: 'data', + description: 'The bytes to parse', + type: 'array' + }, + { + name: 'offset', + description: 'The starting index in the byte array', + type: 'number', + optional: true + }, + { + name: 'length', + description: 'The number of bytes to parse (max 4)', + type: 'number', + optional: true + }, + { + name: 'bigEndian', + description: 'Whether to interpret bytes in big-endian order (defaults to true)', + type: 'boolean', + optional: true + } + ], + return: { + description: 'The parsed float', + type: 'number' + } + }, + parseLittleEndianHexToDouble: { + meta: 'function', + description: 'Parses a little-endian hexadecimal string to a double using IEEE 754 format.', + args: [ + { + name: 'hex', + description: 'The hexadecimal string to parse', + type: 'string' + } + ], + return: { + description: 'The parsed double', + type: 'number' + } + }, + parseBigEndianHexToDouble: { + meta: 'function', + description: 'Parses a big-endian hexadecimal string to a double using IEEE 754 format.', + args: [ + { + name: 'hex', + description: 'The hexadecimal string to parse', + type: 'string' + } + ], + return: { + description: 'The parsed double', + type: 'number' + } + }, + parseHexToDouble: { + meta: 'function', + description: 'Parses a hexadecimal string to a double using IEEE 754 format, optionally specifying endianness.', + args: [ + { + name: 'value', + description: 'The hexadecimal string to parse', + type: 'string' + }, + { + name: 'bigEndian', + description: 'Whether to interpret the string in big-endian order (defaults to true)', + type: 'boolean', + optional: true + } + ], + return: { + description: 'The parsed double', + type: 'number' + } + }, + parseBytesToDouble: { + meta: 'function', + description: 'Parses a list or array of bytes to a double using IEEE 754 format.', + args: [ + { + name: 'data', + description: 'The bytes to parse', + type: 'array' + }, + { + name: 'offset', + description: 'The starting index in the byte array', + type: 'number', + optional: true + }, + { + name: 'length', + description: 'The number of bytes to parse (max 8)', + type: 'number', + optional: true + }, + { + name: 'bigEndian', + description: 'Whether to interpret bytes in big-endian order (defaults to true)', + type: 'boolean', + optional: true + } + ], + return: { + description: 'The parsed double', + type: 'number' + } + }, + parseBytesLongToDouble: { + meta: 'function', + description: 'Parses a list or array of bytes to a double by first interpreting them as a long integer.', + args: [ + { + name: 'data', + description: 'The bytes to parse', + type: 'array' + }, + { + name: 'offset', + description: 'The starting index in the byte array', + type: 'number', + optional: true + }, + { + name: 'length', + description: 'The number of bytes to parse (max 8)', + type: 'number', + optional: true + }, + { + name: 'bigEndian', + description: 'Whether to interpret bytes in big-endian order (defaults to true)', + type: 'boolean', + optional: true + } + ], + return: { + description: 'The parsed double', + type: 'number' + } + }, + toFixed: { + meta: 'function', + description: 'Rounds a number to a specified number of decimal places.', + args: [ + { + name: 'value', + description: 'The number to round', + type: 'number' + }, + { + name: 'precision', + description: 'The number of decimal places', + type: 'number' + } + ], + return: { + description: 'The rounded number', + type: 'number' + } + }, + toInt: { + meta: 'function', + description: 'Converts a double to an integer by rounding.', + args: [ + { + name: 'value', + description: 'The double to convert', + type: 'number' + } + ], + return: { + description: 'The rounded integer', + type: 'number' + } + }, + hexToBytes: { + meta: 'function', + description: 'Converts a hexadecimal string to a list of bytes.', + args: [ + { + name: 'value', + description: 'The hexadecimal string to convert', + type: 'string' + } + ], + return: { + description: 'The list of bytes', + type: 'array' + } + }, + hexToBytesArray: { + meta: 'function', + description: 'Converts a hexadecimal string to an array of bytes.', + args: [ + { + name: 'value', + description: 'The hexadecimal string to convert', + type: 'string' + } + ], + return: { + description: 'The array of bytes', + type: 'array' + } + }, + intToHex: { + meta: 'function', + description: 'Converts an integer to a hexadecimal string.', + args: [ + { + name: 'i', + description: 'The integer to convert', + type: 'number' + }, + { + name: 'bigEndian', + description: 'Whether to use big-endian order (defaults to true)', + type: 'boolean', + optional: true + }, + { + name: 'pref', + description: 'Whether to prefix with "0x" (defaults to false)', + type: 'boolean', + optional: true + }, + { + name: 'len', + description: 'The desired length of the hex string (defaults to minimum required)', + type: 'number', + optional: true + } + ], + return: { + description: 'The hexadecimal string', + type: 'string' + } + }, + longToHex: { + meta: 'function', + description: 'Converts a long integer to a hexadecimal string.', + args: [ + { + name: 'l', + description: 'The long integer to convert', + type: 'number' + }, + { + name: 'bigEndian', + description: 'Whether to use big-endian order (defaults to true)', + type: 'boolean', + optional: true + }, + { + name: 'pref', + description: 'Whether to prefix with "0x" (defaults to false)', + type: 'boolean', + optional: true + }, + { + name: 'len', + description: 'The desired length of the hex string (defaults to minimum required)', + type: 'number', + optional: true + } + ], + return: { + description: 'The hexadecimal string', + type: 'string' + } + }, + intLongToRadixString: { + meta: 'function', + description: 'Converts a long integer to a string in the specified radix.', + args: [ + { + name: 'number', + description: 'The number to convert', + type: 'number' + }, + { + name: 'radix', + description: 'The radix for conversion (e.g., 2, 8, 10, 16, defaults to 10)', + type: 'number', + optional: true + }, + { + name: 'bigEndian', + description: 'Whether to use big-endian order for hex (defaults to true)', + type: 'boolean', + optional: true + }, + { + name: 'pref', + description: 'Whether to prefix hex with "0x" (defaults to false)', + type: 'boolean', + optional: true + } + ], + return: { + description: 'The string representation in the specified radix', + type: 'string' + } + }, + floatToHex: { + meta: 'function', + description: 'Converts a float to its IEEE 754 hexadecimal representation.', + args: [ + { + name: 'f', + description: 'The float to convert', + type: 'number' + }, + { + name: 'bigEndian', + description: 'Whether to use big-endian order (defaults to true)', + type: 'boolean', + optional: true + } + ], + return: { + description: 'The hexadecimal string (e.g., "0x41200000" for 10.0)', + type: 'string' + } + }, + doubleToHex: { + meta: 'function', + description: 'Converts a double to its IEEE 754 hexadecimal representation.', + args: [ + { + name: 'd', + description: 'The double to convert', + type: 'number' + }, + { + name: 'bigEndian', + description: 'Whether to use big-endian order (defaults to true)', + type: 'boolean', + optional: true + } + ], + return: { + description: 'The hexadecimal string (e.g., "0x4024000000000000" for 10.0)', + type: 'string' + } + }, + printUnsignedBytes: { + meta: 'function', + description: 'Converts a list of signed bytes to a list of unsigned integer values.', + args: [ + { + name: 'byteArray', + description: 'The list of bytes to convert', + type: 'array' + } + ], + return: { + description: 'The list of unsigned integers (0-255)', + type: 'array' + } + }, + base64ToHex: { + meta: 'function', + description: 'Converts a Base64 string to a hexadecimal string.', + args: [ + { + name: 'base64', + description: 'The Base64 string to convert', + type: 'string' + } + ], + return: { + description: 'The hexadecimal string', + type: 'string' + } + }, + hexToBase64: { + meta: 'function', + description: 'Converts a hexadecimal string to a Base64 string.', + args: [ + { + name: 'hex', + description: 'The hexadecimal string to convert', + type: 'string' + } + ], + return: { + description: 'The Base64 string', + type: 'string' + } + }, + base64ToBytes: { + meta: 'function', + description: 'Converts a Base64 string to an array of bytes.', + args: [ + { + name: 'input', + description: 'The Base64 string to convert', + type: 'string' + } + ], + return: { + description: 'The array of bytes', + type: 'array' + } + }, + base64ToBytesList: { + meta: 'function', + description: 'Converts a Base64 string to a list of bytes.', + args: [ + { + name: 'input', + description: 'The Base64 string to convert', + type: 'string' + } + ], + return: { + description: 'The list of bytes', + type: 'array' + } + }, + bytesToBase64: { + meta: 'function', + description: 'Converts an array of bytes to a Base64 string.', + args: [ + { + name: 'bytes', + description: 'The array of bytes to convert', + type: 'array' + } + ], + return: { + description: 'The Base64 string', + type: 'string' + } + }, + bytesToHex: { + meta: 'function', + description: 'Converts an array or list of bytes to a hexadecimal string.', + args: [ + { + name: 'bytes', + description: 'The bytes to convert', + type: 'array' + } + ], + return: { + description: 'The hexadecimal string', + type: 'string' + } + }, + toFlatMap: { + meta: 'function', + description: 'Converts a nested map to a flat map, with options for key paths and exclusions.', + args: [ + { + name: 'json', + description: 'The nested map to flatten', + type: 'object' + }, + { + name: 'excludeList', + description: 'List of keys to exclude from flattening', + type: 'array', + optional: true + }, + { + name: 'pathInKey', + description: 'Whether to include full path in keys (defaults to true)', + type: 'boolean', + optional: true + } + ], + return: { + description: 'The flattened map', + type: 'object' + } + }, + encodeURI: { + meta: 'function', + description: 'Encodes a URI string, preserving certain characters as per MDN standards.', + args: [ + { + name: 'uri', + description: 'The URI string to encode', + type: 'string' + } + ], + return: { + description: 'The encoded URI string', + type: 'string' + } + }, + decodeURI: { + meta: 'function', + description: 'Decodes a URI string previously encoded with encodeURI.', + args: [ + { + name: 'uri', + description: 'The URI string to decode', + type: 'string' + } + ], + return: { + description: 'The decoded URI string', + type: 'string' + } + }, + raiseError: { + meta: 'function', + description: 'Throws an error with a custom message.', + args: [ + { + name: 'message', + description: 'The error message to throw', + type: 'string' + } + ], + return: { + description: 'Does not return; throws an exception', + type: 'void' + } + }, + isBinary: { + meta: 'function', + description: 'Checks if a string is a binary number.', + args: [ + { + name: 'str', + description: 'The string to check', + type: 'string' + } + ], + return: { + description: '2 if the string is binary, -1 otherwise', + type: 'number' + } + }, + isOctal: { + meta: 'function', + description: 'Checks if a string is an octal number.', + args: [ + { + name: 'str', + description: 'The string to check', + type: 'string' + } + ], + return: { + description: '8 if the string is octal, -1 otherwise', + type: 'number' + } + }, + isDecimal: { + meta: 'function', + description: 'Checks if a string is a decimal number.', + args: [ + { + name: 'str', + description: 'The string to check', + type: 'string' + } + ], + return: { + description: '10 if the string is decimal, -1 otherwise', + type: 'number' + } + }, + isHexadecimal: { + meta: 'function', + description: 'Checks if a string is a hexadecimal number.', + args: [ + { + name: 'str', + description: 'The string to check', + type: 'string' + } + ], + return: { + description: '16 if the string is hexadecimal, -1 otherwise', + type: 'number' + } + }, + bytesToExecutionArrayList: { + meta: 'function', + description: 'Converts an array of bytes to an execution array list.', + args: [ + { + name: 'byteArray', + description: 'The array of bytes to convert', + type: 'array' + } + ], + return: { + description: 'The execution array list of bytes', + type: 'array' + } + }, + padStart: { + meta: 'function', + description: 'Pads the start of a string with a character until it reaches the target length.', + args: [ + { + name: 'str', + description: 'The string to pad', + type: 'string' + }, + { + name: 'targetLength', + description: 'The desired length of the resulting string', + type: 'number' + }, + { + name: 'padString', + description: 'The character to pad with (single character)', + type: 'string' + } + ], + return: { + description: 'The padded string', + type: 'string' + } + }, + padEnd: { + meta: 'function', + description: 'Pads the end of a string with a character until it reaches the target length.', + args: [ + { + name: 'str', + description: 'The string to pad', + type: 'string' + }, + { + name: 'targetLength', + description: 'The desired length of the resulting string', + type: 'number' + }, + { + name: 'padString', + description: 'The character to pad with (single character)', + type: 'string' + } + ], + return: { + description: 'The padded string', + type: 'string' + } + }, + parseByteToBinaryArray: { + meta: 'function', + description: 'Converts a byte to a binary array.', + args: [ + { + name: 'byteValue', + description: 'The byte value to convert', + type: 'number' + }, + { + name: 'binLength', + description: 'The length of the binary array (defaults to 8)', + type: 'number', + optional: true + }, + { + name: 'bigEndian', + description: 'Whether to use big-endian order (defaults to true)', + type: 'boolean', + optional: true + } + ], + return: { + description: 'The binary array (array of 0s and 1s)', + type: 'array' + } + }, + parseBytesToBinaryArray: { + meta: 'function', + description: 'Converts a list or array of bytes to a binary array.', + args: [ + { + name: 'value', + description: 'The bytes to convert', + type: 'array' + }, + { + name: 'binLength', + description: 'The total length of the binary array (defaults to bytes.length * 8)', + type: 'number', + optional: true + } + ], + return: { + description: 'The binary array (array of 0s and 1s)', + type: 'array' + } + }, + parseLongToBinaryArray: { + meta: 'function', + description: 'Converts a long integer to a binary array.', + args: [ + { + name: 'longValue', + description: 'The long integer to convert', + type: 'number' + }, + { + name: 'binLength', + description: 'The length of the binary array (defaults to 64)', + type: 'number', + optional: true + } + ], + return: { + description: 'The binary array (array of 0s and 1s)', + type: 'array' + } + }, + parseBinaryArrayToInt: { + meta: 'function', + description: 'Converts a binary array to an integer.', + args: [ + { + name: 'value', + description: 'The binary array to convert (array of 0s and 1s)', + type: 'array' + }, + { + name: 'offset', + description: 'The starting index in the array (defaults to 0)', + type: 'number', + optional: true + }, + { + name: 'length', + description: 'The number of bits to parse (defaults to array length)', + type: 'number', + optional: true + } + ], + return: { + description: 'The parsed integer', + type: 'number' + } + } +}); + +const tbelUtilsFuncNames = [ + "btoa", + "atob", + "bytesToString", + "decodeToString", + "decodeToJson", + "stringToBytes", + "parseInt", + "parseLong", + "parseFloat", + "parseHexIntLongToFloat", + "parseDouble", + "parseLittleEndianHexToInt", + "parseBigEndianHexToInt", + "parseHexToInt", + "parseBytesToInt", + "parseLittleEndianHexToLong", + "parseBigEndianHexToLong", + "parseHexToLong", + "parseBytesToLong", + "parseLittleEndianHexToFloat", + "parseBigEndianHexToFloat", + "parseHexToFloat", + "parseBytesToFloat", + "parseBytesIntToFloat", + "parseLittleEndianHexToDouble", + "parseBigEndianHexToDouble", + "parseHexToDouble", + "parseBytesToDouble", + "parseBytesLongToDouble", + "toFixed", + "toInt", + "hexToBytes", + "hexToBytesArray", + "intToHex", + "longToHex", + "intLongToRadixString", + "floatToHex", + "doubleToHex", + "printUnsignedBytes", + "base64ToHex", + "hexToBase64", + "base64ToBytes", + "base64ToBytesList", + "bytesToBase64", + "bytesToHex", + "toFlatMap", + "encodeURI", + "decodeURI", + "raiseError", + "isBinary", + "isOctal", + "isDecimal", + "isHexadecimal", + "bytesToExecutionArrayList", + "padStart", + "padEnd", + "parseByteToBinaryArray", + "parseBytesToBinaryArray", + "parseLongToBinaryArray", + "parseBinaryArrayToInt" +]; + +export const tbelUtilsFuncHighlightRules: Array = + tbelUtilsFuncNames.map(funcName => ({ + token: 'tb.tbel-utils-func', + regex: `\\b${funcName}\\b`, + next: 'no_regex' + })); From 4b2eac542942d20596ca13549497ed5c9bb83a95 Mon Sep 17 00:00:00 2001 From: Dmytro Skarzhynets Date: Fri, 7 Mar 2025 15:20:54 +0200 Subject: [PATCH 243/345] Save strategies: update node details --- .../rule/engine/telemetry/TbMsgAttributesNode.java | 8 ++++++-- .../rule/engine/telemetry/TbMsgTimeseriesNode.java | 4 ++-- 2 files changed, 8 insertions(+), 4 deletions(-) diff --git a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/telemetry/TbMsgAttributesNode.java b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/telemetry/TbMsgAttributesNode.java index 909fca51d4..c04f5b474d 100644 --- a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/telemetry/TbMsgAttributesNode.java +++ b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/telemetry/TbMsgAttributesNode.java @@ -67,10 +67,11 @@ import static org.thingsboard.server.common.data.msg.TbMsgType.POST_ATTRIBUTES_R Saves attribute data with a configurable scope and according to configured processing strategies. """, nodeDetails = """ - Node performs two actions: + Node performs three actions:
    • Attributes: save attribute data to a database.
    • WebSockets: notify WebSockets subscriptions about attribute data updates.
    • +
    • Calculated fields: notify calculated fields about attribute data updates.
    For each action, three processing strategies are available: @@ -86,12 +87,15 @@ import static org.thingsboard.server.common.data.msg.TbMsgType.POST_ATTRIBUTES_R
    • On every message: applies the "On every message" strategy to all actions.
    • Deduplicate: applies the "Deduplicate" strategy (with a specified interval) to all actions.
    • -
    • WebSockets only: applies the "Skip" strategy to Attributes, and the "On every message" strategy to WebSockets.
    • +
    • WebSockets only: for all actions except WebSocket notifications, the "Skip" strategy is applied, while WebSocket notifications use the "On every message" strategy.
  • Advanced: configure each action’s strategy independently.
  • + The node supports three attribute scopes: Client attributes, Shared attributes, and Server attributes. + You can set the default scope in the node configuration, or override it by specifying a valid scope property in the message metadata. +

    Additionally:
    • If Save attributes only if the value changes is enabled, the rule node compares the received attribute value with the current stored value and skips the save operation if they match.
    • diff --git a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/telemetry/TbMsgTimeseriesNode.java b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/telemetry/TbMsgTimeseriesNode.java index 31cfd5c898..8017be9a04 100644 --- a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/telemetry/TbMsgTimeseriesNode.java +++ b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/telemetry/TbMsgTimeseriesNode.java @@ -61,7 +61,7 @@ import static org.thingsboard.server.common.data.msg.TbMsgType.POST_TELEMETRY_RE Saves time series data with a configurable TTL and according to configured processing strategies. """, nodeDetails = """ - Node performs three actions: + Node performs four actions:
      • Time series: save time series data to a ts_kv table in a DB.
      • Latest values: save time series data to a ts_kv_latest table in a DB.
      • @@ -82,7 +82,7 @@ import static org.thingsboard.server.common.data.msg.TbMsgType.POST_TELEMETRY_RE
        • On every message: applies the "On every message" strategy to all actions.
        • Deduplicate: applies the "Deduplicate" strategy (with a specified interval) to all actions.
        • -
        • WebSockets only: applies the "Skip" strategy to Time series and Latest values, and the "On every message" strategy to WebSockets.
        • +
        • WebSockets only: for all actions except WebSocket notifications, the "Skip" strategy is applied, while WebSocket notifications use the "On every message" strategy.
      • Advanced: configure each action’s strategy independently.
      • From c55f9592f60543218b1f9c9eb07bde2832a5d57a Mon Sep 17 00:00:00 2001 From: mpetrov Date: Fri, 7 Mar 2025 15:33:19 +0200 Subject: [PATCH 244/345] Fixed for rulechains --- .../app/shared/components/js-func.component.ts | 17 ++++++++++------- 1 file changed, 10 insertions(+), 7 deletions(-) diff --git a/ui-ngx/src/app/shared/components/js-func.component.ts b/ui-ngx/src/app/shared/components/js-func.component.ts index 682cc872e6..9859fa543e 100644 --- a/ui-ngx/src/app/shared/components/js-func.component.ts +++ b/ui-ngx/src/app/shared/components/js-func.component.ts @@ -241,6 +241,7 @@ export class JsFuncComponent implements OnInit, OnChanges, OnDestroy, ControlVal this.updateJsWorkerGlobals(); this.initialCompleters = this.jsEditor.completers || []; this.updateCompleters(); + this.updateHighlightRules(); this.editorResize$ = new ResizeObserver(() => { this.onAceEditorResize(); }); @@ -577,22 +578,24 @@ export class JsFuncComponent implements OnInit, OnChanges, OnDestroy, ControlVal private updateHighlightRules(): void { // @ts-ignore - if (!!this.highlightRules && !!this.jsEditor.session.$mode) { + if (!!this.jsEditor.session.$mode) { // @ts-ignore const newMode = new this.jsEditor.session.$mode.constructor(); newMode.$highlightRules = new newMode.HighlightRules(); - for(const group in this.highlightRules) { - if(!!newMode.$highlightRules.$rules[group]) { - newMode.$highlightRules.$rules[group].unshift(...this.highlightRules[group]); - } else { - newMode.$highlightRules.$rules[group] = this.highlightRules[group]; + if (!!this.highlightRules) { + for(const group in this.highlightRules) { + if(!!newMode.$highlightRules.$rules[group]) { + newMode.$highlightRules.$rules[group].unshift(...this.highlightRules[group]); + } else { + newMode.$highlightRules.$rules[group] = this.highlightRules[group]; + } } } if (this.scriptLanguage === ScriptLanguage.TBEL) { newMode.$highlightRules.$rules.start = [...tbelUtilsFuncHighlightRules, ...newMode.$highlightRules.$rules.start]; } const identifierRule = newMode.$highlightRules.$rules.no_regex.find(rule => rule.token?.includes('identifier')); - if (identifierRule) { + if (identifierRule && identifierRule.next === 'no_regex') { identifierRule.next = 'start'; } // @ts-ignore From 5b1731b0d8eac6e0fdc45b6e1b3c40f115dd00c5 Mon Sep 17 00:00:00 2001 From: mpetrov Date: Fri, 7 Mar 2025 15:41:18 +0200 Subject: [PATCH 245/345] license header --- .../app/shared/models/ace/tbel-utils.models.ts | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/ui-ngx/src/app/shared/models/ace/tbel-utils.models.ts b/ui-ngx/src/app/shared/models/ace/tbel-utils.models.ts index 24b6d9eaaf..a595428e63 100644 --- a/ui-ngx/src/app/shared/models/ace/tbel-utils.models.ts +++ b/ui-ngx/src/app/shared/models/ace/tbel-utils.models.ts @@ -1,3 +1,19 @@ +/// +/// 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 { AceHighlightRule } from '@shared/models/ace/ace.models'; import { TbEditorCompleter } from '@shared/models/ace/completion.models'; From 9215231ba6eb8c6e1108419bc7a07e9ecdb419dc Mon Sep 17 00:00:00 2001 From: mpetrov Date: Fri, 7 Mar 2025 15:54:45 +0200 Subject: [PATCH 246/345] refactoring --- ui-ngx/src/app/shared/components/js-func.component.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/ui-ngx/src/app/shared/components/js-func.component.ts b/ui-ngx/src/app/shared/components/js-func.component.ts index 9859fa543e..f9674c550a 100644 --- a/ui-ngx/src/app/shared/components/js-func.component.ts +++ b/ui-ngx/src/app/shared/components/js-func.component.ts @@ -241,7 +241,6 @@ export class JsFuncComponent implements OnInit, OnChanges, OnDestroy, ControlVal this.updateJsWorkerGlobals(); this.initialCompleters = this.jsEditor.completers || []; this.updateCompleters(); - this.updateHighlightRules(); this.editorResize$ = new ResizeObserver(() => { this.onAceEditorResize(); }); From 901155cf56bc3bd6179d3caa8004a6c408291654 Mon Sep 17 00:00:00 2001 From: mpetrov Date: Fri, 7 Mar 2025 16:01:47 +0200 Subject: [PATCH 247/345] Updated version --- application/src/main/resources/thingsboard.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/application/src/main/resources/thingsboard.yml b/application/src/main/resources/thingsboard.yml index 67e5c008cd..588d796c9e 100644 --- a/application/src/main/resources/thingsboard.yml +++ b/application/src/main/resources/thingsboard.yml @@ -1284,7 +1284,7 @@ transport: # URL of gateways dashboard repository repository_url: "${TB_GATEWAY_DASHBOARD_SYNC_REPOSITORY_URL:https://github.com/thingsboard/gateway-management-extensions-dist.git}" # Branch of gateways dashboard repository to work with - branch: "${TB_GATEWAY_DASHBOARD_SYNC_BRANCH:release/4.0}" + branch: "${TB_GATEWAY_DASHBOARD_SYNC_BRANCH:release/4.0.0}" # Fetch frequency in hours for gateways dashboard repository fetch_frequency: "${TB_GATEWAY_DASHBOARD_SYNC_FETCH_FREQUENCY:24}" From 83a83c7ae552618caf2acd6dfa0afa3d51d3094c Mon Sep 17 00:00:00 2001 From: IrynaMatveieva Date: Fri, 7 Mar 2025 17:53:27 +0200 Subject: [PATCH 248/345] updated page --- .../en_US/calculated-field/expression_fn.md | 198 +++++++++++++++--- 1 file changed, 171 insertions(+), 27 deletions(-) diff --git a/ui-ngx/src/assets/help/en_US/calculated-field/expression_fn.md b/ui-ngx/src/assets/help/en_US/calculated-field/expression_fn.md index eb06cda0cb..2e764d7c16 100644 --- a/ui-ngx/src/assets/help/en_US/calculated-field/expression_fn.md +++ b/ui-ngx/src/assets/help/en_US/calculated-field/expression_fn.md @@ -1,25 +1,31 @@ #### Calculated field TBEL script function -The **calculate()** function is user-defined and receives arguments configured by the user to generate new telemetry data. -It allows you to perform custom calculations using [TBEL{:target="_blank"}](${siteBaseUrl}/docs${docPlatformPrefix}/user-guide/tbel/). +The **calculate()** function is user-defined script that takes values of arguments configured in the calculated field setup and an additional `ctx` object containing all arguments. + +The **calculate()** function is a user-defined script that allows you to perform custom calculations using [TBEL{:target="_blank"}](${siteBaseUrl}/docs${docPlatformPrefix}/user-guide/tbel/) on telemetry and attribute data. +It receives user-configured arguments and an additional `ctx` object, which provides access to all arguments. ##### Function signature ```javascript - function calculate(arg1, arg2, ...): object | object[] + function calculate(ctx, arg1, arg2, ...): object | object[] ``` -The function automatically receives user-defined arguments **(arg1, arg2, ...)** and should return a JSON object. +* the function automatically receives a `ctx` object containing all arguments. +* user-defined arguments `(arg1, arg2, ...)` are passed based on the calculated field configuration. +* the function must return a JSON object (single result) or an array of objects (multiple telemetry entries). ##### Example: Air Density Calculation +This example demonstrates how to calculate air density using altitude and temperature telemetry data. + ```javascript -function calculate(altitude, temperature) { +function calculate(ctx, altitude, temperature) { var avgTemperature = temperature.mean(); // Get average temperature var temperatureK = (avgTemperature - 32) * (5 / 9) + 273.15; // Convert Fahrenheit to Kelvin // Estimate air pressure based on altitude - var pressure = 101325 * Math.pow((1 - 2.25577e-5 * altitude.value), 5.25588); + var pressure = 101325 * Math.pow((1 - 2.25577e-5 * altitude), 5.25588); // Air density formula var airDensity = pressure / (287.05 * temperatureK); @@ -32,7 +38,21 @@ function calculate(altitude, temperature) { ##### Function arguments -The function receives user-configured arguments, which can be of two types: +* `ctx` - context object containing all predefined arguments. + + Use `ctx.args.argName` to access arguments. + + ```javascript + var altitude = ctx.args.altitude.ts; + var temperature = ctx.args.temperature; + ``` + +* `arg1, arg2, ...` - user-defined arguments configured in the calculated field setup. These arguments follow the previously described types: + * if an argument represents the latest telemetry data or attribute, it will be passed as a direct value. + + * if an argument is a time series rolling argument, it will be provided as a time series rolling argument described above. + +The **calculate()** function receives predefined arguments, which fall into two categories: * single value arguments - represent the latest telemetry data or attribute. ```json @@ -44,11 +64,11 @@ The function receives user-configured arguments, which can be of two types: } ``` - Use dot notation (`.`) to access argument properties. +Use dot notation (`.`) to access argument properties. ```javascript - var altitudeTimestamp = altitude.ts; - var altitudeValue = altitude.value; + var altitudeTimestamp = ctx.args.altitude.ts; + var altitudeValue = ctx.args.altitude.value; ``` * time series rolling arguments - contain historical data within a defined time window. @@ -57,8 +77,7 @@ The function receives user-configured arguments, which can be of two types: "temperature": { "timeWindow": { "startTs": 1740643762896, - "endTs": 1740644662896, - "limit": 5000 + "endTs": 1740644662896 }, "values": [ { "ts": 1740644355935, "value": 72.32 }, @@ -69,8 +88,8 @@ The function receives user-configured arguments, which can be of two types: } } ``` - - Use dot notation (`.`) to access argument properties. + + Use dot notation (`.`) to access argument properties. ```javascript var startOfInterval = temperature.timeWindow.startTs; @@ -81,27 +100,27 @@ The function receives user-configured arguments, which can be of two types: **Built-in methods for rolling arguments** Time series rolling arguments provide built-in functions for calculations. -These functions accept an optional 'ignoreNaN' boolean parameter, which controls how NaN values are handled. -Each method has two function signatures: +These functions accept an optional `ignoreNaN` boolean parameter, which controls how NaN values are handled. +Each function has two function signatures: * **Without parameters:** `method()` → called **without parameters** and defaults to `ignoreNaN = true`, meaning NaN values are ignored. * **With an explicit parameter:** `method(boolean ignoreNaN)` → called with a boolean `ignoreNaN` parameter: * `true` → ignores NaN values (default behavior). * `false` → includes NaN values in calculations. -| Method | Default Behavior (`ignoreNaN = true`) | Alternative (`ignoreNaN = false`) | -|-----------|--------------------------------------------------|---------------------------------------------| -| `max()` | Returns the highest value, ignoring NaN values. | Returns NaN if any NaN values exist. | -| `min()` | Returns the lowest value, ignoring NaN values. | Returns NaN if any NaN values exist. | -| `mean()` | Computes the average value, ignoring NaN values. | Returns NaN if any NaN values exist. | -| `std()` | Calculates the standard deviation, ignoring NaN. | Returns NaN if any NaN values exist. | +| Method | Default Behavior (`ignoreNaN = true`) | Alternative (`ignoreNaN = false`) | +|------------|--------------------------------------------------|---------------------------------------------| +| `max()` | Returns the highest value, ignoring NaN values. | Returns NaN if any NaN values exist. | +| `min()` | Returns the lowest value, ignoring NaN values. | Returns NaN if any NaN values exist. | +| `mean()` | Computes the average value, ignoring NaN values. | Returns NaN if any NaN values exist. | +| `std()` | Calculates the standard deviation, ignoring NaN. | Returns NaN if any NaN values exist. | | `median()` | Returns the median value, ignoring NaN values. | Returns NaN if any NaN values exist. | -| `count()` | Counts only valid (non-NaN) values. | Counts all values, including NaN. | -| `last()` | Returns the most recent non-NaN value. | Returns the last value, even if it is NaN. | -| `first()` | Returns the oldest non-NaN value. | Returns the first value, even if it is NaN. | -| `sum()` | Computes the total sum, ignoring NaN values. | Returns NaN if any NaN values exist. | +| `count()` | Counts only valid (non-NaN) values. | Counts all values, including NaN. | +| `last()` | Returns the most recent non-NaN value. | Returns the last value, even if it is NaN. | +| `first()` | Returns the oldest non-NaN value. | Returns the first value, even if it is NaN. | +| `sum()` | Computes the total sum, ignoring NaN values. | Returns NaN if any NaN values exist. | -##### The following calculations are executed over the provided above arguments: +The following calculations are executed over the provided above arguments: **Example usage: default (`ignoreNaN = true`)** @@ -139,6 +158,131 @@ var valueCount = temperature.count(false); // Counts all values, including NaN } ``` +Time series rolling arguments can be merged for multi-sensor analysis. + +Function Signatures: + +* `merge(other, settings)` - merges the current rolling argument with another rolling argument by aligning timestamps and combining values. + Parameters: + * `other` - another time series rolling argument to merge with. + * `settings` (optional) - configuration object, supports: + * `ignoreNaN` (boolean, default true) - controls whether NaN values should be ignored. + * `timeWindow` (object, default {}) - defines a custom time window for filtering merged values. + Returns: an object with time window and values from each provided time series rolling arguments. + + * `mergeAll(others, settings)` - merges the current rolling argument with multiple rolling arguments by aligning timestamps and combining values. + Parameters: + * `others` - an array of time series rolling arguments to merge with. + * `settings` (optional) - same as `merge()`. + Returns: an object with timeWindow and aligned values. + +```json +{ + "humidity": { + "timeWindow": { + "startTs": 1741356332086, + "endTs": 1741357232086 + }, + "values": [{ + "ts": 1741356882759, + "value": 43 + }, { + "ts": 1741356918779, + "value": 46 + }] + }, + "pressure": { + "timeWindow": { + "startTs": 1741356332086, + "endTs": 1741357232086 + }, + "values": [{ + "ts": 1741357047945, + "value": 1023 + }, { + "ts": 1741357056144, + "value": 1026 + }, { + "ts": 1741357147391, + "value": 1025 + }] + }, + "temperature": { + "timeWindow": { + "startTs": 1741356332086, + "endTs": 1741357232086 + }, + "values": [{ + "ts": 1741356874943, + "value": 76 + }, { + "ts": 1741357063689, + "value": 77 + }] + } +} +``` + +**Example usage** +```javascript +var mergedData = temperature.merge(humidity, { ignoreNaN: false }); +``` + +**Output:** +```json +{ + "mergedData": { + "timeWindow": { + "startTs": 1741356332086, + "endTs": 1741357232086 + }, + "values": [{ + "ts": 1741356874943, + "values": [76.0, "NaN"] + }, { + "ts": 1741356882759, + "values": [76.0, 43.0] + }, { + "ts": 1741356918779, + "values": [76.0, 46.0] + }, { + "ts": 1741357063689, + "values": [77.0, 46.0] + }] + } +} +``` + +**Example usage** +```javascript +var mergedData = temperature.mergeAll([humidity, pressure], { ignoreNaN: true }); +``` + +**Output:** +```json +{ + "mergedData": { + "timeWindow": { + "startTs": 1741356332086, + "endTs": 1741357232086 + }, + "values": [{ + "ts": 1741357047945, + "values": [76.0, 46.0, 1023.0] + }, { + "ts": 1741357056144, + "values": [76.0, 46.0, 1026.0] + }, { + "ts": 1741357063689, + "values": [77.0, 46.0, 1026.0] + }, { + "ts": 1741357147391, + "values": [77.0, 46.0, 1025.0] + }] + } +} +``` + ##### Function return format: The script should return a JSON object formatted according to the [ThingsBoard Telemetry Upload API](${siteBaseUrl}/docs${docPlatformPrefix}/user-guide/telemetry/#time-series-data-upload-api/). From 0e3324ff311ca89f48003528c34c4700c6574c78 Mon Sep 17 00:00:00 2001 From: Igor Kulikov Date: Fri, 7 Mar 2025 18:14:22 +0200 Subject: [PATCH 249/345] UI: Map - improve marker shape/icons. --- .../lib/maps/data-layer/markers-data-layer.ts | 5 +- .../lib/maps/data-layer/trips-data-layer.ts | 2 +- .../home/components/widget/lib/maps/map.ts | 1 + .../map/marker-icon-shapes.component.ts | 2 +- .../map/marker-shape-settings.component.ts | 2 +- .../models/widget/maps/marker-shape.models.ts | 156 +++++++++++++++--- ui-ngx/src/assets/markers/iconContainer1.svg | 21 ++- ui-ngx/src/assets/markers/iconContainer2.svg | 26 +++ ui-ngx/src/assets/markers/iconContainer3.svg | 16 ++ ui-ngx/src/assets/markers/shape1.svg | 23 +-- ui-ngx/src/assets/markers/shape10.svg | 10 +- ui-ngx/src/assets/markers/shape2.svg | 24 ++- ui-ngx/src/assets/markers/shape3.svg | 27 +-- ui-ngx/src/assets/markers/shape4.svg | 31 ++-- ui-ngx/src/assets/markers/shape5.svg | 29 ++-- ui-ngx/src/assets/markers/shape6.svg | 17 +- ui-ngx/src/assets/markers/shape7.svg | 12 +- ui-ngx/src/assets/markers/shape8.svg | 10 +- ui-ngx/src/assets/markers/shape9.svg | 14 +- .../src/assets/markers/tripIconContainer1.svg | 25 ++- .../src/assets/markers/tripIconContainer2.svg | 24 +++ .../src/assets/markers/tripIconContainer3.svg | 28 ++++ ui-ngx/src/assets/markers/tripShape10.svg | 9 +- ui-ngx/src/assets/markers/tripShape2.svg | 32 +++- ui-ngx/src/assets/markers/tripShape3.svg | 9 +- ui-ngx/src/assets/markers/tripShape4.svg | 9 +- ui-ngx/src/assets/markers/tripShape5.svg | 29 +++- ui-ngx/src/assets/markers/tripShape6.svg | 29 +++- ui-ngx/src/assets/markers/tripShape7.svg | 11 +- ui-ngx/src/assets/markers/tripShape8.svg | 9 +- ui-ngx/src/assets/markers/tripShape9.svg | 9 +- 31 files changed, 481 insertions(+), 170 deletions(-) create mode 100644 ui-ngx/src/assets/markers/iconContainer2.svg create mode 100644 ui-ngx/src/assets/markers/iconContainer3.svg create mode 100644 ui-ngx/src/assets/markers/tripIconContainer2.svg create mode 100644 ui-ngx/src/assets/markers/tripIconContainer3.svg 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 f835c0578b..b5dbedf90f 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 @@ -73,8 +73,7 @@ export class MarkerDataProcessor { @@ -171,7 +170,7 @@ export class MarkerDataProcessor { return createColorMarkerIconElement(this.dataLayer.getCtx().$injector.get(MatIconRegistry), this.dataLayer.getCtx().$injector.get(DomSanitizer), - iconContainer, icon, color, this.trip).pipe( + iconContainer, icon, color).pipe( map((element) => { if (rotationAngle !== 0) { element.style.transform = `rotate(${rotationAngle}deg)`; diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/maps/data-layer/trips-data-layer.ts b/ui-ngx/src/app/modules/home/components/widget/lib/maps/data-layer/trips-data-layer.ts index 72400b0f38..9e0a5fe4bf 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/maps/data-layer/trips-data-layer.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/maps/data-layer/trips-data-layer.ts @@ -553,7 +553,7 @@ export class TbTripsDataLayer extends TbMapDataLayer[] = [this.dataProcessor.setup()]; if (this.settings.showPath) { this.pathStrokeColorProcessor = new DataLayerColorProcessor(this, this.settings.pathStrokeColor); 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 c88d8072e3..66475705f7 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 @@ -171,6 +171,7 @@ export abstract class TbMap { } private setupControls(): Observable { + //L.control.scale().addTo(this.map); if (this.map.zoomControl) { this.map.zoomControl.setPosition(this.settings.controlsPosition); } diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/map/marker-icon-shapes.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/map/marker-icon-shapes.component.ts index b4cfa7ae00..3240ce11dd 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/map/marker-icon-shapes.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/map/marker-icon-shapes.component.ts @@ -115,7 +115,7 @@ export class MarkerIconShapesComponent extends PageComponent implements OnInit { this.iconContainers = containersList.map((iconContainer) => { return { iconContainer, - html$: createColorMarkerIconElement(this.iconRegistry, this.domSanitizer, iconContainer, this.icon, tinycolor(this.color), this.trip).pipe( + html$: createColorMarkerIconElement(this.iconRegistry, this.domSanitizer, iconContainer, this.icon, tinycolor(this.color)).pipe( map((element) => { return this.domSanitizer.bypassSecurityTrustHtml(element.outerHTML); }), diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/map/marker-shape-settings.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/map/marker-shape-settings.component.ts index ef6a85ac8c..14e80e4fd2 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/map/marker-shape-settings.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/map/marker-shape-settings.component.ts @@ -208,7 +208,7 @@ export class MarkerShapeSettingsComponent implements ControlValueAccessor, OnIni } else if (this.markerType === MarkerType.icon) { const iconContainer = (this.modelValue as MarkerIconSettings).iconContainer; const icon = (this.modelValue as MarkerIconSettings).icon; - this.iconPreview$ = createColorMarkerIconElement(this.iconRegistry, this.domSanitizer, iconContainer, icon, tinycolor(color), this.trip).pipe( + this.iconPreview$ = createColorMarkerIconElement(this.iconRegistry, this.domSanitizer, iconContainer, icon, tinycolor(color)).pipe( map((element) => { return this.domSanitizer.bypassSecurityTrustHtml(element.outerHTML); }), diff --git a/ui-ngx/src/app/shared/models/widget/maps/marker-shape.models.ts b/ui-ngx/src/app/shared/models/widget/maps/marker-shape.models.ts index 76a8af733f..3d8f44df3f 100644 --- a/ui-ngx/src/app/shared/models/widget/maps/marker-shape.models.ts +++ b/ui-ngx/src/app/shared/models/widget/maps/marker-shape.models.ts @@ -21,6 +21,7 @@ import { Observable, of, shareReplay, switchMap } from 'rxjs'; import { catchError, map, take } from 'rxjs/operators'; import { isSvgIcon, splitIconName } from '@shared/models/icon.models'; import { Element, G, SVG, Text } from '@svgdotjs/svg.js'; +import { guid } from '@core/utils'; export enum MarkerShape { markerShape1 = 'markerShape1', @@ -47,10 +48,14 @@ export enum MarkerShape { export enum MarkerIconContainer { iconContainer1 = 'iconContainer1', - tripIconContainer1 = 'tripIconContainer1' + iconContainer2 = 'iconContainer2', + iconContainer3 = 'iconContainer3', + tripIconContainer1 = 'tripIconContainer1', + tripIconContainer2 = 'tripIconContainer2', + tripIconContainer3 = 'tripIconContainer3' } -export const markerShapeMap = new Map( +const markerShapeMap = new Map( [ [MarkerShape.markerShape1, '/assets/markers/shape1.svg'], [MarkerShape.markerShape2, '/assets/markers/shape2.svg'], @@ -75,10 +80,69 @@ export const markerShapeMap = new Map( ] ); -export const markerIconContainerMap = new Map( +const markerIconContainerMap = new Map( [ [MarkerIconContainer.iconContainer1, '/assets/markers/iconContainer1.svg'], - [MarkerIconContainer.tripIconContainer1, '/assets/markers/tripIconContainer1.svg'] + [MarkerIconContainer.iconContainer2, '/assets/markers/iconContainer2.svg'], + [MarkerIconContainer.iconContainer3, '/assets/markers/iconContainer3.svg'], + [MarkerIconContainer.tripIconContainer1, '/assets/markers/tripIconContainer1.svg'], + [MarkerIconContainer.tripIconContainer2, '/assets/markers/tripIconContainer2.svg'], + [MarkerIconContainer.tripIconContainer3, '/assets/markers/tripIconContainer3.svg'] + ] +); + +interface MarkerIconContainerDefinition { + iconSize: number; + iconColor: (color: tinycolor.Instance) => tinycolor.Instance; + iconAlpha: (color: tinycolor.Instance) => number; + appendIcon?: (svgElement: SVGElement, iconElement: Element) => void; +} + +const emptyIconContainerDefinition: MarkerIconContainerDefinition = { + iconSize: 24, + iconColor: (color) => color, + iconAlpha: color => color.getAlpha() +} + +const defaultIconContainerDefinition: MarkerIconContainerDefinition = { + iconSize: 12, + iconColor: (color) => tinycolor.mix(color.clone().setAlpha(1), tinycolor('rgba(0,0,0,0.38)')), + iconAlpha: color => color.getAlpha() +} + +const defaultTripIconContainerDefinition: MarkerIconContainerDefinition = { + iconSize: 24, + iconColor: () => tinycolor('#000'), + iconAlpha: () => 1, + appendIcon: (svgElement, iconElement) => { + const box = iconElement.bbox(); + let elements = svgElement.getElementsByClassName('icon-mask-exclude'); + if (elements.length) { + elements = elements[0].getElementsByClassName('marker-icon-container'); + if (elements.length) { + const iconContainer = new G(elements[0] as SVGGElement); + iconContainer.add(iconElement.clone().fill('#000').translate(-box.cx, -box.cy)); + } + } + elements = svgElement.getElementsByClassName('icon-mask-overlay'); + if (elements.length) { + elements = elements[0].getElementsByClassName('marker-icon-container'); + if (elements.length) { + const iconContainer = new G(elements[0] as SVGGElement); + iconContainer.add(iconElement.clone().fill('#fff').translate(-box.cx, -box.cy)); + } + } + } +} + +const markerIconContainerDefinitionMap = new Map( + [ + [MarkerIconContainer.iconContainer1, defaultIconContainerDefinition], + [MarkerIconContainer.iconContainer2, defaultIconContainerDefinition], + [MarkerIconContainer.iconContainer3, defaultIconContainerDefinition], + [MarkerIconContainer.tripIconContainer1, defaultTripIconContainerDefinition], + [MarkerIconContainer.tripIconContainer2, {...defaultTripIconContainerDefinition, iconSize: 16}], + [MarkerIconContainer.tripIconContainer3, {...defaultTripIconContainerDefinition, iconSize: 16}] ] ); @@ -108,8 +172,45 @@ export const tripMarkerShapes = [ MarkerShape.tripMarkerShape10 ]; -export const markerIconContainers = [ MarkerIconContainer.iconContainer1 ]; -export const tripMarkerIconContainers = [ MarkerIconContainer.tripIconContainer1 ]; +export const markerIconContainers = [ + MarkerIconContainer.iconContainer1, + MarkerIconContainer.iconContainer2, + MarkerIconContainer.iconContainer3 +]; + +export const tripMarkerIconContainers = [ + MarkerIconContainer.tripIconContainer1, + MarkerIconContainer.tripIconContainer2, + MarkerIconContainer.tripIconContainer3 +]; + +const generateElementId = () => { + const id = guid(); + const firstChar = id.charAt(0); + if (firstChar >= '0' && firstChar <= '9') { + return 'a' + id; + } else { + return id; + } +}; + +const prepareSvgIds = (element: SVGElement): SVGElement => { + let svgContent = element.outerHTML; + const regexp = /\sid="([^"]*)"[\s>]/g; + const unique_id_suffix = '_' + generateElementId(); + const ids: string[] = []; + let match = regexp.exec(svgContent); + while (match !== null) { + ids.push(match[1]); + match = regexp.exec(svgContent); + } + for (const id of ids) { + const newId = id + unique_id_suffix; + svgContent = svgContent.replace(new RegExp('id="'+id+'"', 'g'), 'id="'+newId+'"'); + svgContent = svgContent.replace(new RegExp('url\\(#'+id+'\\)', 'g'), 'url(#'+newId+')'); + } + return SVG(svgContent).node; +}; const createColorMarkerShape = (iconRegistry: MatIconRegistry, domSanitizer: DomSanitizer, assetUrl: string, color: tinycolor.Instance): Observable => { const safeUrl = domSanitizer.bypassSecurityTrustResourceUrl(assetUrl); @@ -131,7 +232,14 @@ const createColorMarkerShape = (iconRegistry: MatIconRegistry, domSanitizer: Dom el.setAttribute('stroke', '#'+color.toHex()); el.setAttribute('stroke-opacity', `${color.getAlpha()}`); }); - return svgElement; + const opacityElements = Array.from(svgElement.getElementsByClassName('marker-opacity')); + if (svgElement.classList.contains('marker-opacity')) { + opacityElements.push(svgElement); + } + opacityElements.forEach(el => { + el.setAttribute('opacity', `${color.getAlpha()}`); + }); + return prepareSvgIds(svgElement); }) ); } @@ -147,10 +255,8 @@ export const createColorMarkerShapeURI = (iconRegistry: MatIconRegistry, domSani ); } -const createIconElement = (iconRegistry: MatIconRegistry, icon: string, size: number, color: tinycolor.Instance, trip = false): Observable => { +const createIconElement = (iconRegistry: MatIconRegistry, icon: string, size: number, iconColor: tinycolor.Instance, iconAlpha: number): Observable => { const isSvg = isSvgIcon(icon); - const iconAlpha = color.getAlpha(); - const iconColor = trip ? color : tinycolor.mix(color.clone().setAlpha(1), tinycolor('rgba(0,0,0,0.38)')); if (isSvg) { const [namespace, iconName] = splitIconName(icon); return iconRegistry @@ -190,24 +296,30 @@ const createIconElement = (iconRegistry: MatIconRegistry, icon: string, size: nu } export const createColorMarkerIconElement = (iconRegistry: MatIconRegistry, domSanitizer: DomSanitizer, - iconContainer: MarkerIconContainer, icon: string, color: tinycolor.Instance, - trip = false): Observable => { + iconContainer: MarkerIconContainer, icon: string, color: tinycolor.Instance): Observable => { const markerShape$: Observable = iconContainer ? createColorMarkerShape(iconRegistry, domSanitizer, markerIconContainerMap.get(iconContainer), color) : of(null); return markerShape$.pipe( switchMap((svgElement) => { - const iconSize = svgElement ? (trip ? 24 : 12) : 24; - const iconColor = svgElement ? (trip ? tinycolor('#fff') : color) : color; - return createIconElement(iconRegistry, icon, iconSize, iconColor, trip || !svgElement).pipe( + const definition = iconContainer ? markerIconContainerDefinitionMap.get(iconContainer) : emptyIconContainerDefinition; + const iconSize = definition.iconSize; + const iconColor = definition.iconColor(color); + const iconAlpha = definition.iconAlpha(color); + return createIconElement(iconRegistry, icon, iconSize, iconColor, iconAlpha).pipe( map((iconElement) => { if (svgElement) { - const elements = svgElement.getElementsByClassName('marker-icon-container'); - if (iconElement && elements.length) { - const iconContainer = new G(elements[0] as SVGGElement); - iconContainer.clear(); - iconContainer.add(iconElement); - const box = iconElement.bbox(); - iconElement.translate(-box.cx, -box.cy); + if (iconElement) { + if (definition.appendIcon) { + definition.appendIcon(svgElement, iconElement); + } else { + const elements = svgElement.getElementsByClassName('marker-icon-container'); + if (elements.length) { + const iconContainer = new G(elements[0] as SVGGElement); + iconContainer.add(iconElement); + const box = iconElement.bbox(); + iconElement.translate(-box.cx, -box.cy); + } + } } return svgElement; } else { diff --git a/ui-ngx/src/assets/markers/iconContainer1.svg b/ui-ngx/src/assets/markers/iconContainer1.svg index bf92e4d90f..d9e00d3934 100644 --- a/ui-ngx/src/assets/markers/iconContainer1.svg +++ b/ui-ngx/src/assets/markers/iconContainer1.svg @@ -1,22 +1,25 @@ - - + + - - - - - - + + + - + + + + diff --git a/ui-ngx/src/assets/markers/iconContainer2.svg b/ui-ngx/src/assets/markers/iconContainer2.svg new file mode 100644 index 0000000000..fa716aa30e --- /dev/null +++ b/ui-ngx/src/assets/markers/iconContainer2.svg @@ -0,0 +1,26 @@ + + + + + + + + + + + + + + + + + + + + + + + diff --git a/ui-ngx/src/assets/markers/iconContainer3.svg b/ui-ngx/src/assets/markers/iconContainer3.svg new file mode 100644 index 0000000000..a64995d1ee --- /dev/null +++ b/ui-ngx/src/assets/markers/iconContainer3.svg @@ -0,0 +1,16 @@ + + + + + + + + + + + + + diff --git a/ui-ngx/src/assets/markers/shape1.svg b/ui-ngx/src/assets/markers/shape1.svg index 0a29bb7dbe..7399bab288 100644 --- a/ui-ngx/src/assets/markers/shape1.svg +++ b/ui-ngx/src/assets/markers/shape1.svg @@ -1,19 +1,22 @@ - - + + - - - - + - + - - - + + + + + + diff --git a/ui-ngx/src/assets/markers/shape10.svg b/ui-ngx/src/assets/markers/shape10.svg index f0d8b68d2f..d13fca2d2b 100644 --- a/ui-ngx/src/assets/markers/shape10.svg +++ b/ui-ngx/src/assets/markers/shape10.svg @@ -1,11 +1,11 @@ - - - + + + - + - + diff --git a/ui-ngx/src/assets/markers/shape2.svg b/ui-ngx/src/assets/markers/shape2.svg index 1c59215800..c60d909881 100644 --- a/ui-ngx/src/assets/markers/shape2.svg +++ b/ui-ngx/src/assets/markers/shape2.svg @@ -1,17 +1,23 @@ - - + + - - - + + + - + - - - + + + + + + diff --git a/ui-ngx/src/assets/markers/shape3.svg b/ui-ngx/src/assets/markers/shape3.svg index eb2fa563b7..e1f0a8ac1b 100644 --- a/ui-ngx/src/assets/markers/shape3.svg +++ b/ui-ngx/src/assets/markers/shape3.svg @@ -1,19 +1,22 @@ - - - + + + - - - - - + + - + - - - + + + + + + diff --git a/ui-ngx/src/assets/markers/shape4.svg b/ui-ngx/src/assets/markers/shape4.svg index 5303d47ba6..4190ce6d06 100644 --- a/ui-ngx/src/assets/markers/shape4.svg +++ b/ui-ngx/src/assets/markers/shape4.svg @@ -1,21 +1,24 @@ - - - + + + - - - - - - - + + + + - + - - - + + + + + + diff --git a/ui-ngx/src/assets/markers/shape5.svg b/ui-ngx/src/assets/markers/shape5.svg index db3c2e0118..e1080d7e19 100644 --- a/ui-ngx/src/assets/markers/shape5.svg +++ b/ui-ngx/src/assets/markers/shape5.svg @@ -1,20 +1,23 @@ - - - + + + - - - - - - + + + - + - - - + + + + + + diff --git a/ui-ngx/src/assets/markers/shape6.svg b/ui-ngx/src/assets/markers/shape6.svg index 19002cec71..b388f13bdf 100644 --- a/ui-ngx/src/assets/markers/shape6.svg +++ b/ui-ngx/src/assets/markers/shape6.svg @@ -1,19 +1,22 @@ - - + + - - - - + - + + + + diff --git a/ui-ngx/src/assets/markers/shape7.svg b/ui-ngx/src/assets/markers/shape7.svg index c7df181b78..aeef8c74c5 100644 --- a/ui-ngx/src/assets/markers/shape7.svg +++ b/ui-ngx/src/assets/markers/shape7.svg @@ -1,6 +1,6 @@ - - + + @@ -9,12 +9,12 @@ - + - - - + + + diff --git a/ui-ngx/src/assets/markers/shape8.svg b/ui-ngx/src/assets/markers/shape8.svg index 1fecf058d9..a303a36ba3 100644 --- a/ui-ngx/src/assets/markers/shape8.svg +++ b/ui-ngx/src/assets/markers/shape8.svg @@ -1,12 +1,12 @@ - - - - + + + + - + diff --git a/ui-ngx/src/assets/markers/shape9.svg b/ui-ngx/src/assets/markers/shape9.svg index f44a6b3626..52d1a03fa0 100644 --- a/ui-ngx/src/assets/markers/shape9.svg +++ b/ui-ngx/src/assets/markers/shape9.svg @@ -1,14 +1,14 @@ - - - + + + - - - + + + - + diff --git a/ui-ngx/src/assets/markers/tripIconContainer1.svg b/ui-ngx/src/assets/markers/tripIconContainer1.svg index df33957809..c1186b2aa9 100644 --- a/ui-ngx/src/assets/markers/tripIconContainer1.svg +++ b/ui-ngx/src/assets/markers/tripIconContainer1.svg @@ -1,12 +1,23 @@ - - - - + + + + + + + + + + + + + - - + + diff --git a/ui-ngx/src/assets/markers/tripIconContainer2.svg b/ui-ngx/src/assets/markers/tripIconContainer2.svg new file mode 100644 index 0000000000..229c98b31e --- /dev/null +++ b/ui-ngx/src/assets/markers/tripIconContainer2.svg @@ -0,0 +1,24 @@ + + + + + + + + + + + + + + + + + + + + + diff --git a/ui-ngx/src/assets/markers/tripIconContainer3.svg b/ui-ngx/src/assets/markers/tripIconContainer3.svg new file mode 100644 index 0000000000..8a8baf2b1b --- /dev/null +++ b/ui-ngx/src/assets/markers/tripIconContainer3.svg @@ -0,0 +1,28 @@ + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/ui-ngx/src/assets/markers/tripShape10.svg b/ui-ngx/src/assets/markers/tripShape10.svg index 507d3725de..5fb5d81af1 100644 --- a/ui-ngx/src/assets/markers/tripShape10.svg +++ b/ui-ngx/src/assets/markers/tripShape10.svg @@ -1,10 +1,13 @@ - + - - + + diff --git a/ui-ngx/src/assets/markers/tripShape2.svg b/ui-ngx/src/assets/markers/tripShape2.svg index 0dfc4e22a1..e53beaf187 100644 --- a/ui-ngx/src/assets/markers/tripShape2.svg +++ b/ui-ngx/src/assets/markers/tripShape2.svg @@ -1,15 +1,29 @@ - - - - - + + + + + + + + + - + + + + + + + + + - - + + diff --git a/ui-ngx/src/assets/markers/tripShape3.svg b/ui-ngx/src/assets/markers/tripShape3.svg index e49d699ad2..ca81dc38e6 100644 --- a/ui-ngx/src/assets/markers/tripShape3.svg +++ b/ui-ngx/src/assets/markers/tripShape3.svg @@ -1,9 +1,12 @@ - + - - + + diff --git a/ui-ngx/src/assets/markers/tripShape4.svg b/ui-ngx/src/assets/markers/tripShape4.svg index f4b48da113..ff06ce5075 100644 --- a/ui-ngx/src/assets/markers/tripShape4.svg +++ b/ui-ngx/src/assets/markers/tripShape4.svg @@ -1,9 +1,12 @@ - + - - + + diff --git a/ui-ngx/src/assets/markers/tripShape5.svg b/ui-ngx/src/assets/markers/tripShape5.svg index a88d26035d..e774b4f92c 100644 --- a/ui-ngx/src/assets/markers/tripShape5.svg +++ b/ui-ngx/src/assets/markers/tripShape5.svg @@ -1,10 +1,29 @@ - - - + + + + + + + + + + + + + + + + + + + - - + + diff --git a/ui-ngx/src/assets/markers/tripShape6.svg b/ui-ngx/src/assets/markers/tripShape6.svg index 23105baba5..aeb7d7f3f5 100644 --- a/ui-ngx/src/assets/markers/tripShape6.svg +++ b/ui-ngx/src/assets/markers/tripShape6.svg @@ -1,10 +1,29 @@ - - - + + + + + + + + + + + + + + + + + + + - - + + diff --git a/ui-ngx/src/assets/markers/tripShape7.svg b/ui-ngx/src/assets/markers/tripShape7.svg index 342c9db364..38d35f6932 100644 --- a/ui-ngx/src/assets/markers/tripShape7.svg +++ b/ui-ngx/src/assets/markers/tripShape7.svg @@ -1,11 +1,14 @@ - - + + - - + + diff --git a/ui-ngx/src/assets/markers/tripShape8.svg b/ui-ngx/src/assets/markers/tripShape8.svg index f8edbe4dc0..60fdc493c5 100644 --- a/ui-ngx/src/assets/markers/tripShape8.svg +++ b/ui-ngx/src/assets/markers/tripShape8.svg @@ -1,9 +1,12 @@ - + - - + + diff --git a/ui-ngx/src/assets/markers/tripShape9.svg b/ui-ngx/src/assets/markers/tripShape9.svg index 9494a976f0..02521777af 100644 --- a/ui-ngx/src/assets/markers/tripShape9.svg +++ b/ui-ngx/src/assets/markers/tripShape9.svg @@ -1,10 +1,13 @@ - + - - + + From e986df7ebc7f60501eaa41c48593efb881756aa6 Mon Sep 17 00:00:00 2001 From: Vladyslav_Prykhodko Date: Fri, 7 Mar 2025 18:41:53 +0200 Subject: [PATCH 250/345] UI: Remove flex-layout --- ui-ngx/package.json | 1 - .../@angular+flex-layout+15.0.0-beta.42.patch | 21 ----- .../dynamic-component-factory.service.ts | 9 +-- .../app/core/services/resources.service.ts | 76 +++---------------- ui-ngx/src/app/modules/common/modules-map.ts | 60 ++++++--------- .../app/shared/legacy/flex-layout.models.ts | 41 ---------- ui-ngx/yarn.lock | 7 -- 7 files changed, 39 insertions(+), 176 deletions(-) delete mode 100644 ui-ngx/patches/@angular+flex-layout+15.0.0-beta.42.patch delete mode 100644 ui-ngx/src/app/shared/legacy/flex-layout.models.ts diff --git a/ui-ngx/package.json b/ui-ngx/package.json index f62c991187..b2e2dc18c3 100644 --- a/ui-ngx/package.json +++ b/ui-ngx/package.json @@ -18,7 +18,6 @@ "@angular/common": "18.2.13", "@angular/compiler": "18.2.13", "@angular/core": "18.2.13", - "@angular/flex-layout": "^15.0.0-beta.42", "@angular/forms": "18.2.13", "@angular/material": "18.2.14", "@angular/platform-browser": "18.2.13", diff --git a/ui-ngx/patches/@angular+flex-layout+15.0.0-beta.42.patch b/ui-ngx/patches/@angular+flex-layout+15.0.0-beta.42.patch deleted file mode 100644 index d4bf9d42cd..0000000000 --- a/ui-ngx/patches/@angular+flex-layout+15.0.0-beta.42.patch +++ /dev/null @@ -1,21 +0,0 @@ -diff --git a/node_modules/@angular/flex-layout/fesm2020/angular-flex-layout-extended.mjs b/node_modules/@angular/flex-layout/fesm2020/angular-flex-layout-extended.mjs -index 735dff5..52b6392 100644 ---- a/node_modules/@angular/flex-layout/fesm2020/angular-flex-layout-extended.mjs -+++ b/node_modules/@angular/flex-layout/fesm2020/angular-flex-layout-extended.mjs -@@ -124,7 +124,7 @@ class ClassDirective extends BaseDirective2 { - if (!this.ngClassInstance) { - // Create an instance NgClass Directive instance only if `ngClass=""` has NOT been defined on - // the same host element; since the responsive variations may be defined... -- this.ngClassInstance = new NgClass(iterableDiffers, keyValueDiffers, elementRef, renderer2); -+ this.ngClassInstance = new NgClass(elementRef, renderer2); - } - this.init(); - this.setValue('', ''); -diff --git a/node_modules/@angular/flex-layout/fesm2020/angular-flex-layout-extended.mjs.map b/node_modules/@angular/flex-layout/fesm2020/angular-flex-layout-extended.mjs.map -index a184784..2d0682e 100644 ---- a/node_modules/@angular/flex-layout/fesm2020/angular-flex-layout-extended.mjs.map -+++ b/node_modules/@angular/flex-layout/fesm2020/angular-flex-layout-extended.mjs.map -@@ -1 +1 @@ --{"version":3,"file":"angular-flex-layout-extended.mjs","sources":["../../../../projects/libs/flex-layout/extended/img-src/img-src.ts","../../../../projects/libs/flex-layout/extended/class/class.ts","../../../../projects/libs/flex-layout/extended/show-hide/show-hide.ts","../../../../projects/libs/flex-layout/extended/style/style-transforms.ts","../../../../projects/libs/flex-layout/extended/style/style.ts","../../../../projects/libs/flex-layout/extended/module.ts","../../../../projects/libs/flex-layout/extended/public-api.ts","../../../../projects/libs/flex-layout/extended/angular-flex-layout-extended.ts"],"sourcesContent":["/**\n * @license\n * Copyright Google LLC All Rights Reserved.\n *\n * Use of this source code is governed by an MIT-style license that can be\n * found in the LICENSE file at https://angular.io/license\n */\nimport {Directive, ElementRef, Inject, PLATFORM_ID, Injectable, Input} from '@angular/core';\nimport {isPlatformServer} from '@angular/common';\nimport {\n MediaMarshaller,\n BaseDirective2,\n SERVER_TOKEN,\n StyleBuilder,\n StyleDefinition,\n StyleUtils,\n} from '@angular/flex-layout/core';\n\n@Injectable({providedIn: 'root'})\nexport class ImgSrcStyleBuilder extends StyleBuilder {\n buildStyles(url: string) {\n return {'content': url ? `url(${url})` : ''};\n }\n}\n\n@Directive()\nexport class ImgSrcDirective extends BaseDirective2 {\n protected override DIRECTIVE_KEY = 'img-src';\n protected defaultSrc = '';\n\n @Input('src')\n set src(val: string) {\n this.defaultSrc = val;\n this.setValue(this.defaultSrc, '');\n }\n\n constructor(elementRef: ElementRef,\n styleBuilder: ImgSrcStyleBuilder,\n styler: StyleUtils,\n marshal: MediaMarshaller,\n @Inject(PLATFORM_ID) protected platformId: Object,\n @Inject(SERVER_TOKEN) protected serverModuleLoaded: boolean) {\n super(elementRef, styleBuilder, styler, marshal);\n this.init();\n this.setValue(this.nativeElement.getAttribute('src') || '', '');\n if (isPlatformServer(this.platformId) && this.serverModuleLoaded) {\n this.nativeElement.setAttribute('src', '');\n }\n }\n\n /**\n * Use the [responsively] activated input value to update\n * the host img src attribute or assign a default `img.src=''`\n * if the src has not been defined.\n *\n * Do nothing to standard `` usages, only when responsive\n * keys are present do we actually call `setAttribute()`\n */\n protected override updateWithValue(value?: string) {\n const url = value || this.defaultSrc;\n if (isPlatformServer(this.platformId) && this.serverModuleLoaded) {\n this.addStyles(url);\n } else {\n this.nativeElement.setAttribute('src', url);\n }\n }\n\n protected override styleCache = imgSrcCache;\n}\n\nconst imgSrcCache: Map = new Map();\n\nconst inputs = [\n 'src.xs', 'src.sm', 'src.md', 'src.lg', 'src.xl',\n 'src.lt-sm', 'src.lt-md', 'src.lt-lg', 'src.lt-xl',\n 'src.gt-xs', 'src.gt-sm', 'src.gt-md', 'src.gt-lg'\n];\n\nconst selector = `\n img[src.xs], img[src.sm], img[src.md], img[src.lg], img[src.xl],\n img[src.lt-sm], img[src.lt-md], img[src.lt-lg], img[src.lt-xl],\n img[src.gt-xs], img[src.gt-sm], img[src.gt-md], img[src.gt-lg]\n`;\n\n/**\n * This directive provides a responsive API for the HTML 'src' attribute\n * and will update the img.src property upon each responsive activation.\n *\n * e.g.\n * \n *\n * @see https://css-tricks.com/responsive-images-youre-just-changing-resolutions-use-src/\n */\n@Directive({selector, inputs})\nexport class DefaultImgSrcDirective extends ImgSrcDirective {\n protected override inputs = inputs;\n}\n","/**\n * @license\n * Copyright Google LLC All Rights Reserved.\n *\n * Use of this source code is governed by an MIT-style license that can be\n * found in the LICENSE file at https://angular.io/license\n */\nimport {\n Directive,\n DoCheck,\n ElementRef,\n Input,\n IterableDiffers,\n KeyValueDiffers,\n Optional,\n Renderer2,\n Self,\n} from '@angular/core';\nimport {NgClass} from '@angular/common';\nimport {BaseDirective2, StyleUtils, MediaMarshaller} from '@angular/flex-layout/core';\n\n@Directive()\nexport class ClassDirective extends BaseDirective2 implements DoCheck {\n\n protected override DIRECTIVE_KEY = 'ngClass';\n\n /**\n * Capture class assignments so we cache the default classes\n * which are merged with activated styles and used as fallbacks.\n */\n @Input('class')\n set klass(val: string) {\n this.ngClassInstance.klass = val;\n this.setValue(val, '');\n }\n\n constructor(elementRef: ElementRef,\n styler: StyleUtils,\n marshal: MediaMarshaller,\n iterableDiffers: IterableDiffers,\n keyValueDiffers: KeyValueDiffers,\n renderer2: Renderer2,\n @Optional() @Self() protected readonly ngClassInstance: NgClass) {\n super(elementRef, null!, styler, marshal);\n if (!this.ngClassInstance) {\n // Create an instance NgClass Directive instance only if `ngClass=\"\"` has NOT been defined on\n // the same host element; since the responsive variations may be defined...\n this.ngClassInstance = new NgClass(iterableDiffers, keyValueDiffers, elementRef, renderer2);\n }\n this.init();\n this.setValue('', '');\n }\n\n protected override updateWithValue(value: any) {\n this.ngClassInstance.ngClass = value;\n this.ngClassInstance.ngDoCheck();\n }\n\n // ******************************************************************\n // Lifecycle Hooks\n // ******************************************************************\n\n /**\n * For ChangeDetectionStrategy.onPush and ngOnChanges() updates\n */\n ngDoCheck() {\n this.ngClassInstance.ngDoCheck();\n }\n}\n\nconst inputs = [\n 'ngClass', 'ngClass.xs', 'ngClass.sm', 'ngClass.md', 'ngClass.lg', 'ngClass.xl',\n 'ngClass.lt-sm', 'ngClass.lt-md', 'ngClass.lt-lg', 'ngClass.lt-xl',\n 'ngClass.gt-xs', 'ngClass.gt-sm', 'ngClass.gt-md', 'ngClass.gt-lg'\n];\n\nconst selector = `\n [ngClass], [ngClass.xs], [ngClass.sm], [ngClass.md], [ngClass.lg], [ngClass.xl],\n [ngClass.lt-sm], [ngClass.lt-md], [ngClass.lt-lg], [ngClass.lt-xl],\n [ngClass.gt-xs], [ngClass.gt-sm], [ngClass.gt-md], [ngClass.gt-lg]\n`;\n\n/**\n * Directive to add responsive support for ngClass.\n * This maintains the core functionality of 'ngClass' and adds responsive API\n * Note: this class is a no-op when rendered on the server\n */\n@Directive({selector, inputs})\nexport class DefaultClassDirective extends ClassDirective {\n protected override inputs = inputs;\n}\n","/**\n * @license\n * Copyright Google LLC All Rights Reserved.\n *\n * Use of this source code is governed by an MIT-style license that can be\n * found in the LICENSE file at https://angular.io/license\n */\nimport {\n Directive,\n ElementRef,\n OnChanges,\n SimpleChanges,\n Inject,\n PLATFORM_ID,\n Injectable,\n AfterViewInit,\n} from '@angular/core';\nimport {isPlatformServer} from '@angular/common';\nimport {\n BaseDirective2,\n LAYOUT_CONFIG,\n LayoutConfigOptions,\n MediaMarshaller,\n SERVER_TOKEN,\n StyleUtils,\n StyleBuilder,\n} from '@angular/flex-layout/core';\nimport {coerceBooleanProperty} from '@angular/cdk/coercion';\nimport {takeUntil} from 'rxjs/operators';\n\nexport interface ShowHideParent {\n display: string;\n isServer: boolean;\n}\n\n@Injectable({providedIn: 'root'})\nexport class ShowHideStyleBuilder extends StyleBuilder {\n buildStyles(show: string, parent: ShowHideParent) {\n const shouldShow = show === 'true';\n return {'display': shouldShow ? parent.display || (parent.isServer ? 'initial' : '') : 'none'};\n }\n}\n\n@Directive()\nexport class ShowHideDirective extends BaseDirective2 implements AfterViewInit, OnChanges {\n protected override DIRECTIVE_KEY = 'show-hide';\n\n /** Original DOM Element CSS display style */\n protected display: string = '';\n protected hasLayout = false;\n protected hasFlexChild = false;\n\n constructor(elementRef: ElementRef,\n styleBuilder: ShowHideStyleBuilder,\n styler: StyleUtils,\n marshal: MediaMarshaller,\n @Inject(LAYOUT_CONFIG) protected layoutConfig: LayoutConfigOptions,\n @Inject(PLATFORM_ID) protected platformId: Object,\n @Inject(SERVER_TOKEN) protected serverModuleLoaded: boolean) {\n super(elementRef, styleBuilder, styler, marshal);\n }\n\n // *********************************************\n // Lifecycle Methods\n // *********************************************\n\n ngAfterViewInit() {\n this.trackExtraTriggers();\n\n const children = Array.from(this.nativeElement.children);\n for (let i = 0; i < children.length; i++) {\n if (this.marshal.hasValue(children[i] as HTMLElement, 'flex')) {\n this.hasFlexChild = true;\n break;\n }\n }\n\n if (DISPLAY_MAP.has(this.nativeElement)) {\n this.display = DISPLAY_MAP.get(this.nativeElement)!;\n } else {\n this.display = this.getDisplayStyle();\n DISPLAY_MAP.set(this.nativeElement, this.display);\n }\n\n this.init();\n // set the default to show unless explicitly overridden\n const defaultValue = this.marshal.getValue(this.nativeElement, this.DIRECTIVE_KEY, '');\n if (defaultValue === undefined || defaultValue === '') {\n this.setValue(true, '');\n } else {\n this.triggerUpdate();\n }\n }\n\n /**\n * On changes to any @Input properties...\n * Default to use the non-responsive Input value ('fxShow')\n * Then conditionally override with the mq-activated Input's current value\n */\n override ngOnChanges(changes: SimpleChanges) {\n Object.keys(changes).forEach(key => {\n if (this.inputs.indexOf(key) !== -1) {\n const inputKey = key.split('.');\n const bp = inputKey.slice(1).join('.');\n const inputValue = changes[key].currentValue;\n let shouldShow = inputValue !== '' ?\n inputValue !== 0 ? coerceBooleanProperty(inputValue) : false\n : true;\n if (inputKey[0] === 'fxHide') {\n shouldShow = !shouldShow;\n }\n this.setValue(shouldShow, bp);\n }\n });\n }\n\n // *********************************************\n // Protected methods\n // *********************************************\n\n /**\n * Watch for these extra triggers to update fxShow, fxHide stylings\n */\n protected trackExtraTriggers() {\n this.hasLayout = this.marshal.hasValue(this.nativeElement, 'layout');\n\n ['layout', 'layout-align'].forEach(key => {\n this.marshal\n .trackValue(this.nativeElement, key)\n .pipe(takeUntil(this.destroySubject))\n .subscribe(this.triggerUpdate.bind(this));\n });\n }\n\n /**\n * Override accessor to the current HTMLElement's `display` style\n * Note: Show/Hide will not change the display to 'flex' but will set it to 'block'\n * unless it was already explicitly specified inline or in a CSS stylesheet.\n */\n protected getDisplayStyle(): string {\n return (this.hasLayout || (this.hasFlexChild && this.layoutConfig.addFlexToParent)) ?\n 'flex' : this.styler.lookupStyle(this.nativeElement, 'display', true);\n }\n\n /** Validate the visibility value and then update the host's inline display style */\n protected override updateWithValue(value: boolean | string = true) {\n if (value === '') {\n return;\n }\n const isServer = isPlatformServer(this.platformId);\n this.addStyles(value ? 'true' : 'false', {display: this.display, isServer});\n if (isServer && this.serverModuleLoaded) {\n this.nativeElement.style.setProperty('display', '');\n }\n this.marshal.triggerUpdate(this.parentElement!, 'layout-gap');\n }\n}\n\nconst DISPLAY_MAP: WeakMap = new WeakMap();\n\nconst inputs = [\n 'fxShow', 'fxShow.print',\n 'fxShow.xs', 'fxShow.sm', 'fxShow.md', 'fxShow.lg', 'fxShow.xl',\n 'fxShow.lt-sm', 'fxShow.lt-md', 'fxShow.lt-lg', 'fxShow.lt-xl',\n 'fxShow.gt-xs', 'fxShow.gt-sm', 'fxShow.gt-md', 'fxShow.gt-lg',\n 'fxHide', 'fxHide.print',\n 'fxHide.xs', 'fxHide.sm', 'fxHide.md', 'fxHide.lg', 'fxHide.xl',\n 'fxHide.lt-sm', 'fxHide.lt-md', 'fxHide.lt-lg', 'fxHide.lt-xl',\n 'fxHide.gt-xs', 'fxHide.gt-sm', 'fxHide.gt-md', 'fxHide.gt-lg'\n];\n\nconst selector = `\n [fxShow], [fxShow.print],\n [fxShow.xs], [fxShow.sm], [fxShow.md], [fxShow.lg], [fxShow.xl],\n [fxShow.lt-sm], [fxShow.lt-md], [fxShow.lt-lg], [fxShow.lt-xl],\n [fxShow.gt-xs], [fxShow.gt-sm], [fxShow.gt-md], [fxShow.gt-lg],\n [fxHide], [fxHide.print],\n [fxHide.xs], [fxHide.sm], [fxHide.md], [fxHide.lg], [fxHide.xl],\n [fxHide.lt-sm], [fxHide.lt-md], [fxHide.lt-lg], [fxHide.lt-xl],\n [fxHide.gt-xs], [fxHide.gt-sm], [fxHide.gt-md], [fxHide.gt-lg]\n`;\n\n/**\n * 'show' Layout API directive\n */\n@Directive({selector, inputs})\nexport class DefaultShowHideDirective extends ShowHideDirective {\n protected override inputs = inputs;\n}\n","/**\n * @license\n * Copyright Google LLC All Rights Reserved.\n *\n * Use of this source code is governed by an MIT-style license that can be\n * found in the LICENSE file at https://angular.io/license\n */\n\nexport type NgStyleRawList = string[];\nexport type NgStyleMap = {[klass: string]: string};\n// NgStyle selectors accept NgStyleType values\nexport type NgStyleType = string | Set | NgStyleRawList | NgStyleMap;\n\n/**\n * Callback function for SecurityContext.STYLE sanitization\n */\nexport type NgStyleSanitizer = (val: any) => string;\n\n/** NgStyle allowed inputs */\nexport class NgStyleKeyValue {\n constructor(public key: string, public value: string, noQuotes = true) {\n this.key = noQuotes ? key.replace(/['\"]/g, '').trim() : key.trim();\n\n this.value = noQuotes ? value.replace(/['\"]/g, '').trim() : value.trim();\n this.value = this.value.replace(/;/, '');\n }\n}\n\nexport function getType(target: any): string {\n let what = typeof target;\n if (what === 'object') {\n return (target.constructor === Array) ? 'array' :\n (target.constructor === Set) ? 'set' : 'object';\n }\n return what;\n}\n\n/**\n * Split string of key:value pairs into Array of k-v pairs\n * e.g. 'key:value; key:value; key:value;' -> ['key:value',...]\n */\nexport function buildRawList(source: any, delimiter = ';'): NgStyleRawList {\n return String(source)\n .trim()\n .split(delimiter)\n .map((val: string) => val.trim())\n .filter(val => val !== '');\n}\n\n/** Convert array of key:value strings to a iterable map object */\nexport function buildMapFromList(styles: NgStyleRawList, sanitize?: NgStyleSanitizer): NgStyleMap {\n const sanitizeValue = (it: NgStyleKeyValue) => {\n if (sanitize) {\n it.value = sanitize(it.value);\n }\n return it;\n };\n\n return styles\n .map(stringToKeyValue)\n .filter(entry => !!entry)\n .map(sanitizeValue)\n .reduce(keyValuesToMap, {} as NgStyleMap);\n}\n\n/** Convert Set or raw Object to an iterable NgStyleMap */\nexport function buildMapFromSet(source: NgStyleType, sanitize?: NgStyleSanitizer): NgStyleMap {\n let list: string[] = [];\n if (getType(source) === 'set') {\n (source as Set).forEach(entry => list.push(entry));\n } else {\n Object.keys(source).forEach((key: string) => {\n list.push(`${key}:${(source as NgStyleMap)[key]}`);\n });\n }\n return buildMapFromList(list, sanitize);\n}\n\n\n/** Convert 'key:value' -> [key, value] */\nexport function stringToKeyValue(it: string): NgStyleKeyValue {\n const [key, ...vals] = it.split(':');\n return new NgStyleKeyValue(key, vals.join(':'));\n}\n\n/** Convert [ [key,value] ] -> { key : value } */\nexport function keyValuesToMap(map: NgStyleMap, entry: NgStyleKeyValue): NgStyleMap {\n if (!!entry.key) {\n map[entry.key] = entry.value;\n }\n return map;\n}\n","/**\n * @license\n * Copyright Google LLC All Rights Reserved.\n *\n * Use of this source code is governed by an MIT-style license that can be\n * found in the LICENSE file at https://angular.io/license\n */\nimport {\n Directive,\n DoCheck,\n ElementRef,\n Inject,\n KeyValueDiffers,\n Optional,\n PLATFORM_ID,\n Renderer2,\n SecurityContext,\n Self,\n} from '@angular/core';\nimport {isPlatformServer, NgStyle} from '@angular/common';\nimport {DomSanitizer} from '@angular/platform-browser';\nimport {\n BaseDirective2,\n StyleUtils,\n MediaMarshaller,\n SERVER_TOKEN,\n} from '@angular/flex-layout/core';\n\nimport {\n NgStyleRawList,\n NgStyleType,\n NgStyleSanitizer,\n buildRawList,\n getType,\n buildMapFromSet,\n NgStyleMap,\n NgStyleKeyValue,\n stringToKeyValue,\n keyValuesToMap,\n} from './style-transforms';\n\n@Directive()\nexport class StyleDirective extends BaseDirective2 implements DoCheck {\n\n protected override DIRECTIVE_KEY = 'ngStyle';\n protected fallbackStyles: NgStyleMap;\n protected isServer: boolean;\n\n constructor(elementRef: ElementRef,\n styler: StyleUtils,\n marshal: MediaMarshaller,\n protected sanitizer: DomSanitizer,\n differs: KeyValueDiffers,\n renderer2: Renderer2,\n @Optional() @Self() private readonly ngStyleInstance: NgStyle,\n @Inject(SERVER_TOKEN) serverLoaded: boolean,\n @Inject(PLATFORM_ID) platformId: Object) {\n super(elementRef, null!, styler, marshal);\n if (!this.ngStyleInstance) {\n // Create an instance NgStyle Directive instance only if `ngStyle=\"\"` has NOT been\n // defined on the same host element; since the responsive variations may be defined...\n this.ngStyleInstance = new NgStyle(elementRef, differs, renderer2);\n }\n this.init();\n const styles = this.nativeElement.getAttribute('style') ?? '';\n this.fallbackStyles = this.buildStyleMap(styles);\n this.isServer = serverLoaded && isPlatformServer(platformId);\n }\n\n /** Add generated styles */\n protected override updateWithValue(value: any) {\n const styles = this.buildStyleMap(value);\n this.ngStyleInstance.ngStyle = {...this.fallbackStyles, ...styles};\n if (this.isServer) {\n this.applyStyleToElement(styles);\n }\n this.ngStyleInstance.ngDoCheck();\n }\n\n /** Remove generated styles */\n protected override clearStyles() {\n this.ngStyleInstance.ngStyle = this.fallbackStyles;\n this.ngStyleInstance.ngDoCheck();\n }\n\n /**\n * Convert raw strings to ngStyleMap; which is required by ngStyle\n * NOTE: Raw string key-value pairs MUST be delimited by `;`\n * Comma-delimiters are not supported due to complexities of\n * possible style values such as `rgba(x,x,x,x)` and others\n */\n protected buildStyleMap(styles: NgStyleType): NgStyleMap {\n // Always safe-guard (aka sanitize) style property values\n const sanitizer: NgStyleSanitizer = (val: any) =>\n this.sanitizer.sanitize(SecurityContext.STYLE, val) ?? '';\n if (styles) {\n switch (getType(styles)) {\n case 'string': return buildMapFromList(buildRawList(styles),\n sanitizer);\n case 'array' : return buildMapFromList(styles as NgStyleRawList, sanitizer);\n case 'set' : return buildMapFromSet(styles, sanitizer);\n default : return buildMapFromSet(styles, sanitizer);\n }\n }\n\n return {};\n }\n\n // ******************************************************************\n // Lifecycle Hooks\n // ******************************************************************\n\n /** For ChangeDetectionStrategy.onPush and ngOnChanges() updates */\n ngDoCheck() {\n this.ngStyleInstance.ngDoCheck();\n }\n}\n\nconst inputs = [\n 'ngStyle',\n 'ngStyle.xs', 'ngStyle.sm', 'ngStyle.md', 'ngStyle.lg', 'ngStyle.xl',\n 'ngStyle.lt-sm', 'ngStyle.lt-md', 'ngStyle.lt-lg', 'ngStyle.lt-xl',\n 'ngStyle.gt-xs', 'ngStyle.gt-sm', 'ngStyle.gt-md', 'ngStyle.gt-lg'\n];\n\nconst selector = `\n [ngStyle],\n [ngStyle.xs], [ngStyle.sm], [ngStyle.md], [ngStyle.lg], [ngStyle.xl],\n [ngStyle.lt-sm], [ngStyle.lt-md], [ngStyle.lt-lg], [ngStyle.lt-xl],\n [ngStyle.gt-xs], [ngStyle.gt-sm], [ngStyle.gt-md], [ngStyle.gt-lg]\n`;\n\n/**\n * Directive to add responsive support for ngStyle.\n *\n */\n@Directive({selector, inputs})\nexport class DefaultStyleDirective extends StyleDirective implements DoCheck {\n protected override inputs = inputs;\n}\n\n/** Build a styles map from a list of styles, while sanitizing bad values first */\nfunction buildMapFromList(styles: NgStyleRawList, sanitize?: NgStyleSanitizer): NgStyleMap {\n const sanitizeValue = (it: NgStyleKeyValue) => {\n if (sanitize) {\n it.value = sanitize(it.value);\n }\n return it;\n };\n\n return styles\n .map(stringToKeyValue)\n .filter(entry => !!entry)\n .map(sanitizeValue)\n .reduce(keyValuesToMap, {} as NgStyleMap);\n}\n","/**\n * @license\n * Copyright Google LLC All Rights Reserved.\n *\n * Use of this source code is governed by an MIT-style license that can be\n * found in the LICENSE file at https://angular.io/license\n */\nimport {NgModule} from '@angular/core';\nimport {CoreModule} from '@angular/flex-layout/core';\n\nimport {DefaultImgSrcDirective} from './img-src/img-src';\nimport {DefaultClassDirective} from './class/class';\nimport {DefaultShowHideDirective} from './show-hide/show-hide';\nimport {DefaultStyleDirective} from './style/style';\n\n\nconst ALL_DIRECTIVES = [\n DefaultShowHideDirective,\n DefaultClassDirective,\n DefaultStyleDirective,\n DefaultImgSrcDirective,\n];\n\n/**\n * *****************************************************************\n * Define module for the Extended API\n * *****************************************************************\n */\n\n@NgModule({\n imports: [CoreModule],\n declarations: [...ALL_DIRECTIVES],\n exports: [...ALL_DIRECTIVES]\n})\nexport class ExtendedModule {\n}\n","/**\n * @license\n * Copyright Google LLC All Rights Reserved.\n *\n * Use of this source code is governed by an MIT-style license that can be\n * found in the LICENSE file at https://angular.io/license\n */\n\nexport * from './module';\n\nexport * from './class/class';\nexport * from './img-src/img-src';\nexport * from './show-hide/show-hide';\nexport * from './style/style';\n","/**\n * Generated bundle index. Do not edit.\n */\n\nexport * from './public-api';\n"],"names":["inputs","selector","buildMapFromList","i2","i3"],"mappings":";;;;;;;;;;AAAA;;;;;;AAMG;AAaG,MAAO,kBAAmB,SAAQ,YAAY,CAAA;AAClD,IAAA,WAAW,CAAC,GAAW,EAAA;AACrB,QAAA,OAAO,EAAC,SAAS,EAAE,GAAG,GAAG,CAAO,IAAA,EAAA,GAAG,GAAG,GAAG,EAAE,EAAC,CAAC;KAC9C;;+GAHU,kBAAkB,EAAA,IAAA,EAAA,IAAA,EAAA,MAAA,EAAA,EAAA,CAAA,eAAA,CAAA,UAAA,EAAA,CAAA,CAAA;AAAlB,kBAAA,CAAA,KAAA,GAAA,EAAA,CAAA,qBAAA,CAAA,EAAA,UAAA,EAAA,QAAA,EAAA,OAAA,EAAA,QAAA,EAAA,QAAA,EAAA,EAAA,EAAA,IAAA,EAAA,kBAAkB,cADN,MAAM,EAAA,CAAA,CAAA;2FAClB,kBAAkB,EAAA,UAAA,EAAA,CAAA;kBAD9B,UAAU;mBAAC,EAAC,UAAU,EAAE,MAAM,EAAC,CAAA;;AAQ1B,MAAO,eAAgB,SAAQ,cAAc,CAAA;IAUjD,WAAY,CAAA,UAAsB,EACtB,YAAgC,EAChC,MAAkB,EAClB,OAAwB,EACO,UAAkB,EACjB,kBAA2B,EAAA;QACrE,KAAK,CAAC,UAAU,EAAE,YAAY,EAAE,MAAM,EAAE,OAAO,CAAC,CAAC;QAFR,IAAU,CAAA,UAAA,GAAV,UAAU,CAAQ;QACjB,IAAkB,CAAA,kBAAA,GAAlB,kBAAkB,CAAS;QAdpD,IAAa,CAAA,aAAA,GAAG,SAAS,CAAC;QACnC,IAAU,CAAA,UAAA,GAAG,EAAE,CAAC;QAuCP,IAAU,CAAA,UAAA,GAAG,WAAW,CAAC;QAxB1C,IAAI,CAAC,IAAI,EAAE,CAAC;AACZ,QAAA,IAAI,CAAC,QAAQ,CAAC,IAAI,CAAC,aAAa,CAAC,YAAY,CAAC,KAAK,CAAC,IAAI,EAAE,EAAE,EAAE,CAAC,CAAC;QAChE,IAAI,gBAAgB,CAAC,IAAI,CAAC,UAAU,CAAC,IAAI,IAAI,CAAC,kBAAkB,EAAE;YAChE,IAAI,CAAC,aAAa,CAAC,YAAY,CAAC,KAAK,EAAE,EAAE,CAAC,CAAC;AAC5C,SAAA;KACF;IAlBD,IACI,GAAG,CAAC,GAAW,EAAA;AACjB,QAAA,IAAI,CAAC,UAAU,GAAG,GAAG,CAAC;QACtB,IAAI,CAAC,QAAQ,CAAC,IAAI,CAAC,UAAU,EAAE,EAAE,CAAC,CAAC;KACpC;AAgBD;;;;;;;AAOG;AACgB,IAAA,eAAe,CAAC,KAAc,EAAA;AAC/C,QAAA,MAAM,GAAG,GAAG,KAAK,IAAI,IAAI,CAAC,UAAU,CAAC;QACrC,IAAI,gBAAgB,CAAC,IAAI,CAAC,UAAU,CAAC,IAAI,IAAI,CAAC,kBAAkB,EAAE;AAChE,YAAA,IAAI,CAAC,SAAS,CAAC,GAAG,CAAC,CAAC;AACrB,SAAA;AAAM,aAAA;YACL,IAAI,CAAC,aAAa,CAAC,YAAY,CAAC,KAAK,EAAE,GAAG,CAAC,CAAC;AAC7C,SAAA;KACF;;4GAvCU,eAAe,EAAA,IAAA,EAAA,CAAA,EAAA,KAAA,EAAA,EAAA,CAAA,UAAA,EAAA,EAAA,EAAA,KAAA,EAAA,kBAAA,EAAA,EAAA,EAAA,KAAA,EAAA,EAAA,CAAA,UAAA,EAAA,EAAA,EAAA,KAAA,EAAA,EAAA,CAAA,eAAA,EAAA,EAAA,EAAA,KAAA,EAcN,WAAW,EAAA,EAAA,EAAA,KAAA,EACX,YAAY,EAAA,CAAA,EAAA,MAAA,EAAA,EAAA,CAAA,eAAA,CAAA,SAAA,EAAA,CAAA,CAAA;gGAfrB,eAAe,EAAA,MAAA,EAAA,EAAA,GAAA,EAAA,KAAA,EAAA,EAAA,eAAA,EAAA,IAAA,EAAA,QAAA,EAAA,EAAA,EAAA,CAAA,CAAA;2FAAf,eAAe,EAAA,UAAA,EAAA,CAAA;kBAD3B,SAAS;;0BAeK,MAAM;2BAAC,WAAW,CAAA;;0BAClB,MAAM;2BAAC,YAAY,CAAA;4CAV5B,GAAG,EAAA,CAAA;sBADN,KAAK;uBAAC,KAAK,CAAA;;AAwCd,MAAM,WAAW,GAAiC,IAAI,GAAG,EAAE,CAAC;AAE5D,MAAMA,QAAM,GAAG;AACb,IAAA,QAAQ,EAAE,QAAQ,EAAE,QAAQ,EAAE,QAAQ,EAAE,QAAQ;AAChD,IAAA,WAAW,EAAE,WAAW,EAAE,WAAW,EAAE,WAAW;AAClD,IAAA,WAAW,EAAE,WAAW,EAAE,WAAW,EAAE,WAAW;CACnD,CAAC;AAEF,MAAMC,UAAQ,GAAG,CAAA;;;;CAIhB,CAAC;AAEF;;;;;;;;AAQG;AAEG,MAAO,sBAAuB,SAAQ,eAAe,CAAA;AAD3D,IAAA,WAAA,GAAA;;QAEqB,IAAM,CAAA,MAAA,GAAGD,QAAM,CAAC;AACpC,KAAA;;mHAFY,sBAAsB,EAAA,IAAA,EAAA,IAAA,EAAA,MAAA,EAAA,EAAA,CAAA,eAAA,CAAA,SAAA,EAAA,CAAA,CAAA;uGAAtB,sBAAsB,EAAA,QAAA,EAAA,wNAAA,EAAA,MAAA,EAAA,EAAA,QAAA,EAAA,QAAA,EAAA,QAAA,EAAA,QAAA,EAAA,QAAA,EAAA,QAAA,EAAA,QAAA,EAAA,QAAA,EAAA,QAAA,EAAA,QAAA,EAAA,WAAA,EAAA,WAAA,EAAA,WAAA,EAAA,WAAA,EAAA,WAAA,EAAA,WAAA,EAAA,WAAA,EAAA,WAAA,EAAA,WAAA,EAAA,WAAA,EAAA,WAAA,EAAA,WAAA,EAAA,WAAA,EAAA,WAAA,EAAA,WAAA,EAAA,WAAA,EAAA,EAAA,eAAA,EAAA,IAAA,EAAA,QAAA,EAAA,EAAA,EAAA,CAAA,CAAA;2FAAtB,sBAAsB,EAAA,UAAA,EAAA,CAAA;kBADlC,SAAS;mBAAC,YAACC,UAAQ,UAAED,QAAM,EAAC,CAAA;;;AC7F7B;;;;;;AAMG;AAgBG,MAAO,cAAe,SAAQ,cAAc,CAAA;AAchD,IAAA,WAAA,CAAY,UAAsB,EACtB,MAAkB,EAClB,OAAwB,EACxB,eAAgC,EAChC,eAAgC,EAChC,SAAoB,EACmB,eAAwB,EAAA;QACzE,KAAK,CAAC,UAAU,EAAE,IAAK,EAAE,MAAM,EAAE,OAAO,CAAC,CAAC;QADO,IAAe,CAAA,eAAA,GAAf,eAAe,CAAS;QAlBxD,IAAa,CAAA,aAAA,GAAG,SAAS,CAAC;AAoB3C,QAAA,IAAI,CAAC,IAAI,CAAC,eAAe,EAAE;;;AAGzB,YAAA,IAAI,CAAC,eAAe,GAAG,IAAI,OAAO,CAAC,eAAe,EAAE,eAAe,EAAE,UAAU,EAAE,SAAS,CAAC,CAAC;AAC7F,SAAA;QACD,IAAI,CAAC,IAAI,EAAE,CAAC;AACZ,QAAA,IAAI,CAAC,QAAQ,CAAC,EAAE,EAAE,EAAE,CAAC,CAAC;KACvB;AAzBD;;;AAGG;IACH,IACI,KAAK,CAAC,GAAW,EAAA;AACnB,QAAA,IAAI,CAAC,eAAe,CAAC,KAAK,GAAG,GAAG,CAAC;AACjC,QAAA,IAAI,CAAC,QAAQ,CAAC,GAAG,EAAE,EAAE,CAAC,CAAC;KACxB;AAmBkB,IAAA,eAAe,CAAC,KAAU,EAAA;AAC3C,QAAA,IAAI,CAAC,eAAe,CAAC,OAAO,GAAG,KAAK,CAAC;AACrC,QAAA,IAAI,CAAC,eAAe,CAAC,SAAS,EAAE,CAAC;KAClC;;;;AAMD;;AAEG;IACH,SAAS,GAAA;AACP,QAAA,IAAI,CAAC,eAAe,CAAC,SAAS,EAAE,CAAC;KAClC;;2GA7CU,cAAc,EAAA,IAAA,EAAA,CAAA,EAAA,KAAA,EAAA,EAAA,CAAA,UAAA,EAAA,EAAA,EAAA,KAAA,EAAA,EAAA,CAAA,UAAA,EAAA,EAAA,EAAA,KAAA,EAAA,EAAA,CAAA,eAAA,EAAA,EAAA,EAAA,KAAA,EAAA,EAAA,CAAA,eAAA,EAAA,EAAA,EAAA,KAAA,EAAA,EAAA,CAAA,eAAA,EAAA,EAAA,EAAA,KAAA,EAAA,EAAA,CAAA,SAAA,EAAA,EAAA,EAAA,KAAA,EAAA,EAAA,CAAA,OAAA,EAAA,QAAA,EAAA,IAAA,EAAA,IAAA,EAAA,IAAA,EAAA,CAAA,EAAA,MAAA,EAAA,EAAA,CAAA,eAAA,CAAA,SAAA,EAAA,CAAA,CAAA;+FAAd,cAAc,EAAA,MAAA,EAAA,EAAA,KAAA,EAAA,CAAA,OAAA,EAAA,OAAA,CAAA,EAAA,EAAA,eAAA,EAAA,IAAA,EAAA,QAAA,EAAA,EAAA,EAAA,CAAA,CAAA;2FAAd,cAAc,EAAA,UAAA,EAAA,CAAA;kBAD1B,SAAS;;0BAqBK,QAAQ;;0BAAI,IAAI;4CAXzB,KAAK,EAAA,CAAA;sBADR,KAAK;uBAAC,OAAO,CAAA;;AAwChB,MAAMA,QAAM,GAAG;IACb,SAAS,EAAE,YAAY,EAAE,YAAY,EAAE,YAAY,EAAE,YAAY,EAAE,YAAY;AAC/E,IAAA,eAAe,EAAE,eAAe,EAAE,eAAe,EAAE,eAAe;AAClE,IAAA,eAAe,EAAE,eAAe,EAAE,eAAe,EAAE,eAAe;CACnE,CAAC;AAEF,MAAMC,UAAQ,GAAG,CAAA;;;;CAIhB,CAAC;AAEF;;;;AAIG;AAEG,MAAO,qBAAsB,SAAQ,cAAc,CAAA;AADzD,IAAA,WAAA,GAAA;;QAEqB,IAAM,CAAA,MAAA,GAAGD,QAAM,CAAC;AACpC,KAAA;;kHAFY,qBAAqB,EAAA,IAAA,EAAA,IAAA,EAAA,MAAA,EAAA,EAAA,CAAA,eAAA,CAAA,SAAA,EAAA,CAAA,CAAA;sGAArB,qBAAqB,EAAA,QAAA,EAAA,qOAAA,EAAA,MAAA,EAAA,EAAA,OAAA,EAAA,SAAA,EAAA,YAAA,EAAA,YAAA,EAAA,YAAA,EAAA,YAAA,EAAA,YAAA,EAAA,YAAA,EAAA,YAAA,EAAA,YAAA,EAAA,YAAA,EAAA,YAAA,EAAA,eAAA,EAAA,eAAA,EAAA,eAAA,EAAA,eAAA,EAAA,eAAA,EAAA,eAAA,EAAA,eAAA,EAAA,eAAA,EAAA,eAAA,EAAA,eAAA,EAAA,eAAA,EAAA,eAAA,EAAA,eAAA,EAAA,eAAA,EAAA,eAAA,EAAA,eAAA,EAAA,EAAA,eAAA,EAAA,IAAA,EAAA,QAAA,EAAA,EAAA,EAAA,CAAA,CAAA;2FAArB,qBAAqB,EAAA,UAAA,EAAA,CAAA;kBADjC,SAAS;mBAAC,YAACC,UAAQ,UAAED,QAAM,EAAC,CAAA;;;ACvF7B;;;;;;AAMG;AA8BG,MAAO,oBAAqB,SAAQ,YAAY,CAAA;IACpD,WAAW,CAAC,IAAY,EAAE,MAAsB,EAAA;AAC9C,QAAA,MAAM,UAAU,GAAG,IAAI,KAAK,MAAM,CAAC;AACnC,QAAA,OAAO,EAAC,SAAS,EAAE,UAAU,GAAG,MAAM,CAAC,OAAO,KAAK,MAAM,CAAC,QAAQ,GAAG,SAAS,GAAG,EAAE,CAAC,GAAG,MAAM,EAAC,CAAC;KAChG;;iHAJU,oBAAoB,EAAA,IAAA,EAAA,IAAA,EAAA,MAAA,EAAA,EAAA,CAAA,eAAA,CAAA,UAAA,EAAA,CAAA,CAAA;AAApB,oBAAA,CAAA,KAAA,GAAA,EAAA,CAAA,qBAAA,CAAA,EAAA,UAAA,EAAA,QAAA,EAAA,OAAA,EAAA,QAAA,EAAA,QAAA,EAAA,EAAA,EAAA,IAAA,EAAA,oBAAoB,cADR,MAAM,EAAA,CAAA,CAAA;2FAClB,oBAAoB,EAAA,UAAA,EAAA,CAAA;kBADhC,UAAU;mBAAC,EAAC,UAAU,EAAE,MAAM,EAAC,CAAA;;AAS1B,MAAO,iBAAkB,SAAQ,cAAc,CAAA;AAQnD,IAAA,WAAA,CAAY,UAAsB,EACtB,YAAkC,EAClC,MAAkB,EAClB,OAAwB,EACS,YAAiC,EACnC,UAAkB,EACjB,kBAA2B,EAAA;QACrE,KAAK,CAAC,UAAU,EAAE,YAAY,EAAE,MAAM,EAAE,OAAO,CAAC,CAAC;QAHN,IAAY,CAAA,YAAA,GAAZ,YAAY,CAAqB;QACnC,IAAU,CAAA,UAAA,GAAV,UAAU,CAAQ;QACjB,IAAkB,CAAA,kBAAA,GAAlB,kBAAkB,CAAS;QAbpD,IAAa,CAAA,aAAA,GAAG,WAAW,CAAC;;QAGrC,IAAO,CAAA,OAAA,GAAW,EAAE,CAAC;QACrB,IAAS,CAAA,SAAA,GAAG,KAAK,CAAC;QAClB,IAAY,CAAA,YAAA,GAAG,KAAK,CAAC;KAU9B;;;;IAMD,eAAe,GAAA;QACb,IAAI,CAAC,kBAAkB,EAAE,CAAC;AAE1B,QAAA,MAAM,QAAQ,GAAG,KAAK,CAAC,IAAI,CAAC,IAAI,CAAC,aAAa,CAAC,QAAQ,CAAC,CAAC;AACzD,QAAA,KAAK,IAAI,CAAC,GAAG,CAAC,EAAE,CAAC,GAAG,QAAQ,CAAC,MAAM,EAAE,CAAC,EAAE,EAAE;AACxC,YAAA,IAAI,IAAI,CAAC,OAAO,CAAC,QAAQ,CAAC,QAAQ,CAAC,CAAC,CAAgB,EAAE,MAAM,CAAC,EAAE;AAC7D,gBAAA,IAAI,CAAC,YAAY,GAAG,IAAI,CAAC;gBACzB,MAAM;AACP,aAAA;AACF,SAAA;QAED,IAAI,WAAW,CAAC,GAAG,CAAC,IAAI,CAAC,aAAa,CAAC,EAAE;YACvC,IAAI,CAAC,OAAO,GAAG,WAAW,CAAC,GAAG,CAAC,IAAI,CAAC,aAAa,CAAE,CAAC;AACrD,SAAA;AAAM,aAAA;AACL,YAAA,IAAI,CAAC,OAAO,GAAG,IAAI,CAAC,eAAe,EAAE,CAAC;YACtC,WAAW,CAAC,GAAG,CAAC,IAAI,CAAC,aAAa,EAAE,IAAI,CAAC,OAAO,CAAC,CAAC;AACnD,SAAA;QAED,IAAI,CAAC,IAAI,EAAE,CAAC;;AAEZ,QAAA,MAAM,YAAY,GAAG,IAAI,CAAC,OAAO,CAAC,QAAQ,CAAC,IAAI,CAAC,aAAa,EAAE,IAAI,CAAC,aAAa,EAAE,EAAE,CAAC,CAAC;AACvF,QAAA,IAAI,YAAY,KAAK,SAAS,IAAI,YAAY,KAAK,EAAE,EAAE;AACrD,YAAA,IAAI,CAAC,QAAQ,CAAC,IAAI,EAAE,EAAE,CAAC,CAAC;AACzB,SAAA;AAAM,aAAA;YACL,IAAI,CAAC,aAAa,EAAE,CAAC;AACtB,SAAA;KACF;AAED;;;;AAIG;AACM,IAAA,WAAW,CAAC,OAAsB,EAAA;QACzC,MAAM,CAAC,IAAI,CAAC,OAAO,CAAC,CAAC,OAAO,CAAC,GAAG,IAAG;YACjC,IAAI,IAAI,CAAC,MAAM,CAAC,OAAO,CAAC,GAAG,CAAC,KAAK,CAAC,CAAC,EAAE;gBACnC,MAAM,QAAQ,GAAG,GAAG,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC;AAChC,gBAAA,MAAM,EAAE,GAAG,QAAQ,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC;gBACvC,MAAM,UAAU,GAAG,OAAO,CAAC,GAAG,CAAC,CAAC,YAAY,CAAC;AAC7C,gBAAA,IAAI,UAAU,GAAG,UAAU,KAAK,EAAE;AAC9B,oBAAA,UAAU,KAAK,CAAC,GAAG,qBAAqB,CAAC,UAAU,CAAC,GAAG,KAAK;sBAC1D,IAAI,CAAC;AACX,gBAAA,IAAI,QAAQ,CAAC,CAAC,CAAC,KAAK,QAAQ,EAAE;oBAC5B,UAAU,GAAG,CAAC,UAAU,CAAC;AAC1B,iBAAA;AACD,gBAAA,IAAI,CAAC,QAAQ,CAAC,UAAU,EAAE,EAAE,CAAC,CAAC;AAC/B,aAAA;AACH,SAAC,CAAC,CAAC;KACJ;;;;AAMD;;AAEG;IACO,kBAAkB,GAAA;AAC1B,QAAA,IAAI,CAAC,SAAS,GAAG,IAAI,CAAC,OAAO,CAAC,QAAQ,CAAC,IAAI,CAAC,aAAa,EAAE,QAAQ,CAAC,CAAC;QAErE,CAAC,QAAQ,EAAE,cAAc,CAAC,CAAC,OAAO,CAAC,GAAG,IAAG;AACvC,YAAA,IAAI,CAAC,OAAO;AACP,iBAAA,UAAU,CAAC,IAAI,CAAC,aAAa,EAAE,GAAG,CAAC;AACnC,iBAAA,IAAI,CAAC,SAAS,CAAC,IAAI,CAAC,cAAc,CAAC,CAAC;iBACpC,SAAS,CAAC,IAAI,CAAC,aAAa,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC,CAAC;AAChD,SAAC,CAAC,CAAC;KACJ;AAED;;;;AAIG;IACO,eAAe,GAAA;AACvB,QAAA,OAAO,CAAC,IAAI,CAAC,SAAS,KAAK,IAAI,CAAC,YAAY,IAAI,IAAI,CAAC,YAAY,CAAC,eAAe,CAAC;AAC9E,YAAA,MAAM,GAAG,IAAI,CAAC,MAAM,CAAC,WAAW,CAAC,IAAI,CAAC,aAAa,EAAE,SAAS,EAAE,IAAI,CAAC,CAAC;KAC3E;;IAGkB,eAAe,CAAC,QAA0B,IAAI,EAAA;QAC/D,IAAI,KAAK,KAAK,EAAE,EAAE;YAChB,OAAO;AACR,SAAA;QACD,MAAM,QAAQ,GAAG,gBAAgB,CAAC,IAAI,CAAC,UAAU,CAAC,CAAC;QACnD,IAAI,CAAC,SAAS,CAAC,KAAK,GAAG,MAAM,GAAG,OAAO,EAAE,EAAC,OAAO,EAAE,IAAI,CAAC,OAAO,EAAE,QAAQ,EAAC,CAAC,CAAC;AAC5E,QAAA,IAAI,QAAQ,IAAI,IAAI,CAAC,kBAAkB,EAAE;YACvC,IAAI,CAAC,aAAa,CAAC,KAAK,CAAC,WAAW,CAAC,SAAS,EAAE,EAAE,CAAC,CAAC;AACrD,SAAA;QACD,IAAI,CAAC,OAAO,CAAC,aAAa,CAAC,IAAI,CAAC,aAAc,EAAE,YAAY,CAAC,CAAC;KAC/D;;AA/GU,iBAAA,CAAA,IAAA,GAAA,EAAA,CAAA,kBAAA,CAAA,EAAA,UAAA,EAAA,QAAA,EAAA,OAAA,EAAA,QAAA,EAAA,QAAA,EAAA,EAAA,EAAA,IAAA,EAAA,iBAAiB,EAYR,IAAA,EAAA,CAAA,EAAA,KAAA,EAAA,EAAA,CAAA,UAAA,EAAA,EAAA,EAAA,KAAA,EAAA,oBAAA,EAAA,EAAA,EAAA,KAAA,EAAA,EAAA,CAAA,UAAA,EAAA,EAAA,EAAA,KAAA,EAAA,EAAA,CAAA,eAAA,EAAA,EAAA,EAAA,KAAA,EAAA,aAAa,EACb,EAAA,EAAA,KAAA,EAAA,WAAW,aACX,YAAY,EAAA,CAAA,EAAA,MAAA,EAAA,EAAA,CAAA,eAAA,CAAA,SAAA,EAAA,CAAA,CAAA;kGAdrB,iBAAiB,EAAA,eAAA,EAAA,IAAA,EAAA,aAAA,EAAA,IAAA,EAAA,QAAA,EAAA,EAAA,EAAA,CAAA,CAAA;2FAAjB,iBAAiB,EAAA,UAAA,EAAA,CAAA;kBAD7B,SAAS;;0BAaK,MAAM;2BAAC,aAAa,CAAA;;0BACpB,MAAM;2BAAC,WAAW,CAAA;;0BAClB,MAAM;2BAAC,YAAY,CAAA;;AAoGlC,MAAM,WAAW,GAAiC,IAAI,OAAO,EAAE,CAAC;AAEhE,MAAMA,QAAM,GAAG;AACb,IAAA,QAAQ,EAAE,cAAc;AACxB,IAAA,WAAW,EAAE,WAAW,EAAE,WAAW,EAAE,WAAW,EAAE,WAAW;AAC/D,IAAA,cAAc,EAAE,cAAc,EAAE,cAAc,EAAE,cAAc;AAC9D,IAAA,cAAc,EAAE,cAAc,EAAE,cAAc,EAAE,cAAc;AAC9D,IAAA,QAAQ,EAAE,cAAc;AACxB,IAAA,WAAW,EAAE,WAAW,EAAE,WAAW,EAAE,WAAW,EAAE,WAAW;AAC/D,IAAA,cAAc,EAAE,cAAc,EAAE,cAAc,EAAE,cAAc;AAC9D,IAAA,cAAc,EAAE,cAAc,EAAE,cAAc,EAAE,cAAc;CAC/D,CAAC;AAEF,MAAMC,UAAQ,GAAG,CAAA;;;;;;;;;CAShB,CAAC;AAEF;;AAEG;AAEG,MAAO,wBAAyB,SAAQ,iBAAiB,CAAA;AAD/D,IAAA,WAAA,GAAA;;QAEqB,IAAM,CAAA,MAAA,GAAGD,QAAM,CAAC;AACpC,KAAA;;qHAFY,wBAAwB,EAAA,IAAA,EAAA,IAAA,EAAA,MAAA,EAAA,EAAA,CAAA,eAAA,CAAA,SAAA,EAAA,CAAA,CAAA;yGAAxB,wBAAwB,EAAA,QAAA,EAAA,idAAA,EAAA,MAAA,EAAA,EAAA,MAAA,EAAA,QAAA,EAAA,cAAA,EAAA,cAAA,EAAA,WAAA,EAAA,WAAA,EAAA,WAAA,EAAA,WAAA,EAAA,WAAA,EAAA,WAAA,EAAA,WAAA,EAAA,WAAA,EAAA,WAAA,EAAA,WAAA,EAAA,cAAA,EAAA,cAAA,EAAA,cAAA,EAAA,cAAA,EAAA,cAAA,EAAA,cAAA,EAAA,cAAA,EAAA,cAAA,EAAA,cAAA,EAAA,cAAA,EAAA,cAAA,EAAA,cAAA,EAAA,cAAA,EAAA,cAAA,EAAA,cAAA,EAAA,cAAA,EAAA,MAAA,EAAA,QAAA,EAAA,cAAA,EAAA,cAAA,EAAA,WAAA,EAAA,WAAA,EAAA,WAAA,EAAA,WAAA,EAAA,WAAA,EAAA,WAAA,EAAA,WAAA,EAAA,WAAA,EAAA,WAAA,EAAA,WAAA,EAAA,cAAA,EAAA,cAAA,EAAA,cAAA,EAAA,cAAA,EAAA,cAAA,EAAA,cAAA,EAAA,cAAA,EAAA,cAAA,EAAA,cAAA,EAAA,cAAA,EAAA,cAAA,EAAA,cAAA,EAAA,cAAA,EAAA,cAAA,EAAA,cAAA,EAAA,cAAA,EAAA,EAAA,eAAA,EAAA,IAAA,EAAA,QAAA,EAAA,EAAA,EAAA,CAAA,CAAA;2FAAxB,wBAAwB,EAAA,UAAA,EAAA,CAAA;kBADpC,SAAS;mBAAC,YAACC,UAAQ,UAAED,QAAM,EAAC,CAAA;;;ACzL7B;;;;;;AAMG;AAYH;MACa,eAAe,CAAA;AAC1B,IAAA,WAAA,CAAmB,GAAW,EAAS,KAAa,EAAE,QAAQ,GAAG,IAAI,EAAA;QAAlD,IAAG,CAAA,GAAA,GAAH,GAAG,CAAQ;QAAS,IAAK,CAAA,KAAA,GAAL,KAAK,CAAQ;QAClD,IAAI,CAAC,GAAG,GAAG,QAAQ,GAAG,GAAG,CAAC,OAAO,CAAC,OAAO,EAAE,EAAE,CAAC,CAAC,IAAI,EAAE,GAAG,GAAG,CAAC,IAAI,EAAE,CAAC;QAEnE,IAAI,CAAC,KAAK,GAAG,QAAQ,GAAG,KAAK,CAAC,OAAO,CAAC,OAAO,EAAE,EAAE,CAAC,CAAC,IAAI,EAAE,GAAG,KAAK,CAAC,IAAI,EAAE,CAAC;AACzE,QAAA,IAAI,CAAC,KAAK,GAAG,IAAI,CAAC,KAAK,CAAC,OAAO,CAAC,GAAG,EAAE,EAAE,CAAC,CAAC;KAC1C;AACF,CAAA;AAEK,SAAU,OAAO,CAAC,MAAW,EAAA;AACjC,IAAA,IAAI,IAAI,GAAG,OAAO,MAAM,CAAC;IACzB,IAAI,IAAI,KAAK,QAAQ,EAAE;AACrB,QAAA,OAAO,CAAC,MAAM,CAAC,WAAW,KAAK,KAAK,IAAI,OAAO;AAC3C,YAAA,CAAC,MAAM,CAAC,WAAW,KAAK,GAAG,IAAI,KAAK,GAAG,QAAQ,CAAC;AACrD,KAAA;AACD,IAAA,OAAO,IAAI,CAAC;AACd,CAAC;AAED;;;AAGG;SACa,YAAY,CAAC,MAAW,EAAE,SAAS,GAAG,GAAG,EAAA;IACvD,OAAO,MAAM,CAAC,MAAM,CAAC;AAChB,SAAA,IAAI,EAAE;SACN,KAAK,CAAC,SAAS,CAAC;SAChB,GAAG,CAAC,CAAC,GAAW,KAAK,GAAG,CAAC,IAAI,EAAE,CAAC;SAChC,MAAM,CAAC,GAAG,IAAI,GAAG,KAAK,EAAE,CAAC,CAAC;AACjC,CAAC;AAED;AACgB,SAAAE,kBAAgB,CAAC,MAAsB,EAAE,QAA2B,EAAA;AAClF,IAAA,MAAM,aAAa,GAAG,CAAC,EAAmB,KAAI;AAC5C,QAAA,IAAI,QAAQ,EAAE;YACZ,EAAE,CAAC,KAAK,GAAG,QAAQ,CAAC,EAAE,CAAC,KAAK,CAAC,CAAC;AAC/B,SAAA;AACD,QAAA,OAAO,EAAE,CAAC;AACZ,KAAC,CAAC;AAEF,IAAA,OAAO,MAAM;SACR,GAAG,CAAC,gBAAgB,CAAC;SACrB,MAAM,CAAC,KAAK,IAAI,CAAC,CAAC,KAAK,CAAC;SACxB,GAAG,CAAC,aAAa,CAAC;AAClB,SAAA,MAAM,CAAC,cAAc,EAAE,EAAgB,CAAC,CAAC;AAChD,CAAC;AAED;AACgB,SAAA,eAAe,CAAC,MAAmB,EAAE,QAA2B,EAAA;IAC9E,IAAI,IAAI,GAAa,EAAE,CAAC;AACxB,IAAA,IAAI,OAAO,CAAC,MAAM,CAAC,KAAK,KAAK,EAAE;AAC5B,QAAA,MAAsB,CAAC,OAAO,CAAC,KAAK,IAAI,IAAI,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC,CAAC;AAC5D,KAAA;AAAM,SAAA;QACL,MAAM,CAAC,IAAI,CAAC,MAAM,CAAC,CAAC,OAAO,CAAC,CAAC,GAAW,KAAI;AAC1C,YAAA,IAAI,CAAC,IAAI,CAAC,CAAA,EAAG,GAAG,CAAA,CAAA,EAAK,MAAqB,CAAC,GAAG,CAAC,CAAE,CAAA,CAAC,CAAC;AACrD,SAAC,CAAC,CAAC;AACJ,KAAA;AACD,IAAA,OAAOA,kBAAgB,CAAC,IAAI,EAAE,QAAQ,CAAC,CAAC;AAC1C,CAAC;AAGD;AACM,SAAU,gBAAgB,CAAC,EAAU,EAAA;AACzC,IAAA,MAAM,CAAC,GAAG,EAAE,GAAG,IAAI,CAAC,GAAG,EAAE,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC;AACrC,IAAA,OAAO,IAAI,eAAe,CAAC,GAAG,EAAE,IAAI,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC,CAAC;AAClD,CAAC;AAED;AACgB,SAAA,cAAc,CAAC,GAAe,EAAE,KAAsB,EAAA;AACpE,IAAA,IAAI,CAAC,CAAC,KAAK,CAAC,GAAG,EAAE;QACf,GAAG,CAAC,KAAK,CAAC,GAAG,CAAC,GAAG,KAAK,CAAC,KAAK,CAAC;AAC9B,KAAA;AACD,IAAA,OAAO,GAAG,CAAC;AACb;;AC3FA;;;;;;AAMG;AAoCG,MAAO,cAAe,SAAQ,cAAc,CAAA;AAMhD,IAAA,WAAA,CAAY,UAAsB,EACtB,MAAkB,EAClB,OAAwB,EACd,SAAuB,EACjC,OAAwB,EACxB,SAAoB,EACiB,eAAwB,EACvC,YAAqB,EACtB,UAAkB,EAAA;QACjD,KAAK,CAAC,UAAU,EAAE,IAAK,EAAE,MAAM,EAAE,OAAO,CAAC,CAAC;QANtB,IAAS,CAAA,SAAA,GAAT,SAAS,CAAc;QAGI,IAAe,CAAA,eAAA,GAAf,eAAe,CAAS;QAVtD,IAAa,CAAA,aAAA,GAAG,SAAS,CAAC;AAc3C,QAAA,IAAI,CAAC,IAAI,CAAC,eAAe,EAAE;;;AAGzB,YAAA,IAAI,CAAC,eAAe,GAAG,IAAI,OAAO,CAAC,UAAU,EAAE,OAAO,EAAE,SAAS,CAAC,CAAC;AACpE,SAAA;QACD,IAAI,CAAC,IAAI,EAAE,CAAC;AACZ,QAAA,MAAM,MAAM,GAAG,IAAI,CAAC,aAAa,CAAC,YAAY,CAAC,OAAO,CAAC,IAAI,EAAE,CAAC;QAC9D,IAAI,CAAC,cAAc,GAAG,IAAI,CAAC,aAAa,CAAC,MAAM,CAAC,CAAC;QACjD,IAAI,CAAC,QAAQ,GAAG,YAAY,IAAI,gBAAgB,CAAC,UAAU,CAAC,CAAC;KAC9D;;AAGkB,IAAA,eAAe,CAAC,KAAU,EAAA;QAC3C,MAAM,MAAM,GAAG,IAAI,CAAC,aAAa,CAAC,KAAK,CAAC,CAAC;AACzC,QAAA,IAAI,CAAC,eAAe,CAAC,OAAO,GAAG,EAAC,GAAG,IAAI,CAAC,cAAc,EAAE,GAAG,MAAM,EAAC,CAAC;QACnE,IAAI,IAAI,CAAC,QAAQ,EAAE;AACjB,YAAA,IAAI,CAAC,mBAAmB,CAAC,MAAM,CAAC,CAAC;AAClC,SAAA;AACD,QAAA,IAAI,CAAC,eAAe,CAAC,SAAS,EAAE,CAAC;KAClC;;IAGkB,WAAW,GAAA;QAC5B,IAAI,CAAC,eAAe,CAAC,OAAO,GAAG,IAAI,CAAC,cAAc,CAAC;AACnD,QAAA,IAAI,CAAC,eAAe,CAAC,SAAS,EAAE,CAAC;KAClC;AAED;;;;;AAKG;AACO,IAAA,aAAa,CAAC,MAAmB,EAAA;;QAEzC,MAAM,SAAS,GAAqB,CAAC,GAAQ,KAC3C,IAAI,CAAC,SAAS,CAAC,QAAQ,CAAC,eAAe,CAAC,KAAK,EAAE,GAAG,CAAC,IAAI,EAAE,CAAC;AAC5D,QAAA,IAAI,MAAM,EAAE;AACV,YAAA,QAAQ,OAAO,CAAC,MAAM,CAAC;AACrB,gBAAA,KAAK,QAAQ,EAAG,OAAO,gBAAgB,CAAC,YAAY,CAAC,MAAM,CAAC,EAC1D,SAAS,CAAC,CAAC;gBACb,KAAK,OAAQ,EAAG,OAAO,gBAAgB,CAAC,MAAwB,EAAE,SAAS,CAAC,CAAC;gBAC7E,KAAK,KAAQ,EAAG,OAAO,eAAe,CAAC,MAAM,EAAE,SAAS,CAAC,CAAC;gBAC1D,SAAgB,OAAO,eAAe,CAAC,MAAM,EAAE,SAAS,CAAC,CAAC;AAC3D,aAAA;AACF,SAAA;AAED,QAAA,OAAO,EAAE,CAAC;KACX;;;;;IAOD,SAAS,GAAA;AACP,QAAA,IAAI,CAAC,eAAe,CAAC,SAAS,EAAE,CAAC;KAClC;;2GAzEU,cAAc,EAAA,IAAA,EAAA,CAAA,EAAA,KAAA,EAAA,EAAA,CAAA,UAAA,EAAA,EAAA,EAAA,KAAA,EAAA,EAAA,CAAA,UAAA,EAAA,EAAA,EAAA,KAAA,EAAA,EAAA,CAAA,eAAA,EAAA,EAAA,EAAA,KAAA,EAAAC,IAAA,CAAA,YAAA,EAAA,EAAA,EAAA,KAAA,EAAA,EAAA,CAAA,eAAA,EAAA,EAAA,EAAA,KAAA,EAAA,EAAA,CAAA,SAAA,EAAA,EAAA,EAAA,KAAA,EAAAC,EAAA,CAAA,OAAA,EAAA,QAAA,EAAA,IAAA,EAAA,IAAA,EAAA,IAAA,EAAA,EAAA,EAAA,KAAA,EAaL,YAAY,EAAA,EAAA,EAAA,KAAA,EACZ,WAAW,EAAA,CAAA,EAAA,MAAA,EAAA,EAAA,CAAA,eAAA,CAAA,SAAA,EAAA,CAAA,CAAA;+FAdpB,cAAc,EAAA,eAAA,EAAA,IAAA,EAAA,QAAA,EAAA,EAAA,EAAA,CAAA,CAAA;2FAAd,cAAc,EAAA,UAAA,EAAA,CAAA;kBAD1B,SAAS;;0BAaK,QAAQ;;0BAAI,IAAI;;0BAChB,MAAM;2BAAC,YAAY,CAAA;;0BACnB,MAAM;2BAAC,WAAW,CAAA;;AA8DjC,MAAM,MAAM,GAAG;IACb,SAAS;AACT,IAAA,YAAY,EAAE,YAAY,EAAE,YAAY,EAAE,YAAY,EAAE,YAAY;AACpE,IAAA,eAAe,EAAE,eAAe,EAAE,eAAe,EAAE,eAAe;AAClE,IAAA,eAAe,EAAE,eAAe,EAAE,eAAe,EAAE,eAAe;CACnE,CAAC;AAEF,MAAM,QAAQ,GAAG,CAAA;;;;;CAKhB,CAAC;AAEF;;;AAGG;AAEG,MAAO,qBAAsB,SAAQ,cAAc,CAAA;AADzD,IAAA,WAAA,GAAA;;QAEqB,IAAM,CAAA,MAAA,GAAG,MAAM,CAAC;AACpC,KAAA;;kHAFY,qBAAqB,EAAA,IAAA,EAAA,IAAA,EAAA,MAAA,EAAA,EAAA,CAAA,eAAA,CAAA,SAAA,EAAA,CAAA,CAAA;sGAArB,qBAAqB,EAAA,QAAA,EAAA,wOAAA,EAAA,MAAA,EAAA,EAAA,OAAA,EAAA,SAAA,EAAA,YAAA,EAAA,YAAA,EAAA,YAAA,EAAA,YAAA,EAAA,YAAA,EAAA,YAAA,EAAA,YAAA,EAAA,YAAA,EAAA,YAAA,EAAA,YAAA,EAAA,eAAA,EAAA,eAAA,EAAA,eAAA,EAAA,eAAA,EAAA,eAAA,EAAA,eAAA,EAAA,eAAA,EAAA,eAAA,EAAA,eAAA,EAAA,eAAA,EAAA,eAAA,EAAA,eAAA,EAAA,eAAA,EAAA,eAAA,EAAA,eAAA,EAAA,eAAA,EAAA,EAAA,eAAA,EAAA,IAAA,EAAA,QAAA,EAAA,EAAA,EAAA,CAAA,CAAA;2FAArB,qBAAqB,EAAA,UAAA,EAAA,CAAA;kBADjC,SAAS;mBAAC,EAAC,QAAQ,EAAE,MAAM,EAAC,CAAA;;AAK7B;AACA,SAAS,gBAAgB,CAAC,MAAsB,EAAE,QAA2B,EAAA;AAC3E,IAAA,MAAM,aAAa,GAAG,CAAC,EAAmB,KAAI;AAC5C,QAAA,IAAI,QAAQ,EAAE;YACZ,EAAE,CAAC,KAAK,GAAG,QAAQ,CAAC,EAAE,CAAC,KAAK,CAAC,CAAC;AAC/B,SAAA;AACD,QAAA,OAAO,EAAE,CAAC;AACZ,KAAC,CAAC;AAEF,IAAA,OAAO,MAAM;SACV,GAAG,CAAC,gBAAgB,CAAC;SACrB,MAAM,CAAC,KAAK,IAAI,CAAC,CAAC,KAAK,CAAC;SACxB,GAAG,CAAC,aAAa,CAAC;AAClB,SAAA,MAAM,CAAC,cAAc,EAAE,EAAgB,CAAC,CAAC;AAC9C;;AC3JA;;;;;;AAMG;AAUH,MAAM,cAAc,GAAG;IACrB,wBAAwB;IACxB,qBAAqB;IACrB,qBAAqB;IACrB,sBAAsB;CACvB,CAAC;AAEF;;;;AAIG;MAOU,cAAc,CAAA;;2GAAd,cAAc,EAAA,IAAA,EAAA,EAAA,EAAA,MAAA,EAAA,EAAA,CAAA,eAAA,CAAA,QAAA,EAAA,CAAA,CAAA;AAAd,cAAA,CAAA,IAAA,GAAA,EAAA,CAAA,mBAAA,CAAA,EAAA,UAAA,EAAA,QAAA,EAAA,OAAA,EAAA,QAAA,EAAA,QAAA,EAAA,EAAA,EAAA,IAAA,EAAA,cAAc,iBAjBzB,wBAAwB;QACxB,qBAAqB;QACrB,qBAAqB;QACrB,sBAAsB,CAAA,EAAA,OAAA,EAAA,CAUZ,UAAU,CAAA,EAAA,OAAA,EAAA,CAbpB,wBAAwB;QACxB,qBAAqB;QACrB,qBAAqB;QACrB,sBAAsB,CAAA,EAAA,CAAA,CAAA;AAcX,cAAA,CAAA,IAAA,GAAA,EAAA,CAAA,mBAAA,CAAA,EAAA,UAAA,EAAA,QAAA,EAAA,OAAA,EAAA,QAAA,EAAA,QAAA,EAAA,EAAA,EAAA,IAAA,EAAA,cAAc,YAJf,UAAU,CAAA,EAAA,CAAA,CAAA;2FAIT,cAAc,EAAA,UAAA,EAAA,CAAA;kBAL1B,QAAQ;AAAC,YAAA,IAAA,EAAA,CAAA;oBACR,OAAO,EAAE,CAAC,UAAU,CAAC;AACrB,oBAAA,YAAY,EAAE,CAAC,GAAG,cAAc,CAAC;AACjC,oBAAA,OAAO,EAAE,CAAC,GAAG,cAAc,CAAC;AAC7B,iBAAA,CAAA;;;ACjCD;;;;;;AAMG;;ACNH;;AAEG;;;;"} -\ No newline at end of file -+{"version":3,"file":"angular-flex-layout-extended.mjs","sources":["../../../../projects/libs/flex-layout/extended/img-src/img-src.ts","../../../../projects/libs/flex-layout/extended/class/class.ts","../../../../projects/libs/flex-layout/extended/show-hide/show-hide.ts","../../../../projects/libs/flex-layout/extended/style/style-transforms.ts","../../../../projects/libs/flex-layout/extended/style/style.ts","../../../../projects/libs/flex-layout/extended/module.ts","../../../../projects/libs/flex-layout/extended/public-api.ts","../../../../projects/libs/flex-layout/extended/angular-flex-layout-extended.ts"],"sourcesContent":["/**\n * @license\n * Copyright Google LLC All Rights Reserved.\n *\n * Use of this source code is governed by an MIT-style license that can be\n * found in the LICENSE file at https://angular.io/license\n */\nimport {Directive, ElementRef, Inject, PLATFORM_ID, Injectable, Input} from '@angular/core';\nimport {isPlatformServer} from '@angular/common';\nimport {\n MediaMarshaller,\n BaseDirective2,\n SERVER_TOKEN,\n StyleBuilder,\n StyleDefinition,\n StyleUtils,\n} from '@angular/flex-layout/core';\n\n@Injectable({providedIn: 'root'})\nexport class ImgSrcStyleBuilder extends StyleBuilder {\n buildStyles(url: string) {\n return {'content': url ? `url(${url})` : ''};\n }\n}\n\n@Directive()\nexport class ImgSrcDirective extends BaseDirective2 {\n protected override DIRECTIVE_KEY = 'img-src';\n protected defaultSrc = '';\n\n @Input('src')\n set src(val: string) {\n this.defaultSrc = val;\n this.setValue(this.defaultSrc, '');\n }\n\n constructor(elementRef: ElementRef,\n styleBuilder: ImgSrcStyleBuilder,\n styler: StyleUtils,\n marshal: MediaMarshaller,\n @Inject(PLATFORM_ID) protected platformId: Object,\n @Inject(SERVER_TOKEN) protected serverModuleLoaded: boolean) {\n super(elementRef, styleBuilder, styler, marshal);\n this.init();\n this.setValue(this.nativeElement.getAttribute('src') || '', '');\n if (isPlatformServer(this.platformId) && this.serverModuleLoaded) {\n this.nativeElement.setAttribute('src', '');\n }\n }\n\n /**\n * Use the [responsively] activated input value to update\n * the host img src attribute or assign a default `img.src=''`\n * if the src has not been defined.\n *\n * Do nothing to standard `` usages, only when responsive\n * keys are present do we actually call `setAttribute()`\n */\n protected override updateWithValue(value?: string) {\n const url = value || this.defaultSrc;\n if (isPlatformServer(this.platformId) && this.serverModuleLoaded) {\n this.addStyles(url);\n } else {\n this.nativeElement.setAttribute('src', url);\n }\n }\n\n protected override styleCache = imgSrcCache;\n}\n\nconst imgSrcCache: Map = new Map();\n\nconst inputs = [\n 'src.xs', 'src.sm', 'src.md', 'src.lg', 'src.xl',\n 'src.lt-sm', 'src.lt-md', 'src.lt-lg', 'src.lt-xl',\n 'src.gt-xs', 'src.gt-sm', 'src.gt-md', 'src.gt-lg'\n];\n\nconst selector = `\n img[src.xs], img[src.sm], img[src.md], img[src.lg], img[src.xl],\n img[src.lt-sm], img[src.lt-md], img[src.lt-lg], img[src.lt-xl],\n img[src.gt-xs], img[src.gt-sm], img[src.gt-md], img[src.gt-lg]\n`;\n\n/**\n * This directive provides a responsive API for the HTML 'src' attribute\n * and will update the img.src property upon each responsive activation.\n *\n * e.g.\n * \n *\n * @see https://css-tricks.com/responsive-images-youre-just-changing-resolutions-use-src/\n */\n@Directive({selector, inputs})\nexport class DefaultImgSrcDirective extends ImgSrcDirective {\n protected override inputs = inputs;\n}\n","/**\n * @license\n * Copyright Google LLC All Rights Reserved.\n *\n * Use of this source code is governed by an MIT-style license that can be\n * found in the LICENSE file at https://angular.io/license\n */\nimport {\n Directive,\n DoCheck,\n ElementRef,\n Input,\n IterableDiffers,\n KeyValueDiffers,\n Optional,\n Renderer2,\n Self,\n} from '@angular/core';\nimport {NgClass} from '@angular/common';\nimport {BaseDirective2, StyleUtils, MediaMarshaller} from '@angular/flex-layout/core';\n\n@Directive()\nexport class ClassDirective extends BaseDirective2 implements DoCheck {\n\n protected override DIRECTIVE_KEY = 'ngClass';\n\n /**\n * Capture class assignments so we cache the default classes\n * which are merged with activated styles and used as fallbacks.\n */\n @Input('class')\n set klass(val: string) {\n this.ngClassInstance.klass = val;\n this.setValue(val, '');\n }\n\n constructor(elementRef: ElementRef,\n styler: StyleUtils,\n marshal: MediaMarshaller,\n iterableDiffers: IterableDiffers,\n keyValueDiffers: KeyValueDiffers,\n renderer2: Renderer2,\n @Optional() @Self() protected readonly ngClassInstance: NgClass) {\n super(elementRef, null!, styler, marshal);\n if (!this.ngClassInstance) {\n // Create an instance NgClass Directive instance only if `ngClass=\"\"` has NOT been defined on\n // the same host element; since the responsive variations may be defined...\n this.ngClassInstance = new NgClass(elementRef, renderer2);\n }\n this.init();\n this.setValue('', '');\n }\n\n protected override updateWithValue(value: any) {\n this.ngClassInstance.ngClass = value;\n this.ngClassInstance.ngDoCheck();\n }\n\n // ******************************************************************\n // Lifecycle Hooks\n // ******************************************************************\n\n /**\n * For ChangeDetectionStrategy.onPush and ngOnChanges() updates\n */\n ngDoCheck() {\n this.ngClassInstance.ngDoCheck();\n }\n}\n\nconst inputs = [\n 'ngClass', 'ngClass.xs', 'ngClass.sm', 'ngClass.md', 'ngClass.lg', 'ngClass.xl',\n 'ngClass.lt-sm', 'ngClass.lt-md', 'ngClass.lt-lg', 'ngClass.lt-xl',\n 'ngClass.gt-xs', 'ngClass.gt-sm', 'ngClass.gt-md', 'ngClass.gt-lg'\n];\n\nconst selector = `\n [ngClass], [ngClass.xs], [ngClass.sm], [ngClass.md], [ngClass.lg], [ngClass.xl],\n [ngClass.lt-sm], [ngClass.lt-md], [ngClass.lt-lg], [ngClass.lt-xl],\n [ngClass.gt-xs], [ngClass.gt-sm], [ngClass.gt-md], [ngClass.gt-lg]\n`;\n\n/**\n * Directive to add responsive support for ngClass.\n * This maintains the core functionality of 'ngClass' and adds responsive API\n * Note: this class is a no-op when rendered on the server\n */\n@Directive({selector, inputs})\nexport class DefaultClassDirective extends ClassDirective {\n protected override inputs = inputs;\n}\n","/**\n * @license\n * Copyright Google LLC All Rights Reserved.\n *\n * Use of this source code is governed by an MIT-style license that can be\n * found in the LICENSE file at https://angular.io/license\n */\nimport {\n Directive,\n ElementRef,\n OnChanges,\n SimpleChanges,\n Inject,\n PLATFORM_ID,\n Injectable,\n AfterViewInit,\n} from '@angular/core';\nimport {isPlatformServer} from '@angular/common';\nimport {\n BaseDirective2,\n LAYOUT_CONFIG,\n LayoutConfigOptions,\n MediaMarshaller,\n SERVER_TOKEN,\n StyleUtils,\n StyleBuilder,\n} from '@angular/flex-layout/core';\nimport {coerceBooleanProperty} from '@angular/cdk/coercion';\nimport {takeUntil} from 'rxjs/operators';\n\nexport interface ShowHideParent {\n display: string;\n isServer: boolean;\n}\n\n@Injectable({providedIn: 'root'})\nexport class ShowHideStyleBuilder extends StyleBuilder {\n buildStyles(show: string, parent: ShowHideParent) {\n const shouldShow = show === 'true';\n return {'display': shouldShow ? parent.display || (parent.isServer ? 'initial' : '') : 'none'};\n }\n}\n\n@Directive()\nexport class ShowHideDirective extends BaseDirective2 implements AfterViewInit, OnChanges {\n protected override DIRECTIVE_KEY = 'show-hide';\n\n /** Original DOM Element CSS display style */\n protected display: string = '';\n protected hasLayout = false;\n protected hasFlexChild = false;\n\n constructor(elementRef: ElementRef,\n styleBuilder: ShowHideStyleBuilder,\n styler: StyleUtils,\n marshal: MediaMarshaller,\n @Inject(LAYOUT_CONFIG) protected layoutConfig: LayoutConfigOptions,\n @Inject(PLATFORM_ID) protected platformId: Object,\n @Inject(SERVER_TOKEN) protected serverModuleLoaded: boolean) {\n super(elementRef, styleBuilder, styler, marshal);\n }\n\n // *********************************************\n // Lifecycle Methods\n // *********************************************\n\n ngAfterViewInit() {\n this.trackExtraTriggers();\n\n const children = Array.from(this.nativeElement.children);\n for (let i = 0; i < children.length; i++) {\n if (this.marshal.hasValue(children[i] as HTMLElement, 'flex')) {\n this.hasFlexChild = true;\n break;\n }\n }\n\n if (DISPLAY_MAP.has(this.nativeElement)) {\n this.display = DISPLAY_MAP.get(this.nativeElement)!;\n } else {\n this.display = this.getDisplayStyle();\n DISPLAY_MAP.set(this.nativeElement, this.display);\n }\n\n this.init();\n // set the default to show unless explicitly overridden\n const defaultValue = this.marshal.getValue(this.nativeElement, this.DIRECTIVE_KEY, '');\n if (defaultValue === undefined || defaultValue === '') {\n this.setValue(true, '');\n } else {\n this.triggerUpdate();\n }\n }\n\n /**\n * On changes to any @Input properties...\n * Default to use the non-responsive Input value ('fxShow')\n * Then conditionally override with the mq-activated Input's current value\n */\n override ngOnChanges(changes: SimpleChanges) {\n Object.keys(changes).forEach(key => {\n if (this.inputs.indexOf(key) !== -1) {\n const inputKey = key.split('.');\n const bp = inputKey.slice(1).join('.');\n const inputValue = changes[key].currentValue;\n let shouldShow = inputValue !== '' ?\n inputValue !== 0 ? coerceBooleanProperty(inputValue) : false\n : true;\n if (inputKey[0] === 'fxHide') {\n shouldShow = !shouldShow;\n }\n this.setValue(shouldShow, bp);\n }\n });\n }\n\n // *********************************************\n // Protected methods\n // *********************************************\n\n /**\n * Watch for these extra triggers to update fxShow, fxHide stylings\n */\n protected trackExtraTriggers() {\n this.hasLayout = this.marshal.hasValue(this.nativeElement, 'layout');\n\n ['layout', 'layout-align'].forEach(key => {\n this.marshal\n .trackValue(this.nativeElement, key)\n .pipe(takeUntil(this.destroySubject))\n .subscribe(this.triggerUpdate.bind(this));\n });\n }\n\n /**\n * Override accessor to the current HTMLElement's `display` style\n * Note: Show/Hide will not change the display to 'flex' but will set it to 'block'\n * unless it was already explicitly specified inline or in a CSS stylesheet.\n */\n protected getDisplayStyle(): string {\n return (this.hasLayout || (this.hasFlexChild && this.layoutConfig.addFlexToParent)) ?\n 'flex' : this.styler.lookupStyle(this.nativeElement, 'display', true);\n }\n\n /** Validate the visibility value and then update the host's inline display style */\n protected override updateWithValue(value: boolean | string = true) {\n if (value === '') {\n return;\n }\n const isServer = isPlatformServer(this.platformId);\n this.addStyles(value ? 'true' : 'false', {display: this.display, isServer});\n if (isServer && this.serverModuleLoaded) {\n this.nativeElement.style.setProperty('display', '');\n }\n this.marshal.triggerUpdate(this.parentElement!, 'layout-gap');\n }\n}\n\nconst DISPLAY_MAP: WeakMap = new WeakMap();\n\nconst inputs = [\n 'fxShow', 'fxShow.print',\n 'fxShow.xs', 'fxShow.sm', 'fxShow.md', 'fxShow.lg', 'fxShow.xl',\n 'fxShow.lt-sm', 'fxShow.lt-md', 'fxShow.lt-lg', 'fxShow.lt-xl',\n 'fxShow.gt-xs', 'fxShow.gt-sm', 'fxShow.gt-md', 'fxShow.gt-lg',\n 'fxHide', 'fxHide.print',\n 'fxHide.xs', 'fxHide.sm', 'fxHide.md', 'fxHide.lg', 'fxHide.xl',\n 'fxHide.lt-sm', 'fxHide.lt-md', 'fxHide.lt-lg', 'fxHide.lt-xl',\n 'fxHide.gt-xs', 'fxHide.gt-sm', 'fxHide.gt-md', 'fxHide.gt-lg'\n];\n\nconst selector = `\n [fxShow], [fxShow.print],\n [fxShow.xs], [fxShow.sm], [fxShow.md], [fxShow.lg], [fxShow.xl],\n [fxShow.lt-sm], [fxShow.lt-md], [fxShow.lt-lg], [fxShow.lt-xl],\n [fxShow.gt-xs], [fxShow.gt-sm], [fxShow.gt-md], [fxShow.gt-lg],\n [fxHide], [fxHide.print],\n [fxHide.xs], [fxHide.sm], [fxHide.md], [fxHide.lg], [fxHide.xl],\n [fxHide.lt-sm], [fxHide.lt-md], [fxHide.lt-lg], [fxHide.lt-xl],\n [fxHide.gt-xs], [fxHide.gt-sm], [fxHide.gt-md], [fxHide.gt-lg]\n`;\n\n/**\n * 'show' Layout API directive\n */\n@Directive({selector, inputs})\nexport class DefaultShowHideDirective extends ShowHideDirective {\n protected override inputs = inputs;\n}\n","/**\n * @license\n * Copyright Google LLC All Rights Reserved.\n *\n * Use of this source code is governed by an MIT-style license that can be\n * found in the LICENSE file at https://angular.io/license\n */\n\nexport type NgStyleRawList = string[];\nexport type NgStyleMap = {[klass: string]: string};\n// NgStyle selectors accept NgStyleType values\nexport type NgStyleType = string | Set | NgStyleRawList | NgStyleMap;\n\n/**\n * Callback function for SecurityContext.STYLE sanitization\n */\nexport type NgStyleSanitizer = (val: any) => string;\n\n/** NgStyle allowed inputs */\nexport class NgStyleKeyValue {\n constructor(public key: string, public value: string, noQuotes = true) {\n this.key = noQuotes ? key.replace(/['\"]/g, '').trim() : key.trim();\n\n this.value = noQuotes ? value.replace(/['\"]/g, '').trim() : value.trim();\n this.value = this.value.replace(/;/, '');\n }\n}\n\nexport function getType(target: any): string {\n let what = typeof target;\n if (what === 'object') {\n return (target.constructor === Array) ? 'array' :\n (target.constructor === Set) ? 'set' : 'object';\n }\n return what;\n}\n\n/**\n * Split string of key:value pairs into Array of k-v pairs\n * e.g. 'key:value; key:value; key:value;' -> ['key:value',...]\n */\nexport function buildRawList(source: any, delimiter = ';'): NgStyleRawList {\n return String(source)\n .trim()\n .split(delimiter)\n .map((val: string) => val.trim())\n .filter(val => val !== '');\n}\n\n/** Convert array of key:value strings to a iterable map object */\nexport function buildMapFromList(styles: NgStyleRawList, sanitize?: NgStyleSanitizer): NgStyleMap {\n const sanitizeValue = (it: NgStyleKeyValue) => {\n if (sanitize) {\n it.value = sanitize(it.value);\n }\n return it;\n };\n\n return styles\n .map(stringToKeyValue)\n .filter(entry => !!entry)\n .map(sanitizeValue)\n .reduce(keyValuesToMap, {} as NgStyleMap);\n}\n\n/** Convert Set or raw Object to an iterable NgStyleMap */\nexport function buildMapFromSet(source: NgStyleType, sanitize?: NgStyleSanitizer): NgStyleMap {\n let list: string[] = [];\n if (getType(source) === 'set') {\n (source as Set).forEach(entry => list.push(entry));\n } else {\n Object.keys(source).forEach((key: string) => {\n list.push(`${key}:${(source as NgStyleMap)[key]}`);\n });\n }\n return buildMapFromList(list, sanitize);\n}\n\n\n/** Convert 'key:value' -> [key, value] */\nexport function stringToKeyValue(it: string): NgStyleKeyValue {\n const [key, ...vals] = it.split(':');\n return new NgStyleKeyValue(key, vals.join(':'));\n}\n\n/** Convert [ [key,value] ] -> { key : value } */\nexport function keyValuesToMap(map: NgStyleMap, entry: NgStyleKeyValue): NgStyleMap {\n if (!!entry.key) {\n map[entry.key] = entry.value;\n }\n return map;\n}\n","/**\n * @license\n * Copyright Google LLC All Rights Reserved.\n *\n * Use of this source code is governed by an MIT-style license that can be\n * found in the LICENSE file at https://angular.io/license\n */\nimport {\n Directive,\n DoCheck,\n ElementRef,\n Inject,\n KeyValueDiffers,\n Optional,\n PLATFORM_ID,\n Renderer2,\n SecurityContext,\n Self,\n} from '@angular/core';\nimport {isPlatformServer, NgStyle} from '@angular/common';\nimport {DomSanitizer} from '@angular/platform-browser';\nimport {\n BaseDirective2,\n StyleUtils,\n MediaMarshaller,\n SERVER_TOKEN,\n} from '@angular/flex-layout/core';\n\nimport {\n NgStyleRawList,\n NgStyleType,\n NgStyleSanitizer,\n buildRawList,\n getType,\n buildMapFromSet,\n NgStyleMap,\n NgStyleKeyValue,\n stringToKeyValue,\n keyValuesToMap,\n} from './style-transforms';\n\n@Directive()\nexport class StyleDirective extends BaseDirective2 implements DoCheck {\n\n protected override DIRECTIVE_KEY = 'ngStyle';\n protected fallbackStyles: NgStyleMap;\n protected isServer: boolean;\n\n constructor(elementRef: ElementRef,\n styler: StyleUtils,\n marshal: MediaMarshaller,\n protected sanitizer: DomSanitizer,\n differs: KeyValueDiffers,\n renderer2: Renderer2,\n @Optional() @Self() private readonly ngStyleInstance: NgStyle,\n @Inject(SERVER_TOKEN) serverLoaded: boolean,\n @Inject(PLATFORM_ID) platformId: Object) {\n super(elementRef, null!, styler, marshal);\n if (!this.ngStyleInstance) {\n // Create an instance NgStyle Directive instance only if `ngStyle=\"\"` has NOT been\n // defined on the same host element; since the responsive variations may be defined...\n this.ngStyleInstance = new NgStyle(elementRef, differs, renderer2);\n }\n this.init();\n const styles = this.nativeElement.getAttribute('style') ?? '';\n this.fallbackStyles = this.buildStyleMap(styles);\n this.isServer = serverLoaded && isPlatformServer(platformId);\n }\n\n /** Add generated styles */\n protected override updateWithValue(value: any) {\n const styles = this.buildStyleMap(value);\n this.ngStyleInstance.ngStyle = {...this.fallbackStyles, ...styles};\n if (this.isServer) {\n this.applyStyleToElement(styles);\n }\n this.ngStyleInstance.ngDoCheck();\n }\n\n /** Remove generated styles */\n protected override clearStyles() {\n this.ngStyleInstance.ngStyle = this.fallbackStyles;\n this.ngStyleInstance.ngDoCheck();\n }\n\n /**\n * Convert raw strings to ngStyleMap; which is required by ngStyle\n * NOTE: Raw string key-value pairs MUST be delimited by `;`\n * Comma-delimiters are not supported due to complexities of\n * possible style values such as `rgba(x,x,x,x)` and others\n */\n protected buildStyleMap(styles: NgStyleType): NgStyleMap {\n // Always safe-guard (aka sanitize) style property values\n const sanitizer: NgStyleSanitizer = (val: any) =>\n this.sanitizer.sanitize(SecurityContext.STYLE, val) ?? '';\n if (styles) {\n switch (getType(styles)) {\n case 'string': return buildMapFromList(buildRawList(styles),\n sanitizer);\n case 'array' : return buildMapFromList(styles as NgStyleRawList, sanitizer);\n case 'set' : return buildMapFromSet(styles, sanitizer);\n default : return buildMapFromSet(styles, sanitizer);\n }\n }\n\n return {};\n }\n\n // ******************************************************************\n // Lifecycle Hooks\n // ******************************************************************\n\n /** For ChangeDetectionStrategy.onPush and ngOnChanges() updates */\n ngDoCheck() {\n this.ngStyleInstance.ngDoCheck();\n }\n}\n\nconst inputs = [\n 'ngStyle',\n 'ngStyle.xs', 'ngStyle.sm', 'ngStyle.md', 'ngStyle.lg', 'ngStyle.xl',\n 'ngStyle.lt-sm', 'ngStyle.lt-md', 'ngStyle.lt-lg', 'ngStyle.lt-xl',\n 'ngStyle.gt-xs', 'ngStyle.gt-sm', 'ngStyle.gt-md', 'ngStyle.gt-lg'\n];\n\nconst selector = `\n [ngStyle],\n [ngStyle.xs], [ngStyle.sm], [ngStyle.md], [ngStyle.lg], [ngStyle.xl],\n [ngStyle.lt-sm], [ngStyle.lt-md], [ngStyle.lt-lg], [ngStyle.lt-xl],\n [ngStyle.gt-xs], [ngStyle.gt-sm], [ngStyle.gt-md], [ngStyle.gt-lg]\n`;\n\n/**\n * Directive to add responsive support for ngStyle.\n *\n */\n@Directive({selector, inputs})\nexport class DefaultStyleDirective extends StyleDirective implements DoCheck {\n protected override inputs = inputs;\n}\n\n/** Build a styles map from a list of styles, while sanitizing bad values first */\nfunction buildMapFromList(styles: NgStyleRawList, sanitize?: NgStyleSanitizer): NgStyleMap {\n const sanitizeValue = (it: NgStyleKeyValue) => {\n if (sanitize) {\n it.value = sanitize(it.value);\n }\n return it;\n };\n\n return styles\n .map(stringToKeyValue)\n .filter(entry => !!entry)\n .map(sanitizeValue)\n .reduce(keyValuesToMap, {} as NgStyleMap);\n}\n","/**\n * @license\n * Copyright Google LLC All Rights Reserved.\n *\n * Use of this source code is governed by an MIT-style license that can be\n * found in the LICENSE file at https://angular.io/license\n */\nimport {NgModule} from '@angular/core';\nimport {CoreModule} from '@angular/flex-layout/core';\n\nimport {DefaultImgSrcDirective} from './img-src/img-src';\nimport {DefaultClassDirective} from './class/class';\nimport {DefaultShowHideDirective} from './show-hide/show-hide';\nimport {DefaultStyleDirective} from './style/style';\n\n\nconst ALL_DIRECTIVES = [\n DefaultShowHideDirective,\n DefaultClassDirective,\n DefaultStyleDirective,\n DefaultImgSrcDirective,\n];\n\n/**\n * *****************************************************************\n * Define module for the Extended API\n * *****************************************************************\n */\n\n@NgModule({\n imports: [CoreModule],\n declarations: [...ALL_DIRECTIVES],\n exports: [...ALL_DIRECTIVES]\n})\nexport class ExtendedModule {\n}\n","/**\n * @license\n * Copyright Google LLC All Rights Reserved.\n *\n * Use of this source code is governed by an MIT-style license that can be\n * found in the LICENSE file at https://angular.io/license\n */\n\nexport * from './module';\n\nexport * from './class/class';\nexport * from './img-src/img-src';\nexport * from './show-hide/show-hide';\nexport * from './style/style';\n","/**\n * Generated bundle index. Do not edit.\n */\n\nexport * from './public-api';\n"],"names":["inputs","selector","buildMapFromList","i2","i3"],"mappings":";;;;;;;;;;AAAA;;;;;;AAMG;AAaG,MAAO,kBAAmB,SAAQ,YAAY,CAAA;AAClD,IAAA,WAAW,CAAC,GAAW,EAAA;AACrB,QAAA,OAAO,EAAC,SAAS,EAAE,GAAG,GAAG,CAAO,IAAA,EAAA,GAAG,GAAG,GAAG,EAAE,EAAC,CAAC;KAC9C;;+GAHU,kBAAkB,EAAA,IAAA,EAAA,IAAA,EAAA,MAAA,EAAA,EAAA,CAAA,eAAA,CAAA,UAAA,EAAA,CAAA,CAAA;AAAlB,kBAAA,CAAA,KAAA,GAAA,EAAA,CAAA,qBAAA,CAAA,EAAA,UAAA,EAAA,QAAA,EAAA,OAAA,EAAA,QAAA,EAAA,QAAA,EAAA,EAAA,EAAA,IAAA,EAAA,kBAAkB,cADN,MAAM,EAAA,CAAA,CAAA;2FAClB,kBAAkB,EAAA,UAAA,EAAA,CAAA;kBAD9B,UAAU;mBAAC,EAAC,UAAU,EAAE,MAAM,EAAC,CAAA;;AAQ1B,MAAO,eAAgB,SAAQ,cAAc,CAAA;IAUjD,WAAY,CAAA,UAAsB,EACtB,YAAgC,EAChC,MAAkB,EAClB,OAAwB,EACO,UAAkB,EACjB,kBAA2B,EAAA;QACrE,KAAK,CAAC,UAAU,EAAE,YAAY,EAAE,MAAM,EAAE,OAAO,CAAC,CAAC;QAFR,IAAU,CAAA,UAAA,GAAV,UAAU,CAAQ;QACjB,IAAkB,CAAA,kBAAA,GAAlB,kBAAkB,CAAS;QAdpD,IAAa,CAAA,aAAA,GAAG,SAAS,CAAC;QACnC,IAAU,CAAA,UAAA,GAAG,EAAE,CAAC;QAuCP,IAAU,CAAA,UAAA,GAAG,WAAW,CAAC;QAxB1C,IAAI,CAAC,IAAI,EAAE,CAAC;AACZ,QAAA,IAAI,CAAC,QAAQ,CAAC,IAAI,CAAC,aAAa,CAAC,YAAY,CAAC,KAAK,CAAC,IAAI,EAAE,EAAE,EAAE,CAAC,CAAC;QAChE,IAAI,gBAAgB,CAAC,IAAI,CAAC,UAAU,CAAC,IAAI,IAAI,CAAC,kBAAkB,EAAE;YAChE,IAAI,CAAC,aAAa,CAAC,YAAY,CAAC,KAAK,EAAE,EAAE,CAAC,CAAC;AAC5C,SAAA;KACF;IAlBD,IACI,GAAG,CAAC,GAAW,EAAA;AACjB,QAAA,IAAI,CAAC,UAAU,GAAG,GAAG,CAAC;QACtB,IAAI,CAAC,QAAQ,CAAC,IAAI,CAAC,UAAU,EAAE,EAAE,CAAC,CAAC;KACpC;AAgBD;;;;;;;AAOG;AACgB,IAAA,eAAe,CAAC,KAAc,EAAA;AAC/C,QAAA,MAAM,GAAG,GAAG,KAAK,IAAI,IAAI,CAAC,UAAU,CAAC;QACrC,IAAI,gBAAgB,CAAC,IAAI,CAAC,UAAU,CAAC,IAAI,IAAI,CAAC,kBAAkB,EAAE;AAChE,YAAA,IAAI,CAAC,SAAS,CAAC,GAAG,CAAC,CAAC;AACrB,SAAA;AAAM,aAAA;YACL,IAAI,CAAC,aAAa,CAAC,YAAY,CAAC,KAAK,EAAE,GAAG,CAAC,CAAC;AAC7C,SAAA;KACF;;4GAvCU,eAAe,EAAA,IAAA,EAAA,CAAA,EAAA,KAAA,EAAA,EAAA,CAAA,UAAA,EAAA,EAAA,EAAA,KAAA,EAAA,kBAAA,EAAA,EAAA,EAAA,KAAA,EAAA,EAAA,CAAA,UAAA,EAAA,EAAA,EAAA,KAAA,EAAA,EAAA,CAAA,eAAA,EAAA,EAAA,EAAA,KAAA,EAcN,WAAW,EAAA,EAAA,EAAA,KAAA,EACX,YAAY,EAAA,CAAA,EAAA,MAAA,EAAA,EAAA,CAAA,eAAA,CAAA,SAAA,EAAA,CAAA,CAAA;gGAfrB,eAAe,EAAA,MAAA,EAAA,EAAA,GAAA,EAAA,KAAA,EAAA,EAAA,eAAA,EAAA,IAAA,EAAA,QAAA,EAAA,EAAA,EAAA,CAAA,CAAA;2FAAf,eAAe,EAAA,UAAA,EAAA,CAAA;kBAD3B,SAAS;;0BAeK,MAAM;2BAAC,WAAW,CAAA;;0BAClB,MAAM;2BAAC,YAAY,CAAA;4CAV5B,GAAG,EAAA,CAAA;sBADN,KAAK;uBAAC,KAAK,CAAA;;AAwCd,MAAM,WAAW,GAAiC,IAAI,GAAG,EAAE,CAAC;AAE5D,MAAMA,QAAM,GAAG;AACb,IAAA,QAAQ,EAAE,QAAQ,EAAE,QAAQ,EAAE,QAAQ,EAAE,QAAQ;AAChD,IAAA,WAAW,EAAE,WAAW,EAAE,WAAW,EAAE,WAAW;AAClD,IAAA,WAAW,EAAE,WAAW,EAAE,WAAW,EAAE,WAAW;CACnD,CAAC;AAEF,MAAMC,UAAQ,GAAG,CAAA;;;;CAIhB,CAAC;AAEF;;;;;;;;AAQG;AAEG,MAAO,sBAAuB,SAAQ,eAAe,CAAA;AAD3D,IAAA,WAAA,GAAA;;QAEqB,IAAM,CAAA,MAAA,GAAGD,QAAM,CAAC;AACpC,KAAA;;mHAFY,sBAAsB,EAAA,IAAA,EAAA,IAAA,EAAA,MAAA,EAAA,EAAA,CAAA,eAAA,CAAA,SAAA,EAAA,CAAA,CAAA;uGAAtB,sBAAsB,EAAA,QAAA,EAAA,wNAAA,EAAA,MAAA,EAAA,EAAA,QAAA,EAAA,QAAA,EAAA,QAAA,EAAA,QAAA,EAAA,QAAA,EAAA,QAAA,EAAA,QAAA,EAAA,QAAA,EAAA,QAAA,EAAA,QAAA,EAAA,WAAA,EAAA,WAAA,EAAA,WAAA,EAAA,WAAA,EAAA,WAAA,EAAA,WAAA,EAAA,WAAA,EAAA,WAAA,EAAA,WAAA,EAAA,WAAA,EAAA,WAAA,EAAA,WAAA,EAAA,WAAA,EAAA,WAAA,EAAA,WAAA,EAAA,WAAA,EAAA,EAAA,eAAA,EAAA,IAAA,EAAA,QAAA,EAAA,EAAA,EAAA,CAAA,CAAA;2FAAtB,sBAAsB,EAAA,UAAA,EAAA,CAAA;kBADlC,SAAS;mBAAC,YAACC,UAAQ,UAAED,QAAM,EAAC,CAAA;;;AC7F7B;;;;;;AAMG;AAgBG,MAAO,cAAe,SAAQ,cAAc,CAAA;AAchD,IAAA,WAAA,CAAY,UAAsB,EACtB,MAAkB,EAClB,OAAwB,EACxB,eAAgC,EAChC,eAAgC,EAChC,SAAoB,EACmB,eAAwB,EAAA;QACzE,KAAK,CAAC,UAAU,EAAE,IAAK,EAAE,MAAM,EAAE,OAAO,CAAC,CAAC;QADO,IAAe,CAAA,eAAA,GAAf,eAAe,CAAS;QAlBxD,IAAa,CAAA,aAAA,GAAG,SAAS,CAAC;AAoB3C,QAAA,IAAI,CAAC,IAAI,CAAC,eAAe,EAAE;;;AAGzB,YAAA,IAAI,CAAC,eAAe,GAAG,IAAI,OAAO,CAAC,eAAe,EAAE,eAAe,EAAE,UAAU,EAAE,SAAS,CAAC,CAAC;AAC7F,SAAA;QACD,IAAI,CAAC,IAAI,EAAE,CAAC;AACZ,QAAA,IAAI,CAAC,QAAQ,CAAC,EAAE,EAAE,EAAE,CAAC,CAAC;KACvB;AAzBD;;;AAGG;IACH,IACI,KAAK,CAAC,GAAW,EAAA;AACnB,QAAA,IAAI,CAAC,eAAe,CAAC,KAAK,GAAG,GAAG,CAAC;AACjC,QAAA,IAAI,CAAC,QAAQ,CAAC,GAAG,EAAE,EAAE,CAAC,CAAC;KACxB;AAmBkB,IAAA,eAAe,CAAC,KAAU,EAAA;AAC3C,QAAA,IAAI,CAAC,eAAe,CAAC,OAAO,GAAG,KAAK,CAAC;AACrC,QAAA,IAAI,CAAC,eAAe,CAAC,SAAS,EAAE,CAAC;KAClC;;;;AAMD;;AAEG;IACH,SAAS,GAAA;AACP,QAAA,IAAI,CAAC,eAAe,CAAC,SAAS,EAAE,CAAC;KAClC;;2GA7CU,cAAc,EAAA,IAAA,EAAA,CAAA,EAAA,KAAA,EAAA,EAAA,CAAA,UAAA,EAAA,EAAA,EAAA,KAAA,EAAA,EAAA,CAAA,UAAA,EAAA,EAAA,EAAA,KAAA,EAAA,EAAA,CAAA,eAAA,EAAA,EAAA,EAAA,KAAA,EAAA,EAAA,CAAA,eAAA,EAAA,EAAA,EAAA,KAAA,EAAA,EAAA,CAAA,eAAA,EAAA,EAAA,EAAA,KAAA,EAAA,EAAA,CAAA,SAAA,EAAA,EAAA,EAAA,KAAA,EAAA,EAAA,CAAA,OAAA,EAAA,QAAA,EAAA,IAAA,EAAA,IAAA,EAAA,IAAA,EAAA,CAAA,EAAA,MAAA,EAAA,EAAA,CAAA,eAAA,CAAA,SAAA,EAAA,CAAA,CAAA;+FAAd,cAAc,EAAA,MAAA,EAAA,EAAA,KAAA,EAAA,CAAA,OAAA,EAAA,OAAA,CAAA,EAAA,EAAA,eAAA,EAAA,IAAA,EAAA,QAAA,EAAA,EAAA,EAAA,CAAA,CAAA;2FAAd,cAAc,EAAA,UAAA,EAAA,CAAA;kBAD1B,SAAS;;0BAqBK,QAAQ;;0BAAI,IAAI;4CAXzB,KAAK,EAAA,CAAA;sBADR,KAAK;uBAAC,OAAO,CAAA;;AAwChB,MAAMA,QAAM,GAAG;IACb,SAAS,EAAE,YAAY,EAAE,YAAY,EAAE,YAAY,EAAE,YAAY,EAAE,YAAY;AAC/E,IAAA,eAAe,EAAE,eAAe,EAAE,eAAe,EAAE,eAAe;AAClE,IAAA,eAAe,EAAE,eAAe,EAAE,eAAe,EAAE,eAAe;CACnE,CAAC;AAEF,MAAMC,UAAQ,GAAG,CAAA;;;;CAIhB,CAAC;AAEF;;;;AAIG;AAEG,MAAO,qBAAsB,SAAQ,cAAc,CAAA;AADzD,IAAA,WAAA,GAAA;;QAEqB,IAAM,CAAA,MAAA,GAAGD,QAAM,CAAC;AACpC,KAAA;;kHAFY,qBAAqB,EAAA,IAAA,EAAA,IAAA,EAAA,MAAA,EAAA,EAAA,CAAA,eAAA,CAAA,SAAA,EAAA,CAAA,CAAA;sGAArB,qBAAqB,EAAA,QAAA,EAAA,qOAAA,EAAA,MAAA,EAAA,EAAA,OAAA,EAAA,SAAA,EAAA,YAAA,EAAA,YAAA,EAAA,YAAA,EAAA,YAAA,EAAA,YAAA,EAAA,YAAA,EAAA,YAAA,EAAA,YAAA,EAAA,YAAA,EAAA,YAAA,EAAA,eAAA,EAAA,eAAA,EAAA,eAAA,EAAA,eAAA,EAAA,eAAA,EAAA,eAAA,EAAA,eAAA,EAAA,eAAA,EAAA,eAAA,EAAA,eAAA,EAAA,eAAA,EAAA,eAAA,EAAA,eAAA,EAAA,eAAA,EAAA,eAAA,EAAA,eAAA,EAAA,EAAA,eAAA,EAAA,IAAA,EAAA,QAAA,EAAA,EAAA,EAAA,CAAA,CAAA;2FAArB,qBAAqB,EAAA,UAAA,EAAA,CAAA;kBADjC,SAAS;mBAAC,YAACC,UAAQ,UAAED,QAAM,EAAC,CAAA;;;ACvF7B;;;;;;AAMG;AA8BG,MAAO,oBAAqB,SAAQ,YAAY,CAAA;IACpD,WAAW,CAAC,IAAY,EAAE,MAAsB,EAAA;AAC9C,QAAA,MAAM,UAAU,GAAG,IAAI,KAAK,MAAM,CAAC;AACnC,QAAA,OAAO,EAAC,SAAS,EAAE,UAAU,GAAG,MAAM,CAAC,OAAO,KAAK,MAAM,CAAC,QAAQ,GAAG,SAAS,GAAG,EAAE,CAAC,GAAG,MAAM,EAAC,CAAC;KAChG;;iHAJU,oBAAoB,EAAA,IAAA,EAAA,IAAA,EAAA,MAAA,EAAA,EAAA,CAAA,eAAA,CAAA,UAAA,EAAA,CAAA,CAAA;AAApB,oBAAA,CAAA,KAAA,GAAA,EAAA,CAAA,qBAAA,CAAA,EAAA,UAAA,EAAA,QAAA,EAAA,OAAA,EAAA,QAAA,EAAA,QAAA,EAAA,EAAA,EAAA,IAAA,EAAA,oBAAoB,cADR,MAAM,EAAA,CAAA,CAAA;2FAClB,oBAAoB,EAAA,UAAA,EAAA,CAAA;kBADhC,UAAU;mBAAC,EAAC,UAAU,EAAE,MAAM,EAAC,CAAA;;AAS1B,MAAO,iBAAkB,SAAQ,cAAc,CAAA;AAQnD,IAAA,WAAA,CAAY,UAAsB,EACtB,YAAkC,EAClC,MAAkB,EAClB,OAAwB,EACS,YAAiC,EACnC,UAAkB,EACjB,kBAA2B,EAAA;QACrE,KAAK,CAAC,UAAU,EAAE,YAAY,EAAE,MAAM,EAAE,OAAO,CAAC,CAAC;QAHN,IAAY,CAAA,YAAA,GAAZ,YAAY,CAAqB;QACnC,IAAU,CAAA,UAAA,GAAV,UAAU,CAAQ;QACjB,IAAkB,CAAA,kBAAA,GAAlB,kBAAkB,CAAS;QAbpD,IAAa,CAAA,aAAA,GAAG,WAAW,CAAC;;QAGrC,IAAO,CAAA,OAAA,GAAW,EAAE,CAAC;QACrB,IAAS,CAAA,SAAA,GAAG,KAAK,CAAC;QAClB,IAAY,CAAA,YAAA,GAAG,KAAK,CAAC;KAU9B;;;;IAMD,eAAe,GAAA;QACb,IAAI,CAAC,kBAAkB,EAAE,CAAC;AAE1B,QAAA,MAAM,QAAQ,GAAG,KAAK,CAAC,IAAI,CAAC,IAAI,CAAC,aAAa,CAAC,QAAQ,CAAC,CAAC;AACzD,QAAA,KAAK,IAAI,CAAC,GAAG,CAAC,EAAE,CAAC,GAAG,QAAQ,CAAC,MAAM,EAAE,CAAC,EAAE,EAAE;AACxC,YAAA,IAAI,IAAI,CAAC,OAAO,CAAC,QAAQ,CAAC,QAAQ,CAAC,CAAC,CAAgB,EAAE,MAAM,CAAC,EAAE;AAC7D,gBAAA,IAAI,CAAC,YAAY,GAAG,IAAI,CAAC;gBACzB,MAAM;AACP,aAAA;AACF,SAAA;QAED,IAAI,WAAW,CAAC,GAAG,CAAC,IAAI,CAAC,aAAa,CAAC,EAAE;YACvC,IAAI,CAAC,OAAO,GAAG,WAAW,CAAC,GAAG,CAAC,IAAI,CAAC,aAAa,CAAE,CAAC;AACrD,SAAA;AAAM,aAAA;AACL,YAAA,IAAI,CAAC,OAAO,GAAG,IAAI,CAAC,eAAe,EAAE,CAAC;YACtC,WAAW,CAAC,GAAG,CAAC,IAAI,CAAC,aAAa,EAAE,IAAI,CAAC,OAAO,CAAC,CAAC;AACnD,SAAA;QAED,IAAI,CAAC,IAAI,EAAE,CAAC;;AAEZ,QAAA,MAAM,YAAY,GAAG,IAAI,CAAC,OAAO,CAAC,QAAQ,CAAC,IAAI,CAAC,aAAa,EAAE,IAAI,CAAC,aAAa,EAAE,EAAE,CAAC,CAAC;AACvF,QAAA,IAAI,YAAY,KAAK,SAAS,IAAI,YAAY,KAAK,EAAE,EAAE;AACrD,YAAA,IAAI,CAAC,QAAQ,CAAC,IAAI,EAAE,EAAE,CAAC,CAAC;AACzB,SAAA;AAAM,aAAA;YACL,IAAI,CAAC,aAAa,EAAE,CAAC;AACtB,SAAA;KACF;AAED;;;;AAIG;AACM,IAAA,WAAW,CAAC,OAAsB,EAAA;QACzC,MAAM,CAAC,IAAI,CAAC,OAAO,CAAC,CAAC,OAAO,CAAC,GAAG,IAAG;YACjC,IAAI,IAAI,CAAC,MAAM,CAAC,OAAO,CAAC,GAAG,CAAC,KAAK,CAAC,CAAC,EAAE;gBACnC,MAAM,QAAQ,GAAG,GAAG,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC;AAChC,gBAAA,MAAM,EAAE,GAAG,QAAQ,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC;gBACvC,MAAM,UAAU,GAAG,OAAO,CAAC,GAAG,CAAC,CAAC,YAAY,CAAC;AAC7C,gBAAA,IAAI,UAAU,GAAG,UAAU,KAAK,EAAE;AAC9B,oBAAA,UAAU,KAAK,CAAC,GAAG,qBAAqB,CAAC,UAAU,CAAC,GAAG,KAAK;sBAC1D,IAAI,CAAC;AACX,gBAAA,IAAI,QAAQ,CAAC,CAAC,CAAC,KAAK,QAAQ,EAAE;oBAC5B,UAAU,GAAG,CAAC,UAAU,CAAC;AAC1B,iBAAA;AACD,gBAAA,IAAI,CAAC,QAAQ,CAAC,UAAU,EAAE,EAAE,CAAC,CAAC;AAC/B,aAAA;AACH,SAAC,CAAC,CAAC;KACJ;;;;AAMD;;AAEG;IACO,kBAAkB,GAAA;AAC1B,QAAA,IAAI,CAAC,SAAS,GAAG,IAAI,CAAC,OAAO,CAAC,QAAQ,CAAC,IAAI,CAAC,aAAa,EAAE,QAAQ,CAAC,CAAC;QAErE,CAAC,QAAQ,EAAE,cAAc,CAAC,CAAC,OAAO,CAAC,GAAG,IAAG;AACvC,YAAA,IAAI,CAAC,OAAO;AACP,iBAAA,UAAU,CAAC,IAAI,CAAC,aAAa,EAAE,GAAG,CAAC;AACnC,iBAAA,IAAI,CAAC,SAAS,CAAC,IAAI,CAAC,cAAc,CAAC,CAAC;iBACpC,SAAS,CAAC,IAAI,CAAC,aAAa,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC,CAAC;AAChD,SAAC,CAAC,CAAC;KACJ;AAED;;;;AAIG;IACO,eAAe,GAAA;AACvB,QAAA,OAAO,CAAC,IAAI,CAAC,SAAS,KAAK,IAAI,CAAC,YAAY,IAAI,IAAI,CAAC,YAAY,CAAC,eAAe,CAAC;AAC9E,YAAA,MAAM,GAAG,IAAI,CAAC,MAAM,CAAC,WAAW,CAAC,IAAI,CAAC,aAAa,EAAE,SAAS,EAAE,IAAI,CAAC,CAAC;KAC3E;;IAGkB,eAAe,CAAC,QAA0B,IAAI,EAAA;QAC/D,IAAI,KAAK,KAAK,EAAE,EAAE;YAChB,OAAO;AACR,SAAA;QACD,MAAM,QAAQ,GAAG,gBAAgB,CAAC,IAAI,CAAC,UAAU,CAAC,CAAC;QACnD,IAAI,CAAC,SAAS,CAAC,KAAK,GAAG,MAAM,GAAG,OAAO,EAAE,EAAC,OAAO,EAAE,IAAI,CAAC,OAAO,EAAE,QAAQ,EAAC,CAAC,CAAC;AAC5E,QAAA,IAAI,QAAQ,IAAI,IAAI,CAAC,kBAAkB,EAAE;YACvC,IAAI,CAAC,aAAa,CAAC,KAAK,CAAC,WAAW,CAAC,SAAS,EAAE,EAAE,CAAC,CAAC;AACrD,SAAA;QACD,IAAI,CAAC,OAAO,CAAC,aAAa,CAAC,IAAI,CAAC,aAAc,EAAE,YAAY,CAAC,CAAC;KAC/D;;AA/GU,iBAAA,CAAA,IAAA,GAAA,EAAA,CAAA,kBAAA,CAAA,EAAA,UAAA,EAAA,QAAA,EAAA,OAAA,EAAA,QAAA,EAAA,QAAA,EAAA,EAAA,EAAA,IAAA,EAAA,iBAAiB,EAYR,IAAA,EAAA,CAAA,EAAA,KAAA,EAAA,EAAA,CAAA,UAAA,EAAA,EAAA,EAAA,KAAA,EAAA,oBAAA,EAAA,EAAA,EAAA,KAAA,EAAA,EAAA,CAAA,UAAA,EAAA,EAAA,EAAA,KAAA,EAAA,EAAA,CAAA,eAAA,EAAA,EAAA,EAAA,KAAA,EAAA,aAAa,EACb,EAAA,EAAA,KAAA,EAAA,WAAW,aACX,YAAY,EAAA,CAAA,EAAA,MAAA,EAAA,EAAA,CAAA,eAAA,CAAA,SAAA,EAAA,CAAA,CAAA;kGAdrB,iBAAiB,EAAA,eAAA,EAAA,IAAA,EAAA,aAAA,EAAA,IAAA,EAAA,QAAA,EAAA,EAAA,EAAA,CAAA,CAAA;2FAAjB,iBAAiB,EAAA,UAAA,EAAA,CAAA;kBAD7B,SAAS;;0BAaK,MAAM;2BAAC,aAAa,CAAA;;0BACpB,MAAM;2BAAC,WAAW,CAAA;;0BAClB,MAAM;2BAAC,YAAY,CAAA;;AAoGlC,MAAM,WAAW,GAAiC,IAAI,OAAO,EAAE,CAAC;AAEhE,MAAMA,QAAM,GAAG;AACb,IAAA,QAAQ,EAAE,cAAc;AACxB,IAAA,WAAW,EAAE,WAAW,EAAE,WAAW,EAAE,WAAW,EAAE,WAAW;AAC/D,IAAA,cAAc,EAAE,cAAc,EAAE,cAAc,EAAE,cAAc;AAC9D,IAAA,cAAc,EAAE,cAAc,EAAE,cAAc,EAAE,cAAc;AAC9D,IAAA,QAAQ,EAAE,cAAc;AACxB,IAAA,WAAW,EAAE,WAAW,EAAE,WAAW,EAAE,WAAW,EAAE,WAAW;AAC/D,IAAA,cAAc,EAAE,cAAc,EAAE,cAAc,EAAE,cAAc;AAC9D,IAAA,cAAc,EAAE,cAAc,EAAE,cAAc,EAAE,cAAc;CAC/D,CAAC;AAEF,MAAMC,UAAQ,GAAG,CAAA;;;;;;;;;CAShB,CAAC;AAEF;;AAEG;AAEG,MAAO,wBAAyB,SAAQ,iBAAiB,CAAA;AAD/D,IAAA,WAAA,GAAA;;QAEqB,IAAM,CAAA,MAAA,GAAGD,QAAM,CAAC;AACpC,KAAA;;qHAFY,wBAAwB,EAAA,IAAA,EAAA,IAAA,EAAA,MAAA,EAAA,EAAA,CAAA,eAAA,CAAA,SAAA,EAAA,CAAA,CAAA;yGAAxB,wBAAwB,EAAA,QAAA,EAAA,idAAA,EAAA,MAAA,EAAA,EAAA,MAAA,EAAA,QAAA,EAAA,cAAA,EAAA,cAAA,EAAA,WAAA,EAAA,WAAA,EAAA,WAAA,EAAA,WAAA,EAAA,WAAA,EAAA,WAAA,EAAA,WAAA,EAAA,WAAA,EAAA,WAAA,EAAA,WAAA,EAAA,cAAA,EAAA,cAAA,EAAA,cAAA,EAAA,cAAA,EAAA,cAAA,EAAA,cAAA,EAAA,cAAA,EAAA,cAAA,EAAA,cAAA,EAAA,cAAA,EAAA,cAAA,EAAA,cAAA,EAAA,cAAA,EAAA,cAAA,EAAA,cAAA,EAAA,cAAA,EAAA,MAAA,EAAA,QAAA,EAAA,cAAA,EAAA,cAAA,EAAA,WAAA,EAAA,WAAA,EAAA,WAAA,EAAA,WAAA,EAAA,WAAA,EAAA,WAAA,EAAA,WAAA,EAAA,WAAA,EAAA,WAAA,EAAA,WAAA,EAAA,cAAA,EAAA,cAAA,EAAA,cAAA,EAAA,cAAA,EAAA,cAAA,EAAA,cAAA,EAAA,cAAA,EAAA,cAAA,EAAA,cAAA,EAAA,cAAA,EAAA,cAAA,EAAA,cAAA,EAAA,cAAA,EAAA,cAAA,EAAA,cAAA,EAAA,cAAA,EAAA,EAAA,eAAA,EAAA,IAAA,EAAA,QAAA,EAAA,EAAA,EAAA,CAAA,CAAA;2FAAxB,wBAAwB,EAAA,UAAA,EAAA,CAAA;kBADpC,SAAS;mBAAC,YAACC,UAAQ,UAAED,QAAM,EAAC,CAAA;;;ACzL7B;;;;;;AAMG;AAYH;MACa,eAAe,CAAA;AAC1B,IAAA,WAAA,CAAmB,GAAW,EAAS,KAAa,EAAE,QAAQ,GAAG,IAAI,EAAA;QAAlD,IAAG,CAAA,GAAA,GAAH,GAAG,CAAQ;QAAS,IAAK,CAAA,KAAA,GAAL,KAAK,CAAQ;QAClD,IAAI,CAAC,GAAG,GAAG,QAAQ,GAAG,GAAG,CAAC,OAAO,CAAC,OAAO,EAAE,EAAE,CAAC,CAAC,IAAI,EAAE,GAAG,GAAG,CAAC,IAAI,EAAE,CAAC;QAEnE,IAAI,CAAC,KAAK,GAAG,QAAQ,GAAG,KAAK,CAAC,OAAO,CAAC,OAAO,EAAE,EAAE,CAAC,CAAC,IAAI,EAAE,GAAG,KAAK,CAAC,IAAI,EAAE,CAAC;AACzE,QAAA,IAAI,CAAC,KAAK,GAAG,IAAI,CAAC,KAAK,CAAC,OAAO,CAAC,GAAG,EAAE,EAAE,CAAC,CAAC;KAC1C;AACF,CAAA;AAEK,SAAU,OAAO,CAAC,MAAW,EAAA;AACjC,IAAA,IAAI,IAAI,GAAG,OAAO,MAAM,CAAC;IACzB,IAAI,IAAI,KAAK,QAAQ,EAAE;AACrB,QAAA,OAAO,CAAC,MAAM,CAAC,WAAW,KAAK,KAAK,IAAI,OAAO;AAC3C,YAAA,CAAC,MAAM,CAAC,WAAW,KAAK,GAAG,IAAI,KAAK,GAAG,QAAQ,CAAC;AACrD,KAAA;AACD,IAAA,OAAO,IAAI,CAAC;AACd,CAAC;AAED;;;AAGG;SACa,YAAY,CAAC,MAAW,EAAE,SAAS,GAAG,GAAG,EAAA;IACvD,OAAO,MAAM,CAAC,MAAM,CAAC;AAChB,SAAA,IAAI,EAAE;SACN,KAAK,CAAC,SAAS,CAAC;SAChB,GAAG,CAAC,CAAC,GAAW,KAAK,GAAG,CAAC,IAAI,EAAE,CAAC;SAChC,MAAM,CAAC,GAAG,IAAI,GAAG,KAAK,EAAE,CAAC,CAAC;AACjC,CAAC;AAED;AACgB,SAAAE,kBAAgB,CAAC,MAAsB,EAAE,QAA2B,EAAA;AAClF,IAAA,MAAM,aAAa,GAAG,CAAC,EAAmB,KAAI;AAC5C,QAAA,IAAI,QAAQ,EAAE;YACZ,EAAE,CAAC,KAAK,GAAG,QAAQ,CAAC,EAAE,CAAC,KAAK,CAAC,CAAC;AAC/B,SAAA;AACD,QAAA,OAAO,EAAE,CAAC;AACZ,KAAC,CAAC;AAEF,IAAA,OAAO,MAAM;SACR,GAAG,CAAC,gBAAgB,CAAC;SACrB,MAAM,CAAC,KAAK,IAAI,CAAC,CAAC,KAAK,CAAC;SACxB,GAAG,CAAC,aAAa,CAAC;AAClB,SAAA,MAAM,CAAC,cAAc,EAAE,EAAgB,CAAC,CAAC;AAChD,CAAC;AAED;AACgB,SAAA,eAAe,CAAC,MAAmB,EAAE,QAA2B,EAAA;IAC9E,IAAI,IAAI,GAAa,EAAE,CAAC;AACxB,IAAA,IAAI,OAAO,CAAC,MAAM,CAAC,KAAK,KAAK,EAAE;AAC5B,QAAA,MAAsB,CAAC,OAAO,CAAC,KAAK,IAAI,IAAI,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC,CAAC;AAC5D,KAAA;AAAM,SAAA;QACL,MAAM,CAAC,IAAI,CAAC,MAAM,CAAC,CAAC,OAAO,CAAC,CAAC,GAAW,KAAI;AAC1C,YAAA,IAAI,CAAC,IAAI,CAAC,CAAA,EAAG,GAAG,CAAA,CAAA,EAAK,MAAqB,CAAC,GAAG,CAAC,CAAE,CAAA,CAAC,CAAC;AACrD,SAAC,CAAC,CAAC;AACJ,KAAA;AACD,IAAA,OAAOA,kBAAgB,CAAC,IAAI,EAAE,QAAQ,CAAC,CAAC;AAC1C,CAAC;AAGD;AACM,SAAU,gBAAgB,CAAC,EAAU,EAAA;AACzC,IAAA,MAAM,CAAC,GAAG,EAAE,GAAG,IAAI,CAAC,GAAG,EAAE,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC;AACrC,IAAA,OAAO,IAAI,eAAe,CAAC,GAAG,EAAE,IAAI,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC,CAAC;AAClD,CAAC;AAED;AACgB,SAAA,cAAc,CAAC,GAAe,EAAE,KAAsB,EAAA;AACpE,IAAA,IAAI,CAAC,CAAC,KAAK,CAAC,GAAG,EAAE;QACf,GAAG,CAAC,KAAK,CAAC,GAAG,CAAC,GAAG,KAAK,CAAC,KAAK,CAAC;AAC9B,KAAA;AACD,IAAA,OAAO,GAAG,CAAC;AACb;;AC3FA;;;;;;AAMG;AAoCG,MAAO,cAAe,SAAQ,cAAc,CAAA;AAMhD,IAAA,WAAA,CAAY,UAAsB,EACtB,MAAkB,EAClB,OAAwB,EACd,SAAuB,EACjC,OAAwB,EACxB,SAAoB,EACiB,eAAwB,EACvC,YAAqB,EACtB,UAAkB,EAAA;QACjD,KAAK,CAAC,UAAU,EAAE,IAAK,EAAE,MAAM,EAAE,OAAO,CAAC,CAAC;QANtB,IAAS,CAAA,SAAA,GAAT,SAAS,CAAc;QAGI,IAAe,CAAA,eAAA,GAAf,eAAe,CAAS;QAVtD,IAAa,CAAA,aAAA,GAAG,SAAS,CAAC;AAc3C,QAAA,IAAI,CAAC,IAAI,CAAC,eAAe,EAAE;;;AAGzB,YAAA,IAAI,CAAC,eAAe,GAAG,IAAI,OAAO,CAAC,UAAU,EAAE,OAAO,EAAE,SAAS,CAAC,CAAC;AACpE,SAAA;QACD,IAAI,CAAC,IAAI,EAAE,CAAC;AACZ,QAAA,MAAM,MAAM,GAAG,IAAI,CAAC,aAAa,CAAC,YAAY,CAAC,OAAO,CAAC,IAAI,EAAE,CAAC;QAC9D,IAAI,CAAC,cAAc,GAAG,IAAI,CAAC,aAAa,CAAC,MAAM,CAAC,CAAC;QACjD,IAAI,CAAC,QAAQ,GAAG,YAAY,IAAI,gBAAgB,CAAC,UAAU,CAAC,CAAC;KAC9D;;AAGkB,IAAA,eAAe,CAAC,KAAU,EAAA;QAC3C,MAAM,MAAM,GAAG,IAAI,CAAC,aAAa,CAAC,KAAK,CAAC,CAAC;AACzC,QAAA,IAAI,CAAC,eAAe,CAAC,OAAO,GAAG,EAAC,GAAG,IAAI,CAAC,cAAc,EAAE,GAAG,MAAM,EAAC,CAAC;QACnE,IAAI,IAAI,CAAC,QAAQ,EAAE;AACjB,YAAA,IAAI,CAAC,mBAAmB,CAAC,MAAM,CAAC,CAAC;AAClC,SAAA;AACD,QAAA,IAAI,CAAC,eAAe,CAAC,SAAS,EAAE,CAAC;KAClC;;IAGkB,WAAW,GAAA;QAC5B,IAAI,CAAC,eAAe,CAAC,OAAO,GAAG,IAAI,CAAC,cAAc,CAAC;AACnD,QAAA,IAAI,CAAC,eAAe,CAAC,SAAS,EAAE,CAAC;KAClC;AAED;;;;;AAKG;AACO,IAAA,aAAa,CAAC,MAAmB,EAAA;;QAEzC,MAAM,SAAS,GAAqB,CAAC,GAAQ,KAC3C,IAAI,CAAC,SAAS,CAAC,QAAQ,CAAC,eAAe,CAAC,KAAK,EAAE,GAAG,CAAC,IAAI,EAAE,CAAC;AAC5D,QAAA,IAAI,MAAM,EAAE;AACV,YAAA,QAAQ,OAAO,CAAC,MAAM,CAAC;AACrB,gBAAA,KAAK,QAAQ,EAAG,OAAO,gBAAgB,CAAC,YAAY,CAAC,MAAM,CAAC,EAC1D,SAAS,CAAC,CAAC;gBACb,KAAK,OAAQ,EAAG,OAAO,gBAAgB,CAAC,MAAwB,EAAE,SAAS,CAAC,CAAC;gBAC7E,KAAK,KAAQ,EAAG,OAAO,eAAe,CAAC,MAAM,EAAE,SAAS,CAAC,CAAC;gBAC1D,SAAgB,OAAO,eAAe,CAAC,MAAM,EAAE,SAAS,CAAC,CAAC;AAC3D,aAAA;AACF,SAAA;AAED,QAAA,OAAO,EAAE,CAAC;KACX;;;;;IAOD,SAAS,GAAA;AACP,QAAA,IAAI,CAAC,eAAe,CAAC,SAAS,EAAE,CAAC;KAClC;;2GAzEU,cAAc,EAAA,IAAA,EAAA,CAAA,EAAA,KAAA,EAAA,EAAA,CAAA,UAAA,EAAA,EAAA,EAAA,KAAA,EAAA,EAAA,CAAA,UAAA,EAAA,EAAA,EAAA,KAAA,EAAA,EAAA,CAAA,eAAA,EAAA,EAAA,EAAA,KAAA,EAAAC,IAAA,CAAA,YAAA,EAAA,EAAA,EAAA,KAAA,EAAA,EAAA,CAAA,eAAA,EAAA,EAAA,EAAA,KAAA,EAAA,EAAA,CAAA,SAAA,EAAA,EAAA,EAAA,KAAA,EAAAC,EAAA,CAAA,OAAA,EAAA,QAAA,EAAA,IAAA,EAAA,IAAA,EAAA,IAAA,EAAA,EAAA,EAAA,KAAA,EAaL,YAAY,EAAA,EAAA,EAAA,KAAA,EACZ,WAAW,EAAA,CAAA,EAAA,MAAA,EAAA,EAAA,CAAA,eAAA,CAAA,SAAA,EAAA,CAAA,CAAA;+FAdpB,cAAc,EAAA,eAAA,EAAA,IAAA,EAAA,QAAA,EAAA,EAAA,EAAA,CAAA,CAAA;2FAAd,cAAc,EAAA,UAAA,EAAA,CAAA;kBAD1B,SAAS;;0BAaK,QAAQ;;0BAAI,IAAI;;0BAChB,MAAM;2BAAC,YAAY,CAAA;;0BACnB,MAAM;2BAAC,WAAW,CAAA;;AA8DjC,MAAM,MAAM,GAAG;IACb,SAAS;AACT,IAAA,YAAY,EAAE,YAAY,EAAE,YAAY,EAAE,YAAY,EAAE,YAAY;AACpE,IAAA,eAAe,EAAE,eAAe,EAAE,eAAe,EAAE,eAAe;AAClE,IAAA,eAAe,EAAE,eAAe,EAAE,eAAe,EAAE,eAAe;CACnE,CAAC;AAEF,MAAM,QAAQ,GAAG,CAAA;;;;;CAKhB,CAAC;AAEF;;;AAGG;AAEG,MAAO,qBAAsB,SAAQ,cAAc,CAAA;AADzD,IAAA,WAAA,GAAA;;QAEqB,IAAM,CAAA,MAAA,GAAG,MAAM,CAAC;AACpC,KAAA;;kHAFY,qBAAqB,EAAA,IAAA,EAAA,IAAA,EAAA,MAAA,EAAA,EAAA,CAAA,eAAA,CAAA,SAAA,EAAA,CAAA,CAAA;sGAArB,qBAAqB,EAAA,QAAA,EAAA,wOAAA,EAAA,MAAA,EAAA,EAAA,OAAA,EAAA,SAAA,EAAA,YAAA,EAAA,YAAA,EAAA,YAAA,EAAA,YAAA,EAAA,YAAA,EAAA,YAAA,EAAA,YAAA,EAAA,YAAA,EAAA,YAAA,EAAA,YAAA,EAAA,eAAA,EAAA,eAAA,EAAA,eAAA,EAAA,eAAA,EAAA,eAAA,EAAA,eAAA,EAAA,eAAA,EAAA,eAAA,EAAA,eAAA,EAAA,eAAA,EAAA,eAAA,EAAA,eAAA,EAAA,eAAA,EAAA,eAAA,EAAA,eAAA,EAAA,eAAA,EAAA,EAAA,eAAA,EAAA,IAAA,EAAA,QAAA,EAAA,EAAA,EAAA,CAAA,CAAA;2FAArB,qBAAqB,EAAA,UAAA,EAAA,CAAA;kBADjC,SAAS;mBAAC,EAAC,QAAQ,EAAE,MAAM,EAAC,CAAA;;AAK7B;AACA,SAAS,gBAAgB,CAAC,MAAsB,EAAE,QAA2B,EAAA;AAC3E,IAAA,MAAM,aAAa,GAAG,CAAC,EAAmB,KAAI;AAC5C,QAAA,IAAI,QAAQ,EAAE;YACZ,EAAE,CAAC,KAAK,GAAG,QAAQ,CAAC,EAAE,CAAC,KAAK,CAAC,CAAC;AAC/B,SAAA;AACD,QAAA,OAAO,EAAE,CAAC;AACZ,KAAC,CAAC;AAEF,IAAA,OAAO,MAAM;SACV,GAAG,CAAC,gBAAgB,CAAC;SACrB,MAAM,CAAC,KAAK,IAAI,CAAC,CAAC,KAAK,CAAC;SACxB,GAAG,CAAC,aAAa,CAAC;AAClB,SAAA,MAAM,CAAC,cAAc,EAAE,EAAgB,CAAC,CAAC;AAC9C;;AC3JA;;;;;;AAMG;AAUH,MAAM,cAAc,GAAG;IACrB,wBAAwB;IACxB,qBAAqB;IACrB,qBAAqB;IACrB,sBAAsB;CACvB,CAAC;AAEF;;;;AAIG;MAOU,cAAc,CAAA;;2GAAd,cAAc,EAAA,IAAA,EAAA,EAAA,EAAA,MAAA,EAAA,EAAA,CAAA,eAAA,CAAA,QAAA,EAAA,CAAA,CAAA;AAAd,cAAA,CAAA,IAAA,GAAA,EAAA,CAAA,mBAAA,CAAA,EAAA,UAAA,EAAA,QAAA,EAAA,OAAA,EAAA,QAAA,EAAA,QAAA,EAAA,EAAA,EAAA,IAAA,EAAA,cAAc,iBAjBzB,wBAAwB;QACxB,qBAAqB;QACrB,qBAAqB;QACrB,sBAAsB,CAAA,EAAA,OAAA,EAAA,CAUZ,UAAU,CAAA,EAAA,OAAA,EAAA,CAbpB,wBAAwB;QACxB,qBAAqB;QACrB,qBAAqB;QACrB,sBAAsB,CAAA,EAAA,CAAA,CAAA;AAcX,cAAA,CAAA,IAAA,GAAA,EAAA,CAAA,mBAAA,CAAA,EAAA,UAAA,EAAA,QAAA,EAAA,OAAA,EAAA,QAAA,EAAA,QAAA,EAAA,EAAA,EAAA,IAAA,EAAA,cAAc,YAJf,UAAU,CAAA,EAAA,CAAA,CAAA;2FAIT,cAAc,EAAA,UAAA,EAAA,CAAA;kBAL1B,QAAQ;AAAC,YAAA,IAAA,EAAA,CAAA;oBACR,OAAO,EAAE,CAAC,UAAU,CAAC;AACrB,oBAAA,YAAY,EAAE,CAAC,GAAG,cAAc,CAAC;AACjC,oBAAA,OAAO,EAAE,CAAC,GAAG,cAAc,CAAC;AAC7B,iBAAA,CAAA;;;ACjCD;;;;;;AAMG;;ACNH;;AAEG;;;;"} diff --git a/ui-ngx/src/app/core/services/dynamic-component-factory.service.ts b/ui-ngx/src/app/core/services/dynamic-component-factory.service.ts index 3dd7f2a055..75bfa6b1e1 100644 --- a/ui-ngx/src/app/core/services/dynamic-component-factory.service.ts +++ b/ui-ngx/src/app/core/services/dynamic-component-factory.service.ts @@ -15,11 +15,10 @@ /// import { Component, Injectable, Type, ɵComponentDef, ɵNG_COMP_DEF } from '@angular/core'; -import { forkJoin, from, Observable, of } from 'rxjs'; +import { from, Observable, of } from 'rxjs'; import { CommonModule } from '@angular/common'; import { mergeMap } from 'rxjs/operators'; import { guid } from '@core/utils'; -import { getFlexLayoutModule } from '@shared/legacy/flex-layout.models'; @Injectable({ providedIn: 'root' @@ -35,9 +34,9 @@ export class DynamicComponentFactoryService { imports?: Type[], preserveWhitespaces?: boolean, styles?: string[]): Observable> { - return forkJoin({flexLayoutModule: getFlexLayoutModule(), compiler: from(import('@angular/compiler'))}).pipe( - mergeMap((data) => { - let componentImports: Type[] = [CommonModule, data.flexLayoutModule]; + return from(import('@angular/compiler')).pipe( + mergeMap(() => { + let componentImports: Type[] = [CommonModule]; if (imports) { componentImports = [...componentImports, ...imports]; } diff --git a/ui-ngx/src/app/core/services/resources.service.ts b/ui-ngx/src/app/core/services/resources.service.ts index e884fb59ab..a50f445266 100644 --- a/ui-ngx/src/app/core/services/resources.service.ts +++ b/ui-ngx/src/app/core/services/resources.service.ts @@ -38,7 +38,6 @@ import { selectIsAuthenticated } from '@core/auth/auth.selectors'; import { AppState } from '@core/core.state'; import { map, tap } from 'rxjs/operators'; import { RequestConfig } from '@core/http/http-utils'; -import { getFlexLayoutModule } from '@app/shared/legacy/flex-layout.models'; import { isJSResource, removeTbResourcePrefix } from '@shared/models/resource.models'; export interface ModuleInfo { @@ -206,42 +205,25 @@ export class ResourcesService { const subject = new ReplaySubject(); this.loadedModulesWithComponents[url] = subject; - forkJoin( - [ - modulesMap.init(), - from(import('@angular/compiler')) - ] - ).subscribe( + forkJoin([ + modulesMap.init(), + from(import('@angular/compiler')) + ]).subscribe( () => { // @ts-ignore System.import(url, undefined, meta).then( (module: any) => { try { const modulesWithComponents = this.extractModulesWithComponents(module); - this.patchModulesWithFlexLayout(modulesWithComponents).subscribe( - { - next: modules => { - if (modules.modules.length || modules.standaloneComponents.length) { - try { - for (const module of modules.modules) { - createNgModule(module.module.type, this.injector); - } - this.loadedModulesWithComponents[url].next(modulesWithComponents); - this.loadedModulesWithComponents[url].complete(); - } catch (e) { - console.log(`Unable to parse module from url: ${url}`, e); - this.loadedModulesWithComponents[url].error(new Error(`Unable to parse module from url: ${url}`)); - } - } else { - this.loadedModulesWithComponents[url].error(new Error(`Module '${url}' doesn't have exported modules or components!`)); - } - }, - error: err => { - console.log(`Unable to patch module with flexLayout, module url: ${url}`, err); - this.loadedModulesWithComponents[url].error(new Error(`Unable to patch module with flexLayout, module url: ${url}`)); - } + if (modulesWithComponents.modules.length || modulesWithComponents.standaloneComponents.length) { + for (const module of modulesWithComponents.modules) { + createNgModule(module.module.type, this.injector); } - ); + this.loadedModulesWithComponents[url].next(modulesWithComponents); + this.loadedModulesWithComponents[url].complete(); + } else { + this.loadedModulesWithComponents[url].error(new Error(`Module '${url}' doesn't have exported modules or components!`)); + } } catch (e) { console.log(`Unable to parse module from url: ${url}`, e); this.loadedModulesWithComponents[url].error(new Error(`Unable to parse module from url: ${url}`)); @@ -343,40 +325,6 @@ export class ResourcesService { return modulesWithComponents; } - private patchModulesWithFlexLayout(modulesWithComponents: ModulesWithComponents): Observable { - return getFlexLayoutModule().pipe( - map((flexLayoutModule) => { - modulesWithComponents.modules.forEach(m => { - if (Array.isArray(m.module.imports)) { - if (!m.module.imports.includes(flexLayoutModule)) { - m.module.imports.push(flexLayoutModule); - } - } else { - const imports = m.module.imports(); - if (!imports.includes(flexLayoutModule)) { - imports.push(flexLayoutModule); - m.module.imports = imports; - } - } - }); - modulesWithComponents.standaloneComponents.forEach(c => { - if (Array.isArray(c.dependencies)) { - if (!c.dependencies.includes(flexLayoutModule)) { - c.dependencies.push(flexLayoutModule); - } - } else { - const dependencies = c.dependencies(); - if (!dependencies.includes(flexLayoutModule)) { - dependencies.push(flexLayoutModule); - c.dependencies = dependencies; - } - } - }); - return modulesWithComponents; - }) - ); - } - private loadResourceByType(type: 'css' | 'js', url: string): Observable { const subject = new ReplaySubject(); this.loadedResources[url] = subject; diff --git a/ui-ngx/src/app/modules/common/modules-map.ts b/ui-ngx/src/app/modules/common/modules-map.ts index 6ba51ee872..c1b4fdfaa6 100644 --- a/ui-ngx/src/app/modules/common/modules-map.ts +++ b/ui-ngx/src/app/modules/common/modules-map.ts @@ -338,8 +338,7 @@ import * as AggregationOptionsConfigComponent from '@shared/components/time/aggr import * as IntervalOptionsConfigPanelComponent from '@shared/components/time/interval-options-config-panel.component'; import { IModulesMap } from '@modules/common/modules-map.models'; -import { Observable, map, of } from 'rxjs'; -import { getFlexLayout } from '@shared/legacy/flex-layout.models'; +import { Observable, of } from 'rxjs'; import { isJSResourceUrl } from '@shared/public-api'; class ModulesMap implements IModulesMap { @@ -352,10 +351,6 @@ class ModulesMap implements IModulesMap { '@angular/common': AngularCommon, '@angular/common/http': HttpClientModule, '@angular/forms': AngularForms, - '@angular/flex-layout': {}, - '@angular/flex-layout/flex': {}, - '@angular/flex-layout/grid': {}, - '@angular/flex-layout/extended': {}, '@angular/platform-browser': AngularPlatformBrowser, '@angular/platform-browser/animations': AngularPlatformBrowserAnimations, '@angular/router': AngularRouter, @@ -678,39 +673,30 @@ class ModulesMap implements IModulesMap { init(): Observable { if (!this.initialized) { - return getFlexLayout().pipe( - map((flexLayout) => { - this.modulesMap['@angular/flex-layout'] = flexLayout; - this.modulesMap['@angular/flex-layout/flex'] = flexLayout; - this.modulesMap['@angular/flex-layout/grid'] = flexLayout; - this.modulesMap['@angular/flex-layout/extended'] = flexLayout; - System.constructor.prototype.resolve = (id: string) => { - try { - if (this.modulesMap[id]) { - return 'app:' + id; - } else { - return id; - } - } catch (err) { - return id; - } - }; - for (const moduleId of Object.keys(this.modulesMap)) { - System.set('app:' + moduleId, this.modulesMap[moduleId]); + System.constructor.prototype.resolve = (id: string) => { + try { + if (this.modulesMap[id]) { + return 'app:' + id; + } else { + return id; } - System.constructor.prototype.shouldFetch = (url: string) => url.endsWith('/download') || isJSResourceUrl(url); - System.constructor.prototype.fetch = (url: string, options: RequestInit & {meta?: any}) => { - if (options?.meta?.additionalHeaders) { - options.headers = { ...options.headers, ...options.meta.additionalHeaders }; - } - return fetch(url, options); - }; - this.initialized = true; - }) - ); - } else { - return of(null); + } catch (err) { + return id; + } + }; + for (const moduleId of Object.keys(this.modulesMap)) { + System.set('app:' + moduleId, this.modulesMap[moduleId]); + } + System.constructor.prototype.shouldFetch = (url: string) => url.endsWith('/download') || isJSResourceUrl(url); + System.constructor.prototype.fetch = (url: string, options: RequestInit & {meta?: any}) => { + if (options?.meta?.additionalHeaders) { + options.headers = { ...options.headers, ...options.meta.additionalHeaders }; + } + return fetch(url, options); + }; + this.initialized = true; } + return of(null); } } diff --git a/ui-ngx/src/app/shared/legacy/flex-layout.models.ts b/ui-ngx/src/app/shared/legacy/flex-layout.models.ts deleted file mode 100644 index 4038fee802..0000000000 --- a/ui-ngx/src/app/shared/legacy/flex-layout.models.ts +++ /dev/null @@ -1,41 +0,0 @@ -/// -/// 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 { Observable } from 'rxjs/internal/Observable'; -import { from, of } from 'rxjs'; -import { map, tap } from 'rxjs/operators'; -import { Type } from '@angular/core'; - -let flexLayoutModule: any; - -export function getFlexLayout(): Observable { - if (flexLayoutModule) { - return of(flexLayoutModule); - } else { - return from(import('@angular/flex-layout')).pipe( - tap((module) => { - module.DEFAULT_CONFIG.addFlexToParent = false; - flexLayoutModule = module; - }) - ); - } -} - -export function getFlexLayoutModule(): Observable> { - return getFlexLayout().pipe( - map(module => module.FlexLayoutModule) - ); -} diff --git a/ui-ngx/yarn.lock b/ui-ngx/yarn.lock index 01a0062a39..31fa4696f6 100644 --- a/ui-ngx/yarn.lock +++ b/ui-ngx/yarn.lock @@ -307,13 +307,6 @@ dependencies: tslib "^2.3.0" -"@angular/flex-layout@^15.0.0-beta.42": - version "15.0.0-beta.42" - resolved "https://registry.yarnpkg.com/@angular/flex-layout/-/flex-layout-15.0.0-beta.42.tgz#ad5e1dda32ee6280ba73765be10fd916c222e38e" - integrity sha512-cTAPVMMxnyIFwpZwdq0PL5mdP9Qh+R8MB7ZBezVaN3Rz2fRrkagzKpLvPX3TFzepXrvHBdpKsU4b8u+NxEC/6g== - dependencies: - tslib "^2.3.0" - "@angular/forms@18.2.13": version "18.2.13" resolved "https://registry.yarnpkg.com/@angular/forms/-/forms-18.2.13.tgz#119f9b32b0da5e2f1bc3c07f506a645e5635f583" From 880ab02743a6a382d7615b0eb335b825e8d5681a Mon Sep 17 00:00:00 2001 From: Igor Kulikov Date: Fri, 7 Mar 2025 19:21:48 +0200 Subject: [PATCH 251/345] UI: Map - add map scale control. --- .../home/components/widget/lib/maps/map.ts | 8 +++++++- .../common/map/map-settings.component.html | 8 ++++++++ .../common/map/map-settings.component.ts | 11 ++++++++--- .../app/shared/models/widget/maps/map.models.ts | 16 ++++++++++++++++ .../src/assets/locale/locale.constant-en_US.json | 3 +++ 5 files changed, 42 insertions(+), 4 deletions(-) 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 66475705f7..83dca0338f 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 @@ -20,6 +20,7 @@ import { DataKeyValuePair, MapBooleanFunction, mapDataLayerTypes, + MapScale, MapType, mergeMapDatasources, parseCenterPosition, @@ -171,7 +172,12 @@ export abstract class TbMap { } private setupControls(): Observable { - //L.control.scale().addTo(this.map); + if (this.settings.scales?.length) { + L.control.scale({ + metric: this.settings.scales.includes(MapScale.metric), + imperial: this.settings.scales.includes(MapScale.imperial) + }).addTo(this.map); + } if (this.map.zoomControl) { this.map.zoomControl.setPosition(this.settings.controlsPosition); } 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 97c686732a..a7b725cb3f 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 @@ -99,6 +99,14 @@ +
        +
        widgets.maps.control.scale
        + + + {{ mapScaleTranslationMap.get(scale) | translate }} + + +
        {{ 'widgets.maps.control.switch-to-drag-mode-using-button' | translate }} diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/map/map-settings.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/map/map-settings.component.ts index 2abaab17f7..f38f2a042c 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/map/map-settings.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/map/map-settings.component.ts @@ -33,7 +33,7 @@ import { mapControlPositions, mapControlsPositionTranslationMap, MapDataLayerSettings, - MapDataLayerType, + MapDataLayerType, mapScales, mapScaleTranslationMap, MapSetting, MapType, mapZoomActions, @@ -74,12 +74,16 @@ export class MapSettingsComponent implements OnInit, ControlValueAccessor, Valid mapControlPositions = mapControlPositions; - mapZoomActions = mapZoomActions; - mapControlsPositionTranslationMap = mapControlsPositionTranslationMap; + mapZoomActions = mapZoomActions; + mapZoomActionTranslationMap = mapZoomActionTranslationMap; + mapScales = mapScales; + + mapScaleTranslationMap = mapScaleTranslationMap; + MapType = MapType; @Input() @@ -144,6 +148,7 @@ export class MapSettingsComponent implements OnInit, ControlValueAccessor, Valid additionalDataSources: [null, []], controlsPosition: [null, []], zoomActions: [null, []], + scales: [null, []], dragModeButton: [null, []], fitMapBounds: [null, []], useDefaultCenterPosition: [null, []], 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 295876e326..20f69bac0f 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 @@ -640,6 +640,20 @@ export const mapZoomActionTranslationMap = new Map( ] ); +export enum MapScale { + metric = 'metric', + imperial = 'imperial' +} + +export const mapScales = Object.keys(MapScale) as MapScale[]; + +export const mapScaleTranslationMap = new Map( + [ + [MapScale.metric, 'widgets.maps.control.scale-metric'], + [MapScale.imperial, 'widgets.maps.control.scale-imperial'], + ] +); + export interface MapActionButtonSettings { label?: string; icon?: string; @@ -666,6 +680,7 @@ export interface BaseMapSettings { additionalDataSources: AdditionalMapDataSourceSettings[]; controlsPosition: MapControlsPosition; zoomActions: MapZoomAction[]; + scales: MapScale[]; dragModeButton: boolean; fitMapBounds: boolean; useDefaultCenterPosition: boolean; @@ -689,6 +704,7 @@ export const defaultBaseMapSettings: BaseMapSettings = { additionalDataSources: [], controlsPosition: MapControlsPosition.topleft, zoomActions: [MapZoomAction.scroll, MapZoomAction.doubleClick, MapZoomAction.controlButtons], + scales: [], dragModeButton: false, fitMapBounds: true, useDefaultCenterPosition: false, 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 48272ada9e..53ddc5a924 100644 --- a/ui-ngx/src/assets/locale/locale.constant-en_US.json +++ b/ui-ngx/src/assets/locale/locale.constant-en_US.json @@ -7705,6 +7705,9 @@ "zoom-scroll": "Scroll", "zoom-double-click": "Double click", "zoom-control-buttons": "Control buttons", + "scale": "Scale", + "scale-metric": "Metric", + "scale-imperial": "Imperial", "switch-to-drag-mode-using-button": "Switch to drag mode using button" }, "timeline": { From 773a206079c27cf55fa9d0c7372dfd3412df2cb2 Mon Sep 17 00:00:00 2001 From: YevhenBondarenko Date: Sat, 8 Mar 2025 20:54:08 +0100 Subject: [PATCH 252/345] added upgrade for the kafka node --- .../rule/engine/kafka/TbKafkaNode.java | 22 +++++ .../rule/engine/kafka/TbKafkaNodeTest.java | 81 +++++++++++++++++-- 2 files changed, 98 insertions(+), 5 deletions(-) diff --git a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/kafka/TbKafkaNode.java b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/kafka/TbKafkaNode.java index 8f6a6dc617..d544d0647d 100644 --- a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/kafka/TbKafkaNode.java +++ b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/kafka/TbKafkaNode.java @@ -15,6 +15,8 @@ */ package org.thingsboard.rule.engine.kafka; +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.node.ObjectNode; import lombok.extern.slf4j.Slf4j; import org.apache.commons.lang3.BooleanUtils; import org.apache.kafka.clients.producer.KafkaProducer; @@ -36,6 +38,7 @@ import org.thingsboard.rule.engine.api.util.TbNodeUtils; import org.thingsboard.rule.engine.external.TbAbstractExternalNode; import org.thingsboard.server.common.data.exception.ThingsboardKafkaClientError; import org.thingsboard.server.common.data.plugin.ComponentType; +import org.thingsboard.server.common.data.util.TbPair; import org.thingsboard.server.common.msg.TbMsg; import org.thingsboard.server.common.msg.TbMsgMetaData; @@ -49,6 +52,7 @@ import java.util.Properties; type = ComponentType.EXTERNAL, name = "kafka", configClazz = TbKafkaNodeConfiguration.class, + version = 1, nodeDescription = "Publish messages to Kafka server", nodeDetails = "Will send record via Kafka producer to Kafka server. " + "Outbound message will contain response fields (offset, partition, topic)" + @@ -201,4 +205,22 @@ public class TbKafkaNode extends TbAbstractExternalNode { .build(); } + @Override + public TbPair upgrade(int fromVersion, JsonNode oldConfiguration) throws TbNodeException { + boolean hasChanges = false; + switch (fromVersion) { + case 0 -> { + if (oldConfiguration.has("keySerializer") || oldConfiguration.has("valueSerializer")) { + ObjectNode objectConfiguration = (ObjectNode) oldConfiguration; + objectConfiguration.remove("keySerializer"); + objectConfiguration.remove("valueSerializer"); + hasChanges = true; + } + } + default -> { + } + } + return new TbPair<>(hasChanges, oldConfiguration); + } + } diff --git a/rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/kafka/TbKafkaNodeTest.java b/rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/kafka/TbKafkaNodeTest.java index de81d55fdb..486e707571 100644 --- a/rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/kafka/TbKafkaNodeTest.java +++ b/rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/kafka/TbKafkaNodeTest.java @@ -39,8 +39,10 @@ import org.mockito.junit.jupiter.MockitoExtension; import org.springframework.test.util.ReflectionTestUtils; import org.thingsboard.common.util.JacksonUtil; import org.thingsboard.common.util.ListeningExecutor; +import org.thingsboard.rule.engine.AbstractRuleNodeUpgradeTest; import org.thingsboard.rule.engine.TestDbCallbackExecutor; import org.thingsboard.rule.engine.api.TbContext; +import org.thingsboard.rule.engine.api.TbNode; import org.thingsboard.rule.engine.api.TbNodeConfiguration; import org.thingsboard.rule.engine.api.TbNodeException; import org.thingsboard.rule.engine.api.util.TbNodeUtils; @@ -73,7 +75,7 @@ import static org.mockito.BDDMockito.willReturn; import static org.mockito.BDDMockito.willThrow; @ExtendWith(MockitoExtension.class) -public class TbKafkaNodeTest { +public class TbKafkaNodeTest extends AbstractRuleNodeUpgradeTest { private final DeviceId DEVICE_ID = new DeviceId(UUID.fromString("5f2eac08-bd1f-4635-a6c2-437369f996cf")); private final RuleNodeId RULE_NODE_ID = new RuleNodeId(UUID.fromString("d46bb666-ecab-4d89-a28f-5abdca23ac29")); @@ -117,8 +119,6 @@ public class TbKafkaNodeTest { assertThat(config.getLinger()).isEqualTo(0); assertThat(config.getBufferMemory()).isEqualTo(33554432); assertThat(config.getAcks()).isEqualTo("-1"); - assertThat(config.getKeySerializer()).isEqualTo(StringSerializer.class.getName()); - assertThat(config.getValueSerializer()).isEqualTo(StringSerializer.class.getName()); assertThat(config.getOtherProperties()).isEmpty(); assertThat(config.isAddMetadataKeyValuesAsKafkaHeaders()).isFalse(); assertThat(config.getKafkaHeadersCharset()).isEqualTo("UTF-8"); @@ -163,8 +163,8 @@ public class TbKafkaNodeTest { Properties expectedProperties = new Properties(); expectedProperties.put(ProducerConfig.CLIENT_ID_CONFIG, "producer-tb-kafka-node-" + RULE_NODE_ID.getId() + "-" + SERVICE_ID_STR); expectedProperties.put(ProducerConfig.BOOTSTRAP_SERVERS_CONFIG, config.getBootstrapServers()); - expectedProperties.put(ProducerConfig.VALUE_SERIALIZER_CLASS_CONFIG, config.getValueSerializer()); - expectedProperties.put(ProducerConfig.KEY_SERIALIZER_CLASS_CONFIG, config.getKeySerializer()); + expectedProperties.put(ProducerConfig.VALUE_SERIALIZER_CLASS_CONFIG, StringSerializer.class); + expectedProperties.put(ProducerConfig.KEY_SERIALIZER_CLASS_CONFIG, StringSerializer.class); expectedProperties.put(ProducerConfig.ACKS_CONFIG, config.getAcks()); expectedProperties.put(ProducerConfig.RETRIES_CONFIG, config.getRetries()); expectedProperties.put(ProducerConfig.BATCH_SIZE_CONFIG, config.getBatchSize()); @@ -454,4 +454,75 @@ public class TbKafkaNodeTest { assertThat(actualMsg).usingRecursiveComparison().ignoringFields("ctx").isEqualTo(expectedMsg); } + private static Stream givenFromVersionAndConfig_whenUpgrade_thenVerifyHasChangesAndConfig() { + return Stream.of( + //config for version 0 + Arguments.of(0, + "{\n" + + " \"topicPattern\": \"test-topic\",\n" + + " \"keyPattern\": \"test-key\",\n" + + " \"bootstrapServers\": \"localhost:9092\",\n" + + " \"retries\": 0,\n" + + " \"batchSize\": 16384,\n" + + " \"linger\": 0,\n" + + " \"bufferMemory\": 33554432,\n" + + " \"acks\": \"-1\",\n" + + " \"otherProperties\": {},\n" + + " \"addMetadataKeyValuesAsKafkaHeaders\": false,\n" + + " \"kafkaHeadersCharset\": \"UTF-8\",\n" + + " \"keySerializer\": \"org.apache.kafka.common.serialization.StringSerializer\",\n" + + " \"valueSerializer\": \"org.apache.kafka.common.serialization.StringSerializer\"\n" + + "}", + true, + "{\n" + + " \"topicPattern\": \"test-topic\",\n" + + " \"keyPattern\": \"test-key\",\n" + + " \"bootstrapServers\": \"localhost:9092\",\n" + + " \"retries\": 0,\n" + + " \"batchSize\": 16384,\n" + + " \"linger\": 0,\n" + + " \"bufferMemory\": 33554432,\n" + + " \"acks\": \"-1\",\n" + + " \"otherProperties\": {},\n" + + " \"addMetadataKeyValuesAsKafkaHeaders\": false,\n" + + " \"kafkaHeadersCharset\": \"UTF-8\"\n" + + "}" + ), + //config for version 1 with upgrade from version 0 + Arguments.of(1, + "{\n" + + " \"topicPattern\": \"test-topic\",\n" + + " \"keyPattern\": \"test-key\",\n" + + " \"bootstrapServers\": \"localhost:9092\",\n" + + " \"retries\": 0,\n" + + " \"batchSize\": 16384,\n" + + " \"linger\": 0,\n" + + " \"bufferMemory\": 33554432,\n" + + " \"acks\": \"-1\",\n" + + " \"otherProperties\": {},\n" + + " \"addMetadataKeyValuesAsKafkaHeaders\": false,\n" + + " \"kafkaHeadersCharset\": \"UTF-8\"\n" + + "}", + false, + "{\n" + + " \"topicPattern\": \"test-topic\",\n" + + " \"keyPattern\": \"test-key\",\n" + + " \"bootstrapServers\": \"localhost:9092\",\n" + + " \"retries\": 0,\n" + + " \"batchSize\": 16384,\n" + + " \"linger\": 0,\n" + + " \"bufferMemory\": 33554432,\n" + + " \"acks\": \"-1\",\n" + + " \"otherProperties\": {},\n" + + " \"addMetadataKeyValuesAsKafkaHeaders\": false,\n" + + " \"kafkaHeadersCharset\": \"UTF-8\"\n" + + "}" + ) + ); + } + + @Override + protected TbNode getTestNode() { + return node; + } } From 83f265e6a9af365047f188ad27bb9581e8ddd044 Mon Sep 17 00:00:00 2001 From: IrynaMatveieva Date: Mon, 10 Mar 2025 10:42:46 +0200 Subject: [PATCH 253/345] updated help page --- .../en_US/calculated-field/expression_fn.md | 484 +++++++++--------- 1 file changed, 244 insertions(+), 240 deletions(-) diff --git a/ui-ngx/src/assets/help/en_US/calculated-field/expression_fn.md b/ui-ngx/src/assets/help/en_US/calculated-field/expression_fn.md index 2e764d7c16..e35572bf97 100644 --- a/ui-ngx/src/assets/help/en_US/calculated-field/expression_fn.md +++ b/ui-ngx/src/assets/help/en_US/calculated-field/expression_fn.md @@ -1,9 +1,7 @@ #### Calculated field TBEL script function -The **calculate()** function is user-defined script that takes values of arguments configured in the calculated field setup and an additional `ctx` object containing all arguments. - The **calculate()** function is a user-defined script that allows you to perform custom calculations using [TBEL{:target="_blank"}](${siteBaseUrl}/docs${docPlatformPrefix}/user-guide/tbel/) on telemetry and attribute data. -It receives user-configured arguments and an additional `ctx` object, which provides access to all arguments. +It receives arguments configured in the calculated field setup and an additional `ctx` object, which provides access to all arguments. ##### Function signature @@ -11,48 +9,11 @@ It receives user-configured arguments and an additional `ctx` object, which prov function calculate(ctx, arg1, arg2, ...): object | object[] ``` -* the function automatically receives a `ctx` object containing all arguments. -* user-defined arguments `(arg1, arg2, ...)` are passed based on the calculated field configuration. -* the function must return a JSON object (single result) or an array of objects (multiple telemetry entries). - -##### Example: Air Density Calculation - -This example demonstrates how to calculate air density using altitude and temperature telemetry data. - -```javascript -function calculate(ctx, altitude, temperature) { - var avgTemperature = temperature.mean(); // Get average temperature - var temperatureK = (avgTemperature - 32) * (5 / 9) + 273.15; // Convert Fahrenheit to Kelvin - - // Estimate air pressure based on altitude - var pressure = 101325 * Math.pow((1 - 2.25577e-5 * altitude), 5.25588); - - // Air density formula - var airDensity = pressure / (287.05 * temperatureK); - - return { - "airDensity": airDensity - }; -} -``` - -##### Function arguments - -* `ctx` - context object containing all predefined arguments. - - Use `ctx.args.argName` to access arguments. - - ```javascript - var altitude = ctx.args.altitude.ts; - var temperature = ctx.args.temperature; - ``` - -* `arg1, arg2, ...` - user-defined arguments configured in the calculated field setup. These arguments follow the previously described types: - * if an argument represents the latest telemetry data or attribute, it will be passed as a direct value. +##### Argument representation in the script - * if an argument is a time series rolling argument, it will be provided as a time series rolling argument described above. +Before describing how arguments are passed to the function, let's define how different argument types are **represented** inside the script. -The **calculate()** function receives predefined arguments, which fall into two categories: +There are two types of arguments that can be used in the function: * single value arguments - represent the latest telemetry data or attribute. ```json @@ -64,13 +25,21 @@ The **calculate()** function receives predefined arguments, which fall into two } ``` -Use dot notation (`.`) to access argument properties. + * when accessed via `ctx.args`, they remain objects: ```javascript var altitudeTimestamp = ctx.args.altitude.ts; var altitudeValue = ctx.args.altitude.value; ``` + * when accessed as a **function parameter**, only the value is passed: + + ```javascript + function calculate(ctx, altitude/*(single value argument)*/, temperature/*(time series rolling argument)*/) { + // altitude = 1035 + } + ``` + * time series rolling arguments - contain historical data within a defined time window. ```json { @@ -89,7 +58,7 @@ Use dot notation (`.`) to access argument properties. } ``` - Use dot notation (`.`) to access argument properties. + * when accessed via `ctx.args`, they remain rolling argument objects: ```javascript var startOfInterval = temperature.timeWindow.startTs; @@ -97,235 +66,270 @@ Use dot notation (`.`) to access argument properties. var firstValue = temperature.values[0].value; ``` -**Built-in methods for rolling arguments** + * when accessed as a **function parameter**, they are passed as rolling arguments, retaining their structure: -Time series rolling arguments provide built-in functions for calculations. -These functions accept an optional `ignoreNaN` boolean parameter, which controls how NaN values are handled. -Each function has two function signatures: + ```javascript + function calculate(ctx, altitude/*(single value argument)*/, temperature/*(time series rolling argument)*/) { + var avgTemp = temperature.mean(); // Use rolling argument functions + } + ``` -* **Without parameters:** `method()` → called **without parameters** and defaults to `ignoreNaN = true`, meaning NaN values are ignored. -* **With an explicit parameter:** `method(boolean ignoreNaN)` → called with a boolean `ignoreNaN` parameter: - * `true` → ignores NaN values (default behavior). - * `false` → includes NaN values in calculations. + **Built-in methods for rolling arguments** -| Method | Default Behavior (`ignoreNaN = true`) | Alternative (`ignoreNaN = false`) | -|------------|--------------------------------------------------|---------------------------------------------| -| `max()` | Returns the highest value, ignoring NaN values. | Returns NaN if any NaN values exist. | -| `min()` | Returns the lowest value, ignoring NaN values. | Returns NaN if any NaN values exist. | -| `mean()` | Computes the average value, ignoring NaN values. | Returns NaN if any NaN values exist. | -| `std()` | Calculates the standard deviation, ignoring NaN. | Returns NaN if any NaN values exist. | -| `median()` | Returns the median value, ignoring NaN values. | Returns NaN if any NaN values exist. | -| `count()` | Counts only valid (non-NaN) values. | Counts all values, including NaN. | -| `last()` | Returns the most recent non-NaN value. | Returns the last value, even if it is NaN. | -| `first()` | Returns the oldest non-NaN value. | Returns the first value, even if it is NaN. | -| `sum()` | Computes the total sum, ignoring NaN values. | Returns NaN if any NaN values exist. | + Time series rolling arguments provide built-in functions for calculations. + These functions accept an optional `ignoreNaN` boolean parameter, which controls how NaN values are handled. + Each function has two function signatures: -The following calculations are executed over the provided above arguments: + * **Without parameters:** `method()` → called **without parameters** and defaults to `ignoreNaN = true`, meaning NaN values are ignored. + * **With an explicit parameter:** `method(boolean ignoreNaN)` → called with a boolean `ignoreNaN` parameter: + * `true` → ignores NaN values (default behavior). + * `false` → includes NaN values in calculations. -**Example usage: default (`ignoreNaN = true`)** + | Method | Default Behavior (`ignoreNaN = true`) | Alternative (`ignoreNaN = false`) | + |------------|--------------------------------------------------|---------------------------------------------| + | `max()` | Returns the highest value, ignoring NaN values. | Returns NaN if any NaN values exist. | + | `min()` | Returns the lowest value, ignoring NaN values. | Returns NaN if any NaN values exist. | + | `mean()` | Computes the average value, ignoring NaN values. | Returns NaN if any NaN values exist. | + | `std()` | Calculates the standard deviation, ignoring NaN. | Returns NaN if any NaN values exist. | + | `median()` | Returns the median value, ignoring NaN values. | Returns NaN if any NaN values exist. | + | `count()` | Counts only valid (non-NaN) values. | Counts all values, including NaN. | + | `last()` | Returns the most recent non-NaN value. | Returns the last value, even if it is NaN. | + | `first()` | Returns the oldest non-NaN value. | Returns the first value, even if it is NaN. | + | `sum()` | Computes the total sum, ignoring NaN values. | Returns NaN if any NaN values exist. | -```javascript -var avgTemp = temperature.mean(); -var tempMax = temperature.max(); -var valueCount = temperature.count(); -``` + The following calculations are executed over the provided above arguments: -**Output:** + **Usage: default (`ignoreNaN = true`)** -```json -{ - "avgTemp": 72.92, - "tempMax": 73.58, - "valueCount": 3 -} -``` + ```javascript + var avgTemp = temperature.mean(); + var tempMax = temperature.max(); + var valueCount = temperature.count(); + ``` -**Example usage: explicit (`ignoreNaN = false`)** + **Output:** -```javascript -var avgTemp = temperature.mean(false); // Returns NaN if any NaN values exist -var tempMax = temperature.max(false); // Returns NaN if any NaN values exist -var valueCount = temperature.count(false); // Counts all values, including NaN -``` + ```json + { + "avgTemp": 72.92, + "tempMax": 73.58, + "valueCount": 3 + } + ``` -**Output:** + **Usage: explicit (`ignoreNaN = false`)** + + ```javascript + var avgTemp = temperature.mean(false); // Returns NaN if any NaN values exist + var tempMax = temperature.max(false); // Returns NaN if any NaN values exist + var valueCount = temperature.count(false); // Counts all values, including NaN + ``` -```json -{ - "avgTemp": "NaN", - "tempMax": "NaN", - "valueCount": 4 -} -``` + **Output:** -Time series rolling arguments can be merged for multi-sensor analysis. - -Function Signatures: - -* `merge(other, settings)` - merges the current rolling argument with another rolling argument by aligning timestamps and combining values. - Parameters: - * `other` - another time series rolling argument to merge with. - * `settings` (optional) - configuration object, supports: - * `ignoreNaN` (boolean, default true) - controls whether NaN values should be ignored. - * `timeWindow` (object, default {}) - defines a custom time window for filtering merged values. - Returns: an object with time window and values from each provided time series rolling arguments. - - * `mergeAll(others, settings)` - merges the current rolling argument with multiple rolling arguments by aligning timestamps and combining values. - Parameters: - * `others` - an array of time series rolling arguments to merge with. - * `settings` (optional) - same as `merge()`. - Returns: an object with timeWindow and aligned values. - -```json -{ - "humidity": { - "timeWindow": { - "startTs": 1741356332086, - "endTs": 1741357232086 - }, - "values": [{ - "ts": 1741356882759, - "value": 43 - }, { - "ts": 1741356918779, - "value": 46 - }] - }, - "pressure": { - "timeWindow": { - "startTs": 1741356332086, - "endTs": 1741357232086 + ```json + { + "avgTemp": "NaN", + "tempMax": "NaN", + "valueCount": 4 + } + ``` + + Time series rolling arguments can be **merged** to align timestamps across multiple datasets. + + | Method | Description | Parameters | Returns | + |:-----------------------------|:-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|:-------------------------------------------------| + | `merge(other, settings)` | Merges the current rolling argument with another rolling argument by aligning timestamps and filling missing values with the previous available value. |
        • `other` (another rolling argument)
        • `settings` (optional) - configuration object, supports:

          • `ignoreNaN` (boolean, default true) - controls whether NaN values should be ignored.
          • `timeWindow` (object, default {}) - defines a custom time window for filtering merged values.
        | Merged object with timeWindow and aligned values. | + | `mergeAll(others, settings)` | Merges the current rolling argument with multiple rolling arguments by aligning timestamps and filling missing values with the previous available value. |
        • `others` (array of rolling arguments)
        • `settings` (optional) - configuration object, supports:

          • `ignoreNaN` (boolean, default true) - controls whether NaN values should be ignored.
          • `timeWindow` (object, default {}) - defines a custom time window for filtering merged values.
        | Merged object with timeWindow and aligned values.| + + **Example arguments:** + + ```json + { + "humidity": { + "timeWindow": { + "startTs": 1741356332086, + "endTs": 1741357232086 + }, + "values": [{ + "ts": 1741356882759, + "value": 43 + }, { + "ts": 1741356918779, + "value": 46 + }] }, - "values": [{ - "ts": 1741357047945, - "value": 1023 - }, { - "ts": 1741357056144, - "value": 1026 - }, { - "ts": 1741357147391, - "value": 1025 - }] - }, - "temperature": { - "timeWindow": { - "startTs": 1741356332086, - "endTs": 1741357232086 + "pressure": { + "timeWindow": { + "startTs": 1741356332086, + "endTs": 1741357232086 + }, + "values": [{ + "ts": 1741357047945, + "value": 1023 + }, { + "ts": 1741357056144, + "value": 1026 + }, { + "ts": 1741357147391, + "value": 1025 + }] }, - "values": [{ - "ts": 1741356874943, - "value": 76 - }, { - "ts": 1741357063689, - "value": 77 - }] + "temperature": { + "timeWindow": { + "startTs": 1741356332086, + "endTs": 1741357232086 + }, + "values": [{ + "ts": 1741356874943, + "value": 76 + }, { + "ts": 1741357063689, + "value": 77 + }] + } } -} -``` + ``` -**Example usage** -```javascript -var mergedData = temperature.merge(humidity, { ignoreNaN: false }); -``` + **Usage:** -**Output:** -```json -{ - "mergedData": { - "timeWindow": { - "startTs": 1741356332086, - "endTs": 1741357232086 - }, - "values": [{ - "ts": 1741356874943, - "values": [76.0, "NaN"] - }, { - "ts": 1741356882759, - "values": [76.0, 43.0] - }, { - "ts": 1741356918779, - "values": [76.0, 46.0] - }, { - "ts": 1741357063689, - "values": [77.0, 46.0] - }] + ```javascript + var mergedData = temperature.merge(humidity, { ignoreNaN: false }); + ``` + + **Output:** + + ```json + { + "mergedData": { + "timeWindow": { + "startTs": 1741356332086, + "endTs": 1741357232086 + }, + "values": [{ + "ts": 1741356874943, + "values": [76.0, "NaN"] + }, { + "ts": 1741356882759, + "values": [76.0, 43.0] + }, { + "ts": 1741356918779, + "values": [76.0, 46.0] + }, { + "ts": 1741357063689, + "values": [77.0, 46.0] + }] + } } -} -``` + ``` -**Example usage** -```javascript -var mergedData = temperature.mergeAll([humidity, pressure], { ignoreNaN: true }); -``` + **Usage:** -**Output:** -```json -{ - "mergedData": { - "timeWindow": { - "startTs": 1741356332086, - "endTs": 1741357232086 - }, - "values": [{ - "ts": 1741357047945, - "values": [76.0, 46.0, 1023.0] - }, { - "ts": 1741357056144, - "values": [76.0, 46.0, 1026.0] - }, { - "ts": 1741357063689, - "values": [77.0, 46.0, 1026.0] - }, { - "ts": 1741357147391, - "values": [77.0, 46.0, 1025.0] - }] + ```javascript + var mergedData = temperature.mergeAll([humidity, pressure], { ignoreNaN: true }); + ``` + + **Output:** + + ```json + { + "mergedData": { + "timeWindow": { + "startTs": 1741356332086, + "endTs": 1741357232086 + }, + "values": [{ + "ts": 1741357047945, + "values": [76.0, 46.0, 1023.0] + }, { + "ts": 1741357056144, + "values": [76.0, 46.0, 1026.0] + }, { + "ts": 1741357063689, + "values": [77.0, 46.0, 1026.0] + }, { + "ts": 1741357147391, + "values": [77.0, 46.0, 1025.0] + }] + } } -} -``` + ``` -##### Function return format: +##### Function arguments -The script should return a JSON object formatted according to the [ThingsBoard Telemetry Upload API](${siteBaseUrl}/docs${docPlatformPrefix}/user-guide/telemetry/#time-series-data-upload-api/). -The return value must match one of the supported telemetry upload formats. -**Example Formats**: +* `ctx` - context object that contains all provided arguments, in the representations described above. -Single key-value format: +Accessing arguments via `ctx`: -```json -{ - "airDensity": 1.06 -} -``` + ```javascript + var altitude = ctx.args.altitude; // single value argument + var temperature = ctx.args.temperature; // time series rolling argument + ``` -Key-value format with a timestamp: +* `arg1, arg2, ...` - user-defined arguments configured in the calculated field setup. -```json -{ - "ts": 1740644636669, - "values": { - "airDensity": 1.06 - } +How they are passed depends on their type: + +* **single value arguments** are passed as raw values **(e.g., 22.5, "ON")**. +* **time series rolling arguments** are passed as objects (containing multiple values). + +##### Example: Air Density Calculation + +This function calculates air density using `altitude` (single value argument) and `temperature` (time series rolling argument). + +```javascript +function calculate(ctx, altitude, temperature) { + var avgTemperature = temperature.mean(); // Get average temperature + var temperatureK = (avgTemperature - 32) * (5 / 9) + 273.15; // Convert Fahrenheit to Kelvin + + // Estimate air pressure based on altitude + var pressure = 101325 * Math.pow((1 - 2.25577e-5 * altitude), 5.25588); + + // Air density formula + var airDensity = pressure / (287.05 * temperatureK); + + return { + "airDensity": airDensity + }; } ``` -Array of telemetry entries: +* `altitude` is a single value, passed as number. +* `temperature` is a rolling argument, retaining its full structure. -```json -[ +##### Function return format + +The script should return a JSON object formatted according to the [ThingsBoard Telemetry Upload API](${siteBaseUrl}/docs${docPlatformPrefix}/user-guide/telemetry/#time-series-data-upload-api/). +The return value must match one of the supported telemetry upload formats. + +The script must return data in a format compatible with ThingsBoard’s APIs. +The correct return format depends on the calculated field output configuration: + +* if latest telemetry is used for output, the function must return data according to the [Telemetry Upload API](${siteBaseUrl}/docs${docPlatformPrefix}/reference/mqtt-api/#attributes-api/). + + * without timestamps + ```json + { + "airDensity": 1.06, + "someKey": "value" + } + ``` + * with a timestamp: + ```json { "ts": 1740644636669, "values": { - "airDensity": 1.06 - } - }, + "airDensity": 1.06, + "someKey": "value" + } + } + ``` + +* if attributes are used for output, the function must return data according to the [Attributes API](${siteBaseUrl}/docs${docPlatformPrefix}/user-guide/telemetry/#time-series-data-upload-api/). + + ```json { - "ts": 1740644662896, - "values": [ - { "ts": 1740644355935, "value": 72.32 }, - { "ts": 1740644365935, "value": 72.86 }, - { "ts": 1740644375935, "value": 73.58 }, - { "ts": 1740644385935, "value": "NaN" } - ] + "airDensity": 1.06, + "someKey": "value" } -] -``` + ``` From f741a90cbb735b3eb6455c6a51f77728124f851c Mon Sep 17 00:00:00 2001 From: Vladyslav_Prykhodko Date: Mon, 10 Mar 2025 11:19:15 +0200 Subject: [PATCH 254/345] UI: Fixed fill-opacity in marker shape7.svg --- ui-ngx/src/assets/markers/shape7.svg | 26 ++++++++++++++------------ 1 file changed, 14 insertions(+), 12 deletions(-) diff --git a/ui-ngx/src/assets/markers/shape7.svg b/ui-ngx/src/assets/markers/shape7.svg index aeef8c74c5..99e2a5a623 100644 --- a/ui-ngx/src/assets/markers/shape7.svg +++ b/ui-ngx/src/assets/markers/shape7.svg @@ -1,13 +1,12 @@ - + - + - - - - - - + + + + + @@ -16,13 +15,16 @@ - - + + - - + + + + + From 6018f69f0ed7d421e94a22df12c223e7bebb60f8 Mon Sep 17 00:00:00 2001 From: Vladyslav_Prykhodko Date: Mon, 10 Mar 2025 11:29:07 +0200 Subject: [PATCH 255/345] UI: Remove process class marker-stroke in marker shape --- .../app/shared/models/widget/maps/marker-shape.models.ts | 8 -------- 1 file changed, 8 deletions(-) diff --git a/ui-ngx/src/app/shared/models/widget/maps/marker-shape.models.ts b/ui-ngx/src/app/shared/models/widget/maps/marker-shape.models.ts index 3d8f44df3f..29e04dbaa1 100644 --- a/ui-ngx/src/app/shared/models/widget/maps/marker-shape.models.ts +++ b/ui-ngx/src/app/shared/models/widget/maps/marker-shape.models.ts @@ -224,14 +224,6 @@ const createColorMarkerShape = (iconRegistry: MatIconRegistry, domSanitizer: Dom el.setAttribute('fill', '#'+color.toHex()); el.setAttribute('fill-opacity', `${color.getAlpha()}`); }); - const strokeElements = Array.from(svgElement.getElementsByClassName('marker-stroke')); - if (svgElement.classList.contains('marker-stroke')) { - strokeElements.push(svgElement); - } - strokeElements.forEach(el => { - el.setAttribute('stroke', '#'+color.toHex()); - el.setAttribute('stroke-opacity', `${color.getAlpha()}`); - }); const opacityElements = Array.from(svgElement.getElementsByClassName('marker-opacity')); if (svgElement.classList.contains('marker-opacity')) { opacityElements.push(svgElement); From c8d7662ff35f7ec1c57506ffbc8bdf85f2c3a9d1 Mon Sep 17 00:00:00 2001 From: Igor Kulikov Date: Mon, 10 Mar 2025 12:11:58 +0200 Subject: [PATCH 256/345] UI: Map - add attribute scope setting for persisting location. --- .../lib/maps/data-layer/circles-data-layer.ts | 2 +- .../lib/maps/data-layer/markers-data-layer.ts | 2 +- .../maps/data-layer/polygons-data-layer.ts | 2 +- .../home/components/widget/lib/maps/map.ts | 9 ++++- .../map/map-data-layer-dialog.component.html | 11 +++++ .../map/map-data-layer-dialog.component.ts | 40 +++++++++++++++++-- .../shared/models/widget/maps/map.models.ts | 4 +- .../assets/locale/locale.constant-en_US.json | 1 + 8 files changed, 61 insertions(+), 10 deletions(-) diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/maps/data-layer/circles-data-layer.ts b/ui-ngx/src/app/modules/home/components/widget/lib/maps/data-layer/circles-data-layer.ts index 62b36b59c6..9e25ddb486 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/maps/data-layer/circles-data-layer.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/maps/data-layer/circles-data-layer.ts @@ -208,7 +208,7 @@ export class TbCirclesDataLayer extends TbShapesDataLayer converted) ); } 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 b5dbedf90f..2b11a3c669 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 @@ -621,7 +621,7 @@ export class TbMarkersDataLayer extends TbLatestMapDataLayer converted) ); } diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/maps/data-layer/polygons-data-layer.ts b/ui-ngx/src/app/modules/home/components/widget/lib/maps/data-layer/polygons-data-layer.ts index d2178169dc..36ab097c5d 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/maps/data-layer/polygons-data-layer.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/maps/data-layer/polygons-data-layer.ts @@ -405,7 +405,7 @@ export class TbPolygonsDataLayer extends TbShapesDataLayer converted) ); } 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 83dca0338f..003d8e379e 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 @@ -72,6 +72,7 @@ import { ComponentRef } from '@angular/core'; import { TbTripsDataLayer } from '@home/components/widget/lib/maps/data-layer/trips-data-layer'; import { CompiledTbFunction } from '@shared/models/js-function.models'; import { TbMapDataLayer } from '@home/components/widget/lib/maps/data-layer/map-data-layer'; +import { EntityType } from '@shared/models/entity-type.models'; import ITooltipsterInstance = JQueryTooltipster.ITooltipsterInstance; import TooltipPositioningSide = JQueryTooltipster.TooltipPositioningSide; @@ -1099,7 +1100,7 @@ export abstract class TbMap { return this.dragMode; } - public saveItemData(datasource: TbMapDatasource, data: DataKeyValuePair[]): Observable { + public saveItemData(datasource: TbMapDatasource, data: DataKeyValuePair[], attributeScope: AttributeScope): Observable { const attributeService = this.ctx.$injector.get(AttributeService); const attributes: AttributeData[] = []; const timeseries: AttributeData[] = []; @@ -1107,6 +1108,10 @@ export abstract class TbMap { entityType: datasource.entityType, id: datasource.entityId }; + + const targetAttributeScope = + entityId.entityType === EntityType.DEVICE && !!attributeScope ? attributeScope : AttributeScope.SERVER_SCOPE; + data.forEach(pair => { const key = pair.dataKey; if (key.type === DataKeyType.attribute) { @@ -1132,7 +1137,7 @@ export abstract class TbMap { if (attributes.length) { observables.push(attributeService.saveEntityAttributes( entityId, - AttributeScope.SERVER_SCOPE, + targetAttributeScope, attributes )); } diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/map/map-data-layer-dialog.component.html b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/map/map-data-layer-dialog.component.html index 012fbf6625..e7f117f980 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/map/map-data-layer-dialog.component.html +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/map/map-data-layer-dialog.component.html @@ -411,6 +411,17 @@
        +
        +
        widgets.maps.data-layer.persist-location-attribute-scope
        + + + {{ telemetryTypeTranslationsShort.get(AttributeScope.SERVER_SCOPE) | translate }} + + + {{ telemetryTypeTranslationsShort.get(AttributeScope.SHARED_SCOPE) | translate }} + + +
        {{ 'widgets.maps.data-layer.enable-snapping' | translate }} diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/map/map-data-layer-dialog.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/map/map-data-layer-dialog.component.ts index 64d75eb78f..dc0123406a 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/map/map-data-layer-dialog.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/map/map-data-layer-dialog.component.ts @@ -26,9 +26,12 @@ import { MapDataLayerType, MapType, MarkersDataLayerSettings, - MarkerType, pathDecoratorSymbols, pathDecoratorSymbolTranslationMap, + MarkerType, + pathDecoratorSymbols, + pathDecoratorSymbolTranslationMap, PolygonsDataLayerSettings, - ShapeDataLayerSettings, TripsDataLayerSettings + ShapeDataLayerSettings, + TripsDataLayerSettings } from '@shared/models/widget/maps/map.models'; import { Store } from '@ngrx/store'; import { AppState } from '@core/core.state'; @@ -36,7 +39,7 @@ import { Router } from '@angular/router'; import { MAT_DIALOG_DATA, MatDialogRef } from '@angular/material/dialog'; import { FormBuilder, UntypedFormGroup, Validators } from '@angular/forms'; import { DataKey, DatasourceType, datasourceTypeTranslationMap, widgetType } from '@shared/models/widget.models'; -import { DataKeyType } from '@shared/models/telemetry/telemetry.models'; +import { AttributeScope, DataKeyType, telemetryTypeTranslationsShort } from '@shared/models/telemetry/telemetry.models'; import { takeUntilDestroyed } from '@angular/core/rxjs-interop'; import { EntityType } from '@shared/models/entity-type.models'; import { MapSettingsContext } from '@home/components/widget/lib/settings/common/map/map-settings.component.models'; @@ -67,6 +70,9 @@ export class MapDataLayerDialogComponent extends DialogComponent, protected router: Router, @Inject(MAT_DIALOG_DATA) public data: MapDataLayerDialogData, @@ -158,6 +189,7 @@ export class MapDataLayerDialogComponent extends DialogComponent Date: Mon, 10 Mar 2025 12:55:56 +0200 Subject: [PATCH 257/345] EDQS: fix entity views sync --- .../server/common/data/edqs/fields/EntityViewFields.java | 7 +++++-- .../server/dao/sql/entityview/EntityViewRepository.java | 5 +++-- 2 files changed, 8 insertions(+), 4 deletions(-) diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/edqs/fields/EntityViewFields.java b/common/data/src/main/java/org/thingsboard/server/common/data/edqs/fields/EntityViewFields.java index 7674fba200..ba3c105a87 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/edqs/fields/EntityViewFields.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/edqs/fields/EntityViewFields.java @@ -15,12 +15,15 @@ */ package org.thingsboard.server.common.data.edqs.fields; +import com.fasterxml.jackson.databind.JsonNode; import lombok.Data; import lombok.NoArgsConstructor; import lombok.experimental.SuperBuilder; import java.util.UUID; +import static org.thingsboard.server.common.data.edqs.fields.FieldsUtil.getText; + @Data @NoArgsConstructor @SuperBuilder @@ -29,9 +32,9 @@ public class EntityViewFields extends AbstractEntityFields { private String type; private String additionalInfo; - public EntityViewFields(UUID id, long createdTime, UUID tenantId, UUID customerId, String name, String type, String additionalInfo, Long version) { + public EntityViewFields(UUID id, long createdTime, UUID tenantId, UUID customerId, String name, String type, JsonNode additionalInfo, Long version) { super(id, createdTime, tenantId, customerId, name, version); this.type = type; - this.additionalInfo = additionalInfo; + this.additionalInfo = getText(additionalInfo); } } diff --git a/dao/src/main/java/org/thingsboard/server/dao/sql/entityview/EntityViewRepository.java b/dao/src/main/java/org/thingsboard/server/dao/sql/entityview/EntityViewRepository.java index 71d2f04b81..6094e9b171 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/sql/entityview/EntityViewRepository.java +++ b/dao/src/main/java/org/thingsboard/server/dao/sql/entityview/EntityViewRepository.java @@ -147,7 +147,8 @@ public interface EntityViewRepository extends JpaRepository :id ORDER BY e.id") + @Query("SELECT new org.thingsboard.server.common.data.edqs.fields.EntityViewFields(e.id, e.createdTime, e.tenantId, " + + "e.customerId, e.name, e.type, e.additionalInfo, e.version) " + + "FROM EntityViewEntity e WHERE e.id > :id ORDER BY e.id") List findNextBatch(@Param("id") UUID id, Limit limit); } From 28fba7645eada65426956023074620f819fdcae4 Mon Sep 17 00:00:00 2001 From: YevhenBondarenko Date: Mon, 10 Mar 2025 12:06:34 +0100 Subject: [PATCH 258/345] added backward compatibility for api usage --- .../DefaultTbApiUsageStateService.java | 39 ++++++++++++++++--- .../apiusage/TbApiUsageStateService.java | 4 +- .../queue/DefaultTbCoreConsumerService.java | 14 +++---- common/proto/src/main/proto/queue.proto | 20 +++++++--- .../InMemoryMonolithQueueFactory.java | 4 +- .../InMemoryTbTransportQueueFactory.java | 2 +- .../provider/KafkaMonolithQueueFactory.java | 12 +++--- .../provider/KafkaTbCoreQueueFactory.java | 12 +++--- .../KafkaTbRuleEngineQueueFactory.java | 6 +-- .../KafkaTbTransportQueueFactory.java | 6 +-- .../KafkaTbVersionControlQueueFactory.java | 6 +-- .../queue/provider/TbCoreQueueFactory.java | 4 +- .../provider/TbCoreQueueProducerProvider.java | 6 +-- .../provider/TbQueueProducerProvider.java | 4 +- .../TbRuleEngineProducerProvider.java | 6 +-- .../TbTransportQueueProducerProvider.java | 6 +-- .../TbUsageStatsClientQueueFactory.java | 4 +- .../TbVersionControlProducerProvider.java | 6 +-- .../DefaultTbApiUsageReportClient.java | 18 ++++----- 19 files changed, 108 insertions(+), 71 deletions(-) diff --git a/application/src/main/java/org/thingsboard/server/service/apiusage/DefaultTbApiUsageStateService.java b/application/src/main/java/org/thingsboard/server/service/apiusage/DefaultTbApiUsageStateService.java index af4da51a45..74926297aa 100644 --- a/application/src/main/java/org/thingsboard/server/service/apiusage/DefaultTbApiUsageStateService.java +++ b/application/src/main/java/org/thingsboard/server/service/apiusage/DefaultTbApiUsageStateService.java @@ -61,7 +61,8 @@ import org.thingsboard.server.dao.tenant.TbTenantProfileCache; import org.thingsboard.server.dao.tenant.TenantService; import org.thingsboard.server.dao.timeseries.TimeseriesService; import org.thingsboard.server.dao.usagerecord.ApiUsageStateService; -import org.thingsboard.server.gen.transport.TransportProtos.ToUsageStatsServiceMsgPack; +import org.thingsboard.server.gen.transport.TransportProtos; +import org.thingsboard.server.gen.transport.TransportProtos.ToUsageStatsServiceMsg; import org.thingsboard.server.gen.transport.TransportProtos.UsageStatsKVProto; import org.thingsboard.server.queue.common.TbProtoQueueMsg; import org.thingsboard.server.queue.discovery.PartitionService; @@ -155,9 +156,30 @@ public class DefaultTbApiUsageStateService extends AbstractPartitionBasedService } @Override - public void process(TbProtoQueueMsg msgPack, TbCallback callback) { - String serviceId = msgPack.getValue().getServiceId(); - msgPack.getValue().getMsgsList().forEach(msg -> { + public void process(TbProtoQueueMsg msgPack, TbCallback callback) { + ToUsageStatsServiceMsg serviceMsg = msgPack.getValue(); + String serviceId = serviceMsg.getServiceId(); + + List msgs; + + //For backward compatibility, remove after release + if (serviceMsg.getTenantIdMSB() != 0) { + TransportProtos.UsageStatsServiceMsg oldMsg = TransportProtos.UsageStatsServiceMsg.newBuilder() + .setTenantIdMSB(serviceMsg.getTenantIdMSB()) + .setTenantIdLSB(serviceMsg.getTenantIdLSB()) + .setCustomerIdMSB(serviceMsg.getCustomerIdMSB()) + .setCustomerIdLSB(serviceMsg.getCustomerIdLSB()) + .setEntityIdMSB(serviceMsg.getEntityIdMSB()) + .setEntityIdLSB(serviceMsg.getEntityIdLSB()) + .addAllValues(serviceMsg.getValuesList()) + .build(); + + msgs = List.of(oldMsg); + } else { + msgs = serviceMsg.getMsgsList(); + } + + msgs.forEach(msg -> { TenantId tenantId = TenantId.fromUUID(new UUID(msg.getTenantIdMSB(), msg.getTenantIdLSB())); EntityId ownerId; if (msg.getCustomerIdMSB() != 0 && msg.getCustomerIdLSB() != 0) { @@ -193,7 +215,14 @@ public class DefaultTbApiUsageStateService extends AbstractPartitionBasedService updatedEntries = new ArrayList<>(ApiUsageRecordKey.values().length); Set apiFeatures = new HashSet<>(); for (UsageStatsKVProto statsItem : values) { - ApiUsageRecordKey recordKey = ProtoUtils.fromProto(statsItem.getKey()); + ApiUsageRecordKey recordKey; + + //For backward compatibility, remove after release + if (StringUtils.isNotEmpty(statsItem.getKey())) { + recordKey = ApiUsageRecordKey.valueOf(statsItem.getKey()); + } else { + recordKey = ProtoUtils.fromProto(statsItem.getKeyProto()); + } StatsCalculationResult calculationResult = usageState.calculate(recordKey, statsItem.getValue(), serviceId); if (calculationResult.isValueChanged()) { diff --git a/application/src/main/java/org/thingsboard/server/service/apiusage/TbApiUsageStateService.java b/application/src/main/java/org/thingsboard/server/service/apiusage/TbApiUsageStateService.java index 27673ccc92..5b5b1b5225 100644 --- a/application/src/main/java/org/thingsboard/server/service/apiusage/TbApiUsageStateService.java +++ b/application/src/main/java/org/thingsboard/server/service/apiusage/TbApiUsageStateService.java @@ -22,13 +22,13 @@ import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.common.data.id.TenantProfileId; import org.thingsboard.server.common.msg.queue.TbCallback; import org.thingsboard.server.common.stats.TbApiUsageStateClient; -import org.thingsboard.server.gen.transport.TransportProtos.ToUsageStatsServiceMsgPack; +import org.thingsboard.server.gen.transport.TransportProtos.ToUsageStatsServiceMsg; import org.thingsboard.server.queue.common.TbProtoQueueMsg; import org.thingsboard.server.queue.discovery.event.PartitionChangeEvent; public interface TbApiUsageStateService extends TbApiUsageStateClient, RuleEngineApiUsageStateService, ApplicationListener { - void process(TbProtoQueueMsg msg, TbCallback callback); + void process(TbProtoQueueMsg msg, TbCallback callback); void onTenantProfileUpdate(TenantProfileId tenantProfileId); diff --git a/application/src/main/java/org/thingsboard/server/service/queue/DefaultTbCoreConsumerService.java b/application/src/main/java/org/thingsboard/server/service/queue/DefaultTbCoreConsumerService.java index 6d4a108107..3aaa36b068 100644 --- a/application/src/main/java/org/thingsboard/server/service/queue/DefaultTbCoreConsumerService.java +++ b/application/src/main/java/org/thingsboard/server/service/queue/DefaultTbCoreConsumerService.java @@ -74,7 +74,7 @@ import org.thingsboard.server.gen.transport.TransportProtos.TbTimeSeriesUpdatePr import org.thingsboard.server.gen.transport.TransportProtos.ToCoreMsg; import org.thingsboard.server.gen.transport.TransportProtos.ToCoreNotificationMsg; import org.thingsboard.server.gen.transport.TransportProtos.ToOtaPackageStateServiceMsg; -import org.thingsboard.server.gen.transport.TransportProtos.ToUsageStatsServiceMsgPack; +import org.thingsboard.server.gen.transport.TransportProtos.ToUsageStatsServiceMsg; import org.thingsboard.server.gen.transport.TransportProtos.TransportToDeviceActorMsg; import org.thingsboard.server.queue.TbQueueConsumer; import org.thingsboard.server.queue.common.TbProtoQueueMsg; @@ -150,7 +150,7 @@ public class DefaultTbCoreConsumerService extends AbstractConsumerService, CoreQueueConfig> mainConsumer; - private QueueConsumerManager> usageStatsConsumer; + private QueueConsumerManager> usageStatsConsumer; private QueueConsumerManager> firmwareStatesConsumer; private volatile ListeningExecutorService deviceActivityEventsExecutor; @@ -207,7 +207,7 @@ public class DefaultTbCoreConsumerService extends AbstractConsumerService>builder() + this.usageStatsConsumer = QueueConsumerManager.>builder() .name("TB Usage Stats") .msgPackProcessor(this::processUsageStatsMsg) .pollInterval(pollInterval) @@ -402,11 +402,11 @@ public class DefaultTbCoreConsumerService extends AbstractConsumerService> msgs, TbQueueConsumer> consumer) throws Exception { - ConcurrentMap> pendingMap = msgs.stream().collect( + private void processUsageStatsMsg(List> msgs, TbQueueConsumer> consumer) throws Exception { + ConcurrentMap> pendingMap = msgs.stream().collect( Collectors.toConcurrentMap(s -> UUID.randomUUID(), Function.identity())); CountDownLatch processingTimeoutLatch = new CountDownLatch(1); - TbPackProcessingContext> ctx = new TbPackProcessingContext<>( + TbPackProcessingContext> ctx = new TbPackProcessingContext<>( processingTimeoutLatch, pendingMap, new ConcurrentHashMap<>()); pendingMap.forEach((id, msg) -> { log.trace("[{}] Creating usage stats callback for message: {}", id, msg.getValue()); @@ -453,7 +453,7 @@ public class DefaultTbCoreConsumerService extends AbstractConsumerService msg, TbCallback callback) { + private void handleUsageStats(TbProtoQueueMsg msg, TbCallback callback) { statsService.process(msg, callback); } diff --git a/common/proto/src/main/proto/queue.proto b/common/proto/src/main/proto/queue.proto index 0e318c3b30..51796d388e 100644 --- a/common/proto/src/main/proto/queue.proto +++ b/common/proto/src/main/proto/queue.proto @@ -1603,11 +1603,24 @@ message ToTransportMsg { } message UsageStatsKVProto { - ApiUsageRecordKeyProto key = 1; + string key = 1 [deprecated=true]; int64 value = 2; + ApiUsageRecordKeyProto keyProto = 3; } message ToUsageStatsServiceMsg { + int64 tenantIdMSB = 1 [deprecated=true]; + int64 tenantIdLSB = 2 [deprecated=true]; + int64 entityIdMSB = 3 [deprecated=true]; + int64 entityIdLSB = 4 [deprecated=true]; + repeated UsageStatsKVProto values = 5 [deprecated=true]; + int64 customerIdMSB = 6 [deprecated=true]; + int64 customerIdLSB = 7 [deprecated=true]; + string serviceId = 8; + repeated UsageStatsServiceMsg msgs = 9; +} + +message UsageStatsServiceMsg { int64 tenantIdMSB = 1; int64 tenantIdLSB = 2; int64 entityIdMSB = 3; @@ -1617,11 +1630,6 @@ message ToUsageStatsServiceMsg { int64 customerIdLSB = 7; } -message ToUsageStatsServiceMsgPack { - repeated ToUsageStatsServiceMsg msgs = 1; - string serviceId = 2; -} - message ToOtaPackageStateServiceMsg { int64 ts = 1; int64 tenantIdMSB = 2; diff --git a/common/queue/src/main/java/org/thingsboard/server/queue/provider/InMemoryMonolithQueueFactory.java b/common/queue/src/main/java/org/thingsboard/server/queue/provider/InMemoryMonolithQueueFactory.java index d4ca701b51..d70cad159b 100644 --- a/common/queue/src/main/java/org/thingsboard/server/queue/provider/InMemoryMonolithQueueFactory.java +++ b/common/queue/src/main/java/org/thingsboard/server/queue/provider/InMemoryMonolithQueueFactory.java @@ -140,7 +140,7 @@ public class InMemoryMonolithQueueFactory implements TbCoreQueueFactory, TbRuleE } @Override - public TbQueueConsumer> createToUsageStatsServiceMsgConsumer() { + public TbQueueConsumer> createToUsageStatsServiceMsgConsumer() { return new InMemoryTbQueueConsumer<>(storage, topicService.buildTopicName(coreSettings.getUsageStatsTopic())); } @@ -155,7 +155,7 @@ public class InMemoryMonolithQueueFactory implements TbCoreQueueFactory, TbRuleE } @Override - public TbQueueProducer> createToUsageStatsServiceMsgProducer() { + public TbQueueProducer> createToUsageStatsServiceMsgProducer() { return new InMemoryTbQueueProducer<>(storage, topicService.buildTopicName(coreSettings.getUsageStatsTopic())); } diff --git a/common/queue/src/main/java/org/thingsboard/server/queue/provider/InMemoryTbTransportQueueFactory.java b/common/queue/src/main/java/org/thingsboard/server/queue/provider/InMemoryTbTransportQueueFactory.java index 2d8e124172..4ee79aef60 100644 --- a/common/queue/src/main/java/org/thingsboard/server/queue/provider/InMemoryTbTransportQueueFactory.java +++ b/common/queue/src/main/java/org/thingsboard/server/queue/provider/InMemoryTbTransportQueueFactory.java @@ -116,7 +116,7 @@ public class InMemoryTbTransportQueueFactory implements TbTransportQueueFactory } @Override - public TbQueueProducer> createToUsageStatsServiceMsgProducer() { + public TbQueueProducer> createToUsageStatsServiceMsgProducer() { return new InMemoryTbQueueProducer<>(storage, topicService.buildTopicName(coreSettings.getUsageStatsTopic())); } diff --git a/common/queue/src/main/java/org/thingsboard/server/queue/provider/KafkaMonolithQueueFactory.java b/common/queue/src/main/java/org/thingsboard/server/queue/provider/KafkaMonolithQueueFactory.java index 18d1a3af00..dd5d61e834 100644 --- a/common/queue/src/main/java/org/thingsboard/server/queue/provider/KafkaMonolithQueueFactory.java +++ b/common/queue/src/main/java/org/thingsboard/server/queue/provider/KafkaMonolithQueueFactory.java @@ -35,7 +35,7 @@ import org.thingsboard.server.gen.transport.TransportProtos.ToOtaPackageStateSer import org.thingsboard.server.gen.transport.TransportProtos.ToRuleEngineMsg; import org.thingsboard.server.gen.transport.TransportProtos.ToRuleEngineNotificationMsg; import org.thingsboard.server.gen.transport.TransportProtos.ToTransportMsg; -import org.thingsboard.server.gen.transport.TransportProtos.ToUsageStatsServiceMsgPack; +import org.thingsboard.server.gen.transport.TransportProtos.ToUsageStatsServiceMsg; import org.thingsboard.server.gen.transport.TransportProtos.ToVersionControlServiceMsg; import org.thingsboard.server.gen.transport.TransportProtos.TransportApiRequestMsg; import org.thingsboard.server.gen.transport.TransportProtos.TransportApiResponseMsg; @@ -320,13 +320,13 @@ public class KafkaMonolithQueueFactory implements TbCoreQueueFactory, TbRuleEngi } @Override - public TbQueueConsumer> createToUsageStatsServiceMsgConsumer() { - TbKafkaConsumerTemplate.TbKafkaConsumerTemplateBuilder> consumerBuilder = TbKafkaConsumerTemplate.builder(); + public TbQueueConsumer> createToUsageStatsServiceMsgConsumer() { + TbKafkaConsumerTemplate.TbKafkaConsumerTemplateBuilder> consumerBuilder = TbKafkaConsumerTemplate.builder(); consumerBuilder.settings(kafkaSettings); consumerBuilder.topic(topicService.buildTopicName(coreSettings.getUsageStatsTopic())); consumerBuilder.clientId("monolith-us-consumer-" + serviceInfoProvider.getServiceId()); consumerBuilder.groupId(topicService.buildTopicName("monolith-us-consumer")); - consumerBuilder.decoder(msg -> new TbProtoQueueMsg<>(msg.getKey(), ToUsageStatsServiceMsgPack.parseFrom(msg.getData()), msg.getHeaders())); + consumerBuilder.decoder(msg -> new TbProtoQueueMsg<>(msg.getKey(), ToUsageStatsServiceMsg.parseFrom(msg.getData()), msg.getHeaders())); consumerBuilder.admin(coreAdmin); consumerBuilder.statsService(consumerStatsService); return consumerBuilder.build(); @@ -356,8 +356,8 @@ public class KafkaMonolithQueueFactory implements TbCoreQueueFactory, TbRuleEngi } @Override - public TbQueueProducer> createToUsageStatsServiceMsgProducer() { - TbKafkaProducerTemplate.TbKafkaProducerTemplateBuilder> requestBuilder = TbKafkaProducerTemplate.builder(); + public TbQueueProducer> createToUsageStatsServiceMsgProducer() { + TbKafkaProducerTemplate.TbKafkaProducerTemplateBuilder> requestBuilder = TbKafkaProducerTemplate.builder(); requestBuilder.settings(kafkaSettings); requestBuilder.clientId("monolith-us-producer-" + serviceInfoProvider.getServiceId()); requestBuilder.defaultTopic(topicService.buildTopicName(coreSettings.getUsageStatsTopic())); diff --git a/common/queue/src/main/java/org/thingsboard/server/queue/provider/KafkaTbCoreQueueFactory.java b/common/queue/src/main/java/org/thingsboard/server/queue/provider/KafkaTbCoreQueueFactory.java index b6a2e252ff..cc0e044917 100644 --- a/common/queue/src/main/java/org/thingsboard/server/queue/provider/KafkaTbCoreQueueFactory.java +++ b/common/queue/src/main/java/org/thingsboard/server/queue/provider/KafkaTbCoreQueueFactory.java @@ -34,7 +34,7 @@ import org.thingsboard.server.gen.transport.TransportProtos.ToOtaPackageStateSer import org.thingsboard.server.gen.transport.TransportProtos.ToRuleEngineMsg; import org.thingsboard.server.gen.transport.TransportProtos.ToRuleEngineNotificationMsg; import org.thingsboard.server.gen.transport.TransportProtos.ToTransportMsg; -import org.thingsboard.server.gen.transport.TransportProtos.ToUsageStatsServiceMsgPack; +import org.thingsboard.server.gen.transport.TransportProtos.ToUsageStatsServiceMsg; import org.thingsboard.server.gen.transport.TransportProtos.ToVersionControlServiceMsg; import org.thingsboard.server.gen.transport.TransportProtos.TransportApiRequestMsg; import org.thingsboard.server.gen.transport.TransportProtos.TransportApiResponseMsg; @@ -269,13 +269,13 @@ public class KafkaTbCoreQueueFactory implements TbCoreQueueFactory { } @Override - public TbQueueConsumer> createToUsageStatsServiceMsgConsumer() { - TbKafkaConsumerTemplate.TbKafkaConsumerTemplateBuilder> consumerBuilder = TbKafkaConsumerTemplate.builder(); + public TbQueueConsumer> createToUsageStatsServiceMsgConsumer() { + TbKafkaConsumerTemplate.TbKafkaConsumerTemplateBuilder> consumerBuilder = TbKafkaConsumerTemplate.builder(); consumerBuilder.settings(kafkaSettings); consumerBuilder.topic(topicService.buildTopicName(coreSettings.getUsageStatsTopic())); consumerBuilder.clientId("tb-core-us-consumer-" + serviceInfoProvider.getServiceId()); consumerBuilder.groupId(topicService.buildTopicName("tb-core-us-consumer")); - consumerBuilder.decoder(msg -> new TbProtoQueueMsg<>(msg.getKey(), ToUsageStatsServiceMsgPack.parseFrom(msg.getData()), msg.getHeaders())); + consumerBuilder.decoder(msg -> new TbProtoQueueMsg<>(msg.getKey(), ToUsageStatsServiceMsg.parseFrom(msg.getData()), msg.getHeaders())); consumerBuilder.admin(coreAdmin); consumerBuilder.statsService(consumerStatsService); return consumerBuilder.build(); @@ -305,8 +305,8 @@ public class KafkaTbCoreQueueFactory implements TbCoreQueueFactory { } @Override - public TbQueueProducer> createToUsageStatsServiceMsgProducer() { - TbKafkaProducerTemplate.TbKafkaProducerTemplateBuilder> requestBuilder = TbKafkaProducerTemplate.builder(); + public TbQueueProducer> createToUsageStatsServiceMsgProducer() { + TbKafkaProducerTemplate.TbKafkaProducerTemplateBuilder> requestBuilder = TbKafkaProducerTemplate.builder(); requestBuilder.settings(kafkaSettings); requestBuilder.clientId("tb-core-us-producer-" + serviceInfoProvider.getServiceId()); requestBuilder.defaultTopic(topicService.buildTopicName(coreSettings.getUsageStatsTopic())); diff --git a/common/queue/src/main/java/org/thingsboard/server/queue/provider/KafkaTbRuleEngineQueueFactory.java b/common/queue/src/main/java/org/thingsboard/server/queue/provider/KafkaTbRuleEngineQueueFactory.java index 68f5fee9c2..87a1a69c2e 100644 --- a/common/queue/src/main/java/org/thingsboard/server/queue/provider/KafkaTbRuleEngineQueueFactory.java +++ b/common/queue/src/main/java/org/thingsboard/server/queue/provider/KafkaTbRuleEngineQueueFactory.java @@ -33,7 +33,7 @@ import org.thingsboard.server.gen.transport.TransportProtos.ToOtaPackageStateSer import org.thingsboard.server.gen.transport.TransportProtos.ToRuleEngineMsg; import org.thingsboard.server.gen.transport.TransportProtos.ToRuleEngineNotificationMsg; import org.thingsboard.server.gen.transport.TransportProtos.ToTransportMsg; -import org.thingsboard.server.gen.transport.TransportProtos.ToUsageStatsServiceMsgPack; +import org.thingsboard.server.gen.transport.TransportProtos.ToUsageStatsServiceMsg; import org.thingsboard.server.queue.TbQueueAdmin; import org.thingsboard.server.queue.TbQueueConsumer; import org.thingsboard.server.queue.TbQueueProducer; @@ -274,8 +274,8 @@ public class KafkaTbRuleEngineQueueFactory implements TbRuleEngineQueueFactory { } @Override - public TbQueueProducer> createToUsageStatsServiceMsgProducer() { - TbKafkaProducerTemplate.TbKafkaProducerTemplateBuilder> requestBuilder = TbKafkaProducerTemplate.builder(); + public TbQueueProducer> createToUsageStatsServiceMsgProducer() { + TbKafkaProducerTemplate.TbKafkaProducerTemplateBuilder> requestBuilder = TbKafkaProducerTemplate.builder(); requestBuilder.settings(kafkaSettings); requestBuilder.clientId("tb-rule-engine-us-producer-" + serviceInfoProvider.getServiceId()); requestBuilder.defaultTopic(topicService.buildTopicName(coreSettings.getUsageStatsTopic())); diff --git a/common/queue/src/main/java/org/thingsboard/server/queue/provider/KafkaTbTransportQueueFactory.java b/common/queue/src/main/java/org/thingsboard/server/queue/provider/KafkaTbTransportQueueFactory.java index daa78a08f6..dd260840f5 100644 --- a/common/queue/src/main/java/org/thingsboard/server/queue/provider/KafkaTbTransportQueueFactory.java +++ b/common/queue/src/main/java/org/thingsboard/server/queue/provider/KafkaTbTransportQueueFactory.java @@ -24,7 +24,7 @@ import org.thingsboard.server.gen.transport.TransportProtos.ToCoreNotificationMs import org.thingsboard.server.gen.transport.TransportProtos.ToHousekeeperServiceMsg; import org.thingsboard.server.gen.transport.TransportProtos.ToRuleEngineMsg; import org.thingsboard.server.gen.transport.TransportProtos.ToTransportMsg; -import org.thingsboard.server.gen.transport.TransportProtos.ToUsageStatsServiceMsgPack; +import org.thingsboard.server.gen.transport.TransportProtos.ToUsageStatsServiceMsg; import org.thingsboard.server.gen.transport.TransportProtos.TransportApiRequestMsg; import org.thingsboard.server.gen.transport.TransportProtos.TransportApiResponseMsg; import org.thingsboard.server.queue.TbQueueAdmin; @@ -165,8 +165,8 @@ public class KafkaTbTransportQueueFactory implements TbTransportQueueFactory { } @Override - public TbQueueProducer> createToUsageStatsServiceMsgProducer() { - TbKafkaProducerTemplate.TbKafkaProducerTemplateBuilder> requestBuilder = TbKafkaProducerTemplate.builder(); + public TbQueueProducer> createToUsageStatsServiceMsgProducer() { + TbKafkaProducerTemplate.TbKafkaProducerTemplateBuilder> requestBuilder = TbKafkaProducerTemplate.builder(); requestBuilder.settings(kafkaSettings); requestBuilder.clientId("transport-node-us-producer-" + serviceInfoProvider.getServiceId()); requestBuilder.defaultTopic(topicService.buildTopicName(coreSettings.getUsageStatsTopic())); diff --git a/common/queue/src/main/java/org/thingsboard/server/queue/provider/KafkaTbVersionControlQueueFactory.java b/common/queue/src/main/java/org/thingsboard/server/queue/provider/KafkaTbVersionControlQueueFactory.java index 7e6c05dcae..aacebb8579 100644 --- a/common/queue/src/main/java/org/thingsboard/server/queue/provider/KafkaTbVersionControlQueueFactory.java +++ b/common/queue/src/main/java/org/thingsboard/server/queue/provider/KafkaTbVersionControlQueueFactory.java @@ -20,7 +20,7 @@ import org.springframework.boot.autoconfigure.condition.ConditionalOnExpression; import org.springframework.stereotype.Component; import org.thingsboard.server.gen.transport.TransportProtos.ToCoreNotificationMsg; import org.thingsboard.server.gen.transport.TransportProtos.ToHousekeeperServiceMsg; -import org.thingsboard.server.gen.transport.TransportProtos.ToUsageStatsServiceMsgPack; +import org.thingsboard.server.gen.transport.TransportProtos.ToUsageStatsServiceMsg; import org.thingsboard.server.gen.transport.TransportProtos.ToVersionControlServiceMsg; import org.thingsboard.server.queue.TbQueueAdmin; import org.thingsboard.server.queue.TbQueueConsumer; @@ -98,8 +98,8 @@ public class KafkaTbVersionControlQueueFactory implements TbVersionControlQueueF } @Override - public TbQueueProducer> createToUsageStatsServiceMsgProducer() { - TbKafkaProducerTemplate.TbKafkaProducerTemplateBuilder> requestBuilder = TbKafkaProducerTemplate.builder(); + public TbQueueProducer> createToUsageStatsServiceMsgProducer() { + TbKafkaProducerTemplate.TbKafkaProducerTemplateBuilder> requestBuilder = TbKafkaProducerTemplate.builder(); requestBuilder.settings(kafkaSettings); requestBuilder.clientId("tb-vc-us-producer-" + serviceInfoProvider.getServiceId()); requestBuilder.defaultTopic(topicService.buildTopicName(coreSettings.getUsageStatsTopic())); diff --git a/common/queue/src/main/java/org/thingsboard/server/queue/provider/TbCoreQueueFactory.java b/common/queue/src/main/java/org/thingsboard/server/queue/provider/TbCoreQueueFactory.java index b9ccfd1866..c4002f4d3e 100644 --- a/common/queue/src/main/java/org/thingsboard/server/queue/provider/TbCoreQueueFactory.java +++ b/common/queue/src/main/java/org/thingsboard/server/queue/provider/TbCoreQueueFactory.java @@ -28,7 +28,7 @@ import org.thingsboard.server.gen.transport.TransportProtos.ToOtaPackageStateSer import org.thingsboard.server.gen.transport.TransportProtos.ToRuleEngineMsg; import org.thingsboard.server.gen.transport.TransportProtos.ToRuleEngineNotificationMsg; import org.thingsboard.server.gen.transport.TransportProtos.ToTransportMsg; -import org.thingsboard.server.gen.transport.TransportProtos.ToUsageStatsServiceMsgPack; +import org.thingsboard.server.gen.transport.TransportProtos.ToUsageStatsServiceMsg; import org.thingsboard.server.gen.transport.TransportProtos.ToVersionControlServiceMsg; import org.thingsboard.server.gen.transport.TransportProtos.TransportApiRequestMsg; import org.thingsboard.server.gen.transport.TransportProtos.TransportApiResponseMsg; @@ -91,7 +91,7 @@ public interface TbCoreQueueFactory extends TbUsageStatsClientQueueFactory, Hous * * @return */ - TbQueueConsumer> createToUsageStatsServiceMsgConsumer(); + TbQueueConsumer> createToUsageStatsServiceMsgConsumer(); /** * Used to consume messages about firmware update notifications by TB Core Service diff --git a/common/queue/src/main/java/org/thingsboard/server/queue/provider/TbCoreQueueProducerProvider.java b/common/queue/src/main/java/org/thingsboard/server/queue/provider/TbCoreQueueProducerProvider.java index 386a525b28..9cf18e6cb4 100644 --- a/common/queue/src/main/java/org/thingsboard/server/queue/provider/TbCoreQueueProducerProvider.java +++ b/common/queue/src/main/java/org/thingsboard/server/queue/provider/TbCoreQueueProducerProvider.java @@ -26,7 +26,7 @@ import org.thingsboard.server.gen.transport.TransportProtos.ToHousekeeperService import org.thingsboard.server.gen.transport.TransportProtos.ToRuleEngineMsg; import org.thingsboard.server.gen.transport.TransportProtos.ToRuleEngineNotificationMsg; import org.thingsboard.server.gen.transport.TransportProtos.ToTransportMsg; -import org.thingsboard.server.gen.transport.TransportProtos.ToUsageStatsServiceMsgPack; +import org.thingsboard.server.gen.transport.TransportProtos.ToUsageStatsServiceMsg; import org.thingsboard.server.gen.transport.TransportProtos.ToVersionControlServiceMsg; import org.thingsboard.server.queue.TbQueueProducer; import org.thingsboard.server.queue.common.TbProtoQueueMsg; @@ -45,7 +45,7 @@ public class TbCoreQueueProducerProvider implements TbQueueProducerProvider { private TbQueueProducer> toEdge; private TbQueueProducer> toEdgeNotifications; private TbQueueProducer> toEdgeEvents; - private TbQueueProducer> toUsageStats; + private TbQueueProducer> toUsageStats; private TbQueueProducer> toVersionControl; private TbQueueProducer> toHousekeeper; @@ -94,7 +94,7 @@ public class TbCoreQueueProducerProvider implements TbQueueProducerProvider { } @Override - public TbQueueProducer> getTbUsageStatsMsgProducer() { + public TbQueueProducer> getTbUsageStatsMsgProducer() { return toUsageStats; } diff --git a/common/queue/src/main/java/org/thingsboard/server/queue/provider/TbQueueProducerProvider.java b/common/queue/src/main/java/org/thingsboard/server/queue/provider/TbQueueProducerProvider.java index d5c011c7b9..ec31763baa 100644 --- a/common/queue/src/main/java/org/thingsboard/server/queue/provider/TbQueueProducerProvider.java +++ b/common/queue/src/main/java/org/thingsboard/server/queue/provider/TbQueueProducerProvider.java @@ -24,7 +24,7 @@ import org.thingsboard.server.gen.transport.TransportProtos.ToHousekeeperService import org.thingsboard.server.gen.transport.TransportProtos.ToRuleEngineMsg; import org.thingsboard.server.gen.transport.TransportProtos.ToRuleEngineNotificationMsg; import org.thingsboard.server.gen.transport.TransportProtos.ToTransportMsg; -import org.thingsboard.server.gen.transport.TransportProtos.ToUsageStatsServiceMsgPack; +import org.thingsboard.server.gen.transport.TransportProtos.ToUsageStatsServiceMsg; import org.thingsboard.server.gen.transport.TransportProtos.ToVersionControlServiceMsg; import org.thingsboard.server.queue.TbQueueProducer; import org.thingsboard.server.queue.common.TbProtoQueueMsg; @@ -74,7 +74,7 @@ public interface TbQueueProducerProvider { * * @return */ - TbQueueProducer> getTbUsageStatsMsgProducer(); + TbQueueProducer> getTbUsageStatsMsgProducer(); /** * Used to push messages to other instances of TB Core Service diff --git a/common/queue/src/main/java/org/thingsboard/server/queue/provider/TbRuleEngineProducerProvider.java b/common/queue/src/main/java/org/thingsboard/server/queue/provider/TbRuleEngineProducerProvider.java index 835b02ec8e..e9f7773a26 100644 --- a/common/queue/src/main/java/org/thingsboard/server/queue/provider/TbRuleEngineProducerProvider.java +++ b/common/queue/src/main/java/org/thingsboard/server/queue/provider/TbRuleEngineProducerProvider.java @@ -27,7 +27,7 @@ import org.thingsboard.server.gen.transport.TransportProtos.ToHousekeeperService import org.thingsboard.server.gen.transport.TransportProtos.ToRuleEngineMsg; import org.thingsboard.server.gen.transport.TransportProtos.ToRuleEngineNotificationMsg; import org.thingsboard.server.gen.transport.TransportProtos.ToTransportMsg; -import org.thingsboard.server.gen.transport.TransportProtos.ToUsageStatsServiceMsgPack; +import org.thingsboard.server.gen.transport.TransportProtos.ToUsageStatsServiceMsg; import org.thingsboard.server.gen.transport.TransportProtos.ToVersionControlServiceMsg; import org.thingsboard.server.queue.TbQueueProducer; import org.thingsboard.server.queue.common.TbProtoQueueMsg; @@ -42,7 +42,7 @@ public class TbRuleEngineProducerProvider implements TbQueueProducerProvider { private TbQueueProducer> toTbCore; private TbQueueProducer> toRuleEngineNotifications; private TbQueueProducer> toTbCoreNotifications; - private TbQueueProducer> toUsageStats; + private TbQueueProducer> toUsageStats; private TbQueueProducer> toHousekeeper; private TbQueueProducer> toEdge; private TbQueueProducer> toEdgeNotifications; @@ -107,7 +107,7 @@ public class TbRuleEngineProducerProvider implements TbQueueProducerProvider { } @Override - public TbQueueProducer> getTbUsageStatsMsgProducer() { + public TbQueueProducer> getTbUsageStatsMsgProducer() { return toUsageStats; } diff --git a/common/queue/src/main/java/org/thingsboard/server/queue/provider/TbTransportQueueProducerProvider.java b/common/queue/src/main/java/org/thingsboard/server/queue/provider/TbTransportQueueProducerProvider.java index 689d236de7..7960cbcf32 100644 --- a/common/queue/src/main/java/org/thingsboard/server/queue/provider/TbTransportQueueProducerProvider.java +++ b/common/queue/src/main/java/org/thingsboard/server/queue/provider/TbTransportQueueProducerProvider.java @@ -27,7 +27,7 @@ import org.thingsboard.server.gen.transport.TransportProtos.ToHousekeeperService import org.thingsboard.server.gen.transport.TransportProtos.ToRuleEngineMsg; import org.thingsboard.server.gen.transport.TransportProtos.ToRuleEngineNotificationMsg; import org.thingsboard.server.gen.transport.TransportProtos.ToTransportMsg; -import org.thingsboard.server.gen.transport.TransportProtos.ToUsageStatsServiceMsgPack; +import org.thingsboard.server.gen.transport.TransportProtos.ToUsageStatsServiceMsg; import org.thingsboard.server.gen.transport.TransportProtos.ToVersionControlServiceMsg; import org.thingsboard.server.queue.TbQueueProducer; import org.thingsboard.server.queue.common.TbProtoQueueMsg; @@ -40,7 +40,7 @@ public class TbTransportQueueProducerProvider implements TbQueueProducerProvider private TbQueueProducer> toRuleEngine; private TbQueueProducer> toTbCore; private TbQueueProducer> toTbCoreNotifications; - private TbQueueProducer> toUsageStats; + private TbQueueProducer> toUsageStats; private TbQueueProducer> toHousekeeper; public TbTransportQueueProducerProvider(TbTransportQueueFactory tbQueueProvider) { @@ -102,7 +102,7 @@ public class TbTransportQueueProducerProvider implements TbQueueProducerProvider } @Override - public TbQueueProducer> getTbUsageStatsMsgProducer() { + public TbQueueProducer> getTbUsageStatsMsgProducer() { return toUsageStats; } diff --git a/common/queue/src/main/java/org/thingsboard/server/queue/provider/TbUsageStatsClientQueueFactory.java b/common/queue/src/main/java/org/thingsboard/server/queue/provider/TbUsageStatsClientQueueFactory.java index c4c5f5215a..0c551fc998 100644 --- a/common/queue/src/main/java/org/thingsboard/server/queue/provider/TbUsageStatsClientQueueFactory.java +++ b/common/queue/src/main/java/org/thingsboard/server/queue/provider/TbUsageStatsClientQueueFactory.java @@ -15,12 +15,12 @@ */ package org.thingsboard.server.queue.provider; -import org.thingsboard.server.gen.transport.TransportProtos.ToUsageStatsServiceMsgPack; +import org.thingsboard.server.gen.transport.TransportProtos.ToUsageStatsServiceMsg; import org.thingsboard.server.queue.TbQueueProducer; import org.thingsboard.server.queue.common.TbProtoQueueMsg; public interface TbUsageStatsClientQueueFactory { - TbQueueProducer> createToUsageStatsServiceMsgProducer(); + TbQueueProducer> createToUsageStatsServiceMsgProducer(); } diff --git a/common/queue/src/main/java/org/thingsboard/server/queue/provider/TbVersionControlProducerProvider.java b/common/queue/src/main/java/org/thingsboard/server/queue/provider/TbVersionControlProducerProvider.java index 400ccfc085..cd4fa12df0 100644 --- a/common/queue/src/main/java/org/thingsboard/server/queue/provider/TbVersionControlProducerProvider.java +++ b/common/queue/src/main/java/org/thingsboard/server/queue/provider/TbVersionControlProducerProvider.java @@ -27,7 +27,7 @@ import org.thingsboard.server.gen.transport.TransportProtos.ToHousekeeperService import org.thingsboard.server.gen.transport.TransportProtos.ToRuleEngineMsg; import org.thingsboard.server.gen.transport.TransportProtos.ToRuleEngineNotificationMsg; import org.thingsboard.server.gen.transport.TransportProtos.ToTransportMsg; -import org.thingsboard.server.gen.transport.TransportProtos.ToUsageStatsServiceMsgPack; +import org.thingsboard.server.gen.transport.TransportProtos.ToUsageStatsServiceMsg; import org.thingsboard.server.gen.transport.TransportProtos.ToVersionControlServiceMsg; import org.thingsboard.server.queue.TbQueueProducer; import org.thingsboard.server.queue.common.TbProtoQueueMsg; @@ -38,7 +38,7 @@ public class TbVersionControlProducerProvider implements TbQueueProducerProvider private final TbVersionControlQueueFactory tbQueueProvider; private TbQueueProducer> toTbCoreNotifications; - private TbQueueProducer> toUsageStats; + private TbQueueProducer> toUsageStats; private TbQueueProducer> toHousekeeper; public TbVersionControlProducerProvider(TbVersionControlQueueFactory tbQueueProvider) { @@ -98,7 +98,7 @@ public class TbVersionControlProducerProvider implements TbQueueProducerProvider } @Override - public TbQueueProducer> getTbUsageStatsMsgProducer() { + public TbQueueProducer> getTbUsageStatsMsgProducer() { return toUsageStats; } diff --git a/common/queue/src/main/java/org/thingsboard/server/queue/usagestats/DefaultTbApiUsageReportClient.java b/common/queue/src/main/java/org/thingsboard/server/queue/usagestats/DefaultTbApiUsageReportClient.java index 8c0439362b..d26042d114 100644 --- a/common/queue/src/main/java/org/thingsboard/server/queue/usagestats/DefaultTbApiUsageReportClient.java +++ b/common/queue/src/main/java/org/thingsboard/server/queue/usagestats/DefaultTbApiUsageReportClient.java @@ -32,8 +32,8 @@ import org.thingsboard.server.common.msg.queue.TopicPartitionInfo; import org.thingsboard.server.common.stats.TbApiUsageReportClient; import org.thingsboard.server.common.util.ProtoUtils; import org.thingsboard.server.gen.transport.TransportProtos; +import org.thingsboard.server.gen.transport.TransportProtos.UsageStatsServiceMsg; import org.thingsboard.server.gen.transport.TransportProtos.ToUsageStatsServiceMsg; -import org.thingsboard.server.gen.transport.TransportProtos.ToUsageStatsServiceMsgPack; import org.thingsboard.server.gen.transport.TransportProtos.UsageStatsKVProto; import org.thingsboard.server.queue.TbQueueProducer; import org.thingsboard.server.queue.common.TbProtoQueueMsg; @@ -74,7 +74,7 @@ public class DefaultTbApiUsageReportClient implements TbApiUsageReportClient { private final TbServiceInfoProvider serviceInfoProvider; private final SchedulerComponent scheduler; private final TbQueueProducerProvider producerProvider; - private TbQueueProducer> msgProducer; + private TbQueueProducer> msgProducer; @PostConstruct private void init() { @@ -94,7 +94,7 @@ public class DefaultTbApiUsageReportClient implements TbApiUsageReportClient { } private void reportStats() { - ConcurrentMap report = new ConcurrentHashMap<>(); + ConcurrentMap report = new ConcurrentHashMap<>(); for (ApiUsageRecordKey key : ApiUsageRecordKey.values()) { ConcurrentMap statsForKey = stats.get(key); @@ -102,8 +102,8 @@ public class DefaultTbApiUsageReportClient implements TbApiUsageReportClient { long value = statsValue.get(); if (value == 0 && key.isCounter()) return; - ToUsageStatsServiceMsg.Builder statsMsg = report.computeIfAbsent(reportLevel.getParentEntity(), parent -> { - ToUsageStatsServiceMsg.Builder newStatsMsg = ToUsageStatsServiceMsg.newBuilder(); + UsageStatsServiceMsg.Builder statsMsg = report.computeIfAbsent(reportLevel.getParentEntity(), parent -> { + UsageStatsServiceMsg.Builder newStatsMsg = UsageStatsServiceMsg.newBuilder(); TenantId tenantId = parent.getTenantId(); newStatsMsg.setTenantIdMSB(tenantId.getId().getMostSignificantBits()); @@ -119,13 +119,13 @@ public class DefaultTbApiUsageReportClient implements TbApiUsageReportClient { }); UsageStatsKVProto.Builder statsItem = UsageStatsKVProto.newBuilder() - .setKey(ProtoUtils.toProto(key)) + .setKeyProto(ProtoUtils.toProto(key)) .setValue(value); statsMsg.addValues(statsItem.build()); }); } - Map> reportStatsPerTpi = new HashMap<>(); + Map> reportStatsPerTpi = new HashMap<>(); report.forEach((parent, statsMsg) -> { try { @@ -152,11 +152,11 @@ public class DefaultTbApiUsageReportClient implements TbApiUsageReportClient { } } - private List toMsgPack(List list) { + private List toMsgPack(List list) { return Lists.partition(list, packSize) .stream() .map(partition -> - ToUsageStatsServiceMsgPack.newBuilder() + ToUsageStatsServiceMsg.newBuilder() .addAllMsgs(partition) .setServiceId(serviceInfoProvider.getServiceId()) .build()) From aa2df212df19d119f9dd6208177f7949e027627f Mon Sep 17 00:00:00 2001 From: YevhenBondarenko Date: Mon, 10 Mar 2025 12:42:29 +0100 Subject: [PATCH 259/345] refactoring accorrding to the comments --- .../rule/engine/kafka/TbKafkaNodeConfiguration.java | 4 ---- 1 file changed, 4 deletions(-) diff --git a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/kafka/TbKafkaNodeConfiguration.java b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/kafka/TbKafkaNodeConfiguration.java index b3f1fe1473..9a31e58ce7 100644 --- a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/kafka/TbKafkaNodeConfiguration.java +++ b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/kafka/TbKafkaNodeConfiguration.java @@ -15,17 +15,13 @@ */ package org.thingsboard.rule.engine.kafka; -import com.fasterxml.jackson.annotation.JsonIgnore; -import com.fasterxml.jackson.annotation.JsonIgnoreProperties; import lombok.Data; -import org.apache.kafka.common.serialization.StringSerializer; import org.thingsboard.rule.engine.api.NodeConfiguration; import java.util.Collections; import java.util.Map; @Data -@JsonIgnoreProperties(ignoreUnknown = true) public class TbKafkaNodeConfiguration implements NodeConfiguration { private String topicPattern; From 7f3f69b0f66af99a9c07f7ff2aa4c2f756935ef0 Mon Sep 17 00:00:00 2001 From: YevhenBondarenko Date: Mon, 10 Mar 2025 13:48:55 +0100 Subject: [PATCH 260/345] fixed tests --- .../cf/ctx/state/ScriptCalculatedFieldStateTest.java | 7 ++++++- .../server/service/script/AbstractTbelInvokeTest.java | 5 +++-- 2 files changed, 9 insertions(+), 3 deletions(-) diff --git a/application/src/test/java/org/thingsboard/server/service/cf/ctx/state/ScriptCalculatedFieldStateTest.java b/application/src/test/java/org/thingsboard/server/service/cf/ctx/state/ScriptCalculatedFieldStateTest.java index 320093e8f4..6060d3f8a6 100644 --- a/application/src/test/java/org/thingsboard/server/service/cf/ctx/state/ScriptCalculatedFieldStateTest.java +++ b/application/src/test/java/org/thingsboard/server/service/cf/ctx/state/ScriptCalculatedFieldStateTest.java @@ -15,6 +15,7 @@ */ package org.thingsboard.server.service.cf.ctx.state; +import io.micrometer.core.instrument.simple.SimpleMeterRegistry; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; @@ -38,6 +39,7 @@ import org.thingsboard.server.common.data.id.DeviceId; import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.common.data.kv.DoubleDataEntry; import org.thingsboard.server.common.data.kv.LongDataEntry; +import org.thingsboard.server.common.stats.DefaultStatsFactory; import org.thingsboard.server.dao.usagerecord.ApiLimitService; import org.thingsboard.server.service.cf.CalculatedFieldResult; @@ -51,7 +53,7 @@ import static org.assertj.core.api.Assertions.assertThat; import static org.mockito.ArgumentMatchers.any; import static org.mockito.Mockito.when; -@SpringBootTest(classes = DefaultTbelInvokeService.class) +@SpringBootTest(classes = {SimpleMeterRegistry.class, DefaultStatsFactory.class, DefaultTbelInvokeService.class}) public class ScriptCalculatedFieldStateTest { private final TenantId TENANT_ID = TenantId.fromUUID(UUID.fromString("5b18e321-3327-4290-b996-d72a65e90382")); @@ -72,6 +74,9 @@ public class ScriptCalculatedFieldStateTest { @MockBean private ApiLimitService apiLimitService; +// @MockBean +// private MeterRegistry meterRegistry; + @BeforeEach void setUp() { when(apiLimitService.getLimit(any(), any())).thenReturn(1000L); diff --git a/application/src/test/java/org/thingsboard/server/service/script/AbstractTbelInvokeTest.java b/application/src/test/java/org/thingsboard/server/service/script/AbstractTbelInvokeTest.java index f8563e5e2a..e3383e32a8 100644 --- a/application/src/test/java/org/thingsboard/server/service/script/AbstractTbelInvokeTest.java +++ b/application/src/test/java/org/thingsboard/server/service/script/AbstractTbelInvokeTest.java @@ -15,6 +15,7 @@ */ package org.thingsboard.server.service.script; +import io.micrometer.core.instrument.simple.SimpleMeterRegistry; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.context.SpringBootTest; import org.thingsboard.common.util.JacksonUtil; @@ -22,7 +23,7 @@ import org.thingsboard.script.api.ScriptType; import org.thingsboard.script.api.tbel.DefaultTbelInvokeService; import org.thingsboard.script.api.tbel.TbelInvokeService; import org.thingsboard.server.common.data.id.TenantId; -import org.thingsboard.server.controller.AbstractControllerTest; +import org.thingsboard.server.common.stats.DefaultStatsFactory; import java.util.Map; import java.util.UUID; @@ -30,7 +31,7 @@ import java.util.concurrent.ExecutionException; import static org.thingsboard.server.common.data.msg.TbMsgType.POST_TELEMETRY_REQUEST; -@SpringBootTest(classes = DefaultTbelInvokeService.class) +@SpringBootTest(classes = {SimpleMeterRegistry.class, DefaultStatsFactory.class, DefaultTbelInvokeService.class }) public abstract class AbstractTbelInvokeTest { @Autowired From 9b870126f117e76db03c32c3ad7ba2fe0e6ead12 Mon Sep 17 00:00:00 2001 From: IrynaMatveieva Date: Mon, 10 Mar 2025 15:27:59 +0200 Subject: [PATCH 261/345] added util method --- .../CalculatedFieldEntityMessageProcessor.java | 4 ++-- .../CalculatedFieldManagerMessageProcessor.java | 10 +++++----- .../service/cf/ctx/state/CalculatedFieldCtx.java | 9 --------- .../java/org/thingsboard/script/api/tbel/TbUtils.java | 8 ++++++-- .../org/thingsboard/script/api/tbel/TbUtilsTest.java | 8 +++++++- 5 files changed, 20 insertions(+), 19 deletions(-) diff --git a/application/src/main/java/org/thingsboard/server/actors/calculatedField/CalculatedFieldEntityMessageProcessor.java b/application/src/main/java/org/thingsboard/server/actors/calculatedField/CalculatedFieldEntityMessageProcessor.java index 9ab019097b..4ff630bc59 100644 --- a/application/src/main/java/org/thingsboard/server/actors/calculatedField/CalculatedFieldEntityMessageProcessor.java +++ b/application/src/main/java/org/thingsboard/server/actors/calculatedField/CalculatedFieldEntityMessageProcessor.java @@ -153,7 +153,7 @@ public class CalculatedFieldEntityMessageProcessor extends AbstractContextAwareM } public void process(EntityCalculatedFieldTelemetryMsg msg) throws CalculatedFieldException { - log.info("[{}] Processing CF telemetry msg.", msg.getEntityId()); + log.debug("[{}] Processing CF telemetry msg.", msg.getEntityId()); var proto = msg.getProto(); var numberOfCallbacks = CALLBACKS_PER_CF * (msg.getEntityIdFields().size() + msg.getProfileIdFields().size()); MultipleTbCallback callback = new MultipleTbCallback(numberOfCallbacks, msg.getCallback()); @@ -168,7 +168,7 @@ public class CalculatedFieldEntityMessageProcessor extends AbstractContextAwareM } public void process(EntityCalculatedFieldLinkedTelemetryMsg msg) throws CalculatedFieldException { - log.info("[{}] Processing CF link telemetry msg.", msg.getEntityId()); + log.debug("[{}] Processing CF link telemetry msg.", msg.getEntityId()); var proto = msg.getProto(); var ctx = msg.getCtx(); var callback = new MultipleTbCallback(CALLBACKS_PER_CF, msg.getCallback()); diff --git a/application/src/main/java/org/thingsboard/server/actors/calculatedField/CalculatedFieldManagerMessageProcessor.java b/application/src/main/java/org/thingsboard/server/actors/calculatedField/CalculatedFieldManagerMessageProcessor.java index 1418821081..b0185fd555 100644 --- a/application/src/main/java/org/thingsboard/server/actors/calculatedField/CalculatedFieldManagerMessageProcessor.java +++ b/application/src/main/java/org/thingsboard/server/actors/calculatedField/CalculatedFieldManagerMessageProcessor.java @@ -318,14 +318,14 @@ public class CalculatedFieldManagerMessageProcessor extends AbstractContextAware public void onTelemetryMsg(CalculatedFieldTelemetryMsg msg) { EntityId entityId = msg.getEntityId(); - log.info("Received telemetry msg from entity [{}]", entityId); + log.debug("Received telemetry msg from entity [{}]", entityId); // 2 = 1 for CF processing + 1 for links processing MultipleTbCallback callback = new MultipleTbCallback(2, msg.getCallback()); // process all cfs related to entity, or it's profile; var entityIdFields = getCalculatedFieldsByEntityId(entityId); var profileIdFields = getCalculatedFieldsByEntityId(getProfileId(tenantId, entityId)); if (!entityIdFields.isEmpty() || !profileIdFields.isEmpty()) { - log.info("Pushing telemetry msg to specific actor [{}]", entityId); + log.debug("Pushing telemetry msg to specific actor [{}]", entityId); getOrCreateActor(entityId).tell(new EntityCalculatedFieldTelemetryMsg(msg, entityIdFields, profileIdFields, callback)); } else { callback.onSuccess(); @@ -342,7 +342,7 @@ public class CalculatedFieldManagerMessageProcessor extends AbstractContextAware public void onLinkedTelemetryMsg(CalculatedFieldLinkedTelemetryMsg msg) { EntityId sourceEntityId = msg.getEntityId(); - log.info("Received linked telemetry msg from entity [{}]", sourceEntityId); + log.debug("Received linked telemetry msg from entity [{}]", sourceEntityId); var proto = msg.getProto(); var linksList = proto.getLinksList(); for (var linkProto : linksList) { @@ -357,14 +357,14 @@ public class CalculatedFieldManagerMessageProcessor extends AbstractContextAware MultipleTbCallback callback = new MultipleTbCallback(entityIds.size(), msg.getCallback()); var newMsg = new EntityCalculatedFieldLinkedTelemetryMsg(tenantId, sourceEntityId, proto.getMsg(), cf, callback); entityIds.forEach(entityId -> { - log.info("Pushing linked telemetry msg to specific actor [{}]", entityId); + log.debug("Pushing linked telemetry msg to specific actor [{}]", entityId); getOrCreateActor(entityId).tell(newMsg); }); } else { msg.getCallback().onSuccess(); } } else { - log.info("Pushing linked telemetry msg to specific actor [{}]", targetEntityId); + log.debug("Pushing linked telemetry msg to specific actor [{}]", targetEntityId); var newMsg = new EntityCalculatedFieldLinkedTelemetryMsg(tenantId, sourceEntityId, proto.getMsg(), cf, msg.getCallback()); getOrCreateActor(targetEntityId).tell(newMsg); } diff --git a/application/src/main/java/org/thingsboard/server/service/cf/ctx/state/CalculatedFieldCtx.java b/application/src/main/java/org/thingsboard/server/service/cf/ctx/state/CalculatedFieldCtx.java index e5a3d0e05e..c51aaa2e72 100644 --- a/application/src/main/java/org/thingsboard/server/service/cf/ctx/state/CalculatedFieldCtx.java +++ b/application/src/main/java/org/thingsboard/server/service/cf/ctx/state/CalculatedFieldCtx.java @@ -34,7 +34,6 @@ import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.common.data.kv.AttributeKvEntry; import org.thingsboard.server.common.data.kv.TsKvEntry; import org.thingsboard.server.common.data.tenant.profile.DefaultTenantProfileConfiguration; -import org.thingsboard.server.common.data.util.TbPair; import org.thingsboard.server.common.util.ProtoUtils; import org.thingsboard.server.dao.usagerecord.ApiLimitService; import org.thingsboard.server.gen.transport.TransportProtos.CalculatedFieldTelemetryMsgProto; @@ -44,7 +43,6 @@ import java.util.ArrayList; import java.util.HashMap; import java.util.List; import java.util.Map; -import java.util.stream.Collectors; @Data public class CalculatedFieldCtx { @@ -58,8 +56,6 @@ public class CalculatedFieldCtx { private final Map arguments; private final Map mainEntityArguments; private final Map> linkedEntityArguments; - - private final Map, String> referencedEntityKeys; private final List argNames; private Output output; private String expression; @@ -93,11 +89,6 @@ public class CalculatedFieldCtx { linkedEntityArguments.computeIfAbsent(refId, key -> new HashMap<>()).put(refKey, entry.getKey()); } } - this.referencedEntityKeys = arguments.entrySet().stream() - .collect(Collectors.toMap( - entry -> new TbPair<>(entry.getValue().getRefEntityId() == null ? entityId : entry.getValue().getRefEntityId(), entry.getValue().getRefEntityKey()), - Map.Entry::getKey - )); this.argNames = new ArrayList<>(arguments.keySet()); this.output = configuration.getOutput(); this.expression = configuration.getExpression(); diff --git a/common/script/script-api/src/main/java/org/thingsboard/script/api/tbel/TbUtils.java b/common/script/script-api/src/main/java/org/thingsboard/script/api/tbel/TbUtils.java index ca3a8db02a..95fff48883 100644 --- a/common/script/script-api/src/main/java/org/thingsboard/script/api/tbel/TbUtils.java +++ b/common/script/script-api/src/main/java/org/thingsboard/script/api/tbel/TbUtils.java @@ -18,8 +18,6 @@ package org.thingsboard.script.api.tbel; import com.google.common.primitives.Bytes; import lombok.extern.slf4j.Slf4j; import org.apache.commons.lang3.ArrayUtils; -import org.mvel2.ConversionHandler; -import org.mvel2.DataConversion; import org.mvel2.ExecutionContext; import org.mvel2.ParserConfiguration; import org.mvel2.execution.ExecutionArrayList; @@ -259,6 +257,8 @@ public class TbUtils { float.class, int.class))); parserConfig.addImport("toInt", new MethodStub(TbUtils.class.getMethod("toInt", double.class))); + parserConfig.addImport("isNaN", new MethodStub(TbUtils.class.getMethod("isNaN", + double.class))); parserConfig.addImport("hexToBytes", new MethodStub(TbUtils.class.getMethod("hexToBytes", ExecutionContext.class, String.class))); parserConfig.addImport("hexToBytesArray", new MethodStub(TbUtils.class.getMethod("hexToBytesArray", @@ -1163,6 +1163,10 @@ public class TbUtils { return BigDecimal.valueOf(value).setScale(0, RoundingMode.HALF_UP).intValue(); } + public static boolean isNaN(double value) { + return Double.isNaN(value); + } + public static ExecutionHashMap toFlatMap(ExecutionContext ctx, Map json) { return toFlatMap(ctx, json, new ArrayList<>(), true); } diff --git a/common/script/script-api/src/test/java/org/thingsboard/script/api/tbel/TbUtilsTest.java b/common/script/script-api/src/test/java/org/thingsboard/script/api/tbel/TbUtilsTest.java index 69987d3132..e3860ed89a 100644 --- a/common/script/script-api/src/test/java/org/thingsboard/script/api/tbel/TbUtilsTest.java +++ b/common/script/script-api/src/test/java/org/thingsboard/script/api/tbel/TbUtilsTest.java @@ -1138,12 +1138,18 @@ public class TbUtilsTest { } @Test - void toInt() { + public void toInt() { Assertions.assertEquals(1729, TbUtils.toInt(doubleVal)); Assertions.assertEquals(13, TbUtils.toInt(12.8)); Assertions.assertEquals(28, TbUtils.toInt(28.0)); } + @Test + public void isNaN() { + Assertions.assertFalse(TbUtils.isNaN(doubleVal)); + Assertions.assertTrue(TbUtils.isNaN(Double.NaN)); + } + private static List toList(byte[] data) { List result = new ArrayList<>(data.length); for (Byte b : data) { From dc680ab00771c891c1fdf675cf72c72a6288cea3 Mon Sep 17 00:00:00 2001 From: Andrii Landiak Date: Mon, 10 Mar 2025 15:50:47 +0200 Subject: [PATCH 262/345] Fix createEdgeEventMsgConsumer --- .../server/queue/provider/KafkaMonolithQueueFactory.java | 2 +- .../server/queue/provider/KafkaTbCoreQueueFactory.java | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/common/queue/src/main/java/org/thingsboard/server/queue/provider/KafkaMonolithQueueFactory.java b/common/queue/src/main/java/org/thingsboard/server/queue/provider/KafkaMonolithQueueFactory.java index bb9c1a028e..f3d1e2d158 100644 --- a/common/queue/src/main/java/org/thingsboard/server/queue/provider/KafkaMonolithQueueFactory.java +++ b/common/queue/src/main/java/org/thingsboard/server/queue/provider/KafkaMonolithQueueFactory.java @@ -494,7 +494,7 @@ public class KafkaMonolithQueueFactory implements TbCoreQueueFactory, TbRuleEngi public TbQueueConsumer> createEdgeEventMsgConsumer(TenantId tenantId, EdgeId edgeId) { TbKafkaConsumerTemplate.TbKafkaConsumerTemplateBuilder> consumerBuilder = TbKafkaConsumerTemplate.builder(); consumerBuilder.settings(kafkaSettings); - consumerBuilder.topic(topicService.buildTopicName("tb_edge_event.notifications." + tenantId + "." + edgeId)); + consumerBuilder.topic(topicService.buildEdgeEventNotificationsTopicPartitionInfo(tenantId, edgeId).getTopic()); consumerBuilder.clientId("monolith-to-edge-event-consumer-" + serviceInfoProvider.getServiceId() + "-" + edgeConsumerCount.incrementAndGet()); consumerBuilder.groupId(topicService.buildTopicName("monolith-edge-event-consumer")); consumerBuilder.decoder(msg -> new TbProtoQueueMsg<>(msg.getKey(), ToEdgeEventNotificationMsg.parseFrom(msg.getData()), msg.getHeaders())); diff --git a/common/queue/src/main/java/org/thingsboard/server/queue/provider/KafkaTbCoreQueueFactory.java b/common/queue/src/main/java/org/thingsboard/server/queue/provider/KafkaTbCoreQueueFactory.java index f3adb266a4..3c6d144a0c 100644 --- a/common/queue/src/main/java/org/thingsboard/server/queue/provider/KafkaTbCoreQueueFactory.java +++ b/common/queue/src/main/java/org/thingsboard/server/queue/provider/KafkaTbCoreQueueFactory.java @@ -440,7 +440,7 @@ public class KafkaTbCoreQueueFactory implements TbCoreQueueFactory { public TbQueueConsumer> createEdgeEventMsgConsumer(TenantId tenantId, EdgeId edgeId) { TbKafkaConsumerTemplate.TbKafkaConsumerTemplateBuilder> consumerBuilder = TbKafkaConsumerTemplate.builder(); consumerBuilder.settings(kafkaSettings); - consumerBuilder.topic(topicService.buildTopicName("tb_edge_event.notifications." + tenantId + "." + edgeId)); + consumerBuilder.topic(topicService.buildEdgeEventNotificationsTopicPartitionInfo(tenantId, edgeId).getTopic()); consumerBuilder.clientId("tb-core-edge-event-consumer-" + serviceInfoProvider.getServiceId() + "-" + edgeConsumerCount.incrementAndGet()); consumerBuilder.groupId(topicService.buildTopicName("tb-core-edge-event-consumer")); consumerBuilder.decoder(msg -> new TbProtoQueueMsg<>(msg.getKey(), ToEdgeEventNotificationMsg.parseFrom(msg.getData()), msg.getHeaders())); From 07d81e58b12c1f8228be49239ad4782a1b4a9d5d Mon Sep 17 00:00:00 2001 From: Andrii Shvaika Date: Mon, 10 Mar 2025 16:09:42 +0200 Subject: [PATCH 263/345] Tuning the documentation of calculated field function --- .../en_US/calculated-field/expression_fn.md | 591 ++++++++++-------- 1 file changed, 328 insertions(+), 263 deletions(-) diff --git a/ui-ngx/src/assets/help/en_US/calculated-field/expression_fn.md b/ui-ngx/src/assets/help/en_US/calculated-field/expression_fn.md index e35572bf97..f46923ebd3 100644 --- a/ui-ngx/src/assets/help/en_US/calculated-field/expression_fn.md +++ b/ui-ngx/src/assets/help/en_US/calculated-field/expression_fn.md @@ -1,335 +1,400 @@ -#### Calculated field TBEL script function +## Calculated Field TBEL Script Function -The **calculate()** function is a user-defined script that allows you to perform custom calculations using [TBEL{:target="_blank"}](${siteBaseUrl}/docs${docPlatformPrefix}/user-guide/tbel/) on telemetry and attribute data. -It receives arguments configured in the calculated field setup and an additional `ctx` object, which provides access to all arguments. +The **calculate()** function is a user-defined script that enables custom calculations using [TBEL](\${siteBaseUrl}/docs\${docPlatformPrefix}/user-guide/tbel/) on telemetry and attribute data. +It receives arguments configured in the calculated field setup, along with an additional `ctx` object that provides access to all arguments. -##### Function signature + +### Function Signature ```javascript - function calculate(ctx, arg1, arg2, ...): object | object[] +function calculate(ctx, arg1, arg2, ...): object | object[] ``` -##### Argument representation in the script +### Supported Arguments -Before describing how arguments are passed to the function, let's define how different argument types are **represented** inside the script. +There are three types of arguments supported in the calculated field configuration: -There are two types of arguments that can be used in the function: +#### Attribute and Latest Telemetry Arguments -* single value arguments - represent the latest telemetry data or attribute. - ```json - { - "altitude": { - "ts": 1740644636669, - "value": 1034 - } - } - ``` +These arguments are single values and may be of type: boolean, int64 (long), double, string, or JSON. - * when accessed via `ctx.args`, they remain objects: +Attribute and Latest telemetry are single value arguments that may be one of: boolean, int64 (long), double, string and JSON. - ```javascript - var altitudeTimestamp = ctx.args.altitude.ts; - var altitudeValue = ctx.args.altitude.value; - ``` +**Example: Convert Temperature from Fahrenheit to Celsius** - * when accessed as a **function parameter**, only the value is passed: +```javascript +var temperatureC = (temperatureF - 32) / 1.8; +return { + "temperatureC": toFixed(temperatureC, 2) +} +``` - ```javascript - function calculate(ctx, altitude/*(single value argument)*/, temperature/*(time series rolling argument)*/) { - // altitude = 1035 - } - ``` +Alternatively, using `ctx` to access the argument as an object: -* time series rolling arguments - contain historical data within a defined time window. - ```json - { - "temperature": { - "timeWindow": { - "startTs": 1740643762896, - "endTs": 1740644662896 - }, - "values": [ - { "ts": 1740644355935, "value": 72.32 }, - { "ts": 1740644365935, "value": 72.86 }, - { "ts": 1740644375935, "value": 73.58 }, - { "ts": 1740644385935, "value": "NaN" } - ] - } +```json +{ + "temperatureF": { + "ts": 1740644636669, + "value": 36.6 } - ``` +} +``` + +You may notice that the object includes both the `value` of an argument and its timestamp as `ts`. +Let's modify the function that converts Fahrenheit to Celsius to also return the timestamp information: - * when accessed via `ctx.args`, they remain rolling argument objects: +```javascript +var temperatureC = (temperatureF - 32) / 1.8; +return { + "ts": ctx.args.temperatureF.ts, + "values": { "temperatureC": toFixed(temperatureC, 2) } +}; +``` - ```javascript - var startOfInterval = temperature.timeWindow.startTs; - var firstTimestamp = temperature.values[0].ts; - var firstValue = temperature.values[0].value; - ``` +#### Time Series Rolling Arguments - * when accessed as a **function parameter**, they are passed as rolling arguments, retaining their structure: +These contain time series data within a defined time window. Example format: - ```javascript - function calculate(ctx, altitude/*(single value argument)*/, temperature/*(time series rolling argument)*/) { - var avgTemp = temperature.mean(); // Use rolling argument functions +```json +{ + "temperature": { + "timeWindow": { + "startTs": 1740643762896, + "endTs": 1740644662896 + }, + "values": [ + { "ts": 1740644350000, "value": 72.32 }, + { "ts": 1740644360000, "value": 72.86 }, + { "ts": 1740644370000, "value": 73.58 }, + { "ts": 1740644380000, "value": "NaN" } + ] } - ``` +} +``` - **Built-in methods for rolling arguments** +The values are always converted to type `double`, and `NaN` is used when conversion fails. One may use `isNaN(double): boolean` function to check that the value is a valid number. - Time series rolling arguments provide built-in functions for calculations. - These functions accept an optional `ignoreNaN` boolean parameter, which controls how NaN values are handled. - Each function has two function signatures: +**Example: Accessing time series rolling argument data** - * **Without parameters:** `method()` → called **without parameters** and defaults to `ignoreNaN = true`, meaning NaN values are ignored. - * **With an explicit parameter:** `method(boolean ignoreNaN)` → called with a boolean `ignoreNaN` parameter: - * `true` → ignores NaN values (default behavior). - * `false` → includes NaN values in calculations. +```javascript +var startOfInterval = temperature.timeWindow.startTs; +var endOfInterval = temperature.timeWindow.endTs; +var firstItem = temperature.values[0]; +var firstItemTs = firstItem.ts; +var firstItemValue = firstItem.value; +var sum = 0.0; +// iterate through all values and calculate the sum using foreach: +foreach(t: temperature) { + if(!isNaN(t.value)) { // check that the value is a valid number; + sum += t.value; + } +} +// iterate through all values and calculate the sum using for loop: +sum = 0.0; +for(var i = 0; i < temperature.values.size; i++) { + sum += temperature.values[i].value; +} +// use built-in function to calculate the sum +sum = t.sum(); +``` - | Method | Default Behavior (`ignoreNaN = true`) | Alternative (`ignoreNaN = false`) | - |------------|--------------------------------------------------|---------------------------------------------| - | `max()` | Returns the highest value, ignoring NaN values. | Returns NaN if any NaN values exist. | - | `min()` | Returns the lowest value, ignoring NaN values. | Returns NaN if any NaN values exist. | - | `mean()` | Computes the average value, ignoring NaN values. | Returns NaN if any NaN values exist. | - | `std()` | Calculates the standard deviation, ignoring NaN. | Returns NaN if any NaN values exist. | - | `median()` | Returns the median value, ignoring NaN values. | Returns NaN if any NaN values exist. | - | `count()` | Counts only valid (non-NaN) values. | Counts all values, including NaN. | - | `last()` | Returns the most recent non-NaN value. | Returns the last value, even if it is NaN. | - | `first()` | Returns the oldest non-NaN value. | Returns the first value, even if it is NaN. | - | `sum()` | Computes the total sum, ignoring NaN values. | Returns NaN if any NaN values exist. | +##### Built-in Methods for Rolling Arguments - The following calculations are executed over the provided above arguments: +Time series rolling arguments support built-in functions for calculations. These functions accept an optional `ignoreNaN` boolean parameter. - **Usage: default (`ignoreNaN = true`)** +| Method | Default Behavior (`ignoreNaN = true`) | Alternative (`ignoreNaN = false`) | +|------------|-----------------------------------------------------|---------------------------------------------| +| `max()` | Returns the highest value, ignoring NaN values. | Returns NaN if any NaN values exist. | +| `min()` | Returns the lowest value, ignoring NaN values. | Returns NaN if any NaN values exist. | +| `mean()` | Computes the average value, ignoring NaN values. | Returns NaN if any NaN values exist. | +| `std()` | Calculates the standard deviation, ignoring NaN. | Returns NaN if any NaN values exist. | +| `median()` | Returns the median value, ignoring NaN values. | Returns NaN if any NaN values exist. | +| `count()` | Counts values, ignoring NaN values. | Counts all values, including NaN. | +| `last()` | Returns the most recent value, skipping NaN values. | Returns the last value, even if it is NaN. | +| `first()` | Returns the oldest value, skipping NaN values. | Returns the first value, even if it is NaN. | +| `sum()` | Computes the total sum, ignoring NaN values. | Returns NaN if any NaN values exist. | - ```javascript - var avgTemp = temperature.mean(); - var tempMax = temperature.max(); - var valueCount = temperature.count(); - ``` - **Output:** +Usage example: - ```json - { - "avgTemp": 72.92, - "tempMax": 73.58, - "valueCount": 3 - } - ``` +```javascript +var avgTemp = temperature.mean(); // Returns 72.92 +var tempMax = temperature.max(); // Returns 73.58 +var valueCount = temperature.count(); // Returns 3 - **Usage: explicit (`ignoreNaN = false`)** - - ```javascript - var avgTemp = temperature.mean(false); // Returns NaN if any NaN values exist - var tempMax = temperature.max(false); // Returns NaN if any NaN values exist - var valueCount = temperature.count(false); // Counts all values, including NaN - ``` +var avgTempNaN = temperature.mean(false); // Returns NaN +var tempMaxNaN = temperature.max(false); // Returns NaN +var valueCountNaN = temperature.count(false); // Returns 4 +``` - **Output:** +This function calculates air density using `altitude` (single value) and `temperature` (time series rolling argument). - ```json - { - "avgTemp": "NaN", - "tempMax": "NaN", - "valueCount": 4 - } - ``` +```javascript +function calculate(ctx, altitude, temperature) { + var avgTemperature = temperature.mean(); // Get average temperature + var temperatureK = (avgTemperature - 32) * (5 / 9) + 273.15; // Convert Fahrenheit to Kelvin - Time series rolling arguments can be **merged** to align timestamps across multiple datasets. + // Estimate air pressure based on altitude + var pressure = 101325 * Math.pow((1 - 2.25577e-5 * altitude), 5.25588); - | Method | Description | Parameters | Returns | - |:-----------------------------|:-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|:-------------------------------------------------| - | `merge(other, settings)` | Merges the current rolling argument with another rolling argument by aligning timestamps and filling missing values with the previous available value. |
        • `other` (another rolling argument)
        • `settings` (optional) - configuration object, supports:

          • `ignoreNaN` (boolean, default true) - controls whether NaN values should be ignored.
          • `timeWindow` (object, default {}) - defines a custom time window for filtering merged values.
        | Merged object with timeWindow and aligned values. | - | `mergeAll(others, settings)` | Merges the current rolling argument with multiple rolling arguments by aligning timestamps and filling missing values with the previous available value. |
        • `others` (array of rolling arguments)
        • `settings` (optional) - configuration object, supports:

          • `ignoreNaN` (boolean, default true) - controls whether NaN values should be ignored.
          • `timeWindow` (object, default {}) - defines a custom time window for filtering merged values.
        | Merged object with timeWindow and aligned values.| + // Air density formula + var airDensity = pressure / (287.05 * temperatureK); - **Example arguments:** + return { + "airDensity": airDensity + }; +} +``` - ```json - { - "humidity": { - "timeWindow": { - "startTs": 1741356332086, - "endTs": 1741357232086 - }, - "values": [{ - "ts": 1741356882759, - "value": 43 - }, { - "ts": 1741356918779, - "value": 46 - }] +##### Merging Time Series Arguments + +Time series rolling arguments can be **merged** to align timestamps across multiple datasets. + +| Method | Description | Parameters | Returns | +|:-----------------------------|:--------------------------------------------------------------------------------------------------------------------------|--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|:-------------------------------------------------| +| `merge(other, settings)` | Merges with another rolling argument. Aligns timestamps and filling missing values with the previous available value. |
        • `other` (another rolling argument)
        • `settings` (optional) - configuration object, supports:

          • `ignoreNaN` (boolean, default true) - controls whether NaN values should be ignored.
          • `timeWindow` (object, default {}) - defines a custom time window for filtering merged values.
        | Merged object with timeWindow and aligned values. | +| `mergeAll(others, settings)` | Merges with multiple rolling arguments. Aligns timestamps and filling missing values with the previous available value. |
        • `others` (array of rolling arguments)
        • `settings` (optional) - configuration object, supports:

          • `ignoreNaN` (boolean, default true) - controls whether NaN values should be ignored.
          • `timeWindow` (object, default {}) - defines a custom time window for filtering merged values.
        | Merged object with timeWindow and aligned values.| + +Assuming the following arguments and their values: + +```json +{ + "humidity": { + "timeWindow": { + "startTs": 1741356332086, + "endTs": 1741357232086 }, - "pressure": { - "timeWindow": { - "startTs": 1741356332086, - "endTs": 1741357232086 - }, - "values": [{ - "ts": 1741357047945, - "value": 1023 - }, { - "ts": 1741357056144, - "value": 1026 - }, { - "ts": 1741357147391, - "value": 1025 - }] + "values": [{ + "ts": 1741356882759, + "value": 43 + }, { + "ts": 1741356918779, + "value": 46 + }] + }, + "pressure": { + "timeWindow": { + "startTs": 1741356332086, + "endTs": 1741357232086 }, - "temperature": { - "timeWindow": { - "startTs": 1741356332086, - "endTs": 1741357232086 - }, - "values": [{ - "ts": 1741356874943, - "value": 76 - }, { - "ts": 1741357063689, - "value": 77 - }] - } + "values": [{ + "ts": 1741357047945, + "value": 1023 + }, { + "ts": 1741357056144, + "value": 1026 + }, { + "ts": 1741357147391, + "value": 1025 + }] + }, + "temperature": { + "timeWindow": { + "startTs": 1741356332086, + "endTs": 1741357232086 + }, + "values": [{ + "ts": 1741356874943, + "value": 76 + }, { + "ts": 1741357063689, + "value": 77 + }] } - ``` +} +``` - **Usage:** +**Usage:** - ```javascript - var mergedData = temperature.merge(humidity, { ignoreNaN: false }); - ``` +```javascript +var mergedData = temperature.merge(humidity, { ignoreNaN: false }); +``` - **Output:** +**Output:** - ```json - { - "mergedData": { - "timeWindow": { - "startTs": 1741356332086, - "endTs": 1741357232086 - }, - "values": [{ - "ts": 1741356874943, - "values": [76.0, "NaN"] - }, { - "ts": 1741356882759, - "values": [76.0, 43.0] - }, { - "ts": 1741356918779, - "values": [76.0, 46.0] - }, { - "ts": 1741357063689, - "values": [77.0, 46.0] - }] - } +```json +{ + "mergedData": { + "timeWindow": { + "startTs": 1741356332086, + "endTs": 1741357232086 + }, + "values": [{ + "ts": 1741356874943, + "values": [76.0, "NaN"] + }, { + "ts": 1741356882759, + "values": [76.0, 43.0] + }, { + "ts": 1741356918779, + "values": [76.0, 46.0] + }, { + "ts": 1741357063689, + "values": [77.0, 46.0] + }] } - ``` +} +``` - **Usage:** +**Usage:** - ```javascript - var mergedData = temperature.mergeAll([humidity, pressure], { ignoreNaN: true }); - ``` +```javascript +var mergedData = temperature.mergeAll([humidity, pressure], { ignoreNaN: true }); +``` - **Output:** +**Output:** - ```json - { - "mergedData": { - "timeWindow": { - "startTs": 1741356332086, - "endTs": 1741357232086 - }, - "values": [{ - "ts": 1741357047945, - "values": [76.0, 46.0, 1023.0] - }, { - "ts": 1741357056144, - "values": [76.0, 46.0, 1026.0] - }, { - "ts": 1741357063689, - "values": [77.0, 46.0, 1026.0] - }, { - "ts": 1741357147391, - "values": [77.0, 46.0, 1025.0] - }] - } +```json +{ + "mergedData": { + "timeWindow": { + "startTs": 1741356332086, + "endTs": 1741357232086 + }, + "values": [{ + "ts": 1741357047945, + "values": [76.0, 46.0, 1023.0] + }, { + "ts": 1741357056144, + "values": [76.0, 46.0, 1026.0] + }, { + "ts": 1741357063689, + "values": [77.0, 46.0, 1026.0] + }, { + "ts": 1741357147391, + "values": [77.0, 46.0, 1025.0] + }] } - ``` +} +``` -##### Function arguments +**Example: Freezer temperature analysis** -* `ctx` - context object that contains all provided arguments, in the representations described above. +This function merges `temperature` data with the fridge's `defrost` status. It then analyzes the merged data to identify instances where the fridge is not in defrost mode, yet the internal air temperature is too high ( > -5° C). -Accessing arguments via `ctx`: +```javascript +function calculate(ctx, temperature, defrost) { + var merged = temperature.merge(defrost); + var result = []; + + foreach(item: merged) { + if (item.v1 > -5.0 && item.v2 == 0) { + result.add({ + ts: item.ts, + values: { + issue: { + temperature: item.v1, + defrostState: false + } + } + }); + } + } - ```javascript - var altitude = ctx.args.altitude; // single value argument - var temperature = ctx.args.temperature; // time series rolling argument - ``` + return result; +} +``` -* `arg1, arg2, ...` - user-defined arguments configured in the calculated field setup. +The result is a list of issues that may be used to configure alarm rules: -How they are passed depends on their type: +```json +[{ + "ts": 1741613833843, + "values": { + "issue": { + "temperature": -3.12, + "defrostState": false + } + } +}, { + "ts": 1741613923848, + "values": { + "issue": { + "temperature": -4.16, + "defrostState": false + } + } +}] +``` -* **single value arguments** are passed as raw values **(e.g., 22.5, "ON")**. -* **time series rolling arguments** are passed as objects (containing multiple values). +### Function return format -##### Example: Air Density Calculation +The return format depends on the output type configured in the calculated field settings (default: **Time Series**). -This function calculates air density using `altitude` (single value argument) and `temperature` (time series rolling argument). +##### Time Series Output -```javascript -function calculate(ctx, altitude, temperature) { - var avgTemperature = temperature.mean(); // Get average temperature - var temperatureK = (avgTemperature - 32) * (5 / 9) + 273.15; // Convert Fahrenheit to Kelvin - - // Estimate air pressure based on altitude - var pressure = 101325 * Math.pow((1 - 2.25577e-5 * altitude), 5.25588); +The function must return a JSON object or array with or without a timestamp. +Examples below return 5 data points: airDensity (double), humidity (integer), hvacEnabled (boolean), hvacState (string) and configuration (JSON): - // Air density formula - var airDensity = pressure / (287.05 * temperatureK); +Without timestamp: - return { - "airDensity": airDensity - }; +```json +{ + "airDensity": 1.06, + "humidity": 70, + "hvacEnabled": true, + "hvacState": "IDLE", + "configuration": { + "someNumber": 42, + "someArray": [1,2,3], + "someNestedObject": {"key": "value"} + } } ``` -* `altitude` is a single value, passed as number. -* `temperature` is a rolling argument, retaining its full structure. +With timestamp: -##### Function return format - -The script should return a JSON object formatted according to the [ThingsBoard Telemetry Upload API](${siteBaseUrl}/docs${docPlatformPrefix}/user-guide/telemetry/#time-series-data-upload-api/). -The return value must match one of the supported telemetry upload formats. - -The script must return data in a format compatible with ThingsBoard’s APIs. -The correct return format depends on the calculated field output configuration: +```json +{ + "ts": 1740644636669, + "values": { + "airDensity": 1.06, + "humidity": 70, + "hvacEnabled": true, + "hvacState": "IDLE", + "configuration": { + "someNumber": 42, + "someArray": [1,2,3], + "someNestedObject": {"key": "value"} + } + } +} +``` -* if latest telemetry is used for output, the function must return data according to the [Telemetry Upload API](${siteBaseUrl}/docs${docPlatformPrefix}/reference/mqtt-api/#attributes-api/). +Array containing multiple timestamps and different values of the `airDensity` : - * without timestamps - ```json - { - "airDensity": 1.06, - "someKey": "value" - } - ``` - * with a timestamp: - ```json +```json +[ { "ts": 1740644636669, "values": { - "airDensity": 1.06, - "someKey": "value" - } - } - ``` - -* if attributes are used for output, the function must return data according to the [Attributes API](${siteBaseUrl}/docs${docPlatformPrefix}/user-guide/telemetry/#time-series-data-upload-api/). - - ```json + "airDensity": 1.06 + } + }, { - "airDensity": 1.06, - "someKey": "value" + "ts": 1740644636670, + "values": { + "airDensity": 1.07 + } + } +] +``` + +##### Attribute Output + +The function must return a JSON object **without timestamp** information. +Example below return 5 data points: airDensity (double), humidity (integer), hvacEnabled (boolean), hvacState (string) and configuration (JSON): + +```json +{ + "airDensity": 1.06, + "humidity": 70, + "hvacEnabled": true, + "hvacState": "IDLE", + "configuration": { + "someNumber": 42, + "someArray": [1,2,3], + "someNestedObject": {"key": "value"} } - ``` +} +``` From 6c99fd4fbaae1ddb98f76db4b7833608493af5e9 Mon Sep 17 00:00:00 2001 From: Igor Kulikov Date: Mon, 10 Mar 2025 16:12:05 +0200 Subject: [PATCH 264/345] UI: Map settings - responsive layout. --- .../home/components/widget/lib/maps/map.ts | 1 + .../panels/map-timeline-panel.component.html | 2 +- .../panels/map-timeline-panel.component.scss | 7 ++ .../panels/map-timeline-panel.component.ts | 38 ++++++++- ...data-layer-pattern-settings.component.html | 30 ++++--- .../map/map-data-layer-dialog.component.html | 80 ++++++++++--------- .../map/map-data-layer-row.component.scss | 31 +++++++ .../common/map/map-data-layers.component.scss | 21 +++++ .../map/map-data-source-row.component.scss | 21 +++++ .../map/map-data-sources.component.scss | 18 +++++ .../common/map/map-layer-row.component.scss | 3 + .../common/map/map-layers.component.scss | 5 ++ .../common/map/map-settings.component.html | 2 +- .../map-tooltip-tag-actions.component.html | 6 +- .../marker-clustering-settings.component.html | 4 +- .../map/trip-timeline-settings.component.html | 4 +- 16 files changed, 213 insertions(+), 60 deletions(-) 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 003d8e379e..805e7b06c2 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 @@ -201,6 +201,7 @@ export abstract class TbMap { parentElement.removeChild(content); parentElement.style.display = 'none'; this.containerElement.append(content); + this.timeLineComponent.panelElement = content as Element; } const setup = [this.doSetupControls()]; if (this.timeline && this.settings.tripTimeline.snapToRealLocation) { diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/maps/panels/map-timeline-panel.component.html b/ui-ngx/src/app/modules/home/components/widget/lib/maps/panels/map-timeline-panel.component.html index 7a725e48eb..41f79eba1f 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/maps/panels/map-timeline-panel.component.html +++ b/ui-ngx/src/app/modules/home/components/widget/lib/maps/panels/map-timeline-panel.component.html @@ -15,7 +15,7 @@ limitations under the License. --> -
        +
        @if(hasData) { { + this.resize(); + }); + this.panelResize$.observe(element); + } + + get panelElement(): Element { + return this.panelElementVal; + } + @Input() anchors: number[] = []; @Output() timeChanged = new EventEmitter(); + column = false; + timestampFormat: DateFormatProcessor; minTimeIndex = 0; @@ -112,6 +128,9 @@ export class MapTimelinePanelComponent implements OnInit { private maxValue: number; private currentTimeValue: number = null; + private panelResize$: ResizeObserver; + private panelElementVal: Element; + constructor(public element: ElementRef, private cd: ChangeDetectorRef, private destroyRef: DestroyRef, @@ -126,6 +145,12 @@ export class MapTimelinePanelComponent implements OnInit { this.speed = this.settings.speedOptions[0]; } + ngOnDestroy() { + if (this.panelResize$) { + this.panelResize$.disconnect(); + } + } + public onIndexChange(index: number) { this.index = index; this.updateCurrentTime(); @@ -208,6 +233,15 @@ export class MapTimelinePanelComponent implements OnInit { } } + private resize(): void { + const width = this.panelElement.getBoundingClientRect().width; + const column = width <= 400; + if (this.column !== column) { + this.column = column; + this.cd.markForCheck(); + } + } + private updateCurrentTime() { const newTime = this.minValue + this.index * this.settings.timeStep; if (this.currentTime !== newTime) { diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/map/data-layer-pattern-settings.component.html b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/map/data-layer-pattern-settings.component.html index 7912c1aeb5..93ec63c128 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/map/data-layer-pattern-settings.component.html +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/map/data-layer-pattern-settings.component.html @@ -21,7 +21,7 @@ [disabled]="!patternSettingsFormGroup.get('show').value"> -
        +
        {{ patternTitle ? patternTitle : ((patternType === 'label' ? 'widgets.maps.data-layer.label' : 'widgets.maps.data-layer.tooltip') | translate) }} @@ -64,19 +64,23 @@ {{ 'widgets.maps.data-layer.auto-close-tooltips' | translate }}
        -
        +
        widgets.maps.data-layer.tooltip-offset
        -
        -
        widgets.maps.data-layer.tooltip-offset-horizontal
        - - - -
        widgets.maps.data-layer.tooltip-offset-vertical
        - - - +
        +
        +
        widgets.maps.data-layer.tooltip-offset-horizontal
        + + + +
        +
        +
        widgets.maps.data-layer.tooltip-offset-vertical
        + + + +
        -
        +
        {{ 'widget-config.datasource' | translate }}
        {{ datasourceTypesTranslations.get(type) | translate }} @@ -71,7 +71,7 @@
        -
        +
        @if (dataLayerType === 'markers') {
        {{ 'widgets.maps.data-layer.marker.marker' | translate }}
        } @else { @@ -198,11 +198,11 @@ -
        +
        widgets.maps.data-layer.marker.shape
        -
        +
        widgets.maps.data-layer.marker.icon
        @@ -210,7 +210,7 @@
        widgets.maps.data-layer.marker.image
        -
        +
        widgets.maps.data-layer.marker.marker-offset
        widgets.maps.data-layer.marker.offset-horizontal
        @@ -225,7 +225,7 @@
        -
        +
        {{ 'widgets.maps.data-layer.marker.rotate-marker' | translate }} @@ -289,9 +289,9 @@ -
        -
        widgets.maps.data-layer.path.decorator-symbol
        -
        +
        +
        widgets.maps.data-layer.path.decorator-symbol
        +
        @@ -299,34 +299,42 @@ - - - px - - - +
        + + + px + + + +
        -
        +
        widgets.maps.data-layer.path.decorator-arrangement
        -
        -
        widgets.maps.data-layer.path.decorator-offset
        - - -
        px
        -
        -
        widgets.maps.data-layer.path.decorator-end-offset
        - - -
        px
        -
        -
        widgets.maps.data-layer.path.decorator-repeat
        - - -
        px
        -
        +
        +
        +
        widgets.maps.data-layer.path.decorator-offset
        + + +
        px
        +
        +
        +
        +
        widgets.maps.data-layer.path.decorator-end-offset
        + + +
        px
        +
        +
        +
        +
        widgets.maps.data-layer.path.decorator-repeat
        + + +
        px
        +
        +
        diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/map/map-data-layer-row.component.scss b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/map/map-data-layer-row.component.scss index 2081bc6dbf..1f17c05f82 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/map/map-data-layer-row.component.scss +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/map/map-data-layer-row.component.scss @@ -13,6 +13,8 @@ * See the License for the specific language governing permissions and * limitations under the License. */ +@import '../../../../../../../../../scss/constants'; + .tb-form-table-row.tb-map-data-layer-row { .tb-source-field { @@ -26,18 +28,47 @@ .tb-x-pos-field { flex: 1 1 25%; + min-width: 0; } .tb-y-pos-field { flex: 1 1 25%; + min-width: 0; } .tb-key-field { flex: 1 1 50%; + min-width: 0; } .tb-remove-button { width: 40px; min-width: 40px; } + + @media #{$mat-lt-lg} { + .tb-source-field { + flex-direction: column; + flex: 1 1 30%; + } + .tb-x-pos-field, .tb-y-pos-field { + flex: 1 1 35%; + } + .tb-key-field { + flex: 1 1 70%; + } + } + @media screen and (min-width: 450px) and (max-width: 599px) { + .tb-source-field { + flex-direction: row; + } + } + @media #{$mat-xs} { + .tb-x-pos-field, .tb-y-pos-field { + display: none; + } + .tb-key-field { + display: none; + } + } } diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/map/map-data-layers.component.scss b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/map/map-data-layers.component.scss index 5bd8618b79..84262b9810 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/map/map-data-layers.component.scss +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/map/map-data-layers.component.scss @@ -13,6 +13,8 @@ * See the License for the specific language governing permissions and * limitations under the License. */ +@import '../../../../../../../../../scss/constants'; + .tb-map-data-layers { .tb-form-table-header-cell { &.tb-source-header { @@ -31,6 +33,25 @@ width: 80px; min-width: 80px; } + @media #{$mat-lt-lg} { + &.tb-source-header { + flex: 1 1 30%; + } + &.tb-x-pos-header, &.tb-y-pos-header { + flex: 1 1 35%; + } + &.tb-key-header { + flex: 1 1 70%; + } + } + @media #{$mat-xs} { + &.tb-x-pos-header, &.tb-y-pos-header { + display: none; + } + &.tb-key-header { + display: none; + } + } } .tb-form-table-body { diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/map/map-data-source-row.component.scss b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/map/map-data-source-row.component.scss index c72a6cb279..2244f6d14f 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/map/map-data-source-row.component.scss +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/map/map-data-source-row.component.scss @@ -13,6 +13,8 @@ * See the License for the specific language governing permissions and * limitations under the License. */ +@import '../../../../../../../../../scss/constants'; + .tb-form-table-row.tb-map-data-source-row { .tb-source-field { @@ -26,10 +28,29 @@ .tb-data-keys-field { flex: 1 1 50%; + min-width: 0; } .tb-remove-button { width: 40px; min-width: 40px; } + + @media #{$mat-lt-lg} { + .tb-source-field { + flex-direction: column; + flex: 1 1 30%; + } + .tb-data-keys-field { + flex: 1 1 70%; + } + @media #{$mat-lt-md} { + .tb-source-field { + flex: 1 1 50%; + } + .tb-data-keys-field { + flex: 1 1 50%; + } + } + } } diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/map/map-data-sources.component.scss b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/map/map-data-sources.component.scss index a94235ab2b..28788cb4f1 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/map/map-data-sources.component.scss +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/map/map-data-sources.component.scss @@ -13,6 +13,8 @@ * See the License for the specific language governing permissions and * limitations under the License. */ +@import '../../../../../../../../../scss/constants'; + .tb-map-data-sources { .tb-form-table-header-cell { &.tb-source-header { @@ -25,6 +27,22 @@ width: 40px; min-width: 40px; } + @media #{$mat-lt-lg} { + &.tb-source-header { + flex: 1 1 30%; + } + &.tb-data-keys-header { + flex: 1 1 70%; + } + @media #{$mat-lt-md} { + &.tb-source-header { + flex: 1 1 50%; + } + &.tb-data-keys-header { + flex: 1 1 50%; + } + } + } } .tb-form-table-body { diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/map/map-layer-row.component.scss b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/map/map-layer-row.component.scss index 84252605b0..89949ec96e 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/map/map-layer-row.component.scss +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/map/map-layer-row.component.scss @@ -27,6 +27,9 @@ .tb-layer-field { flex: 1 1 33.33%; + @media #{$mat-xs} { + display: none; + } } .tb-remove-button { diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/map/map-layers.component.scss b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/map/map-layers.component.scss index 9ec77dd324..e5b035ddd3 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/map/map-layers.component.scss +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/map/map-layers.component.scss @@ -13,6 +13,8 @@ * See the License for the specific language governing permissions and * limitations under the License. */ +@import '../../../../../../../../../scss/constants'; + .tb-map-layers { .tb-form-table-header-cell { &.tb-label-header { @@ -23,6 +25,9 @@ } &.tb-layer-header { flex: 1 1 33.33%; + @media #{$mat-xs} { + display: none; + } } &.tb-actions-header { width: 120px; 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 a7b725cb3f..38a5d13922 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 @@ -35,7 +35,7 @@
        -
        +
        {{ 'widgets.maps.overlays.overlays' | translate }}
        diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/map/map-tooltip-tag-actions.component.html b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/map/map-tooltip-tag-actions.component.html index 2060f394f1..d743f70919 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/map/map-tooltip-tag-actions.component.html +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/map/map-tooltip-tag-actions.component.html @@ -15,10 +15,10 @@ limitations under the License. --> -
        +
        widgets.maps.data-layer.tooltip-tag-actions
        -
        - +
        + diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/map/marker-clustering-settings.component.html b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/map/marker-clustering-settings.component.html index e363444481..2a85f8d514 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/map/marker-clustering-settings.component.html +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/map/marker-clustering-settings.component.html @@ -31,13 +31,13 @@ {{ 'widgets.maps.data-layer.marker.clustering.zoom-on-cluster-click' | translate }}
        -
        +
        widgets.maps.data-layer.marker.clustering.max-zoom
        -
        +
        widgets.maps.data-layer.marker.clustering.max-radius
        diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/map/trip-timeline-settings.component.html b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/map/trip-timeline-settings.component.html index 7af7d77671..9729c3559d 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/map/trip-timeline-settings.component.html +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/map/trip-timeline-settings.component.html @@ -35,7 +35,7 @@ ms
        -
        +
        widgets.maps.timeline.speed-options
        @@ -45,7 +45,7 @@
        -
        +
        {{ 'widgets.maps.timeline.timestamp' | translate }} From e530dec25030358c5f9813344ffb58f418f24284 Mon Sep 17 00:00:00 2001 From: Igor Kulikov Date: Mon, 10 Mar 2025 16:26:56 +0200 Subject: [PATCH 265/345] UI: Fix entity autocomplete component. --- .../panel/calculated-field-argument-panel.component.html | 3 +-- .../components/entity/entity-autocomplete.component.html | 2 +- .../components/entity/entity-autocomplete.component.ts | 8 -------- 3 files changed, 2 insertions(+), 11 deletions(-) diff --git a/ui-ngx/src/app/modules/home/components/calculated-fields/components/panel/calculated-field-argument-panel.component.html b/ui-ngx/src/app/modules/home/components/calculated-fields/components/panel/calculated-field-argument-panel.component.html index 4d7f20d6d5..79f2b6ebbc 100644 --- a/ui-ngx/src/app/modules/home/components/calculated-fields/components/panel/calculated-field-argument-panel.component.html +++ b/ui-ngx/src/app/modules/home/components/calculated-fields/components/panel/calculated-field-argument-panel.component.html @@ -83,8 +83,7 @@ - {{ label | translate }} + {{ label | translate }} Date: Mon, 10 Mar 2025 16:29:24 +0200 Subject: [PATCH 266/345] renamed edqs service to tb-edqs --- docker/docker-compose.edqs.volumes.yml | 4 ++-- docker/docker-compose.edqs.yml | 12 ++++++------ docker/{edqs.env => tb-edqs.env} | 0 docker/{edqs => tb-edqs}/conf/logback.xml | 4 ++-- .../conf/edqs.conf => tb-edqs/conf/tb-edqs.conf} | 4 ++-- 5 files changed, 12 insertions(+), 12 deletions(-) rename docker/{edqs.env => tb-edqs.env} (100%) rename docker/{edqs => tb-edqs}/conf/logback.xml (91%) rename docker/{edqs/conf/edqs.conf => tb-edqs/conf/tb-edqs.conf} (88%) diff --git a/docker/docker-compose.edqs.volumes.yml b/docker/docker-compose.edqs.volumes.yml index 9d2ce946c8..89b4b3a59c 100644 --- a/docker/docker-compose.edqs.volumes.yml +++ b/docker/docker-compose.edqs.volumes.yml @@ -19,10 +19,10 @@ version: '3.0' services: tb-edqs-1: volumes: - - tb-edqs-log-volume:/var/log/edqs + - tb-edqs-log-volume:/var/log/tb-edqs tb-edqs-2: volumes: - - tb-edqs-log-volume:/var/log/edqs + - tb-edqs-log-volume:/var/log/tb-edqs volumes: tb-edqs-log-volume: diff --git a/docker/docker-compose.edqs.yml b/docker/docker-compose.edqs.yml index 67e9c987e3..6dd9606ee6 100644 --- a/docker/docker-compose.edqs.yml +++ b/docker/docker-compose.edqs.yml @@ -33,10 +33,10 @@ services: restart: always image: "${DOCKER_REPO}/${EDQS_DOCKER_NAME}:${TB_VERSION}" env_file: - - edqs.env + - tb-edqs.env volumes: - - ./edqs/conf:/usr/share/edqs/conf - - ./edqs/log:/var/log/edqs + - ./tb-edqs/conf:/usr/share/tb-edqs/conf + - ./tb-edqs/log:/var/log/tb-edqs ports: - "8080" depends_on: @@ -46,10 +46,10 @@ services: restart: always image: "${DOCKER_REPO}/${EDQS_DOCKER_NAME}:${TB_VERSION}" env_file: - - edqs.env + - tb-edqs.env volumes: - - ./edqs/conf:/usr/share/edqs/conf - - ./edqs/log:/var/log/edqs + - ./tb-edqs/conf:/usr/share/tb-edqs/conf + - ./tb-edqs/log:/var/log/tb-edqs ports: - "8080" depends_on: diff --git a/docker/edqs.env b/docker/tb-edqs.env similarity index 100% rename from docker/edqs.env rename to docker/tb-edqs.env diff --git a/docker/edqs/conf/logback.xml b/docker/tb-edqs/conf/logback.xml similarity index 91% rename from docker/edqs/conf/logback.xml rename to docker/tb-edqs/conf/logback.xml index 40481a8c35..14b5b0f04b 100644 --- a/docker/edqs/conf/logback.xml +++ b/docker/tb-edqs/conf/logback.xml @@ -21,10 +21,10 @@ - /var/log/edqs/${TB_SERVICE_ID}/tb-edqs.log + /var/log/tb-edqs/${TB_SERVICE_ID}/tb-edqs.log - /var/log/edqs/tb-edqs.%d{yyyy-MM-dd}.%i.log + /var/log/tb-edqs/tb-edqs.%d{yyyy-MM-dd}.%i.log 100MB 30 3GB diff --git a/docker/edqs/conf/edqs.conf b/docker/tb-edqs/conf/tb-edqs.conf similarity index 88% rename from docker/edqs/conf/edqs.conf rename to docker/tb-edqs/conf/tb-edqs.conf index 8c6b5d1826..a5a4e6a10c 100644 --- a/docker/edqs/conf/edqs.conf +++ b/docker/tb-edqs/conf/tb-edqs.conf @@ -14,9 +14,9 @@ # limitations under the License. # -export JAVA_OPTS="$JAVA_OPTS -Xlog:gc*,heap*,age*,safepoint=debug:file=/var/log/edqs/${TB_SERVICE_ID}-gc.log:time,uptime,level,tags:filecount=10,filesize=10M" +export JAVA_OPTS="$JAVA_OPTS -Xlog:gc*,heap*,age*,safepoint=debug:file=/var/log/tb-edqs/${TB_SERVICE_ID}-gc.log:time,uptime,level,tags:filecount=10,filesize=10M" export JAVA_OPTS="$JAVA_OPTS -XX:+IgnoreUnrecognizedVMOptions -XX:+HeapDumpOnOutOfMemoryError" export JAVA_OPTS="$JAVA_OPTS -XX:-UseBiasedLocking -XX:+UseTLAB -XX:+ResizeTLAB -XX:+PerfDisableSharedMem -XX:+UseCondCardMark" export JAVA_OPTS="$JAVA_OPTS -XX:+UseG1GC -XX:MaxGCPauseMillis=500 -XX:+UseStringDeduplication -XX:+ParallelRefProcEnabled -XX:MaxTenuringThreshold=10" export LOG_FILENAME=tb-edqs.out -export LOADER_PATH=/usr/share/edqs/conf +export LOADER_PATH=/usr/share/tb-edqs/conf From 2c4346b993c6dd6b9c53265e0570e61dd0646183 Mon Sep 17 00:00:00 2001 From: IrynaMatveieva Date: Mon, 10 Mar 2025 16:46:55 +0200 Subject: [PATCH 267/345] moved init cf actor before init rule chains --- .../server/actors/tenant/TenantActor.java | 15 ++++++++++----- 1 file changed, 10 insertions(+), 5 deletions(-) diff --git a/application/src/main/java/org/thingsboard/server/actors/tenant/TenantActor.java b/application/src/main/java/org/thingsboard/server/actors/tenant/TenantActor.java index 4be2024680..ce8ad84682 100644 --- a/application/src/main/java/org/thingsboard/server/actors/tenant/TenantActor.java +++ b/application/src/main/java/org/thingsboard/server/actors/tenant/TenantActor.java @@ -91,6 +91,16 @@ public class TenantActor extends RuleChainManagerActor { isRuleEngine = systemContext.getServiceInfoProvider().isService(ServiceType.TB_RULE_ENGINE); if (isRuleEngine) { if (systemContext.getPartitionService().isManagedByCurrentService(tenantId)) { + try { + //TODO: IM - extend API usage to have CF Exec Enabled? Not in 4.0; + cfActor = ctx.getOrCreateChildActor(new TbStringActorId("CFM|" + tenantId), + () -> DefaultActorService.CF_MANAGER_DISPATCHER_NAME, + () -> new CalculatedFieldManagerActorCreator(systemContext, tenantId), + () -> true); + } catch (Exception e) { + log.info("Failed to init CF Actor.", e); + cantFindTenant = true; + } try { if (getApiUsageState().isReExecEnabled()) { log.debug("[{}] Going to init rule chains", tenantId); @@ -98,11 +108,6 @@ public class TenantActor extends RuleChainManagerActor { } else { log.info("[{}] Skip init of the rule chains due to API limits", tenantId); } - //TODO: IM - extend API usage to have CF Exec Enabled? Not in 4.0; - cfActor = ctx.getOrCreateChildActor(new TbStringActorId("CFM|" + tenantId), - () -> DefaultActorService.CF_MANAGER_DISPATCHER_NAME, - () -> new CalculatedFieldManagerActorCreator(systemContext, tenantId), - () -> true); } catch (Exception e) { log.info("Failed to check ApiUsage \"ReExecEnabled\"!!!", e); cantFindTenant = true; From cfc5a1f9d5478d274b98ee1be9cdcd320bdcafb5 Mon Sep 17 00:00:00 2001 From: IrynaMatveieva Date: Mon, 10 Mar 2025 17:08:11 +0200 Subject: [PATCH 268/345] added check for cf actor --- .../org/thingsboard/server/actors/tenant/TenantActor.java | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/application/src/main/java/org/thingsboard/server/actors/tenant/TenantActor.java b/application/src/main/java/org/thingsboard/server/actors/tenant/TenantActor.java index ce8ad84682..0bd116f806 100644 --- a/application/src/main/java/org/thingsboard/server/actors/tenant/TenantActor.java +++ b/application/src/main/java/org/thingsboard/server/actors/tenant/TenantActor.java @@ -99,7 +99,6 @@ public class TenantActor extends RuleChainManagerActor { () -> true); } catch (Exception e) { log.info("Failed to init CF Actor.", e); - cantFindTenant = true; } try { if (getApiUsageState().isReExecEnabled()) { @@ -190,6 +189,10 @@ public class TenantActor extends RuleChainManagerActor { } private void onToCalculatedFieldSystemActorMsg(ToCalculatedFieldSystemMsg msg, boolean priority) { + if (cfActor == null) { + log.warn("[{}] CF Actor is not initialized.", tenantId); + return; + } if (priority) { cfActor.tellWithHighPriority(msg); } else { From 0b509c9e1f7712136c26d1c717fde2053ceb477d Mon Sep 17 00:00:00 2001 From: YevhenBondarenko Date: Mon, 10 Mar 2025 17:07:15 +0100 Subject: [PATCH 269/345] minor refactoring --- .../service/cf/ctx/state/ScriptCalculatedFieldStateTest.java | 3 --- 1 file changed, 3 deletions(-) diff --git a/application/src/test/java/org/thingsboard/server/service/cf/ctx/state/ScriptCalculatedFieldStateTest.java b/application/src/test/java/org/thingsboard/server/service/cf/ctx/state/ScriptCalculatedFieldStateTest.java index 6060d3f8a6..91eced64f6 100644 --- a/application/src/test/java/org/thingsboard/server/service/cf/ctx/state/ScriptCalculatedFieldStateTest.java +++ b/application/src/test/java/org/thingsboard/server/service/cf/ctx/state/ScriptCalculatedFieldStateTest.java @@ -74,9 +74,6 @@ public class ScriptCalculatedFieldStateTest { @MockBean private ApiLimitService apiLimitService; -// @MockBean -// private MeterRegistry meterRegistry; - @BeforeEach void setUp() { when(apiLimitService.getLimit(any(), any())).thenReturn(1000L); From 77e4e665d7dd17ff441a592239a3131e8b6bd853 Mon Sep 17 00:00:00 2001 From: Igor Kulikov Date: Mon, 10 Mar 2025 18:10:19 +0200 Subject: [PATCH 270/345] UI: Fix marker removal on trip data layer --- .../components/widget/lib/maps/data-layer/trips-data-layer.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/maps/data-layer/trips-data-layer.ts b/ui-ngx/src/app/modules/home/components/widget/lib/maps/data-layer/trips-data-layer.ts index 9e0a5fe4bf..d6cbe5e4c9 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/maps/data-layer/trips-data-layer.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/maps/data-layer/trips-data-layer.ts @@ -127,6 +127,7 @@ class TbTripDataItem extends TbDataLayerItem { From 9bd66e653358416dcf4ef66cc98563e5bd2d39c1 Mon Sep 17 00:00:00 2001 From: Igor Kulikov Date: Mon, 10 Mar 2025 18:39:29 +0200 Subject: [PATCH 271/345] UI: Fix trip map control setup. --- ui-ngx/src/app/modules/home/components/widget/lib/maps/map.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 805e7b06c2..054eb5943e 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 @@ -183,7 +183,7 @@ export abstract class TbMap { this.map.zoomControl.setPosition(this.settings.controlsPosition); } this.dragMode = !this.settings.dragModeButton; - const tripsWithMarkers = this.settings.trips?.length ? this.settings.trips.filter(trip => trip.showMarker) : []; + const tripsWithMarkers = this.settings.trips?.length ? this.settings.trips.filter(trip => trip.showMarker !== false) : []; const showTimeline = this.settings.tripTimeline?.showTimelineControl && tripsWithMarkers.length; if (showTimeline) { From 08b05e200f1eb2d52893b8bec2c6ecdd72b2d15f Mon Sep 17 00:00:00 2001 From: Artem Dzhereleiko Date: Tue, 11 Mar 2025 09:37:29 +0200 Subject: [PATCH 272/345] UI: Metric lable to Absolute --- .../main/data/json/system/scada_symbols/cylindrical-tank.svg | 2 +- .../src/main/data/json/system/scada_symbols/elevated-tank.svg | 2 +- .../main/data/json/system/scada_symbols/horizontal-tank-hp.svg | 2 +- .../src/main/data/json/system/scada_symbols/horizontal-tank.svg | 2 +- .../data/json/system/scada_symbols/large-cylindrical-tank.svg | 2 +- .../json/system/scada_symbols/large-stand-cylindrical-tank.svg | 2 +- .../json/system/scada_symbols/large-stand-vertical-tank.svg | 2 +- .../main/data/json/system/scada_symbols/large-vertical-tank.svg | 2 +- application/src/main/data/json/system/scada_symbols/pool-hp.svg | 2 +- .../data/json/system/scada_symbols/short-vertical-tank-hp.svg | 2 +- .../data/json/system/scada_symbols/small-cylindrical-tank.svg | 2 +- .../data/json/system/scada_symbols/small-spherical-tank.svg | 2 +- .../src/main/data/json/system/scada_symbols/spherical-tank.svg | 2 +- .../data/json/system/scada_symbols/stand-cylindrical-tank.svg | 2 +- .../data/json/system/scada_symbols/stand-horizontal-tank.svg | 2 +- .../json/system/scada_symbols/stand-vertical-short-tank.svg | 2 +- .../main/data/json/system/scada_symbols/stand-vertical-tank.svg | 2 +- .../main/data/json/system/scada_symbols/vertical-short-tank.svg | 2 +- .../main/data/json/system/scada_symbols/vertical-tank-hp.svg | 2 +- .../src/main/data/json/system/scada_symbols/vertical-tank.svg | 2 +- 20 files changed, 20 insertions(+), 20 deletions(-) diff --git a/application/src/main/data/json/system/scada_symbols/cylindrical-tank.svg b/application/src/main/data/json/system/scada_symbols/cylindrical-tank.svg index ab0b5415bd..acb895cd5a 100644 --- a/application/src/main/data/json/system/scada_symbols/cylindrical-tank.svg +++ b/application/src/main/data/json/system/scada_symbols/cylindrical-tank.svg @@ -362,7 +362,7 @@ }, { "value": false, - "label": "Metric" + "label": "Absolute" } ], "disabled": false, diff --git a/application/src/main/data/json/system/scada_symbols/elevated-tank.svg b/application/src/main/data/json/system/scada_symbols/elevated-tank.svg index 7019e3221a..48668e9851 100644 --- a/application/src/main/data/json/system/scada_symbols/elevated-tank.svg +++ b/application/src/main/data/json/system/scada_symbols/elevated-tank.svg @@ -367,7 +367,7 @@ }, { "value": false, - "label": "Metric" + "label": "Absolute" } ], "disabled": false, diff --git a/application/src/main/data/json/system/scada_symbols/horizontal-tank-hp.svg b/application/src/main/data/json/system/scada_symbols/horizontal-tank-hp.svg index 2adde5881f..18f194684b 100644 --- a/application/src/main/data/json/system/scada_symbols/horizontal-tank-hp.svg +++ b/application/src/main/data/json/system/scada_symbols/horizontal-tank-hp.svg @@ -340,7 +340,7 @@ }, { "value": false, - "label": "Metric" + "label": "Absolute" } ], "disabled": false, diff --git a/application/src/main/data/json/system/scada_symbols/horizontal-tank.svg b/application/src/main/data/json/system/scada_symbols/horizontal-tank.svg index f410d709f4..b6b6eede7b 100644 --- a/application/src/main/data/json/system/scada_symbols/horizontal-tank.svg +++ b/application/src/main/data/json/system/scada_symbols/horizontal-tank.svg @@ -362,7 +362,7 @@ }, { "value": false, - "label": "Metric" + "label": "Absolute" } ], "disabled": false, diff --git a/application/src/main/data/json/system/scada_symbols/large-cylindrical-tank.svg b/application/src/main/data/json/system/scada_symbols/large-cylindrical-tank.svg index 98f61f0094..69360fbe9c 100644 --- a/application/src/main/data/json/system/scada_symbols/large-cylindrical-tank.svg +++ b/application/src/main/data/json/system/scada_symbols/large-cylindrical-tank.svg @@ -362,7 +362,7 @@ }, { "value": false, - "label": "Metric" + "label": "Absolute" } ], "disabled": false, diff --git a/application/src/main/data/json/system/scada_symbols/large-stand-cylindrical-tank.svg b/application/src/main/data/json/system/scada_symbols/large-stand-cylindrical-tank.svg index ac94438463..11bd47916f 100644 --- a/application/src/main/data/json/system/scada_symbols/large-stand-cylindrical-tank.svg +++ b/application/src/main/data/json/system/scada_symbols/large-stand-cylindrical-tank.svg @@ -363,7 +363,7 @@ }, { "value": false, - "label": "Metric" + "label": "Absolute" } ] }, diff --git a/application/src/main/data/json/system/scada_symbols/large-stand-vertical-tank.svg b/application/src/main/data/json/system/scada_symbols/large-stand-vertical-tank.svg index 5b534b615c..d9c05bde40 100644 --- a/application/src/main/data/json/system/scada_symbols/large-stand-vertical-tank.svg +++ b/application/src/main/data/json/system/scada_symbols/large-stand-vertical-tank.svg @@ -363,7 +363,7 @@ }, { "value": false, - "label": "Metric" + "label": "Absolute" } ], "disabled": false, diff --git a/application/src/main/data/json/system/scada_symbols/large-vertical-tank.svg b/application/src/main/data/json/system/scada_symbols/large-vertical-tank.svg index 33bd6c2f39..cc168915ad 100644 --- a/application/src/main/data/json/system/scada_symbols/large-vertical-tank.svg +++ b/application/src/main/data/json/system/scada_symbols/large-vertical-tank.svg @@ -362,7 +362,7 @@ }, { "value": false, - "label": "Metric" + "label": "Absolute" } ], "disabled": false, diff --git a/application/src/main/data/json/system/scada_symbols/pool-hp.svg b/application/src/main/data/json/system/scada_symbols/pool-hp.svg index 74757b77d6..925ea8906a 100644 --- a/application/src/main/data/json/system/scada_symbols/pool-hp.svg +++ b/application/src/main/data/json/system/scada_symbols/pool-hp.svg @@ -340,7 +340,7 @@ }, { "value": false, - "label": "Metric" + "label": "Absolute" } ], "disabled": false, diff --git a/application/src/main/data/json/system/scada_symbols/short-vertical-tank-hp.svg b/application/src/main/data/json/system/scada_symbols/short-vertical-tank-hp.svg index 1ce12cafe2..046c5c5802 100644 --- a/application/src/main/data/json/system/scada_symbols/short-vertical-tank-hp.svg +++ b/application/src/main/data/json/system/scada_symbols/short-vertical-tank-hp.svg @@ -340,7 +340,7 @@ }, { "value": false, - "label": "Metric" + "label": "Absolute" } ], "disabled": false, diff --git a/application/src/main/data/json/system/scada_symbols/small-cylindrical-tank.svg b/application/src/main/data/json/system/scada_symbols/small-cylindrical-tank.svg index 9af2be50c4..d4ddda1895 100644 --- a/application/src/main/data/json/system/scada_symbols/small-cylindrical-tank.svg +++ b/application/src/main/data/json/system/scada_symbols/small-cylindrical-tank.svg @@ -363,7 +363,7 @@ }, { "value": false, - "label": "Metric" + "label": "Absolute" } ], "disabled": false, diff --git a/application/src/main/data/json/system/scada_symbols/small-spherical-tank.svg b/application/src/main/data/json/system/scada_symbols/small-spherical-tank.svg index e0b50e3200..b8e3987188 100644 --- a/application/src/main/data/json/system/scada_symbols/small-spherical-tank.svg +++ b/application/src/main/data/json/system/scada_symbols/small-spherical-tank.svg @@ -363,7 +363,7 @@ }, { "value": false, - "label": "Metric" + "label": "Absolute" } ], "disabled": false, diff --git a/application/src/main/data/json/system/scada_symbols/spherical-tank.svg b/application/src/main/data/json/system/scada_symbols/spherical-tank.svg index 11698d6636..cba49c63e0 100644 --- a/application/src/main/data/json/system/scada_symbols/spherical-tank.svg +++ b/application/src/main/data/json/system/scada_symbols/spherical-tank.svg @@ -363,7 +363,7 @@ }, { "value": false, - "label": "Metric" + "label": "Absolute" } ], "disabled": false, diff --git a/application/src/main/data/json/system/scada_symbols/stand-cylindrical-tank.svg b/application/src/main/data/json/system/scada_symbols/stand-cylindrical-tank.svg index 9ee0eb5122..a78060dfaf 100644 --- a/application/src/main/data/json/system/scada_symbols/stand-cylindrical-tank.svg +++ b/application/src/main/data/json/system/scada_symbols/stand-cylindrical-tank.svg @@ -363,7 +363,7 @@ }, { "value": false, - "label": "Metric" + "label": "Absolute" } ], "disabled": false, diff --git a/application/src/main/data/json/system/scada_symbols/stand-horizontal-tank.svg b/application/src/main/data/json/system/scada_symbols/stand-horizontal-tank.svg index 5ec5f34b8f..63a21bcf08 100644 --- a/application/src/main/data/json/system/scada_symbols/stand-horizontal-tank.svg +++ b/application/src/main/data/json/system/scada_symbols/stand-horizontal-tank.svg @@ -363,7 +363,7 @@ }, { "value": false, - "label": "Metric" + "label": "Absolute" } ], "disabled": false, diff --git a/application/src/main/data/json/system/scada_symbols/stand-vertical-short-tank.svg b/application/src/main/data/json/system/scada_symbols/stand-vertical-short-tank.svg index a92ba1e274..96df175589 100644 --- a/application/src/main/data/json/system/scada_symbols/stand-vertical-short-tank.svg +++ b/application/src/main/data/json/system/scada_symbols/stand-vertical-short-tank.svg @@ -364,7 +364,7 @@ }, { "value": false, - "label": "Metric" + "label": "Absolute" } ], "disabled": false, diff --git a/application/src/main/data/json/system/scada_symbols/stand-vertical-tank.svg b/application/src/main/data/json/system/scada_symbols/stand-vertical-tank.svg index 1b19157341..0d0e368b9b 100644 --- a/application/src/main/data/json/system/scada_symbols/stand-vertical-tank.svg +++ b/application/src/main/data/json/system/scada_symbols/stand-vertical-tank.svg @@ -363,7 +363,7 @@ }, { "value": false, - "label": "Metric" + "label": "Absolute" } ] }, diff --git a/application/src/main/data/json/system/scada_symbols/vertical-short-tank.svg b/application/src/main/data/json/system/scada_symbols/vertical-short-tank.svg index 23fe4a2733..57a79d72d7 100644 --- a/application/src/main/data/json/system/scada_symbols/vertical-short-tank.svg +++ b/application/src/main/data/json/system/scada_symbols/vertical-short-tank.svg @@ -363,7 +363,7 @@ }, { "value": false, - "label": "Metric" + "label": "Absolute" } ], "disabled": false, diff --git a/application/src/main/data/json/system/scada_symbols/vertical-tank-hp.svg b/application/src/main/data/json/system/scada_symbols/vertical-tank-hp.svg index e1b57d7906..491fb4477e 100644 --- a/application/src/main/data/json/system/scada_symbols/vertical-tank-hp.svg +++ b/application/src/main/data/json/system/scada_symbols/vertical-tank-hp.svg @@ -340,7 +340,7 @@ }, { "value": false, - "label": "Metric" + "label": "Absolute" } ], "disabled": false, diff --git a/application/src/main/data/json/system/scada_symbols/vertical-tank.svg b/application/src/main/data/json/system/scada_symbols/vertical-tank.svg index 868256e3df..99fa9b649c 100644 --- a/application/src/main/data/json/system/scada_symbols/vertical-tank.svg +++ b/application/src/main/data/json/system/scada_symbols/vertical-tank.svg @@ -363,7 +363,7 @@ }, { "value": false, - "label": "Metric" + "label": "Absolute" } ], "disabled": false, From 23a71b36fdd6de8cc1bdcff555078afae3c2e76a Mon Sep 17 00:00:00 2001 From: Igor Kulikov Date: Tue, 11 Mar 2025 10:33:33 +0200 Subject: [PATCH 273/345] UI: Map widget: Improve default trip data layer settings. --- ui-ngx/src/app/shared/models/widget/maps/map.models.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) 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 5da384da39..6249057cb2 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 @@ -438,19 +438,19 @@ export const defaultBaseTripsDataLayerSettings = (mapType: MapType): Partial Date: Tue, 11 Mar 2025 10:43:34 +0200 Subject: [PATCH 274/345] Improve KafkaEdgeTopicsCleanUpService --- .../service/ttl/KafkaEdgeTopicsCleanUpService.java | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/application/src/main/java/org/thingsboard/server/service/ttl/KafkaEdgeTopicsCleanUpService.java b/application/src/main/java/org/thingsboard/server/service/ttl/KafkaEdgeTopicsCleanUpService.java index 5a74f9f69a..73712542fd 100644 --- a/application/src/main/java/org/thingsboard/server/service/ttl/KafkaEdgeTopicsCleanUpService.java +++ b/application/src/main/java/org/thingsboard/server/service/ttl/KafkaEdgeTopicsCleanUpService.java @@ -53,8 +53,6 @@ import static org.thingsboard.server.service.state.DefaultDeviceStateService.LAS @ConditionalOnExpression("'${queue.type:null}'=='kafka' && ${edges.enabled:true} && ${sql.ttl.edge_events.edge_events_ttl:0} > 0") public class KafkaEdgeTopicsCleanUpService extends AbstractCleanUpService { - private static final String EDGE_EVENT_TOPIC_NAME = "tb_edge_event.notifications."; - private final TopicService topicService; private final TenantService tenantService; private final EdgeService edgeService; @@ -64,6 +62,9 @@ public class KafkaEdgeTopicsCleanUpService extends AbstractCleanUpService { @Value("${sql.ttl.edge_events.edge_events_ttl:2628000}") private long ttlSeconds; + @Value("${queue.edge.event-notifications-topic:tb_edge_event.notifications}") + private String tbEdgeEventNotificationsTopic; + public KafkaEdgeTopicsCleanUpService(PartitionService partitionService, EdgeService edgeService, TenantService tenantService, AttributesService attributesService, TopicService topicService, TbKafkaSettings kafkaSettings, TbKafkaTopicConfigs kafkaTopicConfigs) { @@ -86,7 +87,7 @@ public class KafkaEdgeTopicsCleanUpService extends AbstractCleanUpService { return; } - String edgeTopicPrefix = topicService.buildTopicName(EDGE_EVENT_TOPIC_NAME); + String edgeTopicPrefix = topicService.buildTopicName(tbEdgeEventNotificationsTopic); List matchingTopics = topics.stream().filter(topic -> topic.startsWith(edgeTopicPrefix)).toList(); if (matchingTopics.isEmpty()) { log.debug("No matching topics found with prefix [{}]. Skipping cleanup.", edgeTopicPrefix); @@ -147,7 +148,7 @@ public class KafkaEdgeTopicsCleanUpService extends AbstractCleanUpService { try { String remaining = topic.substring(prefix.length()); String[] parts = remaining.split("\\."); - TenantId tenantId = new TenantId(UUID.fromString(parts[0])); + TenantId tenantId = TenantId.fromUUID(UUID.fromString(parts[0])); EdgeId edgeId = new EdgeId(UUID.fromString(parts[1])); tenantEdgeMap.computeIfAbsent(tenantId, id -> new ArrayList<>()).add(edgeId); } catch (Exception e) { From 6ca15fe600196aeb1d59418554e2b47ab3801311 Mon Sep 17 00:00:00 2001 From: Artem Dzhereleiko Date: Tue, 11 Mar 2025 10:51:24 +0200 Subject: [PATCH 275/345] UI: Fixed drag and drop for data keys chip --- .../home/components/widget/config/data-keys.component.scss | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ui-ngx/src/app/modules/home/components/widget/config/data-keys.component.scss b/ui-ngx/src/app/modules/home/components/widget/config/data-keys.component.scss index 8b377a8456..0329b24432 100644 --- a/ui-ngx/src/app/modules/home/components/widget/config/data-keys.component.scss +++ b/ui-ngx/src/app/modules/home/components/widget/config/data-keys.component.scss @@ -19,7 +19,7 @@ width: 100%; input.tb-dragging { - display: none; + visibility: hidden; } } From b4898568d1ec38c5c6419fdc72adc56f175c3f93 Mon Sep 17 00:00:00 2001 From: dashevchenko Date: Tue, 11 Mar 2025 15:12:01 +0200 Subject: [PATCH 276/345] removed edqs stats logs, fixed edqs apiUsage name --- application/src/main/resources/thingsboard.yml | 2 -- .../server/common/data/edqs/fields/FieldsUtil.java | 2 +- .../thingsboard/server/edqs/repo/TenantRepo.java | 3 +++ .../server/edqs/stats/EdqsStatsService.java | 13 ------------- edqs/src/main/resources/edqs.yml | 2 -- 5 files changed, 4 insertions(+), 18 deletions(-) diff --git a/application/src/main/resources/thingsboard.yml b/application/src/main/resources/thingsboard.yml index badd779350..0483526831 100644 --- a/application/src/main/resources/thingsboard.yml +++ b/application/src/main/resources/thingsboard.yml @@ -1757,8 +1757,6 @@ queue: stats: # Enable/disable statistics for EDQS enabled: "${TB_EDQS_STATS_ENABLED:true}" - # Statistics printing interval for EDQS - print-interval-ms: "${TB_EDQS_STATS_PRINT_INTERVAL_MS:300000}" vc: # Default topic name diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/edqs/fields/FieldsUtil.java b/common/data/src/main/java/org/thingsboard/server/common/data/edqs/fields/FieldsUtil.java index a36514248c..9ba6c20188 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/edqs/fields/FieldsUtil.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/edqs/fields/FieldsUtil.java @@ -289,7 +289,7 @@ public class FieldsUtil { } public static String getText(JsonNode node) { - return node != null ? node.asText() : ""; + return node != null ? node.toString() : ""; } private static UUID getCustomerId(CustomerId customerId) { diff --git a/common/edqs/src/main/java/org/thingsboard/server/edqs/repo/TenantRepo.java b/common/edqs/src/main/java/org/thingsboard/server/edqs/repo/TenantRepo.java index 0c55bc50dd..59255b11cc 100644 --- a/common/edqs/src/main/java/org/thingsboard/server/edqs/repo/TenantRepo.java +++ b/common/edqs/src/main/java/org/thingsboard/server/edqs/repo/TenantRepo.java @@ -430,6 +430,9 @@ public class TenantRepo { private String getEntityName(EntityId entityId) { EntityType entityType = entityId.getEntityType(); + if (entityType == EntityType.TENANT && entityId.getId().equals(TenantId.NULL_UUID)) { + return ""; + } return switch (entityType) { case CUSTOMER, TENANT -> getEntityMap(entityType).get(entityId.getId()).getFields().getName(); default -> throw new RuntimeException("Unsupported entity type: " + entityType); diff --git a/common/edqs/src/main/java/org/thingsboard/server/edqs/stats/EdqsStatsService.java b/common/edqs/src/main/java/org/thingsboard/server/edqs/stats/EdqsStatsService.java index a12a12dbe3..442453fc93 100644 --- a/common/edqs/src/main/java/org/thingsboard/server/edqs/stats/EdqsStatsService.java +++ b/common/edqs/src/main/java/org/thingsboard/server/edqs/stats/EdqsStatsService.java @@ -20,7 +20,6 @@ import lombok.Getter; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; -import org.springframework.scheduling.annotation.Scheduled; import org.springframework.stereotype.Service; import org.thingsboard.server.common.data.ObjectType; import org.thingsboard.server.common.data.edqs.EdqsEventType; @@ -43,18 +42,6 @@ public class EdqsStatsService { private final ConcurrentHashMap statsMap = new ConcurrentHashMap<>(); private final StatsFactory statsFactory; - @Scheduled(initialDelayString = "${queue.edqs.stats.print-interval-ms:300000}", - fixedDelayString = "${queue.edqs.stats.print-interval-ms:300000}") - private void reportStats() { - if (statsMap.isEmpty()) { - return; - } - String values = statsMap.entrySet().stream() - .map(kv -> "TenantId [" + kv.getKey() + "] stats [" + kv.getValue() + "]") - .collect(Collectors.joining(System.lineSeparator())); - log.info("EDQS Stats: {}", values); - } - public void reportEvent(TenantId tenantId, ObjectType objectType, EdqsEventType eventType) { statsMap.computeIfAbsent(tenantId, id -> new EdqsStats(tenantId, statsFactory)) .reportEvent(objectType, eventType); diff --git a/edqs/src/main/resources/edqs.yml b/edqs/src/main/resources/edqs.yml index 1d7f111e9a..f7d0eda841 100644 --- a/edqs/src/main/resources/edqs.yml +++ b/edqs/src/main/resources/edqs.yml @@ -70,8 +70,6 @@ queue: stats: # Enable/disable statistics for EDQS enabled: "${TB_EDQS_STATS_ENABLED:true}" - # Statistics printing interval for EDQS - print-interval-ms: "${TB_EDQS_STATS_PRINT_INTERVAL_MS:300000}" kafka: # Kafka Bootstrap nodes in "host:port" format From e7b931690aa48db8ac45602599a364f8e48df14b Mon Sep 17 00:00:00 2001 From: Artem Barysh Date: Tue, 11 Mar 2025 15:21:33 +0200 Subject: [PATCH 277/345] TBEL formatting --- .../shared/models/ace/tbel-utils.models.ts | 198 +++++++++--------- 1 file changed, 99 insertions(+), 99 deletions(-) diff --git a/ui-ngx/src/app/shared/models/ace/tbel-utils.models.ts b/ui-ngx/src/app/shared/models/ace/tbel-utils.models.ts index a595428e63..53d0ccdd15 100644 --- a/ui-ngx/src/app/shared/models/ace/tbel-utils.models.ts +++ b/ui-ngx/src/app/shared/models/ace/tbel-utils.models.ts @@ -23,7 +23,7 @@ export const tbelUtilsAutocompletes = new TbEditorCompleter({ description: 'Encodes a string to Base64.', args: [ { - name: 'input', + name: 'str', description: 'The string to encode', type: 'string' } @@ -38,7 +38,7 @@ export const tbelUtilsAutocompletes = new TbEditorCompleter({ description: 'Decodes a Base64 encoded string.', args: [ { - name: 'encoded', + name: 'str', description: 'The Base64 encoded string to decode', type: 'string' } @@ -53,9 +53,9 @@ export const tbelUtilsAutocompletes = new TbEditorCompleter({ description: 'Converts a list of bytes to a string, optionally specifying the charset.', args: [ { - name: 'bytesList', + name: 'data', description: 'The list of bytes to convert', - type: 'array' + type: 'list' }, { name: 'charsetName', @@ -71,12 +71,12 @@ export const tbelUtilsAutocompletes = new TbEditorCompleter({ }, decodeToString: { meta: 'function', - description: 'Converts a list of bytes to a string using the default charset.', + description: 'Converts a list of bytes to a string.', args: [ { - name: 'bytesList', + name: 'data', description: 'The list of bytes to convert', - type: 'array' + type: 'list' } ], return: { @@ -89,13 +89,13 @@ export const tbelUtilsAutocompletes = new TbEditorCompleter({ description: 'Parses a JSON string or converts a list of bytes to a string and parses it as JSON.', args: [ { - name: 'input', - description: 'The JSON string or list of bytes to parse', - type: 'string | array' + name: 'data', + description: 'The JSON string or list of bytes to parse into JSON object', + type: 'string | list' } ], return: { - description: 'The parsed JSON object (e.g., object, array, or primitive)', + description: 'The parsed JSON object', type: 'object' } }, @@ -117,7 +117,7 @@ export const tbelUtilsAutocompletes = new TbEditorCompleter({ ], return: { description: 'The list of bytes representing the string', - type: 'array' + type: 'list' } }, parseInt: { @@ -125,13 +125,13 @@ export const tbelUtilsAutocompletes = new TbEditorCompleter({ description: 'Parses a string to an integer, optionally specifying the radix.', args: [ { - name: 'value', + name: 'str', description: 'The string to parse', type: 'string' }, { name: 'radix', - description: 'The radix for parsing (e.g., 2 for binary, 16 for hex, defaults to auto-detection if 0)', + description: 'The radix for parsing (e.g., 2 for binary, 16 for hex). If omitted, it is auto-detected (e.g., 0x for hex).', type: 'number', optional: true } @@ -146,13 +146,13 @@ export const tbelUtilsAutocompletes = new TbEditorCompleter({ description: 'Parses a string to a long integer, optionally specifying the radix.', args: [ { - name: 'value', + name: 'str', description: 'The string to parse', type: 'string' }, { name: 'radix', - description: 'The radix for parsing (e.g., 2 for binary, 16 for hex, defaults to auto-detection if 0)', + description: 'The radix for parsing (e.g., 2 for binary, 16 for hex). If omitted, it is auto-detected (e.g., 0x for hex).', type: 'number', optional: true } @@ -164,16 +164,16 @@ export const tbelUtilsAutocompletes = new TbEditorCompleter({ }, parseFloat: { meta: 'function', - description: 'Parses a string to a float. If radix is 16, interprets the string as hexadecimal IEEE 754 float bits.', + description: 'Parses a string to a float, optionally specifying the radix.', args: [ { - name: 'value', + name: 'str', description: 'The string to parse', type: 'string' }, { name: 'radix', - description: 'The radix for parsing (e.g., 16 for hex, defaults to 10 if 0)', + description: 'The radix for parsing (e.g., 16 indicates a standard IEEE 754 hexadecimal float, defaults to 10 if unspecified)', type: 'number', optional: true } @@ -188,7 +188,7 @@ export const tbelUtilsAutocompletes = new TbEditorCompleter({ description: 'Parses a hexadecimal string to a float, treating it as an integer value.', args: [ { - name: 'value', + name: 'hex', description: 'The hexadecimal string to parse (e.g., "0x0A" for 10.0)', type: 'string' }, @@ -205,16 +205,16 @@ export const tbelUtilsAutocompletes = new TbEditorCompleter({ }, parseDouble: { meta: 'function', - description: 'Parses a string to a double. If radix is 16, interprets the string as hexadecimal IEEE 754 double bits.', + description: 'Parses a string to a double, optionally specifying the radix.', args: [ { - name: 'value', + name: 'str', description: 'The string to parse', type: 'string' }, { name: 'radix', - description: 'The radix for parsing (e.g., 16 for hex, defaults to 10 if unspecified)', + description: 'The radix for parsing (e.g., 16 indicates a standard IEEE 754 double bits, defaults to 10 if unspecified)', type: 'number', optional: true } @@ -259,7 +259,7 @@ export const tbelUtilsAutocompletes = new TbEditorCompleter({ description: 'Parses a hexadecimal string to an integer, optionally specifying endianness.', args: [ { - name: 'value', + name: 'hex', description: 'The hexadecimal string to parse', type: 'string' }, @@ -282,11 +282,11 @@ export const tbelUtilsAutocompletes = new TbEditorCompleter({ { name: 'data', description: 'The bytes to parse', - type: 'array' + type: 'list | array' }, { name: 'offset', - description: 'The starting index in the byte array', + description: 'The starting index in the byte list or array (defaults to 0)', type: 'number', optional: true }, @@ -343,7 +343,7 @@ export const tbelUtilsAutocompletes = new TbEditorCompleter({ description: 'Parses a hexadecimal string to a long integer, optionally specifying endianness.', args: [ { - name: 'value', + name: 'hex', description: 'The hexadecimal string to parse', type: 'string' }, @@ -366,11 +366,11 @@ export const tbelUtilsAutocompletes = new TbEditorCompleter({ { name: 'data', description: 'The bytes to parse', - type: 'array' + type: 'list | array' }, { name: 'offset', - description: 'The starting index in the byte array', + description: 'The starting index in the byte list or array (defaults to 0)', type: 'number', optional: true }, @@ -427,7 +427,7 @@ export const tbelUtilsAutocompletes = new TbEditorCompleter({ description: 'Parses a hexadecimal string to a float using IEEE 754 format, optionally specifying endianness.', args: [ { - name: 'value', + name: 'hex', description: 'The hexadecimal string to parse', type: 'string' }, @@ -450,11 +450,11 @@ export const tbelUtilsAutocompletes = new TbEditorCompleter({ { name: 'data', description: 'The bytes to parse', - type: 'array' + type: 'list | array' }, { name: 'offset', - description: 'The starting index in the byte array', + description: 'The starting index in the byte list or array (defaults to 0)', type: 'number', optional: true }, @@ -483,11 +483,11 @@ export const tbelUtilsAutocompletes = new TbEditorCompleter({ { name: 'data', description: 'The bytes to parse', - type: 'array' + type: 'list | array' }, { name: 'offset', - description: 'The starting index in the byte array', + description: 'The starting index in the byte list or array (defaults to 0)', type: 'number', optional: true }, @@ -544,7 +544,7 @@ export const tbelUtilsAutocompletes = new TbEditorCompleter({ description: 'Parses a hexadecimal string to a double using IEEE 754 format, optionally specifying endianness.', args: [ { - name: 'value', + name: 'hex', description: 'The hexadecimal string to parse', type: 'string' }, @@ -567,11 +567,11 @@ export const tbelUtilsAutocompletes = new TbEditorCompleter({ { name: 'data', description: 'The bytes to parse', - type: 'array' + type: 'list | array' }, { name: 'offset', - description: 'The starting index in the byte array', + description: 'The starting index in the byte list or array (defaults to 0)', type: 'number', optional: true }, @@ -600,11 +600,11 @@ export const tbelUtilsAutocompletes = new TbEditorCompleter({ { name: 'data', description: 'The bytes to parse', - type: 'array' + type: 'list | array' }, { name: 'offset', - description: 'The starting index in the byte array', + description: 'The starting index in the byte list or array (defaults to 0)', type: 'number', optional: true }, @@ -628,11 +628,11 @@ export const tbelUtilsAutocompletes = new TbEditorCompleter({ }, toFixed: { meta: 'function', - description: 'Rounds a number to a specified number of decimal places.', + description: 'Rounds a floating-point number to a set precision using half-up rounding.', args: [ { name: 'value', - description: 'The number to round', + description: 'The floating-point number', type: 'number' }, { @@ -642,17 +642,17 @@ export const tbelUtilsAutocompletes = new TbEditorCompleter({ } ], return: { - description: 'The rounded number', + description: 'The rounded floating-point number.', type: 'number' } }, toInt: { meta: 'function', - description: 'Converts a double to an integer by rounding.', + description: 'Converts a floating-point number to an integer by half-up rounding.', args: [ { name: 'value', - description: 'The double to convert', + description: 'The floating-point number to convert', type: 'number' } ], @@ -666,14 +666,14 @@ export const tbelUtilsAutocompletes = new TbEditorCompleter({ description: 'Converts a hexadecimal string to a list of bytes.', args: [ { - name: 'value', + name: 'hex', description: 'The hexadecimal string to convert', type: 'string' } ], return: { description: 'The list of bytes', - type: 'array' + type: 'list' } }, hexToBytesArray: { @@ -681,7 +681,7 @@ export const tbelUtilsAutocompletes = new TbEditorCompleter({ description: 'Converts a hexadecimal string to an array of bytes.', args: [ { - name: 'value', + name: 'hex', description: 'The hexadecimal string to convert', type: 'string' } @@ -696,7 +696,7 @@ export const tbelUtilsAutocompletes = new TbEditorCompleter({ description: 'Converts an integer to a hexadecimal string.', args: [ { - name: 'i', + name: 'value', description: 'The integer to convert', type: 'number' }, @@ -707,13 +707,13 @@ export const tbelUtilsAutocompletes = new TbEditorCompleter({ optional: true }, { - name: 'pref', + name: 'prefix', description: 'Whether to prefix with "0x" (defaults to false)', type: 'boolean', optional: true }, { - name: 'len', + name: 'length', description: 'The desired length of the hex string (defaults to minimum required)', type: 'number', optional: true @@ -729,7 +729,7 @@ export const tbelUtilsAutocompletes = new TbEditorCompleter({ description: 'Converts a long integer to a hexadecimal string.', args: [ { - name: 'l', + name: 'value', description: 'The long integer to convert', type: 'number' }, @@ -740,13 +740,13 @@ export const tbelUtilsAutocompletes = new TbEditorCompleter({ optional: true }, { - name: 'pref', + name: 'prefix', description: 'Whether to prefix with "0x" (defaults to false)', type: 'boolean', optional: true }, { - name: 'len', + name: 'length', description: 'The desired length of the hex string (defaults to minimum required)', type: 'number', optional: true @@ -762,13 +762,13 @@ export const tbelUtilsAutocompletes = new TbEditorCompleter({ description: 'Converts a long integer to a string in the specified radix.', args: [ { - name: 'number', + name: 'value', description: 'The number to convert', type: 'number' }, { name: 'radix', - description: 'The radix for conversion (e.g., 2, 8, 10, 16, defaults to 10)', + description: 'The radix for conversion (e.g., 2 for binary, 16 for hex). If omitted, it defaults to 10.', type: 'number', optional: true }, @@ -779,7 +779,7 @@ export const tbelUtilsAutocompletes = new TbEditorCompleter({ optional: true }, { - name: 'pref', + name: 'prefix', description: 'Whether to prefix hex with "0x" (defaults to false)', type: 'boolean', optional: true @@ -795,7 +795,7 @@ export const tbelUtilsAutocompletes = new TbEditorCompleter({ description: 'Converts a float to its IEEE 754 hexadecimal representation.', args: [ { - name: 'f', + name: 'value', description: 'The float to convert', type: 'number' }, @@ -807,7 +807,7 @@ export const tbelUtilsAutocompletes = new TbEditorCompleter({ } ], return: { - description: 'The hexadecimal string (e.g., "0x41200000" for 10.0)', + description: 'The hexadecimal string', type: 'string' } }, @@ -816,7 +816,7 @@ export const tbelUtilsAutocompletes = new TbEditorCompleter({ description: 'Converts a double to its IEEE 754 hexadecimal representation.', args: [ { - name: 'd', + name: 'value', description: 'The double to convert', type: 'number' }, @@ -828,7 +828,7 @@ export const tbelUtilsAutocompletes = new TbEditorCompleter({ } ], return: { - description: 'The hexadecimal string (e.g., "0x4024000000000000" for 10.0)', + description: 'The hexadecimal string', type: 'string' } }, @@ -837,14 +837,14 @@ export const tbelUtilsAutocompletes = new TbEditorCompleter({ description: 'Converts a list of signed bytes to a list of unsigned integer values.', args: [ { - name: 'byteArray', + name: 'data', description: 'The list of bytes to convert', - type: 'array' + type: 'list' } ], return: { description: 'The list of unsigned integers (0-255)', - type: 'array' + type: 'list' } }, base64ToHex: { @@ -852,7 +852,7 @@ export const tbelUtilsAutocompletes = new TbEditorCompleter({ description: 'Converts a Base64 string to a hexadecimal string.', args: [ { - name: 'base64', + name: 'str', description: 'The Base64 string to convert', type: 'string' } @@ -882,7 +882,7 @@ export const tbelUtilsAutocompletes = new TbEditorCompleter({ description: 'Converts a Base64 string to an array of bytes.', args: [ { - name: 'input', + name: 'str', description: 'The Base64 string to convert', type: 'string' } @@ -897,14 +897,14 @@ export const tbelUtilsAutocompletes = new TbEditorCompleter({ description: 'Converts a Base64 string to a list of bytes.', args: [ { - name: 'input', + name: 'str', description: 'The Base64 string to convert', type: 'string' } ], return: { description: 'The list of bytes', - type: 'array' + type: 'list' } }, bytesToBase64: { @@ -912,7 +912,7 @@ export const tbelUtilsAutocompletes = new TbEditorCompleter({ description: 'Converts an array of bytes to a Base64 string.', args: [ { - name: 'bytes', + name: 'data', description: 'The array of bytes to convert', type: 'array' } @@ -924,12 +924,12 @@ export const tbelUtilsAutocompletes = new TbEditorCompleter({ }, bytesToHex: { meta: 'function', - description: 'Converts an array or list of bytes to a hexadecimal string.', + description: 'Converts a list or array of bytes to a hexadecimal string.', args: [ { - name: 'bytes', + name: 'data', description: 'The bytes to convert', - type: 'array' + type: 'list | array' } ], return: { @@ -939,7 +939,7 @@ export const tbelUtilsAutocompletes = new TbEditorCompleter({ }, toFlatMap: { meta: 'function', - description: 'Converts a nested map to a flat map, with options for key paths and exclusions.', + description: 'Converts a nested map to a flat map, with customizable key paths and exclusions', args: [ { name: 'json', @@ -947,9 +947,9 @@ export const tbelUtilsAutocompletes = new TbEditorCompleter({ type: 'object' }, { - name: 'excludeList', + name: 'excludeKeys', description: 'List of keys to exclude from flattening', - type: 'array', + type: 'list', optional: true }, { @@ -969,7 +969,7 @@ export const tbelUtilsAutocompletes = new TbEditorCompleter({ description: 'Encodes a URI string, preserving certain characters as per MDN standards.', args: [ { - name: 'uri', + name: 'str', description: 'The URI string to encode', type: 'string' } @@ -981,10 +981,10 @@ export const tbelUtilsAutocompletes = new TbEditorCompleter({ }, decodeURI: { meta: 'function', - description: 'Decodes a URI string previously encoded with encodeURI.', + description: 'Decodes a URI string previously encoded.', args: [ { - name: 'uri', + name: 'str', description: 'The URI string to decode', type: 'string' } @@ -999,7 +999,7 @@ export const tbelUtilsAutocompletes = new TbEditorCompleter({ description: 'Throws an error with a custom message.', args: [ { - name: 'message', + name: 'str', description: 'The error message to throw', type: 'string' } @@ -1071,17 +1071,17 @@ export const tbelUtilsAutocompletes = new TbEditorCompleter({ }, bytesToExecutionArrayList: { meta: 'function', - description: 'Converts an array of bytes to an execution array list.', + description: 'Converts an array of bytes to a list.', args: [ { - name: 'byteArray', + name: 'data', description: 'The array of bytes to convert', type: 'array' } ], return: { - description: 'The execution array list of bytes', - type: 'array' + description: 'The list of bytes', + type: 'list' } }, padStart: { @@ -1094,7 +1094,7 @@ export const tbelUtilsAutocompletes = new TbEditorCompleter({ type: 'string' }, { - name: 'targetLength', + name: 'length', description: 'The desired length of the resulting string', type: 'number' }, @@ -1119,7 +1119,7 @@ export const tbelUtilsAutocompletes = new TbEditorCompleter({ type: 'string' }, { - name: 'targetLength', + name: 'length', description: 'The desired length of the resulting string', type: 'number' }, @@ -1139,12 +1139,12 @@ export const tbelUtilsAutocompletes = new TbEditorCompleter({ description: 'Converts a byte to a binary array.', args: [ { - name: 'byteValue', + name: 'value', description: 'The byte value to convert', type: 'number' }, { - name: 'binLength', + name: 'length', description: 'The length of the binary array (defaults to 8)', type: 'number', optional: true @@ -1157,7 +1157,7 @@ export const tbelUtilsAutocompletes = new TbEditorCompleter({ } ], return: { - description: 'The binary array (array of 0s and 1s)', + description: 'The binary array', type: 'array' } }, @@ -1166,19 +1166,19 @@ export const tbelUtilsAutocompletes = new TbEditorCompleter({ description: 'Converts a list or array of bytes to a binary array.', args: [ { - name: 'value', + name: 'data', description: 'The bytes to convert', - type: 'array' + type: 'list | array' }, { - name: 'binLength', + name: 'length', description: 'The total length of the binary array (defaults to bytes.length * 8)', type: 'number', optional: true } ], return: { - description: 'The binary array (array of 0s and 1s)', + description: 'The binary array', type: 'array' } }, @@ -1187,34 +1187,34 @@ export const tbelUtilsAutocompletes = new TbEditorCompleter({ description: 'Converts a long integer to a binary array.', args: [ { - name: 'longValue', + name: 'value', description: 'The long integer to convert', type: 'number' }, { - name: 'binLength', + name: 'length', description: 'The length of the binary array (defaults to 64)', type: 'number', optional: true } ], return: { - description: 'The binary array (array of 0s and 1s)', + description: 'The binary array', type: 'array' } }, parseBinaryArrayToInt: { meta: 'function', - description: 'Converts a binary array to an integer.', + description: 'Converts a binary list or array to an integer.', args: [ { - name: 'value', - description: 'The binary array to convert (array of 0s and 1s)', - type: 'array' + name: 'data', + description: 'The binary list or array to convert', + type: 'list | array' }, { name: 'offset', - description: 'The starting index in the array (defaults to 0)', + description: 'The starting index in the binary list or array (defaults to 0)', type: 'number', optional: true }, From 3d282836e7f4bd1f047fc6702a99ae501d4acb69 Mon Sep 17 00:00:00 2001 From: Andrii Shvaika Date: Tue, 11 Mar 2025 16:46:25 +0200 Subject: [PATCH 278/345] CF Cluster mode fixes --- ...CalculatedFieldEntityMessageProcessor.java | 8 ++--- .../server/actors/tenant/TenantActor.java | 16 ++++++++- ...faultCalculatedFieldProcessingService.java | 4 ++- ...aultCalculatedFieldEntityProfileCache.java | 8 +++-- .../KafkaCalculatedFieldStateService.java | 10 ++++-- ...faultTbCalculatedFieldConsumerService.java | 8 +++-- .../queue/DefaultTbClusterService.java | 4 +-- .../DefaultTbRuleEngineConsumerService.java | 3 +- .../cf/CalculatedFieldPartitionChangeMsg.java | 2 -- .../queue/discovery/HashPartitionService.java | 34 +++++++++++++++---- .../queue/discovery/PartitionService.java | 2 -- .../server/queue/discovery/QueueKey.java | 3 -- .../discovery/event/PartitionChangeEvent.java | 4 +-- 13 files changed, 72 insertions(+), 34 deletions(-) diff --git a/application/src/main/java/org/thingsboard/server/actors/calculatedField/CalculatedFieldEntityMessageProcessor.java b/application/src/main/java/org/thingsboard/server/actors/calculatedField/CalculatedFieldEntityMessageProcessor.java index 4ff630bc59..31cb159229 100644 --- a/application/src/main/java/org/thingsboard/server/actors/calculatedField/CalculatedFieldEntityMessageProcessor.java +++ b/application/src/main/java/org/thingsboard/server/actors/calculatedField/CalculatedFieldEntityMessageProcessor.java @@ -24,6 +24,7 @@ import org.thingsboard.server.actors.ActorSystemContext; import org.thingsboard.server.actors.TbActorCtx; import org.thingsboard.server.actors.shared.AbstractContextAwareMsgProcessor; import org.thingsboard.server.common.data.AttributeScope; +import org.thingsboard.server.common.data.DataConstants; import org.thingsboard.server.common.data.StringUtils; import org.thingsboard.server.common.data.cf.configuration.Argument; import org.thingsboard.server.common.data.cf.configuration.ArgumentType; @@ -34,6 +35,7 @@ import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.common.data.kv.StringDataEntry; import org.thingsboard.server.common.data.msg.TbMsgType; import org.thingsboard.server.common.msg.cf.CalculatedFieldPartitionChangeMsg; +import org.thingsboard.server.common.msg.queue.ServiceType; import org.thingsboard.server.common.msg.queue.TbCallback; import org.thingsboard.server.gen.transport.TransportProtos.AttributeScopeProto; import org.thingsboard.server.gen.transport.TransportProtos.AttributeValueProto; @@ -74,7 +76,6 @@ public class CalculatedFieldEntityMessageProcessor extends AbstractContextAwareM final EntityId entityId; final CalculatedFieldProcessingService cfService; final CalculatedFieldStateService cfStateService; - final int partition; TbActorCtx ctx; Map states = new HashMap<>(); @@ -85,7 +86,6 @@ public class CalculatedFieldEntityMessageProcessor extends AbstractContextAwareM this.entityId = entityId; this.cfService = systemContext.getCalculatedFieldProcessingService(); this.cfStateService = systemContext.getCalculatedFieldStateService(); - this.partition = systemContext.getCalculatedFieldEntityProfileCache().getEntityIdPartition(tenantId, entityId); } void init(TbActorCtx ctx) { @@ -93,8 +93,8 @@ public class CalculatedFieldEntityMessageProcessor extends AbstractContextAwareM } public void process(CalculatedFieldPartitionChangeMsg msg) { - if (!msg.getPartitions()[partition]) { - log.info("[{}][{}] Stopping entity actor due to change partition event.", partition, entityId); + if (!systemContext.getPartitionService().resolve(ServiceType.TB_RULE_ENGINE, DataConstants.CF_QUEUE_NAME, tenantId, entityId).isMyPartition()) { + log.info("[{}] Stopping entity actor due to change partition event.", entityId); ctx.stop(ctx.getSelf()); } } diff --git a/application/src/main/java/org/thingsboard/server/actors/tenant/TenantActor.java b/application/src/main/java/org/thingsboard/server/actors/tenant/TenantActor.java index 0bd116f806..f9eec324fc 100644 --- a/application/src/main/java/org/thingsboard/server/actors/tenant/TenantActor.java +++ b/application/src/main/java/org/thingsboard/server/actors/tenant/TenantActor.java @@ -98,7 +98,7 @@ public class TenantActor extends RuleChainManagerActor { () -> new CalculatedFieldManagerActorCreator(systemContext, tenantId), () -> true); } catch (Exception e) { - log.info("Failed to init CF Actor.", e); + log.info("[{}] Failed to init CF Actor.", tenantId, e); } try { if (getApiUsageState().isReExecEnabled()) { @@ -259,11 +259,25 @@ public class TenantActor extends RuleChainManagerActor { ServiceType serviceType = msg.getServiceType(); if (ServiceType.TB_RULE_ENGINE.equals(serviceType)) { if (systemContext.getPartitionService().isManagedByCurrentService(tenantId)) { + if (cfActor == null) { + try { + //TODO: IM - extend API usage to have CF Exec Enabled? Not in 4.0; + cfActor = ctx.getOrCreateChildActor(new TbStringActorId("CFM|" + tenantId), + () -> DefaultActorService.CF_MANAGER_DISPATCHER_NAME, + () -> new CalculatedFieldManagerActorCreator(systemContext, tenantId), + () -> true); + } catch (Exception e) { + log.info("[{}] Failed to init CF Actor.", tenantId, e); + } + } if (!ruleChainsInitialized) { log.info("Tenant {} is now managed by this service, initializing rule chains", tenantId); initRuleChains(); } } else { + if (cfActor != null) { + ctx.stop(cfActor.getActorId()); + } if (ruleChainsInitialized) { log.info("Tenant {} is no longer managed by this service, stopping rule chains", tenantId); destroyRuleChains(); diff --git a/application/src/main/java/org/thingsboard/server/service/cf/DefaultCalculatedFieldProcessingService.java b/application/src/main/java/org/thingsboard/server/service/cf/DefaultCalculatedFieldProcessingService.java index 27bc0120c4..e9a6cb09aa 100644 --- a/application/src/main/java/org/thingsboard/server/service/cf/DefaultCalculatedFieldProcessingService.java +++ b/application/src/main/java/org/thingsboard/server/service/cf/DefaultCalculatedFieldProcessingService.java @@ -30,6 +30,7 @@ import org.thingsboard.common.util.ThingsBoardExecutors; import org.thingsboard.server.actors.calculatedField.CalculatedFieldTelemetryMsg; import org.thingsboard.server.actors.calculatedField.MultipleTbCallback; import org.thingsboard.server.cluster.TbClusterService; +import org.thingsboard.server.common.data.DataConstants; import org.thingsboard.server.common.data.EntityType; import org.thingsboard.server.common.data.StringUtils; import org.thingsboard.server.common.data.cf.configuration.Argument; @@ -51,6 +52,7 @@ import org.thingsboard.server.common.data.msg.TbMsgType; import org.thingsboard.server.common.data.tenant.profile.DefaultTenantProfileConfiguration; import org.thingsboard.server.common.msg.TbMsg; import org.thingsboard.server.common.msg.TbMsgMetaData; +import org.thingsboard.server.common.msg.queue.ServiceType; import org.thingsboard.server.common.msg.queue.TbCallback; import org.thingsboard.server.common.msg.queue.TopicPartitionInfo; import org.thingsboard.server.dao.attributes.AttributesService; @@ -200,7 +202,7 @@ public class DefaultCalculatedFieldProcessingService implements CalculatedFieldP if (broadcast) { broadcasts.add(link); } else { - TopicPartitionInfo tpi = partitionService.resolve(QueueKey.CF, link.entityId()); + TopicPartitionInfo tpi = partitionService.resolve(ServiceType.TB_RULE_ENGINE, DataConstants.CF_QUEUE_NAME, link.tenantId(), link.entityId()); unicasts.computeIfAbsent(tpi, k -> new ArrayList<>()).add(link); } } diff --git a/application/src/main/java/org/thingsboard/server/service/cf/cache/DefaultCalculatedFieldEntityProfileCache.java b/application/src/main/java/org/thingsboard/server/service/cf/cache/DefaultCalculatedFieldEntityProfileCache.java index 4cf62c01b3..2f5772ae50 100644 --- a/application/src/main/java/org/thingsboard/server/service/cf/cache/DefaultCalculatedFieldEntityProfileCache.java +++ b/application/src/main/java/org/thingsboard/server/service/cf/cache/DefaultCalculatedFieldEntityProfileCache.java @@ -18,8 +18,10 @@ package org.thingsboard.server.service.cf.cache; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.springframework.stereotype.Service; +import org.thingsboard.server.common.data.DataConstants; import org.thingsboard.server.common.data.id.EntityId; import org.thingsboard.server.common.data.id.TenantId; +import org.thingsboard.server.common.msg.queue.ServiceType; import org.thingsboard.server.common.msg.queue.TopicPartitionInfo; import org.thingsboard.server.queue.discovery.PartitionService; import org.thingsboard.server.queue.discovery.QueueKey; @@ -57,7 +59,7 @@ public class DefaultCalculatedFieldEntityProfileCache extends TbApplicationEvent @Override public void add(TenantId tenantId, EntityId profileId, EntityId entityId) { - var tpi = partitionService.resolve(QueueKey.CF, entityId); + var tpi = partitionService.resolve(ServiceType.TB_RULE_ENGINE, DataConstants.CF_QUEUE_NAME, tenantId, entityId); var partition = tpi.getPartition().orElse(UNKNOWN); tenantCache.computeIfAbsent(tenantId, id -> new TenantEntityProfileCache()) .add(profileId, entityId, partition, tpi.isMyPartition()); @@ -65,7 +67,7 @@ public class DefaultCalculatedFieldEntityProfileCache extends TbApplicationEvent @Override public void update(TenantId tenantId, EntityId oldProfileId, EntityId newProfileId, EntityId entityId) { - var tpi = partitionService.resolve(QueueKey.CF, entityId); + var tpi = partitionService.resolve(ServiceType.TB_RULE_ENGINE, DataConstants.CF_QUEUE_NAME, tenantId, entityId); var partition = tpi.getPartition().orElse(UNKNOWN); var cache = tenantCache.computeIfAbsent(tenantId, id -> new TenantEntityProfileCache()); //TODO: make this method atomic; @@ -86,7 +88,7 @@ public class DefaultCalculatedFieldEntityProfileCache extends TbApplicationEvent @Override public int getEntityIdPartition(TenantId tenantId, EntityId entityId) { - var tpi = partitionService.resolve(QueueKey.CF, entityId); + var tpi = partitionService.resolve(ServiceType.TB_RULE_ENGINE, DataConstants.CF_QUEUE_NAME, tenantId, entityId); return tpi.getPartition().orElse(UNKNOWN); } diff --git a/application/src/main/java/org/thingsboard/server/service/cf/ctx/state/KafkaCalculatedFieldStateService.java b/application/src/main/java/org/thingsboard/server/service/cf/ctx/state/KafkaCalculatedFieldStateService.java index e81fa4d1dc..557768e9c9 100644 --- a/application/src/main/java/org/thingsboard/server/service/cf/ctx/state/KafkaCalculatedFieldStateService.java +++ b/application/src/main/java/org/thingsboard/server/service/cf/ctx/state/KafkaCalculatedFieldStateService.java @@ -20,10 +20,12 @@ import lombok.extern.slf4j.Slf4j; import org.springframework.beans.factory.annotation.Value; import org.springframework.boot.autoconfigure.condition.ConditionalOnExpression; import org.springframework.stereotype.Service; +import org.thingsboard.server.common.data.DataConstants; import org.thingsboard.server.common.data.id.CalculatedFieldId; import org.thingsboard.server.common.data.id.EntityId; import org.thingsboard.server.common.data.id.EntityIdFactory; import org.thingsboard.server.common.data.id.TenantId; +import org.thingsboard.server.common.msg.queue.ServiceType; import org.thingsboard.server.common.msg.queue.TbCallback; import org.thingsboard.server.common.msg.queue.TopicPartitionInfo; import org.thingsboard.server.gen.transport.TransportProtos.CalculatedFieldStateProto; @@ -67,9 +69,11 @@ public class KafkaCalculatedFieldStateService extends AbstractCalculatedFieldSta @Override public void init(PartitionedQueueConsumerManager> eventConsumer) { super.init(eventConsumer); + + var queueKey = new QueueKey(ServiceType.TB_RULE_ENGINE, DataConstants.CF_STATES_QUEUE_NAME); this.stateConsumer = PartitionedQueueConsumerManager.>create() - .queueKey(QueueKey.CF_STATES) - .topic(partitionService.getTopic(QueueKey.CF_STATES)) + .queueKey(queueKey) + .topic(partitionService.getTopic(queueKey)) .pollInterval(pollInterval) .msgPackProcessor((msgs, consumer, config) -> { for (TbProtoQueueMsg msg : msgs) { @@ -101,7 +105,7 @@ public class KafkaCalculatedFieldStateService extends AbstractCalculatedFieldSta @Override protected void doPersist(CalculatedFieldEntityCtxId stateId, CalculatedFieldStateProto stateMsgProto, TbCallback callback) { - TopicPartitionInfo tpi = partitionService.resolve(QueueKey.CF_STATES, stateId.entityId()); + TopicPartitionInfo tpi = partitionService.resolve(ServiceType.TB_RULE_ENGINE, DataConstants.CF_STATES_QUEUE_NAME, stateId.tenantId(), stateId.entityId()); TbProtoQueueMsg msg = new TbProtoQueueMsg<>(stateId.entityId().getId(), stateMsgProto); if (stateMsgProto == null) { putStateId(msg.getHeaders(), stateId); diff --git a/application/src/main/java/org/thingsboard/server/service/queue/DefaultTbCalculatedFieldConsumerService.java b/application/src/main/java/org/thingsboard/server/service/queue/DefaultTbCalculatedFieldConsumerService.java index e76a2be9be..b5c755ad4b 100644 --- a/application/src/main/java/org/thingsboard/server/service/queue/DefaultTbCalculatedFieldConsumerService.java +++ b/application/src/main/java/org/thingsboard/server/service/queue/DefaultTbCalculatedFieldConsumerService.java @@ -27,6 +27,7 @@ import org.thingsboard.common.util.ThingsBoardExecutors; import org.thingsboard.server.actors.ActorSystemContext; import org.thingsboard.server.actors.calculatedField.CalculatedFieldLinkedTelemetryMsg; import org.thingsboard.server.actors.calculatedField.CalculatedFieldTelemetryMsg; +import org.thingsboard.server.common.data.DataConstants; import org.thingsboard.server.common.data.id.EntityIdFactory; import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.common.data.queue.QueueConfig; @@ -108,9 +109,10 @@ public class DefaultTbCalculatedFieldConsumerService extends AbstractConsumerSer public void init() { super.init("tb-cf"); + var queueKey = new QueueKey(ServiceType.TB_RULE_ENGINE, DataConstants.CF_QUEUE_NAME); this.eventConsumer = PartitionedQueueConsumerManager.>create() - .queueKey(QueueKey.CF) - .topic(partitionService.getTopic(QueueKey.CF)) + .queueKey(queueKey) + .topic(partitionService.getTopic(queueKey)) .pollInterval(pollInterval) .msgPackProcessor(this::processMsgs) .consumerCreator((config, partitionId) -> queueFactory.createToCalculatedFieldMsgConsumer()) @@ -140,7 +142,7 @@ public class DefaultTbCalculatedFieldConsumerService extends AbstractConsumerSer // Cleanup old entities after corresponding consumers are stopped. // Any periodic tasks need to check that the entity is still managed by the current server before processing. - actorContext.tell(new CalculatedFieldPartitionChangeMsg(partitionsToBooleanIndexArray(partitions))); + actorContext.tell(new CalculatedFieldPartitionChangeMsg()); } catch (Throwable t) { log.error("Failed to process partition change event: {}", event, t); } diff --git a/application/src/main/java/org/thingsboard/server/service/queue/DefaultTbClusterService.java b/application/src/main/java/org/thingsboard/server/service/queue/DefaultTbClusterService.java index 800578ace4..c7174469b0 100644 --- a/application/src/main/java/org/thingsboard/server/service/queue/DefaultTbClusterService.java +++ b/application/src/main/java/org/thingsboard/server/service/queue/DefaultTbClusterService.java @@ -358,7 +358,7 @@ public class DefaultTbClusterService implements TbClusterService { @Override public void pushMsgToCalculatedFields(TenantId tenantId, EntityId entityId, ToCalculatedFieldMsg msg, TbQueueCallback callback) { - TopicPartitionInfo tpi = partitionService.resolve(QueueKey.CF, entityId); + TopicPartitionInfo tpi = partitionService.resolve(ServiceType.TB_RULE_ENGINE, DataConstants.CF_QUEUE_NAME, tenantId, entityId); pushMsgToCalculatedFields(tpi, UUID.randomUUID(), msg, callback); } @@ -371,7 +371,7 @@ public class DefaultTbClusterService implements TbClusterService { @Override public void pushNotificationToCalculatedFields(TenantId tenantId, EntityId entityId, ToCalculatedFieldNotificationMsg msg, TbQueueCallback callback) { - TopicPartitionInfo tpi = partitionService.resolve(QueueKey.CF, entityId); + TopicPartitionInfo tpi = partitionService.resolve(ServiceType.TB_RULE_ENGINE, DataConstants.CF_QUEUE_NAME, tenantId, entityId); producerProvider.getCalculatedFieldsNotificationsMsgProducer().send(tpi, new TbProtoQueueMsg<>(UUID.randomUUID(), msg), callback); toRuleEngineNfs.incrementAndGet(); } diff --git a/application/src/main/java/org/thingsboard/server/service/queue/DefaultTbRuleEngineConsumerService.java b/application/src/main/java/org/thingsboard/server/service/queue/DefaultTbRuleEngineConsumerService.java index 6391beecf8..d522f11f7b 100644 --- a/application/src/main/java/org/thingsboard/server/service/queue/DefaultTbRuleEngineConsumerService.java +++ b/application/src/main/java/org/thingsboard/server/service/queue/DefaultTbRuleEngineConsumerService.java @@ -22,6 +22,7 @@ import org.springframework.context.event.EventListener; import org.springframework.scheduling.annotation.Scheduled; import org.springframework.stereotype.Service; import org.thingsboard.server.actors.ActorSystemContext; +import org.thingsboard.server.common.data.DataConstants; import org.thingsboard.server.common.data.EntityType; import org.thingsboard.server.common.data.id.QueueId; import org.thingsboard.server.common.data.id.TenantId; @@ -108,7 +109,7 @@ public class DefaultTbRuleEngineConsumerService extends AbstractConsumerService< @Override protected void onTbApplicationEvent(PartitionChangeEvent event) { event.getNewPartitions().forEach((queueKey, partitions) -> { - if (CollectionsUtil.isOneOf(queueKey, QueueKey.CF, QueueKey.CF_STATES)) { + if (DataConstants.CF_QUEUE_NAME.equals(queueKey.getQueueName()) || DataConstants.CF_STATES_QUEUE_NAME.equals(queueKey.getQueueName())) { return; } if (partitionService.isManagedByCurrentService(queueKey.getTenantId())) { diff --git a/common/message/src/main/java/org/thingsboard/server/common/msg/cf/CalculatedFieldPartitionChangeMsg.java b/common/message/src/main/java/org/thingsboard/server/common/msg/cf/CalculatedFieldPartitionChangeMsg.java index 38a4853219..44756013ca 100644 --- a/common/message/src/main/java/org/thingsboard/server/common/msg/cf/CalculatedFieldPartitionChangeMsg.java +++ b/common/message/src/main/java/org/thingsboard/server/common/msg/cf/CalculatedFieldPartitionChangeMsg.java @@ -26,8 +26,6 @@ import java.util.Set; @Data public class CalculatedFieldPartitionChangeMsg implements ToCalculatedFieldSystemMsg { - private final boolean[] partitions; - @Override public TenantId getTenantId() { return TenantId.SYS_TENANT_ID; diff --git a/common/queue/src/main/java/org/thingsboard/server/queue/discovery/HashPartitionService.java b/common/queue/src/main/java/org/thingsboard/server/queue/discovery/HashPartitionService.java index b42f8cc380..eaa33e99d9 100644 --- a/common/queue/src/main/java/org/thingsboard/server/queue/discovery/HashPartitionService.java +++ b/common/queue/src/main/java/org/thingsboard/server/queue/discovery/HashPartitionService.java @@ -24,6 +24,7 @@ import org.jetbrains.annotations.NotNull; import org.springframework.beans.factory.annotation.Value; import org.springframework.context.ApplicationEventPublisher; import org.springframework.stereotype.Service; +import org.thingsboard.server.common.data.DataConstants; import org.thingsboard.server.common.data.exception.TenantNotFoundException; import org.thingsboard.server.common.data.id.EntityId; import org.thingsboard.server.common.data.id.TenantId; @@ -122,11 +123,6 @@ public class HashPartitionService implements PartitionService { partitionSizesMap.put(coreKey, corePartitions); partitionTopicsMap.put(coreKey, coreTopic); - partitionSizesMap.put(QueueKey.CF, cfPartitions); - partitionTopicsMap.put(QueueKey.CF, cfEventTopic); - partitionSizesMap.put(QueueKey.CF_STATES, cfPartitions); - partitionTopicsMap.put(QueueKey.CF_STATES, cfStateTopic); - QueueKey vcKey = new QueueKey(ServiceType.TB_VC_EXECUTOR); partitionSizesMap.put(vcKey, vcPartitions); partitionTopicsMap.put(vcKey, vcTopic); @@ -165,6 +161,14 @@ public class HashPartitionService implements PartitionService { List queueRoutingInfoList = getQueueRoutingInfos(); queueRoutingInfoList.forEach(queue -> { QueueKey queueKey = new QueueKey(ServiceType.TB_RULE_ENGINE, queue); + if (DataConstants.MAIN_QUEUE_NAME.equals(queueKey.getQueueName())) { + QueueKey cfQueueKey = queueKey.withQueueName(DataConstants.CF_QUEUE_NAME); + partitionSizesMap.put(cfQueueKey, queue.getPartitions()); + partitionTopicsMap.put(cfQueueKey, cfEventTopic); + QueueKey cfQueueStatesKey = queueKey.withQueueName(DataConstants.CF_STATES_QUEUE_NAME); + partitionSizesMap.put(cfQueueStatesKey, queue.getPartitions()); + partitionTopicsMap.put(cfQueueStatesKey, cfStateTopic); + } partitionTopicsMap.put(queueKey, queue.getQueueTopic()); partitionSizesMap.put(queueKey, queue.getPartitions()); queueConfigs.put(queueKey, new QueueConfig(queue)); @@ -213,6 +217,14 @@ public class HashPartitionService implements PartitionService { QueueRoutingInfo queueRoutingInfo = new QueueRoutingInfo(queueUpdateMsg); TenantId tenantId = queueRoutingInfo.getTenantId(); QueueKey queueKey = new QueueKey(ServiceType.TB_RULE_ENGINE, queueRoutingInfo.getQueueName(), tenantId); + if (DataConstants.MAIN_QUEUE_NAME.equals(queueKey.getQueueName())) { + QueueKey cfQueueKey = queueKey.withQueueName(DataConstants.CF_QUEUE_NAME); + partitionSizesMap.put(cfQueueKey, queueRoutingInfo.getPartitions()); + partitionTopicsMap.put(cfQueueKey, cfEventTopic); + QueueKey cfQueueStatesKey = queueKey.withQueueName(DataConstants.CF_STATES_QUEUE_NAME); + partitionSizesMap.put(cfQueueStatesKey, queueRoutingInfo.getPartitions()); + partitionTopicsMap.put(cfQueueStatesKey, cfStateTopic); + } partitionTopicsMap.put(queueKey, queueRoutingInfo.getQueueTopic()); partitionSizesMap.put(queueKey, queueRoutingInfo.getPartitions()); queueConfigs.put(queueKey, new QueueConfig(queueRoutingInfo)); @@ -252,6 +264,15 @@ public class HashPartitionService implements PartitionService { partitionTopicsMap.remove(queueKey); partitionSizesMap.remove(queueKey); queueConfigs.remove(queueKey); + + if (DataConstants.MAIN_QUEUE_NAME.equals(queueKey.getQueueName())) { + QueueKey cfQueueKey = queueKey.withQueueName(DataConstants.CF_QUEUE_NAME); + partitionSizesMap.remove(cfQueueKey); + partitionTopicsMap.remove(cfQueueKey); + QueueKey cfQueueStatesKey = queueKey.withQueueName(DataConstants.CF_STATES_QUEUE_NAME); + partitionSizesMap.remove(cfQueueStatesKey); + partitionTopicsMap.remove(cfQueueStatesKey); + } } @Override @@ -336,8 +357,7 @@ public class HashPartitionService implements PartitionService { } } - @Override - public TopicPartitionInfo resolve(QueueKey queueKey, EntityId entityId) { + private TopicPartitionInfo resolve(QueueKey queueKey, EntityId entityId) { Integer partitionSize = partitionSizesMap.get(queueKey); if (partitionSize == null) { throw new IllegalStateException("Partitions info for queue " + queueKey + " is missing"); diff --git a/common/queue/src/main/java/org/thingsboard/server/queue/discovery/PartitionService.java b/common/queue/src/main/java/org/thingsboard/server/queue/discovery/PartitionService.java index 404b0258c0..c7d5bd7acf 100644 --- a/common/queue/src/main/java/org/thingsboard/server/queue/discovery/PartitionService.java +++ b/common/queue/src/main/java/org/thingsboard/server/queue/discovery/PartitionService.java @@ -37,8 +37,6 @@ public interface PartitionService { TopicPartitionInfo resolve(ServiceType serviceType, TenantId tenantId, EntityId entityId); - TopicPartitionInfo resolve(QueueKey queueKey, EntityId entityId); - List resolveAll(ServiceType serviceType, String queueName, TenantId tenantId, EntityId entityId); boolean isMyPartition(ServiceType serviceType, TenantId tenantId, EntityId entityId); diff --git a/common/queue/src/main/java/org/thingsboard/server/queue/discovery/QueueKey.java b/common/queue/src/main/java/org/thingsboard/server/queue/discovery/QueueKey.java index ca38959fdd..6720a9d71e 100644 --- a/common/queue/src/main/java/org/thingsboard/server/queue/discovery/QueueKey.java +++ b/common/queue/src/main/java/org/thingsboard/server/queue/discovery/QueueKey.java @@ -35,9 +35,6 @@ public class QueueKey { private final String queueName; private final TenantId tenantId; - public static final QueueKey CF = new QueueKey(ServiceType.TB_RULE_ENGINE).withQueueName(CF_QUEUE_NAME); - public static final QueueKey CF_STATES = new QueueKey(ServiceType.TB_RULE_ENGINE).withQueueName(CF_STATES_QUEUE_NAME); - public QueueKey(ServiceType type, Queue queue) { this.type = type; this.queueName = queue.getName(); diff --git a/common/queue/src/main/java/org/thingsboard/server/queue/discovery/event/PartitionChangeEvent.java b/common/queue/src/main/java/org/thingsboard/server/queue/discovery/event/PartitionChangeEvent.java index 597463300a..f165f60be7 100644 --- a/common/queue/src/main/java/org/thingsboard/server/queue/discovery/event/PartitionChangeEvent.java +++ b/common/queue/src/main/java/org/thingsboard/server/queue/discovery/event/PartitionChangeEvent.java @@ -64,10 +64,10 @@ public class PartitionChangeEvent extends TbApplicationEvent { } public Set getCfPartitions() { - return newPartitions.getOrDefault(QueueKey.CF, Collections.emptySet()); + return getPartitionsByServiceTypeAndQueueName(ServiceType.TB_RULE_ENGINE, DataConstants.CF_QUEUE_NAME); } - private Set getPartitionsByServiceTypeAndQueueName(ServiceType serviceType, String queueName) { + public Set getPartitionsByServiceTypeAndQueueName(ServiceType serviceType, String queueName) { return newPartitions.entrySet() .stream() .filter(entry -> serviceType.equals(entry.getKey().getType()) && queueName.equals(entry.getKey().getQueueName())) From 55f9f663126d7ff63e55b9b09e35b3ae0dc7469f Mon Sep 17 00:00:00 2001 From: Andrii Shvaika Date: Tue, 11 Mar 2025 16:55:58 +0200 Subject: [PATCH 279/345] CF parameters cleanup --- .../queue/DefaultTbCalculatedFieldConsumerService.java | 10 ---------- application/src/main/resources/thingsboard.yml | 5 +---- .../server/queue/discovery/HashPartitionService.java | 7 ------- .../server/queue/discovery/PartitionService.java | 2 -- 4 files changed, 1 insertion(+), 23 deletions(-) diff --git a/application/src/main/java/org/thingsboard/server/service/queue/DefaultTbCalculatedFieldConsumerService.java b/application/src/main/java/org/thingsboard/server/service/queue/DefaultTbCalculatedFieldConsumerService.java index b5c755ad4b..ad248cc3d4 100644 --- a/application/src/main/java/org/thingsboard/server/service/queue/DefaultTbCalculatedFieldConsumerService.java +++ b/application/src/main/java/org/thingsboard/server/service/queue/DefaultTbCalculatedFieldConsumerService.java @@ -80,8 +80,6 @@ public class DefaultTbCalculatedFieldConsumerService extends AbstractConsumerSer private long pollInterval; @Value("${queue.calculated_fields.pack_processing_timeout:60000}") private long packProcessingTimeout; - @Value("${queue.calculated_fields.pool_size:8}") - private int poolSize; private final TbRuleEngineQueueFactory queueFactory; private final CalculatedFieldStateService stateService; @@ -148,14 +146,6 @@ public class DefaultTbCalculatedFieldConsumerService extends AbstractConsumerSer } } - private boolean[] partitionsToBooleanIndexArray(Set partitions) { - boolean[] myPartitions = new boolean[partitionService.getTotalCalculatedFieldPartitions()]; - for (var tpi : partitions) { - tpi.getPartition().ifPresent(partition -> myPartitions[partition] = true); - } - return myPartitions; - } - private void processMsgs(List> msgs, TbQueueConsumer> consumer, QueueConfig config) throws Exception { List> orderedMsgList = msgs.stream().map(msg -> new IdMsgPair<>(UUID.randomUUID(), msg)).toList(); ConcurrentMap> pendingMap = orderedMsgList.stream().collect( diff --git a/application/src/main/resources/thingsboard.yml b/application/src/main/resources/thingsboard.yml index badd779350..3cc28ad702 100644 --- a/application/src/main/resources/thingsboard.yml +++ b/application/src/main/resources/thingsboard.yml @@ -1759,7 +1759,6 @@ queue: enabled: "${TB_EDQS_STATS_ENABLED:true}" # Statistics printing interval for EDQS print-interval-ms: "${TB_EDQS_STATS_PRINT_INTERVAL_MS:300000}" - vc: # Default topic name topic: "${TB_QUEUE_VC_TOPIC:tb_version_control}" @@ -1816,9 +1815,7 @@ queue: # For high-priority notifications that require minimum latency and processing time notifications_topic: "${TB_QUEUE_CF_NOTIFICATIONS_TOPIC:calculated_field.notifications}" # Interval in milliseconds to poll messages by CF (Rule Engine) microservices - poll_interval: "${TB_QUEUE_CF_POLL_INTERVAL_MS:25}" - # Amount of partitions used by CF microservices - partitions: "${TB_QUEUE_CF_PARTITIONS:10}" + poll_interval: "${TB_QUEUE_CF_POLL_INTERVAL_MS:1000}" # Timeout for processing a message pack by CF microservices pack_processing_timeout: "${TB_QUEUE_CF_PACK_PROCESSING_TIMEOUT_MS:60000}" # Thread pool size for processing of the incoming messages diff --git a/common/queue/src/main/java/org/thingsboard/server/queue/discovery/HashPartitionService.java b/common/queue/src/main/java/org/thingsboard/server/queue/discovery/HashPartitionService.java index eaa33e99d9..345b44e764 100644 --- a/common/queue/src/main/java/org/thingsboard/server/queue/discovery/HashPartitionService.java +++ b/common/queue/src/main/java/org/thingsboard/server/queue/discovery/HashPartitionService.java @@ -68,8 +68,6 @@ public class HashPartitionService implements PartitionService { private String cfEventTopic; @Value("${queue.calculated_fields.state_topic:tb_cf_state}") private String cfStateTopic; - @Value("${queue.calculated_fields.partitions:10}") - private Integer cfPartitions; @Value("${queue.vc.topic:tb_version_control}") private String vcTopic; @Value("${queue.vc.partitions:10}") @@ -572,11 +570,6 @@ public class HashPartitionService implements PartitionService { return list == null ? 0 : list.size(); } - @Override - public int getTotalCalculatedFieldPartitions() { - return cfPartitions; - } - private Map> getServiceKeyListMap(List services) { final Map> currentMap = new HashMap<>(); services.forEach(serviceInfo -> { diff --git a/common/queue/src/main/java/org/thingsboard/server/queue/discovery/PartitionService.java b/common/queue/src/main/java/org/thingsboard/server/queue/discovery/PartitionService.java index c7d5bd7acf..7abd68e25f 100644 --- a/common/queue/src/main/java/org/thingsboard/server/queue/discovery/PartitionService.java +++ b/common/queue/src/main/java/org/thingsboard/server/queue/discovery/PartitionService.java @@ -79,6 +79,4 @@ public interface PartitionService { int resolvePartitionIndex(UUID entityId, int partitions); - int getTotalCalculatedFieldPartitions(); - } From 8c7c54b554f5cdd9a38980eee84269065430d671 Mon Sep 17 00:00:00 2001 From: mpetrov Date: Tue, 11 Mar 2025 18:15:47 +0200 Subject: [PATCH 280/345] Calculated adjustments --- .../calculated-fields-table-config.ts | 11 ++++++++-- ...lated-field-arguments-table.component.html | 2 +- ...culated-field-arguments-table.component.ts | 15 ++++++++++++- ...calculated-field-debug-dialog.component.ts | 4 ++-- ...ulated-field-argument-panel.component.html | 5 +++-- ...lculated-field-argument-panel.component.ts | 18 +++++++++++---- .../vc/entity-version-create.component.ts | 3 ++- .../import-export/import-export.service.ts | 22 +------------------ .../shared/models/calculated-field.models.ts | 8 +++---- .../assets/locale/locale.constant-en_US.json | 15 +++++++------ 10 files changed, 58 insertions(+), 45 deletions(-) diff --git a/ui-ngx/src/app/modules/home/components/calculated-fields/calculated-fields-table-config.ts b/ui-ngx/src/app/modules/home/components/calculated-fields/calculated-fields-table-config.ts index cd9f0373db..60581249d2 100644 --- a/ui-ngx/src/app/modules/home/components/calculated-fields/calculated-fields-table-config.ts +++ b/ui-ngx/src/app/modules/home/components/calculated-fields/calculated-fields-table-config.ts @@ -258,8 +258,15 @@ export class CalculatedFieldsTableConfig extends EntityTableConfig this.getCalculatedFieldDialog(calculatedField, 'action.add')), + filter(Boolean), + switchMap(calculatedField => this.calculatedFieldsService.saveCalculatedField(calculatedField)), + filter(Boolean), + takeUntilDestroyed(this.destroyRef) + ) .subscribe(() => this.updateData()); } diff --git a/ui-ngx/src/app/modules/home/components/calculated-fields/components/arguments-table/calculated-field-arguments-table.component.html b/ui-ngx/src/app/modules/home/components/calculated-fields/components/arguments-table/calculated-field-arguments-table.component.html index 162bd3aa1e..abb34cb502 100644 --- a/ui-ngx/src/app/modules/home/components/calculated-fields/components/arguments-table/calculated-field-arguments-table.component.html +++ b/ui-ngx/src/app/modules/home/components/calculated-fields/components/arguments-table/calculated-field-arguments-table.component.html @@ -97,7 +97,7 @@ matTooltipPosition="above"> ([]); entityNameMap = new Map(); + entityNameErrorSet = new Set(); sortOrder = { direction: 'asc', property: '' }; dataSource = new CalculatedFieldArgumentDatasource(); @@ -168,6 +171,7 @@ export class CalculatedFieldArgumentsTableComponent implements ControlValueAcces buttonTitle: this.argumentsFormArray.at(index)?.value ? 'action.apply' : 'action.add', tenantId: this.tenantId, entityName: this.entityName, + entityHasError: this.entityNameErrorSet.has(argument.refEntityId?.id), usedArgumentNames: this.argumentsFormArray.value.map(({ argumentName }) => argumentName).filter(name => name !== argument.argumentName), }; this.popoverComponent = this.popoverService.displayPopover(trigger, this.renderer, @@ -198,6 +202,8 @@ export class CalculatedFieldArgumentsTableComponent implements ControlValueAcces if (this.calculatedFieldType === CalculatedFieldType.SIMPLE && this.argumentsFormArray.controls.some(control => control.value.refEntityKey.type === ArgumentType.Rolling)) { this.errorText = 'calculated-fields.hint.arguments-simple-with-rolling'; + } else if (this.entityNameErrorSet.size) { + this.errorText = 'calculated-fields.hint.arguments-entity-not-found'; } else if (!this.argumentsFormArray.controls.length) { this.errorText = 'calculated-fields.hint.arguments-empty'; } else { @@ -234,11 +240,18 @@ export class CalculatedFieldArgumentsTableComponent implements ControlValueAcces } private updateEntityNameMap(value: CalculatedFieldArgumentValue[]): void { + this.entityNameErrorSet.clear(); value.forEach(({ refEntityId = {}}) => { if (refEntityId.id && !this.entityNameMap.has(refEntityId.id)) { const { id, entityType } = refEntityId as EntityId; this.entityService.getEntity(entityType as EntityType, id, { ignoreLoading: true, ignoreErrors: true }) - .pipe(takeUntilDestroyed(this.destroyRef)) + .pipe( + catchError(() => { + this.entityNameErrorSet.add(id); + return NEVER; + }), + takeUntilDestroyed(this.destroyRef) + ) .subscribe(entity => this.entityNameMap.set(id, entity.name)); } }); diff --git a/ui-ngx/src/app/modules/home/components/calculated-fields/components/debug-dialog/calculated-field-debug-dialog.component.ts b/ui-ngx/src/app/modules/home/components/calculated-fields/components/debug-dialog/calculated-field-debug-dialog.component.ts index 8618a11990..880de7c281 100644 --- a/ui-ngx/src/app/modules/home/components/calculated-fields/components/debug-dialog/calculated-field-debug-dialog.component.ts +++ b/ui-ngx/src/app/modules/home/components/calculated-fields/components/debug-dialog/calculated-field-debug-dialog.component.ts @@ -20,7 +20,7 @@ import { Store } from '@ngrx/store'; import { AppState } from '@core/core.state'; import { Router } from '@angular/router'; import { DialogComponent } from '@shared/components/dialog.component'; -import { CalculatedFieldEventBody, DebugEventType, EventType } from '@shared/models/event.models'; +import { CalculatedFieldEventBody, DebugEventType, Event, EventType } from '@shared/models/event.models'; import { EventTableComponent } from '@home/components/event/event-table.component'; import { CalculatedFieldDebugDialogData, CalculatedFieldType } from '@shared/models/calculated-field.models'; @@ -46,7 +46,7 @@ export class CalculatedFieldDebugDialogComponent extends DialogComponent this.data.value.type === CalculatedFieldType.SCRIPT; + this.eventsTable.entitiesTable.cellActionDescriptors[0].isEnabled = (event => this.data.value.type === CalculatedFieldType.SCRIPT && !!(event as Event).body.arguments) } cancel(): void { diff --git a/ui-ngx/src/app/modules/home/components/calculated-fields/components/panel/calculated-field-argument-panel.component.html b/ui-ngx/src/app/modules/home/components/calculated-fields/components/panel/calculated-field-argument-panel.component.html index 79f2b6ebbc..15286af377 100644 --- a/ui-ngx/src/app/modules/home/components/calculated-fields/components/panel/calculated-field-argument-panel.component.html +++ b/ui-ngx/src/app/modules/home/components/calculated-fields/components/panel/calculated-field-argument-panel.component.html @@ -82,6 +82,7 @@
        {{ ArgumentEntityTypeParamsMap.get(entityType).title | translate }}
        } @else {
        -
        {{ 'calculated-fields.time-window' | translate }}
        +
        {{ 'calculated-fields.time-window' | translate }}
        @if (maxDataPointsPerRollingArg) {
        -
        {{ 'calculated-fields.limit' | translate }}
        +
        {{ 'calculated-fields.limit' | translate }}
        diff --git a/ui-ngx/src/app/modules/home/components/calculated-fields/components/panel/calculated-field-argument-panel.component.ts b/ui-ngx/src/app/modules/home/components/calculated-fields/components/panel/calculated-field-argument-panel.component.ts index 482851c59c..8aa61eb1a4 100644 --- a/ui-ngx/src/app/modules/home/components/calculated-fields/components/panel/calculated-field-argument-panel.component.ts +++ b/ui-ngx/src/app/modules/home/components/calculated-fields/components/panel/calculated-field-argument-panel.component.ts @@ -14,7 +14,7 @@ /// limitations under the License. /// -import { ChangeDetectorRef, Component, Input, OnInit, output } from '@angular/core'; +import { AfterViewInit, ChangeDetectorRef, Component, Input, OnInit, output, ViewChild } from '@angular/core'; import { TbPopoverComponent } from '@shared/components/popover.component'; import { FormBuilder, FormControl, FormGroup, ValidatorFn, Validators } from '@angular/forms'; import { charsWithNumRegex, oneSpaceInsideRegex } from '@shared/models/regex.constants'; @@ -41,13 +41,14 @@ import { MINUTE } from '@shared/models/time/time.models'; import { getCurrentAuthState } from '@core/auth/auth.selectors'; import { AppState } from '@core/core.state'; import { Store } from '@ngrx/store'; +import { EntityAutocompleteComponent } from '@shared/components/entity/entity-autocomplete.component'; @Component({ selector: 'tb-calculated-field-argument-panel', templateUrl: './calculated-field-argument-panel.component.html', styleUrls: ['./calculated-field-argument-panel.component.scss'] }) -export class CalculatedFieldArgumentPanelComponent implements OnInit { +export class CalculatedFieldArgumentPanelComponent implements OnInit, AfterViewInit { @Input() buttonTitle: string; @Input() index: number; @@ -55,9 +56,12 @@ export class CalculatedFieldArgumentPanelComponent implements OnInit { @Input() entityId: EntityId; @Input() tenantId: string; @Input() entityName: string; + @Input() entityHasError: boolean; @Input() calculatedFieldType: CalculatedFieldType; @Input() usedArgumentNames: string[]; + @ViewChild('entityAutocomplete') entityAutocomplete: EntityAutocompleteComponent; + argumentsDataApplied = output<{ value: CalculatedFieldArgumentValue, index: number }>(); readonly maxDataPointsPerRollingArg = getCurrentAuthState(this.store).maxDataPointsPerRollingArg; @@ -75,8 +79,8 @@ export class CalculatedFieldArgumentPanelComponent implements OnInit { scope: [{ value: AttributeScope.SERVER_SCOPE, disabled: true }, [Validators.required]], }), defaultValue: ['', [Validators.pattern(oneSpaceInsideRegex)]], - limit: [{ value: this.defaultLimit, disabled: !this.maxDataPointsPerRollingArg }], - timeWindow: [MINUTE * 15], + limit: [{ value: this.defaultLimit, disabled: !this.maxDataPointsPerRollingArg }, [Validators.required, Validators.min(1), Validators.max(this.maxDataPointsPerRollingArg)]], + timeWindow: [MINUTE * 15, [Validators.required]], }); argumentTypes: ArgumentType[]; @@ -136,6 +140,12 @@ export class CalculatedFieldArgumentPanelComponent implements OnInit { .filter(type => type !== ArgumentType.Rolling || this.calculatedFieldType === CalculatedFieldType.SCRIPT); } + ngAfterViewInit(): void { + if (this.entityHasError) { + this.entityAutocomplete.selectEntityFormGroup.get('entity').markAsTouched(); + } + } + saveArgument(): void { const { refEntityId, ...restConfig } = this.argumentFormGroup.value; const value = (refEntityId.entityType === ArgumentEntityType.Current ? restConfig : { refEntityId, ...restConfig }) as CalculatedFieldArgumentValue; diff --git a/ui-ngx/src/app/modules/home/components/vc/entity-version-create.component.ts b/ui-ngx/src/app/modules/home/components/vc/entity-version-create.component.ts index 0c4b696ad1..2042b7fdb6 100644 --- a/ui-ngx/src/app/modules/home/components/vc/entity-version-create.component.ts +++ b/ui-ngx/src/app/modules/home/components/vc/entity-version-create.component.ts @@ -89,7 +89,8 @@ export class EntityVersionCreateComponent extends PageComponent implements OnIni {entityName: this.entityName}), [Validators.required, Validators.pattern(/(?:.|\s)*\S(&:.|\s)*/)]], saveRelations: [false, []], saveAttributes: [true, []], - saveCredentials: [true, []] + saveCredentials: [true, []], + saveCalculatedFields: [true, []] }); } diff --git a/ui-ngx/src/app/shared/import-export/import-export.service.ts b/ui-ngx/src/app/shared/import-export/import-export.service.ts index 6109275d6d..2ad60f0139 100644 --- a/ui-ngx/src/app/shared/import-export/import-export.service.ts +++ b/ui-ngx/src/app/shared/import-export/import-export.service.ts @@ -187,18 +187,8 @@ export class ImportExportService { }); } - public importCalculatedField(entityId: EntityId): Observable { + public openCalculatedFieldImportDialog(): Observable { return this.openImportDialog('calculated-fields.import', 'calculated-fields.file').pipe( - mergeMap((calculatedField: CalculatedField) => { - if (!this.validateImportedCalculatedField({ entityId, ...calculatedField })) { - this.store.dispatch(new ActionNotificationShow( - {message: this.translate.instant('calculated-fields.invalid-file-error'), - type: 'error'})); - throw new Error('Invalid calculated field file'); - } else { - return this.calculatedFieldsService.saveCalculatedField(this.prepareImport({ entityId, ...calculatedField })); - } - }), catchError(() => of(null)), ); } @@ -989,16 +979,6 @@ export class ImportExportService { } } - private validateImportedCalculatedField(calculatedField: CalculatedField): boolean { - const { name, configuration, entityId } = calculatedField; - return isNotEmptyStr(name) - && isDefined(configuration) - && isDefined(entityId?.id) - && !!Object.keys(configuration.arguments).length - && isDefined(configuration.expression) - && isDefined(configuration.output) - } - private validateImportedImage(image: ImageExportData): boolean { return !(!isNotEmptyStr(image.data) || !isNotEmptyStr(image.title) diff --git a/ui-ngx/src/app/shared/models/calculated-field.models.ts b/ui-ngx/src/app/shared/models/calculated-field.models.ts index c898342c68..7e553fde5f 100644 --- a/ui-ngx/src/app/shared/models/calculated-field.models.ts +++ b/ui-ngx/src/app/shared/models/calculated-field.models.ts @@ -127,7 +127,7 @@ export const ArgumentTypeTranslations = new Map( export interface CalculatedFieldArgument { refEntityKey: RefEntityKey; defaultValue?: string; - refEntityId?: RefEntityKey; + refEntityId?: RefEntityId; limit?: number; timeWindow?: number; } @@ -138,7 +138,7 @@ export interface RefEntityKey { scope?: AttributeScope; } -export interface RefEntityKey { +export interface RefEntityId { entityType: ArgumentEntityType; id: string; } @@ -563,12 +563,12 @@ export const getCalculatedFieldArgumentsHighlights = ( regex: `\\b${key}\\b`, next: argumentsObj[key].refEntityKey.type === ArgumentType.Rolling ? 'calculatedFieldRollingArgumentValue' - : 'start' + : 'no_regex' })); const calculatedFieldCtxArgumentsHighlightRules = { calculatedFieldCtxArgs: [ dotOperatorHighlightRule, - ...calculatedFieldArgumentsKeys.map(argumentRule => argumentRule.next === 'start' ? {...argumentRule, next: 'calculatedFieldSingleArgumentValue' } : argumentRule), + ...calculatedFieldArgumentsKeys.map(argumentRule => argumentRule.next === 'no_regex' ? {...argumentRule, next: 'calculatedFieldSingleArgumentValue' } : argumentRule), endGroupHighlightRule ] }; 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 883acdbf21..295a8ac35e 100644 --- a/ui-ngx/src/assets/locale/locale.constant-en_US.json +++ b/ui-ngx/src/assets/locale/locale.constant-en_US.json @@ -1074,7 +1074,8 @@ "argument-type-required": "Argument type is required.", "max-args": "Maximum number of arguments reached.", "decimals-range": "Decimals by default should be a number between 0 and 15.", - "expression": "Default expression demonstrates how to transform a temperature from Fahrenheit to Celsius." + "expression": "Default expression demonstrates how to transform a temperature from Fahrenheit to Celsius.", + "arguments-entity-not-found": "Argument target entity not found." } }, "confirm-on-exit": { @@ -5570,12 +5571,12 @@ "max-calculated-fields": "Calculated fields per entity maximum number", "max-calculated-fields-range": "Calculated fields per entity maximum number can't be negative", "max-calculated-fields-required": "Calculated fields per entity maximum number is required", - "max-data-points-per-rolling-arg": "Maximum data points number in rolling arguments", - "max-data-points-per-rolling-arg-range": "Maximum data points number in rolling arguments can't be negative", - "max-data-points-per-rolling-arg-required": "Maximum data points number in rolling arguments is required", - "max-arguments-per-cf": "Arguments per calculated field maximum number", - "max-arguments-per-cf-range": "Arguments per calculated field maximum number can't be negative", - "max-arguments-per-cf-required": "Arguments per calculated field maximum number is required", + "max-data-points-per-rolling-arg": "Max data points number in rolling arguments", + "max-data-points-per-rolling-arg-range": "Max data points number in rolling arguments can't be negative", + "max-data-points-per-rolling-arg-required": "Max data points number in rolling arguments is required", + "max-arguments-per-cf": "Arguments per calculated field max number", + "max-arguments-per-cf-range": "Arguments per calculated field max number can't be negative", + "max-arguments-per-cf-required": "Arguments per calculated field max number is required", "max-state-size": "State maximum size in KB", "max-state-size-range": "State maximum size in KB can't be negative", "max-state-size-required": "State maximum size in KB is required", From bdec59af0e9f91b3fd5217d63270041cba4c523b Mon Sep 17 00:00:00 2001 From: mpetrov Date: Tue, 11 Mar 2025 18:36:07 +0200 Subject: [PATCH 281/345] Resolved review comments --- .../entity-debug-settings-button.component.ts | 3 +- .../entity-debug-settings-panel.component.ts | 3 +- .../debug/entity-debug-settings.model.ts | 33 +++++++++++++++++++ .../debug/entity-debug-settings.service.ts | 3 +- .../shared/models/calculated-field.models.ts | 2 +- ui-ngx/src/app/shared/models/entity.models.ts | 16 --------- 6 files changed, 40 insertions(+), 20 deletions(-) create mode 100644 ui-ngx/src/app/modules/home/components/entity/debug/entity-debug-settings.model.ts diff --git a/ui-ngx/src/app/modules/home/components/entity/debug/entity-debug-settings-button.component.ts b/ui-ngx/src/app/modules/home/components/entity/debug/entity-debug-settings-button.component.ts index 99a08494fc..85a90aef63 100644 --- a/ui-ngx/src/app/modules/home/components/entity/debug/entity-debug-settings-button.component.ts +++ b/ui-ngx/src/app/modules/home/components/entity/debug/entity-debug-settings-button.component.ts @@ -28,13 +28,14 @@ import { MatButton } from '@angular/material/button'; import { takeUntilDestroyed } from '@angular/core/rxjs-interop'; import { BehaviorSubject, of, shareReplay, timer } from 'rxjs'; import { SECOND, MINUTE } from '@shared/models/time/time.models'; -import { AdditionalDebugActionConfig, EntityDebugSettings } from '@shared/models/entity.models'; +import { EntityDebugSettings } from '@shared/models/entity.models'; import { map, switchMap, takeWhile } from 'rxjs/operators'; import { getCurrentAuthState } from '@core/auth/auth.selectors'; import { AppState } from '@core/core.state'; import { Store } from '@ngrx/store'; import { ControlValueAccessor, FormBuilder, NG_VALUE_ACCESSOR } from '@angular/forms'; import { EntityDebugSettingsService } from '@home/components/entity/debug/entity-debug-settings.service'; +import { AdditionalDebugActionConfig } from '@home/components/entity/debug/entity-debug-settings.model'; @Component({ selector: 'tb-entity-debug-settings-button', diff --git a/ui-ngx/src/app/modules/home/components/entity/debug/entity-debug-settings-panel.component.ts b/ui-ngx/src/app/modules/home/components/entity/debug/entity-debug-settings-panel.component.ts index 5fd3613296..1f95a93084 100644 --- a/ui-ngx/src/app/modules/home/components/entity/debug/entity-debug-settings-panel.component.ts +++ b/ui-ngx/src/app/modules/home/components/entity/debug/entity-debug-settings-panel.component.ts @@ -32,8 +32,9 @@ import { SECOND } from '@shared/models/time/time.models'; import { DurationLeftPipe } from '@shared/pipe/duration-left.pipe'; import { of, shareReplay, timer } from 'rxjs'; import { takeUntilDestroyed } from '@angular/core/rxjs-interop'; -import { AdditionalDebugActionConfig, EntityDebugSettings } from '@shared/models/entity.models'; +import { EntityDebugSettings } from '@shared/models/entity.models'; import { distinctUntilChanged, map, startWith, switchMap, takeWhile } from 'rxjs/operators'; +import { AdditionalDebugActionConfig } from '@home/components/entity/debug/entity-debug-settings.model'; @Component({ selector: 'tb-entity-debug-settings-panel', diff --git a/ui-ngx/src/app/modules/home/components/entity/debug/entity-debug-settings.model.ts b/ui-ngx/src/app/modules/home/components/entity/debug/entity-debug-settings.model.ts new file mode 100644 index 0000000000..6560580502 --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/entity/debug/entity-debug-settings.model.ts @@ -0,0 +1,33 @@ +/// +/// 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 { EntityDebugSettings } from '@shared/models/entity.models'; + +export interface AdditionalDebugActionConfig void> { + action: Action; + title: string; +} + +export interface EntityDebugSettingPanelConfig { + debugSettings: EntityDebugSettings; + debugConfig: { + maxDebugModeDuration: number; + debugLimitsConfiguration: string; + entityLabel?: string; + additionalActionConfig?: AdditionalDebugActionConfig; + } + onSettingsAppliedFn: (settings: EntityDebugSettings) => void; +} diff --git a/ui-ngx/src/app/modules/home/components/entity/debug/entity-debug-settings.service.ts b/ui-ngx/src/app/modules/home/components/entity/debug/entity-debug-settings.service.ts index c53bc5b777..873d8f0f3f 100644 --- a/ui-ngx/src/app/modules/home/components/entity/debug/entity-debug-settings.service.ts +++ b/ui-ngx/src/app/modules/home/components/entity/debug/entity-debug-settings.service.ts @@ -16,10 +16,11 @@ import { Injectable, Optional, Renderer2, ViewContainerRef } from '@angular/core'; import { EntityDebugSettingsPanelComponent } from '@home/components/entity/debug/entity-debug-settings-panel.component'; -import { EntityDebugSettingPanelConfig, EntityDebugSettings } from '@shared/models/entity.models'; +import { EntityDebugSettings } from '@shared/models/entity.models'; import { TbPopoverService } from '@shared/components/popover.service'; import { TranslateService } from '@ngx-translate/core'; import { DurationLeftPipe } from '@shared/pipe/duration-left.pipe'; +import { EntityDebugSettingPanelConfig } from '@home/components/entity/debug/entity-debug-settings.model'; @Injectable() export class EntityDebugSettingsService { diff --git a/ui-ngx/src/app/shared/models/calculated-field.models.ts b/ui-ngx/src/app/shared/models/calculated-field.models.ts index 7083ce7ae0..c7b37e46f5 100644 --- a/ui-ngx/src/app/shared/models/calculated-field.models.ts +++ b/ui-ngx/src/app/shared/models/calculated-field.models.ts @@ -15,7 +15,6 @@ /// import { - AdditionalDebugActionConfig, HasEntityDebugSettings, HasTenantId, HasVersion @@ -34,6 +33,7 @@ import { dotOperatorHighlightRule, endGroupHighlightRule } from '@shared/models/ace/ace.models'; +import { AdditionalDebugActionConfig } from '@home/components/entity/debug/entity-debug-settings.model'; export interface CalculatedField extends Omit, 'label'>, HasVersion, HasEntityDebugSettings, HasTenantId, ExportableEntity { configuration: CalculatedFieldConfiguration; diff --git a/ui-ngx/src/app/shared/models/entity.models.ts b/ui-ngx/src/app/shared/models/entity.models.ts index 87536fdc54..5aa526b583 100644 --- a/ui-ngx/src/app/shared/models/entity.models.ts +++ b/ui-ngx/src/app/shared/models/entity.models.ts @@ -203,25 +203,9 @@ export interface EntityDebugSettings { allEnabledUntil?: number; } -export interface EntityDebugSettingPanelConfig { - debugSettings: EntityDebugSettings; - debugConfig: { - maxDebugModeDuration: number; - debugLimitsConfiguration: string; - entityLabel?: string; - additionalActionConfig?: AdditionalDebugActionConfig; - } - onSettingsAppliedFn: (settings: EntityDebugSettings) => void; -} - export interface EntityTestScriptResult { output: string; error: string; } -export interface AdditionalDebugActionConfig void> { - action: Action; - title: string; -} - export type VersionedEntity = EntityInfoData & HasVersion | RuleChainMetaData; From 9570abe2ce8364ece33cec155ca6e1c16aefd977 Mon Sep 17 00:00:00 2001 From: mpetrov Date: Tue, 11 Mar 2025 18:55:07 +0200 Subject: [PATCH 282/345] Naming improvement --- .../action/mobile-action-editor.component.html | 2 +- .../action/mobile-action-editor.component.ts | 10 +++++----- .../common/action/mobile-action-editor.models.ts | 15 ++++++++------- .../home/components/widget/widget.component.ts | 4 ++-- ui-ngx/src/app/shared/models/widget.models.ts | 8 ++++---- .../src/assets/locale/locale.constant-en_US.json | 2 +- 6 files changed, 21 insertions(+), 20 deletions(-) diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/action/mobile-action-editor.component.html b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/action/mobile-action-editor.component.html index c21595eea9..c3194676cf 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/action/mobile-action-editor.component.html +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/action/mobile-action-editor.component.html @@ -36,7 +36,7 @@
        - + processQrCodeFunction; export const getDefaultProcessLocationFunction = () => processLocationFunction; -export const getDefaultProvisioningSuccessFunction = () => provisioningSuccessFunction; +export const getDefaultProvisionSuccessFunction = () => provisionSuccessFunction; export const getDefaultGetLocationFunction = () => getLocationFunctionTemplate; @@ -285,8 +286,8 @@ export const getDefaultHandleEmptyResultFunction = (type: WidgetMobileActionType case WidgetMobileActionType.takeScreenshot: message = 'Take screenshot action was cancelled!'; break; - case WidgetMobileActionType.provisionDevice: - message = 'Provisioning device was not invoked!'; + case WidgetMobileActionType.deviceProvision: + message = 'Device provision was not invoked!'; break; } return handleEmptyResultFunctionTemplate.replace('--MESSAGE--', message); @@ -319,7 +320,7 @@ export const getDefaultHandleErrorFunction = (type: WidgetMobileActionType): TbF case WidgetMobileActionType.takeScreenshot: title = 'Failed to take screenshot'; break; - case WidgetMobileActionType.provisionDevice: + case WidgetMobileActionType.deviceProvision: title = 'Failed to make device provision'; break; } diff --git a/ui-ngx/src/app/modules/home/components/widget/widget.component.ts b/ui-ngx/src/app/modules/home/components/widget/widget.component.ts index ca11c2f4e0..3d20bcbd35 100644 --- a/ui-ngx/src/app/modules/home/components/widget/widget.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/widget.component.ts @@ -1203,7 +1203,7 @@ export class WidgetComponent extends PageComponent implements OnInit, OnChanges, case WidgetMobileActionType.scanQrCode: case WidgetMobileActionType.getLocation: case WidgetMobileActionType.takeScreenshot: - case WidgetMobileActionType.provisionDevice: + case WidgetMobileActionType.deviceProvision: argsObservable = of([]); break; case WidgetMobileActionType.mapDirection: @@ -1293,7 +1293,7 @@ export class WidgetComponent extends PageComponent implements OnInit, OnChanges, ); } break; - case WidgetMobileActionType.provisionDevice: + case WidgetMobileActionType.deviceProvision: const deviceName = actionResult.deviceName; if (isNotEmptyTbFunction(mobileAction.handleProvisionSuccessFunction)) { compileTbFunction(this.http, mobileAction.handleProvisionSuccessFunction, 'deviceName', '$event', 'widgetContext', 'entityId', diff --git a/ui-ngx/src/app/shared/models/widget.models.ts b/ui-ngx/src/app/shared/models/widget.models.ts index 132c82cfd1..4461758c85 100644 --- a/ui-ngx/src/app/shared/models/widget.models.ts +++ b/ui-ngx/src/app/shared/models/widget.models.ts @@ -588,7 +588,7 @@ export enum WidgetMobileActionType { makePhoneCall = 'makePhoneCall', getLocation = 'getLocation', takeScreenshot = 'takeScreenshot', - provisionDevice = 'provisionDevice', + deviceProvision = 'deviceProvision', } export const widgetActionTypes = Object.keys(WidgetActionType) as WidgetActionType[]; @@ -616,7 +616,7 @@ export const widgetMobileActionTypeTranslationMap = new Map { result?: T; 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 4ccb6ed8f5..e71b2f6930 100644 --- a/ui-ngx/src/assets/locale/locale.constant-en_US.json +++ b/ui-ngx/src/assets/locale/locale.constant-en_US.json @@ -6536,7 +6536,7 @@ "URL": "URL", "url-required": "URL is required.", "mobile": { - "provision-device": "Provision device", + "device-provision": "Device provision", "action-type": "Mobile action type", "select-action-type": "Select mobile action type", "action-type-required": "Mobile action type is required", From 12d07f1cc8220a09a67941e4ebee5ab8c3a6033d Mon Sep 17 00:00:00 2001 From: Igor Kulikov Date: Tue, 11 Mar 2025 19:23:56 +0200 Subject: [PATCH 283/345] UI: Map - introduce reference vector layers. --- ui-ngx/package.json | 2 + .../widget/lib/maps/leaflet/leaflet-tb.ts | 21 +- .../components/widget/lib/maps/map-layer.ts | 116 +- .../home/components/widget/lib/maps/map.scss | 8 + .../common/map/map-layer-row.component.ts | 3 +- .../map-layer-settings-panel.component.html | 11 + .../map/map-layer-settings-panel.component.ts | 9 +- .../shared/models/widget/maps/map.models.ts | 22 +- .../assets/locale/locale.constant-en_US.json | 6 + .../openstreetmap_hybrid_reference_style.json | 25800 +++++++ .../world_edition_hybrid_reference_style.json | 57837 ++++++++++++++++ ui-ngx/src/typings/leaflet-extend-tb.d.ts | 1 + ui-ngx/yarn.lock | 228 +- 13 files changed, 84033 insertions(+), 31 deletions(-) create mode 100644 ui-ngx/src/assets/map/openstreetmap_hybrid_reference_style.json create mode 100644 ui-ngx/src/assets/map/world_edition_hybrid_reference_style.json diff --git a/ui-ngx/package.json b/ui-ngx/package.json index b2e2dc18c3..1536858de8 100644 --- a/ui-ngx/package.json +++ b/ui-ngx/package.json @@ -28,6 +28,7 @@ "@flowjs/ngx-flow": "18.0.1", "@geoman-io/leaflet-geoman-free": "2.17.0", "@iplab/ngx-color-picker": "^18.0.1", + "@maplibre/maplibre-gl-leaflet": "^0.0.22", "@mat-datetimepicker/core": "~14.0.0", "@mdi/svg": "^7.4.47", "@messageformat/core": "^3.4.0", @@ -64,6 +65,7 @@ "leaflet.gridlayer.googlemutant": "0.14.1", "leaflet.markercluster": "1.5.3", "libphonenumber-js": "^1.11.15", + "maplibre-gl": "^4.7.1", "marked": "~12.0.2", "moment": "^2.30.1", "moment-timezone": "^0.5.45", diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/maps/leaflet/leaflet-tb.ts b/ui-ngx/src/app/modules/home/components/widget/lib/maps/leaflet/leaflet-tb.ts index 06d49d0343..df02c46482 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/maps/leaflet/leaflet-tb.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/maps/leaflet/leaflet-tb.ts @@ -17,6 +17,7 @@ import L, { TB } from 'leaflet'; import { guid, isNotEmptyStr } from '@core/utils'; import 'leaflet-providers'; +import '@maplibre/maplibre-gl-leaflet'; import '@geoman-io/leaflet-geoman-free'; import 'leaflet.markercluster'; import { MatIconRegistry } from '@angular/material/icon'; @@ -211,15 +212,19 @@ class LayersControl extends SidebarPaneControl { input.on('click', (e: JQuery.MouseEventBase) => { e.stopPropagation(); - layers.forEach((other) => { - if (other.layer === layerData.layer) { - map.addLayer(other.layer); - map.attributionControl.setPrefix(other.attributionPrefix); - } else { - map.removeLayer(other.layer); + if (!map.hasLayer(layerData.layer)) { + map.addLayer(layerData.layer); + map.attributionControl.setPrefix(layerData.attributionPrefix); + if (layerData.onAdd) { + layerData.onAdd(); } - }); - map.fire('baselayerchange', { layer: layerData.layer }); + layers.forEach((other) => { + if (other.layer !== layerData.layer) { + map.removeLayer(other.layer); + } + }); + map.fire('baselayerchange', { layer: layerData.layer }); + } }); item.on('dblclick', (e) => { diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/maps/map-layer.ts b/ui-ngx/src/app/modules/home/components/widget/lib/maps/map-layer.ts index 304100828b..87ca69034f 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/maps/map-layer.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/maps/map-layer.ts @@ -26,17 +26,34 @@ import { HereMapLayerSettings, MapLayerSettings, MapProvider, - OpenStreetMapLayerSettings, + OpenStreetMapLayerSettings, ReferenceLayerType, TencentMapLayerSettings } from '@shared/models/widget/maps/map.models'; import { WidgetContext } from '@home/models/widget-component.models'; import { DeepPartial } from '@shared/models/common'; import { mergeDeep } from '@core/utils'; -import { Observable, of, switchMap } from 'rxjs'; +import { Observable, of, shareReplay, switchMap } from 'rxjs'; import { CustomTranslatePipe } from '@shared/pipe/custom-translate.pipe'; import L from 'leaflet'; import { catchError, map } from 'rxjs/operators'; import { ResourcesService } from '@core/services/resources.service'; +import { StyleSpecification, VectorSourceSpecification } from '@maplibre/maplibre-gl-style-spec'; +import { ResourceType } from 'maplibre-gl'; + +const referenceLayerStyleUrlMap = new Map( + [ + [ReferenceLayerType.openstreetmap_hybrid, '/assets/map/openstreetmap_hybrid_reference_style.json'], + [ReferenceLayerType.world_edition_hybrid, '/assets/map/world_edition_hybrid_reference_style.json'] + ] +); + +const referenceLayerCache = new Map>(); + +interface TbMapLayerData { + layer: L.Layer; + attribution: boolean; + onAdd?: () => void; +} export abstract class TbMapLayer { @@ -65,19 +82,23 @@ export abstract class TbMapLayer { } public loadLayer(theMap: L.Map): Observable { - return this.createLayer().pipe( - switchMap((layer) => { - if (layer) { - return this.createLayer().pipe( - map((mini) => { - if (mini) { - const attribution = layer.getAttribution(); - const attributionPrefix = attribution ? theMap.attributionControl.options.prefix as string : null; + return this.generateLayer().pipe( + switchMap((layerData) => { + if (layerData) { + return this.generateLayer().pipe( + map((miniLayerData) => { + if (miniLayerData) { + const attributionPrefix = layerData.attribution ? theMap.attributionControl.options.prefix as string : null; return { title: this.title(), attributionPrefix: attributionPrefix, - layer, - mini + layer: layerData.layer, + mini: miniLayerData.layer, + onAdd: () => { + if (layerData.onAdd) { + layerData.onAdd(); + } + } }; } else { return null; @@ -91,6 +112,77 @@ export abstract class TbMapLayer { ); } + private generateLayer(): Observable { + return this.createLayer().pipe( + switchMap((baseLayer) => { + if (baseLayer) { + if (this.settings.referenceLayer) { + return this.loadReferenceLayer(this.settings.referenceLayer).pipe( + map((referenceLayer) => { + if (referenceLayer) { + const layer = L.featureGroup(); + baseLayer.addTo(layer); + referenceLayer.addTo(layer); + return { + layer, + attribution: !!baseLayer.getAttribution() || !!referenceLayer.getAttribution(), + onAdd: () => { + (referenceLayer as any)._update(); + } + }; + } else { + return { + layer: baseLayer, + attribution: !!baseLayer.getAttribution() + }; + } + })); + } else { + return of({ + layer: baseLayer, + attribution: !!baseLayer.getAttribution() + }); + } + } else { + return of(null); + } + } + )); + } + + private loadReferenceLayer(referenceLayer: ReferenceLayerType): Observable { + let spec$ = referenceLayerCache.get(referenceLayer); + if (!spec$) { + const styleUrl = referenceLayerStyleUrlMap.get(referenceLayer); + spec$ = this.ctx.http.get(styleUrl).pipe( + shareReplay({ + bufferSize: 1, + refCount: true + }) + ); + referenceLayerCache.set(referenceLayer, spec$); + } + return spec$.pipe( + map(spec => { + const sourceSpec = (spec.sources['esri'] as VectorSourceSpecification); + const tileUrl = sourceSpec.url; + const attribution = sourceSpec.attribution; + const transformRequest = (url: string, resourceType: ResourceType) => { + if (resourceType === 'Tile') { + url = tileUrl + '/' + url; + } + return {url} + } + const gl = L.maplibreGL({ + style: spec, + transformRequest + }); + gl.options.attribution = attribution; + return gl; + }) + ); + } + private title(): string { const customTranslate = this.ctx.$injector.get(CustomTranslatePipe); if (this.settings.label) { diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/maps/map.scss b/ui-ngx/src/app/modules/home/components/widget/lib/maps/map.scss index 592610f8b2..985bdc17f5 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/maps/map.scss +++ b/ui-ngx/src/app/modules/home/components/widget/lib/maps/map.scss @@ -35,6 +35,10 @@ div.tb-widget .tb-widget-content.tb-no-interaction { .tb-map-container { flex-direction: column; + .leaflet-gl-layer.maplibregl-map { + position: relative; + z-index: 1; + } } .tb-map-layout { @@ -47,6 +51,10 @@ div.tb-widget .tb-widget-content.tb-no-interaction { .tb-map { position: relative; flex: 1; + .leaflet-control-attribution { + font-size: 0.6rem; + background: rgba(255,255,255,0.5); + } &.leaflet-touch { .leaflet-bar { border: 1px solid rgba(0,0,0,0.38); diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/map/map-layer-row.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/map/map-layer-row.component.ts index 7112a0c3c2..392c9f9565 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/map/map-layer-row.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/map/map-layer-row.component.ts @@ -123,7 +123,8 @@ export class MapLayerRowComponent implements ControlValueAccessor, OnInit { provider: [null, [Validators.required]], layerType: [null, [Validators.required]], tileUrl: [null, [Validators.required]], - apiKey: [null, [Validators.required]] + apiKey: [null, [Validators.required]], + referenceLayer: [null, []] }); this.layerFormGroup.valueChanges.pipe( takeUntilDestroyed(this.destroyRef) diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/map/map-layer-settings-panel.component.html b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/map/map-layer-settings-panel.component.html index c30033f7e1..cf4c4b90c4 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/map/map-layer-settings-panel.component.html +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/map/map-layer-settings-panel.component.html @@ -82,6 +82,17 @@
        +
        +
        widgets.maps.layer.reference.reference-layer
        + + + {{ 'widgets.maps.layer.reference.no-layer' | translate }} + + {{ referenceLayerTypeTranslationMap.get(layer) | translate }} + + + +
        Date: Wed, 12 Mar 2025 11:59:45 +0200 Subject: [PATCH 292/345] Added help popup width --- .../components/dialog/calculated-field-dialog.component.html | 1 + .../calculated-field-script-test-dialog.component.html | 1 + ui-ngx/src/app/shared/components/js-func.component.html | 2 +- ui-ngx/src/app/shared/components/js-func.component.ts | 2 ++ 4 files changed, 5 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 471c722145..813f6b1b4c 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 @@ -102,6 +102,7 @@ [scriptLanguage]="ScriptLanguage.TBEL" [highlightRules]="argumentsHighlightRules$ | async" [editorCompleter]="argumentsEditorCompleter$ | async" + [helpPopupStyle]="{ width: '1200px' }" helpId="calculated-field/expression_fn" >
        {{ 'api-usage.tbel' | translate }}
        diff --git a/ui-ngx/src/app/modules/home/components/calculated-fields/components/test-dialog/calculated-field-script-test-dialog.component.html b/ui-ngx/src/app/modules/home/components/calculated-fields/components/test-dialog/calculated-field-script-test-dialog.component.html index 6859ff6dab..942d9484b5 100644 --- a/ui-ngx/src/app/modules/home/components/calculated-fields/components/test-dialog/calculated-field-script-test-dialog.component.html +++ b/ui-ngx/src/app/modules/home/components/calculated-fields/components/test-dialog/calculated-field-script-test-dialog.component.html @@ -44,6 +44,7 @@ [highlightRules]="data.argumentsHighlightRules" [scriptLanguage]="ScriptLanguage.TBEL" [editorCompleter]="data.argumentsEditorCompleter" + [helpPopupStyle]="{ width: '1200px' }" resultType="object" helpId="calculated-field/expression_fn" /> diff --git a/ui-ngx/src/app/shared/components/js-func.component.html b/ui-ngx/src/app/shared/components/js-func.component.html index 3155669f29..07f6dfe1b4 100644 --- a/ui-ngx/src/app/shared/components/js-func.component.html +++ b/ui-ngx/src/app/shared/components/js-func.component.html @@ -41,7 +41,7 @@ {{'js-func.tidy' | translate }} -
        +
        ; + @Input() helpPopupStyle: Record = {}; + @Input() @coerceBoolean() disableUndefinedCheck = false; From 52c04a4e02f59a94d4061ad3f5b7daf6fe8fe3ee Mon Sep 17 00:00:00 2001 From: dashevchenko Date: Wed, 12 Mar 2025 12:00:19 +0200 Subject: [PATCH 293/345] refactored getOwnerName --- .../thingsboard/server/edqs/repo/TenantRepo.java | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/common/edqs/src/main/java/org/thingsboard/server/edqs/repo/TenantRepo.java b/common/edqs/src/main/java/org/thingsboard/server/edqs/repo/TenantRepo.java index 59255b11cc..ab7fb3acff 100644 --- a/common/edqs/src/main/java/org/thingsboard/server/edqs/repo/TenantRepo.java +++ b/common/edqs/src/main/java/org/thingsboard/server/edqs/repo/TenantRepo.java @@ -422,19 +422,19 @@ public class TenantRepo { } public String getOwnerName(EntityId ownerId) { - if (ownerId == null || (EntityType.CUSTOMER.equals(ownerId.getEntityType()) && CustomerId.NULL_UUID.equals(ownerId.getId()))) { - ownerId = tenantId; + if (ownerId == null || (ownerId.getEntityType() == EntityType.CUSTOMER && ownerId.isNullUid())) { + return getOwnerEntityName(tenantId); } - return getEntityName(ownerId); + return getOwnerEntityName(ownerId); } - private String getEntityName(EntityId entityId) { + private String getOwnerEntityName(EntityId entityId) { EntityType entityType = entityId.getEntityType(); - if (entityType == EntityType.TENANT && entityId.getId().equals(TenantId.NULL_UUID)) { - return ""; - } return switch (entityType) { - case CUSTOMER, TENANT -> getEntityMap(entityType).get(entityId.getId()).getFields().getName(); + case CUSTOMER, TENANT -> { + EntityFields fields = getEntityMap(entityType).get(entityId.getId()).getFields(); + yield fields != null ? fields.getName() : ""; + } default -> throw new RuntimeException("Unsupported entity type: " + entityType); }; } From fdfb9264ad7738a45d08df538bb860a3e9849e01 Mon Sep 17 00:00:00 2001 From: Artem Dzhereleiko Date: Wed, 12 Mar 2025 12:03:15 +0200 Subject: [PATCH 294/345] UI: Refactoring --- ui-ngx/src/app/shared/components/nav-tree.component.ts | 8 ++------ ui-ngx/src/typings/jquery.jstree.typings.d.ts | 1 + 2 files changed, 3 insertions(+), 6 deletions(-) diff --git a/ui-ngx/src/app/shared/components/nav-tree.component.ts b/ui-ngx/src/app/shared/components/nav-tree.component.ts index 60405613fb..5b698a37d3 100644 --- a/ui-ngx/src/app/shared/components/nav-tree.component.ts +++ b/ui-ngx/src/app/shared/components/nav-tree.component.ts @@ -52,10 +52,6 @@ export interface NavTreeEditCallbacks { clearSearch?: () => void; } -interface JSTreeEventDataWithAction extends JSTreeEventData { - action: string; -} - export type NodesCallback = (nodes: NavTreeNode[]) => void; export type LoadNodesCallback = (node: NavTreeNode, cb: NodesCallback) => void; export type NodeSearchCallback = (searchText: string, node: NavTreeNode) => boolean; @@ -153,9 +149,9 @@ export class NavTreeComponent implements OnInit { this.treeElement = $('.tb-nav-tree-container', this.elementRef.nativeElement).jstree(config); - this.treeElement.on('changed.jstree', (e: any, data: JSTreeEventDataWithAction) => { - const node: NavTreeNode = data.instance.get_selected(true)[0]; + this.treeElement.on('changed.jstree', (e: any, data) => { if (this.onNodeSelected && data.action !== 'ready') { + const node: NavTreeNode = data.instance.get_selected(true)[0]; this.ngZone.run(() => this.onNodeSelected(node, e as Event)); } }); diff --git a/ui-ngx/src/typings/jquery.jstree.typings.d.ts b/ui-ngx/src/typings/jquery.jstree.typings.d.ts index 0aa6a0b92e..933a9c60d5 100644 --- a/ui-ngx/src/typings/jquery.jstree.typings.d.ts +++ b/ui-ngx/src/typings/jquery.jstree.typings.d.ts @@ -27,6 +27,7 @@ interface JQuery { interface JSTreeEventData { instance: JSTree; + action: string; } interface JSTreeModelEventData extends JSTreeEventData { From 6d565b4ccbd49dcee758c1a25396f1e63b65c68d Mon Sep 17 00:00:00 2001 From: Andrii Landiak Date: Wed, 12 Mar 2025 12:27:20 +0200 Subject: [PATCH 295/345] Fix method getEntityDataInfo in VersionController --- .../service/sync/vc/DefaultEntitiesVersionControlService.java | 3 +-- .../thingsboard/server/common/data/sync/vc/EntityDataInfo.java | 1 + 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/application/src/main/java/org/thingsboard/server/service/sync/vc/DefaultEntitiesVersionControlService.java b/application/src/main/java/org/thingsboard/server/service/sync/vc/DefaultEntitiesVersionControlService.java index 8ef6465d4b..3029e0be70 100644 --- a/application/src/main/java/org/thingsboard/server/service/sync/vc/DefaultEntitiesVersionControlService.java +++ b/application/src/main/java/org/thingsboard/server/service/sync/vc/DefaultEntitiesVersionControlService.java @@ -500,10 +500,9 @@ public class DefaultEntitiesVersionControlService implements EntitiesVersionCont @Override public ListenableFuture getEntityDataInfo(User user, EntityId entityId, String versionId) { return Futures.transform(gitServiceQueue.getEntity(user.getTenantId(), versionId, entityId), - entity -> new EntityDataInfo(entity.hasRelations(), entity.hasAttributes(), entity.hasCredentials()), MoreExecutors.directExecutor()); + entity -> new EntityDataInfo(entity.hasRelations(), entity.hasAttributes(), entity.hasCredentials(), entity.hasCalculatedFields()), MoreExecutors.directExecutor()); } - @Override public ListenableFuture> listBranches(TenantId tenantId) { return gitServiceQueue.listBranches(tenantId); diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/sync/vc/EntityDataInfo.java b/common/data/src/main/java/org/thingsboard/server/common/data/sync/vc/EntityDataInfo.java index 0b8f392472..d55a575a75 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/sync/vc/EntityDataInfo.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/sync/vc/EntityDataInfo.java @@ -26,4 +26,5 @@ public class EntityDataInfo { boolean hasRelations; boolean hasAttributes; boolean hasCredentials; + boolean hasCalculatedFields; } From ae02d43142e425db3713adef5377c0cd099fe554 Mon Sep 17 00:00:00 2001 From: IrynaMatveieva Date: Wed, 12 Mar 2025 12:29:00 +0200 Subject: [PATCH 296/345] moved check to validator --- .../CalculatedFieldEntityMessageProcessor.java | 2 ++ .../service/entitiy/queue/DefaultTbQueueService.java | 8 -------- .../org/thingsboard/server/dao/service/DataValidator.java | 4 ++++ 3 files changed, 6 insertions(+), 8 deletions(-) diff --git a/application/src/main/java/org/thingsboard/server/actors/calculatedField/CalculatedFieldEntityMessageProcessor.java b/application/src/main/java/org/thingsboard/server/actors/calculatedField/CalculatedFieldEntityMessageProcessor.java index 8767784a28..a185b71d56 100644 --- a/application/src/main/java/org/thingsboard/server/actors/calculatedField/CalculatedFieldEntityMessageProcessor.java +++ b/application/src/main/java/org/thingsboard/server/actors/calculatedField/CalculatedFieldEntityMessageProcessor.java @@ -289,6 +289,8 @@ public class CalculatedFieldEntityMessageProcessor extends AbstractContextAwareM if (state.isSizeOk()) { if (!calculationResult.isEmpty()) { cfService.pushMsgToRuleEngine(tenantId, entityId, calculationResult, cfIdList, callback); + } else { + callback.onSuccess(); } if (DebugModeUtil.isDebugAllAvailable(ctx.getCalculatedField())) { systemContext.persistCalculatedFieldDebugEvent(tenantId, ctx.getCfId(), entityId, state.getArguments(), tbMsgId, tbMsgType, JacksonUtil.writeValueAsString(calculationResult.getResult()), null); diff --git a/application/src/main/java/org/thingsboard/server/service/entitiy/queue/DefaultTbQueueService.java b/application/src/main/java/org/thingsboard/server/service/entitiy/queue/DefaultTbQueueService.java index 44c5182423..0d4cc26ce7 100644 --- a/application/src/main/java/org/thingsboard/server/service/entitiy/queue/DefaultTbQueueService.java +++ b/application/src/main/java/org/thingsboard/server/service/entitiy/queue/DefaultTbQueueService.java @@ -19,7 +19,6 @@ import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.springframework.stereotype.Service; import org.thingsboard.server.cluster.TbClusterService; -import org.thingsboard.server.common.data.DataConstants; import org.thingsboard.server.common.data.TenantProfile; import org.thingsboard.server.common.data.id.QueueId; import org.thingsboard.server.common.data.id.TenantId; @@ -57,7 +56,6 @@ public class DefaultTbQueueService extends AbstractTbEntityService implements Tb oldQueue = queueService.findQueueById(queue.getTenantId(), queue.getId()); } - checkQueueName(queue.getName()); Queue savedQueue = queueService.saveQueue(queue); createTopicsIfNeeded(savedQueue, oldQueue); tbClusterService.onQueuesUpdate(List.of(savedQueue)); @@ -183,10 +181,4 @@ public class DefaultTbQueueService extends AbstractTbEntityService implements Tb } } - private void checkQueueName(String queueName) { - if (DataConstants.CF_QUEUE_NAME.equals(queueName) || DataConstants.CF_STATES_QUEUE_NAME.equals(queueName)) { - throw new IllegalArgumentException(String.format("The queue name '%s' is not allowed. This name is reserved for internal use. Please choose a different name.", queueName)); - } - } - } diff --git a/dao/src/main/java/org/thingsboard/server/dao/service/DataValidator.java b/dao/src/main/java/org/thingsboard/server/dao/service/DataValidator.java index ba88d446d9..9f1f583108 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/service/DataValidator.java +++ b/dao/src/main/java/org/thingsboard/server/dao/service/DataValidator.java @@ -21,6 +21,7 @@ import org.apache.commons.io.FileUtils; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.context.annotation.Lazy; import org.thingsboard.server.common.data.BaseData; +import org.thingsboard.server.common.data.DataConstants; import org.thingsboard.server.common.data.EntityType; import org.thingsboard.server.common.data.StringUtils; import org.thingsboard.server.common.data.id.EntityId; @@ -158,6 +159,9 @@ public abstract class DataValidator> { protected static void validateQueueName(String name) { validateQueueNameOrTopic(name, NAME); + if (DataConstants.CF_QUEUE_NAME.equals(name) || DataConstants.CF_STATES_QUEUE_NAME.equals(name)) { + throw new DataValidationException(String.format("The queue name '%s' is not allowed. This name is reserved for internal use. Please choose a different name.", name)); + } } protected static void validateQueueTopic(String topic) { From 2d637558ae59d93bb7c5efc2f7c7bbe94510bc78 Mon Sep 17 00:00:00 2001 From: mpetrov Date: Wed, 12 Mar 2025 13:07:46 +0200 Subject: [PATCH 297/345] Added support for arrays result in events table --- .../modules/home/components/event/event-table-config.ts | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/ui-ngx/src/app/modules/home/components/event/event-table-config.ts b/ui-ngx/src/app/modules/home/components/event/event-table-config.ts index 56a17cc7ae..5d770fed3b 100644 --- a/ui-ngx/src/app/modules/home/components/event/event-table-config.ts +++ b/ui-ngx/src/app/modules/home/components/event/event-table-config.ts @@ -478,7 +478,12 @@ export class EventTableConfig extends EntityTableConfig { } if (contentType === ContentType.JSON && sortKeys) { try { - content = JSON.stringify(sortObjectKeys(JSON.parse(content))); + const parsedContent = JSON.parse(content); + if (Array.isArray(parsedContent)) { + content = JSON.stringify(parsedContent.map(item => item && typeof item === 'object' ? sortObjectKeys(item) : item)); + } else { + content = JSON.stringify(sortObjectKeys(parsedContent)); + } } catch (e) {} } this.dialog.open(EventContentDialogComponent, { From e81e248bf2d227901e56475e64e699ce68a1bb56 Mon Sep 17 00:00:00 2001 From: Artem Dzhereleiko Date: Wed, 12 Mar 2025 13:21:06 +0200 Subject: [PATCH 298/345] UI: Fixed entity autocomplete --- .../shared/components/entity/entity-autocomplete.component.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ui-ngx/src/app/shared/components/entity/entity-autocomplete.component.ts b/ui-ngx/src/app/shared/components/entity/entity-autocomplete.component.ts index 848550a94b..ac8054283a 100644 --- a/ui-ngx/src/app/shared/components/entity/entity-autocomplete.component.ts +++ b/ui-ngx/src/app/shared/components/entity/entity-autocomplete.component.ts @@ -391,7 +391,7 @@ export class EntityAutocompleteComponent implements ControlValueAccessor, OnInit private updateView(value: string | EntityId | null, entity: BaseData | null) { if (!isEqual(this.modelValue, value)) { this.modelValue = value; - this.entityURL = !entity ? '' : getEntityDetailsPageURL(entity.id.id, entity.id.entityType as EntityType); + this.entityURL = (typeof entity === 'string' || !entity) ? '' : getEntityDetailsPageURL(entity.id.id, entity.id.entityType as EntityType); this.propagateChange(this.modelValue); this.entityChanged.emit(entity); } From 3c0bedf5f3d577f53e38076198cd6d437c28e361 Mon Sep 17 00:00:00 2001 From: Andrii Shvaika Date: Wed, 12 Mar 2025 13:30:46 +0200 Subject: [PATCH 299/345] Remove ENV SPRING_DATASOURCE_PASSWORD=postgres from docker files --- msa/tb/docker-cassandra/Dockerfile | 1 - msa/tb/docker-postgres/Dockerfile | 1 - 2 files changed, 2 deletions(-) diff --git a/msa/tb/docker-cassandra/Dockerfile b/msa/tb/docker-cassandra/Dockerfile index 9c49ffe2f9..20a0d8cf7a 100644 --- a/msa/tb/docker-cassandra/Dockerfile +++ b/msa/tb/docker-cassandra/Dockerfile @@ -29,7 +29,6 @@ ENV CASSANDRA_DATA=/data/cassandra ENV SPRING_DRIVER_CLASS_NAME=org.postgresql.Driver ENV SPRING_DATASOURCE_URL=jdbc:postgresql://localhost:5432/thingsboard ENV SPRING_DATASOURCE_USERNAME=${pkg.user} -ENV SPRING_DATASOURCE_PASSWORD=postgres ENV CASSANDRA_HOST=localhost ENV CASSANDRA_PORT=9042 diff --git a/msa/tb/docker-postgres/Dockerfile b/msa/tb/docker-postgres/Dockerfile index f9e36ed61e..7c66d626c7 100644 --- a/msa/tb/docker-postgres/Dockerfile +++ b/msa/tb/docker-postgres/Dockerfile @@ -29,7 +29,6 @@ ENV PATH=$PATH:/usr/lib/postgresql/$PG_MAJOR/bin ENV SPRING_DRIVER_CLASS_NAME=org.postgresql.Driver ENV SPRING_DATASOURCE_URL=jdbc:postgresql://localhost:5432/thingsboard ENV SPRING_DATASOURCE_USERNAME=${pkg.user} -ENV SPRING_DATASOURCE_PASSWORD=postgres ENV PGLOG=/var/log/postgres From be9e89955adc16ef99f03e1be3b480d806f949e9 Mon Sep 17 00:00:00 2001 From: Artem Dzhereleiko Date: Wed, 12 Mar 2025 13:47:03 +0200 Subject: [PATCH 300/345] UI: Fixed icon close button position --- .../app/shared/components/string-autocomplete.component.html | 2 +- ui-ngx/src/app/shared/components/unit-input.component.html | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/ui-ngx/src/app/shared/components/string-autocomplete.component.html b/ui-ngx/src/app/shared/components/string-autocomplete.component.html index 459e41fdd8..8a9a1a38d0 100644 --- a/ui-ngx/src/app/shared/components/string-autocomplete.component.html +++ b/ui-ngx/src/app/shared/components/string-autocomplete.component.html @@ -24,7 +24,7 @@ [matAutocomplete]="optionsAutocomplete">
        @@ -361,7 +376,12 @@ px - +
        widgets.maps.data-layer.fill-color
        - +
        widgets.maps.data-layer.stroke
        @@ -388,7 +413,12 @@ px - +
        diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/map/marker-shape-settings.component.html b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/map/marker-shape-settings.component.html index 0a78698f0f..0ee3b1de37 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/map/marker-shape-settings.component.html +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/map/marker-shape-settings.component.html @@ -30,5 +30,9 @@
        - +
        diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/map/marker-shape-settings.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/map/marker-shape-settings.component.ts index 14e80e4fd2..4400c5a151 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/map/marker-shape-settings.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/map/marker-shape-settings.component.ts @@ -49,6 +49,8 @@ import { coerceBoolean } from '@shared/decorators/coercion'; import { MarkerIconShapesComponent } from '@home/components/widget/lib/settings/common/map/marker-icon-shapes.component'; +import { MapSettingsContext } from '@home/components/widget/lib/settings/common/map/map-settings.component.models'; +import { DatasourceType } from '@shared/models/widget.models'; @Component({ selector: 'tb-marker-shape-settings', @@ -69,6 +71,18 @@ export class MarkerShapeSettingsComponent implements ControlValueAccessor, OnIni @Input() disabled: boolean; + @Input() + context: MapSettingsContext; + + @Input() + dsType: DatasourceType; + + @Input() + dsEntityAliasId: string; + + @Input() + dsDeviceId: string; + @Input() markerType: MarkerType; 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 06083f4298..1884e86215 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 @@ -41,7 +41,7 @@ import { Observable, Observer, of, switchMap } from 'rxjs'; import { map } from 'rxjs/operators'; import { ImagePipe } from '@shared/pipe/image.pipe'; import { MarkerIconContainer, MarkerShape } from '@shared/models/widget/maps/marker-shape.models'; -import { DateFormatSettings, simpleDateFormat } from '@shared/models/widget-settings.models'; +import { ColorRange, DateFormatSettings, simpleDateFormat } from '@shared/models/widget-settings.models'; export enum MapType { geoMap = 'geoMap', @@ -233,12 +233,15 @@ export enum MarkerType { export enum DataLayerColorType { constant = 'constant', + range = 'range', function = 'function' } export interface DataLayerColorSettings { type: DataLayerColorType; color: string; + rangeKey?: DataKey; + range?: ColorRange[]; colorFunction?: TbFunction; } 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 eede6bb27e..eacc85992d 100644 --- a/ui-ngx/src/assets/locale/locale.constant-en_US.json +++ b/ui-ngx/src/assets/locale/locale.constant-en_US.json @@ -7979,7 +7979,11 @@ "stroke": "Stroke", "color-settings": "Color settings", "color-type-constant": "Constant", + "color-type-range": "Range", "color-type-function": "Function", + "color-range-source-key": "Color range source key", + "color-range-source-key-required": "Color range source key is required", + "color-range": "Color range", "color-function": "Color function", "label": "Label", "tooltip": "Tooltip", From 37192de5ef8a9d73a099d6611d01330ac4cad505 Mon Sep 17 00:00:00 2001 From: dashevchenko Date: Thu, 13 Mar 2025 13:04:09 +0200 Subject: [PATCH 310/345] fixed missed "title" entity field subscription --- .../thingsboard/server/common/data/edqs/fields/EntityFields.java | 1 + 1 file changed, 1 insertion(+) diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/edqs/fields/EntityFields.java b/common/data/src/main/java/org/thingsboard/server/common/data/edqs/fields/EntityFields.java index 532c4a92ac..1b0975542c 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/edqs/fields/EntityFields.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/edqs/fields/EntityFields.java @@ -148,6 +148,7 @@ public interface EntityFields { default String getAsString(String key) { return switch (key) { case "createdTime" -> Long.toString(getCreatedTime()); + case "title" -> getName(); case "type" -> getType(); case "label" -> getLabel(); case "additionalInfo" -> getAdditionalInfo(); From 09cff3ae5c6702873a70080bbca2aee5921db07c Mon Sep 17 00:00:00 2001 From: Artem Dzhereleiko Date: Thu, 13 Mar 2025 13:32:16 +0200 Subject: [PATCH 311/345] UI: Help for get dashboard state id aciton --- ...t-value-action-settings-panel.component.ts | 2 ++ .../parse_value_get_dashboard_state_id_fn.md | 33 +++++++++++++++++++ 2 files changed, 35 insertions(+) create mode 100644 ui-ngx/src/assets/help/en_US/widget/config/parse_value_get_dashboard_state_id_fn.md diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/action/get-value-action-settings-panel.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/action/get-value-action-settings-panel.component.ts index 1d1df6a8d5..9f85e9dd85 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/action/get-value-action-settings-panel.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/action/get-value-action-settings-panel.component.ts @@ -171,6 +171,8 @@ export class GetValueActionSettingsPanelComponent extends PageComponent implemen const action: GetValueAction = this.getValueSettingsFormGroup.get('action').value; if (action === GetValueAction.GET_DASHBOARD_STATE_OBJECT) { return 'widget/config/parse_value_get_dashboard_state_object_fn'; + } else if (action === GetValueAction.GET_DASHBOARD_STATE) { + return 'widget/config/parse_value_get_dashboard_state_id_fn'; } return 'widget/lib/rpc/parse_value_fn'; } diff --git a/ui-ngx/src/assets/help/en_US/widget/config/parse_value_get_dashboard_state_id_fn.md b/ui-ngx/src/assets/help/en_US/widget/config/parse_value_get_dashboard_state_id_fn.md new file mode 100644 index 0000000000..3a580b4136 --- /dev/null +++ b/ui-ngx/src/assets/help/en_US/widget/config/parse_value_get_dashboard_state_id_fn.md @@ -0,0 +1,33 @@ +#### Parse value function + +
        +
        + +*function (data): boolean* + +A JavaScript function that converts the current dashboard state id into a boolean value. + +**Parameters:** + +
          +
        • data: string - the current dashboard state id. +
        • +
        + +**Returns:** + +`true` if the widget should be in an activated state, `false` otherwise. + +
        + +##### Examples + +* Check if the current dashboard state id is "default": + +```javascript +return data === 'default' ? true : false; +{:copy-code} +``` + +
        +
        From 8c6497d773beac877b12ac46dee99d23949be8fc Mon Sep 17 00:00:00 2001 From: dashevchenko Date: Thu, 13 Mar 2025 14:36:40 +0200 Subject: [PATCH 312/345] fixed relation created for gateway devices --- .../server/service/transport/DefaultTransportApiService.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/application/src/main/java/org/thingsboard/server/service/transport/DefaultTransportApiService.java b/application/src/main/java/org/thingsboard/server/service/transport/DefaultTransportApiService.java index 2c8c942ba3..7fee411704 100644 --- a/application/src/main/java/org/thingsboard/server/service/transport/DefaultTransportApiService.java +++ b/application/src/main/java/org/thingsboard/server/service/transport/DefaultTransportApiService.java @@ -351,7 +351,7 @@ public class DefaultTransportApiService implements TransportApiService { device.setAdditionalInfo(additionalInfo); device = deviceService.saveDevice(device); - relationService.saveRelation(TenantId.SYS_TENANT_ID, new EntityRelation(gateway.getId(), device.getId(), "Created")); + relationService.saveRelation(tenantId, new EntityRelation(gateway.getId(), device.getId(), "Created")); TbMsgMetaData metaData = new TbMsgMetaData(); CustomerId customerId = gateway.getCustomerId(); From 5a238415bee3326c740352abe26cadb08f78543d Mon Sep 17 00:00:00 2001 From: IrynaMatveieva Date: Thu, 13 Mar 2025 14:52:58 +0200 Subject: [PATCH 313/345] fixed cluster issues --- ...alculatedFieldManagerMessageProcessor.java | 31 ++++++++++++++++--- .../server/actors/tenant/TenantActor.java | 2 +- ...faultTbCalculatedFieldConsumerService.java | 8 ++--- .../server/controller/AbstractWebTest.java | 5 +++ 4 files changed, 35 insertions(+), 11 deletions(-) diff --git a/application/src/main/java/org/thingsboard/server/actors/calculatedField/CalculatedFieldManagerMessageProcessor.java b/application/src/main/java/org/thingsboard/server/actors/calculatedField/CalculatedFieldManagerMessageProcessor.java index b0185fd555..cd1d05d726 100644 --- a/application/src/main/java/org/thingsboard/server/actors/calculatedField/CalculatedFieldManagerMessageProcessor.java +++ b/application/src/main/java/org/thingsboard/server/actors/calculatedField/CalculatedFieldManagerMessageProcessor.java @@ -22,6 +22,7 @@ import org.thingsboard.server.actors.TbActorRef; import org.thingsboard.server.actors.TbCalculatedFieldEntityActorId; import org.thingsboard.server.actors.service.DefaultActorService; import org.thingsboard.server.actors.shared.AbstractContextAwareMsgProcessor; +import org.thingsboard.server.common.data.DataConstants; import org.thingsboard.server.common.data.EntityType; import org.thingsboard.server.common.data.cf.CalculatedField; import org.thingsboard.server.common.data.cf.CalculatedFieldLink; @@ -35,6 +36,7 @@ import org.thingsboard.server.common.msg.cf.CalculatedFieldInitMsg; import org.thingsboard.server.common.msg.cf.CalculatedFieldLinkInitMsg; import org.thingsboard.server.common.msg.cf.CalculatedFieldPartitionChangeMsg; import org.thingsboard.server.common.msg.plugin.ComponentLifecycleMsg; +import org.thingsboard.server.common.msg.queue.ServiceType; import org.thingsboard.server.common.msg.queue.TbCallback; import org.thingsboard.server.dao.cf.CalculatedFieldService; import org.thingsboard.server.service.cf.CalculatedFieldProcessingService; @@ -178,6 +180,9 @@ public class CalculatedFieldManagerMessageProcessor extends AbstractContextAware EntityId entityId = msg.getEntityId(); EntityId profileId = getProfileId(tenantId, entityId); cfEntityCache.add(tenantId, profileId, entityId); + if (!isMyPartition(entityId, callback)) { + return; + } var entityIdFields = getCalculatedFieldsByEntityId(entityId); var profileIdFields = getCalculatedFieldsByEntityId(profileId); var fieldsCount = entityIdFields.size() + profileIdFields.size(); @@ -193,6 +198,9 @@ public class CalculatedFieldManagerMessageProcessor extends AbstractContextAware private void onEntityUpdated(ComponentLifecycleMsg msg, TbCallback callback) { if (msg.getOldProfileId() != null && msg.getOldProfileId() != msg.getProfileId()) { cfEntityCache.update(tenantId, msg.getOldProfileId(), msg.getProfileId(), msg.getEntityId()); + if (!isMyPartition(msg.getEntityId(), callback)) { + return; + } var oldProfileCfs = getCalculatedFieldsByEntityId(msg.getOldProfileId()); var newProfileCfs = getCalculatedFieldsByEntityId(msg.getProfileId()); var fieldsCount = oldProfileCfs.size() + newProfileCfs.size(); @@ -209,8 +217,10 @@ public class CalculatedFieldManagerMessageProcessor extends AbstractContextAware private void onEntityDeleted(ComponentLifecycleMsg msg, TbCallback callback) { cfEntityCache.evict(tenantId, msg.getEntityId()); - log.info("Pushing entity lifecycle msg to specific actor [{}]", msg.getEntityId()); - getOrCreateActor(msg.getEntityId()).tell(new CalculatedFieldEntityDeleteMsg(tenantId, msg.getEntityId(), callback)); + if (isMyPartition(msg.getEntityId(), callback)) { + log.info("Pushing entity lifecycle msg to specific actor [{}]", msg.getEntityId()); + getOrCreateActor(msg.getEntityId()).tell(new CalculatedFieldEntityDeleteMsg(tenantId, msg.getEntityId(), callback)); + } } private void onCfCreated(ComponentLifecycleMsg msg, TbCallback callback) throws CalculatedFieldException { @@ -311,7 +321,9 @@ public class CalculatedFieldManagerMessageProcessor extends AbstractContextAware callback.onSuccess(); } } else { - deleteCfForEntity(entityId, cfId, callback); + if (isMyPartition(entityId, callback)) { + deleteCfForEntity(entityId, cfId, callback); + } } } } @@ -418,7 +430,9 @@ public class CalculatedFieldManagerMessageProcessor extends AbstractContextAware callback.onSuccess(); } } else { - initCfForEntity(entityId, cfCtx, forceStateReinit, callback); + if (isMyPartition(entityId, callback)) { + initCfForEntity(entityId, cfCtx, forceStateReinit, callback); + } } } @@ -432,6 +446,15 @@ public class CalculatedFieldManagerMessageProcessor extends AbstractContextAware getOrCreateActor(entityId).tell(new EntityInitCalculatedFieldMsg(tenantId, cfCtx, callback, forceStateReinit)); } + private boolean isMyPartition(EntityId entityId, TbCallback callback) { + if (!systemContext.getPartitionService().resolve(ServiceType.TB_RULE_ENGINE, DataConstants.CF_QUEUE_NAME, tenantId, entityId).isMyPartition()) { + log.debug("[{}] Entity belongs to external partition.", entityId); + callback.onSuccess(); + return false; + } + return true; + } + private static boolean isProfileEntity(EntityType entityType) { return EntityType.DEVICE_PROFILE.equals(entityType) || EntityType.ASSET_PROFILE.equals(entityType); } diff --git a/application/src/main/java/org/thingsboard/server/actors/tenant/TenantActor.java b/application/src/main/java/org/thingsboard/server/actors/tenant/TenantActor.java index f9eec324fc..c1a0980623 100644 --- a/application/src/main/java/org/thingsboard/server/actors/tenant/TenantActor.java +++ b/application/src/main/java/org/thingsboard/server/actors/tenant/TenantActor.java @@ -112,7 +112,7 @@ public class TenantActor extends RuleChainManagerActor { cantFindTenant = true; } } else { - log.info("Tenant {} is not managed by current service, skipping rule chains init", tenantId); + log.info("Tenant {} is not managed by current service, skipping rule chains and cf actor init", tenantId); } } log.debug("[{}] Tenant actor started.", tenantId); diff --git a/application/src/main/java/org/thingsboard/server/service/queue/DefaultTbCalculatedFieldConsumerService.java b/application/src/main/java/org/thingsboard/server/service/queue/DefaultTbCalculatedFieldConsumerService.java index ad248cc3d4..3232a9b89f 100644 --- a/application/src/main/java/org/thingsboard/server/service/queue/DefaultTbCalculatedFieldConsumerService.java +++ b/application/src/main/java/org/thingsboard/server/service/queue/DefaultTbCalculatedFieldConsumerService.java @@ -15,15 +15,12 @@ */ package org.thingsboard.server.service.queue; -import com.google.common.util.concurrent.ListeningExecutorService; -import com.google.common.util.concurrent.MoreExecutors; import jakarta.annotation.PostConstruct; import jakarta.annotation.PreDestroy; import lombok.extern.slf4j.Slf4j; import org.springframework.beans.factory.annotation.Value; import org.springframework.context.ApplicationEventPublisher; import org.springframework.stereotype.Service; -import org.thingsboard.common.util.ThingsBoardExecutors; import org.thingsboard.server.actors.ActorSystemContext; import org.thingsboard.server.actors.calculatedField.CalculatedFieldLinkedTelemetryMsg; import org.thingsboard.server.actors.calculatedField.CalculatedFieldTelemetryMsg; @@ -35,7 +32,7 @@ import org.thingsboard.server.common.msg.cf.CalculatedFieldEntityLifecycleMsg; import org.thingsboard.server.common.msg.cf.CalculatedFieldPartitionChangeMsg; import org.thingsboard.server.common.msg.queue.ServiceType; import org.thingsboard.server.common.msg.queue.TbCallback; -import org.thingsboard.server.common.msg.queue.TopicPartitionInfo; +import org.thingsboard.server.common.util.ProtoUtils; import org.thingsboard.server.dao.tenant.TbTenantProfileCache; import org.thingsboard.server.gen.transport.TransportProtos.CalculatedFieldLinkedTelemetryMsgProto; import org.thingsboard.server.gen.transport.TransportProtos.CalculatedFieldTelemetryMsgProto; @@ -60,7 +57,6 @@ import org.thingsboard.server.service.queue.processing.IdMsgPair; import org.thingsboard.server.service.security.auth.jwt.settings.JwtSettingsService; import java.util.List; -import java.util.Set; import java.util.UUID; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.ConcurrentMap; @@ -220,7 +216,7 @@ public class DefaultTbCalculatedFieldConsumerService extends AbstractConsumerSer protected void handleNotification(UUID id, TbProtoQueueMsg msg, TbCallback callback) { ToCalculatedFieldNotificationMsg toCfNotification = msg.getValue(); if (toCfNotification.hasComponentLifecycleMsg()) { - // from upstream (maybe removed since we don't need to init state for each partition) + handleComponentLifecycleMsg(id, ProtoUtils.fromProto(toCfNotification.getComponentLifecycleMsg())); log.trace("[{}] Forwarding component lifecycle message for processing {}", id, toCfNotification.getComponentLifecycleMsg()); forwardToActorSystem(toCfNotification.getComponentLifecycleMsg(), callback); } else if (toCfNotification.hasLinkedTelemetryMsg()) { diff --git a/application/src/test/java/org/thingsboard/server/controller/AbstractWebTest.java b/application/src/test/java/org/thingsboard/server/controller/AbstractWebTest.java index e90aecb41e..33f2209eca 100644 --- a/application/src/test/java/org/thingsboard/server/controller/AbstractWebTest.java +++ b/application/src/test/java/org/thingsboard/server/controller/AbstractWebTest.java @@ -38,6 +38,7 @@ import org.mockito.Mockito; import org.mockito.invocation.InvocationOnMock; import org.mockito.stubbing.Answer; import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.mock.mockito.MockBean; import org.springframework.boot.test.mock.mockito.SpyBean; import org.springframework.cache.Cache; import org.springframework.cache.CacheManager; @@ -143,6 +144,7 @@ import org.thingsboard.server.dao.device.ClaimDevicesService; import org.thingsboard.server.dao.tenant.TenantProfileService; import org.thingsboard.server.dao.timeseries.TimeseriesService; import org.thingsboard.server.queue.memory.InMemoryStorage; +import org.thingsboard.server.service.cf.CfRocksDb; import org.thingsboard.server.service.entitiy.tenant.profile.TbTenantProfileService; import org.thingsboard.server.service.security.auth.jwt.RefreshTokenRequest; import org.thingsboard.server.service.security.auth.rest.LoginRequest; @@ -276,6 +278,9 @@ public abstract class AbstractWebTest extends AbstractInMemoryStorageTest { @Autowired protected InMemoryStorage storage; + @MockBean + protected CfRocksDb cfRocksDb; + @Rule public TestRule watcher = new TestWatcher() { protected void starting(Description description) { From 1e0fd6c387734494f45186b83de7303a9037f004 Mon Sep 17 00:00:00 2001 From: IrynaMatveieva Date: Thu, 13 Mar 2025 15:34:02 +0200 Subject: [PATCH 314/345] changed log level --- .../CalculatedFieldManagerMessageProcessor.java | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/application/src/main/java/org/thingsboard/server/actors/calculatedField/CalculatedFieldManagerMessageProcessor.java b/application/src/main/java/org/thingsboard/server/actors/calculatedField/CalculatedFieldManagerMessageProcessor.java index cd1d05d726..2d182510e9 100644 --- a/application/src/main/java/org/thingsboard/server/actors/calculatedField/CalculatedFieldManagerMessageProcessor.java +++ b/application/src/main/java/org/thingsboard/server/actors/calculatedField/CalculatedFieldManagerMessageProcessor.java @@ -123,7 +123,7 @@ public class CalculatedFieldManagerMessageProcessor extends AbstractContextAware if (calculatedField != null) { msg.getState().setRequiredArguments(calculatedField.getArgNames()); - log.info("Pushing CF state restore msg to specific actor [{}]", msg.getId().entityId()); + log.debug("Pushing CF state restore msg to specific actor [{}]", msg.getId().entityId()); getOrCreateActor(msg.getId().entityId()).tell(msg); } else { cfStateService.removeState(msg.getId(), msg.getCallback()); @@ -218,7 +218,7 @@ public class CalculatedFieldManagerMessageProcessor extends AbstractContextAware private void onEntityDeleted(ComponentLifecycleMsg msg, TbCallback callback) { cfEntityCache.evict(tenantId, msg.getEntityId()); if (isMyPartition(msg.getEntityId(), callback)) { - log.info("Pushing entity lifecycle msg to specific actor [{}]", msg.getEntityId()); + log.debug("Pushing entity lifecycle msg to specific actor [{}]", msg.getEntityId()); getOrCreateActor(msg.getEntityId()).tell(new CalculatedFieldEntityDeleteMsg(tenantId, msg.getEntityId(), callback)); } } @@ -437,12 +437,12 @@ public class CalculatedFieldManagerMessageProcessor extends AbstractContextAware } private void deleteCfForEntity(EntityId entityId, CalculatedFieldId cfId, TbCallback callback) { - log.info("Pushing delete CF msg to specific actor [{}]", entityId); + log.debug("Pushing delete CF msg to specific actor [{}]", entityId); getOrCreateActor(entityId).tell(new CalculatedFieldEntityDeleteMsg(tenantId, cfId, callback)); } private void initCfForEntity(EntityId entityId, CalculatedFieldCtx cfCtx, boolean forceStateReinit, TbCallback callback) { - log.info("Pushing entity init CF msg to specific actor [{}]", entityId); + log.debug("Pushing entity init CF msg to specific actor [{}]", entityId); getOrCreateActor(entityId).tell(new EntityInitCalculatedFieldMsg(tenantId, cfCtx, callback, forceStateReinit)); } From b4fbc6261e589dc8b38c1d998c3d7d76b7d72d0c Mon Sep 17 00:00:00 2001 From: Artem Dzhereleiko Date: Thu, 13 Mar 2025 15:38:36 +0200 Subject: [PATCH 315/345] UI: Fixed default events and add hot keys for mac --- .../layout/dashboard-layout.component.ts | 25 +++++++++++-------- 1 file changed, 15 insertions(+), 10 deletions(-) diff --git a/ui-ngx/src/app/modules/home/components/dashboard-page/layout/dashboard-layout.component.ts b/ui-ngx/src/app/modules/home/components/dashboard-page/layout/dashboard-layout.component.ts index b732b7ff09..510fc1ce63 100644 --- a/ui-ngx/src/app/modules/home/components/dashboard-page/layout/dashboard-layout.component.ts +++ b/ui-ngx/src/app/modules/home/components/dashboard-page/layout/dashboard-layout.component.ts @@ -174,66 +174,71 @@ export class DashboardLayoutComponent extends PageComponent implements ILayoutCo private initHotKeys(): void { this.hotKeys.push( - new Hotkey('ctrl+c', (event: KeyboardEvent) => { + new Hotkey(['ctrl+c', 'meta+c'], (event: KeyboardEvent) => { if (this.isEdit && !this.isEditingWidget && !this.widgetEditMode) { const widget = this.dashboard.getSelectedWidget(); if (widget) { event.preventDefault(); this.copyWidget(event, widget); } + return false; } - return false; + return true; }, null, this.translate.instant('action.copy')) ); this.hotKeys.push( - new Hotkey('ctrl+r', (event: KeyboardEvent) => { + new Hotkey(['ctrl+r', 'meta+r'], (event: KeyboardEvent) => { if (this.isEdit && !this.isEditingWidget && !this.widgetEditMode) { const widget = this.dashboard.getSelectedWidget(); if (widget) { event.preventDefault(); this.copyWidgetReference(event, widget); } + return false; } - return false; + return true; }, null, this.translate.instant('action.copy-reference')) ); this.hotKeys.push( - new Hotkey('ctrl+v', (event: KeyboardEvent) => { + new Hotkey(['ctrl+v', 'meta+v'], (event: KeyboardEvent) => { if (this.isEdit && !this.isEditingWidget && !this.widgetEditMode) { if (this.itembuffer.hasWidget()) { event.preventDefault(); this.pasteWidget(event); } + return false; } - return false; + return true; }, null, this.translate.instant('action.paste')) ); this.hotKeys.push( - new Hotkey('ctrl+i', (event: KeyboardEvent) => { + new Hotkey(['ctrl+i', 'meta+i'], (event: KeyboardEvent) => { if (this.isEdit && !this.isEditingWidget && !this.widgetEditMode) { if (this.itembuffer.canPasteWidgetReference(this.dashboardCtx.getDashboard(), this.dashboardCtx.state, this.layoutCtx.id, this.layoutCtx.breakpoint)) { event.preventDefault(); this.pasteWidgetReference(event); } + return false; } - return false; + return true; }, null, this.translate.instant('action.paste-reference')) ); this.hotKeys.push( - new Hotkey('ctrl+x', (event: KeyboardEvent) => { + new Hotkey(['ctrl+x', 'meta+x'], (event: KeyboardEvent) => { if (this.isEdit && !this.isEditingWidget && !this.widgetEditMode) { const widget = this.dashboard.getSelectedWidget(); if (widget) { event.preventDefault(); this.layoutCtx.dashboardCtrl.removeWidget(event, this.layoutCtx, widget); } + return false; } - return false; + return true; }, null, this.translate.instant('action.delete')) ); From e900e6ed86955792ac3cb753dece13ee3d29521c Mon Sep 17 00:00:00 2001 From: ViacheslavKlimov Date: Thu, 13 Mar 2025 15:39:15 +0200 Subject: [PATCH 316/345] Fix license header --- .../thingsboard/server/actors/service/ComponentActorTest.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/application/src/test/java/org/thingsboard/server/actors/service/ComponentActorTest.java b/application/src/test/java/org/thingsboard/server/actors/service/ComponentActorTest.java index f847ca7db7..49bf3c8670 100644 --- a/application/src/test/java/org/thingsboard/server/actors/service/ComponentActorTest.java +++ b/application/src/test/java/org/thingsboard/server/actors/service/ComponentActorTest.java @@ -1,5 +1,5 @@ /** - * Copyright © 2016-2024 The Thingsboard Authors + * 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. From 340263f00ba11209491a30d56e47a70b8740bdf4 Mon Sep 17 00:00:00 2001 From: Artem Dzhereleiko Date: Thu, 13 Mar 2025 16:00:55 +0200 Subject: [PATCH 317/345] UI: Refactoring --- ui-ngx/src/app/modules/home/pages/admin/settings-card.scss | 4 ++++ ui-ngx/src/styles.scss | 4 ---- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/ui-ngx/src/app/modules/home/pages/admin/settings-card.scss b/ui-ngx/src/app/modules/home/pages/admin/settings-card.scss index 50cd35d9da..7c707dee6d 100644 --- a/ui-ngx/src/app/modules/home/pages/admin/settings-card.scss +++ b/ui-ngx/src/app/modules/home/pages/admin/settings-card.scss @@ -19,6 +19,10 @@ .mat-mdc-card.settings-card { margin: 8px; + mat-card-header.mat-mdc-card-header { + align-items: center; + } + .fields-group { padding: 0 16px 8px; margin: 10px 0; diff --git a/ui-ngx/src/styles.scss b/ui-ngx/src/styles.scss index 4369514fff..539a3b1960 100644 --- a/ui-ngx/src/styles.scss +++ b/ui-ngx/src/styles.scss @@ -736,10 +736,6 @@ pre.tb-highlight { } } - mat-card-header.mat-mdc-card-header { - align-items: center; - } - .mat-mdc-footer-row::after, .mat-mdc-header-row::after, .mat-mdc-row::after { content: none; } From 46262e870938cb5f9557cbaf1f705481ed817361 Mon Sep 17 00:00:00 2001 From: dashevchenko Date: Thu, 13 Mar 2025 16:37:28 +0200 Subject: [PATCH 318/345] fixed entity relation cleanup method --- .../thingsboard/server/service/entitiy/EntityServiceTest.java | 4 ++++ .../thingsboard/server/dao/relation/BaseRelationService.java | 3 +++ 2 files changed, 7 insertions(+) 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 18687cedba..1e7f5384b5 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 @@ -282,6 +282,10 @@ public class EntityServiceTest extends AbstractControllerTest { data.forEach(entityData -> Assert.assertNotNull(entityData.getLatest().get(EntityKeyType.ENTITY_FIELD).get("phone"))); countByQueryAndCheck(query, 5); + + // delete user + userService.deleteUser(tenantId, users.get(0)); + countByQueryAndCheck(query, 4); } private void createTestUserRelations(TenantId tenantId, List users) { diff --git a/dao/src/main/java/org/thingsboard/server/dao/relation/BaseRelationService.java b/dao/src/main/java/org/thingsboard/server/dao/relation/BaseRelationService.java index a0e75e062c..87f7e44a1f 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/relation/BaseRelationService.java +++ b/dao/src/main/java/org/thingsboard/server/dao/relation/BaseRelationService.java @@ -236,6 +236,7 @@ public class BaseRelationService implements RelationService { return Futures.transform(future, deletedEvent -> { if (deletedEvent != null) { handleEvictEvent(EntityRelationEvent.from(deletedEvent)); + eventPublisher.publishEvent(new RelationActionEvent(tenantId, deletedEvent, ActionType.RELATION_DELETED)); } return deletedEvent != null; }, MoreExecutors.directExecutor()); @@ -267,6 +268,7 @@ public class BaseRelationService implements RelationService { for (EntityRelation relation : inboundRelations) { eventPublisher.publishEvent(EntityRelationEvent.from(relation)); + eventPublisher.publishEvent(new RelationActionEvent(tenantId, relation, ActionType.RELATION_DELETED)); } List outboundRelations; @@ -278,6 +280,7 @@ public class BaseRelationService implements RelationService { for (EntityRelation relation : outboundRelations) { eventPublisher.publishEvent(EntityRelationEvent.from(relation)); + eventPublisher.publishEvent(new RelationActionEvent(tenantId, relation, ActionType.RELATION_DELETED)); } } From 2fb1e23c9178ed2a7ed7cc11f9d3c103cb02473a Mon Sep 17 00:00:00 2001 From: Igor Kulikov Date: Thu, 13 Mar 2025 17:04:31 +0200 Subject: [PATCH 319/345] UI: Fix map marker icon transform fon safari browsers. --- .../models/widget/maps/marker-shape.models.ts | 16 +++++++++------- 1 file changed, 9 insertions(+), 7 deletions(-) diff --git a/ui-ngx/src/app/shared/models/widget/maps/marker-shape.models.ts b/ui-ngx/src/app/shared/models/widget/maps/marker-shape.models.ts index 29e04dbaa1..46899ccd11 100644 --- a/ui-ngx/src/app/shared/models/widget/maps/marker-shape.models.ts +++ b/ui-ngx/src/app/shared/models/widget/maps/marker-shape.models.ts @@ -95,7 +95,7 @@ interface MarkerIconContainerDefinition { iconSize: number; iconColor: (color: tinycolor.Instance) => tinycolor.Instance; iconAlpha: (color: tinycolor.Instance) => number; - appendIcon?: (svgElement: SVGElement, iconElement: Element) => void; + appendIcon?: (svgElement: SVGElement, iconElement: Element, iconSize: number) => void; } const emptyIconContainerDefinition: MarkerIconContainerDefinition = { @@ -114,14 +114,16 @@ const defaultTripIconContainerDefinition: MarkerIconContainerDefinition = { iconSize: 24, iconColor: () => tinycolor('#000'), iconAlpha: () => 1, - appendIcon: (svgElement, iconElement) => { + appendIcon: (svgElement, iconElement, iconSize) => { const box = iconElement.bbox(); + const cx = iconSize/2 + box.x; + const cy = iconSize/2 + box.y; let elements = svgElement.getElementsByClassName('icon-mask-exclude'); if (elements.length) { elements = elements[0].getElementsByClassName('marker-icon-container'); if (elements.length) { const iconContainer = new G(elements[0] as SVGGElement); - iconContainer.add(iconElement.clone().fill('#000').translate(-box.cx, -box.cy)); + iconContainer.add(iconElement.clone().fill('#000').translate(-cx, -cy)); } } elements = svgElement.getElementsByClassName('icon-mask-overlay'); @@ -129,7 +131,7 @@ const defaultTripIconContainerDefinition: MarkerIconContainerDefinition = { elements = elements[0].getElementsByClassName('marker-icon-container'); if (elements.length) { const iconContainer = new G(elements[0] as SVGGElement); - iconContainer.add(iconElement.clone().fill('#fff').translate(-box.cx, -box.cy)); + iconContainer.add(iconElement.clone().fill('#fff').translate(-cx, -cy)); } } } @@ -302,14 +304,14 @@ export const createColorMarkerIconElement = (iconRegistry: MatIconRegistry, domS if (svgElement) { if (iconElement) { if (definition.appendIcon) { - definition.appendIcon(svgElement, iconElement); + definition.appendIcon(svgElement, iconElement, iconSize); } else { const elements = svgElement.getElementsByClassName('marker-icon-container'); if (elements.length) { const iconContainer = new G(elements[0] as SVGGElement); iconContainer.add(iconElement); const box = iconElement.bbox(); - iconElement.translate(-box.cx, -box.cy); + iconElement.translate(-(iconSize/2 + box.x), -(iconSize/2 + box.y)); } } } @@ -321,7 +323,7 @@ export const createColorMarkerIconElement = (iconRegistry: MatIconRegistry, domS iconContainer.translate(iconSize/2,iconSize/2); iconContainer.add(iconElement); const box = iconElement.bbox(); - iconElement.translate(-box.cx, -box.cy); + iconElement.translate(-(iconSize/2 + box.x), -(iconSize/2 + box.y)); svg.add(iconContainer); return svg.node; } From b40fa86bac2ed81fca5be45d08711288f4f8392a Mon Sep 17 00:00:00 2001 From: ViacheslavKlimov Date: Thu, 13 Mar 2025 17:35:00 +0200 Subject: [PATCH 320/345] Refactoring and fixes for CF lifecycle events handling --- .../server/actors/app/AppActor.java | 1 - ...alculatedFieldManagerMessageProcessor.java | 2 +- .../server/actors/tenant/TenantActor.java | 31 +++--- ...faultTbCalculatedFieldConsumerService.java | 19 +--- .../queue/DefaultTbClusterService.java | 101 +++++------------- .../DefaultTbRuleEngineConsumerService.java | 3 +- .../server/cluster/TbClusterService.java | 2 - .../server/common/data/EntityType.java | 11 ++ .../cf/CalculatedFieldEntityLifecycleMsg.java | 3 - common/proto/src/main/proto/queue.proto | 8 +- .../discovery/event/PartitionChangeEvent.java | 1 - 11 files changed, 63 insertions(+), 119 deletions(-) diff --git a/application/src/main/java/org/thingsboard/server/actors/app/AppActor.java b/application/src/main/java/org/thingsboard/server/actors/app/AppActor.java index 50fa7e2f8d..904547a2b6 100644 --- a/application/src/main/java/org/thingsboard/server/actors/app/AppActor.java +++ b/application/src/main/java/org/thingsboard/server/actors/app/AppActor.java @@ -116,7 +116,6 @@ public class AppActor extends ContextAwareActor { case CF_INIT_MSG: case CF_LINK_INIT_MSG: case CF_STATE_RESTORE_MSG: - case CF_ENTITY_LIFECYCLE_MSG: //TODO: use priority from the message body. For example, messages about CF lifecycle are important and Device lifecycle are not. // same for the Linked telemetry. onToCalculatedFieldSystemActorMsg((ToCalculatedFieldSystemMsg) msg, true); diff --git a/application/src/main/java/org/thingsboard/server/actors/calculatedField/CalculatedFieldManagerMessageProcessor.java b/application/src/main/java/org/thingsboard/server/actors/calculatedField/CalculatedFieldManagerMessageProcessor.java index 2d182510e9..fc48e9ad3e 100644 --- a/application/src/main/java/org/thingsboard/server/actors/calculatedField/CalculatedFieldManagerMessageProcessor.java +++ b/application/src/main/java/org/thingsboard/server/actors/calculatedField/CalculatedFieldManagerMessageProcessor.java @@ -196,7 +196,7 @@ public class CalculatedFieldManagerMessageProcessor extends AbstractContextAware } private void onEntityUpdated(ComponentLifecycleMsg msg, TbCallback callback) { - if (msg.getOldProfileId() != null && msg.getOldProfileId() != msg.getProfileId()) { + if (msg.getOldProfileId() != null && !msg.getOldProfileId().equals(msg.getProfileId())) { cfEntityCache.update(tenantId, msg.getOldProfileId(), msg.getProfileId(), msg.getEntityId()); if (!isMyPartition(msg.getEntityId(), callback)) { return; diff --git a/application/src/main/java/org/thingsboard/server/actors/tenant/TenantActor.java b/application/src/main/java/org/thingsboard/server/actors/tenant/TenantActor.java index c1a0980623..846bde508d 100644 --- a/application/src/main/java/org/thingsboard/server/actors/tenant/TenantActor.java +++ b/application/src/main/java/org/thingsboard/server/actors/tenant/TenantActor.java @@ -49,6 +49,7 @@ import org.thingsboard.server.common.msg.TbMsg; import org.thingsboard.server.common.msg.ToCalculatedFieldSystemMsg; import org.thingsboard.server.common.msg.aware.DeviceAwareMsg; import org.thingsboard.server.common.msg.aware.RuleChainAwareMsg; +import org.thingsboard.server.common.msg.cf.CalculatedFieldEntityLifecycleMsg; import org.thingsboard.server.common.msg.plugin.ComponentLifecycleMsg; import org.thingsboard.server.common.msg.queue.PartitionChangeMsg; import org.thingsboard.server.common.msg.queue.QueueToRuleEngineMsg; @@ -175,7 +176,6 @@ public class TenantActor extends RuleChainManagerActor { case CF_LINK_INIT_MSG: case CF_STATE_RESTORE_MSG: case CF_PARTITIONS_CHANGE_MSG: - case CF_ENTITY_LIFECYCLE_MSG: onToCalculatedFieldSystemActorMsg((ToCalculatedFieldSystemMsg) msg, true); break; case CF_TELEMETRY_MSG: @@ -315,19 +315,26 @@ public class TenantActor extends RuleChainManagerActor { onToDeviceActorMsg(new DeviceDeleteMsg(tenantId, deviceId), true); deletedDevices.add(deviceId); } - if (isRuleEngine && ruleChainsInitialized) { - TbActorRef target = getEntityActorRef(msg.getEntityId()); - if (target != null) { - if (msg.getEntityId().getEntityType() == EntityType.RULE_CHAIN) { - RuleChain ruleChain = systemContext.getRuleChainService(). - findRuleChainById(tenantId, new RuleChainId(msg.getEntityId().getId())); - if (ruleChain != null && RuleChainType.CORE.equals(ruleChain.getType())) { - visit(ruleChain, target); + if (isRuleEngine) { + if (ruleChainsInitialized) { + TbActorRef target = getEntityActorRef(msg.getEntityId()); + if (target != null) { + if (msg.getEntityId().getEntityType() == EntityType.RULE_CHAIN) { + RuleChain ruleChain = systemContext.getRuleChainService(). + findRuleChainById(tenantId, new RuleChainId(msg.getEntityId().getId())); + if (ruleChain != null && RuleChainType.CORE.equals(ruleChain.getType())) { + visit(ruleChain, target); + } } + target.tellWithHighPriority(msg); + } else { + log.debug("[{}] Invalid component lifecycle msg: {}", tenantId, msg); + } + } + if (cfActor != null) { + if (msg.getEntityId().getEntityType().isOneOf(EntityType.CALCULATED_FIELD, EntityType.DEVICE, EntityType.ASSET)) { + cfActor.tellWithHighPriority(new CalculatedFieldEntityLifecycleMsg(tenantId, msg)); } - target.tellWithHighPriority(msg); - } else { - log.debug("[{}] Invalid component lifecycle msg: {}", tenantId, msg); } } } diff --git a/application/src/main/java/org/thingsboard/server/service/queue/DefaultTbCalculatedFieldConsumerService.java b/application/src/main/java/org/thingsboard/server/service/queue/DefaultTbCalculatedFieldConsumerService.java index 3232a9b89f..04aab6b39d 100644 --- a/application/src/main/java/org/thingsboard/server/service/queue/DefaultTbCalculatedFieldConsumerService.java +++ b/application/src/main/java/org/thingsboard/server/service/queue/DefaultTbCalculatedFieldConsumerService.java @@ -28,15 +28,12 @@ import org.thingsboard.server.common.data.DataConstants; import org.thingsboard.server.common.data.id.EntityIdFactory; import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.common.data.queue.QueueConfig; -import org.thingsboard.server.common.msg.cf.CalculatedFieldEntityLifecycleMsg; import org.thingsboard.server.common.msg.cf.CalculatedFieldPartitionChangeMsg; import org.thingsboard.server.common.msg.queue.ServiceType; import org.thingsboard.server.common.msg.queue.TbCallback; -import org.thingsboard.server.common.util.ProtoUtils; import org.thingsboard.server.dao.tenant.TbTenantProfileCache; import org.thingsboard.server.gen.transport.TransportProtos.CalculatedFieldLinkedTelemetryMsgProto; import org.thingsboard.server.gen.transport.TransportProtos.CalculatedFieldTelemetryMsgProto; -import org.thingsboard.server.gen.transport.TransportProtos.ComponentLifecycleMsgProto; import org.thingsboard.server.gen.transport.TransportProtos.ToCalculatedFieldMsg; import org.thingsboard.server.gen.transport.TransportProtos.ToCalculatedFieldNotificationMsg; import org.thingsboard.server.queue.TbQueueConsumer; @@ -65,8 +62,6 @@ import java.util.concurrent.Future; import java.util.concurrent.TimeUnit; import java.util.stream.Collectors; -import static org.thingsboard.server.common.util.ProtoUtils.fromProto; - @Service @TbRuleEngineComponent @Slf4j @@ -164,9 +159,6 @@ public class DefaultTbCalculatedFieldConsumerService extends AbstractConsumerSer forwardToActorSystem(toCfMsg.getTelemetryMsg(), callback); } else if (toCfMsg.hasLinkedTelemetryMsg()) { forwardToActorSystem(toCfMsg.getLinkedTelemetryMsg(), callback); - } else if (toCfMsg.hasComponentLifecycleMsg()) { - log.trace("[{}] Forwarding component lifecycle message for processing {}", id, toCfMsg.getComponentLifecycleMsg()); - forwardToActorSystem(toCfMsg.getComponentLifecycleMsg(), callback); } } catch (Throwable e) { log.warn("[{}] Failed to process message: {}", id, msg, e); @@ -215,11 +207,7 @@ public class DefaultTbCalculatedFieldConsumerService extends AbstractConsumerSer @Override protected void handleNotification(UUID id, TbProtoQueueMsg msg, TbCallback callback) { ToCalculatedFieldNotificationMsg toCfNotification = msg.getValue(); - if (toCfNotification.hasComponentLifecycleMsg()) { - handleComponentLifecycleMsg(id, ProtoUtils.fromProto(toCfNotification.getComponentLifecycleMsg())); - log.trace("[{}] Forwarding component lifecycle message for processing {}", id, toCfNotification.getComponentLifecycleMsg()); - forwardToActorSystem(toCfNotification.getComponentLifecycleMsg(), callback); - } else if (toCfNotification.hasLinkedTelemetryMsg()) { + if (toCfNotification.hasLinkedTelemetryMsg()) { forwardToActorSystem(toCfNotification.getLinkedTelemetryMsg(), callback); } } @@ -237,11 +225,6 @@ public class DefaultTbCalculatedFieldConsumerService extends AbstractConsumerSer actorContext.tell(new CalculatedFieldLinkedTelemetryMsg(tenantId, entityId, linkedMsg, callback)); } - private void forwardToActorSystem(ComponentLifecycleMsgProto proto, TbCallback callback) { - var msg = fromProto(proto); - actorContext.tell(new CalculatedFieldEntityLifecycleMsg(msg.getTenantId(), msg, callback)); - } - private TenantId toTenantId(long tenantIdMSB, long tenantIdLSB) { return TenantId.fromUUID(new UUID(tenantIdMSB, tenantIdLSB)); } diff --git a/application/src/main/java/org/thingsboard/server/service/queue/DefaultTbClusterService.java b/application/src/main/java/org/thingsboard/server/service/queue/DefaultTbClusterService.java index c7174469b0..92c48a5fed 100644 --- a/application/src/main/java/org/thingsboard/server/service/queue/DefaultTbClusterService.java +++ b/application/src/main/java/org/thingsboard/server/service/queue/DefaultTbClusterService.java @@ -94,10 +94,8 @@ import org.thingsboard.server.queue.common.MultipleTbQueueCallbackWrapper; import org.thingsboard.server.queue.common.TbProtoQueueMsg; import org.thingsboard.server.queue.common.TbRuleEngineProducerService; import org.thingsboard.server.queue.discovery.PartitionService; -import org.thingsboard.server.queue.discovery.QueueKey; import org.thingsboard.server.queue.discovery.TopicService; import org.thingsboard.server.queue.provider.TbQueueProducerProvider; -import org.thingsboard.server.service.cf.CalculatedFieldProcessingService; import org.thingsboard.server.service.gateway_device.GatewayNotificationsService; import org.thingsboard.server.service.ota.OtaPackageStateService; import org.thingsboard.server.service.profile.TbAssetProfileCache; @@ -146,10 +144,6 @@ public class DefaultTbClusterService implements TbClusterService { @Lazy private OtaPackageStateService otaPackageStateService; - @Autowired - @Lazy - private CalculatedFieldProcessingService calculatedFieldProcessingService; - private final TopicService topicService; private final TbDeviceProfileCache deviceProfileCache; private final TbAssetProfileCache assetProfileCache; @@ -369,13 +363,6 @@ public class DefaultTbClusterService implements TbClusterService { toRuleEngineMsgs.incrementAndGet(); // TODO: add separate counter when we will have new ServiceType.CALCULATED_FIELDS } - @Override - public void pushNotificationToCalculatedFields(TenantId tenantId, EntityId entityId, ToCalculatedFieldNotificationMsg msg, TbQueueCallback callback) { - TopicPartitionInfo tpi = partitionService.resolve(ServiceType.TB_RULE_ENGINE, DataConstants.CF_QUEUE_NAME, tenantId, entityId); - producerProvider.getCalculatedFieldsNotificationsMsgProducer().send(tpi, new TbProtoQueueMsg<>(UUID.randomUUID(), msg), callback); - toRuleEngineNfs.incrementAndGet(); - } - @Override public void broadcastEntityStateChangeEvent(TenantId tenantId, EntityId entityId, ComponentLifecycleEvent state) { log.trace("[{}] Processing {} state change event: {}", tenantId, entityId.getEntityType(), state); @@ -431,7 +418,6 @@ public class DefaultTbClusterService implements TbClusterService { public void onDeviceDeleted(TenantId tenantId, Device device, TbQueueCallback callback) { DeviceId deviceId = device.getId(); gatewayNotificationsService.onDeviceDeleted(device); - handleCalculatedFieldEntityDeleted(tenantId, deviceId); broadcastEntityDeleteToTransport(tenantId, deviceId, device.getName(), callback); sendDeviceStateServiceEvent(tenantId, deviceId, false, false, true); broadcastEntityStateChangeEvent(tenantId, deviceId, ComponentLifecycleEvent.DELETED); @@ -440,7 +426,6 @@ public class DefaultTbClusterService implements TbClusterService { @Override public void onAssetDeleted(TenantId tenantId, Asset asset, TbQueueCallback callback) { AssetId assetId = asset.getId(); - handleCalculatedFieldEntityDeleted(tenantId, assetId); broadcastEntityStateChangeEvent(tenantId, assetId, ComponentLifecycleEvent.DELETED); } @@ -604,6 +589,7 @@ public class DefaultTbClusterService implements TbClusterService { || (entityType.equals(EntityType.DEVICE) && msg.getEvent() == ComponentLifecycleEvent.UPDATED) || entityType.equals(EntityType.ENTITY_VIEW) || entityType.equals(EntityType.NOTIFICATION_RULE) + || entityType.equals(EntityType.CALCULATED_FIELD) ) { TbQueueProducer> toCoreNfProducer = producerProvider.getTbCoreNotificationsMsgProducer(); Set tbCoreServices = partitionService.getAllServiceIds(ServiceType.TB_CORE); @@ -658,38 +644,28 @@ public class DefaultTbClusterService implements TbClusterService { public void onDeviceUpdated(Device entity, Device old) { var created = old == null; broadcastEntityChangeToTransport(entity.getTenantId(), entity.getId(), entity, null); - if (old != null) { + + var msg = ComponentLifecycleMsg.builder() + .tenantId(entity.getTenantId()) + .entityId(entity.getId()) + .profileId(entity.getDeviceProfileId()) + .name(entity.getName()); + if (created) { + msg.event(ComponentLifecycleEvent.CREATED); + } else { boolean deviceNameChanged = !entity.getName().equals(old.getName()); if (deviceNameChanged) { gatewayNotificationsService.onDeviceUpdated(entity, old); } boolean deviceProfileChanged = !entity.getDeviceProfileId().equals(old.getDeviceProfileId()); - if (deviceProfileChanged) { - ComponentLifecycleMsg msg = ComponentLifecycleMsg.builder() - .tenantId(entity.getTenantId()) - .entityId(entity.getId()) - .event(ComponentLifecycleEvent.UPDATED) - .oldProfileId(old.getDeviceProfileId()) - .profileId(entity.getDeviceProfileId()) - .oldName(old.getName()) - .name(entity.getName()) - .build(); - broadcastToCalculatedFields(ToCalculatedFieldNotificationMsg.newBuilder().setComponentLifecycleMsg(toProto(msg)).build(), TbQueueCallback.EMPTY); - } if (deviceNameChanged || deviceProfileChanged) { pushMsgToCore(new DeviceNameOrTypeUpdateMsg(entity.getTenantId(), entity.getId(), entity.getName(), entity.getType()), null); } - } else { - ComponentLifecycleMsg msg = ComponentLifecycleMsg.builder() - .tenantId(entity.getTenantId()) - .entityId(entity.getId()) - .event(ComponentLifecycleEvent.CREATED) - .profileId(entity.getDeviceProfileId()) - .name(entity.getName()) - .build(); - broadcastToCalculatedFields(ToCalculatedFieldNotificationMsg.newBuilder().setComponentLifecycleMsg(toProto(msg)).build(), TbQueueCallback.EMPTY); + msg.event(ComponentLifecycleEvent.UPDATED) + .oldProfileId(old.getDeviceProfileId()) + .oldName(old.getName()); } - broadcastEntityStateChangeEvent(entity.getTenantId(), entity.getId(), created ? ComponentLifecycleEvent.CREATED : ComponentLifecycleEvent.UPDATED); + broadcast(msg.build()); sendDeviceStateServiceEvent(entity.getTenantId(), entity.getId(), created, !created, false); otaPackageStateService.update(entity, old); } @@ -697,48 +673,29 @@ public class DefaultTbClusterService implements TbClusterService { @Override public void onAssetUpdated(Asset entity, Asset old) { var created = old == null; - if (old != null) { - boolean assetTypeChanged = !entity.getAssetProfileId().equals(old.getAssetProfileId()); - if (assetTypeChanged) { - ComponentLifecycleMsg msg = ComponentLifecycleMsg.builder() - .tenantId(entity.getTenantId()) - .entityId(entity.getId()) - .event(ComponentLifecycleEvent.UPDATED) - .oldProfileId(old.getAssetProfileId()) - .profileId(entity.getAssetProfileId()) - .oldName(old.getName()) - .name(entity.getName()) - .build(); - broadcastToCalculatedFields(ToCalculatedFieldNotificationMsg.newBuilder().setComponentLifecycleMsg(toProto(msg)).build(), TbQueueCallback.EMPTY); - } + var msg = ComponentLifecycleMsg.builder() + .tenantId(entity.getTenantId()) + .entityId(entity.getId()) + .profileId(entity.getAssetProfileId()) + .name(entity.getName()); + if (created) { + msg.event(ComponentLifecycleEvent.CREATED); } else { - ComponentLifecycleMsg msg = ComponentLifecycleMsg.builder() - .tenantId(entity.getTenantId()) - .entityId(entity.getId()) - .event(ComponentLifecycleEvent.CREATED) - .profileId(entity.getAssetProfileId()) - .name(entity.getName()) - .build(); - broadcastToCalculatedFields(ToCalculatedFieldNotificationMsg.newBuilder().setComponentLifecycleMsg(toProto(msg)).build(), TbQueueCallback.EMPTY); + msg.event(ComponentLifecycleEvent.UPDATED) + .oldProfileId(old.getAssetProfileId()) + .oldName(old.getName()); } - broadcastEntityStateChangeEvent(entity.getTenantId(), entity.getId(), created ? ComponentLifecycleEvent.CREATED : ComponentLifecycleEvent.UPDATED); + broadcast(msg.build()); } @Override public void onCalculatedFieldUpdated(CalculatedField calculatedField, CalculatedField oldCalculatedField, TbQueueCallback callback) { - var msg = toProto(new ComponentLifecycleMsg(calculatedField.getTenantId(), calculatedField.getId(), oldCalculatedField == null ? ComponentLifecycleEvent.CREATED : ComponentLifecycleEvent.UPDATED)); - onCalculatedFieldLifecycleMsg(msg, callback); + broadcastEntityStateChangeEvent(calculatedField.getTenantId(), calculatedField.getId(), oldCalculatedField == null ? ComponentLifecycleEvent.CREATED : ComponentLifecycleEvent.UPDATED); } @Override public void onCalculatedFieldDeleted(CalculatedField calculatedField, TbQueueCallback callback) { - var msg = toProto(new ComponentLifecycleMsg(calculatedField.getTenantId(), calculatedField.getId(), ComponentLifecycleEvent.DELETED)); - onCalculatedFieldLifecycleMsg(msg, callback); - } - - private void onCalculatedFieldLifecycleMsg(ComponentLifecycleMsgProto msg, TbQueueCallback callback) { - broadcastToCalculatedFields(ToCalculatedFieldNotificationMsg.newBuilder().setComponentLifecycleMsg(msg).build(), callback); - broadcastToCore(ToCoreNotificationMsg.newBuilder().setComponentLifecycle(msg).build()); + broadcastEntityStateChangeEvent(calculatedField.getTenantId(), calculatedField.getId(), ComponentLifecycleEvent.DELETED); } @Override @@ -868,8 +825,4 @@ public class DefaultTbClusterService implements TbClusterService { } } - private void handleCalculatedFieldEntityDeleted(TenantId tenantId, EntityId entityId) { - ComponentLifecycleMsg msg = new ComponentLifecycleMsg(tenantId, entityId, ComponentLifecycleEvent.DELETED); - broadcastToCalculatedFields(ToCalculatedFieldNotificationMsg.newBuilder().setComponentLifecycleMsg(toProto(msg)).build(), TbQueueCallback.EMPTY); - } } diff --git a/application/src/main/java/org/thingsboard/server/service/queue/DefaultTbRuleEngineConsumerService.java b/application/src/main/java/org/thingsboard/server/service/queue/DefaultTbRuleEngineConsumerService.java index d522f11f7b..9cc743e510 100644 --- a/application/src/main/java/org/thingsboard/server/service/queue/DefaultTbRuleEngineConsumerService.java +++ b/application/src/main/java/org/thingsboard/server/service/queue/DefaultTbRuleEngineConsumerService.java @@ -29,7 +29,6 @@ import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.common.data.plugin.ComponentLifecycleEvent; import org.thingsboard.server.common.data.queue.Queue; import org.thingsboard.server.common.data.rpc.RpcError; -import org.thingsboard.server.common.data.util.CollectionsUtil; import org.thingsboard.server.common.msg.plugin.ComponentLifecycleMsg; import org.thingsboard.server.common.msg.queue.ServiceType; import org.thingsboard.server.common.msg.queue.TbCallback; @@ -234,7 +233,7 @@ public class DefaultTbRuleEngineConsumerService extends AbstractConsumerService< if (event.getEvent() == ComponentLifecycleEvent.DELETED) { List toRemove = consumers.keySet().stream() .filter(queueKey -> queueKey.getTenantId().equals(event.getTenantId())) - .collect(Collectors.toList()); + .toList(); toRemove.forEach(queueKey -> { removeConsumer(queueKey).ifPresent(consumer -> consumer.delete(false)); }); diff --git a/common/cluster-api/src/main/java/org/thingsboard/server/cluster/TbClusterService.java b/common/cluster-api/src/main/java/org/thingsboard/server/cluster/TbClusterService.java index 2a2d04e0fd..aed6eb4cf5 100644 --- a/common/cluster-api/src/main/java/org/thingsboard/server/cluster/TbClusterService.java +++ b/common/cluster-api/src/main/java/org/thingsboard/server/cluster/TbClusterService.java @@ -85,8 +85,6 @@ public interface TbClusterService extends TbQueueClusterService { void pushMsgToCalculatedFields(TopicPartitionInfo tpi, UUID msgId, ToCalculatedFieldMsg msg, TbQueueCallback callback); - void pushNotificationToCalculatedFields(TenantId tenantId, EntityId entityId, TransportProtos.ToCalculatedFieldNotificationMsg msg, TbQueueCallback callback); - void broadcastEntityStateChangeEvent(TenantId tenantId, EntityId entityId, ComponentLifecycleEvent state); void onDeviceProfileChange(DeviceProfile deviceProfile, DeviceProfile oldDeviceProfile, TbQueueCallback callback); diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/EntityType.java b/common/data/src/main/java/org/thingsboard/server/common/data/EntityType.java index 4ea66be1b4..1c56aa25dd 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/EntityType.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/EntityType.java @@ -86,4 +86,15 @@ public enum EntityType { this.tableName = tableName; } + public boolean isOneOf(EntityType... types) { + if (types == null) { + return false; + } + for (EntityType type : types) { + if (this == type) { + return true; + } + } + return false; + } } diff --git a/common/message/src/main/java/org/thingsboard/server/common/msg/cf/CalculatedFieldEntityLifecycleMsg.java b/common/message/src/main/java/org/thingsboard/server/common/msg/cf/CalculatedFieldEntityLifecycleMsg.java index 099240f54d..1eb8f425ab 100644 --- a/common/message/src/main/java/org/thingsboard/server/common/msg/cf/CalculatedFieldEntityLifecycleMsg.java +++ b/common/message/src/main/java/org/thingsboard/server/common/msg/cf/CalculatedFieldEntityLifecycleMsg.java @@ -16,19 +16,16 @@ package org.thingsboard.server.common.msg.cf; import lombok.Data; -import org.thingsboard.server.common.data.cf.CalculatedField; import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.common.msg.MsgType; import org.thingsboard.server.common.msg.ToCalculatedFieldSystemMsg; import org.thingsboard.server.common.msg.plugin.ComponentLifecycleMsg; -import org.thingsboard.server.common.msg.queue.TbCallback; @Data public class CalculatedFieldEntityLifecycleMsg implements ToCalculatedFieldSystemMsg { private final TenantId tenantId; private final ComponentLifecycleMsg data; - private final TbCallback callback; @Override public MsgType getMsgType() { diff --git a/common/proto/src/main/proto/queue.proto b/common/proto/src/main/proto/queue.proto index 6ecdf13981..1b2edcc96d 100644 --- a/common/proto/src/main/proto/queue.proto +++ b/common/proto/src/main/proto/queue.proto @@ -1674,14 +1674,12 @@ message ToEdgeEventNotificationMsg { } message ToCalculatedFieldMsg { - ComponentLifecycleMsgProto componentLifecycleMsg = 1; - CalculatedFieldTelemetryMsgProto telemetryMsg = 2; - CalculatedFieldLinkedTelemetryMsgProto linkedTelemetryMsg = 3; + CalculatedFieldTelemetryMsgProto telemetryMsg = 1; + CalculatedFieldLinkedTelemetryMsgProto linkedTelemetryMsg = 2; } message ToCalculatedFieldNotificationMsg { - ComponentLifecycleMsgProto componentLifecycleMsg = 1; - CalculatedFieldLinkedTelemetryMsgProto linkedTelemetryMsg = 2; + CalculatedFieldLinkedTelemetryMsgProto linkedTelemetryMsg = 1; } /* Messages that are handled by ThingsBoard RuleEngine Service */ diff --git a/common/queue/src/main/java/org/thingsboard/server/queue/discovery/event/PartitionChangeEvent.java b/common/queue/src/main/java/org/thingsboard/server/queue/discovery/event/PartitionChangeEvent.java index f165f60be7..32537edba5 100644 --- a/common/queue/src/main/java/org/thingsboard/server/queue/discovery/event/PartitionChangeEvent.java +++ b/common/queue/src/main/java/org/thingsboard/server/queue/discovery/event/PartitionChangeEvent.java @@ -24,7 +24,6 @@ import org.thingsboard.server.queue.discovery.QueueKey; import java.io.Serial; import java.util.Collection; -import java.util.Collections; import java.util.Map; import java.util.Set; import java.util.stream.Collectors; From a8ae13faea3f3552c9b4948be4fa113685c81dac Mon Sep 17 00:00:00 2001 From: Igor Kulikov Date: Thu, 13 Mar 2025 18:07:41 +0200 Subject: [PATCH 321/345] UI: Map widget - fix map layers load event. --- ui-ngx/package.json | 2 +- .../widget/lib/maps/leaflet/leaflet-tb.ts | 3 +++ .../home/components/widget/lib/maps/map-layer.ts | 14 ++++++++++++++ ui-ngx/yarn.lock | 8 ++++---- 4 files changed, 22 insertions(+), 5 deletions(-) diff --git a/ui-ngx/package.json b/ui-ngx/package.json index 3323d15b56..1a32c7d41c 100644 --- a/ui-ngx/package.json +++ b/ui-ngx/package.json @@ -61,7 +61,7 @@ "leaflet": "1.9.4", "leaflet-polylinedecorator": "1.6.0", "leaflet-providers": "2.0.0", - "leaflet.gridlayer.googlemutant": "0.14.1", + "leaflet.gridlayer.googlemutant": "0.15.0", "leaflet.markercluster": "1.5.3", "libphonenumber-js": "^1.11.15", "maplibre-gl": "^5.2.0", diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/maps/leaflet/leaflet-tb.ts b/ui-ngx/src/app/modules/home/components/widget/lib/maps/leaflet/leaflet-tb.ts index 95b8932db0..79e2f11ce6 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/maps/leaflet/leaflet-tb.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/maps/leaflet/leaflet-tb.ts @@ -772,6 +772,9 @@ class MapLibreGLLayer extends L.Layer implements TB.MapLibreGL.MapLibreGLLayer { attributionControl: false }); this._glMap = new MapLibreGLMap(options); + this._glMap.once('load', () => { + this.fire('load'); + }); this._glMap.setMaxBounds(null); this._transformGL(this._glMap); this._actualCanvas = this._glMap._canvas; diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/maps/map-layer.ts b/ui-ngx/src/app/modules/home/components/widget/lib/maps/map-layer.ts index b40abd1d66..b3a41ec2f3 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/maps/map-layer.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/maps/map-layer.ts @@ -116,8 +116,22 @@ export abstract class TbMapLayer { map((referenceLayer) => { if (referenceLayer) { const layer = L.featureGroup(); + let baseLayerLoaded = false; + let referenceLayerLoaded = false; baseLayer.addTo(layer); referenceLayer.addTo(layer); + baseLayer.once('load', () => { + baseLayerLoaded = true; + if (referenceLayerLoaded) { + layer.fire('load'); + } + }); + referenceLayer.once('load', () => { + referenceLayerLoaded = true; + if (baseLayerLoaded) { + layer.fire('load'); + } + }); return { layer, attribution: !!baseLayer.getAttribution() || !!referenceLayer.getAttribution() diff --git a/ui-ngx/yarn.lock b/ui-ngx/yarn.lock index b3cc13904a..04e0c4f77a 100644 --- a/ui-ngx/yarn.lock +++ b/ui-ngx/yarn.lock @@ -6667,10 +6667,10 @@ leaflet-rotatedmarker@^0.2.0: resolved "https://registry.yarnpkg.com/leaflet-rotatedmarker/-/leaflet-rotatedmarker-0.2.0.tgz#4467f49f98d1bfd56959bd9c6705203dd2601277" integrity sha512-yc97gxLXwbZa+Gk9VCcqI0CkvIBC9oNTTjFsHqq4EQvANrvaboib4UdeQLyTnEqDpaXHCqzwwVIDHtvz2mUiDg== -leaflet.gridlayer.googlemutant@0.14.1: - version "0.14.1" - resolved "https://registry.yarnpkg.com/leaflet.gridlayer.googlemutant/-/leaflet.gridlayer.googlemutant-0.14.1.tgz#c282209aa1a39eb2f87d8aaa4e9894181c9e20a0" - integrity sha512-/OYxEjmgxO1U1KOhTzg+m8c0b95J0943LU8DXQmdJu/x2f+1Ur78rvEPO2QCS0cmwZ3m6FvE5I3zXnBzJNWRCA== +leaflet.gridlayer.googlemutant@0.15.0: + version "0.15.0" + resolved "https://registry.yarnpkg.com/leaflet.gridlayer.googlemutant/-/leaflet.gridlayer.googlemutant-0.15.0.tgz#7a32d949578695b8aa8fa5fd11b40bd7a6ce6c23" + integrity sha512-kA5jCOBhCPigyue6YpZMhXMVYA1hM2pDIaJ6u0wSSiSZ2TQ4kZfKuhwkIexcadJfs0BP4FyhZIDZC7hmTlvjOA== leaflet.markercluster@1.5.3: version "1.5.3" From 58a5b9f274473efd1b228ef9f6dabeb4289f0976 Mon Sep 17 00:00:00 2001 From: Vladyslav Prykhodko Date: Thu, 13 Mar 2025 20:41:30 +0200 Subject: [PATCH 322/345] UI: Fixed widget-container.component duplicate import --- .../home/components/widget/widget-container.component.ts | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/ui-ngx/src/app/modules/home/components/widget/widget-container.component.ts b/ui-ngx/src/app/modules/home/components/widget/widget-container.component.ts index 72252ada2c..430cce40b1 100644 --- a/ui-ngx/src/app/modules/home/components/widget/widget-container.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/widget-container.component.ts @@ -44,11 +44,10 @@ import { GridsterItemComponent } from 'angular-gridster2'; import { UtilsService } from '@core/services/utils.service'; import { from } from 'rxjs'; import { DashboardUtilsService } from '@core/services/dashboard-utils.service'; -import ITooltipsterInstance = JQueryTooltipster.ITooltipsterInstance; -import ITooltipsterGeoHelper = JQueryTooltipster.ITooltipsterGeoHelper; import { TbContextMenuEvent } from '@shared/models/jquery-event.models'; import { WidgetHeaderActionButtonType } from '@shared/models/widget.models'; import ITooltipsterInstance = JQueryTooltipster.ITooltipsterInstance; +import ITooltipsterGeoHelper = JQueryTooltipster.ITooltipsterGeoHelper; export enum WidgetComponentActionType { MOUSE_DOWN, From 2833b69145a9b13cd50431446c0b5ee767ee122f Mon Sep 17 00:00:00 2001 From: Artem Dzhereleiko Date: Fri, 14 Mar 2025 09:15:36 +0200 Subject: [PATCH 323/345] UI: Improvent for key event for rule chain page for Mac --- .../home/pages/rulechain/rulechain-page.component.ts | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/ui-ngx/src/app/modules/home/pages/rulechain/rulechain-page.component.ts b/ui-ngx/src/app/modules/home/pages/rulechain/rulechain-page.component.ts index d927f64fdd..2ac9b806e6 100644 --- a/ui-ngx/src/app/modules/home/pages/rulechain/rulechain-page.component.ts +++ b/ui-ngx/src/app/modules/home/pages/rulechain/rulechain-page.component.ts @@ -369,7 +369,7 @@ export class RuleChainPageComponent extends PageComponent private initHotKeys(): void { if (!this.hotKeys.length) { this.hotKeys.push( - new Hotkey('ctrl+a', (event: KeyboardEvent) => { + new Hotkey(['ctrl+a', 'meta+a'], (event: KeyboardEvent) => { if (this.enableHotKeys) { event.preventDefault(); this.ruleChainCanvas.modelService.selectAll(); @@ -380,7 +380,7 @@ export class RuleChainPageComponent extends PageComponent this.translate.instant('rulenode.select-all-objects')) ); this.hotKeys.push( - new Hotkey('ctrl+c', (event: KeyboardEvent) => { + new Hotkey(['ctrl+c', 'meta+c'], (event: KeyboardEvent) => { if (this.enableHotKeys) { event.preventDefault(); this.copyRuleNodes(); @@ -391,7 +391,7 @@ export class RuleChainPageComponent extends PageComponent this.translate.instant('rulenode.copy-selected')) ); this.hotKeys.push( - new Hotkey('ctrl+v', (event: KeyboardEvent) => { + new Hotkey(['ctrl+v', 'meta+v'], (event: KeyboardEvent) => { if (this.enableHotKeys) { event.preventDefault(); if (this.itembuffer.hasRuleNodes()) { @@ -416,7 +416,7 @@ export class RuleChainPageComponent extends PageComponent this.translate.instant('rulenode.deselect-all-objects')) ); this.hotKeys.push( - new Hotkey('ctrl+s', (event: KeyboardEvent) => { + new Hotkey(['ctrl+s', 'meta+s'], (event: KeyboardEvent) => { if (this.enableHotKeys) { event.preventDefault(); this.saveRuleChain(); @@ -427,7 +427,7 @@ export class RuleChainPageComponent extends PageComponent this.translate.instant('action.apply')) ); this.hotKeys.push( - new Hotkey('ctrl+z', (event: KeyboardEvent) => { + new Hotkey(['ctrl+z', 'meta+z'], (event: KeyboardEvent) => { if (this.enableHotKeys) { event.preventDefault(); this.revertRuleChain(); @@ -449,7 +449,7 @@ export class RuleChainPageComponent extends PageComponent this.translate.instant('rulenode.delete-selected-objects')) ); this.hotKeys.push( - new Hotkey('ctrl+r', (event: KeyboardEvent) => { + new Hotkey(['ctrl+r', 'meta+r'], (event: KeyboardEvent) => { if (this.enableHotKeys && this.canCreateNestedRuleChain()) { event.preventDefault(); this.createNestedRuleChain(); From 78fc8445a7a41ee19d322386eba7c5678a839917 Mon Sep 17 00:00:00 2001 From: Vladyslav_Prykhodko Date: Fri, 14 Mar 2025 11:32:38 +0200 Subject: [PATCH 324/345] UI: Refactoring calculate fields models refactoring --- .../calculated-fields-table-config.ts | 15 +++++++------- .../calculated-field-dialog.component.ts | 16 ++++++++++++++- ...ated-field-script-test-dialog.component.ts | 10 +++++++++- .../shared/models/calculated-field.models.ts | 20 +------------------ 4 files changed, 32 insertions(+), 29 deletions(-) diff --git a/ui-ngx/src/app/modules/home/components/calculated-fields/calculated-fields-table-config.ts b/ui-ngx/src/app/modules/home/components/calculated-fields/calculated-fields-table-config.ts index 5689f176e9..5e7d532623 100644 --- a/ui-ngx/src/app/modules/home/components/calculated-fields/calculated-fields-table-config.ts +++ b/ui-ngx/src/app/modules/home/components/calculated-fields/calculated-fields-table-config.ts @@ -39,28 +39,27 @@ import { catchError, filter, switchMap, tap } from 'rxjs/operators'; import { ArgumentType, CalculatedField, - CalculatedFieldEventArguments, CalculatedFieldDebugDialogData, - CalculatedFieldDialogData, - CalculatedFieldTestScriptDialogData, + CalculatedFieldEventArguments, + CalculatedFieldType, + CalculatedFieldTypeTranslations, getCalculatedFieldArgumentsEditorCompleter, getCalculatedFieldArgumentsHighlights, - CalculatedFieldTypeTranslations, - CalculatedFieldType, } from '@shared/models/calculated-field.models'; import { CalculatedFieldDebugDialogComponent, CalculatedFieldDialogComponent, - CalculatedFieldScriptTestDialogComponent + CalculatedFieldDialogData, + CalculatedFieldScriptTestDialogComponent, + CalculatedFieldTestScriptDialogData } from './components/public-api'; import { ImportExportService } from '@shared/import-export/import-export.service'; import { isObject } from '@core/utils'; import { EntityDebugSettingsService } from '@home/components/entity/debug/entity-debug-settings.service'; import { DatePipe } from '@angular/common'; -export class CalculatedFieldsTableConfig extends EntityTableConfig { +export class CalculatedFieldsTableConfig extends EntityTableConfig { - // TODO: [Calculated Fields] remove hardcode when BE variable implemented readonly calculatedFieldsDebugPerTenantLimitsConfiguration = getCurrentAuthState(this.store)['calculatedFieldsDebugPerTenantLimitsConfiguration'] || '1:1'; readonly maxDebugModeDuration = getCurrentAuthState(this.store).maxDebugModeDurationMinutes * MINUTE; diff --git a/ui-ngx/src/app/modules/home/components/calculated-fields/components/dialog/calculated-field-dialog.component.ts b/ui-ngx/src/app/modules/home/components/calculated-fields/components/dialog/calculated-field-dialog.component.ts index 0c3e7464cd..631a55f0f6 100644 --- a/ui-ngx/src/app/modules/home/components/calculated-fields/components/dialog/calculated-field-dialog.component.ts +++ b/ui-ngx/src/app/modules/home/components/calculated-fields/components/dialog/calculated-field-dialog.component.ts @@ -25,7 +25,7 @@ import { CalculatedField, CalculatedFieldConfiguration, calculatedFieldDefaultScript, - CalculatedFieldDialogData, + CalculatedFieldTestScriptFn, CalculatedFieldType, CalculatedFieldTypeTranslations, getCalculatedFieldArgumentsEditorCompleter, @@ -41,6 +41,20 @@ import { takeUntilDestroyed } from '@angular/core/rxjs-interop'; import { ScriptLanguage } from '@shared/models/rule-node.models'; import { CalculatedFieldsService } from '@core/http/calculated-fields.service'; import { Observable } from 'rxjs'; +import { EntityId } from '@shared/models/id/entity-id'; +import { AdditionalDebugActionConfig } from '@home/components/entity/debug/entity-debug-settings.model'; + +export interface CalculatedFieldDialogData { + value?: CalculatedField; + buttonTitle: string; + entityId: EntityId; + debugLimitsConfiguration: string; + tenantId: string; + entityName?: string; + additionalDebugActionConfig: AdditionalDebugActionConfig<(calculatedField: CalculatedField) => void>; + getTestScriptDialogFn: CalculatedFieldTestScriptFn; + isDirty?: boolean; +} @Component({ selector: 'tb-calculated-field-dialog', diff --git a/ui-ngx/src/app/modules/home/components/calculated-fields/components/test-dialog/calculated-field-script-test-dialog.component.ts b/ui-ngx/src/app/modules/home/components/calculated-fields/components/test-dialog/calculated-field-script-test-dialog.component.ts index 78c3b17bcb..fbc389939f 100644 --- a/ui-ngx/src/app/modules/home/components/calculated-fields/components/test-dialog/calculated-field-script-test-dialog.component.ts +++ b/ui-ngx/src/app/modules/home/components/calculated-fields/components/test-dialog/calculated-field-script-test-dialog.component.ts @@ -41,9 +41,17 @@ import { filter } from 'rxjs/operators'; import { ArgumentType, CalculatedFieldEventArguments, - CalculatedFieldTestScriptDialogData, + CalculatedFieldTestScriptInputParams, TestArgumentTypeMap } from '@shared/models/calculated-field.models'; +import { TbEditorCompleter } from '@shared/models/ace/completion.models'; +import { AceHighlightRules } from '@shared/models/ace/ace.models'; + +export interface CalculatedFieldTestScriptDialogData extends CalculatedFieldTestScriptInputParams { + argumentsEditorCompleter: TbEditorCompleter; + argumentsHighlightRules: AceHighlightRules; + openCalculatedFieldEdit?: boolean; +} @Component({ selector: 'tb-calculated-field-script-test-dialog', diff --git a/ui-ngx/src/app/shared/models/calculated-field.models.ts b/ui-ngx/src/app/shared/models/calculated-field.models.ts index c7b37e46f5..8afd280f3a 100644 --- a/ui-ngx/src/app/shared/models/calculated-field.models.ts +++ b/ui-ngx/src/app/shared/models/calculated-field.models.ts @@ -149,18 +149,6 @@ export interface CalculatedFieldArgumentValue extends CalculatedFieldArgument { export type CalculatedFieldTestScriptFn = (calculatedField: CalculatedField, argumentsObj?: Record, closeAllOnSave?: boolean) => Observable; -export interface CalculatedFieldDialogData { - value?: CalculatedField; - buttonTitle: string; - entityId: EntityId; - debugLimitsConfiguration: string; - tenantId: string; - entityName?: string; - additionalDebugActionConfig: AdditionalDebugActionConfig<(calculatedField: CalculatedField) => void>; - getTestScriptDialogFn: CalculatedFieldTestScriptFn; - isDirty?: boolean; -} - export interface CalculatedFieldDebugDialogData { tenantId: string; value: CalculatedField; @@ -172,12 +160,6 @@ export interface CalculatedFieldTestScriptInputParams { expression: string; } -export interface CalculatedFieldTestScriptDialogData extends CalculatedFieldTestScriptInputParams { - argumentsEditorCompleter: TbEditorCompleter; - argumentsHighlightRules: AceHighlightRules; - openCalculatedFieldEdit?: boolean; -} - export interface ArgumentEntityTypeParams { title: string; entityType: EntityType @@ -560,7 +542,7 @@ const calculatedFieldSingleArgumentValueHighlightRules: AceHighlightRules = { } const calculatedFieldRollingArgumentValueFunctionsHighlightRules: Array = - ['max', 'min', 'avg', 'mean', 'std', 'median', 'count', 'last', 'first', 'sum', 'merge', 'mergeAll'].map(funcName => ({ + Object.keys(CalculatedFieldRollingValueArgumentFunctionsAutocomplete).map(funcName => ({ token: 'tb.calculated-field-func', regex: `\\b${funcName}\\b`, next: 'no_regex' From faff769cda3d455de24269a85f9583cdbc04d57f Mon Sep 17 00:00:00 2001 From: Vladyslav_Prykhodko Date: Fri, 14 Mar 2025 11:34:13 +0200 Subject: [PATCH 325/345] UI: Refactoring calculate fields models refactoring --- ui-ngx/src/app/shared/models/calculated-field.models.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/ui-ngx/src/app/shared/models/calculated-field.models.ts b/ui-ngx/src/app/shared/models/calculated-field.models.ts index 8afd280f3a..c124e39a32 100644 --- a/ui-ngx/src/app/shared/models/calculated-field.models.ts +++ b/ui-ngx/src/app/shared/models/calculated-field.models.ts @@ -33,7 +33,6 @@ import { dotOperatorHighlightRule, endGroupHighlightRule } from '@shared/models/ace/ace.models'; -import { AdditionalDebugActionConfig } from '@home/components/entity/debug/entity-debug-settings.model'; export interface CalculatedField extends Omit, 'label'>, HasVersion, HasEntityDebugSettings, HasTenantId, ExportableEntity { configuration: CalculatedFieldConfiguration; From babda72b1edcc0ea790bb3283dfaf3fc59ad9e45 Mon Sep 17 00:00:00 2001 From: Ekaterina Chantsova Date: Fri, 14 Mar 2025 11:49:13 +0200 Subject: [PATCH 326/345] Fixed date time period start/end values check --- .../time/datetime-period.component.ts | 20 +++++++++++-------- 1 file changed, 12 insertions(+), 8 deletions(-) diff --git a/ui-ngx/src/app/shared/components/time/datetime-period.component.ts b/ui-ngx/src/app/shared/components/time/datetime-period.component.ts index 251f9e03b9..99d3fdc661 100644 --- a/ui-ngx/src/app/shared/components/time/datetime-period.component.ts +++ b/ui-ngx/src/app/shared/components/time/datetime-period.component.ts @@ -160,22 +160,26 @@ export class DatetimePeriodComponent implements ControlValueAccessor { private onStartDateChange(startDate: Date) { if (startDate) { - if (startDate.getTime() > this.maxStartTs) { - this.dateTimePeriodFormGroup.get('startDate').patchValue(new Date(this.maxStartTs), { emitEvent: false }); + let startDateTs = startDate.getTime(); + if (startDateTs > this.maxStartTs) { + startDateTs = this.maxStartTs; + this.dateTimePeriodFormGroup.get('startDate').patchValue(new Date(startDateTs), { emitEvent: false }); } - if (startDate.getTime() > this.maxStartDateTs) { - this.dateTimePeriodFormGroup.get('endDate').patchValue(new Date(startDate.getTime() + this.timeShiftMs), { emitEvent: false }); + if (startDateTs > this.maxStartDateTs) { + this.dateTimePeriodFormGroup.get('endDate').patchValue(new Date(startDateTs + this.timeShiftMs), { emitEvent: false }); } } } private onEndDateChange(endDate: Date) { if (endDate) { - if (endDate.getTime() > this.maxEndTs) { - this.dateTimePeriodFormGroup.get('endDate').patchValue(new Date(this.maxEndTs), { emitEvent: false }); + let endDateTs = endDate.getTime(); + if (endDateTs > this.maxEndTs) { + endDateTs = this.maxEndTs; + this.dateTimePeriodFormGroup.get('endDate').patchValue(new Date(endDateTs), { emitEvent: false }); } - if (endDate.getTime() < this.minEndDateTs) { - this.dateTimePeriodFormGroup.get('startDate').patchValue(new Date(endDate.getTime() - this.timeShiftMs), { emitEvent: false }); + if (endDateTs < this.minEndDateTs) { + this.dateTimePeriodFormGroup.get('startDate').patchValue(new Date(endDateTs - this.timeShiftMs), { emitEvent: false }); } } } From 2fec80eef2543db18c6649a25066c5447aef4864 Mon Sep 17 00:00:00 2001 From: Vladyslav_Prykhodko Date: Fri, 14 Mar 2025 11:49:15 +0200 Subject: [PATCH 327/345] UI: Refactoring calculate fields models refactoring --- .../calculated-fields-table-config.ts | 3 +-- .../calculated-field-debug-dialog.component.ts | 12 +++++++++++- .../src/app/shared/models/calculated-field.models.ts | 6 ------ 3 files changed, 12 insertions(+), 9 deletions(-) diff --git a/ui-ngx/src/app/modules/home/components/calculated-fields/calculated-fields-table-config.ts b/ui-ngx/src/app/modules/home/components/calculated-fields/calculated-fields-table-config.ts index 5e7d532623..2c0418bd4f 100644 --- a/ui-ngx/src/app/modules/home/components/calculated-fields/calculated-fields-table-config.ts +++ b/ui-ngx/src/app/modules/home/components/calculated-fields/calculated-fields-table-config.ts @@ -39,7 +39,6 @@ import { catchError, filter, switchMap, tap } from 'rxjs/operators'; import { ArgumentType, CalculatedField, - CalculatedFieldDebugDialogData, CalculatedFieldEventArguments, CalculatedFieldType, CalculatedFieldTypeTranslations, @@ -47,7 +46,7 @@ import { getCalculatedFieldArgumentsHighlights, } from '@shared/models/calculated-field.models'; import { - CalculatedFieldDebugDialogComponent, + CalculatedFieldDebugDialogComponent, CalculatedFieldDebugDialogData, CalculatedFieldDialogComponent, CalculatedFieldDialogData, CalculatedFieldScriptTestDialogComponent, diff --git a/ui-ngx/src/app/modules/home/components/calculated-fields/components/debug-dialog/calculated-field-debug-dialog.component.ts b/ui-ngx/src/app/modules/home/components/calculated-fields/components/debug-dialog/calculated-field-debug-dialog.component.ts index 8618a11990..2b987e124a 100644 --- a/ui-ngx/src/app/modules/home/components/calculated-fields/components/debug-dialog/calculated-field-debug-dialog.component.ts +++ b/ui-ngx/src/app/modules/home/components/calculated-fields/components/debug-dialog/calculated-field-debug-dialog.component.ts @@ -22,7 +22,17 @@ import { Router } from '@angular/router'; import { DialogComponent } from '@shared/components/dialog.component'; import { CalculatedFieldEventBody, DebugEventType, EventType } from '@shared/models/event.models'; import { EventTableComponent } from '@home/components/event/event-table.component'; -import { CalculatedFieldDebugDialogData, CalculatedFieldType } from '@shared/models/calculated-field.models'; +import { + CalculatedField, + CalculatedFieldTestScriptFn, + CalculatedFieldType +} from '@shared/models/calculated-field.models'; + +export interface CalculatedFieldDebugDialogData { + tenantId: string; + value: CalculatedField; + getTestScriptDialogFn: CalculatedFieldTestScriptFn; +} @Component({ selector: 'tb-calculated-field-debug-dialog', diff --git a/ui-ngx/src/app/shared/models/calculated-field.models.ts b/ui-ngx/src/app/shared/models/calculated-field.models.ts index c124e39a32..9dface6a67 100644 --- a/ui-ngx/src/app/shared/models/calculated-field.models.ts +++ b/ui-ngx/src/app/shared/models/calculated-field.models.ts @@ -148,12 +148,6 @@ export interface CalculatedFieldArgumentValue extends CalculatedFieldArgument { export type CalculatedFieldTestScriptFn = (calculatedField: CalculatedField, argumentsObj?: Record, closeAllOnSave?: boolean) => Observable; -export interface CalculatedFieldDebugDialogData { - tenantId: string; - value: CalculatedField; - getTestScriptDialogFn: CalculatedFieldTestScriptFn; -} - export interface CalculatedFieldTestScriptInputParams { arguments: CalculatedFieldEventArguments; expression: string; From 08556ee697b2019f2171b032c3da88b5a19ef8d0 Mon Sep 17 00:00:00 2001 From: Artem Dzhereleiko Date: Fri, 14 Mar 2025 12:03:15 +0200 Subject: [PATCH 328/345] UI: Refactoring tbel utils autocompletes and highlights --- .../shared/models/ace/tbel-utils.models.ts | 72 ++----------------- 1 file changed, 6 insertions(+), 66 deletions(-) diff --git a/ui-ngx/src/app/shared/models/ace/tbel-utils.models.ts b/ui-ngx/src/app/shared/models/ace/tbel-utils.models.ts index f0a04b4762..f29ae083be 100644 --- a/ui-ngx/src/app/shared/models/ace/tbel-utils.models.ts +++ b/ui-ngx/src/app/shared/models/ace/tbel-utils.models.ts @@ -15,9 +15,9 @@ /// import { AceHighlightRule } from '@shared/models/ace/ace.models'; -import { TbEditorCompleter } from '@shared/models/ace/completion.models'; +import { TbEditorCompleter, TbEditorCompletions } from '@shared/models/ace/completion.models'; -export const tbelUtilsAutocompletes = new TbEditorCompleter({ +const tbelEditorCompletions:TbEditorCompletions = { btoa: { meta: 'function', description: 'Encodes a string to Base64.', @@ -1245,71 +1245,11 @@ export const tbelUtilsAutocompletes = new TbEditorCompleter({ type: 'boolean' } }, -}); +} -const tbelUtilsFuncNames = [ - "btoa", - "atob", - "bytesToString", - "decodeToString", - "decodeToJson", - "stringToBytes", - "parseInt", - "parseLong", - "parseFloat", - "parseHexIntLongToFloat", - "parseDouble", - "parseLittleEndianHexToInt", - "parseBigEndianHexToInt", - "parseHexToInt", - "parseBytesToInt", - "parseLittleEndianHexToLong", - "parseBigEndianHexToLong", - "parseHexToLong", - "parseBytesToLong", - "parseLittleEndianHexToFloat", - "parseBigEndianHexToFloat", - "parseHexToFloat", - "parseBytesToFloat", - "parseBytesIntToFloat", - "parseLittleEndianHexToDouble", - "parseBigEndianHexToDouble", - "parseHexToDouble", - "parseBytesToDouble", - "parseBytesLongToDouble", - "toFixed", - "toInt", - "hexToBytes", - "hexToBytesArray", - "intToHex", - "longToHex", - "intLongToRadixString", - "floatToHex", - "doubleToHex", - "printUnsignedBytes", - "base64ToHex", - "hexToBase64", - "base64ToBytes", - "base64ToBytesList", - "bytesToBase64", - "bytesToHex", - "toFlatMap", - "encodeURI", - "decodeURI", - "raiseError", - "isBinary", - "isOctal", - "isDecimal", - "isHexadecimal", - "bytesToExecutionArrayList", - "padStart", - "padEnd", - "parseByteToBinaryArray", - "parseBytesToBinaryArray", - "parseLongToBinaryArray", - "parseBinaryArrayToInt", - "isNaN", -]; +export const tbelUtilsAutocompletes = new TbEditorCompleter(tbelEditorCompletions); + +const tbelUtilsFuncNames = Object.keys(tbelEditorCompletions); export const tbelUtilsFuncHighlightRules: Array = tbelUtilsFuncNames.map(funcName => ({ From 76c400f5f4ee771a387245d3327a471d08723fd3 Mon Sep 17 00:00:00 2001 From: Ekaterina Chantsova Date: Fri, 14 Mar 2025 12:11:31 +0200 Subject: [PATCH 329/345] Added link to help page for timewindow settings --- .../components/time/timewindow-config-dialog.component.html | 2 +- ui-ngx/src/app/shared/models/constants.ts | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/ui-ngx/src/app/shared/components/time/timewindow-config-dialog.component.html b/ui-ngx/src/app/shared/components/time/timewindow-config-dialog.component.html index 2b746e4d25..09bc049d32 100644 --- a/ui-ngx/src/app/shared/components/time/timewindow-config-dialog.component.html +++ b/ui-ngx/src/app/shared/components/time/timewindow-config-dialog.component.html @@ -19,7 +19,7 @@

        {{ 'timewindow.timewindow-settings' | translate }}

        - +