Browse Source

merge with develop/3.5.2

pull/8786/head
ShvaykaD 3 years ago
parent
commit
3478b020ef
  1. 4
      application/src/main/java/org/thingsboard/server/actors/ActorSystemContext.java
  2. 43
      application/src/main/java/org/thingsboard/server/actors/ruleChain/DefaultTbContext.java
  3. 9
      application/src/main/java/org/thingsboard/server/actors/ruleChain/RuleChainManagerActor.java
  4. 78
      application/src/main/java/org/thingsboard/server/actors/shared/RuleChainErrorActor.java
  5. 4
      application/src/main/resources/thingsboard.yml
  6. 17
      common/actor/src/main/java/org/thingsboard/server/actors/TbActorMailbox.java
  7. 22
      common/message/src/main/java/org/thingsboard/server/common/msg/TbActorError.java
  8. 5
      common/message/src/main/java/org/thingsboard/server/common/msg/TbMsg.java
  9. 3
      common/message/src/main/java/org/thingsboard/server/common/msg/aware/RuleChainAwareMsg.java
  10. 290
      common/script/script-api/src/main/java/org/thingsboard/script/api/tbel/TbUtils.java
  11. 167
      common/script/script-api/src/test/java/org/thingsboard/script/api/tbel/TbUtilsTest.java
  12. 6
      rule-engine/rule-engine-api/src/main/java/org/thingsboard/rule/engine/api/TbContext.java
  13. 18
      rule-engine/rule-engine-api/src/main/java/org/thingsboard/rule/engine/api/TbNodeException.java
  14. 2
      rule-engine/rule-engine-api/src/main/java/org/thingsboard/rule/engine/api/util/TbNodeUtils.java
  15. 2
      rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/action/TbCreateAlarmNode.java
  16. 10
      rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/aws/sns/TbSnsNode.java
  17. 9
      rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/aws/sqs/TbSqsNode.java
  18. 61
      rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/external/TbAbstractExternalNode.java
  19. 11
      rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/gcp/pubsub/TbPubSubNode.java
  20. 13
      rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/kafka/TbKafkaNode.java
  21. 12
      rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/mail/TbSendEmailNode.java
  22. 12
      rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/mqtt/TbMqttNode.java
  23. 3
      rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/mqtt/azure/TbAzureIotHubNode.java
  24. 23
      rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/notification/TbNotificationNode.java
  25. 9
      rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/notification/TbSlackNode.java
  26. 12
      rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/rabbitmq/TbRabbitMqNode.java
  27. 25
      rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/rest/TbHttpClient.java
  28. 13
      rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/rest/TbRestApiCallNode.java
  29. 9
      rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/sms/TbSendSmsNode.java
  30. 4
      rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/rest/TbHttpClientTest.java
  31. 10
      ui-ngx/src/app/core/api/alias-controller.ts
  32. 10
      ui-ngx/src/app/core/services/utils.service.ts
  33. 4
      ui-ngx/src/app/core/ws/websocket.service.ts
  34. 3
      ui-ngx/src/app/modules/dashboard/dashboard-pages.routing.module.ts
  35. 2
      ui-ngx/src/app/modules/home/components/dashboard-page/dashboard-page.component.html
  36. 11
      ui-ngx/src/app/modules/home/components/dashboard-page/dashboard-page.component.ts
  37. 11
      ui-ngx/src/app/modules/home/components/widget/config/basic/cards/entities-table-basic-config.component.html
  38. 24
      ui-ngx/src/app/modules/home/components/widget/config/basic/cards/entities-table-basic-config.component.ts
  39. 16
      ui-ngx/src/app/modules/home/components/widget/config/basic/common/data-key-row.component.html
  40. 16
      ui-ngx/src/app/modules/home/components/widget/config/basic/common/data-key-row.component.scss
  41. 29
      ui-ngx/src/app/modules/home/components/widget/config/basic/common/data-key-row.component.ts
  42. 20
      ui-ngx/src/app/modules/home/components/widget/config/basic/common/data-keys-panel.component.html
  43. 17
      ui-ngx/src/app/modules/home/components/widget/config/basic/common/data-keys-panel.component.scss
  44. 7
      ui-ngx/src/app/modules/home/components/widget/config/basic/common/data-keys-panel.component.ts
  45. 7
      ui-ngx/src/app/modules/home/components/widget/config/data-key-config-dialog.component.html
  46. 38
      ui-ngx/src/app/modules/home/components/widget/config/data-key-config-dialog.component.ts
  47. 134
      ui-ngx/src/app/modules/home/components/widget/config/data-key-config.component.html
  48. 23
      ui-ngx/src/app/modules/home/components/widget/config/data-key-config.component.scss
  49. 17
      ui-ngx/src/app/modules/home/components/widget/config/data-key-config.component.ts
  50. 99
      ui-ngx/src/app/modules/home/components/widget/lib/settings/cards/entities-table-key-settings.component.html
  51. 147
      ui-ngx/src/app/modules/home/components/widget/lib/settings/cards/entities-table-widget-settings.component.html
  52. 4
      ui-ngx/src/app/modules/home/components/widget/lib/settings/widget-settings.scss
  53. 4
      ui-ngx/src/app/modules/home/components/widget/widget-config.component.html
  54. 2
      ui-ngx/src/app/modules/home/pages/admin/mail-server.component.html
  55. 3
      ui-ngx/src/app/modules/home/pages/admin/mail-server.component.ts
  56. 1
      ui-ngx/src/app/modules/home/pages/customer/customer-routing.module.ts
  57. 2
      ui-ngx/src/app/modules/home/pages/dashboard/dashboard-routing.module.ts
  58. 1
      ui-ngx/src/app/modules/home/pages/edge/edge-routing.module.ts
  59. 5
      ui-ngx/src/app/shared/models/widget.models.ts
  60. 11
      ui-ngx/src/assets/locale/locale.constant-en_US.json
  61. 30
      ui-ngx/src/styles.scss

4
application/src/main/java/org/thingsboard/server/actors/ActorSystemContext.java

@ -536,6 +536,10 @@ public class ActorSystemContext {
@Getter @Getter
private int maxRpcRetries; private int maxRpcRetries;
@Value("${actors.rule.external.force_ack:false}")
@Getter
private boolean externalNodeForceAck;
@Getter @Getter
@Setter @Setter
private TbActorSystem actorSystem; private TbActorSystem actorSystem;

43
application/src/main/java/org/thingsboard/server/actors/ruleChain/DefaultTbContext.java

@ -33,7 +33,7 @@ import org.thingsboard.rule.engine.api.RuleEngineTelemetryService;
import org.thingsboard.rule.engine.api.ScriptEngine; import org.thingsboard.rule.engine.api.ScriptEngine;
import org.thingsboard.rule.engine.api.SmsService; import org.thingsboard.rule.engine.api.SmsService;
import org.thingsboard.rule.engine.api.TbContext; import org.thingsboard.rule.engine.api.TbContext;
import org.thingsboard.server.common.data.msg.TbNodeConnectionType; import org.thingsboard.rule.engine.api.TbNodeException;
import org.thingsboard.rule.engine.api.slack.SlackService; import org.thingsboard.rule.engine.api.slack.SlackService;
import org.thingsboard.rule.engine.api.sms.SmsSenderFactory; import org.thingsboard.rule.engine.api.sms.SmsSenderFactory;
import org.thingsboard.rule.engine.util.TenantIdLoader; import org.thingsboard.rule.engine.util.TenantIdLoader;
@ -59,6 +59,7 @@ import org.thingsboard.server.common.data.id.RuleChainId;
import org.thingsboard.server.common.data.id.RuleNodeId; import org.thingsboard.server.common.data.id.RuleNodeId;
import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.common.data.id.TenantId;
import org.thingsboard.server.common.data.kv.AttributeKvEntry; import org.thingsboard.server.common.data.kv.AttributeKvEntry;
import org.thingsboard.server.common.data.msg.TbNodeConnectionType;
import org.thingsboard.server.common.data.page.PageData; import org.thingsboard.server.common.data.page.PageData;
import org.thingsboard.server.common.data.page.PageLink; import org.thingsboard.server.common.data.page.PageLink;
import org.thingsboard.server.common.data.rule.RuleNode; import org.thingsboard.server.common.data.rule.RuleNode;
@ -216,6 +217,12 @@ class DefaultTbContext implements TbContext {
enqueueForTellNext(tpi, tbMsg, Collections.singleton(TbNodeConnectionType.FAILURE), failureMessage, null, null); enqueueForTellNext(tpi, tbMsg, Collections.singleton(TbNodeConnectionType.FAILURE), failureMessage, null, null);
} }
@Override
public void enqueueForTellFailure(TbMsg tbMsg, Throwable th) {
TopicPartitionInfo tpi = resolvePartition(tbMsg);
enqueueForTellNext(tpi, tbMsg, Collections.singleton(TbNodeConnectionType.FAILURE), getFailureMessage(th), null, null);
}
@Override @Override
public void enqueueForTellNext(TbMsg tbMsg, String relationType) { public void enqueueForTellNext(TbMsg tbMsg, String relationType) {
TopicPartitionInfo tpi = resolvePartition(tbMsg); TopicPartitionInfo tpi = resolvePartition(tbMsg);
@ -313,16 +320,7 @@ class DefaultTbContext implements TbContext {
if (nodeCtx.getSelf().isDebugMode()) { if (nodeCtx.getSelf().isDebugMode()) {
mainCtx.persistDebugOutput(nodeCtx.getTenantId(), nodeCtx.getSelf().getId(), msg, TbNodeConnectionType.FAILURE, th); mainCtx.persistDebugOutput(nodeCtx.getTenantId(), nodeCtx.getSelf().getId(), msg, TbNodeConnectionType.FAILURE, th);
} }
String failureMessage; String failureMessage = getFailureMessage(th);
if (th != null) {
if (!StringUtils.isEmpty(th.getMessage())) {
failureMessage = th.getMessage();
} else {
failureMessage = th.getClass().getSimpleName();
}
} else {
failureMessage = null;
}
nodeCtx.getChainActor().tell(new RuleNodeToRuleChainTellNextMsg(nodeCtx.getSelf().getRuleChainId(), nodeCtx.getChainActor().tell(new RuleNodeToRuleChainTellNextMsg(nodeCtx.getSelf().getRuleChainId(),
nodeCtx.getSelf().getId(), Collections.singleton(TbNodeConnectionType.FAILURE), nodeCtx.getSelf().getId(), Collections.singleton(TbNodeConnectionType.FAILURE),
msg, failureMessage)); msg, failureMessage));
@ -726,6 +724,11 @@ class DefaultTbContext implements TbContext {
return mainCtx.getSlackService(); return mainCtx.getSlackService();
} }
@Override
public boolean isExternalNodeForceAck() {
return mainCtx.isExternalNodeForceAck();
}
@Override @Override
public RuleEngineRpcService getRpcService() { public RuleEngineRpcService getRpcService() {
return mainCtx.getTbRuleEngineDeviceRpcService(); return mainCtx.getTbRuleEngineDeviceRpcService();
@ -842,10 +845,24 @@ class DefaultTbContext implements TbContext {
} }
@Override @Override
public void checkTenantEntity(EntityId entityId) { public void checkTenantEntity(EntityId entityId) throws TbNodeException {
if (!this.getTenantId().equals(TenantIdLoader.findTenantId(this, entityId))) { if (!this.getTenantId().equals(TenantIdLoader.findTenantId(this, entityId))) {
throw new RuntimeException("Entity with id: '" + entityId + "' specified in the configuration doesn't belong to the current tenant."); throw new TbNodeException("Entity with id: '" + entityId + "' specified in the configuration doesn't belong to the current tenant.", true);
}
}
private static String getFailureMessage(Throwable th) {
String failureMessage;
if (th != null) {
if (!StringUtils.isEmpty(th.getMessage())) {
failureMessage = th.getMessage();
} else {
failureMessage = th.getClass().getSimpleName();
}
} else {
failureMessage = null;
} }
return failureMessage;
} }
private class SimpleTbQueueCallback implements TbQueueCallback { private class SimpleTbQueueCallback implements TbQueueCallback {

9
application/src/main/java/org/thingsboard/server/actors/ruleChain/RuleChainManagerActor.java

@ -23,6 +23,7 @@ import org.thingsboard.server.actors.TbEntityActorId;
import org.thingsboard.server.actors.TbEntityTypeActorIdPredicate; import org.thingsboard.server.actors.TbEntityTypeActorIdPredicate;
import org.thingsboard.server.actors.service.ContextAwareActor; import org.thingsboard.server.actors.service.ContextAwareActor;
import org.thingsboard.server.actors.service.DefaultActorService; import org.thingsboard.server.actors.service.DefaultActorService;
import org.thingsboard.server.actors.shared.RuleChainErrorActor;
import org.thingsboard.server.common.data.EntityType; import org.thingsboard.server.common.data.EntityType;
import org.thingsboard.server.common.data.id.EntityId; import org.thingsboard.server.common.data.id.EntityId;
import org.thingsboard.server.common.data.id.RuleChainId; import org.thingsboard.server.common.data.id.RuleChainId;
@ -31,6 +32,7 @@ import org.thingsboard.server.common.data.page.PageDataIterable;
import org.thingsboard.server.common.data.rule.RuleChain; import org.thingsboard.server.common.data.rule.RuleChain;
import org.thingsboard.server.common.data.rule.RuleChainType; import org.thingsboard.server.common.data.rule.RuleChainType;
import org.thingsboard.server.common.msg.TbActorMsg; import org.thingsboard.server.common.msg.TbActorMsg;
import org.thingsboard.server.common.msg.queue.RuleEngineException;
import org.thingsboard.server.dao.rule.RuleChainService; import org.thingsboard.server.dao.rule.RuleChainService;
import java.util.function.Function; import java.util.function.Function;
@ -86,7 +88,12 @@ public abstract class RuleChainManagerActor extends ContextAwareActor {
() -> DefaultActorService.RULE_DISPATCHER_NAME, () -> DefaultActorService.RULE_DISPATCHER_NAME,
() -> { () -> {
RuleChain ruleChain = provider.apply(ruleChainId); RuleChain ruleChain = provider.apply(ruleChainId);
return new RuleChainActor.ActorCreator(systemContext, tenantId, ruleChain); if (ruleChain == null) {
return new RuleChainErrorActor.ActorCreator(systemContext, tenantId,
new RuleEngineException("Rule Chain with id: " + ruleChainId + " not found!"));
} else {
return new RuleChainActor.ActorCreator(systemContext, tenantId, ruleChain);
}
}); });
} }

78
application/src/main/java/org/thingsboard/server/actors/shared/RuleChainErrorActor.java

@ -0,0 +1,78 @@
/**
* Copyright © 2016-2023 The Thingsboard Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.thingsboard.server.actors.shared;
import lombok.extern.slf4j.Slf4j;
import org.thingsboard.server.actors.ActorSystemContext;
import org.thingsboard.server.actors.TbActor;
import org.thingsboard.server.actors.TbActorId;
import org.thingsboard.server.actors.TbStringActorId;
import org.thingsboard.server.actors.service.ContextAwareActor;
import org.thingsboard.server.actors.service.ContextBasedCreator;
import org.thingsboard.server.common.data.id.TenantId;
import org.thingsboard.server.common.msg.TbActorMsg;
import org.thingsboard.server.common.msg.aware.RuleChainAwareMsg;
import org.thingsboard.server.common.msg.queue.RuleEngineException;
import java.util.UUID;
@Slf4j
public class RuleChainErrorActor extends ContextAwareActor {
private final TenantId tenantId;
private final RuleEngineException error;
private RuleChainErrorActor(ActorSystemContext systemContext, TenantId tenantId, RuleEngineException error) {
super(systemContext);
this.tenantId = tenantId;
this.error = error;
}
@Override
protected boolean doProcess(TbActorMsg msg) {
if (msg instanceof RuleChainAwareMsg) {
log.debug("[{}] Reply with {} for message {}", tenantId, error.getMessage(), msg);
var rcMsg = (RuleChainAwareMsg) msg;
rcMsg.getMsg().getCallback().onFailure(error);
return true;
} else {
return false;
}
}
public static class ActorCreator extends ContextBasedCreator {
private final TenantId tenantId;
private final RuleEngineException error;
public ActorCreator(ActorSystemContext context, TenantId tenantId, RuleEngineException error) {
super(context);
this.tenantId = tenantId;
this.error = error;
}
@Override
public TbActorId createActorId() {
return new TbStringActorId(UUID.randomUUID().toString());
}
@Override
public TbActor createActor() {
return new RuleChainErrorActor(context, tenantId, error);
}
}
}

4
application/src/main/resources/thingsboard.yml

@ -393,6 +393,10 @@ actors:
queue_size: "${ACTORS_RULE_TRANSACTION_QUEUE_SIZE:15000}" queue_size: "${ACTORS_RULE_TRANSACTION_QUEUE_SIZE:15000}"
# Time in milliseconds for transaction to complete # Time in milliseconds for transaction to complete
duration: "${ACTORS_RULE_TRANSACTION_DURATION:60000}" duration: "${ACTORS_RULE_TRANSACTION_DURATION:60000}"
external:
# Force acknowledgement of the incoming message for external rule nodes to decrease processing latency.
# Enqueue the result of external node processing as a separate message to the rule engine.
force_ack: "${ACTORS_RULE_EXTERNAL_NODE_FORCE_ACK:false}"
rpc: rpc:
max_retries: "${ACTORS_RPC_MAX_RETRIES:5}" max_retries: "${ACTORS_RPC_MAX_RETRIES:5}"
sequential: "${ACTORS_RPC_SEQUENTIAL:false}" sequential: "${ACTORS_RPC_SEQUENTIAL:false}"

17
common/actor/src/main/java/org/thingsboard/server/actors/TbActorMailbox.java

@ -19,6 +19,7 @@ import lombok.Data;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
import org.thingsboard.server.common.data.EntityType; import org.thingsboard.server.common.data.EntityType;
import org.thingsboard.server.common.msg.MsgType; import org.thingsboard.server.common.msg.MsgType;
import org.thingsboard.server.common.msg.TbActorError;
import org.thingsboard.server.common.msg.TbActorMsg; import org.thingsboard.server.common.msg.TbActorMsg;
import org.thingsboard.server.common.msg.TbActorStopReason; import org.thingsboard.server.common.msg.TbActorStopReason;
@ -69,9 +70,14 @@ public final class TbActorMailbox implements TbActorCtx {
} }
} }
} catch (Throwable t) { } catch (Throwable t) {
log.debug("[{}] Failed to init actor, attempt: {}", selfId, attempt, t); InitFailureStrategy strategy;
int attemptIdx = attempt + 1; int attemptIdx = attempt + 1;
InitFailureStrategy strategy = actor.onInitFailure(attempt, t); if (isUnrecoverable(t)) {
strategy = InitFailureStrategy.stop();
} else {
log.debug("[{}] Failed to init actor, attempt: {}", selfId, attempt, t);
strategy = actor.onInitFailure(attempt, t);
}
if (strategy.isStop() || (settings.getMaxActorInitAttempts() > 0 && attemptIdx > settings.getMaxActorInitAttempts())) { if (strategy.isStop() || (settings.getMaxActorInitAttempts() > 0 && attemptIdx > settings.getMaxActorInitAttempts())) {
log.info("[{}] Failed to init actor, attempt {}, going to stop attempts.", selfId, attempt, t); log.info("[{}] Failed to init actor, attempt {}, going to stop attempts.", selfId, attempt, t);
stopReason = TbActorStopReason.INIT_FAILED; stopReason = TbActorStopReason.INIT_FAILED;
@ -88,6 +94,13 @@ public final class TbActorMailbox implements TbActorCtx {
} }
} }
private static boolean isUnrecoverable(Throwable t) {
if (t instanceof TbActorException && t.getCause() != null) {
t = t.getCause();
}
return t instanceof TbActorError && ((TbActorError) t).isUnrecoverable();
}
private void enqueue(TbActorMsg msg, boolean highPriority) { private void enqueue(TbActorMsg msg, boolean highPriority) {
if (!destroyInProgress.get()) { if (!destroyInProgress.get()) {
if (highPriority) { if (highPriority) {

22
common/message/src/main/java/org/thingsboard/server/common/msg/TbActorError.java

@ -0,0 +1,22 @@
/**
* Copyright © 2016-2023 The Thingsboard Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.thingsboard.server.common.msg;
public interface TbActorError {
boolean isUnrecoverable();
}

5
common/message/src/main/java/org/thingsboard/server/common/msg/TbMsg.java

@ -277,6 +277,11 @@ public final class TbMsg implements Serializable {
this.metaData, this.dataType, this.data, ruleChainId, ruleNodeId, this.ctx, callback); this.metaData, this.dataType, this.data, ruleChainId, ruleNodeId, this.ctx, callback);
} }
public TbMsg copyWithNewCtx() {
return new TbMsg(this.queueName, this.id, this.ts, this.type, this.originator, this.customerId,
this.metaData, this.dataType, this.data, ruleChainId, ruleNodeId, this.ctx.copy(), TbMsgCallback.EMPTY);
}
public TbMsgCallback getCallback() { public TbMsgCallback getCallback() {
// May be null in case of deserialization; // May be null in case of deserialization;
if (callback != null) { if (callback != null) {

3
common/message/src/main/java/org/thingsboard/server/common/msg/aware/RuleChainAwareMsg.java

@ -17,9 +17,12 @@ package org.thingsboard.server.common.msg.aware;
import org.thingsboard.server.common.data.id.RuleChainId; import org.thingsboard.server.common.data.id.RuleChainId;
import org.thingsboard.server.common.msg.TbActorMsg; import org.thingsboard.server.common.msg.TbActorMsg;
import org.thingsboard.server.common.msg.TbMsg;
public interface RuleChainAwareMsg extends TbActorMsg { public interface RuleChainAwareMsg extends TbActorMsg {
RuleChainId getRuleChainId(); RuleChainId getRuleChainId();
TbMsg getMsg();
} }

290
common/script/script-api/src/main/java/org/thingsboard/script/api/tbel/TbUtils.java

@ -15,6 +15,8 @@
*/ */
package org.thingsboard.script.api.tbel; package org.thingsboard.script.api.tbel;
import com.google.common.primitives.Bytes;
import org.apache.commons.lang3.ArrayUtils;
import org.mvel2.ExecutionContext; import org.mvel2.ExecutionContext;
import org.mvel2.ParserConfiguration; import org.mvel2.ParserConfiguration;
import org.mvel2.execution.ExecutionArrayList; import org.mvel2.execution.ExecutionArrayList;
@ -25,11 +27,13 @@ import org.thingsboard.server.common.data.StringUtils;
import java.io.IOException; import java.io.IOException;
import java.io.UnsupportedEncodingException; import java.io.UnsupportedEncodingException;
import java.math.BigDecimal; import java.math.BigDecimal;
import java.math.BigInteger;
import java.math.RoundingMode; import java.math.RoundingMode;
import java.nio.ByteBuffer; import java.nio.ByteBuffer;
import java.nio.ByteOrder; import java.nio.ByteOrder;
import java.nio.charset.StandardCharsets; import java.nio.charset.StandardCharsets;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Arrays;
import java.util.Base64; import java.util.Base64;
import java.util.Collection; import java.util.Collection;
import java.util.List; import java.util.List;
@ -61,6 +65,10 @@ public class TbUtils {
String.class))); String.class)));
parserConfig.addImport("parseInt", new MethodStub(TbUtils.class.getMethod("parseInt", parserConfig.addImport("parseInt", new MethodStub(TbUtils.class.getMethod("parseInt",
String.class, int.class))); String.class, int.class)));
parserConfig.addImport("parseLong", new MethodStub(TbUtils.class.getMethod("parseLong",
String.class)));
parserConfig.addImport("parseLong", new MethodStub(TbUtils.class.getMethod("parseLong",
String.class, int.class)));
parserConfig.addImport("parseFloat", new MethodStub(TbUtils.class.getMethod("parseFloat", parserConfig.addImport("parseFloat", new MethodStub(TbUtils.class.getMethod("parseFloat",
String.class))); String.class)));
parserConfig.addImport("parseDouble", new MethodStub(TbUtils.class.getMethod("parseDouble", parserConfig.addImport("parseDouble", new MethodStub(TbUtils.class.getMethod("parseDouble",
@ -81,8 +89,58 @@ public class TbUtils {
byte[].class, int.class, int.class))); byte[].class, int.class, int.class)));
parserConfig.addImport("parseBytesToInt", new MethodStub(TbUtils.class.getMethod("parseBytesToInt", parserConfig.addImport("parseBytesToInt", new MethodStub(TbUtils.class.getMethod("parseBytesToInt",
byte[].class, int.class, int.class, boolean.class))); byte[].class, int.class, int.class, boolean.class)));
parserConfig.addImport("parseLittleEndianHexToLong", new MethodStub(TbUtils.class.getMethod("parseLittleEndianHexToLong",
String.class)));
parserConfig.addImport("parseBigEndianHexToLong", new MethodStub(TbUtils.class.getMethod("parseBigEndianHexToLong",
String.class)));
parserConfig.addImport("parseHexToLong", new MethodStub(TbUtils.class.getMethod("parseHexToLong",
String.class)));
parserConfig.addImport("parseHexToLong", new MethodStub(TbUtils.class.getMethod("parseHexToLong",
String.class, boolean.class)));
parserConfig.addImport("parseBytesToLong", new MethodStub(TbUtils.class.getMethod("parseBytesToLong",
List.class, int.class, int.class)));
parserConfig.addImport("parseBytesToLong", new MethodStub(TbUtils.class.getMethod("parseBytesToLong",
List.class, int.class, int.class, boolean.class)));
parserConfig.addImport("parseBytesToLong", new MethodStub(TbUtils.class.getMethod("parseBytesToLong",
byte[].class, int.class, int.class)));
parserConfig.addImport("parseBytesToLong", new MethodStub(TbUtils.class.getMethod("parseBytesToLong",
byte[].class, int.class, int.class, boolean.class)));
parserConfig.addImport("parseLittleEndianHexToFloat", new MethodStub(TbUtils.class.getMethod("parseLittleEndianHexToFloat",
String.class)));
parserConfig.addImport("parseBigEndianHexToFloat", new MethodStub(TbUtils.class.getMethod("parseBigEndianHexToFloat",
String.class)));
parserConfig.addImport("parseHexToFloat", new MethodStub(TbUtils.class.getMethod("parseHexToFloat",
String.class)));
parserConfig.addImport("parseHexToFloat", new MethodStub(TbUtils.class.getMethod("parseHexToFloat",
String.class, boolean.class)));
parserConfig.addImport("parseBytesToFloat", new MethodStub(TbUtils.class.getMethod("parseBytesToFloat",
byte[].class, int.class, boolean.class)));
parserConfig.addImport("parseBytesToFloat", new MethodStub(TbUtils.class.getMethod("parseBytesToFloat",
byte[].class, int.class)));
parserConfig.addImport("parseBytesToFloat", new MethodStub(TbUtils.class.getMethod("parseBytesToFloat",
List.class, int.class, boolean.class)));
parserConfig.addImport("parseBytesToFloat", new MethodStub(TbUtils.class.getMethod("parseBytesToFloat",
List.class, int.class)));
parserConfig.addImport("parseLittleEndianHexToDouble", new MethodStub(TbUtils.class.getMethod("parseLittleEndianHexToDouble",
String.class)));
parserConfig.addImport("parseBigEndianHexToDouble", new MethodStub(TbUtils.class.getMethod("parseBigEndianHexToDouble",
String.class)));
parserConfig.addImport("parseHexToDouble", new MethodStub(TbUtils.class.getMethod("parseHexToDouble",
String.class)));
parserConfig.addImport("parseHexToDouble", new MethodStub(TbUtils.class.getMethod("parseHexToDouble",
String.class, boolean.class)));
parserConfig.addImport("parseBytesToDouble", new MethodStub(TbUtils.class.getMethod("parseBytesToDouble",
byte[].class, int.class)));
parserConfig.addImport("parseBytesToDouble", new MethodStub(TbUtils.class.getMethod("parseBytesToDouble",
byte[].class, int.class, boolean.class)));
parserConfig.addImport("parseBytesToDouble", new MethodStub(TbUtils.class.getMethod("parseBytesToDouble",
List.class, int.class)));
parserConfig.addImport("parseBytesToDouble", new MethodStub(TbUtils.class.getMethod("parseBytesToDouble",
List.class, int.class, boolean.class)));
parserConfig.addImport("toFixed", new MethodStub(TbUtils.class.getMethod("toFixed", parserConfig.addImport("toFixed", new MethodStub(TbUtils.class.getMethod("toFixed",
double.class, int.class))); double.class, int.class)));
parserConfig.addImport("toFixed", new MethodStub(TbUtils.class.getMethod("toFixed",
float.class, int.class)));
parserConfig.addImport("hexToBytes", new MethodStub(TbUtils.class.getMethod("hexToBytes", parserConfig.addImport("hexToBytes", new MethodStub(TbUtils.class.getMethod("hexToBytes",
ExecutionContext.class, String.class))); ExecutionContext.class, String.class)));
parserConfig.addImport("base64ToHex", new MethodStub(TbUtils.class.getMethod("base64ToHex", parserConfig.addImport("base64ToHex", new MethodStub(TbUtils.class.getMethod("base64ToHex",
@ -154,37 +212,73 @@ public class TbUtils {
} }
public static Integer parseInt(String value) { public static Integer parseInt(String value) {
if (value != null) { int radix = getRadix(value);
return parseInt(value, radix);
}
public static Integer parseInt(String value, int radix) {
if (StringUtils.isNotBlank(value)) {
try { try {
int radix = 10; String valueP = prepareNumberString(value);
if (isHexadecimal(value)) { isValidRadix(valueP, radix);
radix = 16; try {
return Integer.parseInt(valueP, radix);
} catch (NumberFormatException e) {
BigInteger bi = new BigInteger(valueP, radix);
if (bi.compareTo(BigInteger.valueOf(Integer.MAX_VALUE)) > 0)
throw new NumberFormatException("Value \"" + value + "\" is greater than the maximum Integer value " + Integer.MAX_VALUE + " !");
if (bi.compareTo(BigInteger.valueOf(Integer.MIN_VALUE)) < 0)
throw new NumberFormatException("Value \"" + value + "\" is less than the minimum Integer value " + Integer.MIN_VALUE + " !");
Float f = parseFloat(valueP);
if (f != null) {
return f.intValue();
} else {
throw new NumberFormatException(e.getMessage());
}
} }
return Integer.parseInt(prepareNumberString(value), radix);
} catch (NumberFormatException e) { } catch (NumberFormatException e) {
Float f = parseFloat(value); throw new NumberFormatException(e.getMessage());
if (f != null) {
return f.intValue();
}
} }
} }
return null; return null;
} }
public static Integer parseInt(String value, int radix) { public static Long parseLong(String value) {
if (value != null) { int radix = getRadix(value);
return parseLong(value, radix);
}
public static Long parseLong(String value, int radix) {
if (StringUtils.isNotBlank(value)) {
try { try {
return Integer.parseInt(prepareNumberString(value), radix); String valueP = prepareNumberString(value);
} catch (NumberFormatException e) { isValidRadix(valueP, radix);
Float f = parseFloat(value); try {
if (f != null) { return Long.parseLong(valueP, radix);
return f.intValue(); } catch (NumberFormatException e) {
BigInteger bi = new BigInteger(valueP, radix);
if (bi.compareTo(BigInteger.valueOf(Long.MAX_VALUE)) > 0)
throw new NumberFormatException("Value \"" + value + "\"is greater than the maximum Long value " + Long.MAX_VALUE + " !");
if (bi.compareTo(BigInteger.valueOf(Long.MIN_VALUE)) < 0)
throw new NumberFormatException("Value \"" + value + "\" is less than the minimum Long value " + Long.MIN_VALUE + " !");
Double dd = parseDouble(valueP);
if (dd != null) {
return dd.longValue();
} else {
throw new NumberFormatException(e.getMessage());
}
} }
} catch (NumberFormatException e) {
throw new NumberFormatException(e.getMessage());
} }
} }
return null; return null;
} }
private static int getRadix(String value, int... radixS) {
return radixS.length > 0 ? radixS[0] : isHexadecimal(value) ? 16 : 10;
}
public static Float parseFloat(String value) { public static Float parseFloat(String value) {
if (value != null) { if (value != null) {
try { try {
@ -218,8 +312,64 @@ public class TbUtils {
} }
public static int parseHexToInt(String hex, boolean bigEndian) { public static int parseHexToInt(String hex, boolean bigEndian) {
byte[] data = prepareHexToBytesNumber(hex, 8);
return parseBytesToInt(data, 0, data.length, bigEndian);
}
public static long parseLittleEndianHexToLong(String hex) {
return parseHexToLong(hex, false);
}
public static long parseBigEndianHexToLong(String hex) {
return parseHexToLong(hex, true);
}
public static long parseHexToLong(String hex) {
return parseHexToLong(hex, true);
}
public static long parseHexToLong(String hex, boolean bigEndian) {
byte[] data = prepareHexToBytesNumber(hex, 16);
return parseBytesToLong(data, 0, data.length, bigEndian);
}
public static float parseLittleEndianHexToFloat(String hex) {
return parseHexToFloat(hex, false);
}
public static float parseBigEndianHexToFloat(String hex) {
return parseHexToFloat(hex, true);
}
public static float parseHexToFloat(String hex) {
return parseHexToFloat(hex, true);
}
public static float parseHexToFloat(String hex, boolean bigEndian) {
byte[] data = prepareHexToBytesNumber(hex, 8);
return parseBytesToFloat(data, 0, bigEndian);
}
public static double parseLittleEndianHexToDouble(String hex) {
return parseHexToDouble(hex, false);
}
public static double parseBigEndianHexToDouble(String hex) {
return parseHexToDouble(hex, true);
}
public static double parseHexToDouble(String hex) {
return parseHexToDouble(hex, true);
}
public static double parseHexToDouble(String hex, boolean bigEndian) {
byte[] data = prepareHexToBytesNumber(hex, 16);
return parseBytesToDouble(data, 0, bigEndian);
}
private static byte[] prepareHexToBytesNumber(String hex, int len) {
int length = hex.length(); int length = hex.length();
if (length > 8) { if (length > len) {
throw new IllegalArgumentException("Hex string is too large. Maximum 8 symbols allowed."); throw new IllegalArgumentException("Hex string is too large. Maximum 8 symbols allowed.");
} }
if (length % 2 > 0) { if (length % 2 > 0) {
@ -229,7 +379,7 @@ public class TbUtils {
for (int i = 0; i < length; i += 2) { for (int i = 0; i < length; i += 2) {
data[i / 2] = (byte) ((Character.digit(hex.charAt(i), 16) << 4) + Character.digit(hex.charAt(i + 1), 16)); data[i / 2] = (byte) ((Character.digit(hex.charAt(i), 16) << 4) + Character.digit(hex.charAt(i + 1), 16));
} }
return parseBytesToInt(data, 0, data.length, bigEndian); return data;
} }
public static ExecutionArrayList<Byte> hexToBytes(ExecutionContext ctx, String hex) { public static ExecutionArrayList<Byte> hexToBytes(ExecutionContext ctx, String hex) {
@ -293,6 +443,91 @@ public class TbUtils {
return bb.getInt(); return bb.getInt();
} }
public static long parseBytesToLong(List<Byte> data, int offset, int length) {
return parseBytesToLong(data, offset, length, true);
}
public static long parseBytesToLong(List<Byte> data, int offset, int length, boolean bigEndian) {
final byte[] bytes = new byte[data.size()];
for (int i = 0; i < bytes.length; i++) {
bytes[i] = data.get(i);
}
return parseBytesToLong(bytes, offset, length, bigEndian);
}
public static long parseBytesToLong(byte[] data, int offset, int length) {
return parseBytesToLong(data, offset, length, true);
}
public static long parseBytesToLong(byte[] data, int offset, int length, boolean bigEndian) {
if (offset > data.length) {
throw new IllegalArgumentException("Offset: " + offset + " is out of bounds for array with length: " + data.length + "!");
}
if (length > 8) {
throw new IllegalArgumentException("Length: " + length + " is too large. Maximum 4 bytes is allowed!");
}
if (offset + length > data.length) {
throw new IllegalArgumentException("Offset: " + offset + " and Length: " + length + " is out of bounds for array with length: " + data.length + "!");
}
var bb = ByteBuffer.allocate(8);
if (!bigEndian) {
bb.order(ByteOrder.LITTLE_ENDIAN);
}
bb.position(bigEndian ? 8 - length : 0);
bb.put(data, offset, length);
bb.position(0);
return bb.getLong();
}
public static float parseBytesToFloat(byte[] data, int offset) {
return parseBytesToFloat(data, offset, true);
}
public static float parseBytesToFloat(List data, int offset) {
return parseBytesToFloat(data, offset, true);
}
public static float parseBytesToFloat(List data, int offset, boolean bigEndian) {
return parseBytesToFloat(Bytes.toArray(data), offset, bigEndian);
}
public static float parseBytesToFloat(byte[] data, int offset, boolean bigEndian) {
byte[] bytesToNumber = prepareBytesToNumber(data, offset, 4, bigEndian);
return ByteBuffer.wrap(bytesToNumber).getFloat();
}
public static double parseBytesToDouble(byte[] data, int offset) {
return parseBytesToDouble(data, offset, true);
}
public static double parseBytesToDouble(List data, int offset) {
return parseBytesToDouble(data, offset, true);
}
public static double parseBytesToDouble(List data, int offset, boolean bigEndian) {
return parseBytesToDouble(Bytes.toArray(data), offset, bigEndian);
}
public static double parseBytesToDouble(byte[] data, int offset, boolean bigEndian) {
byte[] bytesToNumber = prepareBytesToNumber(data, offset, 8, bigEndian);
return ByteBuffer.wrap(bytesToNumber).getDouble();
}
private static byte[] prepareBytesToNumber(byte[] data, int offset, int length, boolean bigEndian) {
if (offset > data.length) {
throw new IllegalArgumentException("Offset: " + offset + " is out of bounds for array with length: " + data.length + "!");
}
if ((offset + length) > data.length) {
throw new IllegalArgumentException("Default length is always " + length + " bytes. Offset: " + offset + " and Length: " + length + " is out of bounds for array with length: " + data.length + "!");
}
byte[] dataBytesArray = Arrays.copyOfRange(data, offset, (offset + length));
if (!bigEndian) {
ArrayUtils.reverse(dataBytesArray);
}
return dataBytesArray;
}
public static String bytesToHex(ExecutionArrayList<?> bytesList) { public static String bytesToHex(ExecutionArrayList<?> bytesList) {
byte[] bytes = new byte[bytesList.size()]; byte[] bytes = new byte[bytesList.size()];
for (int i = 0; i < bytesList.size(); i++) { for (int i = 0; i < bytesList.size(); i++) {
@ -315,6 +550,10 @@ public class TbUtils {
return BigDecimal.valueOf(value).setScale(precision, RoundingMode.HALF_UP).doubleValue(); return BigDecimal.valueOf(value).setScale(precision, RoundingMode.HALF_UP).doubleValue();
} }
public static float toFixed(float value, int precision) {
return BigDecimal.valueOf(value).setScale(precision, RoundingMode.HALF_UP).floatValue();
}
private static boolean isHexadecimal(String value) { private static boolean isHexadecimal(String value) {
return value != null && (value.contains("0x") || value.contains("0X")); return value != null && (value.contains("0x") || value.contains("0X"));
} }
@ -388,4 +627,19 @@ public class TbUtils {
} }
} }
} }
public static boolean isValidRadix(String value, int radix) {
for (int i = 0; i < value.length(); i++) {
if (i == 0 && value.charAt(i) == '-') {
if (value.length() == 1)
throw new NumberFormatException("Failed radix [" + radix + "] for value: \"" + value + "\"!");
else
continue;
}
if (Character.digit(value.charAt(i), radix) < 0)
throw new NumberFormatException("Failed radix: [" + radix + "] for value: \"" + value + "\"!");
}
return true;
}
} }

167
common/script/script-api/src/test/java/org/thingsboard/script/api/tbel/TbUtilsTest.java

@ -15,6 +15,7 @@
*/ */
package org.thingsboard.script.api.tbel; package org.thingsboard.script.api.tbel;
import com.google.common.primitives.Bytes;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
import org.junit.After; import org.junit.After;
import org.junit.Assert; import org.junit.Assert;
@ -26,6 +27,7 @@ import org.mvel2.SandboxedParserConfiguration;
import org.mvel2.execution.ExecutionArrayList; import org.mvel2.execution.ExecutionArrayList;
import org.mvel2.execution.ExecutionHashMap; import org.mvel2.execution.ExecutionHashMap;
import java.math.BigInteger;
import java.nio.ByteBuffer; import java.nio.ByteBuffer;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Calendar; import java.util.Calendar;
@ -38,6 +40,23 @@ public class TbUtilsTest {
private ExecutionContext ctx; private ExecutionContext ctx;
private final String intValHex = "41EA62CC";
private final float floatVal = 29.29824f;
private final String floatValStr = "29.29824";
private final String floatValHexRev = "CC62EA41";
private final float floatValRev = -5.948442E7f;
private final long longVal = 0x409B04B10CB295EAL;
private final String longValHex = "409B04B10CB295EA";
private final long longValRev = 0xEA95B20CB1049B40L;
private final String longValHexRev = "EA95B20CB1049B40";
private final String doubleValStr = "1729.1729";
private final double doubleVal = 1729.1729;
private final double doubleValRev = -2.7208640774822924E205;
@Before @Before
public void before() { public void before() {
SandboxedParserConfiguration parserConfig = ParserContext.enableSandboxedMode(); SandboxedParserConfiguration parserConfig = ParserContext.enableSandboxedMode();
@ -62,6 +81,7 @@ public class TbUtilsTest {
@Test @Test
public void parseHexToInt() { public void parseHexToInt() {
Assert.assertEquals(0xAB, TbUtils.parseHexToInt("AB")); Assert.assertEquals(0xAB, TbUtils.parseHexToInt("AB"));
Assert.assertEquals(0xABBA, TbUtils.parseHexToInt("ABBA", true)); Assert.assertEquals(0xABBA, TbUtils.parseHexToInt("ABBA", true));
Assert.assertEquals(0xBAAB, TbUtils.parseHexToInt("ABBA", false)); Assert.assertEquals(0xBAAB, TbUtils.parseHexToInt("ABBA", false));
Assert.assertEquals(0xAABBCC, TbUtils.parseHexToInt("AABBCC", true)); Assert.assertEquals(0xAABBCC, TbUtils.parseHexToInt("AABBCC", true));
@ -182,9 +202,150 @@ public class TbUtilsTest {
Assert.assertEquals(expectedMapWithoutPaths, actualMapWithoutPaths); Assert.assertEquals(expectedMapWithoutPaths, actualMapWithoutPaths);
} }
@Test
public void parseInt() {
Assert.assertNull(TbUtils.parseInt(null));
Assert.assertNull(TbUtils.parseInt(""));
Assert.assertNull(TbUtils.parseInt(" "));
Assert.assertEquals(java.util.Optional.of(0).get(), TbUtils.parseInt("0"));
Assert.assertEquals(java.util.Optional.of(0).get(), TbUtils.parseInt("-0"));
Assert.assertEquals(java.util.Optional.of(473).get(), TbUtils.parseInt("473"));
Assert.assertEquals(java.util.Optional.of(-255).get(), TbUtils.parseInt("-0xFF"));
Assert.assertThrows(NumberFormatException.class, () -> TbUtils.parseInt("FF"));
Assert.assertThrows(NumberFormatException.class, () -> TbUtils.parseInt("0xFG"));
Assert.assertEquals(java.util.Optional.of(102).get(), TbUtils.parseInt("1100110", 2));
Assert.assertThrows(NumberFormatException.class, () -> TbUtils.parseInt("1100210", 2));
Assert.assertEquals(java.util.Optional.of(63).get(), TbUtils.parseInt("77", 8));
Assert.assertThrows(NumberFormatException.class, () -> TbUtils.parseInt("18", 8));
Assert.assertEquals(java.util.Optional.of(-255).get(), TbUtils.parseInt("-FF", 16));
Assert.assertThrows(NumberFormatException.class, () -> TbUtils.parseInt("FG", 16));
Assert.assertEquals(java.util.Optional.of(Integer.MAX_VALUE).get(), TbUtils.parseInt(Integer.toString(Integer.MAX_VALUE), 10));
Assert.assertThrows(NumberFormatException.class, () -> TbUtils.parseInt(BigInteger.valueOf(Integer.MAX_VALUE).add(BigInteger.valueOf(1)).toString(10), 10));
Assert.assertEquals(java.util.Optional.of(Integer.MIN_VALUE).get(), TbUtils.parseInt(Integer.toString(Integer.MIN_VALUE), 10));
Assert.assertThrows(NumberFormatException.class, () -> TbUtils.parseInt(BigInteger.valueOf(Integer.MIN_VALUE).subtract(BigInteger.valueOf(1)).toString(10), 10));
Assert.assertEquals(java.util.Optional.of(506070563).get(), TbUtils.parseInt("KonaIn", 30));
Assert.assertThrows(NumberFormatException.class, () -> TbUtils.parseInt("KonaIn", 10));
}
@Test
public void parseFloat() {
Assert.assertEquals(java.util.Optional.of(floatVal).get(), TbUtils.parseFloat(floatValStr));
}
@Test
public void toFixedFloat() {
float actualF = TbUtils.toFixed(floatVal, 3);
Assert.assertEquals(1, Float.compare(floatVal, actualF));
Assert.assertEquals(0, Float.compare(29.298f, actualF));
}
@Test
public void parseHexToFloat() {
Assert.assertEquals(0, Float.compare(floatVal, TbUtils.parseHexToFloat(intValHex)));
Assert.assertEquals(0, Float.compare(floatValRev, TbUtils.parseHexToFloat(intValHex, false)));
Assert.assertEquals(0, Float.compare(floatVal, TbUtils.parseBigEndianHexToFloat(intValHex)));
Assert.assertEquals(0, Float.compare(floatVal, TbUtils.parseLittleEndianHexToFloat(floatValHexRev)));
}
@Test
public void arseBytesToFloat() {
byte[] floatValByte = {65, -22, 98, -52};
Assert.assertEquals(0, Float.compare(floatVal, TbUtils.parseBytesToFloat(floatValByte, 0)));
Assert.assertEquals(0, Float.compare(floatValRev, TbUtils.parseBytesToFloat(floatValByte, 0, false)));
List <Byte> floatVaList = Bytes.asList(floatValByte);
Assert.assertEquals(0, Float.compare(floatVal, TbUtils.parseBytesToFloat(floatVaList, 0)));
Assert.assertEquals(0, Float.compare(floatValRev, TbUtils.parseBytesToFloat(floatVaList, 0, false)));
}
@Test
public void parseLong() {
Assert.assertNull(TbUtils.parseLong(null));
Assert.assertNull(TbUtils.parseLong(""));
Assert.assertNull(TbUtils.parseLong(" "));
Assert.assertEquals(java.util.Optional.of(0L).get(), TbUtils.parseLong("0"));
Assert.assertEquals(java.util.Optional.of(0L).get(), TbUtils.parseLong("-0"));
Assert.assertEquals(java.util.Optional.of(473L).get(), TbUtils.parseLong("473"));
Assert.assertEquals(java.util.Optional.of(-65535L).get(), TbUtils.parseLong("-0xFFFF"));
Assert.assertThrows(NumberFormatException.class, () -> TbUtils.parseLong("FFFFFFFF"));
Assert.assertThrows(NumberFormatException.class, () -> TbUtils.parseLong("0xFGFFFFFF"));
Assert.assertEquals(java.util.Optional.of(13158L).get(), TbUtils.parseLong("11001101100110", 2));
Assert.assertThrows(NumberFormatException.class, () -> TbUtils.parseLong("11001101100210", 2));
Assert.assertEquals(java.util.Optional.of(9223372036854775807L).get(), TbUtils.parseLong("777777777777777777777", 8));
Assert.assertThrows(NumberFormatException.class, () -> TbUtils.parseLong("1787", 8));
private static String keyToValue(String key, String extraSymbol) { Assert.assertEquals(java.util.Optional.of(-255L).get(), TbUtils.parseLong("-FF", 16));
return key + "Value" + (extraSymbol == null ? "" : extraSymbol); Assert.assertThrows(NumberFormatException.class, () -> TbUtils.parseLong("FG", 16));
Assert.assertEquals(java.util.Optional.of(Long.MAX_VALUE).get(), TbUtils.parseLong(Long.toString(Long.MAX_VALUE), 10));
Assert.assertThrows(NumberFormatException.class, () -> TbUtils.parseLong(BigInteger.valueOf(Long.MAX_VALUE).add(BigInteger.valueOf(1)).toString(10), 10));
Assert.assertEquals(java.util.Optional.of(Long.MIN_VALUE).get(), TbUtils.parseLong(Long.toString(Long.MIN_VALUE), 10));
Assert.assertThrows(NumberFormatException.class, () -> TbUtils.parseLong(BigInteger.valueOf(Long.MIN_VALUE).subtract(BigInteger.valueOf(1)).toString(10), 10));
Assert.assertEquals(java.util.Optional.of(218840926543L).get(), TbUtils.parseLong("KonaLong", 27));
Assert.assertThrows(NumberFormatException.class, () -> TbUtils.parseLong("KonaLong", 10));
}
@Test
public void parseHexToLong() {
Assert.assertEquals(longVal, TbUtils.parseHexToLong(longValHex));
Assert.assertEquals(longVal, TbUtils.parseHexToLong(longValHexRev, false));
Assert.assertEquals(longVal, TbUtils.parseBigEndianHexToLong(longValHex));
Assert.assertEquals(longVal, TbUtils.parseLittleEndianHexToLong(longValHexRev));
}
@Test
public void parseBytesToLong() {
byte[] longValByte = {64, -101, 4, -79, 12, -78, -107, -22};
Assert.assertEquals(longVal, TbUtils.parseBytesToLong(longValByte, 0, 8));
Bytes.reverse(longValByte);
Assert.assertEquals(longVal, TbUtils.parseBytesToLong(longValByte, 0, 8, false));
List <Byte> longVaList = Bytes.asList(longValByte);
Assert.assertEquals(longVal, TbUtils.parseBytesToLong(longVaList, 0, 8, false));
Assert.assertEquals(longValRev, TbUtils.parseBytesToLong(longVaList, 0, 8));
}
@Test
public void parsDouble() {
Assert.assertEquals(java.util.Optional.of(doubleVal).get(), TbUtils.parseDouble(doubleValStr));
}
@Test
public void toFixedDouble() {
double actualD = TbUtils.toFixed(doubleVal, 3);
Assert.assertEquals(-1, Double.compare(doubleVal, actualD));
Assert.assertEquals(0, Double.compare(1729.173, actualD));
}
@Test
public void parseHexToDouble() {
Assert.assertEquals(0, Double.compare(doubleVal, TbUtils.parseHexToDouble(longValHex)));
Assert.assertEquals(0, Double.compare(doubleValRev, TbUtils.parseHexToDouble(longValHex, false)));
Assert.assertEquals(0, Double.compare(doubleVal, TbUtils.parseBigEndianHexToDouble(longValHex)));
Assert.assertEquals(0, Double.compare(doubleVal, TbUtils.parseLittleEndianHexToDouble(longValHexRev)));
}
@Test
public void parseBytesToDouble() {
byte[] doubleValByte = {64, -101, 4, -79, 12, -78, -107, -22};
Assert.assertEquals(0, Double.compare(doubleVal, TbUtils.parseBytesToDouble(doubleValByte, 0)));
Assert.assertEquals(0, Double.compare(doubleValRev, TbUtils.parseBytesToDouble(doubleValByte, 0, false)));
List <Byte> doubleVaList = Bytes.asList(doubleValByte);
Assert.assertEquals(0, Double.compare(doubleVal, TbUtils.parseBytesToDouble(doubleVaList, 0)));
Assert.assertEquals(0, Double.compare(doubleValRev, TbUtils.parseBytesToDouble(doubleVaList, 0, false)));
} }
private static List<Byte> toList(byte[] data) { private static List<Byte> toList(byte[] data) {
@ -194,5 +355,5 @@ public class TbUtilsTest {
} }
return result; return result;
} }
} }

6
rule-engine/rule-engine-api/src/main/java/org/thingsboard/rule/engine/api/TbContext.java

@ -167,6 +167,8 @@ public interface TbContext {
void enqueueForTellFailure(TbMsg msg, String failureMessage); void enqueueForTellFailure(TbMsg msg, String failureMessage);
void enqueueForTellFailure(TbMsg tbMsg, Throwable t);
void enqueueForTellNext(TbMsg msg, String relationType); void enqueueForTellNext(TbMsg msg, String relationType);
void enqueueForTellNext(TbMsg msg, Set<String> relationTypes); void enqueueForTellNext(TbMsg msg, Set<String> relationTypes);
@ -210,7 +212,7 @@ public interface TbContext {
void schedule(Runnable runnable, long delay, TimeUnit timeUnit); void schedule(Runnable runnable, long delay, TimeUnit timeUnit);
void checkTenantEntity(EntityId entityId); void checkTenantEntity(EntityId entityId) throws TbNodeException;
boolean isLocalEntity(EntityId entityId); boolean isLocalEntity(EntityId entityId);
@ -302,6 +304,8 @@ public interface TbContext {
SlackService getSlackService(); SlackService getSlackService();
boolean isExternalNodeForceAck();
/** /**
* Creates JS Script Engine * Creates JS Script Engine
* @deprecated * @deprecated

18
rule-engine/rule-engine-api/src/main/java/org/thingsboard/rule/engine/api/TbNodeException.java

@ -15,17 +15,33 @@
*/ */
package org.thingsboard.rule.engine.api; package org.thingsboard.rule.engine.api;
import lombok.Getter;
import org.thingsboard.server.common.msg.TbActorError;
/** /**
* Created by ashvayka on 19.01.18. * Created by ashvayka on 19.01.18.
*/ */
public class TbNodeException extends Exception { public class TbNodeException extends Exception implements TbActorError {
@Getter
private final boolean unrecoverable;
public TbNodeException(String message) { public TbNodeException(String message) {
this(message, false);
}
public TbNodeException(String message, boolean unrecoverable) {
super(message); super(message);
this.unrecoverable = unrecoverable;
} }
public TbNodeException(Exception e) { public TbNodeException(Exception e) {
this(e, false);
}
public TbNodeException(Exception e, boolean unrecoverable) {
super(e); super(e);
this.unrecoverable = unrecoverable;
} }
} }

2
rule-engine/rule-engine-api/src/main/java/org/thingsboard/rule/engine/api/util/TbNodeUtils.java

@ -44,7 +44,7 @@ public class TbNodeUtils {
try { try {
return JacksonUtil.treeToValue(configuration.getData(), clazz); return JacksonUtil.treeToValue(configuration.getData(), clazz);
} catch (IllegalArgumentException e) { } catch (IllegalArgumentException e) {
throw new TbNodeException(e); throw new TbNodeException(e, true);
} }
} }

2
rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/action/TbCreateAlarmNode.java

@ -66,7 +66,7 @@ public class TbCreateAlarmNode extends TbAbstractAlarmNode<TbCreateAlarmNodeConf
if (!this.config.isDynamicSeverity()) { if (!this.config.isDynamicSeverity()) {
this.notDynamicAlarmSeverity = EnumUtils.getEnum(AlarmSeverity.class, this.config.getSeverity()); this.notDynamicAlarmSeverity = EnumUtils.getEnum(AlarmSeverity.class, this.config.getSeverity());
if (this.notDynamicAlarmSeverity == null) { if (this.notDynamicAlarmSeverity == null) {
throw new TbNodeException("Incorrect Alarm Severity value: " + this.config.getSeverity()); throw new TbNodeException("Incorrect Alarm Severity value: " + this.config.getSeverity(), true);
} }
} }
} }

10
rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/aws/sns/TbSnsNode.java

@ -26,10 +26,10 @@ import com.google.common.util.concurrent.ListenableFuture;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
import org.thingsboard.rule.engine.api.RuleNode; import org.thingsboard.rule.engine.api.RuleNode;
import org.thingsboard.rule.engine.api.TbContext; import org.thingsboard.rule.engine.api.TbContext;
import org.thingsboard.rule.engine.api.TbNode;
import org.thingsboard.rule.engine.api.TbNodeConfiguration; import org.thingsboard.rule.engine.api.TbNodeConfiguration;
import org.thingsboard.rule.engine.api.TbNodeException; import org.thingsboard.rule.engine.api.TbNodeException;
import org.thingsboard.rule.engine.api.util.TbNodeUtils; import org.thingsboard.rule.engine.api.util.TbNodeUtils;
import org.thingsboard.rule.engine.external.TbAbstractExternalNode;
import org.thingsboard.server.common.data.plugin.ComponentType; import org.thingsboard.server.common.data.plugin.ComponentType;
import org.thingsboard.server.common.msg.TbMsg; import org.thingsboard.server.common.msg.TbMsg;
import org.thingsboard.server.common.msg.TbMsgMetaData; import org.thingsboard.server.common.msg.TbMsgMetaData;
@ -51,7 +51,7 @@ import static org.thingsboard.common.util.DonAsynchron.withCallback;
configDirective = "tbExternalNodeSnsConfig", configDirective = "tbExternalNodeSnsConfig",
iconUrl = "data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAyNCAyNCIgd2lkdGg9IjQ4IiBoZWlnaHQ9IjQ4Ij48cGF0aCBkPSJNMTMuMjMgMTAuNTZWMTBjLTEuOTQgMC0zLjk5LjM5LTMuOTkgMi42NyAwIDEuMTYuNjEgMS45NSAxLjYzIDEuOTUuNzYgMCAxLjQzLS40NyAxLjg2LTEuMjIuNTItLjkzLjUtMS44LjUtMi44NG0yLjcgNi41M2MtLjE4LjE2LS40My4xNy0uNjMuMDYtLjg5LS43NC0xLjA1LTEuMDgtMS41NC0xLjc5LTEuNDcgMS41LTIuNTEgMS45NS00LjQyIDEuOTUtMi4yNSAwLTQuMDEtMS4zOS00LjAxLTQuMTcgMC0yLjE4IDEuMTctMy42NCAyLjg2LTQuMzggMS40Ni0uNjQgMy40OS0uNzYgNS4wNC0uOTNWNy41YzAtLjY2LjA1LTEuNDEtLjMzLTEuOTYtLjMyLS40OS0uOTUtLjctMS41LS43LTEuMDIgMC0xLjkzLjUzLTIuMTUgMS42MS0uMDUuMjQtLjI1LjQ4LS40Ny40OWwtMi42LS4yOGMtLjIyLS4wNS0uNDYtLjIyLS40LS41Ni42LTMuMTUgMy40NS00LjEgNi00LjEgMS4zIDAgMyAuMzUgNC4wMyAxLjMzQzE3LjExIDQuNTUgMTcgNi4xOCAxNyA3Ljk1djQuMTdjMCAxLjI1LjUgMS44MSAxIDIuNDguMTcuMjUuMjEuNTQgMCAuNzFsLTIuMDYgMS43OGgtLjAxIj48L3BhdGg+PHBhdGggZD0iTTIwLjE2IDE5LjU0QzE4IDIxLjE0IDE0LjgyIDIyIDEyLjEgMjJjLTMuODEgMC03LjI1LTEuNDEtOS44NS0zLjc2LS4yLS4xOC0uMDItLjQzLjI1LS4yOSAyLjc4IDEuNjMgNi4yNSAyLjYxIDkuODMgMi42MSAyLjQxIDAgNS4wNy0uNSA3LjUxLTEuNTMuMzctLjE2LjY2LjI0LjMyLjUxIj48L3BhdGg+PHBhdGggZD0iTTIxLjA3IDE4LjVjLS4yOC0uMzYtMS44NS0uMTctMi41Ny0uMDgtLjE5LjAyLS4yMi0uMTYtLjAzLS4zIDEuMjQtLjg4IDMuMjktLjYyIDMuNTMtLjMzLjI0LjMtLjA3IDIuMzUtMS4yNCAzLjMyLS4xOC4xNi0uMzUuMDctLjI2LS4xMS4yNi0uNjcuODUtMi4xNC41Ny0yLjV6Ij48L3BhdGg+PC9zdmc+" iconUrl = "data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAyNCAyNCIgd2lkdGg9IjQ4IiBoZWlnaHQ9IjQ4Ij48cGF0aCBkPSJNMTMuMjMgMTAuNTZWMTBjLTEuOTQgMC0zLjk5LjM5LTMuOTkgMi42NyAwIDEuMTYuNjEgMS45NSAxLjYzIDEuOTUuNzYgMCAxLjQzLS40NyAxLjg2LTEuMjIuNTItLjkzLjUtMS44LjUtMi44NG0yLjcgNi41M2MtLjE4LjE2LS40My4xNy0uNjMuMDYtLjg5LS43NC0xLjA1LTEuMDgtMS41NC0xLjc5LTEuNDcgMS41LTIuNTEgMS45NS00LjQyIDEuOTUtMi4yNSAwLTQuMDEtMS4zOS00LjAxLTQuMTcgMC0yLjE4IDEuMTctMy42NCAyLjg2LTQuMzggMS40Ni0uNjQgMy40OS0uNzYgNS4wNC0uOTNWNy41YzAtLjY2LjA1LTEuNDEtLjMzLTEuOTYtLjMyLS40OS0uOTUtLjctMS41LS43LTEuMDIgMC0xLjkzLjUzLTIuMTUgMS42MS0uMDUuMjQtLjI1LjQ4LS40Ny40OWwtMi42LS4yOGMtLjIyLS4wNS0uNDYtLjIyLS40LS41Ni42LTMuMTUgMy40NS00LjEgNi00LjEgMS4zIDAgMyAuMzUgNC4wMyAxLjMzQzE3LjExIDQuNTUgMTcgNi4xOCAxNyA3Ljk1djQuMTdjMCAxLjI1LjUgMS44MSAxIDIuNDguMTcuMjUuMjEuNTQgMCAuNzFsLTIuMDYgMS43OGgtLjAxIj48L3BhdGg+PHBhdGggZD0iTTIwLjE2IDE5LjU0QzE4IDIxLjE0IDE0LjgyIDIyIDEyLjEgMjJjLTMuODEgMC03LjI1LTEuNDEtOS44NS0zLjc2LS4yLS4xOC0uMDItLjQzLjI1LS4yOSAyLjc4IDEuNjMgNi4yNSAyLjYxIDkuODMgMi42MSAyLjQxIDAgNS4wNy0uNSA3LjUxLTEuNTMuMzctLjE2LjY2LjI0LjMyLjUxIj48L3BhdGg+PHBhdGggZD0iTTIxLjA3IDE4LjVjLS4yOC0uMzYtMS44NS0uMTctMi41Ny0uMDgtLjE5LjAyLS4yMi0uMTYtLjAzLS4zIDEuMjQtLjg4IDMuMjktLjYyIDMuNTMtLjMzLjI0LjMtLjA3IDIuMzUtMS4yNCAzLjMyLS4xOC4xNi0uMzUuMDctLjI2LS4xMS4yNi0uNjcuODUtMi4xNC41Ny0yLjV6Ij48L3BhdGg+PC9zdmc+"
) )
public class TbSnsNode implements TbNode { public class TbSnsNode extends TbAbstractExternalNode {
private static final String MESSAGE_ID = "messageId"; private static final String MESSAGE_ID = "messageId";
private static final String REQUEST_ID = "requestId"; private static final String REQUEST_ID = "requestId";
@ -62,6 +62,7 @@ public class TbSnsNode implements TbNode {
@Override @Override
public void init(TbContext ctx, TbNodeConfiguration configuration) throws TbNodeException { public void init(TbContext ctx, TbNodeConfiguration configuration) throws TbNodeException {
super.init(ctx);
this.config = TbNodeUtils.convert(configuration, TbSnsNodeConfiguration.class); this.config = TbNodeUtils.convert(configuration, TbSnsNodeConfiguration.class);
AWSCredentials awsCredentials = new BasicAWSCredentials(this.config.getAccessKeyId(), this.config.getSecretAccessKey()); AWSCredentials awsCredentials = new BasicAWSCredentials(this.config.getAccessKeyId(), this.config.getSecretAccessKey());
AWSStaticCredentialsProvider credProvider = new AWSStaticCredentialsProvider(awsCredentials); AWSStaticCredentialsProvider credProvider = new AWSStaticCredentialsProvider(awsCredentials);
@ -78,8 +79,9 @@ public class TbSnsNode implements TbNode {
@Override @Override
public void onMsg(TbContext ctx, TbMsg msg) throws ExecutionException, InterruptedException, TbNodeException { public void onMsg(TbContext ctx, TbMsg msg) throws ExecutionException, InterruptedException, TbNodeException {
withCallback(publishMessageAsync(ctx, msg), withCallback(publishMessageAsync(ctx, msg),
ctx::tellSuccess, m -> tellSuccess(ctx, m),
t -> ctx.tellFailure(processException(ctx, msg, t), t)); t -> tellFailure(ctx, processException(ctx, msg, t), t));
ackIfNeeded(ctx, msg);
} }
private ListenableFuture<TbMsg> publishMessageAsync(TbContext ctx, TbMsg msg) { private ListenableFuture<TbMsg> publishMessageAsync(TbContext ctx, TbMsg msg) {

9
rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/aws/sqs/TbSqsNode.java

@ -31,6 +31,7 @@ import org.thingsboard.rule.engine.api.TbNode;
import org.thingsboard.rule.engine.api.TbNodeConfiguration; import org.thingsboard.rule.engine.api.TbNodeConfiguration;
import org.thingsboard.rule.engine.api.TbNodeException; import org.thingsboard.rule.engine.api.TbNodeException;
import org.thingsboard.rule.engine.api.util.TbNodeUtils; import org.thingsboard.rule.engine.api.util.TbNodeUtils;
import org.thingsboard.rule.engine.external.TbAbstractExternalNode;
import org.thingsboard.server.common.data.StringUtils; import org.thingsboard.server.common.data.StringUtils;
import org.thingsboard.server.common.data.plugin.ComponentType; import org.thingsboard.server.common.data.plugin.ComponentType;
import org.thingsboard.server.common.msg.TbMsg; import org.thingsboard.server.common.msg.TbMsg;
@ -55,7 +56,7 @@ import static org.thingsboard.common.util.DonAsynchron.withCallback;
configDirective = "tbExternalNodeSqsConfig", configDirective = "tbExternalNodeSqsConfig",
iconUrl = "data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAyNCAyNCIgd2lkdGg9IjQ4IiBoZWlnaHQ9IjQ4Ij48cGF0aCBkPSJNMTMuMjMgMTAuNTZWMTBjLTEuOTQgMC0zLjk5LjM5LTMuOTkgMi42NyAwIDEuMTYuNjEgMS45NSAxLjYzIDEuOTUuNzYgMCAxLjQzLS40NyAxLjg2LTEuMjIuNTItLjkzLjUtMS44LjUtMi44NG0yLjcgNi41M2MtLjE4LjE2LS40My4xNy0uNjMuMDYtLjg5LS43NC0xLjA1LTEuMDgtMS41NC0xLjc5LTEuNDcgMS41LTIuNTEgMS45NS00LjQyIDEuOTUtMi4yNSAwLTQuMDEtMS4zOS00LjAxLTQuMTcgMC0yLjE4IDEuMTctMy42NCAyLjg2LTQuMzggMS40Ni0uNjQgMy40OS0uNzYgNS4wNC0uOTNWNy41YzAtLjY2LjA1LTEuNDEtLjMzLTEuOTYtLjMyLS40OS0uOTUtLjctMS41LS43LTEuMDIgMC0xLjkzLjUzLTIuMTUgMS42MS0uMDUuMjQtLjI1LjQ4LS40Ny40OWwtMi42LS4yOGMtLjIyLS4wNS0uNDYtLjIyLS40LS41Ni42LTMuMTUgMy40NS00LjEgNi00LjEgMS4zIDAgMyAuMzUgNC4wMyAxLjMzQzE3LjExIDQuNTUgMTcgNi4xOCAxNyA3Ljk1djQuMTdjMCAxLjI1LjUgMS44MSAxIDIuNDguMTcuMjUuMjEuNTQgMCAuNzFsLTIuMDYgMS43OGgtLjAxIj48L3BhdGg+PHBhdGggZD0iTTIwLjE2IDE5LjU0QzE4IDIxLjE0IDE0LjgyIDIyIDEyLjEgMjJjLTMuODEgMC03LjI1LTEuNDEtOS44NS0zLjc2LS4yLS4xOC0uMDItLjQzLjI1LS4yOSAyLjc4IDEuNjMgNi4yNSAyLjYxIDkuODMgMi42MSAyLjQxIDAgNS4wNy0uNSA3LjUxLTEuNTMuMzctLjE2LjY2LjI0LjMyLjUxIj48L3BhdGg+PHBhdGggZD0iTTIxLjA3IDE4LjVjLS4yOC0uMzYtMS44NS0uMTctMi41Ny0uMDgtLjE5LjAyLS4yMi0uMTYtLjAzLS4zIDEuMjQtLjg4IDMuMjktLjYyIDMuNTMtLjMzLjI0LjMtLjA3IDIuMzUtMS4yNCAzLjMyLS4xOC4xNi0uMzUuMDctLjI2LS4xMS4yNi0uNjcuODUtMi4xNC41Ny0yLjV6Ij48L3BhdGg+PC9zdmc+" iconUrl = "data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAyNCAyNCIgd2lkdGg9IjQ4IiBoZWlnaHQ9IjQ4Ij48cGF0aCBkPSJNMTMuMjMgMTAuNTZWMTBjLTEuOTQgMC0zLjk5LjM5LTMuOTkgMi42NyAwIDEuMTYuNjEgMS45NSAxLjYzIDEuOTUuNzYgMCAxLjQzLS40NyAxLjg2LTEuMjIuNTItLjkzLjUtMS44LjUtMi44NG0yLjcgNi41M2MtLjE4LjE2LS40My4xNy0uNjMuMDYtLjg5LS43NC0xLjA1LTEuMDgtMS41NC0xLjc5LTEuNDcgMS41LTIuNTEgMS45NS00LjQyIDEuOTUtMi4yNSAwLTQuMDEtMS4zOS00LjAxLTQuMTcgMC0yLjE4IDEuMTctMy42NCAyLjg2LTQuMzggMS40Ni0uNjQgMy40OS0uNzYgNS4wNC0uOTNWNy41YzAtLjY2LjA1LTEuNDEtLjMzLTEuOTYtLjMyLS40OS0uOTUtLjctMS41LS43LTEuMDIgMC0xLjkzLjUzLTIuMTUgMS42MS0uMDUuMjQtLjI1LjQ4LS40Ny40OWwtMi42LS4yOGMtLjIyLS4wNS0uNDYtLjIyLS40LS41Ni42LTMuMTUgMy40NS00LjEgNi00LjEgMS4zIDAgMyAuMzUgNC4wMyAxLjMzQzE3LjExIDQuNTUgMTcgNi4xOCAxNyA3Ljk1djQuMTdjMCAxLjI1LjUgMS44MSAxIDIuNDguMTcuMjUuMjEuNTQgMCAuNzFsLTIuMDYgMS43OGgtLjAxIj48L3BhdGg+PHBhdGggZD0iTTIwLjE2IDE5LjU0QzE4IDIxLjE0IDE0LjgyIDIyIDEyLjEgMjJjLTMuODEgMC03LjI1LTEuNDEtOS44NS0zLjc2LS4yLS4xOC0uMDItLjQzLjI1LS4yOSAyLjc4IDEuNjMgNi4yNSAyLjYxIDkuODMgMi42MSAyLjQxIDAgNS4wNy0uNSA3LjUxLTEuNTMuMzctLjE2LjY2LjI0LjMyLjUxIj48L3BhdGg+PHBhdGggZD0iTTIxLjA3IDE4LjVjLS4yOC0uMzYtMS44NS0uMTctMi41Ny0uMDgtLjE5LjAyLS4yMi0uMTYtLjAzLS4zIDEuMjQtLjg4IDMuMjktLjYyIDMuNTMtLjMzLjI0LjMtLjA3IDIuMzUtMS4yNCAzLjMyLS4xOC4xNi0uMzUuMDctLjI2LS4xMS4yNi0uNjcuODUtMi4xNC41Ny0yLjV6Ij48L3BhdGg+PC9zdmc+"
) )
public class TbSqsNode implements TbNode { public class TbSqsNode extends TbAbstractExternalNode {
private static final String MESSAGE_ID = "messageId"; private static final String MESSAGE_ID = "messageId";
private static final String REQUEST_ID = "requestId"; private static final String REQUEST_ID = "requestId";
@ -69,6 +70,7 @@ public class TbSqsNode implements TbNode {
@Override @Override
public void init(TbContext ctx, TbNodeConfiguration configuration) throws TbNodeException { public void init(TbContext ctx, TbNodeConfiguration configuration) throws TbNodeException {
super.init(ctx);
this.config = TbNodeUtils.convert(configuration, TbSqsNodeConfiguration.class); this.config = TbNodeUtils.convert(configuration, TbSqsNodeConfiguration.class);
AWSCredentials awsCredentials = new BasicAWSCredentials(this.config.getAccessKeyId(), this.config.getSecretAccessKey()); AWSCredentials awsCredentials = new BasicAWSCredentials(this.config.getAccessKeyId(), this.config.getSecretAccessKey());
AWSStaticCredentialsProvider credProvider = new AWSStaticCredentialsProvider(awsCredentials); AWSStaticCredentialsProvider credProvider = new AWSStaticCredentialsProvider(awsCredentials);
@ -85,8 +87,9 @@ public class TbSqsNode implements TbNode {
@Override @Override
public void onMsg(TbContext ctx, TbMsg msg) { public void onMsg(TbContext ctx, TbMsg msg) {
withCallback(publishMessageAsync(ctx, msg), withCallback(publishMessageAsync(ctx, msg),
ctx::tellSuccess, m -> tellSuccess(ctx, m),
t -> ctx.tellFailure(processException(ctx, msg, t), t)); t -> tellFailure(ctx, processException(ctx, msg, t), t));
ackIfNeeded(ctx, msg);
} }
private ListenableFuture<TbMsg> publishMessageAsync(TbContext ctx, TbMsg msg) { private ListenableFuture<TbMsg> publishMessageAsync(TbContext ctx, TbMsg msg) {

61
rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/external/TbAbstractExternalNode.java

@ -0,0 +1,61 @@
/**
* Copyright © 2016-2023 The Thingsboard Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.thingsboard.rule.engine.external;
import org.thingsboard.rule.engine.api.TbContext;
import org.thingsboard.rule.engine.api.TbNode;
import org.thingsboard.server.common.data.msg.TbNodeConnectionType;
import org.thingsboard.server.common.msg.TbMsg;
public abstract class TbAbstractExternalNode implements TbNode {
private boolean forceAck;
public void init(TbContext ctx) {
this.forceAck = ctx.isExternalNodeForceAck();
}
protected void tellSuccess(TbContext ctx, TbMsg tbMsg) {
if (forceAck) {
ctx.enqueueForTellNext(tbMsg.copyWithNewCtx(), TbNodeConnectionType.SUCCESS);
} else {
ctx.tellSuccess(tbMsg);
}
}
protected void tellFailure(TbContext ctx, TbMsg tbMsg, Throwable t) {
if (forceAck) {
if (t == null) {
ctx.enqueueForTellNext(tbMsg.copyWithNewCtx(), TbNodeConnectionType.FAILURE);
} else {
ctx.enqueueForTellFailure(tbMsg.copyWithNewCtx(), t);
}
} else {
if (t == null) {
ctx.tellNext(tbMsg, TbNodeConnectionType.FAILURE);
} else {
ctx.tellFailure(tbMsg, t);
}
}
}
protected void ackIfNeeded(TbContext ctx, TbMsg msg) {
if (forceAck) {
ctx.ack(msg);
}
}
}

11
rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/gcp/pubsub/TbPubSubNode.java

@ -32,6 +32,7 @@ import org.thingsboard.rule.engine.api.TbNode;
import org.thingsboard.rule.engine.api.TbNodeConfiguration; import org.thingsboard.rule.engine.api.TbNodeConfiguration;
import org.thingsboard.rule.engine.api.TbNodeException; import org.thingsboard.rule.engine.api.TbNodeException;
import org.thingsboard.rule.engine.api.util.TbNodeUtils; import org.thingsboard.rule.engine.api.util.TbNodeUtils;
import org.thingsboard.rule.engine.external.TbAbstractExternalNode;
import org.thingsboard.server.common.data.plugin.ComponentType; import org.thingsboard.server.common.data.plugin.ComponentType;
import org.thingsboard.server.common.msg.TbMsg; import org.thingsboard.server.common.msg.TbMsg;
import org.thingsboard.server.common.msg.TbMsgMetaData; import org.thingsboard.server.common.msg.TbMsgMetaData;
@ -53,7 +54,7 @@ import java.util.concurrent.TimeUnit;
configDirective = "tbExternalNodePubSubConfig", configDirective = "tbExternalNodePubSubConfig",
iconUrl = "data:image/svg+xml;base64,PHN2ZyBpZD0iTGF5ZXJfMSIgZGF0YS1uYW1lPSJMYXllciAxIiB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSIxMjgiIGhlaWdodD0iMTI4IiB2aWV3Qm94PSIwIDAgMTI4IDEyOCI+Cjx0aXRsZT5DbG91ZCBQdWJTdWI8L3RpdGxlPgo8Zz4KPHBhdGggZD0iTTEyNi40Nyw1OC4xMmwtMjYuMy00NS43NEExMS41NiwxMS41NiwwLDAsMCw5MC4zMSw2LjVIMzcuN2ExMS41NSwxMS41NSwwLDAsMC05Ljg2LDUuODhMMS41Myw1OGExMS40OCwxMS40OCwwLDAsMCwwLDExLjQ0bDI2LjMsNDZhMTEuNzcsMTEuNzcsMCwwLDAsOS44Niw2LjA5SDkwLjNhMTEuNzMsMTEuNzMsMCwwLDAsOS44Ny02LjA2bDI2LjMtNDUuNzRBMTEuNzMsMTEuNzMsMCwwLDAsMTI2LjQ3LDU4LjEyWiIgc3R5bGU9ImZpbGw6ICM3MzViMmYiLz4KPHBhdGggZD0iTTg5LjIyLDQ3Ljc0LDgzLjM2LDQ5bC0xNC42LTE0LjZMNjQuMDksNDMuMSw2MS41NSw1My4ybDQuMjksNC4yOUw1Ny42LDU5LjE4LDQ2LjMsNDcuODhsLTcuNjcsNy4zOEw1Mi43Niw2OS4zN2wtMTUsMTEuOUw3OCwxMjEuNUg5MC4zYTExLjczLDExLjczLDAsMCwwLDkuODctNi4wNmwyMC43Mi0zNloiIHN0eWxlPSJvcGFjaXR5OiAwLjA3MDAwMDAwMDI5ODAyMztpc29sYXRpb246IGlzb2xhdGUiLz4KPHBhdGggZD0iTTgyLjg2LDQ3YTUuMzIsNS4zMiwwLDEsMS0xLjk1LDcuMjdBNS4zMiw1LjMyLDAsMCwxLDgyLjg2LDQ3IiBzdHlsZT0iZmlsbDogI2ZmZiIvPgo8cGF0aCBkPSJNMzkuODIsNTYuMThhNS4zMiw1LjMyLDAsMSwxLDcuMjctMS45NSw1LjMyLDUuMzIsMCwwLDEtNy4yNywxLjk1IiBzdHlsZT0iZmlsbDogI2ZmZiIvPgo8cGF0aCBkPSJNNjkuMzIsODguODVBNS4zMiw1LjMyLDAsMSwxLDY0LDgzLjUyYTUuMzIsNS4zMiwwLDAsMSw1LjMyLDUuMzIiIHN0eWxlPSJmaWxsOiAjZmZmIi8+CjxnPgo8cGF0aCBkPSJNNjQsNTIuOTRhMTEuMDYsMTEuMDYsMCwwLDEsMi40Ni4yOFYzOS4xNUg2MS41NFY1My4yMkExMS4wNiwxMS4wNiwwLDAsMSw2NCw1Mi45NFoiIHN0eWxlPSJmaWxsOiAjZmZmIi8+CjxwYXRoIGQ9Ik03NC41Nyw2Ny4yNmExMSwxMSwwLDAsMS0yLjQ3LDQuMjVsMTIuMTksNywyLjQ2LTQuMjZaIiBzdHlsZT0iZmlsbDogI2ZmZiIvPgo8cGF0aCBkPSJNNTMuNDMsNjcuMjZsLTEyLjE4LDcsMi40Niw0LjI2LDEyLjE5LTdBMTEsMTEsMCwwLDEsNTMuNDMsNjcuMjZaIiBzdHlsZT0iZmlsbDogI2ZmZiIvPgo8L2c+CjxwYXRoIGQ9Ik03Mi42LDY0QTguNiw4LjYsMCwxLDEsNjQsNTUuNCw4LjYsOC42LDAsMCwxLDcyLjYsNjQiIHN0eWxlPSJmaWxsOiAjZmZmIi8+CjxwYXRoIGQ9Ik0zOS4xLDcwLjU3YTYuNzYsNi43NiwwLDEsMS0yLjQ3LDkuMjMsNi43Niw2Ljc2LDAsMCwxLDIuNDctOS4yMyIgc3R5bGU9ImZpbGw6ICNmZmYiLz4KPHBhdGggZD0iTTgyLjE0LDgyLjI3YTYuNzYsNi43NiwwLDEsMSw5LjIzLTIuNDcsNi43NSw2Ljc1LDAsMCwxLTkuMjMsMi40NyIgc3R5bGU9ImZpbGw6ICNmZmYiLz4KPHBhdGggZD0iTTcwLjc2LDM5LjE1QTYuNzYsNi43NiwwLDEsMSw2NCwzMi4zOWE2Ljc2LDYuNzYsMCwwLDEsNi43Niw2Ljc2IiBzdHlsZT0iZmlsbDogI2ZmZiIvPgo8L2c+Cjwvc3ZnPgo=" iconUrl = "data:image/svg+xml;base64,PHN2ZyBpZD0iTGF5ZXJfMSIgZGF0YS1uYW1lPSJMYXllciAxIiB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSIxMjgiIGhlaWdodD0iMTI4IiB2aWV3Qm94PSIwIDAgMTI4IDEyOCI+Cjx0aXRsZT5DbG91ZCBQdWJTdWI8L3RpdGxlPgo8Zz4KPHBhdGggZD0iTTEyNi40Nyw1OC4xMmwtMjYuMy00NS43NEExMS41NiwxMS41NiwwLDAsMCw5MC4zMSw2LjVIMzcuN2ExMS41NSwxMS41NSwwLDAsMC05Ljg2LDUuODhMMS41Myw1OGExMS40OCwxMS40OCwwLDAsMCwwLDExLjQ0bDI2LjMsNDZhMTEuNzcsMTEuNzcsMCwwLDAsOS44Niw2LjA5SDkwLjNhMTEuNzMsMTEuNzMsMCwwLDAsOS44Ny02LjA2bDI2LjMtNDUuNzRBMTEuNzMsMTEuNzMsMCwwLDAsMTI2LjQ3LDU4LjEyWiIgc3R5bGU9ImZpbGw6ICM3MzViMmYiLz4KPHBhdGggZD0iTTg5LjIyLDQ3Ljc0LDgzLjM2LDQ5bC0xNC42LTE0LjZMNjQuMDksNDMuMSw2MS41NSw1My4ybDQuMjksNC4yOUw1Ny42LDU5LjE4LDQ2LjMsNDcuODhsLTcuNjcsNy4zOEw1Mi43Niw2OS4zN2wtMTUsMTEuOUw3OCwxMjEuNUg5MC4zYTExLjczLDExLjczLDAsMCwwLDkuODctNi4wNmwyMC43Mi0zNloiIHN0eWxlPSJvcGFjaXR5OiAwLjA3MDAwMDAwMDI5ODAyMztpc29sYXRpb246IGlzb2xhdGUiLz4KPHBhdGggZD0iTTgyLjg2LDQ3YTUuMzIsNS4zMiwwLDEsMS0xLjk1LDcuMjdBNS4zMiw1LjMyLDAsMCwxLDgyLjg2LDQ3IiBzdHlsZT0iZmlsbDogI2ZmZiIvPgo8cGF0aCBkPSJNMzkuODIsNTYuMThhNS4zMiw1LjMyLDAsMSwxLDcuMjctMS45NSw1LjMyLDUuMzIsMCwwLDEtNy4yNywxLjk1IiBzdHlsZT0iZmlsbDogI2ZmZiIvPgo8cGF0aCBkPSJNNjkuMzIsODguODVBNS4zMiw1LjMyLDAsMSwxLDY0LDgzLjUyYTUuMzIsNS4zMiwwLDAsMSw1LjMyLDUuMzIiIHN0eWxlPSJmaWxsOiAjZmZmIi8+CjxnPgo8cGF0aCBkPSJNNjQsNTIuOTRhMTEuMDYsMTEuMDYsMCwwLDEsMi40Ni4yOFYzOS4xNUg2MS41NFY1My4yMkExMS4wNiwxMS4wNiwwLDAsMSw2NCw1Mi45NFoiIHN0eWxlPSJmaWxsOiAjZmZmIi8+CjxwYXRoIGQ9Ik03NC41Nyw2Ny4yNmExMSwxMSwwLDAsMS0yLjQ3LDQuMjVsMTIuMTksNywyLjQ2LTQuMjZaIiBzdHlsZT0iZmlsbDogI2ZmZiIvPgo8cGF0aCBkPSJNNTMuNDMsNjcuMjZsLTEyLjE4LDcsMi40Niw0LjI2LDEyLjE5LTdBMTEsMTEsMCwwLDEsNTMuNDMsNjcuMjZaIiBzdHlsZT0iZmlsbDogI2ZmZiIvPgo8L2c+CjxwYXRoIGQ9Ik03Mi42LDY0QTguNiw4LjYsMCwxLDEsNjQsNTUuNCw4LjYsOC42LDAsMCwxLDcyLjYsNjQiIHN0eWxlPSJmaWxsOiAjZmZmIi8+CjxwYXRoIGQ9Ik0zOS4xLDcwLjU3YTYuNzYsNi43NiwwLDEsMS0yLjQ3LDkuMjMsNi43Niw2Ljc2LDAsMCwxLDIuNDctOS4yMyIgc3R5bGU9ImZpbGw6ICNmZmYiLz4KPHBhdGggZD0iTTgyLjE0LDgyLjI3YTYuNzYsNi43NiwwLDEsMSw5LjIzLTIuNDcsNi43NSw2Ljc1LDAsMCwxLTkuMjMsMi40NyIgc3R5bGU9ImZpbGw6ICNmZmYiLz4KPHBhdGggZD0iTTcwLjc2LDM5LjE1QTYuNzYsNi43NiwwLDEsMSw2NCwzMi4zOWE2Ljc2LDYuNzYsMCwwLDEsNi43Niw2Ljc2IiBzdHlsZT0iZmlsbDogI2ZmZiIvPgo8L2c+Cjwvc3ZnPgo="
) )
public class TbPubSubNode implements TbNode { public class TbPubSubNode extends TbAbstractExternalNode {
private static final String MESSAGE_ID = "messageId"; private static final String MESSAGE_ID = "messageId";
private static final String ERROR = "error"; private static final String ERROR = "error";
@ -63,8 +64,9 @@ public class TbPubSubNode implements TbNode {
@Override @Override
public void init(TbContext ctx, TbNodeConfiguration configuration) throws TbNodeException { public void init(TbContext ctx, TbNodeConfiguration configuration) throws TbNodeException {
super.init(ctx);
this.config = TbNodeUtils.convert(configuration, TbPubSubNodeConfiguration.class);
try { try {
this.config = TbNodeUtils.convert(configuration, TbPubSubNodeConfiguration.class);
this.pubSubClient = initPubSubClient(); this.pubSubClient = initPubSubClient();
} catch (Exception e) { } catch (Exception e) {
throw new TbNodeException(e); throw new TbNodeException(e);
@ -74,6 +76,7 @@ public class TbPubSubNode implements TbNode {
@Override @Override
public void onMsg(TbContext ctx, TbMsg msg) { public void onMsg(TbContext ctx, TbMsg msg) {
publishMessage(ctx, msg); publishMessage(ctx, msg);
ackIfNeeded(ctx, msg);
} }
@Override @Override
@ -101,12 +104,12 @@ public class TbPubSubNode implements TbNode {
ApiFutures.addCallback(messageIdFuture, new ApiFutureCallback<String>() { ApiFutures.addCallback(messageIdFuture, new ApiFutureCallback<String>() {
public void onSuccess(String messageId) { public void onSuccess(String messageId) {
TbMsg next = processPublishResult(ctx, msg, messageId); TbMsg next = processPublishResult(ctx, msg, messageId);
ctx.tellSuccess(next); tellSuccess(ctx, next);
} }
public void onFailure(Throwable t) { public void onFailure(Throwable t) {
TbMsg next = processException(ctx, msg, t); TbMsg next = processException(ctx, msg, t);
ctx.tellFailure(next, t); tellFailure(ctx, next, t);
} }
}, },
ctx.getExternalCallExecutor()); ctx.getExternalCallExecutor());

13
rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/kafka/TbKafkaNode.java

@ -28,11 +28,10 @@ import org.apache.kafka.common.header.internals.RecordHeaders;
import org.springframework.util.ReflectionUtils; import org.springframework.util.ReflectionUtils;
import org.thingsboard.rule.engine.api.RuleNode; import org.thingsboard.rule.engine.api.RuleNode;
import org.thingsboard.rule.engine.api.TbContext; import org.thingsboard.rule.engine.api.TbContext;
import org.thingsboard.rule.engine.api.TbNode;
import org.thingsboard.rule.engine.api.TbNodeConfiguration; import org.thingsboard.rule.engine.api.TbNodeConfiguration;
import org.thingsboard.rule.engine.api.TbNodeException; import org.thingsboard.rule.engine.api.TbNodeException;
import org.thingsboard.server.common.data.msg.TbNodeConnectionType;
import org.thingsboard.rule.engine.api.util.TbNodeUtils; import org.thingsboard.rule.engine.api.util.TbNodeUtils;
import org.thingsboard.rule.engine.external.TbAbstractExternalNode;
import org.thingsboard.server.common.data.exception.ThingsboardKafkaClientError; import org.thingsboard.server.common.data.exception.ThingsboardKafkaClientError;
import org.thingsboard.server.common.data.plugin.ComponentType; import org.thingsboard.server.common.data.plugin.ComponentType;
import org.thingsboard.server.common.msg.TbMsg; import org.thingsboard.server.common.msg.TbMsg;
@ -56,7 +55,7 @@ import java.util.Properties;
configDirective = "tbExternalNodeKafkaConfig", configDirective = "tbExternalNodeKafkaConfig",
iconUrl = "data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMTUzOCIgaGVpZ2h0PSIyNTAwIiB2aWV3Qm94PSIwIDAgMjU2IDQxNiIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIiBwcmVzZXJ2ZUFzcGVjdFJhdGlvPSJ4TWlkWU1pZCI+PHBhdGggZD0iTTIwMS44MTYgMjMwLjIxNmMtMTYuMTg2IDAtMzAuNjk3IDcuMTcxLTQwLjYzNCAxOC40NjFsLTI1LjQ2My0xOC4wMjZjMi43MDMtNy40NDIgNC4yNTUtMTUuNDMzIDQuMjU1LTIzLjc5NyAwLTguMjE5LTEuNDk4LTE2LjA3Ni00LjExMi0yMy40MDhsMjUuNDA2LTE3LjgzNWM5LjkzNiAxMS4yMzMgMjQuNDA5IDE4LjM2NSA0MC41NDggMTguMzY1IDI5Ljg3NSAwIDU0LjE4NC0yNC4zMDUgNTQuMTg0LTU0LjE4NCAwLTI5Ljg3OS0yNC4zMDktNTQuMTg0LTU0LjE4NC01NC4xODQtMjkuODc1IDAtNTQuMTg0IDI0LjMwNS01NC4xODQgNTQuMTg0IDAgNS4zNDguODA4IDEwLjUwNSAyLjI1OCAxNS4zODlsLTI1LjQyMyAxNy44NDRjLTEwLjYyLTEzLjE3NS0yNS45MTEtMjIuMzc0LTQzLjMzMy0yNS4xODJ2LTMwLjY0YzI0LjU0NC01LjE1NSA0My4wMzctMjYuOTYyIDQzLjAzNy01My4wMTlDMTI0LjE3MSAyNC4zMDUgOTkuODYyIDAgNjkuOTg3IDAgNDAuMTEyIDAgMTUuODAzIDI0LjMwNSAxNS44MDMgNTQuMTg0YzAgMjUuNzA4IDE4LjAxNCA0Ny4yNDYgNDIuMDY3IDUyLjc2OXYzMS4wMzhDMjUuMDQ0IDE0My43NTMgMCAxNzIuNDAxIDAgMjA2Ljg1NGMwIDM0LjYyMSAyNS4yOTIgNjMuMzc0IDU4LjM1NSA2OC45NHYzMi43NzRjLTI0LjI5OSA1LjM0MS00Mi41NTIgMjcuMDExLTQyLjU1MiA1Mi44OTQgMCAyOS44NzkgMjQuMzA5IDU0LjE4NCA1NC4xODQgNTQuMTg0IDI5Ljg3NSAwIDU0LjE4NC0yNC4zMDUgNTQuMTg0LTU0LjE4NCAwLTI1Ljg4My0xOC4yNTMtNDcuNTUzLTQyLjU1Mi01Mi44OTR2LTMyLjc3NWE2OS45NjUgNjkuOTY1IDAgMCAwIDQyLjYtMjQuNzc2bDI1LjYzMyAxOC4xNDNjLTEuNDIzIDQuODQtMi4yMiA5Ljk0Ni0yLjIyIDE1LjI0IDAgMjkuODc5IDI0LjMwOSA1NC4xODQgNTQuMTg0IDU0LjE4NCAyOS44NzUgMCA1NC4xODQtMjQuMzA1IDU0LjE4NC01NC4xODQgMC0yOS44NzktMjQuMzA5LTU0LjE4NC01NC4xODQtNTQuMTg0em0wLTEyNi42OTVjMTQuNDg3IDAgMjYuMjcgMTEuNzg4IDI2LjI3IDI2LjI3MXMtMTEuNzgzIDI2LjI3LTI2LjI3IDI2LjI3LTI2LjI3LTExLjc4Ny0yNi4yNy0yNi4yN2MwLTE0LjQ4MyAxMS43ODMtMjYuMjcxIDI2LjI3LTI2LjI3MXptLTE1OC4xLTQ5LjMzN2MwLTE0LjQ4MyAxMS43ODQtMjYuMjcgMjYuMjcxLTI2LjI3czI2LjI3IDExLjc4NyAyNi4yNyAyNi4yN2MwIDE0LjQ4My0xMS43ODMgMjYuMjctMjYuMjcgMjYuMjdzLTI2LjI3MS0xMS43ODctMjYuMjcxLTI2LjI3em01Mi41NDEgMzA3LjI3OGMwIDE0LjQ4My0xMS43ODMgMjYuMjctMjYuMjcgMjYuMjdzLTI2LjI3MS0xMS43ODctMjYuMjcxLTI2LjI3YzAtMTQuNDgzIDExLjc4NC0yNi4yNyAyNi4yNzEtMjYuMjdzMjYuMjcgMTEuNzg3IDI2LjI3IDI2LjI3em0tMjYuMjcyLTExNy45N2MtMjAuMjA1IDAtMzYuNjQyLTE2LjQzNC0zNi42NDItMzYuNjM4IDAtMjAuMjA1IDE2LjQzNy0zNi42NDIgMzYuNjQyLTM2LjY0MiAyMC4yMDQgMCAzNi42NDEgMTYuNDM3IDM2LjY0MSAzNi42NDIgMCAyMC4yMDQtMTYuNDM3IDM2LjYzOC0zNi42NDEgMzYuNjM4em0xMzEuODMxIDY3LjE3OWMtMTQuNDg3IDAtMjYuMjctMTEuNzg4LTI2LjI3LTI2LjI3MXMxMS43ODMtMjYuMjcgMjYuMjctMjYuMjcgMjYuMjcgMTEuNzg3IDI2LjI3IDI2LjI3YzAgMTQuNDgzLTExLjc4MyAyNi4yNzEtMjYuMjcgMjYuMjcxeiIvPjwvc3ZnPg==" iconUrl = "data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMTUzOCIgaGVpZ2h0PSIyNTAwIiB2aWV3Qm94PSIwIDAgMjU2IDQxNiIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIiBwcmVzZXJ2ZUFzcGVjdFJhdGlvPSJ4TWlkWU1pZCI+PHBhdGggZD0iTTIwMS44MTYgMjMwLjIxNmMtMTYuMTg2IDAtMzAuNjk3IDcuMTcxLTQwLjYzNCAxOC40NjFsLTI1LjQ2My0xOC4wMjZjMi43MDMtNy40NDIgNC4yNTUtMTUuNDMzIDQuMjU1LTIzLjc5NyAwLTguMjE5LTEuNDk4LTE2LjA3Ni00LjExMi0yMy40MDhsMjUuNDA2LTE3LjgzNWM5LjkzNiAxMS4yMzMgMjQuNDA5IDE4LjM2NSA0MC41NDggMTguMzY1IDI5Ljg3NSAwIDU0LjE4NC0yNC4zMDUgNTQuMTg0LTU0LjE4NCAwLTI5Ljg3OS0yNC4zMDktNTQuMTg0LTU0LjE4NC01NC4xODQtMjkuODc1IDAtNTQuMTg0IDI0LjMwNS01NC4xODQgNTQuMTg0IDAgNS4zNDguODA4IDEwLjUwNSAyLjI1OCAxNS4zODlsLTI1LjQyMyAxNy44NDRjLTEwLjYyLTEzLjE3NS0yNS45MTEtMjIuMzc0LTQzLjMzMy0yNS4xODJ2LTMwLjY0YzI0LjU0NC01LjE1NSA0My4wMzctMjYuOTYyIDQzLjAzNy01My4wMTlDMTI0LjE3MSAyNC4zMDUgOTkuODYyIDAgNjkuOTg3IDAgNDAuMTEyIDAgMTUuODAzIDI0LjMwNSAxNS44MDMgNTQuMTg0YzAgMjUuNzA4IDE4LjAxNCA0Ny4yNDYgNDIuMDY3IDUyLjc2OXYzMS4wMzhDMjUuMDQ0IDE0My43NTMgMCAxNzIuNDAxIDAgMjA2Ljg1NGMwIDM0LjYyMSAyNS4yOTIgNjMuMzc0IDU4LjM1NSA2OC45NHYzMi43NzRjLTI0LjI5OSA1LjM0MS00Mi41NTIgMjcuMDExLTQyLjU1MiA1Mi44OTQgMCAyOS44NzkgMjQuMzA5IDU0LjE4NCA1NC4xODQgNTQuMTg0IDI5Ljg3NSAwIDU0LjE4NC0yNC4zMDUgNTQuMTg0LTU0LjE4NCAwLTI1Ljg4My0xOC4yNTMtNDcuNTUzLTQyLjU1Mi01Mi44OTR2LTMyLjc3NWE2OS45NjUgNjkuOTY1IDAgMCAwIDQyLjYtMjQuNzc2bDI1LjYzMyAxOC4xNDNjLTEuNDIzIDQuODQtMi4yMiA5Ljk0Ni0yLjIyIDE1LjI0IDAgMjkuODc5IDI0LjMwOSA1NC4xODQgNTQuMTg0IDU0LjE4NCAyOS44NzUgMCA1NC4xODQtMjQuMzA1IDU0LjE4NC01NC4xODQgMC0yOS44NzktMjQuMzA5LTU0LjE4NC01NC4xODQtNTQuMTg0em0wLTEyNi42OTVjMTQuNDg3IDAgMjYuMjcgMTEuNzg4IDI2LjI3IDI2LjI3MXMtMTEuNzgzIDI2LjI3LTI2LjI3IDI2LjI3LTI2LjI3LTExLjc4Ny0yNi4yNy0yNi4yN2MwLTE0LjQ4MyAxMS43ODMtMjYuMjcxIDI2LjI3LTI2LjI3MXptLTE1OC4xLTQ5LjMzN2MwLTE0LjQ4MyAxMS43ODQtMjYuMjcgMjYuMjcxLTI2LjI3czI2LjI3IDExLjc4NyAyNi4yNyAyNi4yN2MwIDE0LjQ4My0xMS43ODMgMjYuMjctMjYuMjcgMjYuMjdzLTI2LjI3MS0xMS43ODctMjYuMjcxLTI2LjI3em01Mi41NDEgMzA3LjI3OGMwIDE0LjQ4My0xMS43ODMgMjYuMjctMjYuMjcgMjYuMjdzLTI2LjI3MS0xMS43ODctMjYuMjcxLTI2LjI3YzAtMTQuNDgzIDExLjc4NC0yNi4yNyAyNi4yNzEtMjYuMjdzMjYuMjcgMTEuNzg3IDI2LjI3IDI2LjI3em0tMjYuMjcyLTExNy45N2MtMjAuMjA1IDAtMzYuNjQyLTE2LjQzNC0zNi42NDItMzYuNjM4IDAtMjAuMjA1IDE2LjQzNy0zNi42NDIgMzYuNjQyLTM2LjY0MiAyMC4yMDQgMCAzNi42NDEgMTYuNDM3IDM2LjY0MSAzNi42NDIgMCAyMC4yMDQtMTYuNDM3IDM2LjYzOC0zNi42NDEgMzYuNjM4em0xMzEuODMxIDY3LjE3OWMtMTQuNDg3IDAtMjYuMjctMTEuNzg4LTI2LjI3LTI2LjI3MXMxMS43ODMtMjYuMjcgMjYuMjctMjYuMjcgMjYuMjcgMTEuNzg3IDI2LjI3IDI2LjI3YzAgMTQuNDgzLTExLjc4MyAyNi4yNzEtMjYuMjcgMjYuMjcxeiIvPjwvc3ZnPg=="
) )
public class TbKafkaNode implements TbNode { public class TbKafkaNode extends TbAbstractExternalNode {
private static final String OFFSET = "offset"; private static final String OFFSET = "offset";
private static final String PARTITION = "partition"; private static final String PARTITION = "partition";
@ -78,6 +77,7 @@ public class TbKafkaNode implements TbNode {
@Override @Override
public void init(TbContext ctx, TbNodeConfiguration configuration) throws TbNodeException { public void init(TbContext ctx, TbNodeConfiguration configuration) throws TbNodeException {
super.init(ctx);
this.config = TbNodeUtils.convert(configuration, TbKafkaNodeConfiguration.class); this.config = TbNodeUtils.convert(configuration, TbKafkaNodeConfiguration.class);
this.initError = null; this.initError = null;
Properties properties = new Properties(); Properties properties = new Properties();
@ -129,6 +129,7 @@ public class TbKafkaNode implements TbNode {
return null; return null;
}); });
} }
ackIfNeeded(ctx, msg);
} catch (Exception e) { } catch (Exception e) {
ctx.tellFailure(msg, e); ctx.tellFailure(msg, e);
} }
@ -164,11 +165,9 @@ public class TbKafkaNode implements TbNode {
private void processRecord(TbContext ctx, TbMsg msg, RecordMetadata metadata, Exception e) { private void processRecord(TbContext ctx, TbMsg msg, RecordMetadata metadata, Exception e) {
if (e == null) { if (e == null) {
TbMsg next = processResponse(ctx, msg, metadata); tellSuccess(ctx, processResponse(ctx, msg, metadata));
ctx.tellNext(next, TbNodeConnectionType.SUCCESS);
} else { } else {
TbMsg next = processException(ctx, msg, e); tellFailure(ctx, processException(ctx, msg, e), e);
ctx.tellFailure(next, e);
} }
} }

12
rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/mail/TbSendEmailNode.java

@ -22,10 +22,10 @@ import org.springframework.mail.javamail.JavaMailSenderImpl;
import org.thingsboard.rule.engine.api.RuleNode; import org.thingsboard.rule.engine.api.RuleNode;
import org.thingsboard.rule.engine.api.TbContext; import org.thingsboard.rule.engine.api.TbContext;
import org.thingsboard.rule.engine.api.TbEmail; import org.thingsboard.rule.engine.api.TbEmail;
import org.thingsboard.rule.engine.api.TbNode;
import org.thingsboard.rule.engine.api.TbNodeConfiguration; import org.thingsboard.rule.engine.api.TbNodeConfiguration;
import org.thingsboard.rule.engine.api.TbNodeException; import org.thingsboard.rule.engine.api.TbNodeException;
import org.thingsboard.rule.engine.api.util.TbNodeUtils; import org.thingsboard.rule.engine.api.util.TbNodeUtils;
import org.thingsboard.rule.engine.external.TbAbstractExternalNode;
import org.thingsboard.server.common.data.plugin.ComponentType; import org.thingsboard.server.common.data.plugin.ComponentType;
import org.thingsboard.server.common.msg.TbMsg; import org.thingsboard.server.common.msg.TbMsg;
@ -47,7 +47,7 @@ import static org.thingsboard.common.util.DonAsynchron.withCallback;
configDirective = "tbExternalNodeSendEmailConfig", configDirective = "tbExternalNodeSendEmailConfig",
icon = "send" icon = "send"
) )
public class TbSendEmailNode implements TbNode { public class TbSendEmailNode extends TbAbstractExternalNode {
private static final String MAIL_PROP = "mail."; private static final String MAIL_PROP = "mail.";
static final String SEND_EMAIL_TYPE = "SEND_EMAIL"; static final String SEND_EMAIL_TYPE = "SEND_EMAIL";
@ -56,8 +56,9 @@ public class TbSendEmailNode implements TbNode {
@Override @Override
public void init(TbContext ctx, TbNodeConfiguration configuration) throws TbNodeException { public void init(TbContext ctx, TbNodeConfiguration configuration) throws TbNodeException {
super.init(ctx);
this.config = TbNodeUtils.convert(configuration, TbSendEmailNodeConfiguration.class);
try { try {
this.config = TbNodeUtils.convert(configuration, TbSendEmailNodeConfiguration.class);
if (!this.config.isUseSystemSmtpSettings()) { if (!this.config.isUseSystemSmtpSettings()) {
mailSender = createMailSender(); mailSender = createMailSender();
} }
@ -75,8 +76,9 @@ public class TbSendEmailNode implements TbNode {
sendEmail(ctx, msg, email); sendEmail(ctx, msg, email);
return null; return null;
}), }),
ok -> ctx.tellSuccess(msg), ok -> tellSuccess(ctx, msg),
fail -> ctx.tellFailure(msg, fail)); fail -> tellFailure(ctx, msg, fail));
ackIfNeeded(ctx, msg);
} catch (Exception ex) { } catch (Exception ex) {
ctx.tellFailure(msg, ex); ctx.tellFailure(msg, ex);
} }

12
rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/mqtt/TbMqttNode.java

@ -32,6 +32,7 @@ import org.thingsboard.rule.engine.api.util.TbNodeUtils;
import org.thingsboard.rule.engine.credentials.BasicCredentials; import org.thingsboard.rule.engine.credentials.BasicCredentials;
import org.thingsboard.rule.engine.credentials.ClientCredentials; import org.thingsboard.rule.engine.credentials.ClientCredentials;
import org.thingsboard.rule.engine.credentials.CredentialsType; import org.thingsboard.rule.engine.credentials.CredentialsType;
import org.thingsboard.rule.engine.external.TbAbstractExternalNode;
import org.thingsboard.server.common.data.StringUtils; import org.thingsboard.server.common.data.StringUtils;
import org.thingsboard.server.common.data.plugin.ComponentClusteringMode; import org.thingsboard.server.common.data.plugin.ComponentClusteringMode;
import org.thingsboard.server.common.data.plugin.ComponentType; import org.thingsboard.server.common.data.plugin.ComponentType;
@ -55,7 +56,7 @@ import java.util.concurrent.TimeoutException;
configDirective = "tbExternalNodeMqttConfig", configDirective = "tbExternalNodeMqttConfig",
icon = "call_split" icon = "call_split"
) )
public class TbMqttNode implements TbNode { public class TbMqttNode extends TbAbstractExternalNode {
private static final Charset UTF8 = Charset.forName("UTF-8"); private static final Charset UTF8 = Charset.forName("UTF-8");
@ -67,8 +68,9 @@ public class TbMqttNode implements TbNode {
@Override @Override
public void init(TbContext ctx, TbNodeConfiguration configuration) throws TbNodeException { public void init(TbContext ctx, TbNodeConfiguration configuration) throws TbNodeException {
super.init(ctx);
this.mqttNodeConfiguration = TbNodeUtils.convert(configuration, TbMqttNodeConfiguration.class);
try { try {
this.mqttNodeConfiguration = TbNodeUtils.convert(configuration, TbMqttNodeConfiguration.class);
this.mqttClient = initClient(ctx); this.mqttClient = initClient(ctx);
} catch (Exception e) { } catch (Exception e) {
throw new TbNodeException(e); throw new TbNodeException(e);
@ -81,13 +83,13 @@ public class TbMqttNode implements TbNode {
this.mqttClient.publish(topic, Unpooled.wrappedBuffer(msg.getData().getBytes(UTF8)), MqttQoS.AT_LEAST_ONCE, mqttNodeConfiguration.isRetainedMessage()) this.mqttClient.publish(topic, Unpooled.wrappedBuffer(msg.getData().getBytes(UTF8)), MqttQoS.AT_LEAST_ONCE, mqttNodeConfiguration.isRetainedMessage())
.addListener(future -> { .addListener(future -> {
if (future.isSuccess()) { if (future.isSuccess()) {
ctx.tellSuccess(msg); tellSuccess(ctx, msg);
} else { } else {
TbMsg next = processException(ctx, msg, future.cause()); tellFailure(ctx, processException(ctx, msg, future.cause()), future.cause());
ctx.tellFailure(next, future.cause());
} }
} }
); );
ackIfNeeded(ctx, msg);
} }
private TbMsg processException(TbContext ctx, TbMsg origMsg, Throwable e) { private TbMsg processException(TbContext ctx, TbMsg origMsg, Throwable e) {

3
rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/mqtt/azure/TbAzureIotHubNode.java

@ -48,8 +48,9 @@ import javax.net.ssl.SSLException;
public class TbAzureIotHubNode extends TbMqttNode { public class TbAzureIotHubNode extends TbMqttNode {
@Override @Override
public void init(TbContext ctx, TbNodeConfiguration configuration) throws TbNodeException { public void init(TbContext ctx, TbNodeConfiguration configuration) throws TbNodeException {
super.init(ctx);
this.mqttNodeConfiguration = TbNodeUtils.convert(configuration, TbMqttNodeConfiguration.class);
try { try {
this.mqttNodeConfiguration = TbNodeUtils.convert(configuration, TbMqttNodeConfiguration.class);
mqttNodeConfiguration.setPort(8883); mqttNodeConfiguration.setPort(8883);
mqttNodeConfiguration.setCleanSession(true); mqttNodeConfiguration.setCleanSession(true);
ClientCredentials credentials = mqttNodeConfiguration.getCredentials(); ClientCredentials credentials = mqttNodeConfiguration.getCredentials();

23
rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/notification/TbNotificationNode.java

@ -23,6 +23,7 @@ import org.thingsboard.rule.engine.api.TbNode;
import org.thingsboard.rule.engine.api.TbNodeConfiguration; import org.thingsboard.rule.engine.api.TbNodeConfiguration;
import org.thingsboard.rule.engine.api.TbNodeException; import org.thingsboard.rule.engine.api.TbNodeException;
import org.thingsboard.rule.engine.api.util.TbNodeUtils; import org.thingsboard.rule.engine.api.util.TbNodeUtils;
import org.thingsboard.rule.engine.external.TbAbstractExternalNode;
import org.thingsboard.server.common.data.notification.NotificationRequest; import org.thingsboard.server.common.data.notification.NotificationRequest;
import org.thingsboard.server.common.data.notification.NotificationRequestConfig; import org.thingsboard.server.common.data.notification.NotificationRequestConfig;
import org.thingsboard.server.common.data.notification.info.RuleEngineOriginatedNotificationInfo; import org.thingsboard.server.common.data.notification.info.RuleEngineOriginatedNotificationInfo;
@ -42,12 +43,13 @@ import java.util.concurrent.ExecutionException;
configDirective = "tbExternalNodeNotificationConfig", configDirective = "tbExternalNodeNotificationConfig",
icon = "notifications" icon = "notifications"
) )
public class TbNotificationNode implements TbNode { public class TbNotificationNode extends TbAbstractExternalNode {
private TbNotificationNodeConfiguration config; private TbNotificationNodeConfiguration config;
@Override @Override
public void init(TbContext ctx, TbNodeConfiguration configuration) throws TbNodeException { public void init(TbContext ctx, TbNodeConfiguration configuration) throws TbNodeException {
super.init(ctx);
this.config = TbNodeUtils.convert(configuration, TbNotificationNodeConfiguration.class); this.config = TbNodeUtils.convert(configuration, TbNotificationNodeConfiguration.class);
} }
@ -69,15 +71,16 @@ public class TbNotificationNode implements TbNode {
.originatorEntityId(ctx.getSelf().getRuleChainId()) .originatorEntityId(ctx.getSelf().getRuleChainId())
.build(); .build();
DonAsynchron.withCallback(ctx.getNotificationExecutor().executeAsync(() -> { DonAsynchron.withCallback(ctx.getNotificationExecutor().executeAsync(() ->
return ctx.getNotificationCenter().processNotificationRequest(ctx.getTenantId(), notificationRequest, stats -> { ctx.getNotificationCenter().processNotificationRequest(ctx.getTenantId(), notificationRequest, stats -> {
TbMsgMetaData metaData = msg.getMetaData().copy(); TbMsgMetaData metaData = msg.getMetaData().copy();
metaData.putValue("notificationRequestResult", JacksonUtil.toString(stats)); metaData.putValue("notificationRequestResult", JacksonUtil.toString(stats));
ctx.tellSuccess(TbMsg.transformMsg(msg, metaData)); tellSuccess(ctx, TbMsg.transformMsg(msg, metaData));
}); })),
}), r -> {
r -> {}, },
e -> ctx.tellFailure(msg, e)); e -> tellFailure(ctx, msg, e));
ackIfNeeded(ctx, msg);
} }
} }

9
rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/notification/TbSlackNode.java

@ -22,6 +22,7 @@ import org.thingsboard.rule.engine.api.TbNode;
import org.thingsboard.rule.engine.api.TbNodeConfiguration; import org.thingsboard.rule.engine.api.TbNodeConfiguration;
import org.thingsboard.rule.engine.api.TbNodeException; import org.thingsboard.rule.engine.api.TbNodeException;
import org.thingsboard.rule.engine.api.util.TbNodeUtils; import org.thingsboard.rule.engine.api.util.TbNodeUtils;
import org.thingsboard.rule.engine.external.TbAbstractExternalNode;
import org.thingsboard.server.common.data.plugin.ComponentType; import org.thingsboard.server.common.data.plugin.ComponentType;
import org.thingsboard.server.common.msg.TbMsg; import org.thingsboard.server.common.msg.TbMsg;
@ -37,12 +38,13 @@ import java.util.concurrent.ExecutionException;
configDirective = "tbExternalNodeSlackConfig", configDirective = "tbExternalNodeSlackConfig",
iconUrl = "data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAyNCAyNCI+PHBhdGggZD0iTTYsMTVBMiwyIDAgMCwxIDQsMTdBMiwyIDAgMCwxIDIsMTVBMiwyIDAgMCwxIDQsMTNINlYxNU03LDE1QTIsMiAwIDAsMSA5LDEzQTIsMiAwIDAsMSAxMSwxNVYyMEEyLDIgMCAwLDEgOSwyMkEyLDIgMCAwLDEgNywyMFYxNU05LDdBMiwyIDAgMCwxIDcsNUEyLDIgMCAwLDEgOSwzQTIsMiAwIDAsMSAxMSw1VjdIOU05LDhBMiwyIDAgMCwxIDExLDEwQTIsMiAwIDAsMSA5LDEySDRBMiwyIDAgMCwxIDIsMTBBMiwyIDAgMCwxIDQsOEg5TTE3LDEwQTIsMiAwIDAsMSAxOSw4QTIsMiAwIDAsMSAyMSwxMEEyLDIgMCAwLDEgMTksMTJIMTdWMTBNMTYsMTBBMiwyIDAgMCwxIDE0LDEyQTIsMiAwIDAsMSAxMiwxMFY1QTIsMiAwIDAsMSAxNCwzQTIsMiAwIDAsMSAxNiw1VjEwTTE0LDE4QTIsMiAwIDAsMSAxNiwyMEEyLDIgMCAwLDEgMTQsMjJBMiwyIDAgMCwxIDEyLDIwVjE4SDE0TTE0LDE3QTIsMiAwIDAsMSAxMiwxNUEyLDIgMCAwLDEgMTQsMTNIMTlBMiwyIDAgMCwxIDIxLDE1QTIsMiAwIDAsMSAxOSwxN0gxNFoiIC8+PC9zdmc+" iconUrl = "data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAyNCAyNCI+PHBhdGggZD0iTTYsMTVBMiwyIDAgMCwxIDQsMTdBMiwyIDAgMCwxIDIsMTVBMiwyIDAgMCwxIDQsMTNINlYxNU03LDE1QTIsMiAwIDAsMSA5LDEzQTIsMiAwIDAsMSAxMSwxNVYyMEEyLDIgMCAwLDEgOSwyMkEyLDIgMCAwLDEgNywyMFYxNU05LDdBMiwyIDAgMCwxIDcsNUEyLDIgMCAwLDEgOSwzQTIsMiAwIDAsMSAxMSw1VjdIOU05LDhBMiwyIDAgMCwxIDExLDEwQTIsMiAwIDAsMSA5LDEySDRBMiwyIDAgMCwxIDIsMTBBMiwyIDAgMCwxIDQsOEg5TTE3LDEwQTIsMiAwIDAsMSAxOSw4QTIsMiAwIDAsMSAyMSwxMEEyLDIgMCAwLDEgMTksMTJIMTdWMTBNMTYsMTBBMiwyIDAgMCwxIDE0LDEyQTIsMiAwIDAsMSAxMiwxMFY1QTIsMiAwIDAsMSAxNCwzQTIsMiAwIDAsMSAxNiw1VjEwTTE0LDE4QTIsMiAwIDAsMSAxNiwyMEEyLDIgMCAwLDEgMTQsMjJBMiwyIDAgMCwxIDEyLDIwVjE4SDE0TTE0LDE3QTIsMiAwIDAsMSAxMiwxNUEyLDIgMCAwLDEgMTQsMTNIMTlBMiwyIDAgMCwxIDIxLDE1QTIsMiAwIDAsMSAxOSwxN0gxNFoiIC8+PC9zdmc+"
) )
public class TbSlackNode implements TbNode { public class TbSlackNode extends TbAbstractExternalNode {
private TbSlackNodeConfiguration config; private TbSlackNodeConfiguration config;
@Override @Override
public void init(TbContext ctx, TbNodeConfiguration configuration) throws TbNodeException { public void init(TbContext ctx, TbNodeConfiguration configuration) throws TbNodeException {
super.init(ctx);
this.config = TbNodeUtils.convert(configuration, TbSlackNodeConfiguration.class); this.config = TbNodeUtils.convert(configuration, TbSlackNodeConfiguration.class);
} }
@ -62,8 +64,9 @@ public class TbSlackNode implements TbNode {
DonAsynchron.withCallback(ctx.getExternalCallExecutor().executeAsync(() -> { DonAsynchron.withCallback(ctx.getExternalCallExecutor().executeAsync(() -> {
ctx.getSlackService().sendMessage(ctx.getTenantId(), token, config.getConversation().getId(), message); ctx.getSlackService().sendMessage(ctx.getTenantId(), token, config.getConversation().getId(), message);
}), }),
r -> ctx.tellSuccess(msg), r -> tellSuccess(ctx, msg),
e -> ctx.tellFailure(msg, e)); e -> tellFailure(ctx, msg, e));
ackIfNeeded(ctx, msg);
} }
} }

12
rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/rabbitmq/TbRabbitMqNode.java

@ -28,6 +28,7 @@ import org.thingsboard.rule.engine.api.TbNode;
import org.thingsboard.rule.engine.api.TbNodeConfiguration; import org.thingsboard.rule.engine.api.TbNodeConfiguration;
import org.thingsboard.rule.engine.api.TbNodeException; import org.thingsboard.rule.engine.api.TbNodeException;
import org.thingsboard.rule.engine.api.util.TbNodeUtils; import org.thingsboard.rule.engine.api.util.TbNodeUtils;
import org.thingsboard.rule.engine.external.TbAbstractExternalNode;
import org.thingsboard.server.common.data.StringUtils; import org.thingsboard.server.common.data.StringUtils;
import org.thingsboard.server.common.data.plugin.ComponentType; import org.thingsboard.server.common.data.plugin.ComponentType;
import org.thingsboard.server.common.msg.TbMsg; import org.thingsboard.server.common.msg.TbMsg;
@ -48,7 +49,7 @@ import static org.thingsboard.common.util.DonAsynchron.withCallback;
configDirective = "tbExternalNodeRabbitMqConfig", configDirective = "tbExternalNodeRabbitMqConfig",
iconUrl = "data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHhtbDpzcGFjZT0icHJlc2VydmUiIHZlcnNpb249IjEuMSIgeT0iMHB4IiB4PSIwcHgiIHZpZXdCb3g9IjAgMCAxMDAwIDEwMDAiPjxwYXRoIHN0cm9rZS13aWR0aD0iLjg0OTU2IiBkPSJtODYwLjQ3IDQxNi4zMmgtMjYyLjAxYy0xMi45MTMgMC0yMy42MTgtMTAuNzA0LTIzLjYxOC0yMy42MTh2LTI3Mi43MWMwLTIwLjMwNS0xNi4yMjctMzYuMjc2LTM2LjI3Ni0zNi4yNzZoLTkzLjc5MmMtMjAuMzA1IDAtMzYuMjc2IDE2LjIyNy0zNi4yNzYgMzYuMjc2djI3MC44NGMtMC4yNTQ4NyAxNC4xMDMtMTEuNDY5IDI1LjU3Mi0yNS43NDIgMjUuNTcybC04NS42MzYgMC42Nzk2NWMtMTQuMTAzIDAtMjUuNTcyLTExLjQ2OS0yNS41NzItMjUuNTcybDAuNjc5NjUtMjcxLjUyYzAtMjAuMzA1LTE2LjIyNy0zNi4yNzYtMzYuMjc2LTM2LjI3NmgtOTMuNTM3Yy0yMC4zMDUgMC0zNi4yNzYgMTYuMjI3LTM2LjI3NiAzNi4yNzZ2NzYzLjg0YzAgMTguMDk2IDE0Ljc4MiAzMi40NTMgMzIuNDUzIDMyLjQ1M2g3MjIuODFjMTguMDk2IDAgMzIuNDUzLTE0Ljc4MiAzMi40NTMtMzIuNDUzdi00MzUuMzFjLTEuMTg5NC0xOC4xODEtMTUuMjkyLTMyLjE5OC0zMy4zODgtMzIuMTk4em0tMTIyLjY4IDI4Ny4wN2MwIDIzLjYxOC0xOC44NiA0Mi40NzgtNDIuNDc4IDQyLjQ3OGgtNzMuOTk3Yy0yMy42MTggMC00Mi40NzgtMTguODYtNDIuNDc4LTQyLjQ3OHYtNzQuMjUyYzAtMjMuNjE4IDE4Ljg2LTQyLjQ3OCA0Mi40NzgtNDIuNDc4aDczLjk5N2MyMy42MTggMCA0Mi40NzggMTguODYgNDIuNDc4IDQyLjQ3OHoiLz48L3N2Zz4=" iconUrl = "data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHhtbDpzcGFjZT0icHJlc2VydmUiIHZlcnNpb249IjEuMSIgeT0iMHB4IiB4PSIwcHgiIHZpZXdCb3g9IjAgMCAxMDAwIDEwMDAiPjxwYXRoIHN0cm9rZS13aWR0aD0iLjg0OTU2IiBkPSJtODYwLjQ3IDQxNi4zMmgtMjYyLjAxYy0xMi45MTMgMC0yMy42MTgtMTAuNzA0LTIzLjYxOC0yMy42MTh2LTI3Mi43MWMwLTIwLjMwNS0xNi4yMjctMzYuMjc2LTM2LjI3Ni0zNi4yNzZoLTkzLjc5MmMtMjAuMzA1IDAtMzYuMjc2IDE2LjIyNy0zNi4yNzYgMzYuMjc2djI3MC44NGMtMC4yNTQ4NyAxNC4xMDMtMTEuNDY5IDI1LjU3Mi0yNS43NDIgMjUuNTcybC04NS42MzYgMC42Nzk2NWMtMTQuMTAzIDAtMjUuNTcyLTExLjQ2OS0yNS41NzItMjUuNTcybDAuNjc5NjUtMjcxLjUyYzAtMjAuMzA1LTE2LjIyNy0zNi4yNzYtMzYuMjc2LTM2LjI3NmgtOTMuNTM3Yy0yMC4zMDUgMC0zNi4yNzYgMTYuMjI3LTM2LjI3NiAzNi4yNzZ2NzYzLjg0YzAgMTguMDk2IDE0Ljc4MiAzMi40NTMgMzIuNDUzIDMyLjQ1M2g3MjIuODFjMTguMDk2IDAgMzIuNDUzLTE0Ljc4MiAzMi40NTMtMzIuNDUzdi00MzUuMzFjLTEuMTg5NC0xOC4xODEtMTUuMjkyLTMyLjE5OC0zMy4zODgtMzIuMTk4em0tMTIyLjY4IDI4Ny4wN2MwIDIzLjYxOC0xOC44NiA0Mi40NzgtNDIuNDc4IDQyLjQ3OGgtNzMuOTk3Yy0yMy42MTggMC00Mi40NzgtMTguODYtNDIuNDc4LTQyLjQ3OHYtNzQuMjUyYzAtMjMuNjE4IDE4Ljg2LTQyLjQ3OCA0Mi40NzgtNDIuNDc4aDczLjk5N2MyMy42MTggMCA0Mi40NzggMTguODYgNDIuNDc4IDQyLjQ3OHoiLz48L3N2Zz4="
) )
public class TbRabbitMqNode implements TbNode { public class TbRabbitMqNode extends TbAbstractExternalNode {
private static final Charset UTF8 = Charset.forName("UTF-8"); private static final Charset UTF8 = Charset.forName("UTF-8");
@ -61,6 +62,7 @@ public class TbRabbitMqNode implements TbNode {
@Override @Override
public void init(TbContext ctx, TbNodeConfiguration configuration) throws TbNodeException { public void init(TbContext ctx, TbNodeConfiguration configuration) throws TbNodeException {
super.init(ctx);
this.config = TbNodeUtils.convert(configuration, TbRabbitMqNodeConfiguration.class); this.config = TbNodeUtils.convert(configuration, TbRabbitMqNodeConfiguration.class);
ConnectionFactory factory = new ConnectionFactory(); ConnectionFactory factory = new ConnectionFactory();
factory.setHost(this.config.getHost()); factory.setHost(this.config.getHost());
@ -83,11 +85,9 @@ public class TbRabbitMqNode implements TbNode {
@Override @Override
public void onMsg(TbContext ctx, TbMsg msg) { public void onMsg(TbContext ctx, TbMsg msg) {
withCallback(publishMessageAsync(ctx, msg), withCallback(publishMessageAsync(ctx, msg),
ctx::tellSuccess, m -> tellSuccess(ctx, m),
t -> { t -> tellFailure(ctx, processException(ctx, msg, t), t));
TbMsg next = processException(ctx, msg, t); ackIfNeeded(ctx, msg);
ctx.tellFailure(next, t);
});
} }
private ListenableFuture<TbMsg> publishMessageAsync(TbContext ctx, TbMsg msg) { private ListenableFuture<TbMsg> publishMessageAsync(TbContext ctx, TbMsg msg) {

25
rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/rest/TbHttpClient.java

@ -43,7 +43,6 @@ import org.springframework.web.util.UriComponentsBuilder;
import org.thingsboard.common.util.JacksonUtil; import org.thingsboard.common.util.JacksonUtil;
import org.thingsboard.rule.engine.api.TbContext; import org.thingsboard.rule.engine.api.TbContext;
import org.thingsboard.rule.engine.api.TbNodeException; import org.thingsboard.rule.engine.api.TbNodeException;
import org.thingsboard.server.common.data.msg.TbNodeConnectionType;
import org.thingsboard.rule.engine.api.util.TbNodeUtils; import org.thingsboard.rule.engine.api.util.TbNodeUtils;
import org.thingsboard.rule.engine.credentials.BasicCredentials; import org.thingsboard.rule.engine.credentials.BasicCredentials;
import org.thingsboard.rule.engine.credentials.ClientCredentials; import org.thingsboard.rule.engine.credentials.ClientCredentials;
@ -65,6 +64,7 @@ import java.util.Map;
import java.util.concurrent.ConcurrentLinkedDeque; import java.util.concurrent.ConcurrentLinkedDeque;
import java.util.concurrent.TimeUnit; import java.util.concurrent.TimeUnit;
import java.util.function.BiConsumer; import java.util.function.BiConsumer;
import java.util.function.Consumer;
@Data @Data
@Slf4j @Slf4j
@ -182,14 +182,16 @@ public class TbHttpClient {
} }
} }
public void processMessage(TbContext ctx, TbMsg msg) { public void processMessage(TbContext ctx, TbMsg msg,
Consumer<TbMsg> onSuccess,
BiConsumer<TbMsg, Throwable> onFailure) {
String endpointUrl = TbNodeUtils.processPattern(config.getRestEndpointUrlPattern(), msg); String endpointUrl = TbNodeUtils.processPattern(config.getRestEndpointUrlPattern(), msg);
HttpHeaders headers = prepareHeaders(msg); HttpHeaders headers = prepareHeaders(msg);
HttpMethod method = HttpMethod.valueOf(config.getRequestMethod()); HttpMethod method = HttpMethod.valueOf(config.getRequestMethod());
HttpEntity<String> entity; HttpEntity<String> entity;
if(HttpMethod.GET.equals(method) || HttpMethod.HEAD.equals(method) || if (HttpMethod.GET.equals(method) || HttpMethod.HEAD.equals(method) ||
HttpMethod.OPTIONS.equals(method) || HttpMethod.TRACE.equals(method) || HttpMethod.OPTIONS.equals(method) || HttpMethod.TRACE.equals(method) ||
config.isIgnoreRequestBody()) { config.isIgnoreRequestBody()) {
entity = new HttpEntity<>(headers); entity = new HttpEntity<>(headers);
} else { } else {
entity = new HttpEntity<>(getData(msg), headers); entity = new HttpEntity<>(getData(msg), headers);
@ -198,21 +200,18 @@ public class TbHttpClient {
URI uri = buildEncodedUri(endpointUrl); URI uri = buildEncodedUri(endpointUrl);
ListenableFuture<ResponseEntity<String>> future = httpClient.exchange( ListenableFuture<ResponseEntity<String>> future = httpClient.exchange(
uri, method, entity, String.class); uri, method, entity, String.class);
future.addCallback(new ListenableFutureCallback<ResponseEntity<String>>() { future.addCallback(new ListenableFutureCallback<>() {
@Override @Override
public void onFailure(Throwable throwable) { public void onFailure(Throwable throwable) {
TbMsg next = processException(ctx, msg, throwable); onFailure.accept(processException(ctx, msg, throwable), throwable);
ctx.tellFailure(next, throwable);
} }
@Override @Override
public void onSuccess(ResponseEntity<String> responseEntity) { public void onSuccess(ResponseEntity<String> responseEntity) {
if (responseEntity.getStatusCode().is2xxSuccessful()) { if (responseEntity.getStatusCode().is2xxSuccessful()) {
TbMsg next = processResponse(ctx, msg, responseEntity); onSuccess.accept(processResponse(ctx, msg, responseEntity));
ctx.tellSuccess(next);
} else { } else {
TbMsg next = processFailureResponse(ctx, msg, responseEntity); onFailure.accept(processFailureResponse(ctx, msg, responseEntity), null);
ctx.tellNext(next, TbNodeConnectionType.FAILURE);
} }
} }
}); });
@ -248,7 +247,7 @@ public class TbHttpClient {
if (config.isTrimDoubleQuotes()) { if (config.isTrimDoubleQuotes()) {
final String dataBefore = data; final String dataBefore = data;
data = data.replaceAll("^\"|\"$", "");; data = data.replaceAll("^\"|\"$", "");
log.trace("Trimming double quotes. Before trim: [{}], after trim: [{}]", dataBefore, data); log.trace("Trimming double quotes. Before trim: [{}], after trim: [{}]", dataBefore, data);
} }

13
rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/rest/TbRestApiCallNode.java

@ -22,6 +22,7 @@ import org.thingsboard.rule.engine.api.TbNode;
import org.thingsboard.rule.engine.api.TbNodeConfiguration; import org.thingsboard.rule.engine.api.TbNodeConfiguration;
import org.thingsboard.rule.engine.api.TbNodeException; import org.thingsboard.rule.engine.api.TbNodeException;
import org.thingsboard.rule.engine.api.util.TbNodeUtils; import org.thingsboard.rule.engine.api.util.TbNodeUtils;
import org.thingsboard.rule.engine.external.TbAbstractExternalNode;
import org.thingsboard.server.common.data.plugin.ComponentType; import org.thingsboard.server.common.data.plugin.ComponentType;
import org.thingsboard.server.common.msg.TbMsg; import org.thingsboard.server.common.msg.TbMsg;
@ -43,24 +44,26 @@ import org.thingsboard.server.common.msg.TbMsg;
configDirective = "tbExternalNodeRestApiCallConfig", configDirective = "tbExternalNodeRestApiCallConfig",
iconUrl = "data:image/svg+xml;base64,PHN2ZyBzdHlsZT0iZW5hYmxlLWJhY2tncm91bmQ6bmV3IDAgMCA1MTIgNTEyIiB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHhtbDpzcGFjZT0icHJlc2VydmUiIHZpZXdCb3g9IjAgMCA1MTIgNTEyIiB2ZXJzaW9uPSIxLjEiIHk9IjBweCIgeD0iMHB4Ij48ZyB0cmFuc2Zvcm09Im1hdHJpeCguOTQ5NzUgMCAwIC45NDk3NSAxNy4xMiAyNi40OTIpIj48cGF0aCBkPSJtMTY5LjExIDEwOC41NGMtOS45MDY2IDAuMDczNC0xOS4wMTQgNi41NzI0LTIyLjAxNCAxNi40NjlsLTY5Ljk5MyAyMzEuMDhjLTMuNjkwNCAxMi4xODEgMy4yODkyIDI1LjIyIDE1LjQ2OSAyOC45MSAyLjIyNTkgMC42NzQ4MSA0LjQ5NjkgMSA2LjcyODUgMSA5Ljk3MjEgMCAxOS4xNjUtNi41MTUzIDIyLjE4Mi0xNi40NjdhNi41MjI0IDYuNTIyNCAwIDAgMCAwLjAwMiAtMC4wMDJsNjkuOTktMjMxLjA3YTYuNTIyNCA2LjUyMjQgMCAwIDAgMCAtMC4wMDJjMy42ODU1LTEyLjE4MS0zLjI4Ny0yNS4yMjUtMTUuNDcxLTI4LjkxMi0yLjI4MjUtMC42OTE0NS00LjYxMTYtMS4wMTY5LTYuODk4NC0xem04NC45ODggMGMtOS45MDQ4IDAuMDczNC0xOS4wMTggNi41Njc1LTIyLjAxOCAxNi40NjlsLTY5Ljk4NiAyMzEuMDhjLTMuNjg5OCAxMi4xNzkgMy4yODUzIDI1LjIxNyAxNS40NjUgMjguOTA4IDIuMjI5NyAwLjY3NjQ3IDQuNTAwOCAxLjAwMiA2LjczMjQgMS4wMDIgOS45NzIxIDAgMTkuMTY1LTYuNTE1MyAyMi4xODItMTYuNDY3YTYuNTIyNCA2LjUyMjQgMCAwIDAgMC4wMDIgLTAuMDAybDY5Ljk4OC0yMzEuMDdjMy42OTA4LTEyLjE4MS0zLjI4NTItMjUuMjIzLTE1LjQ2Ny0yOC45MTItMi4yODE0LTAuNjkyMzEtNC42MTA4LTEuMDE4OS02Ljg5ODQtMS4wMDJ6bS0yMTcuMjkgNDIuMjNjLTEyLjcyOS0wLjAwMDg3LTIzLjE4OCAxMC40NTYtMjMuMTg4IDIzLjE4NiAwLjAwMSAxMi43MjggMTAuNDU5IDIzLjE4NiAyMy4xODggMjMuMTg2IDEyLjcyNy0wLjAwMSAyMy4xODMtMTAuNDU5IDIzLjE4NC0yMy4xODYgMC4wMDA4NzYtMTIuNzI4LTEwLjQ1Ni0yMy4xODUtMjMuMTg0LTIzLjE4NnptMCAxNDYuNjRjLTEyLjcyNy0wLjAwMDg3LTIzLjE4NiAxMC40NTUtMjMuMTg4IDIzLjE4NC0wLjAwMDg3MyAxMi43MjkgMTAuNDU4IDIzLjE4OCAyMy4xODggMjMuMTg4IDEyLjcyOC0wLjAwMSAyMy4xODQtMTAuNDYgMjMuMTg0LTIzLjE4OC0wLjAwMS0xMi43MjYtMTAuNDU3LTIzLjE4My0yMy4xODQtMjMuMTg0em0yNzAuNzkgNDIuMjExYy0xMi43MjcgMC0yMy4xODQgMTAuNDU3LTIzLjE4NCAyMy4xODRzMTAuNDU1IDIzLjE4OCAyMy4xODQgMjMuMTg4aDE1NC45OGMxMi43MjkgMCAyMy4xODYtMTAuNDYgMjMuMTg2LTIzLjE4OCAwLjAwMS0xMi43MjgtMTAuNDU4LTIzLjE4NC0yMy4xODYtMjMuMTg0eiIgdHJhbnNmb3JtPSJtYXRyaXgoMS4wMzc2IDAgMCAxLjAzNzYgLTcuNTY3NiAtMTQuOTI1KSIgc3Ryb2tlLXdpZHRoPSIxLjI2OTMiLz48L2c+PC9zdmc+" iconUrl = "data:image/svg+xml;base64,PHN2ZyBzdHlsZT0iZW5hYmxlLWJhY2tncm91bmQ6bmV3IDAgMCA1MTIgNTEyIiB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHhtbDpzcGFjZT0icHJlc2VydmUiIHZpZXdCb3g9IjAgMCA1MTIgNTEyIiB2ZXJzaW9uPSIxLjEiIHk9IjBweCIgeD0iMHB4Ij48ZyB0cmFuc2Zvcm09Im1hdHJpeCguOTQ5NzUgMCAwIC45NDk3NSAxNy4xMiAyNi40OTIpIj48cGF0aCBkPSJtMTY5LjExIDEwOC41NGMtOS45MDY2IDAuMDczNC0xOS4wMTQgNi41NzI0LTIyLjAxNCAxNi40NjlsLTY5Ljk5MyAyMzEuMDhjLTMuNjkwNCAxMi4xODEgMy4yODkyIDI1LjIyIDE1LjQ2OSAyOC45MSAyLjIyNTkgMC42NzQ4MSA0LjQ5NjkgMSA2LjcyODUgMSA5Ljk3MjEgMCAxOS4xNjUtNi41MTUzIDIyLjE4Mi0xNi40NjdhNi41MjI0IDYuNTIyNCAwIDAgMCAwLjAwMiAtMC4wMDJsNjkuOTktMjMxLjA3YTYuNTIyNCA2LjUyMjQgMCAwIDAgMCAtMC4wMDJjMy42ODU1LTEyLjE4MS0zLjI4Ny0yNS4yMjUtMTUuNDcxLTI4LjkxMi0yLjI4MjUtMC42OTE0NS00LjYxMTYtMS4wMTY5LTYuODk4NC0xem04NC45ODggMGMtOS45MDQ4IDAuMDczNC0xOS4wMTggNi41Njc1LTIyLjAxOCAxNi40NjlsLTY5Ljk4NiAyMzEuMDhjLTMuNjg5OCAxMi4xNzkgMy4yODUzIDI1LjIxNyAxNS40NjUgMjguOTA4IDIuMjI5NyAwLjY3NjQ3IDQuNTAwOCAxLjAwMiA2LjczMjQgMS4wMDIgOS45NzIxIDAgMTkuMTY1LTYuNTE1MyAyMi4xODItMTYuNDY3YTYuNTIyNCA2LjUyMjQgMCAwIDAgMC4wMDIgLTAuMDAybDY5Ljk4OC0yMzEuMDdjMy42OTA4LTEyLjE4MS0zLjI4NTItMjUuMjIzLTE1LjQ2Ny0yOC45MTItMi4yODE0LTAuNjkyMzEtNC42MTA4LTEuMDE4OS02Ljg5ODQtMS4wMDJ6bS0yMTcuMjkgNDIuMjNjLTEyLjcyOS0wLjAwMDg3LTIzLjE4OCAxMC40NTYtMjMuMTg4IDIzLjE4NiAwLjAwMSAxMi43MjggMTAuNDU5IDIzLjE4NiAyMy4xODggMjMuMTg2IDEyLjcyNy0wLjAwMSAyMy4xODMtMTAuNDU5IDIzLjE4NC0yMy4xODYgMC4wMDA4NzYtMTIuNzI4LTEwLjQ1Ni0yMy4xODUtMjMuMTg0LTIzLjE4NnptMCAxNDYuNjRjLTEyLjcyNy0wLjAwMDg3LTIzLjE4NiAxMC40NTUtMjMuMTg4IDIzLjE4NC0wLjAwMDg3MyAxMi43MjkgMTAuNDU4IDIzLjE4OCAyMy4xODggMjMuMTg4IDEyLjcyOC0wLjAwMSAyMy4xODQtMTAuNDYgMjMuMTg0LTIzLjE4OC0wLjAwMS0xMi43MjYtMTAuNDU3LTIzLjE4My0yMy4xODQtMjMuMTg0em0yNzAuNzkgNDIuMjExYy0xMi43MjcgMC0yMy4xODQgMTAuNDU3LTIzLjE4NCAyMy4xODRzMTAuNDU1IDIzLjE4OCAyMy4xODQgMjMuMTg4aDE1NC45OGMxMi43MjkgMCAyMy4xODYtMTAuNDYgMjMuMTg2LTIzLjE4OCAwLjAwMS0xMi43MjgtMTAuNDU4LTIzLjE4NC0yMy4xODYtMjMuMTg0eiIgdHJhbnNmb3JtPSJtYXRyaXgoMS4wMzc2IDAgMCAxLjAzNzYgLTcuNTY3NiAtMTQuOTI1KSIgc3Ryb2tlLXdpZHRoPSIxLjI2OTMiLz48L2c+PC9zdmc+"
) )
public class TbRestApiCallNode implements TbNode { public class TbRestApiCallNode extends TbAbstractExternalNode {
private boolean useRedisQueueForMsgPersistence;
protected TbHttpClient httpClient; protected TbHttpClient httpClient;
@Override @Override
public void init(TbContext ctx, TbNodeConfiguration configuration) throws TbNodeException { public void init(TbContext ctx, TbNodeConfiguration configuration) throws TbNodeException {
super.init(ctx);
TbRestApiCallNodeConfiguration config = TbNodeUtils.convert(configuration, TbRestApiCallNodeConfiguration.class); TbRestApiCallNodeConfiguration config = TbNodeUtils.convert(configuration, TbRestApiCallNodeConfiguration.class);
httpClient = new TbHttpClient(config, ctx.getSharedEventLoop()); httpClient = new TbHttpClient(config, ctx.getSharedEventLoop());
useRedisQueueForMsgPersistence = config.isUseRedisQueueForMsgPersistence(); if (config.isUseRedisQueueForMsgPersistence()) {
if (useRedisQueueForMsgPersistence) {
log.warn("[{}][{}] Usage of Redis Template is deprecated starting 2.5 and will have no affect", ctx.getTenantId(), ctx.getSelfId()); log.warn("[{}][{}] Usage of Redis Template is deprecated starting 2.5 and will have no affect", ctx.getTenantId(), ctx.getSelfId());
} }
} }
@Override @Override
public void onMsg(TbContext ctx, TbMsg msg) { public void onMsg(TbContext ctx, TbMsg msg) {
httpClient.processMessage(ctx, msg); httpClient.processMessage(ctx, msg,
m -> tellSuccess(ctx, m),
(m, t) -> tellFailure(ctx, m, t));
ackIfNeeded(ctx, msg);
} }
@Override @Override

9
rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/sms/TbSendSmsNode.java

@ -23,6 +23,7 @@ import org.thingsboard.rule.engine.api.TbNodeConfiguration;
import org.thingsboard.rule.engine.api.TbNodeException; import org.thingsboard.rule.engine.api.TbNodeException;
import org.thingsboard.rule.engine.api.sms.SmsSender; import org.thingsboard.rule.engine.api.sms.SmsSender;
import org.thingsboard.rule.engine.api.util.TbNodeUtils; import org.thingsboard.rule.engine.api.util.TbNodeUtils;
import org.thingsboard.rule.engine.external.TbAbstractExternalNode;
import org.thingsboard.server.common.data.plugin.ComponentType; import org.thingsboard.server.common.data.plugin.ComponentType;
import org.thingsboard.server.common.msg.TbMsg; import org.thingsboard.server.common.msg.TbMsg;
@ -39,13 +40,14 @@ import static org.thingsboard.common.util.DonAsynchron.withCallback;
configDirective = "tbExternalNodeSendSmsConfig", configDirective = "tbExternalNodeSendSmsConfig",
icon = "sms" icon = "sms"
) )
public class TbSendSmsNode implements TbNode { public class TbSendSmsNode extends TbAbstractExternalNode {
private TbSendSmsNodeConfiguration config; private TbSendSmsNodeConfiguration config;
private SmsSender smsSender; private SmsSender smsSender;
@Override @Override
public void init(TbContext ctx, TbNodeConfiguration configuration) throws TbNodeException { public void init(TbContext ctx, TbNodeConfiguration configuration) throws TbNodeException {
super.init(ctx);
try { try {
this.config = TbNodeUtils.convert(configuration, TbSendSmsNodeConfiguration.class); this.config = TbNodeUtils.convert(configuration, TbSendSmsNodeConfiguration.class);
if (!this.config.isUseSystemSmsSettings()) { if (!this.config.isUseSystemSmsSettings()) {
@ -63,8 +65,9 @@ public class TbSendSmsNode implements TbNode {
sendSms(ctx, msg); sendSms(ctx, msg);
return null; return null;
}), }),
ok -> ctx.tellSuccess(msg), ok -> tellSuccess(ctx, msg),
fail -> ctx.tellFailure(msg, fail)); fail -> tellFailure(ctx, msg, fail));
ackIfNeeded(ctx, msg);
} catch (Exception ex) { } catch (Exception ex) {
ctx.tellFailure(msg, ex); ctx.tellFailure(msg, ex);
} }

4
rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/rest/TbHttpClientTest.java

@ -167,7 +167,9 @@ public class TbHttpClientTest {
capturedData.capture() capturedData.capture()
)).thenReturn(successMsg); )).thenReturn(successMsg);
httpClient.processMessage(ctx, msg); httpClient.processMessage(ctx, msg,
m -> ctx.tellSuccess(msg),
(m, t) -> ctx.tellFailure(m, t));
Awaitility.await() Awaitility.await()
.atMost(30, TimeUnit.SECONDS) .atMost(30, TimeUnit.SECONDS)

10
ui-ngx/src/app/core/api/alias-controller.ts

@ -252,10 +252,9 @@ export class AliasController implements IAliasController {
private resolveDatasource(datasource: Datasource, forceFilter = false): Observable<Datasource> { private resolveDatasource(datasource: Datasource, forceFilter = false): Observable<Datasource> {
const newDatasource = deepClone(datasource); const newDatasource = deepClone(datasource);
if (newDatasource.type === DatasourceType.device) { if (newDatasource.type === DatasourceType.entity
newDatasource.type = DatasourceType.entity; || newDatasource.type === DatasourceType.device
} || newDatasource.type === DatasourceType.entityCount
if (newDatasource.type === DatasourceType.entity || newDatasource.type === DatasourceType.entityCount
|| newDatasource.type === DatasourceType.alarmCount) { || newDatasource.type === DatasourceType.alarmCount) {
if (newDatasource.filterId) { if (newDatasource.filterId) {
newDatasource.keyFilters = this.getKeyFilters(newDatasource.filterId); newDatasource.keyFilters = this.getKeyFilters(newDatasource.filterId);
@ -263,7 +262,8 @@ export class AliasController implements IAliasController {
if (newDatasource.type === DatasourceType.alarmCount) { if (newDatasource.type === DatasourceType.alarmCount) {
newDatasource.alarmFilter = this.entityService.resolveAlarmFilter(newDatasource.alarmFilterConfig, false); newDatasource.alarmFilter = this.entityService.resolveAlarmFilter(newDatasource.alarmFilterConfig, false);
} }
if (newDatasource.deviceId) { if (newDatasource.type === DatasourceType.device) {
newDatasource.type = DatasourceType.entity;
newDatasource.entityFilter = singleEntityFilterFromDeviceId(newDatasource.deviceId); newDatasource.entityFilter = singleEntityFilterFromDeviceId(newDatasource.deviceId);
if (forceFilter) { if (forceFilter) {
return this.entityService.findSingleEntityInfoByEntityFilter(newDatasource.entityFilter, return this.entityService.findSingleEntityInfoByEntityFilter(newDatasource.entityFilter,

10
ui-ngx/src/app/core/services/utils.service.ts

@ -282,13 +282,9 @@ export class UtilsService {
public validateDatasources(datasources: Array<Datasource>): Array<Datasource> { public validateDatasources(datasources: Array<Datasource>): Array<Datasource> {
datasources.forEach((datasource) => { datasources.forEach((datasource) => {
// @ts-ignore if (datasource.type === DatasourceType.device) {
if (datasource.type === 'device') { if (datasource.deviceAliasId) {
datasource.type = DatasourceType.entity; datasource.type = DatasourceType.entity;
datasource.entityType = EntityType.DEVICE;
if (datasource.deviceId) {
datasource.entityId = datasource.deviceId;
} else if (datasource.deviceAliasId) {
datasource.entityAliasId = datasource.deviceAliasId; datasource.entityAliasId = datasource.deviceAliasId;
} }
if (datasource.deviceName) { if (datasource.deviceName) {

4
ui-ngx/src/app/core/ws/websocket.service.ts

@ -97,7 +97,9 @@ export abstract class WebsocketService<T extends WsSubscriber> implements WsServ
this.dataStream.next(this.cmdWrapper.preparePublishCommands(MAX_PUBLISH_COMMANDS)); this.dataStream.next(this.cmdWrapper.preparePublishCommands(MAX_PUBLISH_COMMANDS));
this.checkToClose(); this.checkToClose();
} }
this.tryOpenSocket(); if (this.subscribersCount > 0) {
this.tryOpenSocket();
}
} }
private checkToClose() { private checkToClose() {

3
ui-ngx/src/app/modules/dashboard/dashboard-pages.routing.module.ts

@ -25,6 +25,7 @@ import { DashboardUtilsService } from '@core/services/dashboard-utils.service';
import { DashboardResolver } from '@app/modules/home/pages/dashboard/dashboard-routing.module'; import { DashboardResolver } from '@app/modules/home/pages/dashboard/dashboard-routing.module';
import { UtilsService } from '@core/services/utils.service'; import { UtilsService } from '@core/services/utils.service';
import { Widget } from '@app/shared/models/widget.models'; import { Widget } from '@app/shared/models/widget.models';
import { ConfirmOnExitGuard } from '@core/guards/confirm-on-exit.guard';
@Injectable() @Injectable()
export class WidgetEditorDashboardResolver implements Resolve<Dashboard> { export class WidgetEditorDashboardResolver implements Resolve<Dashboard> {
@ -59,6 +60,7 @@ const routes: Routes = [
{ {
path: 'dashboard/:dashboardId', path: 'dashboard/:dashboardId',
component: DashboardPageComponent, component: DashboardPageComponent,
canDeactivate: [ConfirmOnExitGuard],
data: { data: {
breadcrumb: { breadcrumb: {
skip: true skip: true
@ -75,6 +77,7 @@ const routes: Routes = [
{ {
path: 'widget-editor', path: 'widget-editor',
component: DashboardPageComponent, component: DashboardPageComponent,
canDeactivate: [ConfirmOnExitGuard],
data: { data: {
breadcrumb: { breadcrumb: {
skip: true skip: true

2
ui-ngx/src/app/modules/home/components/dashboard-page/dashboard-page.component.html

@ -115,7 +115,7 @@
matTooltip="{{'action.save' | translate}}" matTooltip="{{'action.save' | translate}}"
matTooltipPosition="below"> matTooltipPosition="below">
<mat-icon>done</mat-icon></button> <mat-icon>done</mat-icon></button>
<button fxHide.lt-lg mat-button (click)="saveDashboard()"> <button fxHide.lt-lg mat-stroked-button (click)="saveDashboard()">
<mat-icon>done</mat-icon>{{ 'action.save' | translate }}</button> <mat-icon>done</mat-icon>{{ 'action.save' | translate }}</button>
<button fxHide.gt-md mat-icon-button (click)="toggleDashboardEditMode()" <button fxHide.gt-md mat-icon-button (click)="toggleDashboardEditMode()"
matTooltip="{{'action.cancel' | translate}}" matTooltip="{{'action.cancel' | translate}}"

11
ui-ngx/src/app/modules/home/components/dashboard-page/dashboard-page.component.ts

@ -150,6 +150,7 @@ import { tap } from 'rxjs/operators';
import { LayoutFixedSize, LayoutWidthType } from '@home/components/dashboard-page/layout/layout.models'; import { LayoutFixedSize, LayoutWidthType } from '@home/components/dashboard-page/layout/layout.models';
import { TbPopoverComponent } from '@shared/components/popover.component'; import { TbPopoverComponent } from '@shared/components/popover.component';
import { ResizeObserver } from '@juggle/resize-observer'; import { ResizeObserver } from '@juggle/resize-observer';
import { HasDirtyFlag } from '@core/guards/confirm-on-exit.guard';
// @dynamic // @dynamic
@Component({ @Component({
@ -159,7 +160,15 @@ import { ResizeObserver } from '@juggle/resize-observer';
encapsulation: ViewEncapsulation.None, encapsulation: ViewEncapsulation.None,
changeDetection: ChangeDetectionStrategy.OnPush changeDetection: ChangeDetectionStrategy.OnPush
}) })
export class DashboardPageComponent extends PageComponent implements IDashboardController, OnInit, AfterViewInit, OnDestroy { export class DashboardPageComponent extends PageComponent implements IDashboardController, HasDirtyFlag, OnInit, AfterViewInit, OnDestroy {
get isDirty(): boolean {
return this.isEdit;
}
set isDirty(value: boolean) {
}
authState: AuthState = getCurrentAuthState(this.store); authState: AuthState = getCurrentAuthState(this.store);

11
ui-ngx/src/app/modules/home/components/widget/config/basic/cards/entities-table-basic-config.component.html

@ -28,6 +28,7 @@
<tb-data-keys-panel <tb-data-keys-panel
panelTitle="{{ 'widgets.table.columns' | translate }}" panelTitle="{{ 'widgets.table.columns' | translate }}"
addKeyTitle="{{ 'widgets.table.add-column' | translate }}" addKeyTitle="{{ 'widgets.table.add-column' | translate }}"
keySettingsTitle="{{ 'widgets.table.column-settings' | translate }}"
removeKeyTitle="{{ 'widgets.table.remove-column' | translate }}" removeKeyTitle="{{ 'widgets.table.remove-column' | translate }}"
noKeysText="{{ 'widgets.table.no-columns' | translate }}" noKeysText="{{ 'widgets.table.no-columns' | translate }}"
hideDataKeyColor hideDataKeyColor
@ -61,6 +62,14 @@
</tb-color-input> </tb-color-input>
</div> </div>
</div> </div>
<div class="tb-widget-config-row space-between">
<div translate>widgets.table.show-card-buttons</div>
<mat-chip-listbox multiple formControlName="cardButtons">
<mat-chip-option value="search">{{ 'action.search' | translate }}</mat-chip-option>
<mat-chip-option value="columnsToDisplay">{{ 'widgets.table.columns-to-display' | translate }}</mat-chip-option>
<mat-chip-option value="fullscreen">{{ 'fullscreen.fullscreen' | translate }}</mat-chip-option>
</mat-chip-listbox>
</div>
<div class="tb-widget-config-row space-between same-padding"> <div class="tb-widget-config-row space-between same-padding">
<div>{{ 'widget-config.text-color' | translate }}</div> <div>{{ 'widget-config.text-color' | translate }}</div>
<div fxLayout="row" fxLayoutAlign="start center" fxLayoutGap="16px"> <div fxLayout="row" fxLayoutAlign="start center" fxLayoutGap="16px">
@ -71,7 +80,7 @@
</div> </div>
</div> </div>
<div class="tb-widget-config-row space-between same-padding"> <div class="tb-widget-config-row space-between same-padding">
<div>{{ 'widget-config.background' | translate }}</div> <div>{{ 'widget-config.background-color' | translate }}</div>
<div fxLayout="row" fxLayoutAlign="start center" fxLayoutGap="16px"> <div fxLayout="row" fxLayoutAlign="start center" fxLayoutGap="16px">
<mat-divider vertical></mat-divider> <mat-divider vertical></mat-divider>
<tb-color-input asBoxInput <tb-color-input asBoxInput

24
ui-ngx/src/app/modules/home/components/widget/config/basic/cards/entities-table-basic-config.component.ts

@ -24,7 +24,7 @@ import {
DataKey, DataKey,
Datasource, Datasource,
datasourcesHasAggregation, datasourcesHasAggregation,
datasourcesHasOnlyComparisonAggregation datasourcesHasOnlyComparisonAggregation, WidgetConfig
} from '@shared/models/widget.models'; } from '@shared/models/widget.models';
import { WidgetConfigComponent } from '@home/components/widget/widget-config.component'; import { WidgetConfigComponent } from '@home/components/widget/widget-config.component';
import { DataKeyType } from '@shared/models/telemetry/telemetry.models'; import { DataKeyType } from '@shared/models/telemetry/telemetry.models';
@ -85,6 +85,7 @@ export class EntitiesTableBasicConfigComponent extends BasicWidgetConfigComponen
showTitleIcon: [configData.config.showTitleIcon, []], showTitleIcon: [configData.config.showTitleIcon, []],
titleIcon: [configData.config.titleIcon, []], titleIcon: [configData.config.titleIcon, []],
iconColor: [configData.config.iconColor, []], iconColor: [configData.config.iconColor, []],
cardButtons: [this.getCardButtons(configData.config), []],
color: [configData.config.color, []], color: [configData.config.color, []],
backgroundColor: [configData.config.backgroundColor, []], backgroundColor: [configData.config.backgroundColor, []],
actions: [configData.config.actions || {}, []] actions: [configData.config.actions || {}, []]
@ -104,6 +105,7 @@ export class EntitiesTableBasicConfigComponent extends BasicWidgetConfigComponen
this.widgetConfig.config.showTitleIcon = config.showTitleIcon; this.widgetConfig.config.showTitleIcon = config.showTitleIcon;
this.widgetConfig.config.titleIcon = config.titleIcon; this.widgetConfig.config.titleIcon = config.titleIcon;
this.widgetConfig.config.iconColor = config.iconColor; this.widgetConfig.config.iconColor = config.iconColor;
this.setCardButtons(config.cardButtons, this.widgetConfig.config);
this.widgetConfig.config.color = config.color; this.widgetConfig.config.color = config.color;
this.widgetConfig.config.backgroundColor = config.backgroundColor; this.widgetConfig.config.backgroundColor = config.backgroundColor;
return this.widgetConfig; return this.widgetConfig;
@ -151,4 +153,24 @@ export class EntitiesTableBasicConfigComponent extends BasicWidgetConfigComponen
} }
} }
private getCardButtons(config: WidgetConfig): string[] {
const buttons: string[] = [];
if (config.settings?.enableSearch) {
buttons.push('search');
}
if (config.settings?.enableSelectColumnDisplay) {
buttons.push('columnsToDisplay');
}
if (config.enableFullscreen) {
buttons.push('fullscreen');
}
return buttons;
}
private setCardButtons(buttons: string[], config: WidgetConfig) {
config.settings.enableSearch = buttons.includes('search');
config.settings.enableSelectColumnDisplay = buttons.includes('columnsToDisplay');
config.enableFullscreen = buttons.includes('fullscreen');
}
} }

16
ui-ngx/src/app/modules/home/components/widget/config/basic/common/data-key-row.component.html

@ -151,6 +151,22 @@
<input matInput formControlName="decimals" type="number" min="0" max="15" step="1" placeholder="{{ 'widget-config.set' | translate }}"> <input matInput formControlName="decimals" type="number" min="0" max="15" step="1" placeholder="{{ 'widget-config.set' | translate }}">
</mat-form-field> </mat-form-field>
</div> </div>
<div class="tb-data-keys-table-row-buttons">
<button type="button"
mat-icon-button
(click)="editKey(true)"
[matTooltip]="keySettingsTitle"
matTooltipPosition="above">
<mat-icon>settings</mat-icon>
</button>
<button type="button"
mat-icon-button
(click)="keyRemoved.emit()"
[matTooltip]="removeKeyTitle"
matTooltipPosition="above">
<mat-icon>delete</mat-icon>
</button>
</div>
</div> </div>
<ng-template #keyName> <ng-template #keyName>
<ng-container *ngIf="dataKeyHasPostprocessing(); else keyName"> <ng-container *ngIf="dataKeyHasPostprocessing(); else keyName">

16
ui-ngx/src/app/modules/home/components/widget/config/basic/common/data-key-row.component.scss

@ -51,3 +51,19 @@
align-items: center; align-items: center;
} }
} }
.tb-data-keys-table-row-buttons {
display: flex;
flex-direction: row;
button.mat-mdc-icon-button.mat-mdc-button-base {
padding: 7px;
width: 38px;
height: 38px;
.mat-icon {
color: rgba(0, 0, 0, 0.38);
}
&.tb-hidden {
visibility: hidden;
}
}
}

29
ui-ngx/src/app/modules/home/components/widget/config/basic/common/data-key-row.component.ts

@ -18,10 +18,12 @@ import {
ChangeDetectorRef, ChangeDetectorRef,
Component, Component,
ElementRef, ElementRef,
EventEmitter,
forwardRef, forwardRef,
Input, Input,
OnChanges, OnChanges,
OnInit, OnInit,
Output,
SimpleChanges, SimpleChanges,
ViewChild, ViewChild,
ViewEncapsulation ViewEncapsulation
@ -37,7 +39,14 @@ import {
} from '@angular/forms'; } from '@angular/forms';
import { MatDialog } from '@angular/material/dialog'; import { MatDialog } from '@angular/material/dialog';
import { WidgetConfigComponent } from '@home/components/widget/widget-config.component'; import { WidgetConfigComponent } from '@home/components/widget/widget-config.component';
import { DataKey, DatasourceType, JsonSettingsSchema, Widget, widgetType } from '@shared/models/widget.models'; import {
DataKey,
DataKeyConfigMode,
DatasourceType,
JsonSettingsSchema,
Widget,
widgetType
} from '@shared/models/widget.models';
import { DataKeysPanelComponent } from '@home/components/widget/config/basic/common/data-keys-panel.component'; import { DataKeysPanelComponent } from '@home/components/widget/config/basic/common/data-keys-panel.component';
import { DataKeyType } from '@shared/models/telemetry/telemetry.models'; import { DataKeyType } from '@shared/models/telemetry/telemetry.models';
import { AggregationType } from '@shared/models/time/time.models'; import { AggregationType } from '@shared/models/time/time.models';
@ -104,6 +113,9 @@ export class DataKeyRowComponent implements ControlValueAccessor, OnInit, OnChan
@Input() @Input()
deviceId: string; deviceId: string;
@Output()
keyRemoved = new EventEmitter();
keyFormControl: UntypedFormControl; keyFormControl: UntypedFormControl;
keyRowFormGroup: UntypedFormGroup; keyRowFormGroup: UntypedFormGroup;
@ -169,6 +181,18 @@ export class DataKeyRowComponent implements ControlValueAccessor, OnInit, OnChan
return this.modelValue.type && ![ DataKeyType.alarm, DataKeyType.entityField, DataKeyType.count ].includes(this.modelValue.type); return this.modelValue.type && ![ DataKeyType.alarm, DataKeyType.entityField, DataKeyType.count ].includes(this.modelValue.type);
} }
get keySettingsTitle(): string {
return this.dataKeysPanelComponent.keySettingsTitle;
}
get removeKeyTitle(): string {
return this.dataKeysPanelComponent.removeKeyTitle;
}
get dragEnabled(): boolean {
return this.dataKeysPanelComponent.dragEnabled;
}
private propagateChange = (_val: any) => {}; private propagateChange = (_val: any) => {};
constructor(private fb: UntypedFormBuilder, constructor(private fb: UntypedFormBuilder,
@ -291,13 +315,14 @@ export class DataKeyRowComponent implements ControlValueAccessor, OnInit, OnChan
} }
} }
editKey() { editKey(advanced = false) {
this.dialog.open<DataKeyConfigDialogComponent, DataKeyConfigDialogData, DataKey>(DataKeyConfigDialogComponent, this.dialog.open<DataKeyConfigDialogComponent, DataKeyConfigDialogData, DataKey>(DataKeyConfigDialogComponent,
{ {
disableClose: true, disableClose: true,
panelClass: ['tb-dialog', 'tb-fullscreen-dialog'], panelClass: ['tb-dialog', 'tb-fullscreen-dialog'],
data: { data: {
dataKey: deepClone(this.modelValue), dataKey: deepClone(this.modelValue),
dataKeyConfigMode: advanced ? DataKeyConfigMode.advanced : DataKeyConfigMode.general,
dataKeySettingsSchema: this.datakeySettingsSchema, dataKeySettingsSchema: this.datakeySettingsSchema,
dataKeySettingsDirective: this.dataKeySettingsDirective, dataKeySettingsDirective: this.dataKeySettingsDirective,
dashboard: this.dashboard, dashboard: this.dashboard,

20
ui-ngx/src/app/modules/home/components/widget/config/basic/common/data-keys-panel.component.html

@ -26,27 +26,25 @@
<div class="tb-data-keys-header-cell tb-decimals-header" translate>widget-config.decimals-short</div> <div class="tb-data-keys-header-cell tb-decimals-header" translate>widget-config.decimals-short</div>
<div class="tb-data-keys-header-cell tb-actions-header"></div> <div class="tb-data-keys-header-cell tb-actions-header"></div>
</div> </div>
<div *ngIf="keysFormArray().controls.length; else noKeys" class="tb-data-keys-body tb-drop-list" cdkDropList cdkDropListOrientation="vertical" <div *ngIf="keysFormArray().controls.length; else noKeys" class="tb-data-keys-body tb-drop-list"
cdkDropList cdkDropListOrientation="vertical"
[cdkDropListDisabled]="!dragEnabled"
(cdkDropListDropped)="keyDrop($event)"> (cdkDropListDropped)="keyDrop($event)">
<div cdkDrag class="tb-data-keys-table-row tb-draggable" *ngFor="let keyControl of keysFormArray().controls; trackBy: trackByKey; <div cdkDrag [cdkDragDisabled]="!dragEnabled"
let $index = index;"> class="tb-data-keys-table-row tb-draggable"
*ngFor="let keyControl of keysFormArray().controls; trackBy: trackByKey; let $index = index;">
<tb-data-key-row fxFlex <tb-data-key-row fxFlex
[formControl]="keyControl" [formControl]="keyControl"
[datasourceType]="datasourceType" [datasourceType]="datasourceType"
[deviceId]="deviceId" [deviceId]="deviceId"
[entityAliasId]="entityAliasId"> [entityAliasId]="entityAliasId"
(keyRemoved)="removeKey($index)">
</tb-data-key-row> </tb-data-key-row>
<div class="tb-data-keys-table-row-buttons"> <div class="tb-data-keys-table-row-buttons">
<button type="button"
mat-icon-button
(click)="removeKey($index)"
[matTooltip]="removeKeyTitle"
matTooltipPosition="above">
<mat-icon>delete</mat-icon>
</button>
<button mat-icon-button <button mat-icon-button
type="button" type="button"
cdkDragHandle cdkDragHandle
[ngClass]="{'tb-hidden': !dragEnabled}"
matTooltip="{{ 'action.drag' | translate }}" matTooltip="{{ 'action.drag' | translate }}"
matTooltipPosition="above"> matTooltipPosition="above">
<mat-icon>drag_indicator</mat-icon> <mat-icon>drag_indicator</mat-icon>

17
ui-ngx/src/app/modules/home/components/widget/config/basic/common/data-keys-panel.component.scss

@ -39,7 +39,7 @@
width: 60px; width: 60px;
} }
&.tb-actions-header { &.tb-actions-header {
width: 76px; width: 114px;
} }
} }
} }
@ -57,20 +57,5 @@
height: 38px; height: 38px;
display: flex; display: flex;
flex-direction: row; flex-direction: row;
gap: 12px;
background: #fff; background: #fff;
.tb-data-keys-table-row-buttons {
display: flex;
flex-direction: row;
button.mat-mdc-icon-button.mat-mdc-button-base {
padding: 7px;
width: 38px;
height: 38px;
.mat-icon {
color: rgba(0, 0, 0, 0.38);
}
}
}
} }

7
ui-ngx/src/app/modules/home/components/widget/config/basic/common/data-keys-panel.component.ts

@ -75,6 +75,9 @@ export class DataKeysPanelComponent implements ControlValueAccessor, OnInit, OnC
@Input() @Input()
addKeyTitle: string; addKeyTitle: string;
@Input()
keySettingsTitle: string;
@Input() @Input()
removeKeyTitle: string; removeKeyTitle: string;
@ -112,6 +115,10 @@ export class DataKeysPanelComponent implements ControlValueAccessor, OnInit, OnC
return this.widgetConfigComponent.modelValue?.dataKeySettingsSchema; return this.widgetConfigComponent.modelValue?.dataKeySettingsSchema;
} }
get dragEnabled(): boolean {
return this.keysFormArray().controls.length > 1;
}
private propagateChange = (_val: any) => {}; private propagateChange = (_val: any) => {};
constructor(private fb: UntypedFormBuilder, constructor(private fb: UntypedFormBuilder,

7
ui-ngx/src/app/modules/home/components/widget/config/data-key-config-dialog.component.html

@ -19,6 +19,10 @@
<mat-toolbar color="primary"> <mat-toolbar color="primary">
<h2>{{ 'datakey.configuration' | translate }}</h2> <h2>{{ 'datakey.configuration' | translate }}</h2>
<span fxFlex></span> <span fxFlex></span>
<tb-toggle-header *ngIf="hasAdvanced" (valueChange)="dataKeyConfigMode = $event" ignoreMdLgSize="true"
appearance="fill-invert" [options]="dataKeyConfigHeaderOptions"
[value]="dataKeyConfigMode" name="dataKeyConfigModeHeader" useSelectOnMdLg="false">
</tb-toggle-header>
<button mat-icon-button <button mat-icon-button
(click)="cancel()" (click)="cancel()"
type="button"> type="button">
@ -28,8 +32,9 @@
<mat-progress-bar color="warn" mode="indeterminate" *ngIf="isLoading$ | async"> <mat-progress-bar color="warn" mode="indeterminate" *ngIf="isLoading$ | async">
</mat-progress-bar> </mat-progress-bar>
<div style="height: 4px;" *ngIf="!(isLoading$ | async)"></div> <div style="height: 4px;" *ngIf="!(isLoading$ | async)"></div>
<div mat-dialog-content> <div mat-dialog-content style="padding: 0; height: 800px; overflow: auto;">
<tb-data-key-config #dataKeyConfig <tb-data-key-config #dataKeyConfig
[dataKeyConfigMode]="dataKeyConfigMode"
[dataKeySettingsSchema]="data.dataKeySettingsSchema" [dataKeySettingsSchema]="data.dataKeySettingsSchema"
[dataKeySettingsDirective]="data.dataKeySettingsDirective" [dataKeySettingsDirective]="data.dataKeySettingsDirective"
[deviceId]="data.deviceId" [deviceId]="data.deviceId"

38
ui-ngx/src/app/modules/home/components/widget/config/data-key-config-dialog.component.ts

@ -19,17 +19,27 @@ import { ErrorStateMatcher } from '@angular/material/core';
import { MAT_DIALOG_DATA, MatDialogRef } from '@angular/material/dialog'; import { MAT_DIALOG_DATA, MatDialogRef } from '@angular/material/dialog';
import { Store } from '@ngrx/store'; import { Store } from '@ngrx/store';
import { AppState } from '@core/core.state'; import { AppState } from '@core/core.state';
import { UntypedFormBuilder, UntypedFormControl, UntypedFormGroup, FormGroupDirective, NgForm, Validators } from '@angular/forms'; import {
FormGroupDirective,
NgForm,
UntypedFormBuilder,
UntypedFormControl,
UntypedFormGroup,
Validators
} from '@angular/forms';
import { Router } from '@angular/router'; import { Router } from '@angular/router';
import { DialogComponent } from '@shared/components/dialog.component'; import { DialogComponent } from '@shared/components/dialog.component';
import { DataKey, Widget, widgetType } from '@shared/models/widget.models'; import { DataKey, DataKeyConfigMode, Widget, widgetType } from '@shared/models/widget.models';
import { DataKeysCallbacks } from './data-keys.component.models'; import { DataKeysCallbacks } from './data-keys.component.models';
import { DataKeyConfigComponent } from '@home/components/widget/config/data-key-config.component'; import { DataKeyConfigComponent } from '@home/components/widget/config/data-key-config.component';
import { Dashboard } from '@shared/models/dashboard.models'; import { Dashboard } from '@shared/models/dashboard.models';
import { IAliasController } from '@core/api/widget-api.models'; import { IAliasController } from '@core/api/widget-api.models';
import { ToggleHeaderOption } from '@shared/components/toggle-header.component';
import { TranslateService } from '@ngx-translate/core';
export interface DataKeyConfigDialogData { export interface DataKeyConfigDialogData {
dataKey: DataKey; dataKey: DataKey;
dataKeyConfigMode?: DataKeyConfigMode;
dataKeySettingsSchema: any; dataKeySettingsSchema: any;
dataKeySettingsDirective: string; dataKeySettingsDirective: string;
dashboard: Dashboard; dashboard: Dashboard;
@ -57,6 +67,12 @@ export class DataKeyConfigDialogComponent extends DialogComponent<DataKeyConfigD
@ViewChild('dataKeyConfig', {static: true}) dataKeyConfig: DataKeyConfigComponent; @ViewChild('dataKeyConfig', {static: true}) dataKeyConfig: DataKeyConfigComponent;
hasAdvanced = false;
dataKeyConfigHeaderOptions: ToggleHeaderOption[];
dataKeyConfigMode: DataKeyConfigMode = DataKeyConfigMode.general;
dataKeyFormGroup: UntypedFormGroup; dataKeyFormGroup: UntypedFormGroup;
submitted = false; submitted = false;
@ -66,6 +82,7 @@ export class DataKeyConfigDialogComponent extends DialogComponent<DataKeyConfigD
@Inject(MAT_DIALOG_DATA) public data: DataKeyConfigDialogData, @Inject(MAT_DIALOG_DATA) public data: DataKeyConfigDialogData,
@SkipSelf() private errorStateMatcher: ErrorStateMatcher, @SkipSelf() private errorStateMatcher: ErrorStateMatcher,
public dialogRef: MatDialogRef<DataKeyConfigDialogComponent, DataKey>, public dialogRef: MatDialogRef<DataKeyConfigDialogComponent, DataKey>,
private translate: TranslateService,
public fb: UntypedFormBuilder) { public fb: UntypedFormBuilder) {
super(store, router, dialogRef); super(store, router, dialogRef);
} }
@ -74,6 +91,23 @@ export class DataKeyConfigDialogComponent extends DialogComponent<DataKeyConfigD
this.dataKeyFormGroup = this.fb.group({ this.dataKeyFormGroup = this.fb.group({
dataKey: [this.data.dataKey, [Validators.required]] dataKey: [this.data.dataKey, [Validators.required]]
}); });
if (this.data.dataKeySettingsSchema && this.data.dataKeySettingsSchema.schema ||
this.data.dataKeySettingsDirective && this.data.dataKeySettingsDirective.length) {
this.hasAdvanced = true;
this.dataKeyConfigHeaderOptions = [
{
name: this.translate.instant('datakey.general'),
value: DataKeyConfigMode.general
},
{
name: this.translate.instant('datakey.advanced'),
value: DataKeyConfigMode.advanced
}
];
if (this.data.dataKeyConfigMode) {
this.dataKeyConfigMode = this.data.dataKeyConfigMode;
}
}
} }
isErrorState(control: UntypedFormControl | null, form: FormGroupDirective | NgForm | null): boolean { isErrorState(control: UntypedFormControl | null, form: FormGroupDirective | NgForm | null): boolean {

134
ui-ngx/src/app/modules/home/components/widget/config/data-key-config.component.html

@ -15,10 +15,12 @@
limitations under the License. limitations under the License.
--> -->
<mat-tab-group class="tb-datakey-config" [ngClass]="{'tb-headless': !displayAdvanced}"> <div class="tb-datakey-config" *ngIf="dataKeyConfigMode === dataKeyConfigModes.general; else advanced"
<mat-tab [formGroup]="dataKeyFormGroup" label="{{ 'datakey.settings' | translate }}"> [formGroup]="dataKeyFormGroup">
<div class="mat-padding settings" fxLayout="column"> <div class="mat-padding" fxLayout="column" style="gap: 16px;">
<mat-form-field class="mat-block" *ngIf="modelValue.type !== dataKeyTypes.function"> <div class="tb-widget-config-panel">
<div class="tb-widget-config-panel-title" translate>datakey.general</div>
<mat-form-field *ngIf="modelValue.type !== dataKeyTypes.function" subscriptSizing="dynamic">
<mat-label>{{ 'entity.key' | translate }}</mat-label> <mat-label>{{ 'entity.key' | translate }}</mat-label>
<input matInput type="text" placeholder="{{ 'entity.key-name' | translate }}" <input matInput type="text" placeholder="{{ 'entity.key-name' | translate }}"
#keyInput #keyInput
@ -39,31 +41,35 @@
</mat-option> </mat-option>
</mat-autocomplete> </mat-autocomplete>
</mat-form-field> </mat-form-field>
<div fxLayout="row" fxLayout.xs="column" fxLayoutAlign="start center" fxLayoutAlign.xs fxLayoutGap="8px"> <mat-form-field *ngIf="!hideDataKeyLabel" subscriptSizing="dynamic">
<mat-form-field *ngIf="!hideDataKeyLabel" fxFlex class="mat-block"> <mat-label translate>datakey.label</mat-label>
<mat-label translate>datakey.label</mat-label> <input matInput formControlName="label" required>
<input matInput formControlName="label" required> </mat-form-field>
</mat-form-field> <ng-container *ngIf="modelValue.type !== dataKeyTypes.alarm">
<tb-color-input *ngIf="!hideDataKeyColor" fxFlex <div class="tb-widget-config-row space-between" *ngIf="!hideDataKeyUnits">
required <div translate>widget-config.units-short</div>
label="{{'datakey.color' | translate}}" <tb-widget-units
icon="format_color_fill" formControlName="units">
openOnInput </tb-widget-units>
formControlName="color"> </div>
</tb-color-input> <div class="tb-widget-config-row space-between" *ngIf="!hideDataKeyDecimals">
</div> <div translate>widget-config.decimals-short</div>
<div fxLayout="row" fxLayout.xs="column" fxLayoutAlign="start center" fxLayoutAlign.xs fxLayoutGap="8px" *ngIf="modelValue.type !== dataKeyTypes.alarm"> <mat-form-field appearance="outline" class="center number" subscriptSizing="dynamic">
<mat-form-field *ngIf="!hideDataKeyUnits" fxFlex> <input matInput formControlName="decimals" type="number" min="0" max="15" step="1" placeholder="{{ 'widget-config.set' | translate }}">
<mat-label translate>datakey.units</mat-label> </mat-form-field>
<input matInput formControlName="units"> </div>
</mat-form-field> </ng-container>
<mat-form-field *ngIf="!hideDataKeyDecimals" fxFlex> <div class="tb-widget-config-row space-between same-padding" *ngIf="!hideDataKeyColor">
<mat-label translate>datakey.decimals</mat-label> <div>{{ 'datakey.color' | translate }}</div>
<input matInput formControlName="decimals" type="number" min="0" max="15" step="1"> <div fxLayout="row" fxLayoutAlign="start center" fxLayoutGap="16px">
</mat-form-field> <mat-divider vertical></mat-divider>
<tb-color-input asBoxInput
formControlName="color">
</tb-color-input>
</div>
</div> </div>
<section *ngIf="widgetType === widgetTypes.latest && modelValue.type === dataKeyTypes.timeseries" fxLayout="column"> <ng-container *ngIf="widgetType === widgetTypes.latest && modelValue.type === dataKeyTypes.timeseries">
<mat-form-field> <mat-form-field subscriptSizing="dynamic">
<mat-label translate>datakey.aggregation</mat-label> <mat-label translate>datakey.aggregation</mat-label>
<mat-select formControlName="aggregationType" style="min-width: 150px;"> <mat-select formControlName="aggregationType" style="min-width: 150px;">
<mat-option *ngFor="let aggregation of aggregations" [value]="aggregation"> <mat-option *ngFor="let aggregation of aggregations" [value]="aggregation">
@ -80,7 +86,7 @@
<fieldset *ngIf="dataKeyFormGroup.get('aggregationType').value && dataKeyFormGroup.get('aggregationType').value !== aggregationTypes.NONE" class="fields-group fields-group-slider"> <fieldset *ngIf="dataKeyFormGroup.get('aggregationType').value && dataKeyFormGroup.get('aggregationType').value !== aggregationTypes.NONE" class="fields-group fields-group-slider">
<legend class="group-title" translate>datakey.delta-calculation</legend> <legend class="group-title" translate>datakey.delta-calculation</legend>
<mat-expansion-panel class="tb-settings comparison" [expanded]="dataKeyFormGroup.get('comparisonEnabled').value" [disabled]="!dataKeyFormGroup.get('comparisonEnabled').value"> <mat-expansion-panel class="tb-settings comparison" [expanded]="dataKeyFormGroup.get('comparisonEnabled').value" [disabled]="!dataKeyFormGroup.get('comparisonEnabled').value">
<mat-expansion-panel-header fxLayout="row wrap"> <mat-expansion-panel-header fxLayout="row wrap" class="fill-width">
<mat-panel-title fxLayout="column" fxLayoutAlign="center start"> <mat-panel-title fxLayout="column" fxLayoutAlign="center start">
<mat-slide-toggle formControlName="comparisonEnabled" (click)="$event.stopPropagation()"> <mat-slide-toggle formControlName="comparisonEnabled" (click)="$event.stopPropagation()">
{{ 'datakey.enable-delta-calculation' | translate }} {{ 'datakey.enable-delta-calculation' | translate }}
@ -129,42 +135,54 @@
</ng-template> </ng-template>
</mat-expansion-panel> </mat-expansion-panel>
</fieldset> </fieldset>
</section> </ng-container>
<section fxLayout="column" *ngIf="modelValue.type === dataKeyTypes.function"> <section fxLayout="column" *ngIf="modelValue.type === dataKeyTypes.function">
<span translate>datakey.data-generation-func</span> <span translate>datakey.data-generation-func</span>
<br/> <br/>
<tb-js-func #funcBodyEdit <tb-js-func #funcBodyEdit
[functionArgs]="['time', 'prevValue']" [functionArgs]="['time', 'prevValue']"
[globalVariables]="functionScopeVariables"
[validationArgs]="[[1, 1],[1, '1']]"
resultType="any"
helpId="widget/config/datakey_generation_fn"
formControlName="funcBody">
</tb-js-func>
</section>
<section fxLayout="column" *ngIf="(modelValue.type === dataKeyTypes.timeseries || modelValue.type === dataKeyTypes.attribute || modelValue.type === dataKeyTypes.count) && showPostProcessing">
<mat-checkbox formControlName="usePostProcessing">
{{ 'datakey.use-data-post-processing-func' | translate }}
</mat-checkbox>
<tb-js-func *ngIf="dataKeyFormGroup.get('usePostProcessing').value" #postFuncBodyEdit
[functionArgs]="['time', 'value', 'prevValue', 'timePrev', 'prevOrigValue']"
[globalVariables]="functionScopeVariables" [globalVariables]="functionScopeVariables"
[validationArgs]="[[1, 1, 1, 1, 1],[1, '1', '1', 1, '1']]" [validationArgs]="[[1, 1],[1, '1']]"
resultType="any" resultType="any"
helpId="widget/config/datakey_postprocess_fn" helpId="widget/config/datakey_generation_fn"
formControlName="postFuncBody"> formControlName="funcBody">
</tb-js-func> </tb-js-func>
<label *ngIf="dataKeyFormGroup.get('usePostProcessing').value" class="tb-title" style="margin-left: 15px;">
time - {{ 'datakey.time-description' | translate }}<br/>
value - {{ 'datakey.value-description' | translate }}<br/>
prevValue - {{ 'datakey.prev-value-description' | translate }}<br/>
timePrev - {{ 'datakey.time-prev-description' | translate }}<br/>
prevOrigValue - {{ 'datakey.prev-orig-value-description' | translate }}
</label>
</section> </section>
</div> </div>
</mat-tab> <div class="tb-widget-config-panel tb-slide-toggle"
<mat-tab [formGroup]="dataKeySettingsFormGroup" label="{{ 'datakey.advanced' | translate }}" *ngIf="displayAdvanced"> *ngIf="(modelValue.type === dataKeyTypes.timeseries || modelValue.type === dataKeyTypes.attribute || modelValue.type === dataKeyTypes.count) && showPostProcessing">
<mat-expansion-panel class="tb-settings" [expanded]="dataKeyFormGroup.get('usePostProcessing').value" [disabled]="!dataKeyFormGroup.get('usePostProcessing').value">
<mat-expansion-panel-header fxLayout="row wrap">
<mat-panel-title>
<mat-slide-toggle class="mat-slide" formControlName="usePostProcessing" (click)="$event.stopPropagation()"
fxLayoutAlign="center">
{{ 'datakey.use-data-post-processing-func' | translate }}
</mat-slide-toggle>
</mat-panel-title>
</mat-expansion-panel-header>
<ng-template matExpansionPanelContent>
<tb-js-func *ngIf="dataKeyFormGroup.get('usePostProcessing').value" #postFuncBodyEdit
[functionArgs]="['time', 'value', 'prevValue', 'timePrev', 'prevOrigValue']"
[globalVariables]="functionScopeVariables"
[validationArgs]="[[1, 1, 1, 1, 1],[1, '1', '1', 1, '1']]"
resultType="any"
helpId="widget/config/datakey_postprocess_fn"
formControlName="postFuncBody">
</tb-js-func>
<label *ngIf="dataKeyFormGroup.get('usePostProcessing').value" class="tb-title" style="margin-left: 15px;">
time - {{ 'datakey.time-description' | translate }}<br/>
value - {{ 'datakey.value-description' | translate }}<br/>
prevValue - {{ 'datakey.prev-value-description' | translate }}<br/>
timePrev - {{ 'datakey.time-prev-description' | translate }}<br/>
prevOrigValue - {{ 'datakey.prev-orig-value-description' | translate }}
</label>
</ng-template>
</mat-expansion-panel>
</div>
</div>
</div>
<ng-template #advanced>
<div class="tb-datakey-config" [formGroup]="dataKeySettingsFormGroup">
<div class="mat-padding" fxLayout="column"> <div class="mat-padding" fxLayout="column">
<tb-widget-settings <tb-widget-settings
[dashboard]="dashboard" [dashboard]="dashboard"
@ -173,5 +191,5 @@
formControlName="settings"> formControlName="settings">
</tb-widget-settings> </tb-widget-settings>
</div> </div>
</mat-tab> </div>
</mat-tab-group> </ng-template>

23
ui-ngx/src/app/modules/home/components/widget/config/data-key-config.component.scss

@ -17,18 +17,6 @@
:host { :host {
.tb-datakey-config { .tb-datakey-config {
height: 100%;
.settings{
&> :first-child{
padding-top: 12px;
}
&> :nth-child(n){
padding-right: 12px;
padding-left: 12px;
}
}
.fields-group { .fields-group {
padding: 0 16px 8px; padding: 0 16px 8px;
margin-bottom: 10px; margin-bottom: 10px;
@ -60,7 +48,7 @@
} }
.tb-hint.after-fields { .tb-hint.after-fields {
margin-top: -1.25em; margin-top: -0.75em;
max-width: fit-content; max-width: fit-content;
line-height: 15px; line-height: 15px;
} }
@ -69,15 +57,6 @@
:host ::ng-deep { :host ::ng-deep {
.tb-datakey-config { .tb-datakey-config {
.mat-mdc-tab-body.mat-mdc-tab-body-active {
.mat-mdc-tab-body-content > div {
height: calc(65vh - 50px);
overflow: initial;
@media #{$mat-xs} {
height: 500px;
}
}
}
.mat-expansion-panel { .mat-expansion-panel {
&.tb-settings { &.tb-settings {
box-shadow: none; box-shadow: none;

17
ui-ngx/src/app/modules/home/components/widget/config/data-key-config.component.ts

@ -22,7 +22,7 @@ import {
ComparisonResultType, ComparisonResultType,
comparisonResultTypeTranslationMap, comparisonResultTypeTranslationMap,
DataKey, DataKey,
dataKeyAggregationTypeHintTranslationMap, dataKeyAggregationTypeHintTranslationMap, DataKeyConfigMode,
Widget, Widget,
widgetType widgetType
} from '@shared/models/widget.models'; } from '@shared/models/widget.models';
@ -73,6 +73,8 @@ import { coerceBoolean } from '@shared/decorators/coercion';
}) })
export class DataKeyConfigComponent extends PageComponent implements OnInit, ControlValueAccessor, Validator { export class DataKeyConfigComponent extends PageComponent implements OnInit, ControlValueAccessor, Validator {
dataKeyConfigModes = DataKeyConfigMode;
dataKeyTypes = DataKeyType; dataKeyTypes = DataKeyType;
widgetTypes = widgetType; widgetTypes = widgetType;
@ -91,6 +93,9 @@ export class DataKeyConfigComponent extends PageComponent implements OnInit, Con
comparisonResultTypeTranslations = comparisonResultTypeTranslationMap; comparisonResultTypeTranslations = comparisonResultTypeTranslationMap;
@Input()
dataKeyConfigMode: DataKeyConfigMode;
@Input() @Input()
deviceId: string; deviceId: string;
@ -142,7 +147,7 @@ export class DataKeyConfigComponent extends PageComponent implements OnInit, Con
@ViewChild('funcBodyEdit') funcBodyEdit: JsFuncComponent; @ViewChild('funcBodyEdit') funcBodyEdit: JsFuncComponent;
@ViewChild('postFuncBodyEdit') postFuncBodyEdit: JsFuncComponent; @ViewChild('postFuncBodyEdit') postFuncBodyEdit: JsFuncComponent;
displayAdvanced = false; hasAdvanced = false;
modelValue: DataKey; modelValue: DataKey;
@ -193,7 +198,7 @@ export class DataKeyConfigComponent extends PageComponent implements OnInit, Con
} }
if (this.dataKeySettingsSchema && this.dataKeySettingsSchema.schema || if (this.dataKeySettingsSchema && this.dataKeySettingsSchema.schema ||
this.dataKeySettingsDirective && this.dataKeySettingsDirective.length) { this.dataKeySettingsDirective && this.dataKeySettingsDirective.length) {
this.displayAdvanced = true; this.hasAdvanced = true;
this.dataKeySettingsData = { this.dataKeySettingsData = {
schema: this.dataKeySettingsSchema?.schema || { schema: this.dataKeySettingsSchema?.schema || {
type: 'object', type: 'object',
@ -291,7 +296,7 @@ export class DataKeyConfigComponent extends PageComponent implements OnInit, Con
} }
this.dataKeyFormGroup.patchValue(this.modelValue, {emitEvent: false}); this.dataKeyFormGroup.patchValue(this.modelValue, {emitEvent: false});
this.updateValidators(); this.updateValidators();
if (this.displayAdvanced) { if (this.hasAdvanced) {
this.dataKeySettingsData.model = this.modelValue.settings; this.dataKeySettingsData.model = this.modelValue.settings;
this.dataKeySettingsFormGroup.patchValue({ this.dataKeySettingsFormGroup.patchValue({
settings: this.dataKeySettingsData settings: this.dataKeySettingsData
@ -370,7 +375,7 @@ export class DataKeyConfigComponent extends PageComponent implements OnInit, Con
private updateModel() { private updateModel() {
this.modelValue = {...this.modelValue, ...this.dataKeyFormGroup.value}; this.modelValue = {...this.modelValue, ...this.dataKeyFormGroup.value};
if (this.displayAdvanced) { if (this.hasAdvanced) {
this.modelValue.settings = this.dataKeySettingsFormGroup.get('settings').value.model; this.modelValue.settings = this.dataKeySettingsFormGroup.get('settings').value.model;
} }
if (this.modelValue.name) { if (this.modelValue.name) {
@ -449,7 +454,7 @@ export class DataKeyConfigComponent extends PageComponent implements OnInit, Con
} }
}; };
} }
if (this.displayAdvanced && (!this.dataKeySettingsFormGroup.valid || !this.modelValue.settings)) { if (this.hasAdvanced && (!this.dataKeySettingsFormGroup.valid || !this.modelValue.settings)) {
return { return {
dataKeySettings: { dataKeySettings: {
valid: false valid: false

99
ui-ngx/src/app/modules/home/components/widget/lib/settings/cards/entities-table-key-settings.component.html

@ -15,21 +15,56 @@
limitations under the License. limitations under the License.
--> -->
<section class="tb-widget-settings" [formGroup]="entitiesTableKeySettingsForm" fxLayout="column"> <ng-container [formGroup]="entitiesTableKeySettingsForm">
<mat-form-field fxFlex class="mat-block"> <div class="tb-widget-config-panel">
<mat-label translate>widgets.table.custom-title</mat-label> <div class="tb-widget-config-panel-title" translate>widgets.table.column-settings</div>
<input matInput formControlName="customTitle"> <div class="tb-widget-config-row">
</mat-form-field> <div>{{ 'widgets.table.custom-title' | translate }}</div>
<mat-form-field fxFlex class="mat-block"> <mat-form-field fxFlex appearance="outline" subscriptSizing="dynamic">
<mat-label translate>widgets.table.column-width</mat-label> <input matInput formControlName="customTitle" placeholder="{{ 'widget-config.set' | translate }}">
<input matInput formControlName="columnWidth"> </mat-form-field>
</mat-form-field> </div>
<fieldset class="fields-group fields-group-slider"> <div class="tb-widget-config-row space-between">
<legend class="group-title" translate>widgets.table.cell-style</legend> <div>{{ 'widgets.table.column-width' | translate }}</div>
<mat-form-field appearance="outline" class="center" subscriptSizing="dynamic">
<input matInput formControlName="columnWidth" placeholder="{{ 'widget-config.set' | translate }}">
</mat-form-field>
</div>
<div class="tb-widget-config-row space-between">
<div>{{ 'widgets.table.default-column-visibility' | translate }}</div>
<mat-form-field style="width: 220px;" appearance="outline" subscriptSizing="dynamic">
<mat-select formControlName="defaultColumnVisibility">
<mat-option [value]="'visible'">
{{ 'widgets.table.column-visibility-visible' | translate }}
</mat-option>
<mat-option [value]="'hidden'">
{{ 'widgets.table.column-visibility-hidden' | translate }}
</mat-option>
<mat-option [value]="'hidden-mobile'">
{{ 'widgets.table.column-visibility-hidden-mobile' | translate }}
</mat-option>
</mat-select>
</mat-form-field>
</div>
<div class="tb-widget-config-row space-between">
<div>{{ 'widgets.table.column-selection-to-display' | translate }}</div>
<mat-form-field style="width: 220px;" appearance="outline" subscriptSizing="dynamic">
<mat-select formControlName="columnSelectionToDisplay">
<mat-option [value]="'enabled'">
{{ 'widgets.table.column-selection-to-display-enabled' | translate }}
</mat-option>
<mat-option [value]="'disabled'">
{{ 'widgets.table.column-selection-to-display-disabled' | translate }}
</mat-option>
</mat-select>
</mat-form-field>
</div>
</div>
<div class="tb-widget-config-panel tb-slide-toggle">
<mat-expansion-panel class="tb-settings" [expanded]="entitiesTableKeySettingsForm.get('useCellStyleFunction').value"> <mat-expansion-panel class="tb-settings" [expanded]="entitiesTableKeySettingsForm.get('useCellStyleFunction').value">
<mat-expansion-panel-header fxLayout="row wrap"> <mat-expansion-panel-header fxLayout="row wrap" class="fill-width">
<mat-panel-title fxFlex="60"> <mat-panel-title fxFlex="60">
<mat-slide-toggle formControlName="useCellStyleFunction" (click)="$event.stopPropagation()" <mat-slide-toggle class="mat-slide" formControlName="useCellStyleFunction" (click)="$event.stopPropagation()"
fxLayoutAlign="center"> fxLayoutAlign="center">
{{ 'widgets.table.use-cell-style-function' | translate }} {{ 'widgets.table.use-cell-style-function' | translate }}
</mat-slide-toggle> </mat-slide-toggle>
@ -48,13 +83,12 @@
</tb-js-func> </tb-js-func>
</ng-template> </ng-template>
</mat-expansion-panel> </mat-expansion-panel>
</fieldset> </div>
<fieldset class="fields-group fields-group-slider"> <div class="tb-widget-config-panel tb-slide-toggle">
<legend class="group-title" translate>widgets.table.cell-content</legend>
<mat-expansion-panel class="tb-settings" [expanded]="entitiesTableKeySettingsForm.get('useCellContentFunction').value"> <mat-expansion-panel class="tb-settings" [expanded]="entitiesTableKeySettingsForm.get('useCellContentFunction').value">
<mat-expansion-panel-header fxLayout="row wrap"> <mat-expansion-panel-header fxLayout="row wrap" class="fill-width">
<mat-panel-title fxFlex="60"> <mat-panel-title fxFlex="60">
<mat-slide-toggle formControlName="useCellContentFunction" (click)="$event.stopPropagation()" <mat-slide-toggle class="mat-slide" formControlName="useCellContentFunction" (click)="$event.stopPropagation()"
fxLayoutAlign="center"> fxLayoutAlign="center">
{{ 'widgets.table.use-cell-content-function' | translate }} {{ 'widgets.table.use-cell-content-function' | translate }}
</mat-slide-toggle> </mat-slide-toggle>
@ -73,30 +107,5 @@
</tb-js-func> </tb-js-func>
</ng-template> </ng-template>
</mat-expansion-panel> </mat-expansion-panel>
</fieldset> </div>
<mat-form-field fxFlex class="mat-block"> </ng-container>
<mat-label translate>widgets.table.default-column-visibility</mat-label>
<mat-select formControlName="defaultColumnVisibility">
<mat-option [value]="'visible'">
{{ 'widgets.table.column-visibility-visible' | translate }}
</mat-option>
<mat-option [value]="'hidden'">
{{ 'widgets.table.column-visibility-hidden' | translate }}
</mat-option>
<mat-option [value]="'hidden-mobile'">
{{ 'widgets.table.column-visibility-hidden-mobile' | translate }}
</mat-option>
</mat-select>
</mat-form-field>
<mat-form-field fxFlex class="mat-block">
<mat-label translate>widgets.table.column-selection-to-display</mat-label>
<mat-select formControlName="columnSelectionToDisplay">
<mat-option [value]="'enabled'">
{{ 'widgets.table.column-selection-to-display-enabled' | translate }}
</mat-option>
<mat-option [value]="'disabled'">
{{ 'widgets.table.column-selection-to-display-disabled' | translate }}
</mat-option>
</mat-select>
</mat-form-field>
</section>

147
ui-ngx/src/app/modules/home/components/widget/lib/settings/cards/entities-table-widget-settings.component.html

@ -15,32 +15,59 @@
limitations under the License. limitations under the License.
--> -->
<section class="tb-widget-settings" [formGroup]="entitiesTableWidgetSettingsForm" fxLayout="column"> <ng-container [formGroup]="entitiesTableWidgetSettingsForm">
<fieldset class="fields-group"> <div class="tb-widget-config-panel">
<legend class="group-title" translate>widgets.table.common-table-settings</legend> <div class="tb-widget-config-panel-title" translate>widgets.table.table-header</div>
<mat-form-field fxFlex class="mat-block"> <div class="tb-widget-config-row">
<mat-label translate>widgets.table.entities-table-title</mat-label> <div>{{ 'widgets.table.entities-table-title' | translate }}</div>
<input matInput formControlName="entitiesTitle"> <mat-form-field fxFlex appearance="outline" subscriptSizing="dynamic">
</mat-form-field> <input matInput formControlName="entitiesTitle" placeholder="{{ 'widget-config.set' | translate }}">
<section fxLayout="column" fxLayout.gt-xs="row" fxLayoutGap.gt-xs="8px"> </mat-form-field>
<section fxLayout="column" fxFlex> </div>
<mat-checkbox formControlName="enableSearch"> <div class="tb-widget-config-row">
{{ 'widgets.table.enable-search' | translate }} <mat-slide-toggle class="mat-slide" formControlName="enableStickyHeader">
</mat-checkbox> {{ 'widgets.table.enable-sticky-header' | translate }}
<mat-checkbox formControlName="enableSelectColumnDisplay"> </mat-slide-toggle>
{{ 'widgets.table.enable-select-column-display' | translate }} </div>
</mat-checkbox> <div class="tb-widget-config-panel stroked">
</section> <div class="tb-widget-config-panel-title" translate>widgets.table.header-buttons</div>
<section fxLayout="column" fxFlex> <mat-slide-toggle class="mat-slide no-margin" formControlName="enableSearch">
<mat-checkbox formControlName="enableStickyHeader"> {{ 'widgets.table.enable-search' | translate }}
{{ 'widgets.table.enable-sticky-header' | translate }} </mat-slide-toggle>
</mat-checkbox> <mat-slide-toggle class="mat-slide no-margin" formControlName="enableSelectColumnDisplay">
<mat-checkbox formControlName="enableStickyAction"> {{ 'widgets.table.enable-select-column-display' | translate }}
{{ 'widgets.table.enable-sticky-action' | translate }} </mat-slide-toggle>
</mat-checkbox> </div>
</section> </div>
</section> <div class="tb-widget-config-panel">
<mat-form-field fxFlex class="mat-block"> <div class="tb-widget-config-panel-title" translate>widgets.table.columns</div>
<div class="tb-widget-config-row">
<mat-slide-toggle class="mat-slide" formControlName="displayEntityName">
{{ 'widgets.table.display-entity-name' | translate }}
</mat-slide-toggle>
<mat-form-field fxFlex appearance="outline" subscriptSizing="dynamic">
<input matInput formControlName="entityNameColumnTitle" placeholder="{{ 'widget-config.set' | translate }}">
</mat-form-field>
</div>
<div class="tb-widget-config-row">
<mat-slide-toggle class="mat-slide" formControlName="displayEntityName">
{{ 'widgets.table.display-entity-label' | translate }}
</mat-slide-toggle>
<mat-form-field fxFlex appearance="outline" subscriptSizing="dynamic">
<input matInput formControlName="entityLabelColumnTitle" placeholder="{{ 'widget-config.set' | translate }}">
</mat-form-field>
</div>
<div class="tb-widget-config-row">
<mat-slide-toggle class="mat-slide" formControlName="displayEntityType">
{{ 'widgets.table.display-entity-type' | translate }}
</mat-slide-toggle>
</div>
<div class="tb-widget-config-row">
<mat-slide-toggle class="mat-slide" formControlName="enableStickyAction">
{{ 'widgets.table.enable-sticky-action' | translate }}
</mat-slide-toggle>
</div>
<mat-form-field fxFlex subscriptSizing="dynamic">
<mat-label translate>widgets.table.hidden-cell-button-display-mode</mat-label> <mat-label translate>widgets.table.hidden-cell-button-display-mode</mat-label>
<mat-select formControlName="reserveSpaceForHiddenAction"> <mat-select formControlName="reserveSpaceForHiddenAction">
<mat-option [value]="'true'"> <mat-option [value]="'true'">
@ -51,54 +78,34 @@
</mat-option> </mat-option>
</mat-select> </mat-select>
</mat-form-field> </mat-form-field>
<section fxLayout="column" fxLayoutGap.gt-xs="8px"> <mat-form-field fxFlex subscriptSizing="dynamic">
<section fxLayout="column" fxLayout.gt-xs="row" fxLayoutGap="8px" fxLayoutAlign.gt-xs="start center"> <mat-label translate>widgets.table.default-sort-order</mat-label>
<mat-slide-toggle fxFlex formControlName="displayEntityName"> <input matInput formControlName="defaultSortOrder">
{{ 'widgets.table.display-entity-name' | translate }} </mat-form-field>
</mat-slide-toggle> </div>
<mat-form-field fxFlex class="mat-block"> <div class="tb-widget-config-panel">
<mat-label translate>widgets.table.entity-name-column-title</mat-label> <div class="tb-widget-config-panel-title" translate>widgets.table.pagination</div>
<input matInput formControlName="entityNameColumnTitle"> <mat-slide-toggle class="mat-slide no-margin" formControlName="displayPagination">
</mat-form-field> {{ 'widgets.table.display-pagination' | translate }}
</section> </mat-slide-toggle>
<section fxLayout="column" fxLayout.gt-xs="row" fxLayoutGap="8px" fxLayoutAlign.gt-xs="start center"> <div class="tb-widget-config-row space-between">
<mat-slide-toggle fxFlex formControlName="displayEntityLabel"> <div translate>widgets.table.default-page-size</div>
{{ 'widgets.table.display-entity-label' | translate }} <mat-form-field appearance="outline" class="center number" subscriptSizing="dynamic">
</mat-slide-toggle> <input matInput formControlName="defaultPageSize" type="number" min="1" step="1" placeholder="{{ 'widget-config.set' | translate }}">
<mat-form-field fxFlex class="mat-block">
<mat-label translate>widgets.table.entity-label-column-title</mat-label>
<input matInput formControlName="entityLabelColumnTitle">
</mat-form-field>
</section>
<mat-slide-toggle formControlName="displayEntityType">
{{ 'widgets.table.display-entity-type' | translate }}
</mat-slide-toggle>
<section fxLayout="column" fxLayout.gt-xs="row" fxLayoutGap="8px" fxLayoutAlign.gt-xs="start center">
<mat-slide-toggle fxFlex formControlName="displayPagination">
{{ 'widgets.table.display-pagination' | translate }}
</mat-slide-toggle>
<mat-form-field fxFlex class="mat-block">
<mat-label translate>widgets.table.default-page-size</mat-label>
<input matInput type="number" min="1" step="1" formControlName="defaultPageSize">
</mat-form-field>
</section>
<mat-form-field fxFlex class="mat-block">
<mat-label translate>widgets.table.default-sort-order</mat-label>
<input matInput formControlName="defaultSortOrder">
</mat-form-field> </mat-form-field>
</section> </div>
</fieldset> </div>
<fieldset class="fields-group fields-group-slider"> <div class="tb-widget-config-panel tb-slide-toggle">
<legend class="group-title" translate>widgets.table.row-style</legend> <div class="tb-widget-config-panel-title" translate>widgets.table.rows</div>
<mat-expansion-panel class="tb-settings" [expanded]="entitiesTableWidgetSettingsForm.get('useRowStyleFunction').value"> <mat-expansion-panel class="tb-settings" [expanded]="entitiesTableWidgetSettingsForm.get('useRowStyleFunction').value">
<mat-expansion-panel-header fxLayout="row wrap"> <mat-expansion-panel-header fxLayout="row wrap" class="fill-width">
<mat-panel-title> <mat-panel-title fxFlex="60">
<mat-slide-toggle formControlName="useRowStyleFunction" (click)="$event.stopPropagation()" <mat-slide-toggle class="mat-slide" formControlName="useRowStyleFunction" (click)="$event.stopPropagation()"
fxLayoutAlign="center"> fxLayoutAlign="center">
{{ 'widgets.table.use-row-style-function' | translate }} {{ 'widgets.table.use-row-style-function' | translate }}
</mat-slide-toggle> </mat-slide-toggle>
</mat-panel-title> </mat-panel-title>
<mat-panel-description fxLayoutAlign="end center" fxHide.xs translate> <mat-panel-description fxFlex="40" fxLayoutAlign="end center" fxHide.xs translate>
widget-config.advanced-settings widget-config.advanced-settings
</mat-panel-description> </mat-panel-description>
</mat-expansion-panel-header> </mat-expansion-panel-header>
@ -112,5 +119,5 @@
</tb-js-func> </tb-js-func>
</ng-template> </ng-template>
</mat-expansion-panel> </mat-expansion-panel>
</fieldset> </div>
</section> </ng-container>

4
ui-ngx/src/app/modules/home/components/widget/lib/settings/widget-settings.scss

@ -16,6 +16,10 @@
@import '../../../../../../../scss/constants'; @import '../../../../../../../scss/constants';
:host { :host {
display: flex;
flex-direction: column;
gap: 16px;
.tb-widget-settings { .tb-widget-settings {
.fields-group { .fields-group {
padding: 0 16px 8px; padding: 0 16px 8px;

4
ui-ngx/src/app/modules/home/components/widget/widget-config.component.html

@ -83,7 +83,7 @@
<div class="tb-widget-config-panel"> <div class="tb-widget-config-panel">
<div class="tb-widget-config-panel-title" translate>widget-config.card-style</div> <div class="tb-widget-config-panel-title" translate>widget-config.card-style</div>
<div class="tb-widget-config-row space-between same-padding"> <div class="tb-widget-config-row space-between same-padding">
<div>{{ 'widget-config.text' | translate }}</div> <div>{{ 'widget-config.text-color' | translate }}</div>
<div fxLayout="row" fxLayoutAlign="start center" fxLayoutGap="16px"> <div fxLayout="row" fxLayoutAlign="start center" fxLayoutGap="16px">
<mat-divider vertical></mat-divider> <mat-divider vertical></mat-divider>
<tb-color-input asBoxInput <tb-color-input asBoxInput
@ -92,7 +92,7 @@
</div> </div>
</div> </div>
<div class="tb-widget-config-row space-between same-padding"> <div class="tb-widget-config-row space-between same-padding">
<div>{{ 'widget-config.background' | translate }}</div> <div>{{ 'widget-config.background-color' | translate }}</div>
<div fxLayout="row" fxLayoutAlign="start center" fxLayoutGap="16px"> <div fxLayout="row" fxLayoutAlign="start center" fxLayoutGap="16px">
<mat-divider vertical></mat-divider> <mat-divider vertical></mat-divider>
<tb-color-input asBoxInput <tb-color-input asBoxInput

2
ui-ngx/src/app/modules/home/pages/admin/mail-server.component.html

@ -331,7 +331,7 @@
</fieldset> </fieldset>
<div fxLayout="row" fxLayoutAlign="end center" fxLayout.xs="column" fxLayoutAlign.xs="end" fxLayoutGap="16px"> <div fxLayout="row" fxLayoutAlign="end center" fxLayout.xs="column" fxLayoutAlign.xs="end" fxLayoutGap="16px">
<button mat-raised-button type="button" <button mat-raised-button type="button"
[disabled]="(isLoading$ | async) || mailSettings.invalid || domainForm.invalid || (mailSettings.dirty || domainForm.dirty)" [disabled]="(isLoading$ | async) || mailSettings.get('enableOauth2').value ? mailSettings.invalid || domainForm.invalid || (mailSettings.dirty || domainForm.dirty) : mailSettings.invalid"
(click)="sendTestMail()"> (click)="sendTestMail()">
{{'admin.send-test-mail' | translate}} {{'admin.send-test-mail' | translate}}
</button> </button>

3
ui-ngx/src/app/modules/home/pages/admin/mail-server.component.ts

@ -416,6 +416,9 @@ export class MailServerComponent extends PageComponent implements OnInit, OnDest
private get mailSettingsFormValue(): MailServerSettings { private get mailSettingsFormValue(): MailServerSettings {
const formValue = this.mailSettings.getRawValue() as Required<typeof this.mailSettings.value>; const formValue = this.mailSettings.getRawValue() as Required<typeof this.mailSettings.value>;
delete formValue.changePassword; delete formValue.changePassword;
if (!isDefinedAndNotNull(formValue.password)) {
delete formValue.password;
}
return formValue; return formValue;
} }

1
ui-ngx/src/app/modules/home/pages/customer/customer-routing.module.ts

@ -251,6 +251,7 @@ const routes: Routes = [
{ {
path: ':dashboardId', path: ':dashboardId',
component: DashboardPageComponent, component: DashboardPageComponent,
canDeactivate: [ConfirmOnExitGuard],
data: { data: {
breadcrumb: { breadcrumb: {
labelFunction: dashboardBreadcumbLabelFunction, labelFunction: dashboardBreadcumbLabelFunction,

2
ui-ngx/src/app/modules/home/pages/dashboard/dashboard-routing.module.ts

@ -32,6 +32,7 @@ import { UserDashboardAction } from '@shared/models/user-settings.models';
import { Store } from '@ngrx/store'; import { Store } from '@ngrx/store';
import { AppState } from '@core/core.state'; import { AppState } from '@core/core.state';
import { getCurrentAuthUser } from '@core/auth/auth.selectors'; import { getCurrentAuthUser } from '@core/auth/auth.selectors';
import { ConfirmOnExitGuard } from '@core/guards/confirm-on-exit.guard';
@Injectable() @Injectable()
export class DashboardResolver implements Resolve<Dashboard> { export class DashboardResolver implements Resolve<Dashboard> {
@ -85,6 +86,7 @@ const routes: Routes = [
{ {
path: ':dashboardId', path: ':dashboardId',
component: DashboardPageComponent, component: DashboardPageComponent,
canDeactivate: [ConfirmOnExitGuard],
data: { data: {
breadcrumb: { breadcrumb: {
labelFunction: dashboardBreadcumbLabelFunction, labelFunction: dashboardBreadcumbLabelFunction,

1
ui-ngx/src/app/modules/home/pages/edge/edge-routing.module.ts

@ -241,6 +241,7 @@ const routes: Routes = [
{ {
path: ':dashboardId', path: ':dashboardId',
component: DashboardPageComponent, component: DashboardPageComponent,
canDeactivate: [ConfirmOnExitGuard],
data: { data: {
breadcrumb: { breadcrumb: {
labelFunction: dashboardBreadcumbLabelFunction, labelFunction: dashboardBreadcumbLabelFunction,

5
ui-ngx/src/app/shared/models/widget.models.ts

@ -318,6 +318,11 @@ export interface DataKey extends KeyInfo {
_hash?: number; _hash?: number;
} }
export enum DataKeyConfigMode {
general = 'general',
advanced = 'advanced'
}
export enum DatasourceType { export enum DatasourceType {
function = 'function', function = 'function',
device = 'device', device = 'device',

11
ui-ngx/src/assets/locale/locale.constant-en_US.json

@ -1130,6 +1130,7 @@
}, },
"datakey": { "datakey": {
"settings": "Settings", "settings": "Settings",
"general": "General",
"advanced": "Advanced", "advanced": "Advanced",
"key": "Key", "key": "Key",
"label": "Label", "label": "Label",
@ -4242,7 +4243,6 @@
"preview": "Preview", "preview": "Preview",
"set": "Set", "set": "Set",
"set-message": "Set message", "set-message": "Set message",
"card-title": "Card title",
"advanced-title-style": "Advanced title style", "advanced-title-style": "Advanced title style",
"card-style": "Card style", "card-style": "Card style",
"text": "Text", "text": "Text",
@ -5231,9 +5231,16 @@
"display-alarm-activity": "Display alarm activity", "display-alarm-activity": "Display alarm activity",
"allow-alarms-assign": "Allow alarms assignment", "allow-alarms-assign": "Allow alarms assignment",
"columns": "Columns", "columns": "Columns",
"column-settings": "Column settings",
"remove-column": "Remove column", "remove-column": "Remove column",
"add-column": "Add column", "add-column": "Add column",
"no-columns": "No columns configured" "no-columns": "No columns configured",
"show-card-buttons": "Show card buttons",
"columns-to-display": "Columns to display",
"table-header": "Table header",
"header-buttons": "Header buttons",
"pagination": "Pagination",
"rows": "Rows"
}, },
"value-source": { "value-source": {
"value-source": "Value source", "value-source": "Value source",

30
ui-ngx/src/styles.scss

@ -224,6 +224,8 @@ div {
line-height: var(--mdc-typography-caption-line-height, 20px); line-height: var(--mdc-typography-caption-line-height, 20px);
font-weight: var(--mdc-typography-caption-font-weight, 400); font-weight: var(--mdc-typography-caption-font-weight, 400);
letter-spacing: var(--mdc-typography-caption-letter-spacing, 0.0333333333em); letter-spacing: var(--mdc-typography-caption-letter-spacing, 0.0333333333em);
color: rgba(0, 0, 0, 0.6);
white-space: normal;
} }
.mat-caption { .mat-caption {
@ -1199,11 +1201,31 @@ mat-label {
&.no-padding-bottom { &.no-padding-bottom {
padding-bottom: 0; padding-bottom: 0;
} }
&.no-padding {
padding: 0;
}
&.stroked { &.stroked {
box-shadow: none; box-shadow: none;
border: 1px solid rgba(0, 0, 0, 0.12); border: 1px solid rgba(0, 0, 0, 0.12);
border-radius: 6px; border-radius: 6px;
} }
&.tb-slide-toggle {
padding: 0;
gap: 0;
.tb-widget-config-panel-title {
padding-top: 16px;
padding-left: 16px;
}
.mat-expansion-panel {
padding: 16px;
.mat-expansion-panel-header {
height: 32px;
.mat-slide {
margin: 0;
}
}
}
}
.mat-expansion-panel { .mat-expansion-panel {
&.tb-settings { &.tb-settings {
box-shadow: none; box-shadow: none;
@ -1220,6 +1242,11 @@ mat-label {
flex: 0; flex: 0;
white-space: nowrap; white-space: nowrap;
} }
&.fill-width {
.mat-content {
flex: 1;
}
}
&:hover { &:hover {
background: none; background: none;
} }
@ -1246,6 +1273,9 @@ mat-label {
} }
.mat-slide { .mat-slide {
margin: 8px 0; margin: 8px 0;
&.no-margin {
margin: 0;
}
.mdc-form-field>label { .mdc-form-field>label {
font-weight: 400; font-weight: 400;
font-size: 16px; font-size: 16px;

Loading…
Cancel
Save