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 db65248a8f..5ea79e42a8 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 @@ -107,7 +107,7 @@ public class CalculatedFieldEntityMessageProcessor extends AbstractContextAwareM public void process(CalculatedFieldStateRestoreMsg msg) { CalculatedFieldId cfId = msg.getId().cfId(); - log.info("[{}] [{}] Processing CF state restore msg.", msg.getId().entityId(), cfId); + log.debug("[{}] [{}] Processing CF state restore msg.", msg.getId().entityId(), cfId); if (msg.getState() != null) { states.put(cfId, msg.getState()); } else { @@ -116,10 +116,10 @@ public class CalculatedFieldEntityMessageProcessor extends AbstractContextAwareM } public void process(EntityInitCalculatedFieldMsg msg) throws CalculatedFieldException { - log.info("[{}] Processing entity init CF msg.", msg.getCtx().getCfId()); + log.debug("[{}] Processing entity init CF msg.", msg.getCtx().getCfId()); var ctx = msg.getCtx(); if (msg.isForceReinit()) { - log.info("Force reinitialization of CF: [{}].", ctx.getCfId()); + log.debug("Force reinitialization of CF: [{}].", ctx.getCfId()); states.remove(ctx.getCfId()); } try { @@ -138,7 +138,7 @@ public class CalculatedFieldEntityMessageProcessor extends AbstractContextAwareM } public void process(CalculatedFieldEntityDeleteMsg msg) { - log.info("[{}] Processing CF entity delete msg.", msg.getEntityId()); + log.debug("[{}] Processing CF entity delete msg.", msg.getEntityId()); if (this.entityId.equals(msg.getEntityId())) { if (states.isEmpty()) { msg.getCallback().onSuccess(); @@ -244,7 +244,7 @@ public class CalculatedFieldEntityMessageProcessor extends AbstractContextAwareM private void processArgumentValuesUpdate(CalculatedFieldCtx ctx, List cfIdList, MultipleTbCallback callback, Map newArgValues, UUID tbMsgId, TbMsgType tbMsgType) throws CalculatedFieldException { if (newArgValues.isEmpty()) { - log.info("[{}] No new argument values to process for CF.", ctx.getCfId()); + log.debug("[{}] No new argument values to process for CF.", ctx.getCfId()); callback.onSuccess(CALLBACKS_PER_CF); } CalculatedFieldState state = states.get(ctx.getCfId()); 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 e109e83e4d..31152c7c98 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 @@ -236,12 +236,12 @@ public class CalculatedFieldManagerMessageProcessor extends AbstractContextAware private void onCfCreated(ComponentLifecycleMsg msg, TbCallback callback) throws CalculatedFieldException { var cfId = new CalculatedFieldId(msg.getEntityId().getId()); if (calculatedFields.containsKey(cfId)) { - log.warn("[{}] CF was already initialized [{}]", tenantId, cfId); + log.debug("[{}] CF was already initialized [{}]", tenantId, cfId); callback.onSuccess(); } else { var cf = cfDaoService.findById(msg.getTenantId(), cfId); if (cf == null) { - log.warn("[{}] Failed to lookup CF by id [{}]", tenantId, cfId); + log.debug("[{}] Failed to lookup CF by id [{}]", tenantId, cfId); callback.onSuccess(); } else { var cfCtx = new CalculatedFieldCtx(cf, systemContext.getTbelInvokeService(), systemContext.getApiLimitService()); @@ -268,7 +268,7 @@ public class CalculatedFieldManagerMessageProcessor extends AbstractContextAware } else { var newCf = cfDaoService.findById(msg.getTenantId(), cfId); if (newCf == null) { - log.warn("[{}] Failed to lookup CF by id [{}]", tenantId, cfId); + log.debug("[{}] Failed to lookup CF by id [{}]", tenantId, cfId); callback.onSuccess(); } else { var newCfCtx = new CalculatedFieldCtx(newCf, systemContext.getTbelInvokeService(), systemContext.getApiLimitService()); @@ -313,7 +313,7 @@ public class CalculatedFieldManagerMessageProcessor extends AbstractContextAware var cfId = new CalculatedFieldId(msg.getEntityId().getId()); var cfCtx = calculatedFields.remove(cfId); if (cfCtx == null) { - log.warn("[{}] CF was already deleted [{}]", tenantId, cfId); + log.debug("[{}] CF was already deleted [{}]", tenantId, cfId); callback.onSuccess(); } else { entityIdCalculatedFields.get(cfCtx.getEntityId()).remove(cfCtx); @@ -390,9 +390,11 @@ public class CalculatedFieldManagerMessageProcessor extends AbstractContextAware msg.getCallback().onSuccess(); } } else { - 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); + if (isMyPartition(targetEntityId, msg.getCallback())) { + 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/controller/AuthController.java b/application/src/main/java/org/thingsboard/server/controller/AuthController.java index 66e5cf6f90..0e21444431 100644 --- a/application/src/main/java/org/thingsboard/server/controller/AuthController.java +++ b/application/src/main/java/org/thingsboard/server/controller/AuthController.java @@ -228,10 +228,9 @@ public class AuthController extends BaseController { @ApiOperation(value = "Reset password (resetPassword)", notes = "Checks the password reset token and updates the password. " + - "If token is valid, returns the object that contains [JWT](https://jwt.io/) access and refresh tokens. " + "If token is not valid, returns '400 Bad Request'.") @PostMapping(value = "/noauth/resetPassword") - public JwtPair resetPassword(@Parameter(description = "Reset password request.") + public void resetPassword(@Parameter(description = "Reset password request.") @RequestBody ResetPasswordRequest resetPasswordRequest, HttpServletRequest request) throws ThingsboardException { String resetToken = resetPasswordRequest.getResetToken(); @@ -263,8 +262,6 @@ public class AuthController extends BaseController { } eventPublisher.publishEvent(new UserCredentialsInvalidationEvent(securityUser.getId())); - - return tokenFactory.createTokenPair(securityUser); } else { throw new ThingsboardException("Invalid reset token!", ThingsboardErrorCode.BAD_REQUEST_PARAMS); } diff --git a/application/src/main/java/org/thingsboard/server/controller/ControllerConstants.java b/application/src/main/java/org/thingsboard/server/controller/ControllerConstants.java index e45d041950..8817c24efe 100644 --- a/application/src/main/java/org/thingsboard/server/controller/ControllerConstants.java +++ b/application/src/main/java/org/thingsboard/server/controller/ControllerConstants.java @@ -189,6 +189,29 @@ public class ControllerConstants { protected static final String EVENT_DEBUG_RULE_CHAIN_FILTER_OBJ = MARKDOWN_CODE_BLOCK_START + "{\n" + " \"eventType\":\"DEBUG_RULE_CHAIN\",\n" + DEBUG_FILTER_OBJ + MARKDOWN_CODE_BLOCK_END; + protected static final String EVENT_DEBUG_CALCULATED_FIELD_FILTER_OBJ = MARKDOWN_CODE_BLOCK_START + "{\n" + + " \"eventType\":\"DEBUG_CALCULATED_FIELD\",\n" + + " \"server\":\"ip-172-31-24-152\",\n" + + " \"isError\":\"false\",\n" + + " \"errorStr\":\"Error Message\"\n" + + " \"entityId\":\"cf4b8741-f618-471f-ae08-d881ca7f9fe9\",\n" + + " \"msgId\":\"5cf7d3a0-aee7-40dd-a737-ade05528e7eb\",\n" + + " \"msgType\":\"POST_TELEMETRY_REQUEST\",\n" + + " \"arguments\":\"{\n" + + " \"x\": {\n" + + " \"ts\": 1739432016629,\n" + + " \"value\": 20\n" + + " },\n" + + " \"y\": {\n" + + " \"ts\": 1739429717656,\n" + + " \"value\": 12\n" + + " }\n" + + " }\",\n" + + " \"result\":\"{\n" + + " \"x + y\": 32\n" + + " }\",\n" + + "}" + MARKDOWN_CODE_BLOCK_END; + protected static final String IS_BOOTSTRAP_SERVER_PARAM_DESCRIPTION = "A Boolean value representing the Server SecurityInfo for future Bootstrap client mode settings. Values: 'true' for Bootstrap Server; 'false' for Lwm2m Server. "; protected static final String DEVICE_WITH_DEVICE_CREDENTIALS_ACCESS_TOKEN_PARAM_DESCRIPTION = diff --git a/application/src/main/java/org/thingsboard/server/controller/EventController.java b/application/src/main/java/org/thingsboard/server/controller/EventController.java index d64f6c469d..dd27ed9c33 100644 --- a/application/src/main/java/org/thingsboard/server/controller/EventController.java +++ b/application/src/main/java/org/thingsboard/server/controller/EventController.java @@ -47,6 +47,7 @@ import static org.thingsboard.server.controller.ControllerConstants.ENTITY_ID; import static org.thingsboard.server.controller.ControllerConstants.ENTITY_ID_PARAM_DESCRIPTION; import static org.thingsboard.server.controller.ControllerConstants.ENTITY_TYPE; import static org.thingsboard.server.controller.ControllerConstants.ENTITY_TYPE_PARAM_DESCRIPTION; +import static org.thingsboard.server.controller.ControllerConstants.EVENT_DEBUG_CALCULATED_FIELD_FILTER_OBJ; import static org.thingsboard.server.controller.ControllerConstants.EVENT_DEBUG_RULE_CHAIN_FILTER_OBJ; import static org.thingsboard.server.controller.ControllerConstants.EVENT_DEBUG_RULE_NODE_FILTER_OBJ; import static org.thingsboard.server.controller.ControllerConstants.EVENT_END_TIME_DESCRIPTION; @@ -70,7 +71,7 @@ import static org.thingsboard.server.controller.ControllerConstants.TENANT_ID_PA public class EventController extends BaseController { private static final String EVENT_FILTER_DEFINITION = "# Event Filter Definition" + NEW_LINE + - "5 different eventFilter objects could be set for different event types. " + + "6 different eventFilter objects could be set for different event types. " + "The eventType field is required. Others are optional. If some of them are set, the filtering will be applied according to them. " + "See the examples below for all the fields used for each event type filtering. " + NEW_LINE + "Note," + NEW_LINE + @@ -98,6 +99,15 @@ public class EventController extends BaseController { " * 'relationType' - string value representing the type of message routing;\n" + " * 'entityId' - string value representing the entity id in the event body (originator of the message);\n" + " * 'msgType' - string value representing the message type;\n" + + " * 'isError' - boolean value to filter the errors." + NEW_LINE + + "## Debug Calculated Field Event Filter" + NEW_LINE + + EVENT_DEBUG_CALCULATED_FIELD_FILTER_OBJ + NEW_LINE + + " * 'entityId' - string value representing the entity id in the event body;\n" + + " * 'entityType' - string value representing the entity type;\n" + + " * 'msgId' - string value representing the message id in the rule engine;\n" + + " * 'msgType' - string value representing the message type;\n" + + " * 'arguments' - string value representing the arguments that were used in the calculation performed;\n" + + " * 'result' - string value representing the result of a calculation;\n" + " * 'isError' - boolean value to filter the errors." + NEW_LINE; @Autowired diff --git a/application/src/main/java/org/thingsboard/server/service/cf/DefaultCalculatedFieldCache.java b/application/src/main/java/org/thingsboard/server/service/cf/DefaultCalculatedFieldCache.java index 64487d9b3e..219a261183 100644 --- a/application/src/main/java/org/thingsboard/server/service/cf/DefaultCalculatedFieldCache.java +++ b/application/src/main/java/org/thingsboard/server/service/cf/DefaultCalculatedFieldCache.java @@ -144,6 +144,9 @@ public class DefaultCalculatedFieldCache implements CalculatedFieldCache { calculatedFieldFetchLock.lock(); try { CalculatedField calculatedField = calculatedFieldService.findById(tenantId, calculatedFieldId); + if (calculatedField == null) { + return; + } EntityId cfEntityId = calculatedField.getEntityId(); calculatedFields.put(calculatedFieldId, calculatedField); diff --git a/application/src/main/java/org/thingsboard/server/service/cf/ctx/state/CalculatedFieldCtx.java b/application/src/main/java/org/thingsboard/server/service/cf/ctx/state/CalculatedFieldCtx.java index fdbbb37568..674ce3726f 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 @@ -44,6 +44,8 @@ import java.util.HashMap; import java.util.List; import java.util.Map; +import static org.thingsboard.common.util.ExpressionFunctionsUtil.userDefinedFunctions; + @Data public class CalculatedFieldCtx { @@ -111,6 +113,7 @@ public class CalculatedFieldCtx { if (isValidExpression(expression)) { this.customExpression = ThreadLocal.withInitial(() -> new ExpressionBuilder(expression) + .functions(userDefinedFunctions) .implicitMultiplication(true) .variables(this.arguments.keySet()) .build() diff --git a/application/src/main/java/org/thingsboard/server/service/edge/EdgeMsgConstructorUtils.java b/application/src/main/java/org/thingsboard/server/service/edge/EdgeMsgConstructorUtils.java index b59dc096ee..d996a1dbc9 100644 --- a/application/src/main/java/org/thingsboard/server/service/edge/EdgeMsgConstructorUtils.java +++ b/application/src/main/java/org/thingsboard/server/service/edge/EdgeMsgConstructorUtils.java @@ -16,6 +16,7 @@ package org.thingsboard.server.service.edge; import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.node.ObjectNode; import com.google.gson.Gson; import com.google.gson.JsonArray; import com.google.gson.JsonElement; @@ -24,6 +25,11 @@ import com.google.gson.JsonPrimitive; import com.google.gson.reflect.TypeToken; import lombok.extern.slf4j.Slf4j; import org.thingsboard.common.util.JacksonUtil; +import org.thingsboard.rule.engine.action.TbSaveToCustomCassandraTableNode; +import org.thingsboard.rule.engine.aws.lambda.TbAwsLambdaNode; +import org.thingsboard.rule.engine.rest.TbSendRestApiCallReplyNode; +import org.thingsboard.rule.engine.telemetry.TbMsgAttributesNode; +import org.thingsboard.rule.engine.telemetry.TbMsgTimeseriesNode; import org.thingsboard.server.common.adaptor.JsonConverter; import org.thingsboard.server.common.data.Customer; import org.thingsboard.server.common.data.Dashboard; @@ -89,6 +95,7 @@ import org.thingsboard.server.gen.edge.v1.DeviceProfileUpdateMsg; import org.thingsboard.server.gen.edge.v1.DeviceRpcCallMsg; import org.thingsboard.server.gen.edge.v1.DeviceUpdateMsg; import org.thingsboard.server.gen.edge.v1.EdgeConfiguration; +import org.thingsboard.server.gen.edge.v1.EdgeVersion; import org.thingsboard.server.gen.edge.v1.EntityDataProto; import org.thingsboard.server.gen.edge.v1.EntityViewUpdateMsg; import org.thingsboard.server.gen.edge.v1.NotificationRuleUpdateMsg; @@ -113,11 +120,36 @@ import org.thingsboard.server.gen.edge.v1.WidgetTypeUpdateMsg; import org.thingsboard.server.gen.edge.v1.WidgetsBundleUpdateMsg; import org.thingsboard.server.gen.transport.TransportProtos; +import java.util.Iterator; import java.util.List; +import java.util.Map; +import java.util.Set; import java.util.UUID; @Slf4j public class EdgeMsgConstructorUtils { + public static final Map> IGNORED_PARAMS_BY_EDGE_VERSION = Map.of( + EdgeVersion.V_3_8_0, + Map.of( + TbMsgTimeseriesNode.class.getName(), "processingSettings", + TbMsgAttributesNode.class.getName(), "processingSettings", + TbSaveToCustomCassandraTableNode.class.getName(), "defaultTtl" + ), + EdgeVersion.V_3_7_0, + Map.of( + TbMsgTimeseriesNode.class.getName(), "processingSettings", + TbMsgAttributesNode.class.getName(), "processingSettings", + TbSaveToCustomCassandraTableNode.class.getName(), "defaultTtl" + ) + ); + + public static final Map> EXCLUDED_NODES_BY_EDGE_VERSION = Map.of( + EdgeVersion.V_3_7_0, + Set.of( + TbSendRestApiCallReplyNode.class.getName(), + TbAwsLambdaNode.class.getName() + ) + ); public static AlarmUpdateMsg constructAlarmUpdatedMsg(UpdateMsgType msgType, Alarm alarm) { return AlarmUpdateMsg.newBuilder().setMsgType(msgType) @@ -417,8 +449,50 @@ public class EdgeMsgConstructorUtils { .setIdLSB(ruleChainId.getId().getLeastSignificantBits()).build(); } - public static RuleChainMetadataUpdateMsg constructRuleChainMetadataUpdatedMsg(UpdateMsgType msgType, RuleChainMetaData ruleChainMetaData) { - return RuleChainMetadataUpdateMsg.newBuilder().setMsgType(msgType).setEntity(JacksonUtil.toString(ruleChainMetaData)).build(); + public static RuleChainMetadataUpdateMsg constructRuleChainMetadataUpdatedMsg(UpdateMsgType msgType, RuleChainMetaData ruleChainMetaData, EdgeVersion edgeVersion) { + String metaData = sanitizeMetadataForLegacyEdgeVersion(ruleChainMetaData, edgeVersion); + + return RuleChainMetadataUpdateMsg.newBuilder() + .setMsgType(msgType) + .setEntity(metaData) + .build(); + } + + private static String sanitizeMetadataForLegacyEdgeVersion(RuleChainMetaData ruleChainMetaData, EdgeVersion edgeVersion) { + JsonNode jsonNode = JacksonUtil.valueToTree(ruleChainMetaData); + JsonNode nodes = jsonNode.get("nodes"); + + updateNodeConfigurationsForLegacyEdge(nodes, edgeVersion); + removeExcludedNodesForLegacyEdge(nodes, edgeVersion); + + return JacksonUtil.toString(jsonNode); + } + + private static void updateNodeConfigurationsForLegacyEdge(JsonNode nodes, EdgeVersion edgeVersion) { + nodes.forEach(node -> { + if (node.isObject() && node.has("configuration")) { + String nodeType = node.get("type").asText(); + Map ignoredParams = IGNORED_PARAMS_BY_EDGE_VERSION.get(edgeVersion); + + if (ignoredParams != null && ignoredParams.containsKey(nodeType)) { + ((ObjectNode) node.get("configuration")).remove(ignoredParams.get(nodeType)); + } + } + }); + } + + private static void removeExcludedNodesForLegacyEdge(JsonNode nodes, EdgeVersion edgeVersion) { + Iterator iterator = nodes.iterator(); + + while (iterator.hasNext()) { + JsonNode node = iterator.next(); + String type = node.get("type").asText(); + Set missNodes = EXCLUDED_NODES_BY_EDGE_VERSION.get(edgeVersion); + + if (missNodes != null && missNodes.contains(type)) { + iterator.remove(); + } + } } public static EntityDataProto constructEntityDataMsg(TenantId tenantId, EntityId entityId, EdgeEventActionType actionType, JsonElement entityData) { @@ -433,7 +507,7 @@ public class EdgeMsgConstructorUtils { JsonObject data = entityData.getAsJsonObject(); builder.setPostTelemetryMsg(JsonConverter.convertToTelemetryProto(data.getAsJsonObject("data"), ts)); } catch (Exception e) { - log.warn("[{}][{}] Can't convert to telemetry proto, entityData [{}]", tenantId, entityId, entityData, e); + log.trace("[{}][{}] Can't convert to telemetry proto, entityData [{}]", tenantId, entityId, entityData, e); } break; case ATTRIBUTES_UPDATED: @@ -448,7 +522,7 @@ public class EdgeMsgConstructorUtils { builder.setPostAttributeScope(getScopeOfDefault(data)); builder.setAttributeTs(ts); } catch (Exception e) { - log.warn("[{}][{}] Can't convert to AttributesUpdatedMsg proto, entityData [{}]", tenantId, entityId, entityData, e); + log.trace("[{}][{}] Can't convert to AttributesUpdatedMsg proto, entityData [{}]", tenantId, entityId, entityData, e); } break; case POST_ATTRIBUTES: @@ -459,7 +533,7 @@ public class EdgeMsgConstructorUtils { builder.setPostAttributeScope(getScopeOfDefault(data)); builder.setAttributeTs(ts); } catch (Exception e) { - log.warn("[{}][{}] Can't convert to PostAttributesMsg, entityData [{}]", tenantId, entityId, entityData, e); + log.trace("[{}][{}] Can't convert to PostAttributesMsg, entityData [{}]", tenantId, entityId, entityData, e); } break; case ATTRIBUTES_DELETED: @@ -467,12 +541,12 @@ public class EdgeMsgConstructorUtils { AttributeDeleteMsg.Builder attributeDeleteMsg = AttributeDeleteMsg.newBuilder(); attributeDeleteMsg.setScope(entityData.getAsJsonObject().getAsJsonPrimitive("scope").getAsString()); JsonArray jsonArray = entityData.getAsJsonObject().getAsJsonArray("keys"); - List keys = new Gson().fromJson(jsonArray.toString(), new TypeToken<>(){}.getType()); + List keys = new Gson().fromJson(jsonArray.toString(), new TypeToken<>() {}.getType()); attributeDeleteMsg.addAllAttributeNames(keys); attributeDeleteMsg.build(); builder.setAttributeDeleteMsg(attributeDeleteMsg); } catch (Exception e) { - log.warn("[{}][{}] Can't convert to AttributeDeleteMsg proto, entityData [{}]", tenantId, entityId, entityData, e); + log.trace("[{}][{}] Can't convert to AttributeDeleteMsg proto, entityData [{}]", tenantId, entityId, entityData, e); } break; } diff --git a/application/src/main/java/org/thingsboard/server/service/edge/rpc/EdgeGrpcSession.java b/application/src/main/java/org/thingsboard/server/service/edge/rpc/EdgeGrpcSession.java index a970097548..9ce5973639 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 @@ -203,7 +203,7 @@ public abstract class EdgeGrpcSession implements Closeable { @Override public void onError(Throwable t) { - log.error("[{}][{}] Stream was terminated due to error:", tenantId, sessionId, t); + log.trace("[{}][{}] Stream was terminated due to error:", tenantId, sessionId, t); closeSession(); } @@ -255,7 +255,7 @@ public abstract class EdgeGrpcSession implements Closeable { private void doSync(EdgeSyncCursor cursor) { if (cursor.hasNext()) { EdgeEventFetcher next = cursor.getNext(); - log.info("[{}][{}] starting sync process, cursor current idx = {}, class = {}", + log.debug("[{}][{}] starting sync process, cursor current idx = {}, class = {}", tenantId, edge.getId(), cursor.getCurrentIdx(), next.getClass().getSimpleName()); ListenableFuture> future = startProcessingEdgeEvents(next); Futures.addCallback(future, new FutureCallback<>() { @@ -651,7 +651,7 @@ public abstract class EdgeGrpcSession implements Closeable { default -> log.warn("[{}][{}] Unsupported action type [{}]", tenantId, sessionId, edgeEvent.getAction()); } } catch (Exception e) { - log.error("[{}][{}] Exception during converting edge event to downlink msg", tenantId, sessionId, e); + log.trace("[{}][{}] Exception during converting edge event to downlink msg", tenantId, sessionId, e); } if (downlinkMsg != null) { result.add(downlinkMsg); @@ -763,7 +763,7 @@ public abstract class EdgeGrpcSession implements Closeable { try { outputStream.onNext(responseMsg); } catch (Exception e) { - log.error("[{}][{}] Failed to send downlink message [{}]", tenantId, sessionId, downlinkMsgStr, e); + log.trace("[{}][{}] Failed to send downlink message [{}]", tenantId, sessionId, downlinkMsgStr, e); connected = false; sessionCloseListener.accept(edge, sessionId); } finally { @@ -780,7 +780,7 @@ public abstract class EdgeGrpcSession implements Closeable { return null; } - return ctx.getProcessor(edgeEvent.getType()).convertEdgeEventToDownlink(edgeEvent); + return ctx.getProcessor(edgeEvent.getType()).convertEdgeEventToDownlink(edgeEvent, edgeVersion); } public void addEventToHighPriorityQueue(EdgeEvent edgeEvent) { @@ -909,7 +909,7 @@ public abstract class EdgeGrpcSession implements Closeable { } } catch (Exception e) { String failureMsg = String.format("Can't process uplink msg [%s] from edge", uplinkMsg); - log.error("[{}][{}] Can't process uplink msg [{}]", edge.getTenantId(), sessionId, uplinkMsg, e); + log.trace("[{}][{}] Can't process uplink msg [{}]", edge.getTenantId(), sessionId, uplinkMsg, e); ctx.getRuleProcessor().process(EdgeCommunicationFailureTrigger.builder().tenantId(edge.getTenantId()).edgeId(edge.getId()) .customerId(edge.getCustomerId()).edgeName(edge.getName()).failureMsg(failureMsg).error(e.getMessage()).build()); return Futures.immediateFailedFuture(e); 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 dd9f9a2b42..351c9b411b 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 @@ -65,8 +65,10 @@ public class EdgeSyncCursor { fetchers.add(new AdminSettingsEdgeEventFetcher(ctx.getAdminSettingsService())); fetchers.add(new TenantAdminUsersEdgeEventFetcher(ctx.getUserService())); } - Customer publicCustomer = ctx.getCustomerService().findOrCreatePublicCustomer(edge.getTenantId()); - fetchers.add(new CustomerEdgeEventFetcher(publicCustomer.getId())); + Customer publicCustomer = ctx.getCustomerService().findPublicCustomer(edge.getTenantId()); + if (publicCustomer != null) { + fetchers.add(new CustomerEdgeEventFetcher(publicCustomer.getId())); + } if (edge.getCustomerId() != null && !EntityId.NULL_UUID.equals(edge.getCustomerId().getId())) { fetchers.add(new CustomerEdgeEventFetcher(edge.getCustomerId())); fetchers.add(new CustomerUsersEdgeEventFetcher(ctx.getUserService(), edge.getCustomerId())); diff --git a/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/EdgeProcessor.java b/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/EdgeProcessor.java index 0f17949437..dd6221c62b 100644 --- a/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/EdgeProcessor.java +++ b/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/EdgeProcessor.java @@ -20,13 +20,14 @@ import org.thingsboard.server.common.data.edge.EdgeEvent; import org.thingsboard.server.common.data.edge.EdgeEventType; import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.gen.edge.v1.DownlinkMsg; +import org.thingsboard.server.gen.edge.v1.EdgeVersion; import org.thingsboard.server.gen.transport.TransportProtos; public interface EdgeProcessor { ListenableFuture processEntityNotification(TenantId tenantId, TransportProtos.EdgeNotificationMsgProto msg); - default DownlinkMsg convertEdgeEventToDownlink(EdgeEvent edgeEvent) { + default DownlinkMsg convertEdgeEventToDownlink(EdgeEvent edgeEvent, EdgeVersion edgeVersion) { return null; } diff --git a/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/alarm/AlarmEdgeProcessor.java b/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/alarm/AlarmEdgeProcessor.java index df9af20f84..6c4cbe7a94 100644 --- a/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/alarm/AlarmEdgeProcessor.java +++ b/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/alarm/AlarmEdgeProcessor.java @@ -30,6 +30,7 @@ import org.thingsboard.server.common.data.id.EdgeId; import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.gen.edge.v1.AlarmUpdateMsg; import org.thingsboard.server.gen.edge.v1.DownlinkMsg; +import org.thingsboard.server.gen.edge.v1.EdgeVersion; import org.thingsboard.server.gen.edge.v1.UpdateMsgType; import org.thingsboard.server.gen.transport.TransportProtos; import org.thingsboard.server.queue.util.TbCoreComponent; @@ -55,7 +56,7 @@ public class AlarmEdgeProcessor extends BaseAlarmProcessor implements AlarmProce } @Override - public DownlinkMsg convertEdgeEventToDownlink(EdgeEvent edgeEvent) { + public DownlinkMsg convertEdgeEventToDownlink(EdgeEvent edgeEvent, EdgeVersion edgeVersion) { AlarmUpdateMsg alarmUpdateMsg = convertAlarmEventToAlarmMsg(edgeEvent); if (alarmUpdateMsg != null) { return DownlinkMsg.newBuilder() diff --git a/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/alarm/comment/AlarmCommentEdgeProcessor.java b/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/alarm/comment/AlarmCommentEdgeProcessor.java index 449f4867d1..4a76a3527b 100644 --- a/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/alarm/comment/AlarmCommentEdgeProcessor.java +++ b/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/alarm/comment/AlarmCommentEdgeProcessor.java @@ -31,6 +31,7 @@ import org.thingsboard.server.common.data.id.EdgeId; import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.gen.edge.v1.AlarmCommentUpdateMsg; import org.thingsboard.server.gen.edge.v1.DownlinkMsg; +import org.thingsboard.server.gen.edge.v1.EdgeVersion; import org.thingsboard.server.gen.edge.v1.UpdateMsgType; import org.thingsboard.server.gen.transport.TransportProtos; import org.thingsboard.server.queue.util.TbCoreComponent; @@ -57,7 +58,7 @@ public class AlarmCommentEdgeProcessor extends BaseAlarmProcessor implements Ala } @Override - public DownlinkMsg convertEdgeEventToDownlink(EdgeEvent edgeEvent) { + public DownlinkMsg convertEdgeEventToDownlink(EdgeEvent edgeEvent, EdgeVersion edgeVersion) { UpdateMsgType msgType = getUpdateMsgType(edgeEvent.getAction()); switch (edgeEvent.getAction()) { case ADDED_COMMENT: diff --git a/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/asset/AssetEdgeProcessor.java b/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/asset/AssetEdgeProcessor.java index e7a9720419..47f4c11362 100644 --- a/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/asset/AssetEdgeProcessor.java +++ b/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/asset/AssetEdgeProcessor.java @@ -36,6 +36,7 @@ import org.thingsboard.server.common.msg.TbMsgMetaData; import org.thingsboard.server.dao.exception.DataValidationException; import org.thingsboard.server.gen.edge.v1.AssetUpdateMsg; import org.thingsboard.server.gen.edge.v1.DownlinkMsg; +import org.thingsboard.server.gen.edge.v1.EdgeVersion; import org.thingsboard.server.gen.edge.v1.UpdateMsgType; import org.thingsboard.server.queue.util.TbCoreComponent; import org.thingsboard.server.service.edge.EdgeMsgConstructorUtils; @@ -107,7 +108,7 @@ public class AssetEdgeProcessor extends BaseAssetProcessor implements AssetProce } @Override - public DownlinkMsg convertEdgeEventToDownlink(EdgeEvent edgeEvent) { + public DownlinkMsg convertEdgeEventToDownlink(EdgeEvent edgeEvent, EdgeVersion edgeVersion) { AssetId assetId = new AssetId(edgeEvent.getEntityId()); switch (edgeEvent.getAction()) { case ADDED, UPDATED, ASSIGNED_TO_EDGE, ASSIGNED_TO_CUSTOMER, UNASSIGNED_FROM_CUSTOMER -> { diff --git a/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/asset/profile/AssetProfileEdgeProcessor.java b/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/asset/profile/AssetProfileEdgeProcessor.java index 7a0ae0ccc6..e05b6f41c3 100644 --- a/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/asset/profile/AssetProfileEdgeProcessor.java +++ b/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/asset/profile/AssetProfileEdgeProcessor.java @@ -36,6 +36,7 @@ import org.thingsboard.server.common.msg.TbMsgMetaData; import org.thingsboard.server.dao.exception.DataValidationException; import org.thingsboard.server.gen.edge.v1.AssetProfileUpdateMsg; import org.thingsboard.server.gen.edge.v1.DownlinkMsg; +import org.thingsboard.server.gen.edge.v1.EdgeVersion; import org.thingsboard.server.gen.edge.v1.UpdateMsgType; import org.thingsboard.server.queue.util.TbCoreComponent; import org.thingsboard.server.service.edge.EdgeMsgConstructorUtils; @@ -94,7 +95,7 @@ public class AssetProfileEdgeProcessor extends BaseAssetProfileProcessor impleme } @Override - public DownlinkMsg convertEdgeEventToDownlink(EdgeEvent edgeEvent) { + public DownlinkMsg convertEdgeEventToDownlink(EdgeEvent edgeEvent, EdgeVersion edgeVersion) { AssetProfileId assetProfileId = new AssetProfileId(edgeEvent.getEntityId()); switch (edgeEvent.getAction()) { case ADDED, UPDATED -> { diff --git a/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/customer/CustomerEdgeProcessor.java b/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/customer/CustomerEdgeProcessor.java index 40cbe3985f..5f02fbc554 100644 --- a/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/customer/CustomerEdgeProcessor.java +++ b/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/customer/CustomerEdgeProcessor.java @@ -32,6 +32,7 @@ import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.common.data.page.PageDataIterable; import org.thingsboard.server.gen.edge.v1.CustomerUpdateMsg; import org.thingsboard.server.gen.edge.v1.DownlinkMsg; +import org.thingsboard.server.gen.edge.v1.EdgeVersion; import org.thingsboard.server.gen.edge.v1.UpdateMsgType; import org.thingsboard.server.gen.transport.TransportProtos; import org.thingsboard.server.queue.util.TbCoreComponent; @@ -48,7 +49,7 @@ import java.util.UUID; public class CustomerEdgeProcessor extends BaseEdgeProcessor { @Override - public DownlinkMsg convertEdgeEventToDownlink(EdgeEvent edgeEvent) { + public DownlinkMsg convertEdgeEventToDownlink(EdgeEvent edgeEvent, EdgeVersion edgeVersion) { CustomerId customerId = new CustomerId(edgeEvent.getEntityId()); switch (edgeEvent.getAction()) { case ADDED, UPDATED -> { @@ -80,13 +81,16 @@ public class CustomerEdgeProcessor extends BaseEdgeProcessor { UUID uuid = new UUID(edgeNotificationMsg.getEntityIdMSB(), edgeNotificationMsg.getEntityIdLSB()); CustomerId customerId = new CustomerId(EntityIdFactory.getByEdgeEventTypeAndUuid(type, uuid).getId()); switch (actionType) { - case UPDATED: - List> futures = new ArrayList<>(); - PageDataIterable edges = new PageDataIterable<>(link -> edgeCtx.getEdgeService().findEdgesByTenantIdAndCustomerId(tenantId, customerId, link), 1024); - for (Edge edge : edges) { - futures.add(saveEdgeEvent(tenantId, edge.getId(), type, actionType, customerId, null)); + case ADDED: + Customer customerById = edgeCtx.getCustomerService().findCustomerById(tenantId, customerId); + if (customerById != null && customerById.isPublic()) { + return findEdgesAndSaveEdgeEvents(link -> edgeCtx.getEdgeService().findEdgesByTenantId(tenantId, link), + tenantId, type, actionType, customerId); } - return Futures.transform(Futures.allAsList(futures), voids -> null, dbCallbackExecutorService); + return Futures.immediateFuture(null); + case UPDATED: + return findEdgesAndSaveEdgeEvents(link -> edgeCtx.getEdgeService().findEdgesByTenantIdAndCustomerId(tenantId, customerId, link), + tenantId, type, actionType, customerId); case DELETED: EdgeId edgeId = new EdgeId(new UUID(edgeNotificationMsg.getEdgeIdMSB(), edgeNotificationMsg.getEdgeIdLSB())); return saveEdgeEvent(tenantId, edgeId, type, actionType, customerId, null); @@ -95,6 +99,16 @@ public class CustomerEdgeProcessor extends BaseEdgeProcessor { } } + public ListenableFuture findEdgesAndSaveEdgeEvents(PageDataIterable.FetchFunction edgeFetcher, TenantId tenantId, + EdgeEventType type, EdgeEventActionType actionType, CustomerId customerId) { + List> futures = new ArrayList<>(); + PageDataIterable edges = new PageDataIterable<>(edgeFetcher, 1024); + for (Edge edge : edges) { + futures.add(saveEdgeEvent(tenantId, edge.getId(), type, actionType, customerId, null)); + } + return Futures.transform(Futures.allAsList(futures), voids -> null, dbCallbackExecutorService); + } + @Override public EdgeEventType getEdgeEventType() { return EdgeEventType.CUSTOMER; diff --git a/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/dashboard/DashboardEdgeProcessor.java b/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/dashboard/DashboardEdgeProcessor.java index f9a7311f31..e1259a7e0e 100644 --- a/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/dashboard/DashboardEdgeProcessor.java +++ b/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/dashboard/DashboardEdgeProcessor.java @@ -33,6 +33,7 @@ import org.thingsboard.server.common.msg.TbMsgMetaData; import org.thingsboard.server.dao.exception.DataValidationException; import org.thingsboard.server.gen.edge.v1.DashboardUpdateMsg; import org.thingsboard.server.gen.edge.v1.DownlinkMsg; +import org.thingsboard.server.gen.edge.v1.EdgeVersion; import org.thingsboard.server.gen.edge.v1.UpdateMsgType; import org.thingsboard.server.queue.util.TbCoreComponent; import org.thingsboard.server.service.edge.EdgeMsgConstructorUtils; @@ -100,7 +101,7 @@ public class DashboardEdgeProcessor extends BaseDashboardProcessor implements Da } @Override - public DownlinkMsg convertEdgeEventToDownlink(EdgeEvent edgeEvent) { + public DownlinkMsg convertEdgeEventToDownlink(EdgeEvent edgeEvent, EdgeVersion edgeVersion) { DashboardId dashboardId = new DashboardId(edgeEvent.getEntityId()); switch (edgeEvent.getAction()) { case ADDED, UPDATED, ASSIGNED_TO_EDGE, ASSIGNED_TO_CUSTOMER, UNASSIGNED_FROM_CUSTOMER -> { diff --git a/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/device/DeviceEdgeProcessor.java b/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/device/DeviceEdgeProcessor.java index 08f6b294bf..ab01f83cd8 100644 --- a/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/device/DeviceEdgeProcessor.java +++ b/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/device/DeviceEdgeProcessor.java @@ -49,6 +49,7 @@ import org.thingsboard.server.gen.edge.v1.DeviceCredentialsUpdateMsg; import org.thingsboard.server.gen.edge.v1.DeviceRpcCallMsg; import org.thingsboard.server.gen.edge.v1.DeviceUpdateMsg; import org.thingsboard.server.gen.edge.v1.DownlinkMsg; +import org.thingsboard.server.gen.edge.v1.EdgeVersion; import org.thingsboard.server.gen.edge.v1.UpdateMsgType; import org.thingsboard.server.queue.TbQueueCallback; import org.thingsboard.server.queue.TbQueueMsgMetadata; @@ -222,7 +223,7 @@ public class DeviceEdgeProcessor extends BaseDeviceProcessor implements DevicePr } @Override - public DownlinkMsg convertEdgeEventToDownlink(EdgeEvent edgeEvent) { + public DownlinkMsg convertEdgeEventToDownlink(EdgeEvent edgeEvent, EdgeVersion edgeVersion) { DeviceId deviceId = new DeviceId(edgeEvent.getEntityId()); switch (edgeEvent.getAction()) { case ADDED: diff --git a/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/device/profile/DeviceProfileEdgeProcessor.java b/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/device/profile/DeviceProfileEdgeProcessor.java index 35857d8edb..3355bc819f 100644 --- a/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/device/profile/DeviceProfileEdgeProcessor.java +++ b/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/device/profile/DeviceProfileEdgeProcessor.java @@ -36,6 +36,7 @@ import org.thingsboard.server.common.msg.TbMsgMetaData; import org.thingsboard.server.dao.exception.DataValidationException; import org.thingsboard.server.gen.edge.v1.DeviceProfileUpdateMsg; import org.thingsboard.server.gen.edge.v1.DownlinkMsg; +import org.thingsboard.server.gen.edge.v1.EdgeVersion; import org.thingsboard.server.gen.edge.v1.UpdateMsgType; import org.thingsboard.server.queue.util.TbCoreComponent; import org.thingsboard.server.service.edge.EdgeMsgConstructorUtils; @@ -94,7 +95,7 @@ public class DeviceProfileEdgeProcessor extends BaseDeviceProfileProcessor imple } @Override - public DownlinkMsg convertEdgeEventToDownlink(EdgeEvent edgeEvent) { + public DownlinkMsg convertEdgeEventToDownlink(EdgeEvent edgeEvent, EdgeVersion edgeVersion) { DeviceProfileId deviceProfileId = new DeviceProfileId(edgeEvent.getEntityId()); switch (edgeEvent.getAction()) { case ADDED, UPDATED -> { diff --git a/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/edge/EdgeEntityProcessor.java b/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/edge/EdgeEntityProcessor.java index 77fa31c028..5d4674ba1f 100644 --- a/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/edge/EdgeEntityProcessor.java +++ b/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/edge/EdgeEntityProcessor.java @@ -32,6 +32,7 @@ import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.common.data.page.PageData; import org.thingsboard.server.common.data.page.PageLink; import org.thingsboard.server.gen.edge.v1.DownlinkMsg; +import org.thingsboard.server.gen.edge.v1.EdgeVersion; import org.thingsboard.server.gen.transport.TransportProtos; import org.thingsboard.server.queue.util.TbCoreComponent; import org.thingsboard.server.service.edge.EdgeMsgConstructorUtils; @@ -101,7 +102,7 @@ public class EdgeEntityProcessor extends BaseEdgeProcessor { } @Override - public DownlinkMsg convertEdgeEventToDownlink(EdgeEvent edgeEvent) { + public DownlinkMsg convertEdgeEventToDownlink(EdgeEvent edgeEvent, EdgeVersion edgeVersion) { EdgeId edgeId = new EdgeId(edgeEvent.getEntityId()); switch (edgeEvent.getAction()) { case ASSIGNED_TO_CUSTOMER, UNASSIGNED_FROM_CUSTOMER -> { diff --git a/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/entityview/EntityViewEdgeProcessor.java b/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/entityview/EntityViewEdgeProcessor.java index a8844e7397..56785f9ac0 100644 --- a/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/entityview/EntityViewEdgeProcessor.java +++ b/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/entityview/EntityViewEdgeProcessor.java @@ -34,6 +34,7 @@ import org.thingsboard.server.common.data.msg.TbMsgType; import org.thingsboard.server.common.msg.TbMsgMetaData; import org.thingsboard.server.dao.exception.DataValidationException; import org.thingsboard.server.gen.edge.v1.DownlinkMsg; +import org.thingsboard.server.gen.edge.v1.EdgeVersion; import org.thingsboard.server.gen.edge.v1.EntityViewUpdateMsg; import org.thingsboard.server.gen.edge.v1.UpdateMsgType; import org.thingsboard.server.queue.util.TbCoreComponent; @@ -106,7 +107,7 @@ public class EntityViewEdgeProcessor extends BaseEntityViewProcessor implements } @Override - public DownlinkMsg convertEdgeEventToDownlink(EdgeEvent edgeEvent) { + public DownlinkMsg convertEdgeEventToDownlink(EdgeEvent edgeEvent, EdgeVersion edgeVersion) { EntityViewId entityViewId = new EntityViewId(edgeEvent.getEntityId()); switch (edgeEvent.getAction()) { case ADDED, UPDATED, ASSIGNED_TO_EDGE, ASSIGNED_TO_CUSTOMER, UNASSIGNED_FROM_CUSTOMER -> { diff --git a/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/notification/NotificationRuleEdgeProcessor.java b/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/notification/NotificationRuleEdgeProcessor.java index 77f97e0775..918ca024cd 100644 --- a/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/notification/NotificationRuleEdgeProcessor.java +++ b/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/notification/NotificationRuleEdgeProcessor.java @@ -25,6 +25,7 @@ import org.thingsboard.server.common.data.id.NotificationRuleId; import org.thingsboard.server.common.data.notification.rule.NotificationRule; import org.thingsboard.server.dao.notification.NotificationRuleService; import org.thingsboard.server.gen.edge.v1.DownlinkMsg; +import org.thingsboard.server.gen.edge.v1.EdgeVersion; import org.thingsboard.server.gen.edge.v1.NotificationRuleUpdateMsg; import org.thingsboard.server.gen.edge.v1.UpdateMsgType; import org.thingsboard.server.queue.util.TbCoreComponent; @@ -40,7 +41,7 @@ public class NotificationRuleEdgeProcessor extends BaseEdgeProcessor { private NotificationRuleService notificationRuleService; @Override - public DownlinkMsg convertEdgeEventToDownlink(EdgeEvent edgeEvent) { + public DownlinkMsg convertEdgeEventToDownlink(EdgeEvent edgeEvent, EdgeVersion edgeVersion) { NotificationRuleId notificationRuleId = new NotificationRuleId(edgeEvent.getEntityId()); DownlinkMsg downlinkMsg = null; switch (edgeEvent.getAction()) { diff --git a/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/notification/NotificationTargetEdgeProcessor.java b/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/notification/NotificationTargetEdgeProcessor.java index 5dfad971b0..0f95248c74 100644 --- a/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/notification/NotificationTargetEdgeProcessor.java +++ b/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/notification/NotificationTargetEdgeProcessor.java @@ -25,6 +25,7 @@ import org.thingsboard.server.common.data.id.NotificationTargetId; import org.thingsboard.server.common.data.notification.targets.NotificationTarget; import org.thingsboard.server.dao.notification.NotificationTargetService; import org.thingsboard.server.gen.edge.v1.DownlinkMsg; +import org.thingsboard.server.gen.edge.v1.EdgeVersion; import org.thingsboard.server.gen.edge.v1.NotificationTargetUpdateMsg; import org.thingsboard.server.gen.edge.v1.UpdateMsgType; import org.thingsboard.server.queue.util.TbCoreComponent; @@ -40,7 +41,7 @@ public class NotificationTargetEdgeProcessor extends BaseEdgeProcessor { private NotificationTargetService notificationTargetService; @Override - public DownlinkMsg convertEdgeEventToDownlink(EdgeEvent edgeEvent) { + public DownlinkMsg convertEdgeEventToDownlink(EdgeEvent edgeEvent, EdgeVersion edgeVersion) { NotificationTargetId notificationTargetId = new NotificationTargetId(edgeEvent.getEntityId()); DownlinkMsg downlinkMsg = null; switch (edgeEvent.getAction()) { diff --git a/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/notification/NotificationTemplateEdgeProcessor.java b/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/notification/NotificationTemplateEdgeProcessor.java index b79f4ab4b8..f3ae939514 100644 --- a/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/notification/NotificationTemplateEdgeProcessor.java +++ b/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/notification/NotificationTemplateEdgeProcessor.java @@ -25,6 +25,7 @@ import org.thingsboard.server.common.data.id.NotificationTemplateId; import org.thingsboard.server.common.data.notification.template.NotificationTemplate; import org.thingsboard.server.dao.notification.NotificationTemplateService; import org.thingsboard.server.gen.edge.v1.DownlinkMsg; +import org.thingsboard.server.gen.edge.v1.EdgeVersion; import org.thingsboard.server.gen.edge.v1.NotificationTemplateUpdateMsg; import org.thingsboard.server.gen.edge.v1.UpdateMsgType; import org.thingsboard.server.queue.util.TbCoreComponent; @@ -40,7 +41,7 @@ public class NotificationTemplateEdgeProcessor extends BaseEdgeProcessor { private NotificationTemplateService notificationTemplateService; @Override - public DownlinkMsg convertEdgeEventToDownlink(EdgeEvent edgeEvent) { + public DownlinkMsg convertEdgeEventToDownlink(EdgeEvent edgeEvent, EdgeVersion edgeVersion) { NotificationTemplateId notificationTemplateId = new NotificationTemplateId(edgeEvent.getEntityId()); DownlinkMsg downlinkMsg = null; switch (edgeEvent.getAction()) { diff --git a/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/oauth2/DomainEdgeProcessor.java b/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/oauth2/DomainEdgeProcessor.java index 6b3d429a2f..07247d946c 100644 --- a/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/oauth2/DomainEdgeProcessor.java +++ b/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/oauth2/DomainEdgeProcessor.java @@ -26,6 +26,7 @@ import org.thingsboard.server.common.data.id.DomainId; import org.thingsboard.server.common.data.oauth2.OAuth2Client; import org.thingsboard.server.dao.domain.DomainService; import org.thingsboard.server.gen.edge.v1.DownlinkMsg; +import org.thingsboard.server.gen.edge.v1.EdgeVersion; import org.thingsboard.server.gen.edge.v1.OAuth2ClientUpdateMsg; import org.thingsboard.server.gen.edge.v1.OAuth2DomainUpdateMsg; import org.thingsboard.server.gen.edge.v1.UpdateMsgType; @@ -42,7 +43,7 @@ public class DomainEdgeProcessor extends BaseEdgeProcessor { private DomainService domainService; @Override - public DownlinkMsg convertEdgeEventToDownlink(EdgeEvent edgeEvent) { + public DownlinkMsg convertEdgeEventToDownlink(EdgeEvent edgeEvent, EdgeVersion edgeVersion) { DomainId domainId = new DomainId(edgeEvent.getEntityId()); switch (edgeEvent.getAction()) { case ADDED, UPDATED -> { diff --git a/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/oauth2/OAuth2ClientEdgeProcessor.java b/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/oauth2/OAuth2ClientEdgeProcessor.java index 20bd5b3f7e..08b0405702 100644 --- a/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/oauth2/OAuth2ClientEdgeProcessor.java +++ b/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/oauth2/OAuth2ClientEdgeProcessor.java @@ -23,6 +23,7 @@ import org.thingsboard.server.common.data.edge.EdgeEventType; import org.thingsboard.server.common.data.id.OAuth2ClientId; import org.thingsboard.server.common.data.oauth2.OAuth2Client; import org.thingsboard.server.gen.edge.v1.DownlinkMsg; +import org.thingsboard.server.gen.edge.v1.EdgeVersion; import org.thingsboard.server.gen.edge.v1.OAuth2ClientUpdateMsg; import org.thingsboard.server.gen.edge.v1.UpdateMsgType; import org.thingsboard.server.queue.util.TbCoreComponent; @@ -35,7 +36,7 @@ import org.thingsboard.server.service.edge.rpc.processor.BaseEdgeProcessor; public class OAuth2ClientEdgeProcessor extends BaseEdgeProcessor { @Override - public DownlinkMsg convertEdgeEventToDownlink(EdgeEvent edgeEvent) { + public DownlinkMsg convertEdgeEventToDownlink(EdgeEvent edgeEvent, EdgeVersion edgeVersion) { OAuth2ClientId oAuth2ClientId = new OAuth2ClientId(edgeEvent.getEntityId()); switch (edgeEvent.getAction()) { diff --git a/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/ota/OtaPackageEdgeProcessor.java b/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/ota/OtaPackageEdgeProcessor.java index b2080e8176..e22ed3bafa 100644 --- a/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/ota/OtaPackageEdgeProcessor.java +++ b/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/ota/OtaPackageEdgeProcessor.java @@ -23,6 +23,7 @@ import org.thingsboard.server.common.data.edge.EdgeEvent; import org.thingsboard.server.common.data.edge.EdgeEventType; import org.thingsboard.server.common.data.id.OtaPackageId; import org.thingsboard.server.gen.edge.v1.DownlinkMsg; +import org.thingsboard.server.gen.edge.v1.EdgeVersion; import org.thingsboard.server.gen.edge.v1.OtaPackageUpdateMsg; import org.thingsboard.server.gen.edge.v1.UpdateMsgType; import org.thingsboard.server.queue.util.TbCoreComponent; @@ -35,7 +36,7 @@ import org.thingsboard.server.service.edge.rpc.processor.BaseEdgeProcessor; public class OtaPackageEdgeProcessor extends BaseEdgeProcessor { @Override - public DownlinkMsg convertEdgeEventToDownlink(EdgeEvent edgeEvent) { + public DownlinkMsg convertEdgeEventToDownlink(EdgeEvent edgeEvent, EdgeVersion edgeVersion) { OtaPackageId otaPackageId = new OtaPackageId(edgeEvent.getEntityId()); switch (edgeEvent.getAction()) { case ADDED, UPDATED -> { diff --git a/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/queue/QueueEdgeProcessor.java b/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/queue/QueueEdgeProcessor.java index ebea0c8255..f5fcac3342 100644 --- a/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/queue/QueueEdgeProcessor.java +++ b/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/queue/QueueEdgeProcessor.java @@ -23,6 +23,7 @@ import org.thingsboard.server.common.data.edge.EdgeEventType; import org.thingsboard.server.common.data.id.QueueId; import org.thingsboard.server.common.data.queue.Queue; import org.thingsboard.server.gen.edge.v1.DownlinkMsg; +import org.thingsboard.server.gen.edge.v1.EdgeVersion; import org.thingsboard.server.gen.edge.v1.QueueUpdateMsg; import org.thingsboard.server.gen.edge.v1.UpdateMsgType; import org.thingsboard.server.queue.util.TbCoreComponent; @@ -35,7 +36,7 @@ import org.thingsboard.server.service.edge.rpc.processor.BaseEdgeProcessor; public class QueueEdgeProcessor extends BaseEdgeProcessor { @Override - public DownlinkMsg convertEdgeEventToDownlink(EdgeEvent edgeEvent) { + public DownlinkMsg convertEdgeEventToDownlink(EdgeEvent edgeEvent, EdgeVersion edgeVersion) { QueueId queueId = new QueueId(edgeEvent.getEntityId()); switch (edgeEvent.getAction()) { case ADDED, UPDATED -> { diff --git a/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/relation/RelationEdgeProcessor.java b/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/relation/RelationEdgeProcessor.java index da6f749269..70666e8b0e 100644 --- a/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/relation/RelationEdgeProcessor.java +++ b/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/relation/RelationEdgeProcessor.java @@ -21,7 +21,6 @@ import lombok.extern.slf4j.Slf4j; import org.springframework.stereotype.Component; import org.thingsboard.common.util.JacksonUtil; import org.thingsboard.server.common.data.EdgeUtils; -import org.thingsboard.server.common.data.EntityType; import org.thingsboard.server.common.data.edge.Edge; import org.thingsboard.server.common.data.edge.EdgeEvent; import org.thingsboard.server.common.data.edge.EdgeEventActionType; @@ -30,6 +29,7 @@ import org.thingsboard.server.common.data.id.EdgeId; import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.common.data.relation.EntityRelation; import org.thingsboard.server.gen.edge.v1.DownlinkMsg; +import org.thingsboard.server.gen.edge.v1.EdgeVersion; import org.thingsboard.server.gen.edge.v1.RelationUpdateMsg; import org.thingsboard.server.gen.edge.v1.UpdateMsgType; import org.thingsboard.server.gen.transport.TransportProtos; @@ -85,7 +85,7 @@ public class RelationEdgeProcessor extends BaseRelationProcessor implements Rela } @Override - public DownlinkMsg convertEdgeEventToDownlink(EdgeEvent edgeEvent) { + public DownlinkMsg convertEdgeEventToDownlink(EdgeEvent edgeEvent, EdgeVersion edgeVersion) { EntityRelation entityRelation = JacksonUtil.convertValue(edgeEvent.getBody(), EntityRelation.class); UpdateMsgType msgType = getUpdateMsgType(edgeEvent.getAction()); RelationUpdateMsg relationUpdateMsg = EdgeMsgConstructorUtils.constructRelationUpdatedMsg(msgType, entityRelation); diff --git a/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/resource/ResourceEdgeProcessor.java b/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/resource/ResourceEdgeProcessor.java index 8d2c06025a..de31bf6a9c 100644 --- a/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/resource/ResourceEdgeProcessor.java +++ b/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/resource/ResourceEdgeProcessor.java @@ -29,6 +29,7 @@ import org.thingsboard.server.common.data.id.TbResourceId; import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.dao.exception.DataValidationException; import org.thingsboard.server.gen.edge.v1.DownlinkMsg; +import org.thingsboard.server.gen.edge.v1.EdgeVersion; import org.thingsboard.server.gen.edge.v1.ResourceUpdateMsg; import org.thingsboard.server.gen.edge.v1.UpdateMsgType; import org.thingsboard.server.queue.util.TbCoreComponent; @@ -73,7 +74,7 @@ public class ResourceEdgeProcessor extends BaseResourceProcessor implements Reso } @Override - public DownlinkMsg convertEdgeEventToDownlink(EdgeEvent edgeEvent) { + public DownlinkMsg convertEdgeEventToDownlink(EdgeEvent edgeEvent, EdgeVersion edgeVersion) { TbResourceId tbResourceId = new TbResourceId(edgeEvent.getEntityId()); switch (edgeEvent.getAction()) { case ADDED, UPDATED -> { diff --git a/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/rule/RuleChainEdgeProcessor.java b/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/rule/RuleChainEdgeProcessor.java index 06fb4c37a2..b949660e70 100644 --- a/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/rule/RuleChainEdgeProcessor.java +++ b/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/rule/RuleChainEdgeProcessor.java @@ -33,6 +33,7 @@ import org.thingsboard.server.common.data.rule.RuleChainType; import org.thingsboard.server.common.msg.TbMsgMetaData; import org.thingsboard.server.dao.exception.DataValidationException; import org.thingsboard.server.gen.edge.v1.DownlinkMsg; +import org.thingsboard.server.gen.edge.v1.EdgeVersion; import org.thingsboard.server.gen.edge.v1.RuleChainMetadataUpdateMsg; import org.thingsboard.server.gen.edge.v1.RuleChainUpdateMsg; import org.thingsboard.server.gen.edge.v1.UpdateMsgType; @@ -134,7 +135,7 @@ public class RuleChainEdgeProcessor extends BaseRuleChainProcessor { } @Override - public DownlinkMsg convertEdgeEventToDownlink(EdgeEvent edgeEvent) { + public DownlinkMsg convertEdgeEventToDownlink(EdgeEvent edgeEvent, EdgeVersion edgeVersion) { RuleChainId ruleChainId = new RuleChainId(edgeEvent.getEntityId()); DownlinkMsg downlinkMsg = null; switch (edgeEvent.getAction()) { @@ -161,7 +162,7 @@ public class RuleChainEdgeProcessor extends BaseRuleChainProcessor { RuleChainMetaData ruleChainMetaData = edgeCtx.getRuleChainService().loadRuleChainMetaData(edgeEvent.getTenantId(), ruleChainId); RuleChainMetadataUpdateMsg ruleChainMetadataUpdateMsg = EdgeMsgConstructorUtils - .constructRuleChainMetadataUpdatedMsg(msgType, ruleChainMetaData); + .constructRuleChainMetadataUpdatedMsg(msgType, ruleChainMetaData, edgeVersion); builder.addRuleChainMetadataUpdateMsg(ruleChainMetadataUpdateMsg); downlinkMsg = builder.build(); } diff --git a/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/rule/RuleChainMetadataEdgeProcessor.java b/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/rule/RuleChainMetadataEdgeProcessor.java index fe9a66c456..845d2a7204 100644 --- a/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/rule/RuleChainMetadataEdgeProcessor.java +++ b/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/rule/RuleChainMetadataEdgeProcessor.java @@ -24,6 +24,7 @@ import org.thingsboard.server.common.data.id.RuleChainId; import org.thingsboard.server.common.data.rule.RuleChain; import org.thingsboard.server.common.data.rule.RuleChainMetaData; import org.thingsboard.server.gen.edge.v1.DownlinkMsg; +import org.thingsboard.server.gen.edge.v1.EdgeVersion; import org.thingsboard.server.gen.edge.v1.RuleChainMetadataUpdateMsg; import org.thingsboard.server.gen.edge.v1.UpdateMsgType; import org.thingsboard.server.queue.util.TbCoreComponent; @@ -36,14 +37,14 @@ import org.thingsboard.server.service.edge.rpc.processor.BaseEdgeProcessor; public class RuleChainMetadataEdgeProcessor extends BaseEdgeProcessor { @Override - public DownlinkMsg convertEdgeEventToDownlink(EdgeEvent edgeEvent) { + public DownlinkMsg convertEdgeEventToDownlink(EdgeEvent edgeEvent, EdgeVersion edgeVersion) { RuleChainId ruleChainId = new RuleChainId(edgeEvent.getEntityId()); RuleChain ruleChain = edgeCtx.getRuleChainService().findRuleChainById(edgeEvent.getTenantId(), ruleChainId); if (ruleChain != null) { RuleChainMetaData ruleChainMetaData = edgeCtx.getRuleChainService().loadRuleChainMetaData(edgeEvent.getTenantId(), ruleChainId); UpdateMsgType msgType = getUpdateMsgType(edgeEvent.getAction()); RuleChainMetadataUpdateMsg ruleChainMetadataUpdateMsg = EdgeMsgConstructorUtils - .constructRuleChainMetadataUpdatedMsg(msgType, ruleChainMetaData); + .constructRuleChainMetadataUpdatedMsg(msgType, ruleChainMetaData, edgeVersion); return DownlinkMsg.newBuilder() .setDownlinkMsgId(EdgeUtils.nextPositiveInt()) .addRuleChainMetadataUpdateMsg(ruleChainMetadataUpdateMsg) diff --git a/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/settings/AdminSettingsEdgeProcessor.java b/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/settings/AdminSettingsEdgeProcessor.java index 6028273dc2..f2f82fe9de 100644 --- a/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/settings/AdminSettingsEdgeProcessor.java +++ b/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/settings/AdminSettingsEdgeProcessor.java @@ -24,6 +24,7 @@ import org.thingsboard.server.common.data.edge.EdgeEvent; import org.thingsboard.server.common.data.edge.EdgeEventType; import org.thingsboard.server.gen.edge.v1.AdminSettingsUpdateMsg; import org.thingsboard.server.gen.edge.v1.DownlinkMsg; +import org.thingsboard.server.gen.edge.v1.EdgeVersion; import org.thingsboard.server.queue.util.TbCoreComponent; import org.thingsboard.server.service.edge.rpc.processor.BaseEdgeProcessor; @@ -33,7 +34,7 @@ import org.thingsboard.server.service.edge.rpc.processor.BaseEdgeProcessor; public class AdminSettingsEdgeProcessor extends BaseEdgeProcessor { @Override - public DownlinkMsg convertEdgeEventToDownlink(EdgeEvent edgeEvent) { + public DownlinkMsg convertEdgeEventToDownlink(EdgeEvent edgeEvent, EdgeVersion edgeVersion) { AdminSettings adminSettings = JacksonUtil.convertValue(edgeEvent.getBody(), AdminSettings.class); if (adminSettings == null) { return null; diff --git a/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/tenant/TenantEdgeProcessor.java b/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/tenant/TenantEdgeProcessor.java index e21ed0b4ea..f0f41b5cc6 100644 --- a/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/tenant/TenantEdgeProcessor.java +++ b/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/tenant/TenantEdgeProcessor.java @@ -25,6 +25,7 @@ import org.thingsboard.server.common.data.edge.EdgeEventActionType; import org.thingsboard.server.common.data.edge.EdgeEventType; import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.gen.edge.v1.DownlinkMsg; +import org.thingsboard.server.gen.edge.v1.EdgeVersion; import org.thingsboard.server.gen.edge.v1.TenantProfileUpdateMsg; import org.thingsboard.server.gen.edge.v1.TenantUpdateMsg; import org.thingsboard.server.gen.edge.v1.UpdateMsgType; @@ -38,7 +39,7 @@ import org.thingsboard.server.service.edge.rpc.processor.BaseEdgeProcessor; public class TenantEdgeProcessor extends BaseEdgeProcessor { @Override - public DownlinkMsg convertEdgeEventToDownlink(EdgeEvent edgeEvent) { + public DownlinkMsg convertEdgeEventToDownlink(EdgeEvent edgeEvent, EdgeVersion edgeVersion) { TenantId tenantId = TenantId.fromUUID(edgeEvent.getEntityId()); if (EdgeEventActionType.UPDATED.equals(edgeEvent.getAction())) { Tenant tenant = edgeCtx.getTenantService().findTenantById(tenantId); diff --git a/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/tenant/TenantProfileEdgeProcessor.java b/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/tenant/TenantProfileEdgeProcessor.java index bcbeeb82b3..a0bb32503e 100644 --- a/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/tenant/TenantProfileEdgeProcessor.java +++ b/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/tenant/TenantProfileEdgeProcessor.java @@ -24,6 +24,7 @@ import org.thingsboard.server.common.data.edge.EdgeEventActionType; import org.thingsboard.server.common.data.edge.EdgeEventType; import org.thingsboard.server.common.data.id.TenantProfileId; import org.thingsboard.server.gen.edge.v1.DownlinkMsg; +import org.thingsboard.server.gen.edge.v1.EdgeVersion; import org.thingsboard.server.gen.edge.v1.TenantProfileUpdateMsg; import org.thingsboard.server.gen.edge.v1.UpdateMsgType; import org.thingsboard.server.queue.util.TbCoreComponent; @@ -36,7 +37,7 @@ import org.thingsboard.server.service.edge.rpc.processor.BaseEdgeProcessor; public class TenantProfileEdgeProcessor extends BaseEdgeProcessor { @Override - public DownlinkMsg convertEdgeEventToDownlink(EdgeEvent edgeEvent) { + public DownlinkMsg convertEdgeEventToDownlink(EdgeEvent edgeEvent, EdgeVersion edgeVersion) { TenantProfileId tenantProfileId = new TenantProfileId(edgeEvent.getEntityId()); if (EdgeEventActionType.UPDATED.equals(edgeEvent.getAction())) { TenantProfile tenantProfile = edgeCtx.getTenantProfileService().findTenantProfileById(edgeEvent.getTenantId(), tenantProfileId); diff --git a/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/user/UserEdgeProcessor.java b/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/user/UserEdgeProcessor.java index c8c0b49124..fdd03d63f3 100644 --- a/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/user/UserEdgeProcessor.java +++ b/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/user/UserEdgeProcessor.java @@ -24,6 +24,7 @@ import org.thingsboard.server.common.data.edge.EdgeEventType; import org.thingsboard.server.common.data.id.UserId; import org.thingsboard.server.common.data.security.UserCredentials; import org.thingsboard.server.gen.edge.v1.DownlinkMsg; +import org.thingsboard.server.gen.edge.v1.EdgeVersion; import org.thingsboard.server.gen.edge.v1.UpdateMsgType; import org.thingsboard.server.gen.edge.v1.UserCredentialsUpdateMsg; import org.thingsboard.server.queue.util.TbCoreComponent; @@ -36,7 +37,7 @@ import org.thingsboard.server.service.edge.rpc.processor.BaseEdgeProcessor; public class UserEdgeProcessor extends BaseEdgeProcessor { @Override - public DownlinkMsg convertEdgeEventToDownlink(EdgeEvent edgeEvent) { + public DownlinkMsg convertEdgeEventToDownlink(EdgeEvent edgeEvent, EdgeVersion edgeVersion) { UserId userId = new UserId(edgeEvent.getEntityId()); switch (edgeEvent.getAction()) { case ADDED, UPDATED -> { diff --git a/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/widget/WidgetBundleEdgeProcessor.java b/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/widget/WidgetBundleEdgeProcessor.java index 4c22ddf898..52d7370470 100644 --- a/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/widget/WidgetBundleEdgeProcessor.java +++ b/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/widget/WidgetBundleEdgeProcessor.java @@ -23,6 +23,7 @@ import org.thingsboard.server.common.data.edge.EdgeEventType; import org.thingsboard.server.common.data.id.WidgetsBundleId; import org.thingsboard.server.common.data.widget.WidgetsBundle; import org.thingsboard.server.gen.edge.v1.DownlinkMsg; +import org.thingsboard.server.gen.edge.v1.EdgeVersion; import org.thingsboard.server.gen.edge.v1.UpdateMsgType; import org.thingsboard.server.gen.edge.v1.WidgetsBundleUpdateMsg; import org.thingsboard.server.queue.util.TbCoreComponent; @@ -37,7 +38,7 @@ import java.util.List; public class WidgetBundleEdgeProcessor extends BaseEdgeProcessor { @Override - public DownlinkMsg convertEdgeEventToDownlink(EdgeEvent edgeEvent) { + public DownlinkMsg convertEdgeEventToDownlink(EdgeEvent edgeEvent, EdgeVersion edgeVersion) { WidgetsBundleId widgetsBundleId = new WidgetsBundleId(edgeEvent.getEntityId()); switch (edgeEvent.getAction()) { case ADDED, UPDATED -> { diff --git a/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/widget/WidgetTypeEdgeProcessor.java b/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/widget/WidgetTypeEdgeProcessor.java index 4da508b22b..47f30afa93 100644 --- a/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/widget/WidgetTypeEdgeProcessor.java +++ b/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/widget/WidgetTypeEdgeProcessor.java @@ -23,6 +23,7 @@ import org.thingsboard.server.common.data.edge.EdgeEventType; import org.thingsboard.server.common.data.id.WidgetTypeId; import org.thingsboard.server.common.data.widget.WidgetTypeDetails; import org.thingsboard.server.gen.edge.v1.DownlinkMsg; +import org.thingsboard.server.gen.edge.v1.EdgeVersion; import org.thingsboard.server.gen.edge.v1.UpdateMsgType; import org.thingsboard.server.gen.edge.v1.WidgetTypeUpdateMsg; import org.thingsboard.server.queue.util.TbCoreComponent; @@ -35,7 +36,7 @@ import org.thingsboard.server.service.edge.rpc.processor.BaseEdgeProcessor; public class WidgetTypeEdgeProcessor extends BaseEdgeProcessor { @Override - public DownlinkMsg convertEdgeEventToDownlink(EdgeEvent edgeEvent) { + public DownlinkMsg convertEdgeEventToDownlink(EdgeEvent edgeEvent, EdgeVersion edgeVersion) { WidgetTypeId widgetTypeId = new WidgetTypeId(edgeEvent.getEntityId()); switch (edgeEvent.getAction()) { case ADDED, UPDATED -> { diff --git a/application/src/main/java/org/thingsboard/server/service/edge/rpc/utils/EdgeVersionUtils.java b/application/src/main/java/org/thingsboard/server/service/edge/rpc/utils/EdgeVersionUtils.java index 07e2024e1f..9902118eb2 100644 --- a/application/src/main/java/org/thingsboard/server/service/edge/rpc/utils/EdgeVersionUtils.java +++ b/application/src/main/java/org/thingsboard/server/service/edge/rpc/utils/EdgeVersionUtils.java @@ -24,4 +24,5 @@ public final class EdgeVersionUtils { public static boolean isEdgeVersionOlderThan(EdgeVersion currentVersion, EdgeVersion requiredVersion) { return currentVersion.ordinal() < requiredVersion.ordinal(); } + } 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 79e0e60983..871e1fa5e7 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 @@ -161,13 +161,17 @@ public abstract class EdqsSyncService { private void processRelationBatch(List relations) { for (RelationEntity relation : relations) { - if (RelationTypeGroup.COMMON.name().equals(relation.getRelationTypeGroup())) { - EntityIdInfo entityIdInfo = entityInfoMap.get(relation.getFromId()); - if (entityIdInfo != null) { - process(entityIdInfo.tenantId(), RELATION, relation.toData()); - } else { - log.info("Relation from id not found: {} ", relation); + try { + if (RelationTypeGroup.COMMON.name().equals(relation.getRelationTypeGroup())) { + EntityIdInfo entityIdInfo = entityInfoMap.get(relation.getFromId()); + if (entityIdInfo != null) { + process(entityIdInfo.tenantId(), RELATION, relation.toData()); + } else { + log.info("Relation from id not found: {} ", relation); + } } + } catch (Exception e) { + log.error("Failed to sync relation: {}", relation, e); } } } @@ -207,19 +211,23 @@ public abstract class EdqsSyncService { 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); - if (entityIdInfo == null) { - log.debug("Skipping attribute with entity UUID {} as it is not found in entityInfoMap", entityId); - continue; + try { + 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); + } catch (Exception e) { + log.error("Failed to sync attribute: {}", attribute, e); } - AttributeKv attributeKv = new AttributeKv( - EntityIdFactory.getByTypeAndUuid(entityIdInfo.entityType(), entityId), - AttributeScope.valueOf(attribute.getId().getAttributeType()), - attribute.toData(), - attribute.getVersion()); - process(entityIdInfo.tenantId(), ATTRIBUTE_KV, attributeKv); } } 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 125a06299d..e3bc3be90d 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 @@ -16,7 +16,6 @@ package org.thingsboard.server.service.queue; import jakarta.annotation.PreDestroy; -import lombok.extern.slf4j.Slf4j; import org.apache.commons.collections4.CollectionUtils; import org.springframework.beans.factory.annotation.Value; import org.springframework.context.ApplicationEventPublisher; @@ -70,7 +69,6 @@ import java.util.stream.Collectors; @Service @TbRuleEngineComponent -@Slf4j public class DefaultTbCalculatedFieldConsumerService extends AbstractPartitionBasedConsumerService implements TbCalculatedFieldConsumerService { @Value("${queue.calculated_fields.poll_interval:25}") 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 92c48a5fed..561dc7122a 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 @@ -551,11 +551,15 @@ public class DefaultTbClusterService implements TbClusterService { } private void processEdgeNotification(EdgeId edgeId, ToEdgeNotificationMsg toEdgeNotificationMsg) { - var serviceIdOpt = Optional.ofNullable(edgeIdServiceIdCache.get(edgeId)); - serviceIdOpt.ifPresentOrElse( - serviceId -> pushMsgToEdgeNotification(toEdgeNotificationMsg, serviceId.get()), - () -> broadcastEdgeNotification(edgeId, toEdgeNotificationMsg) - ); + if (edgesEnabled) { + var serviceIdOpt = Optional.ofNullable(edgeIdServiceIdCache.get(edgeId)); + serviceIdOpt.ifPresentOrElse( + serviceId -> pushMsgToEdgeNotification(toEdgeNotificationMsg, serviceId.get()), + () -> broadcastEdgeNotification(edgeId, toEdgeNotificationMsg) + ); + } else { + log.trace("Edges disabled. Ignoring edge notification {} for edgeId: {}", toEdgeNotificationMsg, edgeId); + } } private void pushMsgToEdgeNotification(ToEdgeNotificationMsg toEdgeNotificationMsg, String serviceId) { 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 a3003ba6ff..e115e4f7b5 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.extern.slf4j.Slf4j; import org.apache.commons.lang3.StringUtils; import org.springframework.beans.factory.annotation.Value; import org.springframework.context.ApplicationEventPublisher; @@ -118,7 +117,6 @@ import java.util.stream.Collectors; @Service @TbCoreComponent -@Slf4j public class DefaultTbCoreConsumerService extends AbstractConsumerService implements TbCoreConsumerService { @Value("${queue.core.poll-interval}") diff --git a/application/src/main/java/org/thingsboard/server/service/queue/DefaultTbEdgeConsumerService.java b/application/src/main/java/org/thingsboard/server/service/queue/DefaultTbEdgeConsumerService.java index fdaa2103e2..d3dc2932f0 100644 --- a/application/src/main/java/org/thingsboard/server/service/queue/DefaultTbEdgeConsumerService.java +++ b/application/src/main/java/org/thingsboard/server/service/queue/DefaultTbEdgeConsumerService.java @@ -19,8 +19,6 @@ import com.google.common.util.concurrent.FutureCallback; import com.google.common.util.concurrent.Futures; import com.google.common.util.concurrent.ListenableFuture; import com.google.common.util.concurrent.MoreExecutors; -import lombok.Data; -import lombok.extern.slf4j.Slf4j; import org.checkerframework.checker.nullness.qual.Nullable; import org.jetbrains.annotations.NotNull; import org.springframework.beans.factory.annotation.Value; @@ -45,12 +43,13 @@ import org.thingsboard.server.gen.transport.TransportProtos.ToEdgeMsg; import org.thingsboard.server.gen.transport.TransportProtos.ToEdgeNotificationMsg; 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.discovery.QueueKey; import org.thingsboard.server.queue.discovery.event.PartitionChangeEvent; import org.thingsboard.server.queue.provider.TbCoreQueueFactory; import org.thingsboard.server.queue.util.TbCoreComponent; import org.thingsboard.server.service.edge.EdgeContextComponent; -import org.thingsboard.server.queue.common.consumer.MainQueueConsumerManager; +import org.thingsboard.server.service.edge.rpc.EdgeRpcService; import org.thingsboard.server.service.queue.processing.AbstractConsumerService; import org.thingsboard.server.service.queue.processing.IdMsgPair; @@ -65,7 +64,6 @@ import java.util.concurrent.Future; import java.util.concurrent.TimeUnit; import java.util.stream.Collectors; -@Slf4j @Service @TbCoreComponent public class DefaultTbEdgeConsumerService extends AbstractConsumerService implements TbEdgeConsumerService { @@ -195,36 +193,42 @@ public class DefaultTbEdgeConsumerService extends AbstractConsumerService msg, TbCallback callback) { ToEdgeNotificationMsg toEdgeNotificationMsg = msg.getValue(); try { + EdgeRpcService edgeRpcService = edgeCtx.getEdgeRpcService(); + if (edgeRpcService == null) { + log.debug("No EdgeRpcService available (edge functionality disabled), ignoring msg: {}", toEdgeNotificationMsg); + callback.onSuccess(); + return; + } if (toEdgeNotificationMsg.hasEdgeHighPriority()) { EdgeSessionMsg edgeSessionMsg = ProtoUtils.fromProto(toEdgeNotificationMsg.getEdgeHighPriority()); - edgeCtx.getEdgeRpcService().onToEdgeSessionMsg(edgeSessionMsg.getTenantId(), edgeSessionMsg); + edgeRpcService.onToEdgeSessionMsg(edgeSessionMsg.getTenantId(), edgeSessionMsg); callback.onSuccess(); } else if (toEdgeNotificationMsg.hasEdgeEventUpdate()) { EdgeSessionMsg edgeSessionMsg = ProtoUtils.fromProto(toEdgeNotificationMsg.getEdgeEventUpdate()); - edgeCtx.getEdgeRpcService().onToEdgeSessionMsg(edgeSessionMsg.getTenantId(), edgeSessionMsg); + edgeRpcService.onToEdgeSessionMsg(edgeSessionMsg.getTenantId(), edgeSessionMsg); callback.onSuccess(); } else if (toEdgeNotificationMsg.hasToEdgeSyncRequest()) { EdgeSessionMsg edgeSessionMsg = ProtoUtils.fromProto(toEdgeNotificationMsg.getToEdgeSyncRequest()); - edgeCtx.getEdgeRpcService().onToEdgeSessionMsg(edgeSessionMsg.getTenantId(), edgeSessionMsg); + edgeRpcService.onToEdgeSessionMsg(edgeSessionMsg.getTenantId(), edgeSessionMsg); callback.onSuccess(); } else if (toEdgeNotificationMsg.hasFromEdgeSyncResponse()) { EdgeSessionMsg edgeSessionMsg = ProtoUtils.fromProto(toEdgeNotificationMsg.getFromEdgeSyncResponse()); - edgeCtx.getEdgeRpcService().onToEdgeSessionMsg(edgeSessionMsg.getTenantId(), edgeSessionMsg); + edgeRpcService.onToEdgeSessionMsg(edgeSessionMsg.getTenantId(), edgeSessionMsg); callback.onSuccess(); } else if (toEdgeNotificationMsg.hasComponentLifecycle()) { ComponentLifecycleMsg componentLifecycle = ProtoUtils.fromProto(toEdgeNotificationMsg.getComponentLifecycle()); TenantId tenantId = componentLifecycle.getTenantId(); EdgeId edgeId = new EdgeId(componentLifecycle.getEntityId().getId()); if (ComponentLifecycleEvent.DELETED.equals(componentLifecycle.getEvent())) { - edgeCtx.getEdgeRpcService().deleteEdge(tenantId, edgeId); + edgeRpcService.deleteEdge(tenantId, edgeId); } else if (ComponentLifecycleEvent.UPDATED.equals(componentLifecycle.getEvent())) { Edge edge = edgeCtx.getEdgeService().findEdgeById(tenantId, edgeId); - edgeCtx.getEdgeRpcService().updateEdge(tenantId, edge); + edgeRpcService.updateEdge(tenantId, edge); } callback.onSuccess(); } } catch (Exception e) { - log.error("Error processing edge notification message", e); + log.error("Error processing edge notification message {}", toEdgeNotificationMsg, e); callback.onFailure(e); } 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 98f857750a..f51678310e 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 @@ -15,7 +15,6 @@ */ package org.thingsboard.server.service.queue; -import lombok.extern.slf4j.Slf4j; import org.springframework.context.ApplicationEventPublisher; import org.springframework.context.event.EventListener; import org.springframework.scheduling.annotation.Scheduled; @@ -65,7 +64,6 @@ import java.util.stream.Collectors; @Service @TbRuleEngineComponent -@Slf4j public class DefaultTbRuleEngineConsumerService extends AbstractPartitionBasedConsumerService implements TbRuleEngineConsumerService { private final TbRuleEngineConsumerContext ctx; diff --git a/application/src/main/java/org/thingsboard/server/service/queue/processing/AbstractConsumerService.java b/application/src/main/java/org/thingsboard/server/service/queue/processing/AbstractConsumerService.java index d12bd896ff..26b689df9d 100644 --- a/application/src/main/java/org/thingsboard/server/service/queue/processing/AbstractConsumerService.java +++ b/application/src/main/java/org/thingsboard/server/service/queue/processing/AbstractConsumerService.java @@ -17,7 +17,8 @@ package org.thingsboard.server.service.queue.processing; import jakarta.annotation.PreDestroy; import lombok.RequiredArgsConstructor; -import lombok.extern.slf4j.Slf4j; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; import org.springframework.context.ApplicationEventPublisher; import org.thingsboard.common.util.ThingsBoardExecutors; import org.thingsboard.common.util.ThingsBoardThreadFactory; @@ -62,10 +63,11 @@ import java.util.concurrent.ScheduledExecutorService; import java.util.concurrent.TimeUnit; import java.util.stream.Collectors; -@Slf4j @RequiredArgsConstructor public abstract class AbstractConsumerService extends TbApplicationEventListener { + protected final Logger log = LoggerFactory.getLogger(getClass()); + protected final ActorSystemContext actorContext; protected final TbTenantProfileCache tenantProfileCache; protected final TbDeviceProfileCache deviceProfileCache; diff --git a/application/src/main/java/org/thingsboard/server/service/queue/processing/AbstractPartitionBasedConsumerService.java b/application/src/main/java/org/thingsboard/server/service/queue/processing/AbstractPartitionBasedConsumerService.java index 97aa81d41c..f42908bcb5 100644 --- a/application/src/main/java/org/thingsboard/server/service/queue/processing/AbstractPartitionBasedConsumerService.java +++ b/application/src/main/java/org/thingsboard/server/service/queue/processing/AbstractPartitionBasedConsumerService.java @@ -28,6 +28,8 @@ import org.thingsboard.server.service.profile.TbAssetProfileCache; import org.thingsboard.server.service.profile.TbDeviceProfileCache; import org.thingsboard.server.service.security.auth.jwt.settings.JwtSettingsService; +import java.util.ArrayList; +import java.util.List; import java.util.concurrent.locks.Lock; import java.util.concurrent.locks.ReentrantLock; @@ -35,7 +37,7 @@ public abstract class AbstractPartitionBasedConsumerService pendingEvents = new ArrayList<>(); public AbstractPartitionBasedConsumerService(ActorSystemContext actorContext, TbTenantProfileCache tenantProfileCache, @@ -61,8 +63,16 @@ public abstract class AbstractPartitionBasedConsumerService { TbTimeSeriesSubscription sub = (TbTimeSeriesSubscription) s; List updateData = null; + Map keyStates = sub.getKeyStates(); if (sub.isAllKeys()) { - updateData = data; + if (sub.isLatestValues()) { + for (TsKvEntry kv : data) { + Long stateTs = keyStates.get(kv.getKey()); + if (stateTs == null || kv.getTs() > stateTs) { + if (updateData == null) { + updateData = new ArrayList<>(); + } + updateData.add(kv); + } + } + } else { + updateData = data; + } } else { for (TsKvEntry kv : data) { - if (sub.getKeyStates().containsKey((kv.getKey()))) { - if (updateData == null) { - updateData = new ArrayList<>(); + Long stateTs = keyStates.get(kv.getKey()); + if (stateTs != null) { + if (!sub.isLatestValues() || kv.getTs() > stateTs) { + if (updateData == null) { + updateData = new ArrayList<>(); + } + updateData.add(kv); } - updateData.add(kv); } } } diff --git a/application/src/main/java/org/thingsboard/server/service/sync/ie/DefaultEntitiesExportImportService.java b/application/src/main/java/org/thingsboard/server/service/sync/ie/DefaultEntitiesExportImportService.java index 06fe7f4036..db7e37b368 100644 --- a/application/src/main/java/org/thingsboard/server/service/sync/ie/DefaultEntitiesExportImportService.java +++ b/application/src/main/java/org/thingsboard/server/service/sync/ie/DefaultEntitiesExportImportService.java @@ -31,7 +31,6 @@ import org.thingsboard.server.common.data.relation.EntityRelation; import org.thingsboard.server.common.data.sync.ie.EntityExportData; import org.thingsboard.server.common.data.sync.ie.EntityImportResult; import org.thingsboard.server.common.data.util.ThrowingRunnable; -import org.thingsboard.server.dao.cf.CalculatedFieldService; import org.thingsboard.server.dao.exception.DataValidationException; import org.thingsboard.server.dao.relation.RelationService; import org.thingsboard.server.queue.util.TbCoreComponent; @@ -62,7 +61,6 @@ public class DefaultEntitiesExportImportService implements EntitiesExportImportS private final Map> importServices = new HashMap<>(); private final RelationService relationService; - private final CalculatedFieldService calculatedFieldService; private final RateLimitService rateLimitService; private final TbLogEntityActionService logEntityActionService; diff --git a/application/src/main/java/org/thingsboard/server/service/ws/DefaultWebSocketService.java b/application/src/main/java/org/thingsboard/server/service/ws/DefaultWebSocketService.java index 7288a8bac9..2e4fe9730a 100644 --- a/application/src/main/java/org/thingsboard/server/service/ws/DefaultWebSocketService.java +++ b/application/src/main/java/org/thingsboard/server/service/ws/DefaultWebSocketService.java @@ -105,6 +105,8 @@ import java.util.function.BiConsumer; import java.util.function.Consumer; import java.util.stream.Collectors; +import static org.thingsboard.server.common.data.DataConstants.LATEST_TELEMETRY_SCOPE; + /** * Created by ashvayka on 27.03.18. */ @@ -667,24 +669,7 @@ public class DefaultWebSocketService implements WebSocketService { data.forEach(v -> subState.put(v.getKey(), v.getTs())); Lock subLock = new ReentrantLock(); - TbTimeSeriesSubscription sub = TbTimeSeriesSubscription.builder() - .serviceId(serviceId) - .sessionId(sessionId) - .subscriptionId(registerNewSessionSubId(sessionId, sessionRef, cmd.getCmdId())) - .tenantId(sessionRef.getSecurityCtx().getTenantId()) - .entityId(entityId) - .updateProcessor((subscription, update) -> { - subLock.lock(); - try { - sendUpdate(subscription.getSessionId(), cmd.getCmdId(), update); - } finally { - subLock.unlock(); - } - }) - .queryTs(queryTs) - .allKeys(true) - .keyStates(subState) - .build(); + TbTimeSeriesSubscription sub = getTsSubscription(subState, subLock, sessionId, sessionRef, cmd, entityId, queryTs, true); subLock.lock(); try { @@ -712,6 +697,28 @@ public class DefaultWebSocketService implements WebSocketService { on(r -> Futures.addCallback(tsService.findAllLatest(sessionRef.getSecurityCtx().getTenantId(), entityId), callback, executor), callback::onFailure)); } + private TbTimeSeriesSubscription getTsSubscription(Map subState, Lock subLock, String sessionId, WebSocketSessionRef sessionRef, TimeseriesSubscriptionCmd cmd, EntityId entityId, long queryTs, boolean allKeys) { + return TbTimeSeriesSubscription.builder() + .serviceId(serviceId) + .sessionId(sessionId) + .subscriptionId(registerNewSessionSubId(sessionId, sessionRef, cmd.getCmdId())) + .tenantId(sessionRef.getSecurityCtx().getTenantId()) + .entityId(entityId) + .updateProcessor((subscription, update) -> { + subLock.lock(); + try { + sendUpdate(subscription.getSessionId(), cmd.getCmdId(), update); + } finally { + subLock.unlock(); + } + }) + .queryTs(queryTs) + .allKeys(allKeys) + .keyStates(subState) + .latestValues(LATEST_TELEMETRY_SCOPE.equals(cmd.getScope())) + .build(); + } + private FutureCallback> getSubscriptionCallback(final WebSocketSessionRef sessionRef, final TimeseriesSubscriptionCmd cmd, final String sessionId, final EntityId entityId, final long queryTs, final long startTs, final List keys) { return new FutureCallback<>() { @@ -722,24 +729,7 @@ public class DefaultWebSocketService implements WebSocketService { data.forEach(v -> subState.put(v.getKey(), v.getTs())); Lock subLock = new ReentrantLock(); - TbTimeSeriesSubscription sub = TbTimeSeriesSubscription.builder() - .serviceId(serviceId) - .sessionId(sessionId) - .subscriptionId(registerNewSessionSubId(sessionId, sessionRef, cmd.getCmdId())) - .tenantId(sessionRef.getSecurityCtx().getTenantId()) - .entityId(entityId) - .updateProcessor((subscription, update) -> { - subLock.lock(); - try { - sendUpdate(subscription.getSessionId(), cmd.getCmdId(), update); - } finally { - subLock.unlock(); - } - }) - .queryTs(queryTs) - .allKeys(false) - .keyStates(subState) - .build(); + TbTimeSeriesSubscription sub = getTsSubscription(subState, subLock, sessionId, sessionRef, cmd, entityId, queryTs, false); subLock.lock(); try { diff --git a/application/src/main/resources/banner.txt b/application/src/main/resources/banner.txt index 57f907c5e0..cf6ed4da7d 100644 --- a/application/src/main/resources/banner.txt +++ b/application/src/main/resources/banner.txt @@ -1,10 +1,10 @@ - ______ __ _ ____ __ - /_ __/ / /_ (_) ____ ____ _ _____ / __ ) ____ ____ _ _____ ____/ / - / / / __ \ / / / __ \ / __ `/ / ___/ / __ | / __ \ / __ `/ / ___/ / __ / - / / / / / / / / / / / / / /_/ / (__ ) / /_/ / / /_/ // /_/ / / / / /_/ / -/_/ /_/ /_/ /_/ /_/ /_/ \__, / /____/ /_____/ \____/ \__,_/ /_/ \__,_/ - /____/ + _____ _ _ ____ _ + |_ _| |__ (_)_ __ __ _ ___| __ ) ___ __ _ _ __ __| | + | | | '_ \| | '_ \ / _` / __| _ \ / _ \ / _` | '__/ _` | + | | | | | | | | | | (_| \__ \ |_) | (_) | (_| | | | (_| | + |_| |_| |_|_|_| |_|\__, |___/____/ \___/ \__,_|_| \__,_| + |___/ - =================================================== + =========================================================== :: ${application.title} :: ${application.formatted-version} - =================================================== + =========================================================== diff --git a/application/src/main/resources/thingsboard.yml b/application/src/main/resources/thingsboard.yml index 7ede65e375..716797c2d2 100644 --- a/application/src/main/resources/thingsboard.yml +++ b/application/src/main/resources/thingsboard.yml @@ -1421,6 +1421,9 @@ device: port: "${DEVICE_CONNECTIVITY_COAPS_PORT:5684}" # Path to the COAP CA root certificate file pem_cert_file: "${DEVICE_CONNECTIVITY_COAPS_CA_ROOT_CERT:cafile.pem}" + gateway: + # The docker tag for thingsboard/tb-gateway image used in docker-compose file for gateway launch + image_version: "${DEVICE_CONNECTIVITY_GATEWAY_IMAGE_VERSION:3.7-stable}" # Edges parameters edges: @@ -1587,11 +1590,24 @@ queue: # tb_rule_engine.sq: # - key: max.poll.records # value: "${TB_QUEUE_KAFKA_SQ_MAX_POLL_RECORDS:1024}" + tb_edge: + # Properties for consumers targeting edge service update topics. + - key: max.poll.records + # Define the maximum number of records that can be polled from tb_edge topics per request. + value: "${TB_QUEUE_KAFKA_EDGE_EVENTS_MAX_POLL_RECORDS:10}" + tb_edge.notifications: + # Properties for consumers targeting high-priority edge notifications. + # These notifications include RPC calls, lifecycle events, and new queue messages, + # requiring minimal latency and swift processing. + - key: max.poll.records + # Define the maximum number of records that can be polled from tb_edge.notifications. topics. + value: "${TB_QUEUE_KAFKA_EDGE_HP_EVENTS_MAX_POLL_RECORDS:10}" tb_edge_event.notifications: - # Example of specific consumer properties value per topic for edge event + # Properties for consumers targeting downlinks meant for specific edge topics. + # Topic names are dynamically constructed using tenant and edge identifiers. - key: max.poll.records - # Example of specific consumer properties value per topic for edge event - value: "${TB_QUEUE_KAFKA_EDGE_EVENT_MAX_POLL_RECORDS:50}" + # Define the maximum number of records that can be polled from tb_edge_event.notifications.. topics. + value: "${TB_QUEUE_KAFKA_EDGE_NOTIFICATIONS_MAX_POLL_RECORDS:10}" tb_housekeeper: # Consumer properties for Housekeeper tasks topic - key: max.poll.records @@ -1762,9 +1778,13 @@ queue: max_pending_requests: "${TB_EDQS_MAX_PENDING_REQUESTS:10000}" # Maximum timeout for requests to EDQS max_request_timeout: "${TB_EDQS_MAX_REQUEST_TIMEOUT:20000}" + # Strings longer than this threshold will be compressed + string_compression_length_threshold: "${TB_EDQS_STRING_COMPRESSION_LENGTH_THRESHOLD:512}" stats: # Enable/disable statistics for EDQS enabled: "${TB_EDQS_STATS_ENABLED:true}" + # Threshold for slow queries to log, in milliseconds + slow_query_threshold: "${TB_EDQS_SLOW_QUERY_THRESHOLD_MS:3000}" vc: # Default topic name topic: "${TB_QUEUE_VC_TOPIC:tb_version_control}" @@ -1834,11 +1854,13 @@ queue: # Interval in milliseconds to poll messages poll_interval: "${TB_QUEUE_TRANSPORT_NOTIFICATIONS_POLL_INTERVAL_MS:25}" edge: - # Default topic name + # Topic name to notify edge service on entity updates, assignment, etc. topic: "${TB_QUEUE_EDGE_TOPIC:tb_edge}" - # For high-priority notifications that require minimum latency and processing time + # Topic prefix for high-priority edge notifications (rpc, lifecycle, new messages in queue) that require minimum latency and processing time. + # Each tb-core has its own topic: . notifications_topic: "${TB_QUEUE_EDGE_NOTIFICATIONS_TOPIC:tb_edge.notifications}" - # For edge events messages + # Topic prefix for downlinks to be pushed to specific edge. + # Every edge has its own unique topic: .. 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}" diff --git a/application/src/test/java/org/thingsboard/server/controller/CalculatedFieldControllerTest.java b/application/src/test/java/org/thingsboard/server/controller/CalculatedFieldControllerTest.java index ee66f664cc..af43b34558 100644 --- a/application/src/test/java/org/thingsboard/server/controller/CalculatedFieldControllerTest.java +++ b/application/src/test/java/org/thingsboard/server/controller/CalculatedFieldControllerTest.java @@ -31,7 +31,6 @@ import org.thingsboard.server.common.data.cf.configuration.OutputType; import org.thingsboard.server.common.data.cf.configuration.ReferencedEntityKey; import org.thingsboard.server.common.data.cf.configuration.SimpleCalculatedFieldConfiguration; import org.thingsboard.server.common.data.id.DeviceId; -import org.thingsboard.server.common.data.id.EntityId; import org.thingsboard.server.common.data.security.Authority; import org.thingsboard.server.dao.service.DaoSqlTest; @@ -85,7 +84,7 @@ public class CalculatedFieldControllerTest extends AbstractControllerTest { assertThat(savedCalculatedField.getEntityId()).isEqualTo(calculatedField.getEntityId()); assertThat(savedCalculatedField.getType()).isEqualTo(calculatedField.getType()); assertThat(savedCalculatedField.getName()).isEqualTo(calculatedField.getName()); - assertThat(savedCalculatedField.getConfiguration()).isEqualTo(getCalculatedFieldConfig(testDevice.getId())); + assertThat(savedCalculatedField.getConfiguration()).isEqualTo(getCalculatedFieldConfig()); assertThat(savedCalculatedField.getVersion()).isEqualTo(1L); savedCalculatedField.setName("Test CF"); @@ -134,16 +133,16 @@ public class CalculatedFieldControllerTest extends AbstractControllerTest { calculatedField.setType(CalculatedFieldType.SIMPLE); calculatedField.setName("Test Calculated Field"); calculatedField.setConfigurationVersion(1); - calculatedField.setConfiguration(getCalculatedFieldConfig(null)); + calculatedField.setConfiguration(getCalculatedFieldConfig()); calculatedField.setVersion(1L); return calculatedField; } - private CalculatedFieldConfiguration getCalculatedFieldConfig(EntityId referencedEntityId) { + private CalculatedFieldConfiguration getCalculatedFieldConfig() { SimpleCalculatedFieldConfiguration config = new SimpleCalculatedFieldConfiguration(); Argument argument = new Argument(); - argument.setRefEntityId(referencedEntityId); + argument.setRefEntityId(null); ReferencedEntityKey refEntityKey = new ReferencedEntityKey("temperature", ArgumentType.TS_LATEST, null); argument.setRefEntityKey(refEntityKey); diff --git a/application/src/test/java/org/thingsboard/server/controller/DeviceConnectivityControllerTest.java b/application/src/test/java/org/thingsboard/server/controller/DeviceConnectivityControllerTest.java index 2a6c847591..4ccf40415d 100644 --- a/application/src/test/java/org/thingsboard/server/controller/DeviceConnectivityControllerTest.java +++ b/application/src/test/java/org/thingsboard/server/controller/DeviceConnectivityControllerTest.java @@ -23,6 +23,7 @@ import org.junit.After; import org.junit.Assert; import org.junit.Before; import org.junit.Test; +import org.springframework.beans.factory.annotation.Value; import org.springframework.test.annotation.DirtiesContext; import org.springframework.test.context.TestPropertySource; import org.thingsboard.common.util.JacksonUtil; @@ -94,6 +95,9 @@ public class DeviceConnectivityControllerTest extends AbstractControllerTest { private DeviceProfileId mqttDeviceProfileId; private DeviceProfileId coapDeviceProfileId; + @Value("${device.connectivity.gateway.image_version:3.7-stable}") + private String gatewayImageVersion; + @Before public void beforeTest() throws Exception { loginSysAdmin(); @@ -298,7 +302,7 @@ public class DeviceConnectivityControllerTest extends AbstractControllerTest { "services:\n" + " # ThingsBoard IoT Gateway Service Configuration\n" + " tb-gateway:\n" + - " image: thingsboard/tb-gateway\n" + + " image: thingsboard/tb-gateway:" + gatewayImageVersion + "\n" + " container_name: tb-gateway\n" + " restart: always\n" + "\n" + @@ -847,7 +851,7 @@ public class DeviceConnectivityControllerTest extends AbstractControllerTest { "-t \"application/json\" -e \"{temperature:25}\" coap://test.domain:5683/api/v1/%s/telemetry", credentials.getCredentialsId())); assertThat(linuxCoapCommands.get(COAPS).get(0).asText()).isEqualTo("curl -f -S -o " + CA_ROOT_CERT_PEM + " http://localhost:80/api/device-connectivity/coaps/certificate/download"); assertThat(linuxCoapCommands.get(COAPS).get(1).asText()).isEqualTo(String.format("coap-client-openssl -v 6 -m POST " + - "-R "+ CA_ROOT_CERT_PEM + " -t \"application/json\" -e \"{temperature:25}\" coaps://test.domain:5684/api/v1/%s/telemetry", credentials.getCredentialsId())); + "-R " + CA_ROOT_CERT_PEM + " -t \"application/json\" -e \"{temperature:25}\" coaps://test.domain:5684/api/v1/%s/telemetry", credentials.getCredentialsId())); JsonNode dockerCoapCommands = commands.get(COAP).get(DOCKER); assertThat(dockerCoapCommands.get(COAP).asText()).isEqualTo(String.format("docker run --rm -it " + diff --git a/application/src/test/java/org/thingsboard/server/controller/EdgeControllerTest.java b/application/src/test/java/org/thingsboard/server/controller/EdgeControllerTest.java index 41a85721a8..27c7da09b2 100644 --- a/application/src/test/java/org/thingsboard/server/controller/EdgeControllerTest.java +++ b/application/src/test/java/org/thingsboard/server/controller/EdgeControllerTest.java @@ -885,6 +885,10 @@ public class EdgeControllerTest extends AbstractControllerTest { device.setType("default"); Device savedDevice = doPost("/api/device", device, Device.class); + // create public customer + doPost("/api/customer/public/device/" + savedDevice.getId().getId(), Device.class); + doDelete("/api/customer/device/" + savedDevice.getId().getId(), Device.class); + simulateEdgeActivation(edge); doPost("/api/edge/" + edge.getId().getId().toString() diff --git a/application/src/test/java/org/thingsboard/server/controller/EntityQueryControllerTest.java b/application/src/test/java/org/thingsboard/server/controller/EntityQueryControllerTest.java index 90b7ea7c7b..29a208a805 100644 --- a/application/src/test/java/org/thingsboard/server/controller/EntityQueryControllerTest.java +++ b/application/src/test/java/org/thingsboard/server/controller/EntityQueryControllerTest.java @@ -499,7 +499,8 @@ public class EntityQueryControllerTest extends AbstractControllerTest { ); EntityDataPageLink pageLink = new EntityDataPageLink(10, 0, null, sortOrder); List entityFields = Collections.singletonList(new EntityKey(EntityKeyType.ENTITY_FIELD, "name")); - List latestValues = Collections.singletonList(new EntityKey(EntityKeyType.ATTRIBUTE, "temperature")); + List latestValues = List.of(new EntityKey(EntityKeyType.ATTRIBUTE, "temperature"), + new EntityKey(EntityKeyType.ATTRIBUTE, "non-existing-attribute")); EntityDataQuery query = new EntityDataQuery(filter, pageLink, entityFields, latestValues, null); PageData data = findByQueryAndCheck(query, 67); @@ -517,6 +518,14 @@ public class EntityQueryControllerTest extends AbstractControllerTest { List deviceTemperatures = temperatures.stream().map(aLong -> Long.toString(aLong)).collect(Collectors.toList()); Assert.assertEquals(deviceTemperatures, loadedTemperatures); + // check ts value == 0, value is empty string for non-existing data points + List loadedNonExistingAttributes = loadedEntities.stream().map(entityData -> + entityData.getLatest().get(EntityKeyType.ATTRIBUTE).get("non-existing-attribute")).toList(); + loadedNonExistingAttributes.forEach(tsValue -> { + assertThat(tsValue.getTs()).isEqualTo(0L); + assertThat(tsValue.getValue()).isEqualTo(""); + }); + pageLink = new EntityDataPageLink(10, 0, null, sortOrder); KeyFilter highTemperatureFilter = new KeyFilter(); highTemperatureFilter.setKey(new EntityKey(EntityKeyType.ATTRIBUTE, "temperature")); diff --git a/application/src/test/java/org/thingsboard/server/controller/TbTestWebSocketClient.java b/application/src/test/java/org/thingsboard/server/controller/TbTestWebSocketClient.java index 959db43125..1c5b1dd154 100644 --- a/application/src/test/java/org/thingsboard/server/controller/TbTestWebSocketClient.java +++ b/application/src/test/java/org/thingsboard/server/controller/TbTestWebSocketClient.java @@ -31,6 +31,7 @@ import org.thingsboard.server.service.ws.AuthCmd; import org.thingsboard.server.service.ws.WsCmd; import org.thingsboard.server.service.ws.WsCommandsWrapper; import org.thingsboard.server.service.ws.telemetry.cmd.v1.AttributesSubscriptionCmd; +import org.thingsboard.server.service.ws.telemetry.cmd.v1.TimeseriesSubscriptionCmd; import org.thingsboard.server.service.ws.telemetry.cmd.v2.AlarmCountUpdate; import org.thingsboard.server.service.ws.telemetry.cmd.v2.EntityCountUpdate; import org.thingsboard.server.service.ws.telemetry.cmd.v2.EntityDataCmd; @@ -38,6 +39,7 @@ import org.thingsboard.server.service.ws.telemetry.cmd.v2.EntityDataUpdate; import org.thingsboard.server.service.ws.telemetry.cmd.v2.EntityHistoryCmd; import org.thingsboard.server.service.ws.telemetry.cmd.v2.LatestValueCmd; import org.thingsboard.server.service.ws.telemetry.cmd.v2.TimeSeriesCmd; +import org.thingsboard.server.service.ws.telemetry.sub.TelemetrySubscriptionUpdate; import java.net.URI; import java.nio.channels.NotYetConnectedException; @@ -271,6 +273,18 @@ public class TbTestWebSocketClient extends WebSocketClient { return sendEntityDataQuery(edq); } + public JsonNode sendTimeseriesCmd(EntityId entityId, String scope) { + log.warn("sendTimeseriesCmd entityId: {}, scope: {}", entityId, scope); + TimeseriesSubscriptionCmd cmd = new TimeseriesSubscriptionCmd(0, 0, 0, 10, null); + cmd.setEntityId(entityId.getId().toString()); + cmd.setEntityType(entityId.getEntityType().toString()); + cmd.setCmdId(1); + cmd.setScope(scope); + send(cmd); + String msg = this.waitForReply(); + return JacksonUtil.fromString(msg, JsonNode.class); + } + public void send(WsCmd... cmds) { WsCommandsWrapper cmdsWrapper = new WsCommandsWrapper(); cmdsWrapper.setCmds(List.of(cmds)); diff --git a/application/src/test/java/org/thingsboard/server/controller/UserControllerTest.java b/application/src/test/java/org/thingsboard/server/controller/UserControllerTest.java index 7d40e56a57..d7aa6f5770 100644 --- a/application/src/test/java/org/thingsboard/server/controller/UserControllerTest.java +++ b/application/src/test/java/org/thingsboard/server/controller/UserControllerTest.java @@ -233,16 +233,9 @@ public class UserControllerTest extends AbstractControllerTest { .put("password", "testPassword2"); Mockito.doNothing().when(mailService).sendPasswordWasResetEmail(anyString(), anyString()); - JsonNode tokenInfo = readResponse( - doPost("/api/noauth/resetPassword", resetPasswordRequest) - .andExpect(status().isOk()), JsonNode.class); + doPost("/api/noauth/resetPassword", resetPasswordRequest) + .andExpect(status().isOk()); Mockito.verify(mailService).sendPasswordWasResetEmail(anyString(), anyString()); - validateAndSetJwtToken(tokenInfo, email); - - doGet("/api/auth/user") - .andExpect(status().isOk()) - .andExpect(jsonPath("$.authority", is(Authority.TENANT_ADMIN.name()))) - .andExpect(jsonPath("$.email", is(email))); resetTokens(); diff --git a/application/src/test/java/org/thingsboard/server/controller/WebsocketApiTest.java b/application/src/test/java/org/thingsboard/server/controller/WebsocketApiTest.java index 9801907d3b..3842cf917b 100644 --- a/application/src/test/java/org/thingsboard/server/controller/WebsocketApiTest.java +++ b/application/src/test/java/org/thingsboard/server/controller/WebsocketApiTest.java @@ -68,11 +68,13 @@ import org.thingsboard.server.service.ws.telemetry.cmd.v2.AlarmStatusUpdate; import org.thingsboard.server.service.ws.telemetry.cmd.v2.EntityCountCmd; import org.thingsboard.server.service.ws.telemetry.cmd.v2.EntityCountUpdate; import org.thingsboard.server.service.ws.telemetry.cmd.v2.EntityDataUpdate; +import org.thingsboard.server.service.ws.telemetry.sub.TelemetrySubscriptionUpdate; import java.util.ArrayList; import java.util.Arrays; import java.util.Collections; import java.util.List; +import java.util.Map; import java.util.concurrent.CountDownLatch; import java.util.concurrent.TimeUnit; @@ -584,6 +586,33 @@ public class WebsocketApiTest extends AbstractControllerTest { Assert.assertNull(msg); } + @Test + public void testTimeseriesSubscriptionCmd() throws Exception { + long now = System.currentTimeMillis() - 100; + + long lastTs = now - TimeUnit.MINUTES.toMillis(1); + TsKvEntry dataPoint1 = new BasicTsKvEntry(lastTs, new LongDataEntry("temperature", 42L)); + sendTelemetry(device, List.of(dataPoint1)); + + JsonNode update = getWsClient().sendTimeseriesCmd(device.getId(), "LATEST_TELEMETRY"); + JsonNode data = update.get("data"); + Assert.assertEquals(1, data.size()); + Assert.assertEquals(JacksonUtil.newArrayNode().add(lastTs).add("42"), data.get("temperature").get(0)); + + //Sending update from the past, while latest value has new timestamp; + TsKvEntry dataPoint4 = new BasicTsKvEntry(now - TimeUnit.MINUTES.toMillis(5), new LongDataEntry("temperature", 45L)); + getWsClient().registerWaitForUpdate(); + sendTelemetry(device, List.of(dataPoint4)); + String msg = getWsClient().waitForUpdate(TimeUnit.SECONDS.toMillis(1)); + Assert.assertNull(msg); + + //Sending duplicate update again + getWsClient().registerWaitForUpdate(); + sendTelemetry(device, List.of(dataPoint4)); + msg = getWsClient().waitForUpdate(TimeUnit.SECONDS.toMillis(1)); + Assert.assertNull(msg); + } + @Test public void testEntityDataLatestTsWsCmd() throws Exception { long now = System.currentTimeMillis(); 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 feac7adb04..d84e0ebea0 100644 --- a/application/src/test/java/org/thingsboard/server/edge/AbstractEdgeTest.java +++ b/application/src/test/java/org/thingsboard/server/edge/AbstractEdgeTest.java @@ -168,6 +168,10 @@ abstract public class AbstractEdgeTest extends AbstractControllerTest { Device savedDevice = saveDevice("Edge Device 1", THERMOSTAT_DEVICE_PROFILE_NAME); + // create public customer + doPost("/api/customer/public/device/" + savedDevice.getId().getId(), Device.class); + doDelete("/api/customer/device/" + savedDevice.getId().getId(), Device.class); + Asset savedAsset = saveAsset("Edge Asset 1"); updateRootRuleChainMetadata(); diff --git a/application/src/test/java/org/thingsboard/server/edge/TenantProfileEdgeTest.java b/application/src/test/java/org/thingsboard/server/edge/TenantProfileEdgeTest.java index e58126737d..51cfa44ff9 100644 --- a/application/src/test/java/org/thingsboard/server/edge/TenantProfileEdgeTest.java +++ b/application/src/test/java/org/thingsboard/server/edge/TenantProfileEdgeTest.java @@ -44,9 +44,8 @@ public class TenantProfileEdgeTest extends AbstractEdgeTest { @Test public void testTenantProfiles() throws Exception { loginSysAdmin(); - - // save current values into tmp to revert after test - TenantProfile edgeTenantProfile = doGet("/api/tenantProfile/" + tenantProfileId.getId(), TenantProfile.class); + TenantProfile originalTenantProfile = doGet("/api/tenantProfile/" + tenantProfileId.getId(), TenantProfile.class); + TenantProfile edgeTenantProfile = new TenantProfile(originalTenantProfile); // updated edge tenant profile edgeTenantProfile.setName("Tenant Profile Edge Test"); @@ -64,14 +63,15 @@ public class TenantProfileEdgeTest extends AbstractEdgeTest { Assert.assertEquals("Updated tenant profile Edge Test", tenantProfileMsg.getDescription()); Assert.assertEquals("Tenant Profile Edge Test", tenantProfileMsg.getName()); + doPost("/api/tenantProfile", originalTenantProfile, TenantProfile.class); loginTenantAdmin(); } @Test public void testIsolatedTenantProfile() throws Exception { loginSysAdmin(); - - TenantProfile edgeTenantProfile = doGet("/api/tenantProfile/" + tenantProfileId.getId(), TenantProfile.class); + TenantProfile originalTenantProfile = doGet("/api/tenantProfile/" + tenantProfileId.getId(), TenantProfile.class); + TenantProfile edgeTenantProfile = new TenantProfile(originalTenantProfile); // set tenant profile isolated and add 2 queues - main and isolated edgeTenantProfile.setIsolatedTbRuleEngine(true); @@ -110,6 +110,10 @@ public class TenantProfileEdgeTest extends AbstractEdgeTest { Assert.assertNotNull(queue); Assert.assertEquals(tenantId, queue.getTenantId()); } + + loginSysAdmin(); + doPost("/api/tenantProfile", originalTenantProfile, TenantProfile.class); + loginTenantAdmin(); } private TenantProfileQueueConfiguration createQueueConfig(String queueName, String queueTopic) { diff --git a/application/src/test/java/org/thingsboard/server/service/edge/EdgeMsgConstructorUtilsTest.java b/application/src/test/java/org/thingsboard/server/service/edge/EdgeMsgConstructorUtilsTest.java new file mode 100644 index 0000000000..a4c2133f1c --- /dev/null +++ b/application/src/test/java/org/thingsboard/server/service/edge/EdgeMsgConstructorUtilsTest.java @@ -0,0 +1,156 @@ +/** + * Copyright © 2016-2025 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.service.edge; + +import lombok.extern.slf4j.Slf4j; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.TestInstance; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.MethodSource; +import org.thingsboard.common.util.JacksonUtil; +import org.thingsboard.rule.engine.action.TbSaveToCustomCassandraTableNode; +import org.thingsboard.rule.engine.api.NodeConfiguration; +import org.thingsboard.rule.engine.api.TbNode; +import org.thingsboard.rule.engine.aws.lambda.TbAwsLambdaNode; +import org.thingsboard.rule.engine.filter.TbCheckRelationNode; +import org.thingsboard.rule.engine.flow.TbAckNode; +import org.thingsboard.rule.engine.math.TbMathNode; +import org.thingsboard.rule.engine.metadata.CalculateDeltaNode; +import org.thingsboard.rule.engine.metadata.TbGetTelemetryNode; +import org.thingsboard.rule.engine.rest.TbSendRestApiCallReplyNode; +import org.thingsboard.rule.engine.telemetry.TbMsgAttributesNode; +import org.thingsboard.rule.engine.telemetry.TbMsgTimeseriesNode; +import org.thingsboard.server.common.data.rule.RuleChainMetaData; +import org.thingsboard.server.common.data.rule.RuleNode; +import org.thingsboard.server.gen.edge.v1.EdgeVersion; +import org.thingsboard.server.gen.edge.v1.UpdateMsgType; + +import java.lang.reflect.Constructor; +import java.util.List; +import java.util.Optional; +import java.util.stream.Stream; + +import static org.thingsboard.server.service.edge.EdgeMsgConstructorUtils.EXCLUDED_NODES_BY_EDGE_VERSION; +import static org.thingsboard.server.service.edge.EdgeMsgConstructorUtils.IGNORED_PARAMS_BY_EDGE_VERSION; + +@Slf4j +@TestInstance(TestInstance.Lifecycle.PER_CLASS) +public class EdgeMsgConstructorUtilsTest { + + private static final int CONFIGURATION_VERSION = 5; + + static Stream provideEdgeVersions() { + return Stream.of( + EdgeVersion.V_4_0_0, + EdgeVersion.V_3_9_0, + EdgeVersion.V_3_8_0, + EdgeVersion.V_3_7_0 + ); + } + + private static final RuleChainMetaData RULE_CHAIN_META_DATA = new RuleChainMetaData(); + private static final List TEST_NODES = + List.of( + new TbSaveToCustomCassandraTableNode(), + new TbMsgAttributesNode(), + new TbMsgTimeseriesNode(), + new TbSendRestApiCallReplyNode(), + new TbAwsLambdaNode(), + + new TbMathNode(), + new CalculateDeltaNode(), + new TbAckNode(), + new TbCheckRelationNode(), + new TbGetTelemetryNode() + ); + + @BeforeAll + static void setUp() { + List ruleNodes = TEST_NODES.stream() + .map(node -> { + RuleNode ruleNode = new RuleNode(); + ruleNode.setName(node.getClass().getName()); + ruleNode.setType(node.getClass().getName()); + ruleNode.setConfigurationVersion(CONFIGURATION_VERSION); + ruleNode.setConfiguration(JacksonUtil.valueToTree(createDefaultConfiguration(node))); + return ruleNode; + }) + .toList(); + + RULE_CHAIN_META_DATA.setFirstNodeIndex(0); + RULE_CHAIN_META_DATA.setNodes(ruleNodes); + } + + private static NodeConfiguration createDefaultConfiguration(TbNode node) { + try { + org.thingsboard.rule.engine.api.RuleNode annotation = node.getClass().getAnnotation(org.thingsboard.rule.engine.api.RuleNode.class); + Constructor constructor = annotation.configClazz().getConstructor(); + NodeConfiguration configInstance = (NodeConfiguration) constructor.newInstance(); + + return configInstance.defaultConfiguration(); + } catch (Exception e) { + throw new RuntimeException("Exception during creating RuleNodeConfiguration for node - " + node, e); + } + } + + @ParameterizedTest(name = "Test Sanitize Metadata For Edge: {0}") + @MethodSource("provideEdgeVersions") + @DisplayName("Test Sanitize Metadata For Legacy Edge Version") + public void testSanitizeMetadataForLegacyEdgeVersion(EdgeVersion edgeVersion) { + // WHEN + List ruleNodes = sanitizeMetadataForLegacyEdgeVersion(edgeVersion); + + // THEN + ruleNodes.forEach(ruleNode -> { + checkUpdateNodeConfigurationsForLegacyEdge(ruleNode, edgeVersion); + checkRemoveExcludedNodesForLegacyEdge(ruleNode, edgeVersion); + }); + } + + private List sanitizeMetadataForLegacyEdgeVersion(EdgeVersion edgeVersion) { + String metadataUpdateMsg = EdgeMsgConstructorUtils.constructRuleChainMetadataUpdatedMsg( + UpdateMsgType.ENTITY_CREATED_RPC_MESSAGE, + RULE_CHAIN_META_DATA, + edgeVersion + ).getEntity(); + + RuleChainMetaData updatedMetaData = JacksonUtil.fromString(metadataUpdateMsg, RuleChainMetaData.class, true); + Assertions.assertNotNull(updatedMetaData, "RuleChainMetaData should not be null after update."); + + return updatedMetaData.getNodes(); + } + + private void checkUpdateNodeConfigurationsForLegacyEdge(RuleNode ruleNode, EdgeVersion edgeVersion) { + if (IGNORED_PARAMS_BY_EDGE_VERSION.containsKey(edgeVersion) && IGNORED_PARAMS_BY_EDGE_VERSION.get(edgeVersion).containsKey(ruleNode.getType())) { + String ignoredParam = IGNORED_PARAMS_BY_EDGE_VERSION.get(edgeVersion).get(ruleNode.getType()); + + Assertions.assertFalse(ruleNode.getConfiguration().has(ignoredParam), + String.format("RuleNode '%s' for EdgeVersion '%s' should ignore '%s' config parameter.", ruleNode.getName(), edgeVersion, ignoredParam)); + } + } + + private void checkRemoveExcludedNodesForLegacyEdge(RuleNode ruleNode, EdgeVersion edgeVersion) { + boolean isNodeExcluded = Optional.ofNullable(EXCLUDED_NODES_BY_EDGE_VERSION.get(edgeVersion)) + .map(excludedNodes -> !excludedNodes.contains(ruleNode.getType())) + .orElse(true); + + Assertions.assertTrue(isNodeExcluded, + String.format("For EdgeVersion '%s', ruleNode '%s' should not be included.", edgeVersion, ruleNode.getType())); + } + +} diff --git a/application/src/test/java/org/thingsboard/server/service/entitiy/EntityServiceTest.java b/application/src/test/java/org/thingsboard/server/service/entitiy/EntityServiceTest.java index e5c4009715..a2c3fc07c8 100644 --- a/application/src/test/java/org/thingsboard/server/service/entitiy/EntityServiceTest.java +++ b/application/src/test/java/org/thingsboard/server/service/entitiy/EntityServiceTest.java @@ -95,7 +95,6 @@ import org.thingsboard.server.dao.edge.EdgeService; import org.thingsboard.server.dao.entity.EntityService; import org.thingsboard.server.dao.entityview.EntityViewDao; import org.thingsboard.server.dao.entityview.EntityViewService; -import org.thingsboard.server.dao.model.sqlts.ts.TsKvEntity; import org.thingsboard.server.dao.relation.RelationService; import org.thingsboard.server.dao.service.DaoSqlTest; import org.thingsboard.server.dao.sql.relation.RelationRepository; @@ -1664,7 +1663,7 @@ public class EntityServiceTest extends AbstractControllerTest { List> timeseriesFutures = new ArrayList<>(); for (int i = 0; i < devices.size(); i++) { Device device = devices.get(i); - timeseriesFutures.add(saveLongTimeseries(device.getId(), "temperature", temperatures.get(i))); + timeseriesFutures.add(saveTimeseries(device.getId(), "temperature", temperatures.get(i))); } Futures.allAsList(timeseriesFutures).get(); @@ -1711,6 +1710,13 @@ public class EntityServiceTest extends AbstractControllerTest { List entitiesTelemetry = loadedEntities.stream().map(entityData -> entityData.getLatest().get(EntityKeyType.TIME_SERIES).get("temperature").getValue()).toList(); assertThat(entitiesTelemetry).containsExactlyElementsOf(expectedSortedList); + // update temperature to long value for one of device + long longTempValue = -100L; + saveTimeseries(devices.get(new Random().nextInt(66)).getId(), "temperature", longTempValue).get(); + loadedEntities = loadAllData(querySortByTemp, deviceTemperatures.size()); + entitiesTelemetry = loadedEntities.stream().map(entityData -> entityData.getLatest().get(EntityKeyType.TIME_SERIES).get("temperature").getValue()).toList(); + assertThat(entitiesTelemetry.get(0)).isEqualTo(String.valueOf(longTempValue)); + deviceService.deleteDevicesByTenantId(tenantId); } @@ -2252,15 +2258,18 @@ public class EntityServiceTest extends AbstractControllerTest { return attributesService.save(tenantId, entityId, scope, Collections.singletonList(attr)); } - private ListenableFuture saveLongTimeseries(EntityId entityId, String key, Double value) { - TsKvEntity tsKv = new TsKvEntity(); - tsKv.setStrKey(key); - tsKv.setDoubleValue(value); + private ListenableFuture saveTimeseries(EntityId entityId, String key, Double value) { KvEntry telemetryValue = new DoubleDataEntry(key, value); BasicTsKvEntry timeseries = new BasicTsKvEntry(42L, telemetryValue); return timeseriesService.save(tenantId, entityId, timeseries); } + private ListenableFuture saveTimeseries(EntityId entityId, String key, Long value) { + KvEntry telemetryValue = new LongDataEntry(key, value); + BasicTsKvEntry timeseries = new BasicTsKvEntry(42L, telemetryValue); + return timeseriesService.save(tenantId, entityId, timeseries); + } + protected void createMultiRootHierarchy(List buildings, List apartments, Map> entityNameByTypeMap, Map childParentRelationMap) throws InterruptedException { diff --git a/application/src/test/java/org/thingsboard/server/service/sync/vc/VersionControlTest.java b/application/src/test/java/org/thingsboard/server/service/sync/vc/VersionControlTest.java index 06f61ca4e5..29c3af5387 100644 --- a/application/src/test/java/org/thingsboard/server/service/sync/vc/VersionControlTest.java +++ b/application/src/test/java/org/thingsboard/server/service/sync/vc/VersionControlTest.java @@ -621,6 +621,7 @@ public class VersionControlTest extends AbstractControllerTest { assertThat(importedField.getName()).isEqualTo(deviceCalculatedField.getName()); assertThat(importedField.getType()).isEqualTo(deviceCalculatedField.getType()); assertThat(importedField.getId()).isNotEqualTo(deviceCalculatedField.getId()); + assertThat(importedField.getConfiguration().getArguments().get("T").getRefEntityId()).isEqualTo(importedAsset.getId()); }); List importedAssetCalculatedFields = findCalculatedFieldsByEntityId(importedAsset.getId()); @@ -629,6 +630,7 @@ public class VersionControlTest extends AbstractControllerTest { assertThat(importedField.getName()).isEqualTo(assetCalculatedField.getName()); assertThat(importedField.getType()).isEqualTo(assetCalculatedField.getType()); assertThat(importedField.getId()).isNotEqualTo(assetCalculatedField.getId()); + assertThat(importedField.getConfiguration().getArguments().get("T").getRefEntityId()).isEqualTo(importedDevice.getId()); }); } diff --git a/common/dao-api/src/main/java/org/thingsboard/server/dao/customer/CustomerService.java b/common/dao-api/src/main/java/org/thingsboard/server/dao/customer/CustomerService.java index 5420cd5297..d478e2099a 100644 --- a/common/dao-api/src/main/java/org/thingsboard/server/dao/customer/CustomerService.java +++ b/common/dao-api/src/main/java/org/thingsboard/server/dao/customer/CustomerService.java @@ -41,6 +41,8 @@ public interface CustomerService extends EntityDaoService { Customer findOrCreatePublicCustomer(TenantId tenantId); + Customer findPublicCustomer(TenantId tenantId); + PageData findCustomersByTenantId(TenantId tenantId, PageLink pageLink); void deleteCustomersByTenantId(TenantId tenantId); diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/DataConstants.java b/common/data/src/main/java/org/thingsboard/server/common/data/DataConstants.java index b53d6daec2..b2d9d59cca 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/DataConstants.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/DataConstants.java @@ -148,4 +148,6 @@ public class DataConstants { public static final String CF_QUEUE_NAME = "CalculatedFields"; public static final String CF_STATES_QUEUE_NAME = "CalculatedFieldStates"; + public static final String LATEST_TELEMETRY_SCOPE = "LATEST_TELEMETRY"; + } diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/JavaSerDesUtil.java b/common/data/src/main/java/org/thingsboard/server/common/data/JavaSerDesUtil.java index e14e2d5fa1..55a0cd90aa 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/JavaSerDesUtil.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/JavaSerDesUtil.java @@ -36,7 +36,7 @@ public class JavaSerDesUtil { try (ObjectInputStream ois = new ObjectInputStream(is)) { return (T) ois.readObject(); } catch (IOException | ClassNotFoundException e) { - log.error("Error during deserialization message, [{}]", e.getMessage()); + log.error("Error during deserialization", e); return null; } } @@ -50,7 +50,7 @@ public class JavaSerDesUtil { ois.writeObject(msq); return boas.toByteArray(); } catch (IOException e) { - log.error("Error during serialization message, [{}]", e.getMessage()); + log.error("Error during serialization", e); throw new RuntimeException(e); } } diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/cf/CalculatedField.java b/common/data/src/main/java/org/thingsboard/server/common/data/cf/CalculatedField.java index b86f30ca78..3b2ddf0627 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 @@ -64,7 +64,7 @@ public class CalculatedField extends BaseData implements HasN @Schema(description = "Version of calculated field configuration.", example = "0") private int configurationVersion; @Schema(implementation = SimpleCalculatedFieldConfiguration.class) - private transient CalculatedFieldConfiguration configuration; + private CalculatedFieldConfiguration configuration; @Getter @Setter private Long version; @@ -112,6 +112,18 @@ public class CalculatedField extends BaseData implements HasN return super.getCreatedTime(); } + // Getter is ignored for serialization + @JsonIgnore + public boolean isDebugMode() { + return debugMode; + } + + // Setter is annotated for deserialization + @JsonSetter + public void setDebugMode(boolean debugMode) { + this.debugMode = debugMode; + } + @Override public String toString() { return new StringBuilder() @@ -128,16 +140,4 @@ public class CalculatedField extends BaseData implements HasN .toString(); } - // Getter is ignored for serialization - @JsonIgnore - public boolean isDebugMode() { - return debugMode; - } - - // Setter is annotated for deserialization - @JsonSetter - public void setDebugMode(boolean debugMode) { - this.debugMode = debugMode; - } - } diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/cf/configuration/ScriptCalculatedFieldConfiguration.java b/common/data/src/main/java/org/thingsboard/server/common/data/cf/configuration/ScriptCalculatedFieldConfiguration.java index 0971217fdf..c2dde43b8e 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/cf/configuration/ScriptCalculatedFieldConfiguration.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/cf/configuration/ScriptCalculatedFieldConfiguration.java @@ -16,13 +16,16 @@ package org.thingsboard.server.common.data.cf.configuration; import lombok.Data; +import lombok.EqualsAndHashCode; import org.thingsboard.server.common.data.cf.CalculatedFieldType; @Data +@EqualsAndHashCode(callSuper = true) public class ScriptCalculatedFieldConfiguration extends BaseCalculatedFieldConfiguration implements CalculatedFieldConfiguration { @Override public CalculatedFieldType getType() { return CalculatedFieldType.SCRIPT; } + } diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/cf/configuration/SimpleCalculatedFieldConfiguration.java b/common/data/src/main/java/org/thingsboard/server/common/data/cf/configuration/SimpleCalculatedFieldConfiguration.java index 79a0518ba0..5c0ce71e86 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/cf/configuration/SimpleCalculatedFieldConfiguration.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/cf/configuration/SimpleCalculatedFieldConfiguration.java @@ -16,13 +16,16 @@ package org.thingsboard.server.common.data.cf.configuration; import lombok.Data; +import lombok.EqualsAndHashCode; import org.thingsboard.server.common.data.cf.CalculatedFieldType; @Data +@EqualsAndHashCode(callSuper = true) public class SimpleCalculatedFieldConfiguration extends BaseCalculatedFieldConfiguration implements CalculatedFieldConfiguration { @Override public CalculatedFieldType getType() { return CalculatedFieldType.SIMPLE; } + } 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 9ba6c20188..77ef4fe5a2 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.toString() : ""; + return node != null && !node.isNull() ? node.toString() : ""; } private static UUID getCustomerId(CustomerId customerId) { 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 index 9863506ed4..6f4c7643c0 100644 --- 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 @@ -35,6 +35,11 @@ public class UserFields extends AbstractEntityFields { private String phone; private String additionalInfo; + @Override + public String getName() { + return super.getEmail(); + } + public UserFields(UUID id, long createdTime, UUID tenantId, UUID customerId, Long version, String firstName, String lastName, String email, String phone, JsonNode additionalInfo) { diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/event/CalculatedFieldDebugEventFilter.java b/common/data/src/main/java/org/thingsboard/server/common/data/event/CalculatedFieldDebugEventFilter.java index 55ce036d9e..a2cd119d32 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/event/CalculatedFieldDebugEventFilter.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/event/CalculatedFieldDebugEventFilter.java @@ -37,10 +37,9 @@ public class CalculatedFieldDebugEventFilter extends DebugEventFilter { example = "{\"x\":{\"ts\":1739432016629,\"value\":20},\"y\":{\"ts\":1739429717656,\"value\":12}}") protected String arguments; @Schema(description = "String value representing the result of a calculation", - example = "{\"x + y\":54}") + example = "{\"x + y\":32}") protected String result; - @Override public EventType getEventType() { return EventType.DEBUG_CALCULATED_FIELD; 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 072be6acf2..a1692aef04 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 @@ -107,7 +107,7 @@ public class EntityExportData> { @JsonIgnore public boolean hasCalculatedFields() { - return calculatedFields != null; + return calculatedFields != null && !calculatedFields.isEmpty(); } } diff --git a/common/edqs/src/main/java/org/thingsboard/server/edqs/data/dp/BoolDataPoint.java b/common/edqs/src/main/java/org/thingsboard/server/edqs/data/dp/BoolDataPoint.java index 70a14917ba..adb03a5cb1 100644 --- a/common/edqs/src/main/java/org/thingsboard/server/edqs/data/dp/BoolDataPoint.java +++ b/common/edqs/src/main/java/org/thingsboard/server/edqs/data/dp/BoolDataPoint.java @@ -46,6 +46,10 @@ public class BoolDataPoint extends AbstractDataPoint { @Override public int compareTo(DataPoint dataPoint) { - return Boolean.compare(value, dataPoint.getBool()); + if (dataPoint.getType() == DataType.BOOLEAN) { + return Boolean.compare(value, dataPoint.getBool()); + } else { + return super.compareTo(dataPoint); + } } } 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 634b63e012..cf4267e443 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 @@ -17,13 +17,12 @@ package org.thingsboard.server.edqs.data.dp; import lombok.Getter; import lombok.SneakyThrows; +import org.thingsboard.common.util.TbBytePool; 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 { - public static final int MIN_STR_SIZE_TO_COMPRESS = 512; @Getter private final byte[] compressedValue; diff --git a/common/edqs/src/main/java/org/thingsboard/server/edqs/data/dp/DoubleDataPoint.java b/common/edqs/src/main/java/org/thingsboard/server/edqs/data/dp/DoubleDataPoint.java index 2ca8f2c03a..4660b3eb56 100644 --- a/common/edqs/src/main/java/org/thingsboard/server/edqs/data/dp/DoubleDataPoint.java +++ b/common/edqs/src/main/java/org/thingsboard/server/edqs/data/dp/DoubleDataPoint.java @@ -46,6 +46,10 @@ public class DoubleDataPoint extends AbstractDataPoint { @Override public int compareTo(DataPoint dataPoint) { - return Double.compare(value, dataPoint.getDouble()); + if (dataPoint.getType() == DataType.DOUBLE || dataPoint.getType() == DataType.LONG) { + return Double.compare(value, dataPoint.getDouble()); + } else { + return super.compareTo(dataPoint); + } } } diff --git a/common/edqs/src/main/java/org/thingsboard/server/edqs/data/dp/JsonDataPoint.java b/common/edqs/src/main/java/org/thingsboard/server/edqs/data/dp/JsonDataPoint.java index 3a8d570f43..593a74e3c8 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.util.TbStringPool; +import org.thingsboard.common.util.TbStringPool; public class JsonDataPoint extends AbstractDataPoint { diff --git a/common/edqs/src/main/java/org/thingsboard/server/edqs/data/dp/LongDataPoint.java b/common/edqs/src/main/java/org/thingsboard/server/edqs/data/dp/LongDataPoint.java index 92c0a972e7..a1d33203b7 100644 --- a/common/edqs/src/main/java/org/thingsboard/server/edqs/data/dp/LongDataPoint.java +++ b/common/edqs/src/main/java/org/thingsboard/server/edqs/data/dp/LongDataPoint.java @@ -51,6 +51,12 @@ public class LongDataPoint extends AbstractDataPoint { @Override public int compareTo(DataPoint dataPoint) { - return Long.compare(value, dataPoint.getLong()); + if (dataPoint.getType() == DataType.DOUBLE) { + return Double.compare(getDouble(), dataPoint.getDouble()); + } else if (dataPoint.getType() == DataType.LONG) { + return Long.compare(value, dataPoint.getLong()); + } else { + return super.compareTo(dataPoint); + } } } 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 54156500fe..52205e2f72 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.util.TbStringPool; +import org.thingsboard.common.util.TbStringPool; public class StringDataPoint extends AbstractDataPoint { diff --git a/common/edqs/src/main/java/org/thingsboard/server/edqs/repo/DefaultEdqsRepository.java b/common/edqs/src/main/java/org/thingsboard/server/edqs/repo/DefaultEdqsRepository.java index 1deaca83a7..215c64194f 100644 --- a/common/edqs/src/main/java/org/thingsboard/server/edqs/repo/DefaultEdqsRepository.java +++ b/common/edqs/src/main/java/org/thingsboard/server/edqs/repo/DefaultEdqsRepository.java @@ -27,10 +27,9 @@ import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.common.data.page.PageData; 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.common.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; @@ -42,7 +41,7 @@ import java.util.function.Predicate; public class DefaultEdqsRepository implements EdqsRepository { private final static ConcurrentMap repos = new ConcurrentHashMap<>(); - private final Optional statsService; + private final EdqsStatsService statsService; public TenantRepo get(TenantId tenantId) { return repos.computeIfAbsent(tenantId, id -> new TenantRepo(id, statsService)); 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 b9f3589280..a47559d6d8 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 @@ -25,7 +25,6 @@ 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.EntityFields; import org.thingsboard.server.common.data.edqs.query.QueryResult; import org.thingsboard.server.common.data.id.CustomerId; @@ -41,6 +40,7 @@ import org.thingsboard.server.common.data.query.EntityKeyType; 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.common.stats.EdqsStatsService; import org.thingsboard.server.edqs.data.ApiUsageStateData; import org.thingsboard.server.edqs.data.AssetData; import org.thingsboard.server.edqs.data.CustomerData; @@ -55,9 +55,7 @@ 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 org.thingsboard.server.edqs.util.TbStringPool; import java.util.ArrayList; import java.util.Collections; @@ -65,7 +63,6 @@ import java.util.Comparator; import java.util.HashMap; import java.util.List; import java.util.Map; -import java.util.Optional; import java.util.Set; import java.util.TreeSet; import java.util.UUID; @@ -96,9 +93,9 @@ public class TenantRepo { private final Lock entityUpdateLock = new ReentrantLock(); private final TenantId tenantId; - private final Optional edqsStatsService; + private final EdqsStatsService edqsStatsService; - public TenantRepo(TenantId tenantId, Optional edqsStatsService) { + public TenantRepo(TenantId tenantId, EdqsStatsService edqsStatsService) { this.tenantId = tenantId; this.edqsStatsService = edqsStatsService; } @@ -144,9 +141,9 @@ public class TenantRepo { 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())); + boolean added = repo.add(from, to, entity.getType()); if (added) { - edqsStatsService.ifPresent(statService -> statService.reportEvent(tenantId, ObjectType.RELATION, EdqsEventType.UPDATED)); + edqsStatsService.reportAdded(ObjectType.RELATION); } } else if (RelationTypeGroup.DASHBOARD.equals(entity.getTypeGroup())) { if (EntityRelation.CONTAINS_TYPE.equals(entity.getType()) && entity.getFrom().getEntityType() == EntityType.CUSTOMER) { @@ -166,7 +163,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.reportEvent(tenantId, ObjectType.RELATION, EdqsEventType.DELETED)); + edqsStatsService.reportRemoved(ObjectType.RELATION); } } } else if (RelationTypeGroup.DASHBOARD.equals(entityRelation.getTypeGroup())) { @@ -188,7 +185,6 @@ public class TenantRepo { EntityType entityType = entity.getType(); EntityData entityData = getOrCreate(entityType, entityId); - processFields(fields); EntityFields oldFields = entityData.getFields(); entityData.setFields(fields); if (oldFields == null) { @@ -225,7 +221,7 @@ public class TenantRepo { if (removed.getFields() != null) { getEntitySet(entityType).remove(removed); } - edqsStatsService.ifPresent(statService -> statService.reportEvent(tenantId, ObjectType.fromEntityType(entityType), EdqsEventType.DELETED)); + edqsStatsService.reportRemoved(entity.type()); UUID customerId = removed.getCustomerId(); if (customerId != null) { @@ -246,7 +242,7 @@ public class TenantRepo { Integer keyId = KeyDictionary.get(attributeKv.getKey()); boolean added = entityData.putAttr(keyId, attributeKv.getScope(), attributeKv.getDataPoint()); if (added) { - edqsStatsService.ifPresent(statService -> statService.reportEvent(tenantId, ObjectType.ATTRIBUTE_KV, EdqsEventType.UPDATED)); + edqsStatsService.reportAdded(ObjectType.ATTRIBUTE_KV); } } } @@ -256,7 +252,7 @@ public class TenantRepo { if (entityData != null) { boolean removed = entityData.removeAttr(KeyDictionary.get(attributeKv.getKey()), attributeKv.getScope()); if (removed) { - edqsStatsService.ifPresent(statService -> statService.reportEvent(tenantId, ObjectType.ATTRIBUTE_KV, EdqsEventType.DELETED)); + edqsStatsService.reportRemoved(ObjectType.ATTRIBUTE_KV); } } } @@ -267,7 +263,7 @@ public class TenantRepo { Integer keyId = KeyDictionary.get(latestTsKv.getKey()); boolean added = entityData.putTs(keyId, latestTsKv.getDataPoint()); if (added) { - edqsStatsService.ifPresent(statService -> statService.reportEvent(tenantId, ObjectType.LATEST_TS_KV, EdqsEventType.UPDATED)); + edqsStatsService.reportAdded(ObjectType.LATEST_TS_KV); } } } @@ -277,17 +273,11 @@ public class TenantRepo { if (entityData != null) { boolean removed = entityData.removeTs(KeyDictionary.get(latestTsKv.getKey())); if (removed) { - edqsStatsService.ifPresent(statService -> statService.reportEvent(tenantId, ObjectType.LATEST_TS_KV, EdqsEventType.DELETED)); + edqsStatsService.reportRemoved(ObjectType.LATEST_TS_KV); } } } - public void processFields(EntityFields fields) { - if (fields instanceof AssetFields assetFields) { - assetFields.setType(TbStringPool.intern(assetFields.getType())); - } - } - public ConcurrentMap> getEntityMap(EntityType entityType) { return entityMapByType.computeIfAbsent(entityType, et -> new ConcurrentHashMap<>()); } @@ -301,7 +291,7 @@ public class TenantRepo { return getEntityMap(entityType).computeIfAbsent(entityId, id -> { log.debug("[{}] Adding {} {}", tenantId, entityType, id); EntityData entityData = constructEntityData(entityType, entityId); - edqsStatsService.ifPresent(statService -> statService.reportEvent(tenantId, ObjectType.fromEntityType(entityType), EdqsEventType.UPDATED)); + edqsStatsService.reportAdded(ObjectType.fromEntityType(entityType)); return entityData; }); } diff --git a/common/edqs/src/main/java/org/thingsboard/server/edqs/stats/DefaultEdqsStatsService.java b/common/edqs/src/main/java/org/thingsboard/server/edqs/stats/DefaultEdqsStatsService.java new file mode 100644 index 0000000000..3767d0f60b --- /dev/null +++ b/common/edqs/src/main/java/org/thingsboard/server/edqs/stats/DefaultEdqsStatsService.java @@ -0,0 +1,92 @@ +/** + * 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.edqs.stats; + +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.ObjectType; +import org.thingsboard.server.common.data.id.TenantId; +import org.thingsboard.server.common.data.query.EntityCountQuery; +import org.thingsboard.server.common.data.query.EntityDataQuery; +import org.thingsboard.server.common.stats.EdqsStatsService; +import org.thingsboard.server.common.stats.StatsFactory; +import org.thingsboard.server.common.stats.StatsTimer; +import org.thingsboard.server.common.stats.StatsType; + +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicInteger; + +@Service +@Slf4j +@ConditionalOnExpression("'${queue.edqs.api.supported:true}' == 'true' && '${queue.edqs.stats.enabled:true}' == 'true'") +public class DefaultEdqsStatsService implements EdqsStatsService { + + private final StatsFactory statsFactory; + + @Value("${queue.edqs.stats.slow_query_threshold:3000}") + private int slowQueryThreshold; + + private final ConcurrentHashMap objectCounters = new ConcurrentHashMap<>(); + private final StatsTimer dataQueryTimer; + private final StatsTimer countQueryTimer; + + private DefaultEdqsStatsService(StatsFactory statsFactory) { + this.statsFactory = statsFactory; + dataQueryTimer = statsFactory.createTimer(StatsType.EDQS, "entityDataQueryTimer"); + countQueryTimer = statsFactory.createTimer(StatsType.EDQS, "entityCountQueryTimer"); + } + + @Override + public void reportAdded(ObjectType objectType) { + getObjectCounter(objectType).incrementAndGet(); + } + + @Override + public void reportRemoved(ObjectType objectType) { + getObjectCounter(objectType).decrementAndGet(); + } + + @Override + public void reportDataQuery(TenantId tenantId, EntityDataQuery query, long timingNanos) { + double timingMs = timingNanos / 1000_000.0; + if (timingMs < slowQueryThreshold) { + log.debug("[{}] Executed data query in {} ms: {}", tenantId, timingMs, query); + } else { + log.warn("[{}] Executed slow data query in {} ms: {}", tenantId, timingMs, query); + } + dataQueryTimer.record(timingNanos, TimeUnit.NANOSECONDS); + } + + @Override + public void reportCountQuery(TenantId tenantId, EntityCountQuery query, long timingNanos) { + double timingMs = timingNanos / 1000_000.0; + if (timingMs < slowQueryThreshold) { + log.debug("[{}] Executed count query in {} ms: {}", tenantId, timingMs, query); + } else { + log.warn("[{}] Executed slow count query in {} ms: {}", tenantId, timingMs, query); + } + countQueryTimer.record(timingNanos, TimeUnit.NANOSECONDS); + } + + private AtomicInteger getObjectCounter(ObjectType objectType) { + return objectCounters.computeIfAbsent(objectType, type -> + statsFactory.createGauge("edqsObjectsCount", new AtomicInteger(), "objectType", type.name())); + } + +} 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 deleted file mode 100644 index 442453fc93..0000000000 --- a/common/edqs/src/main/java/org/thingsboard/server/edqs/stats/EdqsStatsService.java +++ /dev/null @@ -1,81 +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.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.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; - - public void reportEvent(TenantId tenantId, ObjectType objectType, EdqsEventType eventType) { - statsMap.computeIfAbsent(tenantId, id -> new EdqsStats(tenantId, statsFactory)) - .reportEvent(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 reportEvent(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/EdqsConverter.java b/common/edqs/src/main/java/org/thingsboard/server/edqs/util/EdqsConverter.java index 5b4cd7ac4a..167037b889 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 @@ -17,13 +17,19 @@ package org.thingsboard.server.edqs.util; import com.fasterxml.jackson.annotation.JsonAutoDetect; import com.fasterxml.jackson.annotation.PropertyAccessor; +import com.fasterxml.jackson.core.JsonParser; +import com.fasterxml.jackson.databind.DeserializationContext; import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.deser.std.StdDeserializer; import com.fasterxml.jackson.databind.json.JsonMapper; +import com.fasterxml.jackson.databind.module.SimpleModule; import com.google.protobuf.ByteString; import lombok.RequiredArgsConstructor; import lombok.SneakyThrows; import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Value; import org.springframework.stereotype.Service; +import org.thingsboard.common.util.TbStringPool; import org.thingsboard.server.common.data.AttributeScope; import org.thingsboard.server.common.data.EntityType; import org.thingsboard.server.common.data.ObjectType; @@ -49,6 +55,7 @@ import org.thingsboard.server.gen.transport.TransportProtos; import org.thingsboard.server.gen.transport.TransportProtos.DataPointProto; import org.xerial.snappy.Snappy; +import java.io.IOException; import java.util.HashMap; import java.util.Map; import java.util.UUID; @@ -57,6 +64,9 @@ import java.util.UUID; @Slf4j public class EdqsConverter { + @Value("${queue.edqs.string_compression_length_threshold:512}") + private int stringCompressionLengthThreshold; + private final Map> converters = new HashMap<>(); private final Converter defaultConverter = new JsonConverter<>(Entity.class); @@ -125,7 +135,7 @@ public class EdqsConverter { }); } - public static DataPointProto toDataPointProto(long ts, KvEntry kvEntry) { + public DataPointProto toDataPointProto(long ts, KvEntry kvEntry) { DataPointProto.Builder proto = DataPointProto.newBuilder(); proto.setTs(ts); switch (kvEntry.getDataType()) { @@ -134,7 +144,7 @@ public class EdqsConverter { case DOUBLE -> proto.setDoubleV(kvEntry.getDoubleValue().get()); case STRING -> { String strValue = kvEntry.getStrValue().get(); - if (strValue.length() < CompressedStringDataPoint.MIN_STR_SIZE_TO_COMPRESS) { + if (strValue.length() < stringCompressionLengthThreshold) { proto.setStringV(strValue); } else { proto.setCompressedStringV(ByteString.copyFrom(compress(strValue))); @@ -142,7 +152,7 @@ public class EdqsConverter { } case JSON -> { String jsonValue = kvEntry.getJsonValue().get(); - if (jsonValue.length() < CompressedStringDataPoint.MIN_STR_SIZE_TO_COMPRESS) { + if (jsonValue.length() < stringCompressionLengthThreshold) { proto.setJsonV(jsonValue); } else { proto.setCompressedJsonV(ByteString.copyFrom(compress(jsonValue))); @@ -152,7 +162,7 @@ public class EdqsConverter { return proto.build(); } - public static DataPoint fromDataPointProto(DataPointProto proto) { + public DataPoint fromDataPointProto(DataPointProto proto) { long ts = proto.getTs(); if (proto.hasBoolV()) { return new BoolDataPoint(ts, proto.getBoolV()); @@ -188,14 +198,6 @@ public class EdqsConverter { 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) { @@ -220,12 +222,18 @@ public class EdqsConverter { @RequiredArgsConstructor private static class JsonConverter implements Converter { + private static final SimpleModule module = new SimpleModule(); 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(); + static { + module.addDeserializer(String.class, new InterningStringDeserializer()); + mapper.registerModule(module); + } + private final Class type; @SneakyThrows @@ -250,4 +258,17 @@ public class EdqsConverter { } + public static class InterningStringDeserializer extends StdDeserializer { + + public InterningStringDeserializer() { + super(String.class); + } + + @Override + public String deserialize(JsonParser p, DeserializationContext ctx) throws IOException { + return TbStringPool.intern(p.getText()); + } + + } + } diff --git a/common/edqs/src/main/java/org/thingsboard/server/edqs/util/RepositoryUtils.java b/common/edqs/src/main/java/org/thingsboard/server/edqs/util/RepositoryUtils.java index 71b58d759a..f355d83e62 100644 --- a/common/edqs/src/main/java/org/thingsboard/server/edqs/util/RepositoryUtils.java +++ b/common/edqs/src/main/java/org/thingsboard/server/edqs/util/RepositoryUtils.java @@ -344,7 +344,7 @@ public class RepositoryUtils { if (dp != null) { return new TsValue(dp.getTs() > 0 ? dp.getTs() : ts, dp.valueToString()); } else { - return new TsValue(ts, ""); + return TsValue.EMPTY; } } 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 2a0fad0643..7271e1cb10 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 @@ -38,7 +38,7 @@ public class TopicService { @Value("${queue.rule-engine.notifications-topic:tb_rule_engine.notifications}") private String tbRuleEngineNotificationsTopic; - @Value("${queue.transport.notifications-topics:tb_transport.notifications}") + @Value("${queue.transport.notifications-topic:tb_transport.notifications}") private String tbTransportNotificationsTopic; @Value("${queue.edge.notifications-topic:tb_edge.notifications}") @@ -47,7 +47,7 @@ public class TopicService { @Value("${queue.edge.event-notifications-topic:tb_edge_event.notifications}") private String tbEdgeEventNotificationsTopic; - @Value("${queue.calculated_fields.notifications-topic:calculated_field.notifications}") + @Value("${queue.calculated-fields.notifications-topic:calculated_field.notifications}") private String tbCalculatedFieldNotificationsTopic; private final ConcurrentMap tbCoreNotificationTopics = new ConcurrentHashMap<>(); diff --git a/common/queue/src/main/java/org/thingsboard/server/queue/kafka/TbKafkaSettings.java b/common/queue/src/main/java/org/thingsboard/server/queue/kafka/TbKafkaSettings.java index 181d422f16..82b5af179f 100644 --- a/common/queue/src/main/java/org/thingsboard/server/queue/kafka/TbKafkaSettings.java +++ b/common/queue/src/main/java/org/thingsboard/server/queue/kafka/TbKafkaSettings.java @@ -50,8 +50,6 @@ import java.util.Properties; @Component public class TbKafkaSettings { - private static final List DYNAMIC_TOPICS = List.of("tb_edge_event.notifications"); - @Value("${queue.kafka.bootstrap.servers}") private String servers; @@ -163,18 +161,20 @@ public class TbKafkaSettings { props.put(ConsumerConfig.KEY_DESERIALIZER_CLASS_CONFIG, StringDeserializer.class); props.put(ConsumerConfig.VALUE_DESERIALIZER_CLASS_CONFIG, ByteArrayDeserializer.class); - consumerPropertiesPerTopic - .getOrDefault(topic, Collections.emptyList()) - .forEach(kv -> props.put(kv.getKey(), kv.getValue())); - if (topic != null) { - DYNAMIC_TOPICS.stream() - .filter(topic::startsWith) - .findFirst() - .ifPresent(prefix -> consumerPropertiesPerTopic.getOrDefault(prefix, Collections.emptyList()) - .forEach(kv -> props.put(kv.getKey(), kv.getValue()))); + List properties = consumerPropertiesPerTopic.get(topic); + if (properties == null) { + for (Map.Entry> entry : consumerPropertiesPerTopic.entrySet()) { + if (topic.startsWith(entry.getKey())) { + properties = entry.getValue(); + break; + } + } + } + if (properties != null) { + properties.forEach(kv -> props.put(kv.getKey(), kv.getValue())); + } } - return props; } 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 7c3e415e9f..98a3d78304 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 @@ -115,7 +115,6 @@ public class TbCoreQueueProducerProvider implements TbQueueProducerProvider { return toHousekeeper; } - @Override public TbQueueProducer> getTbEdgeMsgProducer() { return toEdge; diff --git a/common/stats/src/main/java/org/thingsboard/server/common/stats/DummyEdqsStatsService.java b/common/stats/src/main/java/org/thingsboard/server/common/stats/DummyEdqsStatsService.java new file mode 100644 index 0000000000..df78e5fc89 --- /dev/null +++ b/common/stats/src/main/java/org/thingsboard/server/common/stats/DummyEdqsStatsService.java @@ -0,0 +1,41 @@ +/** + * 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.stats; + +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.id.TenantId; +import org.thingsboard.server.common.data.query.EntityCountQuery; +import org.thingsboard.server.common.data.query.EntityDataQuery; + +@Service +@ConditionalOnMissingBean(value = EdqsStatsService.class, ignored = DummyEdqsStatsService.class) +public class DummyEdqsStatsService implements EdqsStatsService { + + @Override + public void reportAdded(ObjectType objectType) {} + + @Override + public void reportRemoved(ObjectType objectType) {} + + @Override + public void reportDataQuery(TenantId tenantId, EntityDataQuery query, long timingNanos) {} + + @Override + public void reportCountQuery(TenantId tenantId, EntityCountQuery query, long timingNanos) {} + +} diff --git a/common/stats/src/main/java/org/thingsboard/server/common/stats/EdqsStatsService.java b/common/stats/src/main/java/org/thingsboard/server/common/stats/EdqsStatsService.java new file mode 100644 index 0000000000..106e43e913 --- /dev/null +++ b/common/stats/src/main/java/org/thingsboard/server/common/stats/EdqsStatsService.java @@ -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. + */ +package org.thingsboard.server.common.stats; + +import org.thingsboard.server.common.data.ObjectType; +import org.thingsboard.server.common.data.id.TenantId; +import org.thingsboard.server.common.data.query.EntityCountQuery; +import org.thingsboard.server.common.data.query.EntityDataQuery; + +public interface EdqsStatsService { + + void reportAdded(ObjectType objectType); + + void reportRemoved(ObjectType objectType); + + void reportDataQuery(TenantId tenantId, EntityDataQuery query, long timingNanos); + + void reportCountQuery(TenantId tenantId, EntityCountQuery query, long timingNanos); + +} diff --git a/common/stats/src/main/java/org/thingsboard/server/common/stats/StatsTimer.java b/common/stats/src/main/java/org/thingsboard/server/common/stats/StatsTimer.java index 89f33ac922..b9f3533b92 100644 --- a/common/stats/src/main/java/org/thingsboard/server/common/stats/StatsTimer.java +++ b/common/stats/src/main/java/org/thingsboard/server/common/stats/StatsTimer.java @@ -34,10 +34,14 @@ public class StatsTimer { this.timer = micrometerTimer; } - public void record(long timeMs) { + public void record(long timeMs) { + record(timeMs, TimeUnit.MILLISECONDS); + } + + public void record(long timing, TimeUnit timeUnit) { count++; - totalTime += timeMs; - timer.record(timeMs, TimeUnit.MILLISECONDS); + totalTime += timeUnit.toMillis(timing); + timer.record(timing, timeUnit); } public double getAvg() { @@ -47,7 +51,7 @@ public class StatsTimer { return (double) totalTime / count; } - public void reset() { + public void reset() { count = 0; totalTime = 0; } diff --git a/common/util/pom.xml b/common/util/pom.xml index 82768cdb68..7fdd3036ae 100644 --- a/common/util/pom.xml +++ b/common/util/pom.xml @@ -111,6 +111,11 @@ org.locationtech.jts jts-core + + net.objecthunter + exp4j + ${exp4j.version} + diff --git a/common/util/src/main/java/org/thingsboard/common/util/ExpressionFunctionsUtil.java b/common/util/src/main/java/org/thingsboard/common/util/ExpressionFunctionsUtil.java new file mode 100644 index 0000000000..b1753e7a17 --- /dev/null +++ b/common/util/src/main/java/org/thingsboard/common/util/ExpressionFunctionsUtil.java @@ -0,0 +1,78 @@ +/** + * 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.common.util; + +import net.objecthunter.exp4j.function.Function; +import net.objecthunter.exp4j.function.Functions; + +import java.util.ArrayList; +import java.util.List; + +public class ExpressionFunctionsUtil { + + public static final List userDefinedFunctions = new ArrayList<>(); + + static { + userDefinedFunctions.add( + new Function("ln") { + @Override + public double apply(double... args) { + return Math.log(args[0]); + } + } + ); + userDefinedFunctions.add( + new Function("lg") { + @Override + public double apply(double... args) { + return Math.log10(args[0]); + } + } + ); + userDefinedFunctions.add( + new Function("logab", 2) { + @Override + public double apply(double... args) { + return Math.log(args[1]) / Math.log(args[0]); + } + } + ); + userDefinedFunctions.add(Functions.getBuiltinFunction("sin")); + userDefinedFunctions.add(Functions.getBuiltinFunction("cos")); + userDefinedFunctions.add(Functions.getBuiltinFunction("tan")); + userDefinedFunctions.add(Functions.getBuiltinFunction("cot")); + userDefinedFunctions.add(Functions.getBuiltinFunction("log")); + userDefinedFunctions.add(Functions.getBuiltinFunction("log2")); + userDefinedFunctions.add(Functions.getBuiltinFunction("log10")); + userDefinedFunctions.add(Functions.getBuiltinFunction("log1p")); + userDefinedFunctions.add(Functions.getBuiltinFunction("abs")); + userDefinedFunctions.add(Functions.getBuiltinFunction("acos")); + userDefinedFunctions.add(Functions.getBuiltinFunction("asin")); + userDefinedFunctions.add(Functions.getBuiltinFunction("atan")); + userDefinedFunctions.add(Functions.getBuiltinFunction("cbrt")); + userDefinedFunctions.add(Functions.getBuiltinFunction("floor")); + userDefinedFunctions.add(Functions.getBuiltinFunction("sinh")); + userDefinedFunctions.add(Functions.getBuiltinFunction("sqrt")); + userDefinedFunctions.add(Functions.getBuiltinFunction("tanh")); + userDefinedFunctions.add(Functions.getBuiltinFunction("cosh")); + userDefinedFunctions.add(Functions.getBuiltinFunction("ceil")); + userDefinedFunctions.add(Functions.getBuiltinFunction("pow")); + userDefinedFunctions.add(Functions.getBuiltinFunction("exp")); + userDefinedFunctions.add(Functions.getBuiltinFunction("expm1")); + userDefinedFunctions.add(Functions.getBuiltinFunction("signum")); + } + +} diff --git a/common/edqs/src/main/java/org/thingsboard/server/edqs/util/TbBytePool.java b/common/util/src/main/java/org/thingsboard/common/util/TbBytePool.java similarity index 96% rename from common/edqs/src/main/java/org/thingsboard/server/edqs/util/TbBytePool.java rename to common/util/src/main/java/org/thingsboard/common/util/TbBytePool.java index 3b135be59c..fe16a14e7c 100644 --- a/common/edqs/src/main/java/org/thingsboard/server/edqs/util/TbBytePool.java +++ b/common/util/src/main/java/org/thingsboard/common/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.util; +package org.thingsboard.common.util; import com.google.common.hash.Hashing; import org.springframework.util.ConcurrentReferenceHashMap; diff --git a/common/edqs/src/main/java/org/thingsboard/server/edqs/util/TbStringPool.java b/common/util/src/main/java/org/thingsboard/common/util/TbStringPool.java similarity index 96% rename from common/edqs/src/main/java/org/thingsboard/server/edqs/util/TbStringPool.java rename to common/util/src/main/java/org/thingsboard/common/util/TbStringPool.java index 9c9c3b5b13..38c010fbd3 100644 --- a/common/edqs/src/main/java/org/thingsboard/server/edqs/util/TbStringPool.java +++ b/common/util/src/main/java/org/thingsboard/common/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.util; +package org.thingsboard.common.util; import org.springframework.util.ConcurrentReferenceHashMap; diff --git a/common/version-control/src/main/java/org/thingsboard/server/service/sync/vc/GitRepository.java b/common/version-control/src/main/java/org/thingsboard/server/service/sync/vc/GitRepository.java index 2bbca7a578..df0f79171f 100644 --- a/common/version-control/src/main/java/org/thingsboard/server/service/sync/vc/GitRepository.java +++ b/common/version-control/src/main/java/org/thingsboard/server/service/sync/vc/GitRepository.java @@ -41,6 +41,7 @@ import org.eclipse.jgit.diff.HistogramDiff; import org.eclipse.jgit.diff.RawText; import org.eclipse.jgit.diff.RawTextComparator; import org.eclipse.jgit.errors.LargeObjectException; +import org.eclipse.jgit.errors.RepositoryNotFoundException; import org.eclipse.jgit.lib.Constants; import org.eclipse.jgit.lib.ObjectId; import org.eclipse.jgit.lib.ObjectLoader; @@ -147,22 +148,31 @@ public class GitRepository { } public static GitRepository openOrClone(Path directory, RepositorySettings settings, boolean fetch) throws IOException, GitAPIException { - GitRepository repository; if (GitRepository.exists(directory.toString())) { - repository = GitRepository.open(directory.toFile(), settings); - if (fetch) { - repository.fetch(); + try { + GitRepository repository = GitRepository.open(directory.toFile(), settings); + if (fetch) { + repository.fetch(); + } + return repository; + } catch (RepositoryNotFoundException e) { + log.warn("{} not a git repository, reinitializing", directory); + } catch (org.eclipse.jgit.errors.TransportException | org.eclipse.jgit.api.errors.TransportException e) { + if (StringUtils.containsIgnoreCase(e.getMessage(), "missing commit")) { + log.warn("Couldn't fetch {} due to {}, reinitializing", directory, e.getMessage()); + } else { + throw e; + } } + } + + FileUtils.deleteDirectory(directory.toFile()); + Files.createDirectories(directory); + if (settings.isLocalOnly()) { + return GitRepository.create(settings, directory.toFile()); } else { - FileUtils.deleteDirectory(directory.toFile()); - Files.createDirectories(directory); - if (settings.isLocalOnly()) { - repository = GitRepository.create(settings, directory.toFile()); - } else { - repository = GitRepository.clone(settings, directory.toFile()); - } + return GitRepository.clone(settings, directory.toFile()); } - return repository; } public static void test(RepositorySettings settings, File directory) throws Exception { diff --git a/dao/src/main/java/org/thingsboard/server/dao/customer/CustomerServiceImpl.java b/dao/src/main/java/org/thingsboard/server/dao/customer/CustomerServiceImpl.java index 4b05cdbc75..b06902d95e 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/customer/CustomerServiceImpl.java +++ b/dao/src/main/java/org/thingsboard/server/dao/customer/CustomerServiceImpl.java @@ -213,12 +213,11 @@ public class CustomerServiceImpl extends AbstractCachedEntityService INCORRECT_TENANT_ID + id); - Optional publicCustomerOpt = customerDao.findPublicCustomerByTenantId(tenantId.getId()); - if (publicCustomerOpt.isPresent()) { - return publicCustomerOpt.get(); + var publicCustomer = findPublicCustomer(tenantId); + if (publicCustomer != null) { + return publicCustomer; } - var publicCustomer = new Customer(); + publicCustomer = new Customer(); publicCustomer.setTenantId(tenantId); publicCustomer.setTitle(PUBLIC_CUSTOMER_TITLE); try { @@ -230,7 +229,7 @@ public class CustomerServiceImpl extends AbstractCachedEntityService publicCustomerOpt = customerDao.findPublicCustomerByTenantId(tenantId.getId()); if (publicCustomerOpt.isPresent()) { return publicCustomerOpt.get(); } @@ -239,6 +238,14 @@ public class CustomerServiceImpl extends AbstractCachedEntityService INCORRECT_TENANT_ID + id); + Optional publicCustomerOpt = customerDao.findPublicCustomerByTenantId(tenantId.getId()); + return publicCustomerOpt.orElse(null); + } + @Override public PageData findCustomersByTenantId(TenantId tenantId, PageLink pageLink) { log.trace("Executing findCustomersByTenantId, tenantId [{}], pageLink [{}]", tenantId, pageLink); diff --git a/dao/src/main/java/org/thingsboard/server/dao/device/DeviceConnectivityServiceImpl.java b/dao/src/main/java/org/thingsboard/server/dao/device/DeviceConnectivityServiceImpl.java index 606388e465..6f22e95c87 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/device/DeviceConnectivityServiceImpl.java +++ b/dao/src/main/java/org/thingsboard/server/dao/device/DeviceConnectivityServiceImpl.java @@ -41,7 +41,6 @@ import org.thingsboard.server.common.data.security.DeviceCredentialsType; import org.thingsboard.server.dao.settings.AdminSettingsService; import org.thingsboard.server.dao.util.DeviceConnectivityUtil; -import java.io.IOException; import java.io.InputStream; import java.io.InputStreamReader; import java.net.URISyntaxException; @@ -86,6 +85,8 @@ public class DeviceConnectivityServiceImpl implements DeviceConnectivityService private String mqttsPemCertFile; @Value("${device.connectivity.coaps.pem_cert_file:}") private String coapsPemCertFile; + @Value("${device.connectivity.gateway.image_version:3.7-stable}") + private String gatewayImageVersion; @Override public JsonNode findDevicePublishTelemetryCommands(String baseUrl, Device device) throws URISyntaxException { @@ -157,7 +158,8 @@ public class DeviceConnectivityServiceImpl implements DeviceConnectivityService String mqttType = isEnabled(MQTTS) ? MQTTS : MQTT; DeviceConnectivityInfo properties = getConnectivity(mqttType); DeviceCredentials creds = deviceCredentialsService.findDeviceCredentialsByDeviceId(device.getTenantId(), device.getId()); - return DeviceConnectivityUtil.getGatewayDockerComposeFile(baseUrl, properties, creds, mqttType); + String host = getHost(baseUrl, properties, mqttType); + return DeviceConnectivityUtil.getGatewayDockerComposeFile(host, gatewayImageVersion, creds); } private DeviceConnectivityInfo getConnectivity(String protocol) { 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 a762aabf38..4df33779ee 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 @@ -20,6 +20,7 @@ import org.springframework.beans.factory.annotation.Autowired; import org.springframework.context.annotation.Lazy; import org.springframework.stereotype.Service; import org.springframework.util.CollectionUtils; +import org.thingsboard.common.util.TbStopWatch; import org.thingsboard.server.common.data.EntityType; import org.thingsboard.server.common.data.HasCustomerId; import org.thingsboard.server.common.data.HasEmail; @@ -46,6 +47,7 @@ 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.msg.edqs.EdqsApiService; +import org.thingsboard.server.common.stats.EdqsStatsService; import org.thingsboard.server.dao.exception.IncorrectParameterException; import java.util.ArrayList; @@ -89,6 +91,9 @@ public class BaseEntityService extends AbstractEntityService implements EntitySe @Lazy private EdqsApiService edqsApiService; + @Autowired + private EdqsStatsService edqsStatsService; + @Override public long countEntitiesByQuery(TenantId tenantId, CustomerId customerId, EntityCountQuery query) { log.trace("Executing countEntitiesByQuery, tenantId [{}], customerId [{}], query [{}]", tenantId, customerId, query); @@ -96,14 +101,19 @@ public class BaseEntityService extends AbstractEntityService implements EntitySe validateId(customerId, id -> INCORRECT_CUSTOMER_ID + id); validateEntityCountQuery(query); + TbStopWatch stopWatch = TbStopWatch.create(); + Long result; if (edqsApiService.isEnabled() && validForEdqs(query) && !tenantId.isSysTenantId()) { EdqsRequest request = EdqsRequest.builder() .entityCountQuery(query) .build(); EdqsResponse response = processEdqsRequest(tenantId, customerId, request); - return response.getEntityCountQueryResult(); + result = response.getEntityCountQueryResult(); + } else { + result = entityQueryDao.countEntitiesByQuery(tenantId, customerId, query); } - return this.entityQueryDao.countEntitiesByQuery(tenantId, customerId, query); + edqsStatsService.reportCountQuery(tenantId, query, stopWatch.stopAndGetTotalTimeNanos()); + return result; } @Override @@ -113,27 +123,31 @@ public class BaseEntityService extends AbstractEntityService implements EntitySe validateId(customerId, id -> INCORRECT_CUSTOMER_ID + id); validateEntityDataQuery(query); + TbStopWatch stopWatch = TbStopWatch.create(); + PageData result; if (edqsApiService.isEnabled() && validForEdqs(query)) { EdqsRequest request = EdqsRequest.builder() .entityDataQuery(query) .build(); EdqsResponse response = processEdqsRequest(tenantId, customerId, request); - return response.getEntityDataQueryResult(); - } - - if (!isValidForOptimization(query)) { - return this.entityQueryDao.findEntityDataByQuery(tenantId, customerId, query); - } - - // 1 step - find entity data by filter and sort columns - PageData entityDataByQuery = findEntityIdsByFilterAndSorterColumns(tenantId, customerId, query); - if (entityDataByQuery == null || entityDataByQuery.getData().isEmpty()) { - return entityDataByQuery; + result = response.getEntityDataQueryResult(); + } else { + if (!isValidForOptimization(query)) { + result = entityQueryDao.findEntityDataByQuery(tenantId, customerId, query); + } else { + // 1 step - find entity data by filter and sort columns + PageData entityDataByQuery = findEntityIdsByFilterAndSorterColumns(tenantId, customerId, query); + if (entityDataByQuery == null || entityDataByQuery.getData().isEmpty()) { + result = entityDataByQuery; + } else { + // 2 step - find entity data by entity ids from the 1st step + List entities = fetchEntityDataByIdsFromInitialQuery(tenantId, customerId, query, entityDataByQuery.getData()); + result = new PageData<>(entities, entityDataByQuery.getTotalPages(), entityDataByQuery.getTotalElements(), entityDataByQuery.hasNext()); + } + } } - - // 2 step - find entity data by entity ids from the 1st step - List result = fetchEntityDataByIdsFromInitialQuery(tenantId, customerId, query, entityDataByQuery.getData()); - return new PageData<>(result, entityDataByQuery.getTotalPages(), entityDataByQuery.getTotalElements(), entityDataByQuery.hasNext()); + edqsStatsService.reportDataQuery(tenantId, query, stopWatch.stopAndGetTotalTimeNanos()); + return result; } private boolean validForEdqs(EntityCountQuery query) { // for compatibility with PE diff --git a/dao/src/main/java/org/thingsboard/server/dao/sql/NamedParameterJdbcTemplateConfiguration.java b/dao/src/main/java/org/thingsboard/server/dao/sql/NamedParameterJdbcTemplateConfiguration.java new file mode 100644 index 0000000000..ef8d0ed064 --- /dev/null +++ b/dao/src/main/java/org/thingsboard/server/dao/sql/NamedParameterJdbcTemplateConfiguration.java @@ -0,0 +1,43 @@ +/** + * Copyright © 2016-2025 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.dao.sql; + +import jakarta.annotation.PostConstruct; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.jdbc.core.namedparam.NamedParameterJdbcTemplate; +import org.springframework.stereotype.Component; + +import java.util.concurrent.TimeUnit; + +@Component +@RequiredArgsConstructor +@Slf4j +public class NamedParameterJdbcTemplateConfiguration { + + @Value("${spring.jpa.properties.javax.persistence.query.timeout:30000}") + private int queryTimeout; + + private final NamedParameterJdbcTemplate namedParameterJdbcTemplate; + + @PostConstruct + private void init() { + int timeout = Math.max(1, (int) TimeUnit.MILLISECONDS.toSeconds(queryTimeout)); + log.info("Set jdbcTemplate query timeout [{}] second(s)", timeout); + namedParameterJdbcTemplate.getJdbcTemplate().setQueryTimeout(timeout); + } +} diff --git a/dao/src/main/java/org/thingsboard/server/dao/sql/event/JpaBaseEventDao.java b/dao/src/main/java/org/thingsboard/server/dao/sql/event/JpaBaseEventDao.java index 592ed21f9a..189e024608 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/sql/event/JpaBaseEventDao.java +++ b/dao/src/main/java/org/thingsboard/server/dao/sql/event/JpaBaseEventDao.java @@ -200,6 +200,7 @@ public class JpaBaseEventDao implements EventDao { break; case DEBUG_CALCULATED_FIELD: removeEventsByFilter(tenantId, entityId, (CalculatedFieldDebugEventFilter) eventFilter, startTime, endTime); + break; default: throw new RuntimeException("Not supported event type: " + eventFilter.getEventType()); } 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 6e43034a44..d45182442a 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,18 +33,14 @@ 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 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 e8ef37b3b5..c546fc21ea 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 @@ -189,7 +189,7 @@ public class SqlTimeseriesLatestDao extends BaseAbstractSqlTimeseriesDao impleme } - private ListenableFuture getNewLatestEntryFuture(TenantId tenantId, EntityId entityId, DeleteTsKvQuery query) { + private ListenableFuture getNewLatestEntryFuture(TenantId tenantId, EntityId entityId, DeleteTsKvQuery query, Long version) { ListenableFuture> future = findNewLatestEntryFuture(tenantId, entityId, query); return Futures.transformAsync(future, entryList -> { if (entryList.size() == 1) { @@ -198,7 +198,7 @@ public class SqlTimeseriesLatestDao extends BaseAbstractSqlTimeseriesDao impleme } else { log.trace("Could not find new latest value for [{}], key - {}", entityId, query.getKey()); } - return Futures.immediateFuture(new TsKvLatestRemovingResult(query.getKey(), true)); + return Futures.immediateFuture(new TsKvLatestRemovingResult(query.getKey(), true, version)); }, service); } @@ -241,7 +241,7 @@ public class SqlTimeseriesLatestDao extends BaseAbstractSqlTimeseriesDao impleme rs -> rs.next() ? rs.getLong(1) : null, entityId.getId(), keyDictionaryDao.getOrSaveKeyId(query.getKey()))); isRemoved = true; if (query.getRewriteLatestIfDeleted()) { - return getNewLatestEntryFuture(tenantId, entityId, query); + return getNewLatestEntryFuture(tenantId, entityId, query, version); } } return Futures.immediateFuture(new TsKvLatestRemovingResult(query.getKey(), isRemoved, version)); diff --git a/dao/src/main/java/org/thingsboard/server/dao/util/DeviceConnectivityUtil.java b/dao/src/main/java/org/thingsboard/server/dao/util/DeviceConnectivityUtil.java index 56e0e44778..cae2d56ae5 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/util/DeviceConnectivityUtil.java +++ b/dao/src/main/java/org/thingsboard/server/dao/util/DeviceConnectivityUtil.java @@ -92,15 +92,13 @@ public class DeviceConnectivityUtil { return command.toString(); } - public static Resource getGatewayDockerComposeFile(String baseUrl, DeviceConnectivityInfo properties, DeviceCredentials deviceCredentials, String mqttType) throws URISyntaxException { - String host = getHost(baseUrl, properties, mqttType); - + public static Resource getGatewayDockerComposeFile(String host, String gatewayImageVersion, DeviceCredentials deviceCredentials) { StringBuilder dockerComposeBuilder = new StringBuilder(); dockerComposeBuilder.append("version: '3.4'\n"); dockerComposeBuilder.append("services:\n"); dockerComposeBuilder.append(" # ThingsBoard IoT Gateway Service Configuration\n"); dockerComposeBuilder.append(" tb-gateway:\n"); - dockerComposeBuilder.append(" image: thingsboard/tb-gateway\n"); + dockerComposeBuilder.append(" image: thingsboard/tb-gateway:").append(gatewayImageVersion).append("\n"); dockerComposeBuilder.append(" container_name: tb-gateway\n"); dockerComposeBuilder.append(" restart: always\n"); dockerComposeBuilder.append("\n"); diff --git a/dao/src/test/java/org/thingsboard/server/dao/service/timeseries/BaseTimeseriesServiceTest.java b/dao/src/test/java/org/thingsboard/server/dao/service/timeseries/BaseTimeseriesServiceTest.java index 6a38159804..56b835c528 100644 --- a/dao/src/test/java/org/thingsboard/server/dao/service/timeseries/BaseTimeseriesServiceTest.java +++ b/dao/src/test/java/org/thingsboard/server/dao/service/timeseries/BaseTimeseriesServiceTest.java @@ -81,21 +81,21 @@ public abstract class BaseTimeseriesServiceTest extends AbstractServiceTest { protected static final int MAX_TIMEOUT = 30; - private static final String STRING_KEY = "stringKey"; + protected static final String STRING_KEY = "stringKey"; private static final String LONG_KEY = "longKey"; private static final String DOUBLE_KEY = "doubleKey"; private static final String BOOLEAN_KEY = "booleanKey"; - private static final long TS = 42L; + protected static final long TS = 42L; private static final String DESC_ORDER = "DESC"; - KvEntry stringKvEntry = new StringDataEntry(STRING_KEY, "value"); + protected KvEntry stringKvEntry = new StringDataEntry(STRING_KEY, "value"); KvEntry longKvEntry = new LongDataEntry(LONG_KEY, Long.MAX_VALUE); KvEntry doubleKvEntry = new DoubleDataEntry(DOUBLE_KEY, Double.MAX_VALUE); KvEntry booleanKvEntry = new BooleanDataEntry(BOOLEAN_KEY, Boolean.TRUE); protected TenantId tenantId; - DeviceId deviceId = new DeviceId(Uuids.timeBased()); + protected DeviceId deviceId = new DeviceId(Uuids.timeBased()); @Before public void before() { @@ -811,11 +811,11 @@ public abstract class BaseTimeseriesServiceTest extends AbstractServiceTest { tsService.saveWithoutLatest(tenantId, deviceId, tsKvEntry, 0).get(MAX_TIMEOUT, TimeUnit.SECONDS); } - private static TsKvEntry toTsEntry(long ts, KvEntry entry) { + protected static TsKvEntry toTsEntry(long ts, KvEntry entry) { return new BasicTsKvEntry(ts, entry); } - private static void equalsIgnoreVersion(TsKvEntry expected, TsKvEntry actual) { + protected static void equalsIgnoreVersion(TsKvEntry expected, TsKvEntry actual) { assertEquals(expected.getKey(), actual.getKey()); assertEquals(expected.getValue(), actual.getValue()); assertEquals(expected.getTs(), actual.getTs()); diff --git a/dao/src/test/java/org/thingsboard/server/dao/service/timeseries/sql/TimeseriesServiceSqlTest.java b/dao/src/test/java/org/thingsboard/server/dao/service/timeseries/sql/TimeseriesServiceSqlTest.java index 06a81adac3..7fa5b5f7e1 100644 --- a/dao/src/test/java/org/thingsboard/server/dao/service/timeseries/sql/TimeseriesServiceSqlTest.java +++ b/dao/src/test/java/org/thingsboard/server/dao/service/timeseries/sql/TimeseriesServiceSqlTest.java @@ -15,9 +15,42 @@ */ package org.thingsboard.server.dao.service.timeseries.sql; +import org.junit.Test; +import org.thingsboard.server.common.data.kv.TsKvEntry; import org.thingsboard.server.dao.service.DaoSqlTest; import org.thingsboard.server.dao.service.timeseries.BaseTimeseriesServiceTest; +import java.util.List; +import java.util.Optional; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.TimeoutException; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.awaitility.Awaitility.await; + @DaoSqlTest public class TimeseriesServiceSqlTest extends BaseTimeseriesServiceTest { + + @Test + public void testRemoveLatestAndNoValuePresentInDB() throws ExecutionException, InterruptedException, TimeoutException { + TsKvEntry tsKvEntry = toTsEntry(TS, stringKvEntry); + tsService.save(tenantId, deviceId, tsKvEntry).get(MAX_TIMEOUT, TimeUnit.SECONDS); + + Optional tsKvEntryOpt = tsService.findLatest(tenantId, deviceId, STRING_KEY).get(MAX_TIMEOUT, TimeUnit.SECONDS); + + assertThat(tsKvEntryOpt).isPresent(); + equalsIgnoreVersion(tsKvEntry, tsKvEntryOpt.get()); + assertThat(tsKvEntryOpt.get().getVersion()).isNotNull(); + + tsService.removeLatest(tenantId, deviceId, List.of(STRING_KEY)); + + await().alias("Wait until ts last is removed from the cache").atMost(MAX_TIMEOUT, TimeUnit.SECONDS) + .pollInterval(1, TimeUnit.SECONDS) + .untilAsserted(() -> { + Optional tsKvEntryAfterRemoval = tsService.findLatest(tenantId, deviceId, STRING_KEY).get(MAX_TIMEOUT, TimeUnit.SECONDS); + assertThat(tsKvEntryAfterRemoval).isNotPresent(); + }); + } + } diff --git a/dao/src/test/java/org/thingsboard/server/dao/sql/JdbcTemplateTest.java b/dao/src/test/java/org/thingsboard/server/dao/sql/JdbcTemplateTest.java new file mode 100644 index 0000000000..d0af99021a --- /dev/null +++ b/dao/src/test/java/org/thingsboard/server/dao/sql/JdbcTemplateTest.java @@ -0,0 +1,39 @@ +/** + * Copyright © 2016-2025 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.dao.sql; + +import org.junit.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.dao.DataAccessResourceFailureException; +import org.springframework.jdbc.core.namedparam.NamedParameterJdbcTemplate; +import org.springframework.test.context.TestPropertySource; +import org.thingsboard.server.dao.AbstractJpaDaoTest; + +import static org.junit.jupiter.api.Assertions.assertThrows; + +@TestPropertySource(properties = { + "spring.jpa.properties.javax.persistence.query.timeout=500" +}) +public class JdbcTemplateTest extends AbstractJpaDaoTest { + + @Autowired + private NamedParameterJdbcTemplate jdbcTemplate; + + @Test + public void queryTimeoutTest() { + assertThrows(DataAccessResourceFailureException.class, () -> jdbcTemplate.query("SELECT pg_sleep(10)", rs -> {})); + } +} diff --git a/edqs/src/main/resources/edqs.yml b/edqs/src/main/resources/edqs.yml index 353d76391b..2c0e0b8c5d 100644 --- a/edqs/src/main/resources/edqs.yml +++ b/edqs/src/main/resources/edqs.yml @@ -71,6 +71,8 @@ queue: max_pending_requests: "${TB_EDQS_MAX_PENDING_REQUESTS:10000}" # Maximum timeout for requests to EDQS max_request_timeout: "${TB_EDQS_MAX_REQUEST_TIMEOUT:20000}" + # Strings longer than this threshold will be compressed + string_compression_length_threshold: "${TB_EDQS_STRING_COMPRESSION_LENGTH_THRESHOLD:512}" stats: # Enable/disable statistics for EDQS enabled: "${TB_EDQS_STATS_ENABLED:true}" 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 01a7495148..330d22a3c6 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 @@ -20,6 +20,7 @@ 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.boot.test.mock.mockito.MockBean; import org.springframework.context.annotation.ComponentScan; import org.springframework.context.annotation.Configuration; import org.springframework.test.context.TestExecutionListeners; @@ -58,6 +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.common.stats.DummyEdqsStatsService; import org.thingsboard.server.edqs.util.EdqsConverter; import java.util.Collections; @@ -78,6 +80,8 @@ public abstract class AbstractEDQTest { protected DefaultEdqsRepository repository; @Autowired protected EdqsConverter edqsConverter; + @MockBean + private DummyEdqsStatsService edqsStatsService; protected final TenantId tenantId = TenantId.fromUUID(UUID.randomUUID()); protected final CustomerId customerId = new CustomerId(UUID.randomUUID()); diff --git a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/math/TbMathNode.java b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/math/TbMathNode.java index 500aef0fe5..76ba71163a 100644 --- a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/math/TbMathNode.java +++ b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/math/TbMathNode.java @@ -53,6 +53,7 @@ import java.util.function.BiFunction; import java.util.function.Function; import java.util.stream.Collectors; +import static org.thingsboard.common.util.ExpressionFunctionsUtil.userDefinedFunctions; import static org.thingsboard.rule.engine.math.TbMathArgumentType.CONSTANT; @SuppressWarnings("UnstableApiUsage") @@ -310,6 +311,7 @@ public class TbMathNode implements TbNode { var expr = customExpression.get(); if (expr == null) { expr = new ExpressionBuilder(config.getCustomFunction()) + .functions(userDefinedFunctions) .implicitMultiplication(true) .variables(config.getArguments().stream().map(TbMathArgument::getName).collect(Collectors.toSet())) .build(); diff --git a/rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/math/TbMathNodeTest.java b/rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/math/TbMathNodeTest.java index 71e9ed8350..dcdd2530df 100644 --- a/rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/math/TbMathNodeTest.java +++ b/rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/math/TbMathNodeTest.java @@ -56,6 +56,8 @@ import org.thingsboard.server.common.msg.TbMsgMetaData; import org.thingsboard.server.dao.attributes.AttributesService; import org.thingsboard.server.dao.timeseries.TimeseriesService; +import java.math.BigDecimal; +import java.math.RoundingMode; import java.util.Arrays; import java.util.List; import java.util.Optional; @@ -848,6 +850,42 @@ public class TbMathNodeTest { verify(ctx, never()).tellFailure(any(), any()); } + @ParameterizedTest + @MethodSource + public void testCustomFunctions(String customFunction, double result) { + var node = initNodeWithCustomFunction(customFunction, + new TbMathResult(TbMathArgumentType.MESSAGE_BODY, "result", 2, false, false, null), + new TbMathArgument("a", TbMathArgumentType.MESSAGE_BODY, "argumentA"), + new TbMathArgument("b", TbMathArgumentType.MESSAGE_BODY, "argumentB") + ); + + TbMsg msg = TbMsg.newMsg() + .type(TbMsgType.POST_TELEMETRY_REQUEST) + .originator(originator) + .metaData(TbMsgMetaData.EMPTY) + .data("{\"argumentA\":2,\"argumentB\":5}") + .build(); + + node.onMsg(ctx, msg); + + ArgumentCaptor msgCaptor = ArgumentCaptor.forClass(TbMsg.class); + verify(ctx, timeout(TIMEOUT)).tellSuccess(msgCaptor.capture()); + TbMsg outMsg = msgCaptor.getValue(); + assertThat(outMsg).isNotNull(); + assertThat(outMsg.getData()).isNotNull(); + var resultJson = JacksonUtil.toJsonNode(outMsg.getData()); + assertThat(resultJson.has("result")).isTrue(); + assertThat(resultJson.get("result").asDouble()).isEqualTo(new BigDecimal(result).setScale(2, RoundingMode.HALF_UP).doubleValue()); + } + + private static Stream testCustomFunctions() { + return Stream.of( + Arguments.of("ln(a)", Math.log(2)), + Arguments.of("lg(a)", Math.log10(2)), + Arguments.of("logab(a, b)", Math.log(5) / Math.log(2)) + ); + } + static class RuleDispatcherExecutor extends AbstractListeningExecutor { @Override protected int getThreadPollSize() { diff --git a/ui-ngx/package.json b/ui-ngx/package.json index 6280fb94c2..e927abe7b2 100644 --- a/ui-ngx/package.json +++ b/ui-ngx/package.json @@ -26,7 +26,7 @@ "@auth0/angular-jwt": "^5.2.0", "@flowjs/flow.js": "^2.14.1", "@flowjs/ngx-flow": "18.0.1", - "@geoman-io/leaflet-geoman-free": "2.17.0", + "@geoman-io/leaflet-geoman-free": "2.18.3", "@iplab/ngx-color-picker": "^18.0.1", "@mat-datetimepicker/core": "~14.0.0", "@mdi/svg": "^7.4.47", diff --git a/ui-ngx/src/app/core/api/entity-data-subscription.ts b/ui-ngx/src/app/core/api/entity-data-subscription.ts index 6579b952af..3adda4cd27 100644 --- a/ui-ngx/src/app/core/api/entity-data-subscription.ts +++ b/ui-ngx/src/app/core/api/entity-data-subscription.ts @@ -64,7 +64,7 @@ import { PageData } from '@shared/models/page/page-data'; import { DataAggregator, onAggregatedData } from '@core/api/data-aggregator'; import { NULL_UUID } from '@shared/models/id/has-uuid'; import { EntityType } from '@shared/models/entity-type.models'; -import { firstValueFrom, from, Observable, of, ReplaySubject, Subject, tap } from 'rxjs'; +import { firstValueFrom, from, Observable, of, ReplaySubject, Subject, Subscription } from 'rxjs'; import { EntityId } from '@shared/models/id/entity-id'; import { TelemetryWebsocketService } from '@core/ws/telemetry-websocket.service'; import { @@ -75,6 +75,7 @@ import { } from '@shared/models/js-function.models'; import { HttpClient } from '@angular/common/http'; import Timeout = NodeJS.Timeout; +import { finalize, switchMap } from 'rxjs/operators'; declare type DataKeyFunction = (time: number, prevValue: any) => any; declare type DataKeyPostFunction = (time: number, value: any, prevValue: any, timePrev: number, prevOrigValue: any) => any; @@ -141,6 +142,7 @@ export class EntityDataSubscription { private aggTsValues: Array; private aggTsComparisonValues: Array; + private subscribeSubscription: Subscription; private entityDataResolveSubject: Subject; private pageData: PageData; private prematureUpdates: Array>; @@ -216,7 +218,7 @@ export class EntityDataSubscription { if (isNotEmptyTbFunction(dataKey.postFuncBody) && !dataKey.postFunc) { try { dataKey.postFunc = await firstValueFrom(compileTbFunction(this.http, dataKey.postFuncBody, 'time', 'value', 'prevValue', 'timePrev', 'prevOrigValue')); - } catch (e) {} + } catch (e) {/**/} } } let key: string; @@ -245,6 +247,10 @@ export class EntityDataSubscription { } public unsubscribe() { + if (this.subscribeSubscription) { + this.subscribeSubscription.unsubscribe(); + this.subscribeSubscription = null; + } if (this.timeseriesTimer) { clearTimeout(this.timeseriesTimer); this.timeseriesTimer = null; @@ -277,8 +283,14 @@ export class EntityDataSubscription { public subscribe(): Observable { this.entityDataResolveSubject = new ReplaySubject(1); - from(this.initializeSubscription()).pipe( - tap(() => { + const subscribeSubject = new ReplaySubject(1); + this.subscribeSubscription = from(this.initializeSubscription()).pipe( + finalize(() => { + subscribeSubject.next(); + subscribeSubject.complete(); + }) + ).subscribe( + () => { if (this.entityDataSubscriptionOptions.isPaginatedDataSubscription) { this.started = true; this.dataResolved = true; @@ -621,13 +633,17 @@ export class EntityDataSubscription { ); this.subscriber.subscribe(); } + } + ); + return subscribeSubject.pipe( + switchMap(() => { + if (this.entityDataSubscriptionOptions.isPaginatedDataSubscription) { + return of(null); + } else { + return this.entityDataResolveSubject.asObservable(); + } }) - ).subscribe(); - if (this.entityDataSubscriptionOptions.isPaginatedDataSubscription) { - return of(null); - } else { - return this.entityDataResolveSubject.asObservable(); - } + ); } public start() { diff --git a/ui-ngx/src/app/core/auth/auth.models.ts b/ui-ngx/src/app/core/auth/auth.models.ts index 1944c693ac..e5cc1424ab 100644 --- a/ui-ngx/src/app/core/auth/auth.models.ts +++ b/ui-ngx/src/app/core/auth/auth.models.ts @@ -31,6 +31,7 @@ export interface SysParamsState { maxDataPointsPerRollingArg: number; maxArgumentsPerCF: number; ruleChainDebugPerTenantLimitsConfiguration?: string; + calculatedFieldDebugPerTenantLimitsConfiguration?: string; } export interface SysParams extends SysParamsState { diff --git a/ui-ngx/src/app/core/auth/auth.service.ts b/ui-ngx/src/app/core/auth/auth.service.ts index ba0161ed02..f8ffd5e8b6 100644 --- a/ui-ngx/src/app/core/auth/auth.service.ts +++ b/ui-ngx/src/app/core/auth/auth.service.ts @@ -152,12 +152,8 @@ export class AuthService { )); } - public resetPassword(resetToken: string, password: string): Observable { - return this.http.post('/api/noauth/resetPassword', {resetToken, password}, defaultHttpOptions()).pipe( - tap((loginResponse: LoginResponse) => { - this.setUserFromJwtToken(loginResponse.token, loginResponse.refreshToken, true); - } - )); + public resetPassword(resetToken: string, password: string): Observable { + return this.http.post('/api/noauth/resetPassword', {resetToken, password}, defaultHttpOptions()); } public changePassword(currentPassword: string, newPassword: string, config?: RequestConfig) { diff --git a/ui-ngx/src/app/core/services/utils.service.ts b/ui-ngx/src/app/core/services/utils.service.ts index 61f16d1f67..bfaad1148d 100644 --- a/ui-ngx/src/app/core/services/utils.service.ts +++ b/ui-ngx/src/app/core/services/utils.service.ts @@ -208,7 +208,7 @@ export class UtilsService { return parseException(exception, lineOffset); } - public customTranslation(translationValue: string, defaultValue: string): string { + public customTranslation(translationValue: string, defaultValue: string = translationValue): string { if (translationValue && isString(translationValue)) { if (translationValue.includes(`{${i18nPrefix}`)) { const matches = translationValue.match(i18nRegExp); diff --git a/ui-ngx/src/app/modules/common/modules-map.ts b/ui-ngx/src/app/modules/common/modules-map.ts index 70432e8cdf..530a41f006 100644 --- a/ui-ngx/src/app/modules/common/modules-map.ts +++ b/ui-ngx/src/app/modules/common/modules-map.ts @@ -76,6 +76,8 @@ import * as TranslateCore from '@ngx-translate/core'; import * as MatDateTimePicker from '@mat-datetimepicker/core'; import _moment from 'moment'; import * as tslib from 'tslib'; +import * as Echarts from 'echarts'; +import * as EchartsCore from 'echarts/core'; import * as TbCore from '@core/public-api'; import * as TbShared from '@shared/public-api'; @@ -405,6 +407,8 @@ class ModulesMap implements IModulesMap { '@mat-datetimepicker/core': MatDateTimePicker, moment: _moment, tslib, + 'echarts': Echarts, + 'echarts/core': EchartsCore, '@core/public-api': TbCore, '@shared/public-api': TbShared, diff --git a/ui-ngx/src/app/modules/home/components/alarm/alarm-table-config.ts b/ui-ngx/src/app/modules/home/components/alarm/alarm-table-config.ts index fb0ff6de90..b450940ca1 100644 --- a/ui-ngx/src/app/modules/home/components/alarm/alarm-table-config.ts +++ b/ui-ngx/src/app/modules/home/components/alarm/alarm-table-config.ts @@ -243,7 +243,7 @@ export class AlarmTableConfig extends EntityTableConfig if ($event) { $event.stopPropagation(); } - const target = $event.target || $event.srcElement || $event.currentTarget; + const target = $event.target || $event.currentTarget; const config = new OverlayConfig(); config.backdropClass = 'cdk-overlay-transparent-backdrop'; config.hasBackdrop = true; diff --git a/ui-ngx/src/app/modules/home/components/attribute/attribute-table.component.ts b/ui-ngx/src/app/modules/home/components/attribute/attribute-table.component.ts index 538afa3a06..d4ff2dc041 100644 --- a/ui-ngx/src/app/modules/home/components/attribute/attribute-table.component.ts +++ b/ui-ngx/src/app/modules/home/components/attribute/attribute-table.component.ts @@ -338,7 +338,7 @@ export class AttributeTableComponent extends PageComponent implements AfterViewI if (this.isClientSideTelemetryTypeMap.get(this.attributeScope)) { return; } - const target = $event.target || $event.srcElement || $event.currentTarget; + const target = $event.target || $event.currentTarget; const config = new OverlayConfig(); config.backdropClass = 'cdk-overlay-transparent-backdrop'; config.hasBackdrop = true; @@ -389,7 +389,7 @@ export class AttributeTableComponent extends PageComponent implements AfterViewI if ($event) { $event.stopPropagation(); } - const target = $event.target || $event.srcElement || $event.currentTarget; + const target = $event.target || $event.currentTarget; const config = new OverlayConfig({ panelClass: 'tb-filter-panel', backdropClass: 'cdk-overlay-transparent-backdrop', 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 c5b256ea89..07f8f0293c 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 @@ -27,10 +27,9 @@ import { PageLink } from '@shared/models/page/page-link'; import { Observable, of } from 'rxjs'; import { PageData } from '@shared/models/page/page-data'; import { EntityId } from '@shared/models/id/entity-id'; -import { MINUTE } from '@shared/models/time/time.models'; import { Store } from '@ngrx/store'; import { AppState } from '@core/core.state'; -import { getCurrentAuthState, getCurrentAuthUser } from '@core/auth/auth.selectors'; +import { getCurrentAuthUser } from '@core/auth/auth.selectors'; import { DestroyRef, Renderer2 } from '@angular/core'; import { EntityDebugSettings } from '@shared/models/entity.models'; import { takeUntilDestroyed } from '@angular/core/rxjs-interop'; @@ -47,7 +46,8 @@ import { getCalculatedFieldArgumentsHighlights, } from '@shared/models/calculated-field.models'; import { - CalculatedFieldDebugDialogComponent, CalculatedFieldDebugDialogData, + CalculatedFieldDebugDialogComponent, + CalculatedFieldDebugDialogData, CalculatedFieldDialogComponent, CalculatedFieldDialogData, CalculatedFieldScriptTestDialogComponent, @@ -60,9 +60,6 @@ import { DatePipe } from '@angular/common'; export class CalculatedFieldsTableConfig extends EntityTableConfig { - readonly calculatedFieldsDebugPerTenantLimitsConfiguration = - getCurrentAuthState(this.store)['calculatedFieldsDebugPerTenantLimitsConfiguration']; - readonly maxDebugModeDuration = getCurrentAuthState(this.store).maxDebugModeDurationMinutes * MINUTE; readonly tenantId = getCurrentAuthUser(this.store).tenantId; additionalDebugActionConfig = { title: this.translate.instant('calculated-fields.see-debug-events'), @@ -189,9 +186,7 @@ export class CalculatedFieldsTableConfig extends EntityTableConfig this.onDebugConfigChanged(id.id, settings) @@ -215,7 +210,6 @@ export class CalculatedFieldsTableConfig extends EntityTableConfig 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 0fcac393f0..478447d1d9 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 @@ -55,8 +55,8 @@ export class CalculatedFieldDebugDialogComponent extends DialogComponent this.data.value.type === CalculatedFieldType.SCRIPT && !!(event as Event).body.arguments); this.eventsTable.entitiesTable.updateData(); - 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/dialog/calculated-field-dialog.component.html b/ui-ngx/src/app/modules/home/components/calculated-fields/components/dialog/calculated-field-dialog.component.html index 813f6b1b4c..8e9baa6f90 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 @@ -49,8 +49,7 @@ @@ -75,9 +74,17 @@ />
-
{{ 'calculated-fields.expression' | translate }}
+
+ {{ (fieldFormGroup.get('type').value === CalculatedFieldType.SIMPLE ? 'calculated-fields.expression' : 'calculated-fields.type.script' ) | translate }} +
+
+
@if (configFormGroup.get('expressionSIMPLE').errors && configFormGroup.get('expressionSIMPLE').touched) { @if (configFormGroup.get('expressionSIMPLE').hasError('required')) { @@ -108,7 +115,7 @@
{{ 'api-usage.tbel' | 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 169d6dff50..52051aa6f7 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 @@ -48,7 +48,6 @@ export interface CalculatedFieldDialogData { value?: CalculatedField; buttonTitle: string; entityId: EntityId; - debugLimitsConfiguration: string; tenantId: string; entityName?: string; additionalDebugActionConfig: AdditionalDebugActionConfig<(calculatedField: CalculatedField) => 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 7521395cb4..92855d882f 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 @@ -55,11 +55,11 @@ class="tb-error"> warning - } @else if (argumentFormGroup.get('argumentName').touched && argumentFormGroup.get('argumentName').hasError('equalCtx')) { + } @else if (argumentFormGroup.get('argumentName').touched && argumentFormGroup.get('argumentName').hasError('forbiddenName')) { warning 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 2a64c90f20..6f46e47af9 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 @@ -67,7 +67,7 @@ export class CalculatedFieldArgumentPanelComponent implements OnInit, AfterViewI readonly defaultLimit = Math.floor(this.maxDataPointsPerRollingArg / 10); argumentFormGroup = this.fb.group({ - argumentName: ['', [Validators.required, this.uniqNameRequired(), this.notEqualCtxValidator(), Validators.pattern(charsWithNumRegex), Validators.maxLength(255)]], + argumentName: ['', [Validators.required, this.uniqNameRequired(), this.forbiddenArgumentNameValidator(), Validators.pattern(charsWithNumRegex), Validators.maxLength(255)]], refEntityId: this.fb.group({ entityType: [ArgumentEntityType.Current], id: [''] @@ -254,10 +254,11 @@ export class CalculatedFieldArgumentPanelComponent implements OnInit, AfterViewI } } - private notEqualCtxValidator(): ValidatorFn { + private forbiddenArgumentNameValidator(): ValidatorFn { return (control: FormControl) => { const trimmedValue = control.value.trim().toLowerCase(); - return trimmedValue === 'ctx' ? { equalCtx: true } : null; + const forbiddenArgumentNames = ['ctx', 'e', 'pi']; + return forbiddenArgumentNames.includes(trimmedValue) ? { forbiddenName: true } : null; }; } diff --git a/ui-ngx/src/app/modules/home/components/dashboard-page/dashboard-page.component.html b/ui-ngx/src/app/modules/home/components/dashboard-page/dashboard-page.component.html index 5fd247e072..77f882d191 100644 --- a/ui-ngx/src/app/modules/home/components/dashboard-page/dashboard-page.component.html +++ b/ui-ngx/src/app/modules/home/components/dashboard-page/dashboard-page.component.html @@ -365,7 +365,7 @@ close { this.mobileService.onDashboardLoaded(this.layouts.right.show, this.isRightLayoutOpened); @@ -1335,8 +1336,8 @@ export class DashboardPageComponent extends PageComponent implements IDashboardC addWidgetFromType(widget: WidgetInfo) { this.onAddWidgetClosed(); - this.widgetComponentService.getWidgetInfo(widget.typeFullFqn).subscribe( - (widgetTypeInfo) => { + this.widgetComponentService.getWidgetInfo(widget.typeFullFqn).subscribe({ + next: (widgetTypeInfo) => { const config: WidgetConfig = this.dashboardUtils.widgetConfigFromWidgetType(widgetTypeInfo); if (!config.title) { config.title = 'New ' + widgetTypeInfo.widgetName; @@ -1389,8 +1390,13 @@ export class DashboardPageComponent extends PageComponent implements IDashboardC } }); } + }, + error: (errorData) => { + const errorMessages: string[] = errorData.errorMessages; + this.dialogService.alert(this.translate.instant('widget.widget-type-load-error'), + errorMessages.join('
').replace(/\n/g, '
')); } - ); + }); } onRevertWidgetEdit() { 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 4243044fc8..f79abfe904 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 @@ -16,6 +16,7 @@ --> + + + diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/map/shape-fill-image-settings-panel.component.scss b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/map/shape-fill-image-settings-panel.component.scss new file mode 100644 index 0000000000..5c127549db --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/map/shape-fill-image-settings-panel.component.scss @@ -0,0 +1,54 @@ +/** + * Copyright © 2016-2025 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +@import '../../../../../../../../../scss/constants'; + +.tb-shape-fill-image-settings-panel { + width: 700px; + max-width: 90vw; + min-height: 300px; + max-height: 90vh; + display: flex; + flex-direction: column; + gap: 16px; + @media #{$mat-xs} { + width: 90vw; + } + .tb-shape-fill-image-settings-title { + font-size: 16px; + font-weight: 500; + line-height: 24px; + letter-spacing: 0.25px; + color: rgba(0, 0, 0, 0.87); + } + .tb-form-row { + height: auto; + } + .tb-shape-fill-image-settings-panel-body { + flex: 1; + display: flex; + flex-direction: column; + min-height: 0; + overflow: auto; + } + .tb-shape-fill-image-settings-panel-buttons { + height: 40px; + display: flex; + flex-direction: row; + gap: 16px; + justify-content: flex-end; + align-items: flex-end; + } +} diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/map/shape-fill-image-settings-panel.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/map/shape-fill-image-settings-panel.component.ts new file mode 100644 index 0000000000..fef64d2e44 --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/map/shape-fill-image-settings-panel.component.ts @@ -0,0 +1,104 @@ +/// +/// Copyright © 2016-2025 The Thingsboard Authors +/// +/// Licensed under the Apache License, Version 2.0 (the "License"); +/// you may not use this file except in compliance with the License. +/// You may obtain a copy of the License at +/// +/// http://www.apache.org/licenses/LICENSE-2.0 +/// +/// Unless required by applicable law or agreed to in writing, software +/// distributed under the License is distributed on an "AS IS" BASIS, +/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +/// See the License for the specific language governing permissions and +/// limitations under the License. +/// + +import { Component, DestroyRef, EventEmitter, Input, OnInit, Output, ViewEncapsulation } from '@angular/core'; +import { TbPopoverComponent } from '@shared/components/popover.component'; +import { UntypedFormBuilder, UntypedFormGroup, Validators } from '@angular/forms'; +import { WidgetService } from '@core/http/widget.service'; +import { takeUntilDestroyed } from '@angular/core/rxjs-interop'; +import { ShapeFillImageSettings, ShapeFillImageType } from '@shared/models/widget/maps/map.models'; + +@Component({ + selector: 'tb-shape-fill-image-settings-panel', + templateUrl: './shape-fill-image-settings-panel.component.html', + providers: [], + styleUrls: ['./shape-fill-image-settings-panel.component.scss'], + encapsulation: ViewEncapsulation.None +}) +export class ShapeFillImageSettingsPanelComponent implements OnInit { + + @Input() + shapeFillImageSettings: ShapeFillImageSettings; + + @Output() + shapeFillImageSettingsApplied = new EventEmitter(); + + ShapeFillImageType = ShapeFillImageType; + + shapeFillImageSettingsFormGroup: UntypedFormGroup; + + functionScopeVariables = this.widgetService.getWidgetScopeVariables(); + + constructor(private fb: UntypedFormBuilder, + private popover: TbPopoverComponent, + private widgetService: WidgetService, + private destroyRef: DestroyRef) { + } + + ngOnInit(): void { + this.shapeFillImageSettingsFormGroup = this.fb.group( + { + type: [this.shapeFillImageSettings?.type || ShapeFillImageType.image, []], + image: [this.shapeFillImageSettings?.image, [Validators.required]], + preserveAspectRatio: [this.shapeFillImageSettings?.preserveAspectRatio, []], + opacity: [this.shapeFillImageSettings?.opacity, [Validators.min(0), Validators.max(1)]], + angle: [this.shapeFillImageSettings?.angle, [Validators.min(0), Validators.max(360)]], + scale: [this.shapeFillImageSettings?.scale, [Validators.min(0)]], + imageFunction: [this.shapeFillImageSettings?.imageFunction, [Validators.required]], + images: [this.shapeFillImageSettings?.images, []] + } + ); + this.shapeFillImageSettingsFormGroup.get('type').valueChanges.pipe( + takeUntilDestroyed(this.destroyRef) + ).subscribe(() => { + this.updateValidators(); + setTimeout(() => {this.popover?.updatePosition();}, 0); + }); + this.updateValidators(); + } + + cancel() { + this.popover?.hide(); + } + + applyShapeFillImageSettings() { + const shapeFillImageSettings: ShapeFillImageSettings = this.shapeFillImageSettingsFormGroup.value; + this.shapeFillImageSettingsApplied.emit(shapeFillImageSettings); + this.popover?.hide(); + } + + private updateValidators() { + const type: ShapeFillImageType = this.shapeFillImageSettingsFormGroup.get('type').value; + if (type === ShapeFillImageType.image) { + this.shapeFillImageSettingsFormGroup.get('image').enable({emitEvent: false}); + this.shapeFillImageSettingsFormGroup.get('preserveAspectRatio').enable({emitEvent: false}); + this.shapeFillImageSettingsFormGroup.get('opacity').enable({emitEvent: false}); + this.shapeFillImageSettingsFormGroup.get('angle').enable({emitEvent: false}); + this.shapeFillImageSettingsFormGroup.get('scale').enable({emitEvent: false}); + this.shapeFillImageSettingsFormGroup.get('imageFunction').disable({emitEvent: false}); + this.shapeFillImageSettingsFormGroup.get('images').disable({emitEvent: false}); + } else { + this.shapeFillImageSettingsFormGroup.get('image').disable({emitEvent: false}); + this.shapeFillImageSettingsFormGroup.get('preserveAspectRatio').disable({emitEvent: false}); + this.shapeFillImageSettingsFormGroup.get('opacity').disable({emitEvent: false}); + this.shapeFillImageSettingsFormGroup.get('angle').disable({emitEvent: false}); + this.shapeFillImageSettingsFormGroup.get('scale').disable({emitEvent: false}); + this.shapeFillImageSettingsFormGroup.get('imageFunction').enable({emitEvent: false}); + this.shapeFillImageSettingsFormGroup.get('images').enable({emitEvent: false}); + } + } + +} diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/map/shape-fill-image-settings.component.html b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/map/shape-fill-image-settings.component.html new file mode 100644 index 0000000000..da4feaccf2 --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/map/shape-fill-image-settings.component.html @@ -0,0 +1,29 @@ + + + + + diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/map/shape-fill-image-settings.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/map/shape-fill-image-settings.component.ts new file mode 100644 index 0000000000..5f707abced --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/map/shape-fill-image-settings.component.ts @@ -0,0 +1,96 @@ +/// +/// Copyright © 2016-2025 The Thingsboard Authors +/// +/// Licensed under the Apache License, Version 2.0 (the "License"); +/// you may not use this file except in compliance with the License. +/// You may obtain a copy of the License at +/// +/// http://www.apache.org/licenses/LICENSE-2.0 +/// +/// Unless required by applicable law or agreed to in writing, software +/// distributed under the License is distributed on an "AS IS" BASIS, +/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +/// See the License for the specific language governing permissions and +/// limitations under the License. +/// + +import { ChangeDetectorRef, Component, forwardRef, Input, Renderer2, ViewContainerRef } from '@angular/core'; +import { ControlValueAccessor, NG_VALUE_ACCESSOR } from '@angular/forms'; +import { MatButton } from '@angular/material/button'; +import { TbPopoverService } from '@shared/components/popover.service'; +import { ShapeFillImageSettings, ShapeFillImageType } from '@shared/models/widget/maps/map.models'; +import { + ShapeFillImageSettingsPanelComponent +} from '@home/components/widget/lib/settings/common/map/shape-fill-image-settings-panel.component'; + +@Component({ + selector: 'tb-shape-fill-image-settings', + templateUrl: './shape-fill-image-settings.component.html', + styleUrls: [], + providers: [ + { + provide: NG_VALUE_ACCESSOR, + useExisting: forwardRef(() => ShapeFillImageSettingsComponent), + multi: true + } + ] +}) +export class ShapeFillImageSettingsComponent implements ControlValueAccessor { + + @Input() + disabled: boolean; + + ShapeFillImageType = ShapeFillImageType; + + modelValue: ShapeFillImageSettings; + + private propagateChange: (v: any) => void = () => { }; + + constructor(private popoverService: TbPopoverService, + private renderer: Renderer2, + private cd: ChangeDetectorRef, + private viewContainerRef: ViewContainerRef) {} + + registerOnChange(fn: any): void { + this.propagateChange = fn; + } + + registerOnTouched(_fn: any): void { + } + + setDisabledState(isDisabled: boolean): void { + this.disabled = isDisabled; + } + + writeValue(value: ShapeFillImageSettings): void { + if (value) { + this.modelValue = value; + } + } + + openImageSettingsPopup($event: Event, matButton: MatButton) { + if ($event) { + $event.stopPropagation(); + } + const trigger = matButton._elementRef.nativeElement; + if (this.popoverService.hasPopover(trigger)) { + this.popoverService.hidePopover(trigger); + } else { + this.popoverService.displayPopover({ + trigger, + renderer: this.renderer, + componentType: ShapeFillImageSettingsPanelComponent, + hostView: this.viewContainerRef, + preferredPlacement: 'left', + context: { + shapeFillImageSettings: this.modelValue, + }, + isModal: true + }).tbComponentRef.instance.shapeFillImageSettingsApplied.subscribe((shapeFillImageSettings) => { + this.modelValue = shapeFillImageSettings; + this.propagateChange(this.modelValue); + this.cd.detectChanges(); + }); + } + } +} diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/map/shape-fill-stripe-settings-panel.component.html b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/map/shape-fill-stripe-settings-panel.component.html new file mode 100644 index 0000000000..c4b931dafd --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/map/shape-fill-stripe-settings-panel.component.html @@ -0,0 +1,84 @@ + +
+
widgets.maps.data-layer.shape.stripe-pattern
+
+
+
+
widgets.maps.data-layer.shape.first-stripe
+
+ + + px + + +
+
+
+
widgets.maps.data-layer.shape.second-stripe
+
+ + + px + + +
+
+
+
widgets.maps.data-layer.shape.angle
+ + +
deg
+
+
+
+
+ widgets.background.preview +
+
+
+
+
+
+
+ + + +
+
diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/map/shape-fill-stripe-settings-panel.component.scss b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/map/shape-fill-stripe-settings-panel.component.scss new file mode 100644 index 0000000000..5f3af9894b --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/map/shape-fill-stripe-settings-panel.component.scss @@ -0,0 +1,80 @@ +/** + * Copyright © 2016-2025 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +@import '../../../../../../../../../scss/constants'; + +.tb-shape-fill-stripe-settings-panel { + width: 700px; + max-width: 90vw; + min-height: 300px; + max-height: 90vh; + display: flex; + flex-direction: column; + gap: 16px; + @media #{$mat-xs} { + width: 90vw; + } + .tb-shape-fill-stripe-settings-title { + font-size: 16px; + font-weight: 500; + line-height: 24px; + letter-spacing: 0.25px; + color: rgba(0, 0, 0, 0.87); + } + .tb-form-row { + height: auto; + } + .tb-shape-fill-stripe-settings-panel-body { + flex: 1; + display: flex; + flex-direction: column; + min-height: 0; + overflow: auto; + } + .tb-shape-fill-stripe-settings-preview { + flex: 1; + background: #fff; + border-radius: 4px; + border: 1px solid rgba(0, 0, 0, 0.12); + display: flex; + flex-direction: column; + padding: 12px 16px 24px 16px; + align-items: center; + gap: 12px; + } + .tb-shape-fill-stripe-settings-preview-title { + align-self: stretch; + font-size: 16px; + font-style: normal; + font-weight: 500; + line-height: 24px; + color: rgba(0, 0, 0, 0.38); + } + .tb-shape-fill-stripe-settings-preview-box { + position: relative; + width: 136px; + height: 118px; + border-radius: 2.666px; + } + + .tb-shape-fill-stripe-settings-panel-buttons { + height: 40px; + display: flex; + flex-direction: row; + gap: 16px; + justify-content: flex-end; + align-items: flex-end; + } +} diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/map/shape-fill-stripe-settings-panel.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/map/shape-fill-stripe-settings-panel.component.ts new file mode 100644 index 0000000000..86d1ae67fd --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/map/shape-fill-stripe-settings-panel.component.ts @@ -0,0 +1,106 @@ +/// +/// Copyright © 2016-2025 The Thingsboard Authors +/// +/// Licensed under the Apache License, Version 2.0 (the "License"); +/// you may not use this file except in compliance with the License. +/// You may obtain a copy of the License at +/// +/// http://www.apache.org/licenses/LICENSE-2.0 +/// +/// Unless required by applicable law or agreed to in writing, software +/// distributed under the License is distributed on an "AS IS" BASIS, +/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +/// See the License for the specific language governing permissions and +/// limitations under the License. +/// + +import { Component, DestroyRef, EventEmitter, Input, OnInit, Output, ViewEncapsulation } from '@angular/core'; +import { TbPopoverComponent } from '@shared/components/popover.component'; +import { UntypedFormBuilder, UntypedFormGroup, Validators } from '@angular/forms'; +import { takeUntilDestroyed } from '@angular/core/rxjs-interop'; +import { MapDataLayerType, ShapeFillStripeSettings } from '@shared/models/widget/maps/map.models'; +import { DomSanitizer } from '@angular/platform-browser'; +import { + generateStripePreviewUrl +} from '@home/components/widget/lib/settings/common/map/shape-fill-stripe-settings.component'; +import { ComponentStyle } from '@shared/models/widget-settings.models'; +import { MapSettingsContext } from '@home/components/widget/lib/settings/common/map/map-settings.component.models'; +import { DatasourceType } from '@shared/models/widget.models'; + +@Component({ + selector: 'tb-shape-fill-stripe-settings-panel', + templateUrl: './shape-fill-stripe-settings-panel.component.html', + providers: [], + styleUrls: ['./shape-fill-stripe-settings-panel.component.scss'], + encapsulation: ViewEncapsulation.None +}) +export class ShapeFillStripeSettingsPanelComponent implements OnInit { + + @Input() + shapeFillStripeSettings: ShapeFillStripeSettings; + + @Input() + context: MapSettingsContext; + + @Input() + dsType: DatasourceType; + + @Input() + dsEntityAliasId: string; + + @Input() + dsDeviceId: string; + + @Input() + dataLayerType: MapDataLayerType; + + @Output() + shapeFillStripeSettingsApplied = new EventEmitter(); + + stripePreviewStyle: ComponentStyle; + + shapeFillStripeSettingsFormGroup: UntypedFormGroup; + + constructor(private fb: UntypedFormBuilder, + private sanitizer: DomSanitizer, + private popover: TbPopoverComponent, + private destroyRef: DestroyRef) { + } + + ngOnInit(): void { + this.shapeFillStripeSettingsFormGroup = this.fb.group( + { + weight: [this.shapeFillStripeSettings?.weight, [Validators.min(0)]], + color: [this.shapeFillStripeSettings?.color, []], + spaceWeight: [this.shapeFillStripeSettings?.spaceWeight, [Validators.min(0)]], + spaceColor: [this.shapeFillStripeSettings?.spaceColor, []], + angle: [this.shapeFillStripeSettings?.angle, [Validators.min(0), Validators.max(180)]] + } + ); + this.shapeFillStripeSettingsFormGroup.valueChanges.pipe( + takeUntilDestroyed(this.destroyRef) + ).subscribe(() => { + this.updatePreview(); + }); + this.updatePreview(); + } + + cancel() { + this.popover?.hide(); + } + + applyShapeFillStripeSettings() { + const shapeFillStripeSettings: ShapeFillStripeSettings = this.shapeFillStripeSettingsFormGroup.value; + this.shapeFillStripeSettingsApplied.emit(shapeFillStripeSettings); + this.popover?.hide(); + } + + private updatePreview() { + const shapeFillStripeSettings: ShapeFillStripeSettings = this.shapeFillStripeSettingsFormGroup.value; + const previewUrl = generateStripePreviewUrl(shapeFillStripeSettings, 136, 118); + this.stripePreviewStyle = { + background: this.sanitizer.bypassSecurityTrustStyle(`url(${previewUrl}) no-repeat 50% 50% / cover`) + }; + } + +} diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/map/shape-fill-stripe-settings.component.html b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/map/shape-fill-stripe-settings.component.html new file mode 100644 index 0000000000..2188e97ae6 --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/map/shape-fill-stripe-settings.component.html @@ -0,0 +1,27 @@ + + diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/map/shape-fill-stripe-settings.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/map/shape-fill-stripe-settings.component.ts new file mode 100644 index 0000000000..678d57b1e3 --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/map/shape-fill-stripe-settings.component.ts @@ -0,0 +1,148 @@ +/// +/// Copyright © 2016-2025 The Thingsboard Authors +/// +/// Licensed under the Apache License, Version 2.0 (the "License"); +/// you may not use this file except in compliance with the License. +/// You may obtain a copy of the License at +/// +/// http://www.apache.org/licenses/LICENSE-2.0 +/// +/// Unless required by applicable law or agreed to in writing, software +/// distributed under the License is distributed on an "AS IS" BASIS, +/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +/// See the License for the specific language governing permissions and +/// limitations under the License. +/// + +import { ChangeDetectorRef, Component, forwardRef, Input, Renderer2, ViewContainerRef } from '@angular/core'; +import { ControlValueAccessor, NG_VALUE_ACCESSOR } from '@angular/forms'; +import { MatButton } from '@angular/material/button'; +import { TbPopoverService } from '@shared/components/popover.service'; +import { MapDataLayerType, ShapeFillStripeSettings } from '@shared/models/widget/maps/map.models'; +import { DomSanitizer, SafeUrl } from '@angular/platform-browser'; +import { isDefinedAndNotNull, stringToBase64 } from '@core/utils'; +import { MapSettingsContext } from '@home/components/widget/lib/settings/common/map/map-settings.component.models'; +import { DatasourceType } from '@shared/models/widget.models'; +import { + ShapeFillStripeSettingsPanelComponent +} from '@home/components/widget/lib/settings/common/map/shape-fill-stripe-settings-panel.component'; + +@Component({ + selector: 'tb-shape-fill-stripe-settings', + templateUrl: './shape-fill-stripe-settings.component.html', + styleUrls: [], + providers: [ + { + provide: NG_VALUE_ACCESSOR, + useExisting: forwardRef(() => ShapeFillStripeSettingsComponent), + multi: true + } + ] +}) +export class ShapeFillStripeSettingsComponent implements ControlValueAccessor { + + @Input() + disabled: boolean; + + @Input() + context: MapSettingsContext; + + @Input() + dsType: DatasourceType; + + @Input() + dsEntityAliasId: string; + + @Input() + dsDeviceId: string; + + @Input() + dataLayerType: MapDataLayerType; + + modelValue: ShapeFillStripeSettings; + + stripePreviewUrl: SafeUrl; + + private propagateChange: (v: any) => void = () => { }; + + constructor(private popoverService: TbPopoverService, + private sanitizer: DomSanitizer, + private renderer: Renderer2, + private cd: ChangeDetectorRef, + private viewContainerRef: ViewContainerRef) {} + + registerOnChange(fn: any): void { + this.propagateChange = fn; + } + + registerOnTouched(_fn: any): void { + } + + setDisabledState(isDisabled: boolean): void { + this.disabled = isDisabled; + } + + writeValue(value: ShapeFillStripeSettings): void { + if (value) { + this.modelValue = value; + } + this.updatePreview(); + } + + openStripeSettingsPopup($event: Event, matButton: MatButton) { + if ($event) { + $event.stopPropagation(); + } + const trigger = matButton._elementRef.nativeElement; + if (this.popoverService.hasPopover(trigger)) { + this.popoverService.hidePopover(trigger); + } else { + this.popoverService.displayPopover({ + trigger, + renderer: this.renderer, + componentType: ShapeFillStripeSettingsPanelComponent, + hostView: this.viewContainerRef, + preferredPlacement: 'left', + context: { + shapeFillStripeSettings: this.modelValue, + context: this.context, + dsType: this.dsType, + dsEntityAliasId: this.dsEntityAliasId, + dsDeviceId: this.dsDeviceId, + dataLayerType: this.dataLayerType + }, + isModal: true + }).tbComponentRef.instance.shapeFillStripeSettingsApplied.subscribe((shapeFillStripeSettings) => { + this.modelValue = shapeFillStripeSettings; + this.updatePreview(); + this.propagateChange(this.modelValue); + this.cd.detectChanges(); + }); + } + } + + private updatePreview() { + this.stripePreviewUrl = this.sanitizer.bypassSecurityTrustUrl(generateStripePreviewUrl(this.modelValue)); + } +} + +export const generateStripePreviewUrl = (settings: ShapeFillStripeSettings, previewWidth = 48, previewHeight = 48): string => { + const weight = isDefinedAndNotNull(settings?.weight) ? settings.weight : 3; + const spaceWeight = isDefinedAndNotNull(settings?.spaceWeight) ? settings.spaceWeight : 9; + const angle = isDefinedAndNotNull(settings?.angle) ? settings.angle : 45; + const height = weight + spaceWeight; + const color = settings?.color?.color || '#8f8f8f'; + const spaceColor = settings?.spaceColor?.color || 'rgba(143,143,143,0)'; + const svgStr = ` + + + + + + + + `; + const encodedSvg = stringToBase64(svgStr); + return `data:image/svg+xml;base64,${encodedSvg}`; +} diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/widget-settings-common.module.ts b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/widget-settings-common.module.ts index 7ec4a4626d..233b202684 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/widget-settings-common.module.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/widget-settings-common.module.ts @@ -69,6 +69,9 @@ import { } from '@home/components/widget/lib/settings/common/action/set-value-action-settings-panel.component'; import { CssSizeInputComponent } from '@home/components/widget/lib/settings/common/css-size-input.component'; import { WidgetActionComponent } from '@home/components/widget/lib/settings/common/action/widget-action.component'; +import { + MapItemTooltipsComponent +} from '@home/components/widget/lib/settings/common/action/map-item-tooltips.component'; import { CustomActionPrettyResourcesTabsComponent } from '@home/components/widget/lib/settings/common/action/custom-action-pretty-resources-tabs.component'; @@ -252,6 +255,18 @@ import { MapDataSourcesComponent } from '@home/components/widget/lib/settings/co import { MapDataSourceRowComponent } from '@home/components/widget/lib/settings/common/map/map-data-source-row.component'; +import { + ShapeFillImageSettingsComponent +} from '@home/components/widget/lib/settings/common/map/shape-fill-image-settings.component'; +import { + ShapeFillImageSettingsPanelComponent +} from '@home/components/widget/lib/settings/common/map/shape-fill-image-settings-panel.component'; +import { + ShapeFillStripeSettingsComponent +} from '@home/components/widget/lib/settings/common/map/shape-fill-stripe-settings.component'; +import { + ShapeFillStripeSettingsPanelComponent +} from '@home/components/widget/lib/settings/common/map/shape-fill-stripe-settings-panel.component'; @NgModule({ declarations: [ @@ -283,6 +298,7 @@ import { SetValueActionSettingsComponent, SetValueActionSettingsPanelComponent, WidgetActionComponent, + MapItemTooltipsComponent, CustomActionPrettyResourcesTabsComponent, CustomActionPrettyEditorComponent, MobileActionEditorComponent, @@ -338,6 +354,10 @@ import { MarkerImageSettingsComponent, MarkerImageSettingsPanelComponent, MarkerClusteringSettingsComponent, + ShapeFillStripeSettingsComponent, + ShapeFillStripeSettingsPanelComponent, + ShapeFillImageSettingsComponent, + ShapeFillImageSettingsPanelComponent, MapDataLayerDialogComponent, MapDataLayerRowComponent, MapDataLayersComponent, 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 b3e0f21b83..d370fde972 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 @@ -49,7 +49,7 @@ {{ 'widgets.table.display-pagination' | translate }}
-
{{ 'widgets.table.page-step-settings' | translate }}
+
{{ 'widgets.table.page-step-settings' | translate }}
widgets.table.page-step-increment
@@ -67,7 +67,7 @@ warning - +
widgets.table.page-step-count
-
{{ 'widgets.table.page-step-settings' | translate }}
+
{{ 'widgets.table.page-step-settings' | translate }}
widgets.table.page-step-increment
@@ -110,7 +110,7 @@ warning - +
widgets.table.page-step-count
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 fc996fb2c6..9287b92d57 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 @@ -54,7 +54,6 @@ import { import { PageComponent } from '@shared/components/page.component'; import { Store } from '@ngrx/store'; import { AppState } from '@core/core.state'; -import { WidgetService } from '@core/http/widget.service'; import { UtilsService } from '@core/services/utils.service'; import { forkJoin, Observable, of, ReplaySubject, Subscription, throwError } from 'rxjs'; import { @@ -205,7 +204,6 @@ export class WidgetComponent extends PageComponent implements OnInit, OnChanges, @Inject(EMBED_DASHBOARD_DIALOG_TOKEN) private embedDashboardDialogComponent: ComponentType, @Inject(DASHBOARD_PAGE_COMPONENT_TOKEN) private dashboardPageComponent: ComponentType, @Optional() @Inject(MODULES_MAP) private modulesMap: IModulesMap, - private widgetService: WidgetService, private resources: ResourcesService, private timeService: TimeService, private deviceService: DeviceService, @@ -346,17 +344,17 @@ export class WidgetComponent extends PageComponent implements OnInit, OnChanges, this.subscriptionContext.widgetUtils = this.widgetContext.utils; this.subscriptionContext.getServerTimeDiff = this.dashboardService.getServerTimeDiff.bind(this.dashboardService); - this.widgetComponentService.getWidgetInfo(this.widget.typeFullFqn).subscribe( - (widgetInfo) => { + this.widgetComponentService.getWidgetInfo(this.widget.typeFullFqn).subscribe({ + next: (widgetInfo) => { this.widgetInfo = widgetInfo; this.loadFromWidgetInfo(); }, - (errorData) => { + error: (errorData) => { this.widgetInfo = errorData.widgetInfo; this.errorMessages = errorData.errorMessages; this.loadFromWidgetInfo(); } - ); + }); const noDataDisplayMessage = this.widget.config.noDataDisplayMessage; if (isNotEmptyStr(noDataDisplayMessage)) { @@ -521,15 +519,15 @@ export class WidgetComponent extends PageComponent implements OnInit, OnChanges, this.widgetTypeInstance.onDestroy = () => {}; } - this.initialize().subscribe( - () => { + this.initialize().subscribe({ + next: () => { this.onInit(); }, - (err) => { + error: () => { this.widgetContext.inited = true; // console.log(err); } - ); + }); } private detectChanges(detectContainerChanges = false) { @@ -681,8 +679,8 @@ export class WidgetComponent extends PageComponent implements OnInit, OnChanges, private reInitImpl() { this.onDestroy(); if (!this.typeParameters.useCustomDatasources) { - this.createDefaultSubscription().subscribe( - () => { + this.createDefaultSubscription().subscribe({ + next: () => { if (this.destroyed) { this.onDestroy(); } else { @@ -692,7 +690,7 @@ export class WidgetComponent extends PageComponent implements OnInit, OnChanges, this.onInit(); } }, - () => { + error: () => { if (this.destroyed) { this.onDestroy(); } else { @@ -701,7 +699,7 @@ export class WidgetComponent extends PageComponent implements OnInit, OnChanges, this.onInit(); } } - ); + }); } else { this.widgetContext.reset(); this.subscriptionInited = true; @@ -751,8 +749,8 @@ export class WidgetComponent extends PageComponent implements OnInit, OnChanges, } )); if (!this.typeParameters.useCustomDatasources) { - this.createDefaultSubscription().subscribe( - () => { + this.createDefaultSubscription().subscribe({ + next: () => { this.subscriptionInited = true; try { this.configureDynamicWidgetComponent(); @@ -762,11 +760,11 @@ export class WidgetComponent extends PageComponent implements OnInit, OnChanges, initSubject.error(err); } }, - (err) => { + error: (err) => { this.subscriptionInited = true; initSubject.error(err); } - ); + }); } else { this.loadingData = false; this.subscriptionInited = true; @@ -791,7 +789,7 @@ export class WidgetComponent extends PageComponent implements OnInit, OnChanges, } } - private handleWidgetException(e) { + private handleWidgetException(e: any) { console.error(e); this.widgetErrorData = this.utils.processWidgetException(e); this.detectChanges(); @@ -866,8 +864,8 @@ export class WidgetComponent extends PageComponent implements OnInit, OnChanges, const createSubscriptionSubject = new ReplaySubject(); options.dashboardTimewindow = this.widgetContext.dashboardTimewindow; const subscription: IWidgetSubscription = new WidgetSubscription(this.subscriptionContext, options); - subscription.init$.subscribe( - () => { + subscription.init$.subscribe({ + next: () => { this.widgetContext.subscriptions[subscription.id] = subscription; if (subscribe) { subscription.subscribe(); @@ -875,10 +873,10 @@ export class WidgetComponent extends PageComponent implements OnInit, OnChanges, createSubscriptionSubject.next(subscription); createSubscriptionSubject.complete(); }, - (err) => { + error: (err) => { createSubscriptionSubject.error(err); } - ); + }); return createSubscriptionSubject.asObservable(); } @@ -900,15 +898,15 @@ export class WidgetComponent extends PageComponent implements OnInit, OnChanges, } else { options.datasources = this.entityService.createDatasourcesFromSubscriptionsInfo(subscriptionsInfo); } - this.createSubscription(options, subscribe).subscribe( - (subscription) => { + this.createSubscription(options, subscribe).subscribe({ + next: (subscription) => { createSubscriptionSubject.next(subscription); createSubscriptionSubject.complete(); }, - (err) => { + error: (err) => { createSubscriptionSubject.error(err); } - ); + }); return createSubscriptionSubject.asObservable(); } @@ -937,7 +935,7 @@ export class WidgetComponent extends PageComponent implements OnInit, OnChanges, this.dataUpdatePending = true; } } - } catch (e){} + } catch (e){/**/} }, onLatestDataUpdated: () => { try { @@ -948,15 +946,15 @@ export class WidgetComponent extends PageComponent implements OnInit, OnChanges, this.latestDataUpdatePending = true; } } - } catch (e){} + } catch (e){/**/} }, - onDataUpdateError: (subscription, e) => { + onDataUpdateError: (_subscription, e) => { this.handleWidgetException(e); }, - onLatestDataUpdateError: (subscription, e) => { + onLatestDataUpdateError: (_subscription, e) => { this.handleWidgetException(e); }, - onSubscriptionMessage: (subscription, message) => { + onSubscriptionMessage: (_subscription, message) => { if (this.displayWidgetInstance()) { if (this.widgetInstanceInited) { this.displayMessage(message.severity, message.message); @@ -965,7 +963,7 @@ export class WidgetComponent extends PageComponent implements OnInit, OnChanges, } } }, - onInitialPageDataChanged: (subscription, nextPageData) => { + onInitialPageDataChanged: (_subscription, _nextPageData) => { this.reInit(); }, forceReInit: () => { @@ -977,12 +975,12 @@ export class WidgetComponent extends PageComponent implements OnInit, OnChanges, this.detectChanges(); } }, - legendDataUpdated: (subscription, detectChanges) => { + legendDataUpdated: (_subscription, detectChanges) => { if (detectChanges) { this.detectChanges(); } }, - timeWindowUpdated: (subscription, timeWindowConfig) => { + timeWindowUpdated: (_subscription, timeWindowConfig) => { this.ngZone.run(() => { this.widget.config.timewindow = timeWindowConfig; this.detectChanges(true); @@ -1017,8 +1015,8 @@ export class WidgetComponent extends PageComponent implements OnInit, OnChanges, this.defaultComponentsOptions(options); - this.createSubscription(options).subscribe( - (subscription) => { + this.createSubscription(options).subscribe({ + next: (subscription) => { // backward compatibility this.widgetContext.datasources = subscription.datasources; @@ -1032,12 +1030,12 @@ export class WidgetComponent extends PageComponent implements OnInit, OnChanges, createSubscriptionSubject.complete(); }); }, - (err) => { + error: (err) => { this.ngZone.run(() => { createSubscriptionSubject.error(err); }); } - ); + }); } else if (this.widget.type === widgetType.rpc) { this.loadingData = false; options = { @@ -1074,7 +1072,7 @@ export class WidgetComponent extends PageComponent implements OnInit, OnChanges, this.detectChanges(); } }, - onRpcErrorCleared: (subscription) => { + onRpcErrorCleared: (_subscription) => { if (this.dynamicWidgetComponent) { this.dynamicWidgetComponent.rpcErrorText = null; this.dynamicWidgetComponent.rpcRejection = null; @@ -1085,20 +1083,20 @@ export class WidgetComponent extends PageComponent implements OnInit, OnChanges, } } }; - this.createSubscription(options).subscribe( - (subscription) => { + this.createSubscription(options).subscribe({ + next: (subscription) => { this.widgetContext.defaultSubscription = subscription; this.ngZone.run(() => { createSubscriptionSubject.next(); createSubscriptionSubject.complete(); }); }, - (err) => { + error: (err) => { this.ngZone.run(() => { createSubscriptionSubject.error(err); }); } - ); + }); this.detectChanges(); } else if (this.widget.type === widgetType.static) { this.loadingData = false; @@ -1159,7 +1157,7 @@ export class WidgetComponent extends PageComponent implements OnInit, OnChanges, } const state = objToBase64URI([ stateObject ]); const isSinglePage = this.route.snapshot.data.singlePageMode; - let url; + let url: string; if (isSinglePage) { url = `/dashboard/${targetDashboardId}?state=${state}`; } else { @@ -1168,7 +1166,7 @@ export class WidgetComponent extends PageComponent implements OnInit, OnChanges, if (descriptor.openNewBrowserTab) { window.open(url, '_blank'); } else { - this.router.navigateByUrl(url); + this.router.navigateByUrl(url).then(() => {}); } break; case WidgetActionType.openURL: @@ -1467,7 +1465,7 @@ export class WidgetComponent extends PageComponent implements OnInit, OnChanges, popoverWidth = '25vw', popoverHeight = '25vh', popoverStyle: { [klass: string]: any } = {}) { - const trigger = ($event.target || $event.srcElement || $event.currentTarget) as Element; + const trigger = ($event.target || $event.currentTarget) as Element; if (this.popoverService.hasPopover(trigger)) { this.popoverService.hidePopover(trigger); } else { @@ -1560,7 +1558,7 @@ export class WidgetComponent extends PageComponent implements OnInit, OnChanges, } private elementClick($event: Event) { - const elementClicked = ($event.target || $event.srcElement) as Element; + const elementClicked = ($event.target) as Element; const descriptors = this.getActionDescriptors('elementClick'); if (descriptors.length) { const idsList = descriptors.map(descriptor => `#${descriptor.name}`).join(','); 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 00afa2b68d..aee890a5b2 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 @@ -115,6 +115,7 @@ import { DataKeySettingsFunction } from '@home/components/widget/lib/settings/co import { UtilsService } from '@core/services/utils.service'; import { CompiledTbFunction } from '@shared/models/js-function.models'; import { FormProperty } from '@shared/models/dynamic-form.models'; +import { ExportableEntity } from '@shared/models/base-data'; export interface IWidgetAction { name: string; @@ -573,7 +574,7 @@ export interface IDynamicWidgetComponent { [key: string]: any; } -export interface WidgetInfo extends WidgetTypeDescriptor, WidgetControllerDescriptor { +export interface WidgetInfo extends WidgetTypeDescriptor, WidgetControllerDescriptor, ExportableEntity { widgetName: string; fullFqn: string; deprecated: boolean; @@ -681,6 +682,7 @@ export const toWidgetInfo = (widgetTypeEntity: WidgetType): WidgetInfo => ({ fullFqn: fullWidgetTypeFqn(widgetTypeEntity), deprecated: widgetTypeEntity.deprecated, scada: widgetTypeEntity.scada, + externalId: widgetTypeEntity.externalId, type: widgetTypeEntity.descriptor.type, sizeX: widgetTypeEntity.descriptor.sizeX, sizeY: widgetTypeEntity.descriptor.sizeY, @@ -736,6 +738,7 @@ export const toWidgetType = (widgetInfo: WidgetInfo, id: WidgetTypeId, tenantId: name: widgetInfo.widgetName, deprecated: widgetInfo.deprecated, scada: widgetInfo.scada, + externalId: widgetInfo.externalId, descriptor }; }; diff --git a/ui-ngx/src/app/modules/home/pages/admin/resource/js-library-table-config.resolver.ts b/ui-ngx/src/app/modules/home/pages/admin/resource/js-library-table-config.resolver.ts index 9c5dba6dbb..8525983a36 100644 --- a/ui-ngx/src/app/modules/home/pages/admin/resource/js-library-table-config.resolver.ts +++ b/ui-ngx/src/app/modules/home/pages/admin/resource/js-library-table-config.resolver.ts @@ -97,7 +97,7 @@ export class JsLibraryTableConfigResolver { entity => checkBoxCell(entity.tenantId.id === NULL_UUID)), ); - this.config.cellActionDescriptors = this.configureCellActions(getCurrentAuthUser(this.store)); + this.config.cellActionDescriptors = this.configureCellActions(); this.config.groupActionDescriptors = [{ name: this.translate.instant('action.delete'), @@ -137,6 +137,7 @@ export class JsLibraryTableConfigResolver { resourceSubType: '' }; const authUser = getCurrentAuthUser(this.store); + this.config.deleteEnabled = (resource) => this.isResourceEditable(resource, authUser.authority); this.config.entitySelectionEnabled = (resource) => this.isResourceEditable(resource, authUser.authority); this.config.detailsReadonly = (resource) => this.detailsReadonly(resource, authUser.authority); return this.config; @@ -309,7 +310,7 @@ export class JsLibraryTableConfigResolver { } } - private configureCellActions(authUser: AuthUser): Array> { + private configureCellActions(): Array> { const actions: Array> = []; actions.push( { @@ -321,7 +322,7 @@ export class JsLibraryTableConfigResolver { { name: this.translate.instant('javascript.delete'), icon: 'delete', - isEnabled: (resource) => this.isResourceEditable(resource, authUser.authority), + isEnabled: (resource) => this.config.deleteEnabled(resource), onAction: ($event, entity) => this.deleteResource($event, entity) }, ); diff --git a/ui-ngx/src/app/modules/home/pages/admin/resource/js-resource.component.html b/ui-ngx/src/app/modules/home/pages/admin/resource/js-resource.component.html index 6287d27f2c..62036a8ea1 100644 --- a/ui-ngx/src/app/modules/home/pages/admin/resource/js-resource.component.html +++ b/ui-ngx/src/app/modules/home/pages/admin/resource/js-resource.component.html @@ -80,6 +80,7 @@ (fileNameChanged)="entityForm?.get('fileName').patchValue($event)"> - {{ 'javascript.module-script' | translate }} - + {{ 'javascript.module-script' | translate }} = { base_url: '/assets/tinymce', suffix: '.min', - plugins: ['lists'], + plugins: ['link', 'table', 'image', 'imagetools', 'lists'], menubar: 'edit insert view format', toolbar: ['fontfamily fontsize | bold italic underline strikethrough forecolor backcolor', - 'alignleft aligncenter alignright alignjustify | bullist'], + 'alignleft aligncenter alignright alignjustify | bullist | link table image'], toolbar_mode: 'sliding', height: 400, autofocus: false, diff --git a/ui-ngx/src/app/modules/home/pages/rulechain/rule-node-details.component.html b/ui-ngx/src/app/modules/home/pages/rulechain/rule-node-details.component.html index 1923d3ef91..b55dcdaa73 100644 --- a/ui-ngx/src/app/modules/home/pages/rulechain/rule-node-details.component.html +++ b/ui-ngx/src/app/modules/home/pages/rulechain/rule-node-details.component.html @@ -22,7 +22,7 @@
-
+
rulenode.name @@ -38,8 +38,7 @@
- -
{{ noFileText }}
-
{{ fileName }}
-
dashboard.maximum-upload-file-size
+ +
{{ noFileText }}
+
{{ fileName }}
+
dashboard.maximum-upload-file-size
diff --git a/ui-ngx/src/app/shared/components/file-input.component.scss b/ui-ngx/src/app/shared/components/file-input.component.scss index 00598adc3b..1c21eb1798 100644 --- a/ui-ngx/src/app/shared/components/file-input.component.scss +++ b/ui-ngx/src/app/shared/components/file-input.component.scss @@ -131,15 +131,11 @@ $previewSize: 100px !default; .tb-file-name { color: rgba(0, 0, 0, 0.54); font-weight: 500; - overflow: hidden; - text-overflow: ellipsis; } .tb-file-hint { color: rgba(0, 0, 0, 0.38); font-weight: 400; - overflow: hidden; - text-overflow: ellipsis; } } 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 44aab1abbf..7c43eaccf2 100644 --- a/ui-ngx/src/app/shared/components/js-func.component.scss +++ b/ui-ngx/src/app/shared/components/js-func.component.scss @@ -85,4 +85,8 @@ color: rgb(49, 132, 149); } } + + label.tb-title.tb-required::after { + content: "*"; + } } diff --git a/ui-ngx/src/app/shared/components/markdown.component.scss b/ui-ngx/src/app/shared/components/markdown.component.scss index 4989a77021..65ad64b189 100644 --- a/ui-ngx/src/app/shared/components/markdown.component.scss +++ b/ui-ngx/src/app/shared/components/markdown.component.scss @@ -270,7 +270,7 @@ outline: none; position: absolute; width: 206px; - height: 42px; + height: 32px; top: 0; right: 32px; background: 0 0; @@ -281,11 +281,11 @@ user-select: none; &.multiline { - right: 44px; + right: 38px; } p { - padding: 8px; + padding: 8px 8px 0; top: 1px; transition: .2s; color: #2a7dec; @@ -301,10 +301,10 @@ background-color: #fff; position: absolute; width: 38px; - height: 38px; + height: 28px; top: 3px; right: 3px; - padding: 10px; + padding: 10px 10px 0; img { position: initial; diff --git a/ui-ngx/src/app/shared/components/popover.component.ts b/ui-ngx/src/app/shared/components/popover.component.ts index 4344b32fd7..2ccbb615d5 100644 --- a/ui-ngx/src/app/shared/components/popover.component.ts +++ b/ui-ngx/src/app/shared/components/popover.component.ts @@ -375,7 +375,6 @@ export class TbPopoverComponent implements OnDestroy, OnInit { tbComponentInjector: Injector | null = null; tbComponentStyle: { [klass: string]: any } = {}; tbOverlayClassName!: string; - tbOverlayStyle: { [klass: string]: any } = {}; tbPopoverInnerStyle: { [klass: string]: any } = {}; tbPopoverInnerContentStyle: { [klass: string]: any } = {}; tbBackdrop = false; @@ -470,6 +469,16 @@ export class TbPopoverComponent implements OnDestroy, OnInit { return this.tbModal ? 'tb-popover-overlay-backdrop' : ''; } + + set tbOverlayStyle(value: { [klass: string]: any }) { + this._tbOverlayStyle = value; + this.cdr.markForCheck(); + } + + get tbOverlayStyle(): { [klass: string]: any } { + return this._tbOverlayStyle; + } + preferredPlacement: PopoverPlacement = 'top'; strictPosition = false; origin!: CdkOverlayOrigin; @@ -485,6 +494,7 @@ export class TbPopoverComponent implements OnDestroy, OnInit { this.cdr.markForCheck(); } }, {threshold: [0.5]}); + private _tbOverlayStyle: { [klass: string]: any } = {}; constructor( public cdr: ChangeDetectorRef, diff --git a/ui-ngx/src/app/shared/components/string-items-list.component.html b/ui-ngx/src/app/shared/components/string-items-list.component.html index b50e0ecce8..d467fbe5f0 100644 --- a/ui-ngx/src/app/shared/components/string-items-list.component.html +++ b/ui-ngx/src/app/shared/components/string-items-list.component.html @@ -30,7 +30,7 @@ close @@ -49,12 +48,11 @@ - - - - - {{ 'common.not-found' | translate }} - + @for (value of filteredValues | async; track value.value) { + + } @empty { + {{ 'common.not-found' | translate }} + } {{ hint }} diff --git a/ui-ngx/src/app/shared/components/string-items-list.component.ts b/ui-ngx/src/app/shared/components/string-items-list.component.ts index a2fa28d0ff..5fe6f44609 100644 --- a/ui-ngx/src/app/shared/components/string-items-list.component.ts +++ b/ui-ngx/src/app/shared/components/string-items-list.component.ts @@ -30,6 +30,7 @@ import { coerceArray, coerceBoolean } from '@shared/decorators/coercion'; import { Observable, of } from 'rxjs'; import { filter, mergeMap, share, tap } from 'rxjs/operators'; import { MatAutocompleteTrigger } from '@angular/material/autocomplete'; +import { isDefined, isUndefined } from '@core/utils'; export interface StringItemsOption { name: string; @@ -116,6 +117,13 @@ export class StringItemsListComponent implements ControlValueAccessor, OnInit { @coerceArray() predefinedValues: StringItemsOption[]; + @Input() + fetchOptionsFn: (searchText?: string) => Observable>; + + @Input() + @coerceBoolean() + allowUserValue = false; + get itemsControl(): AbstractControl { return this.stringItemsForm.get('items'); } @@ -124,7 +132,7 @@ export class StringItemsListComponent implements ControlValueAccessor, OnInit { return this.stringItemsForm.get('item'); } - onTouched = () => {}; + private onTouched = () => {}; private propagateChange: (value: any) => void = () => {}; private dirty = false; @@ -136,7 +144,7 @@ export class StringItemsListComponent implements ControlValueAccessor, OnInit { } ngOnInit() { - if (this.predefinedValues) { + if (this.predefinedValues || isDefined(this.fetchOptionsFn)) { this.filteredValues = this.itemControl.valueChanges .pipe( tap((value) => { @@ -147,7 +155,8 @@ export class StringItemsListComponent implements ControlValueAccessor, OnInit { } }), filter((value) => typeof value === 'string'), - mergeMap(name => this.fetchValues(name)), + tap(name => this.searchText = name), + mergeMap(name => this.fetchOptionsFn ? this.fetchOptionsFn(name) : this.fetchValues(name)), share() ); } @@ -180,7 +189,7 @@ export class StringItemsListComponent implements ControlValueAccessor, OnInit { if (value != null && value.length > 0) { this.modelValue = [...value]; this.itemList = []; - if (this.predefinedValues) { + if (this.predefinedValues && !this.allowUserValue) { value.forEach(item => { const findItem = this.predefinedValues.find(option => option.value === item); if (findItem) { @@ -199,19 +208,16 @@ export class StringItemsListComponent implements ControlValueAccessor, OnInit { this.dirty = true; } - addItem(event: MatChipInputEvent): void { - const item = event.value?.trim() ?? ''; - if (item) { - if (this.predefinedValues) { - const findItems = this.predefinedValues - .filter(value => value.name.toLowerCase().includes(item.toLowerCase())); - if (findItems.length === 1) { - this.add(findItems[0]); - } - } else { - this.add({value: item, name: item}); - } + addOnBlur(event: FocusEvent) { + const target: HTMLElement = event.relatedTarget as HTMLElement; + if (target && target.tagName !== 'MAT-OPTION') { + this.addItem(this.stringItemInput.nativeElement.value ?? '') } + this.onTouched(); + } + + addOnEnd(event: MatChipInputEvent): void { + this.addItem(event.value ?? '') } removeItems(item: StringItemsOption) { @@ -239,6 +245,27 @@ export class StringItemsListComponent implements ControlValueAccessor, OnInit { return values ? values.name : undefined; } + private addItem(searchText: string) { + searchText = searchText.trim(); + if (searchText) { + if (this.allowUserValue || !this.predefinedValues && isUndefined(this.fetchOptionsFn)) { + this.add({value: searchText, name: searchText}); + } else if (this.predefinedValues) { + const findItems = this.predefinedValues + .filter(value => value.name.toLowerCase().includes(searchText.toLowerCase())); + if (findItems.length === 1) { + this.add(findItems[0]); + } + } else if (isDefined(this.fetchOptionsFn)) { + this.fetchOptionsFn(searchText).subscribe((findItems) => { + if (findItems.length === 1) { + this.add(findItems[0]); + } + }) + } + } + } + private add(item: StringItemsOption) { if (!this.modelValue || this.modelValue.indexOf(item.value) === -1) { if (!this.modelValue) { @@ -256,7 +283,6 @@ export class StringItemsListComponent implements ControlValueAccessor, OnInit { if (!this.predefinedValues?.length) { return of([]); } - this.searchText = searchText; let result = this.predefinedValues; if (searchText && searchText.length) { result = this.predefinedValues.filter(option => option.name.toLowerCase().includes(searchText.toLowerCase())); diff --git a/ui-ngx/src/app/shared/components/time/timewindow-config-dialog.component.ts b/ui-ngx/src/app/shared/components/time/timewindow-config-dialog.component.ts index 1ad4dab88c..1e1f831c4f 100644 --- a/ui-ngx/src/app/shared/components/time/timewindow-config-dialog.component.ts +++ b/ui-ngx/src/app/shared/components/time/timewindow-config-dialog.component.ts @@ -554,7 +554,7 @@ export class TimewindowConfigDialogComponent extends PageComponent implements On if ($event) { $event.stopPropagation(); } - const trigger = ($event.target || $event.srcElement || $event.currentTarget) as Element; + const trigger = ($event.target || $event.currentTarget) as Element; if (this.popoverService.hasPopover(trigger)) { this.popoverService.hidePopover(trigger); } else { @@ -614,7 +614,7 @@ export class TimewindowConfigDialogComponent extends PageComponent implements On if ($event) { $event.stopPropagation(); } - const trigger = ($event.target || $event.srcElement || $event.currentTarget) as Element; + const trigger = ($event.target || $event.currentTarget) as Element; if (this.popoverService.hasPopover(trigger)) { this.popoverService.hidePopover(trigger); } else { diff --git a/ui-ngx/src/app/shared/components/time/timezone.component.ts b/ui-ngx/src/app/shared/components/time/timezone.component.ts index 34e2486106..19affc2aab 100644 --- a/ui-ngx/src/app/shared/components/time/timezone.component.ts +++ b/ui-ngx/src/app/shared/components/time/timezone.component.ts @@ -151,7 +151,7 @@ export class TimezoneComponent implements ControlValueAccessor, OnInit { if (this.disablePanel) { return; } - const trigger = ($event.target || $event.srcElement || $event.currentTarget) as Element; + const trigger = ($event.target || $event.currentTarget) as Element; if (this.popoverService.hasPopover(trigger)) { this.popoverService.hidePopover(trigger); } else { diff --git a/ui-ngx/src/app/shared/components/toggle-header.component.html b/ui-ngx/src/app/shared/components/toggle-header.component.html index 9b3c387432..fdfabd08b5 100644 --- a/ui-ngx/src/app/shared/components/toggle-header.component.html +++ b/ui-ngx/src/app/shared/components/toggle-header.component.html @@ -41,7 +41,16 @@ [name]="name" [(ngModel)]="value" (ngModelChange)="valueChange.emit(value)"> - {{ option.name }} + + {{ option.name }} + + warning + +