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
private int maxRpcRetries;
@Value("${actors.rule.external.force_ack:false}")
@Getter
private boolean externalNodeForceAck;
@Getter
@Setter
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.SmsService;
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.sms.SmsSenderFactory;
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.TenantId;
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.PageLink;
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);
}
@Override
public void enqueueForTellFailure(TbMsg tbMsg, Throwable th) {
TopicPartitionInfo tpi = resolvePartition(tbMsg);
enqueueForTellNext(tpi, tbMsg, Collections.singleton(TbNodeConnectionType.FAILURE), getFailureMessage(th), null, null);
}
@Override
public void enqueueForTellNext(TbMsg tbMsg, String relationType) {
TopicPartitionInfo tpi = resolvePartition(tbMsg);
@ -313,16 +320,7 @@ class DefaultTbContext implements TbContext {
if (nodeCtx.getSelf().isDebugMode()) {
mainCtx.persistDebugOutput(nodeCtx.getTenantId(), nodeCtx.getSelf().getId(), msg, TbNodeConnectionType.FAILURE, th);
}
String failureMessage;
if (th != null) {
if (!StringUtils.isEmpty(th.getMessage())) {
failureMessage = th.getMessage();
} else {
failureMessage = th.getClass().getSimpleName();
}
} else {
failureMessage = null;
}
String failureMessage = getFailureMessage(th);
nodeCtx.getChainActor().tell(new RuleNodeToRuleChainTellNextMsg(nodeCtx.getSelf().getRuleChainId(),
nodeCtx.getSelf().getId(), Collections.singleton(TbNodeConnectionType.FAILURE),
msg, failureMessage));
@ -726,6 +724,11 @@ class DefaultTbContext implements TbContext {
return mainCtx.getSlackService();
}
@Override
public boolean isExternalNodeForceAck() {
return mainCtx.isExternalNodeForceAck();
}
@Override
public RuleEngineRpcService getRpcService() {
return mainCtx.getTbRuleEngineDeviceRpcService();
@ -842,10 +845,24 @@ class DefaultTbContext implements TbContext {
}
@Override
public void checkTenantEntity(EntityId entityId) {
public void checkTenantEntity(EntityId entityId) throws TbNodeException {
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 {

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.service.ContextAwareActor;
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.id.EntityId;
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.RuleChainType;
import org.thingsboard.server.common.msg.TbActorMsg;
import org.thingsboard.server.common.msg.queue.RuleEngineException;
import org.thingsboard.server.dao.rule.RuleChainService;
import java.util.function.Function;
@ -86,7 +88,12 @@ public abstract class RuleChainManagerActor extends ContextAwareActor {
() -> DefaultActorService.RULE_DISPATCHER_NAME,
() -> {
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}"
# Time in milliseconds for transaction to complete
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:
max_retries: "${ACTORS_RPC_MAX_RETRIES:5}"
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 org.thingsboard.server.common.data.EntityType;
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.TbActorStopReason;
@ -69,9 +70,14 @@ public final class TbActorMailbox implements TbActorCtx {
}
}
} catch (Throwable t) {
log.debug("[{}] Failed to init actor, attempt: {}", selfId, attempt, t);
InitFailureStrategy strategy;
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())) {
log.info("[{}] Failed to init actor, attempt {}, going to stop attempts.", selfId, attempt, t);
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) {
if (!destroyInProgress.get()) {
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);
}
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() {
// May be null in case of deserialization;
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.msg.TbActorMsg;
import org.thingsboard.server.common.msg.TbMsg;
public interface RuleChainAwareMsg extends TbActorMsg {
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;
import com.google.common.primitives.Bytes;
import org.apache.commons.lang3.ArrayUtils;
import org.mvel2.ExecutionContext;
import org.mvel2.ParserConfiguration;
import org.mvel2.execution.ExecutionArrayList;
@ -25,11 +27,13 @@ import org.thingsboard.server.common.data.StringUtils;
import java.io.IOException;
import java.io.UnsupportedEncodingException;
import java.math.BigDecimal;
import java.math.BigInteger;
import java.math.RoundingMode;
import java.nio.ByteBuffer;
import java.nio.ByteOrder;
import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Base64;
import java.util.Collection;
import java.util.List;
@ -61,6 +65,10 @@ public class TbUtils {
String.class)));
parserConfig.addImport("parseInt", new MethodStub(TbUtils.class.getMethod("parseInt",
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",
String.class)));
parserConfig.addImport("parseDouble", new MethodStub(TbUtils.class.getMethod("parseDouble",
@ -81,8 +89,58 @@ public class TbUtils {
byte[].class, int.class, int.class)));
parserConfig.addImport("parseBytesToInt", new MethodStub(TbUtils.class.getMethod("parseBytesToInt",
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",
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",
ExecutionContext.class, String.class)));
parserConfig.addImport("base64ToHex", new MethodStub(TbUtils.class.getMethod("base64ToHex",
@ -154,37 +212,73 @@ public class TbUtils {
}
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 {
int radix = 10;
if (isHexadecimal(value)) {
radix = 16;
String valueP = prepareNumberString(value);
isValidRadix(valueP, radix);
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) {
Float f = parseFloat(value);
if (f != null) {
return f.intValue();
}
throw new NumberFormatException(e.getMessage());
}
}
return null;
}
public static Integer parseInt(String value, int radix) {
if (value != null) {
public static Long parseLong(String value) {
int radix = getRadix(value);
return parseLong(value, radix);
}
public static Long parseLong(String value, int radix) {
if (StringUtils.isNotBlank(value)) {
try {
return Integer.parseInt(prepareNumberString(value), radix);
} catch (NumberFormatException e) {
Float f = parseFloat(value);
if (f != null) {
return f.intValue();
String valueP = prepareNumberString(value);
isValidRadix(valueP, radix);
try {
return Long.parseLong(valueP, radix);
} 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;
}
private static int getRadix(String value, int... radixS) {
return radixS.length > 0 ? radixS[0] : isHexadecimal(value) ? 16 : 10;
}
public static Float parseFloat(String value) {
if (value != null) {
try {
@ -218,8 +312,64 @@ public class TbUtils {
}
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();
if (length > 8) {
if (length > len) {
throw new IllegalArgumentException("Hex string is too large. Maximum 8 symbols allowed.");
}
if (length % 2 > 0) {
@ -229,7 +379,7 @@ public class TbUtils {
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));
}
return parseBytesToInt(data, 0, data.length, bigEndian);
return data;
}
public static ExecutionArrayList<Byte> hexToBytes(ExecutionContext ctx, String hex) {
@ -293,6 +443,91 @@ public class TbUtils {
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) {
byte[] bytes = new byte[bytesList.size()];
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();
}
public static float toFixed(float value, int precision) {
return BigDecimal.valueOf(value).setScale(precision, RoundingMode.HALF_UP).floatValue();
}
private static boolean isHexadecimal(String value) {
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;
import com.google.common.primitives.Bytes;
import lombok.extern.slf4j.Slf4j;
import org.junit.After;
import org.junit.Assert;
@ -26,6 +27,7 @@ import org.mvel2.SandboxedParserConfiguration;
import org.mvel2.execution.ExecutionArrayList;
import org.mvel2.execution.ExecutionHashMap;
import java.math.BigInteger;
import java.nio.ByteBuffer;
import java.util.ArrayList;
import java.util.Calendar;
@ -38,6 +40,23 @@ public class TbUtilsTest {
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
public void before() {
SandboxedParserConfiguration parserConfig = ParserContext.enableSandboxedMode();
@ -62,6 +81,7 @@ public class TbUtilsTest {
@Test
public void parseHexToInt() {
Assert.assertEquals(0xAB, TbUtils.parseHexToInt("AB"));
Assert.assertEquals(0xABBA, TbUtils.parseHexToInt("ABBA", true));
Assert.assertEquals(0xBAAB, TbUtils.parseHexToInt("ABBA", false));
Assert.assertEquals(0xAABBCC, TbUtils.parseHexToInt("AABBCC", true));
@ -182,9 +202,150 @@ public class TbUtilsTest {
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) {
return key + "Value" + (extraSymbol == null ? "" : extraSymbol);
Assert.assertEquals(java.util.Optional.of(-255L).get(), TbUtils.parseLong("-FF", 16));
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) {
@ -194,5 +355,5 @@ public class TbUtilsTest {
}
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 tbMsg, Throwable t);
void enqueueForTellNext(TbMsg msg, String relationType);
void enqueueForTellNext(TbMsg msg, Set<String> relationTypes);
@ -210,7 +212,7 @@ public interface TbContext {
void schedule(Runnable runnable, long delay, TimeUnit timeUnit);
void checkTenantEntity(EntityId entityId);
void checkTenantEntity(EntityId entityId) throws TbNodeException;
boolean isLocalEntity(EntityId entityId);
@ -302,6 +304,8 @@ public interface TbContext {
SlackService getSlackService();
boolean isExternalNodeForceAck();
/**
* Creates JS Script Engine
* @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;
import lombok.Getter;
import org.thingsboard.server.common.msg.TbActorError;
/**
* 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) {
this(message, false);
}
public TbNodeException(String message, boolean unrecoverable) {
super(message);
this.unrecoverable = unrecoverable;
}
public TbNodeException(Exception e) {
this(e, false);
}
public TbNodeException(Exception e, boolean unrecoverable) {
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 {
return JacksonUtil.treeToValue(configuration.getData(), clazz);
} 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()) {
this.notDynamicAlarmSeverity = EnumUtils.getEnum(AlarmSeverity.class, this.config.getSeverity());
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 org.thingsboard.rule.engine.api.RuleNode;
import org.thingsboard.rule.engine.api.TbContext;
import org.thingsboard.rule.engine.api.TbNode;
import org.thingsboard.rule.engine.api.TbNodeConfiguration;
import org.thingsboard.rule.engine.api.TbNodeException;
import org.thingsboard.rule.engine.api.util.TbNodeUtils;
import org.thingsboard.rule.engine.external.TbAbstractExternalNode;
import org.thingsboard.server.common.data.plugin.ComponentType;
import org.thingsboard.server.common.msg.TbMsg;
import org.thingsboard.server.common.msg.TbMsgMetaData;
@ -51,7 +51,7 @@ import static org.thingsboard.common.util.DonAsynchron.withCallback;
configDirective = "tbExternalNodeSnsConfig",
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 REQUEST_ID = "requestId";
@ -62,6 +62,7 @@ public class TbSnsNode implements TbNode {
@Override
public void init(TbContext ctx, TbNodeConfiguration configuration) throws TbNodeException {
super.init(ctx);
this.config = TbNodeUtils.convert(configuration, TbSnsNodeConfiguration.class);
AWSCredentials awsCredentials = new BasicAWSCredentials(this.config.getAccessKeyId(), this.config.getSecretAccessKey());
AWSStaticCredentialsProvider credProvider = new AWSStaticCredentialsProvider(awsCredentials);
@ -78,8 +79,9 @@ public class TbSnsNode implements TbNode {
@Override
public void onMsg(TbContext ctx, TbMsg msg) throws ExecutionException, InterruptedException, TbNodeException {
withCallback(publishMessageAsync(ctx, msg),
ctx::tellSuccess,
t -> ctx.tellFailure(processException(ctx, msg, t), t));
m -> tellSuccess(ctx, m),
t -> tellFailure(ctx, processException(ctx, msg, t), t));
ackIfNeeded(ctx, 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.TbNodeException;
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.plugin.ComponentType;
import org.thingsboard.server.common.msg.TbMsg;
@ -55,7 +56,7 @@ import static org.thingsboard.common.util.DonAsynchron.withCallback;
configDirective = "tbExternalNodeSqsConfig",
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 REQUEST_ID = "requestId";
@ -69,6 +70,7 @@ public class TbSqsNode implements TbNode {
@Override
public void init(TbContext ctx, TbNodeConfiguration configuration) throws TbNodeException {
super.init(ctx);
this.config = TbNodeUtils.convert(configuration, TbSqsNodeConfiguration.class);
AWSCredentials awsCredentials = new BasicAWSCredentials(this.config.getAccessKeyId(), this.config.getSecretAccessKey());
AWSStaticCredentialsProvider credProvider = new AWSStaticCredentialsProvider(awsCredentials);
@ -85,8 +87,9 @@ public class TbSqsNode implements TbNode {
@Override
public void onMsg(TbContext ctx, TbMsg msg) {
withCallback(publishMessageAsync(ctx, msg),
ctx::tellSuccess,
t -> ctx.tellFailure(processException(ctx, msg, t), t));
m -> tellSuccess(ctx, m),
t -> tellFailure(ctx, processException(ctx, msg, t), t));
ackIfNeeded(ctx, 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.TbNodeException;
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.msg.TbMsg;
import org.thingsboard.server.common.msg.TbMsgMetaData;
@ -53,7 +54,7 @@ import java.util.concurrent.TimeUnit;
configDirective = "tbExternalNodePubSubConfig",
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 ERROR = "error";
@ -63,8 +64,9 @@ public class TbPubSubNode implements TbNode {
@Override
public void init(TbContext ctx, TbNodeConfiguration configuration) throws TbNodeException {
super.init(ctx);
this.config = TbNodeUtils.convert(configuration, TbPubSubNodeConfiguration.class);
try {
this.config = TbNodeUtils.convert(configuration, TbPubSubNodeConfiguration.class);
this.pubSubClient = initPubSubClient();
} catch (Exception e) {
throw new TbNodeException(e);
@ -74,6 +76,7 @@ public class TbPubSubNode implements TbNode {
@Override
public void onMsg(TbContext ctx, TbMsg msg) {
publishMessage(ctx, msg);
ackIfNeeded(ctx, msg);
}
@Override
@ -101,12 +104,12 @@ public class TbPubSubNode implements TbNode {
ApiFutures.addCallback(messageIdFuture, new ApiFutureCallback<String>() {
public void onSuccess(String messageId) {
TbMsg next = processPublishResult(ctx, msg, messageId);
ctx.tellSuccess(next);
tellSuccess(ctx, next);
}
public void onFailure(Throwable t) {
TbMsg next = processException(ctx, msg, t);
ctx.tellFailure(next, t);
tellFailure(ctx, next, t);
}
},
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.thingsboard.rule.engine.api.RuleNode;
import org.thingsboard.rule.engine.api.TbContext;
import org.thingsboard.rule.engine.api.TbNode;
import org.thingsboard.rule.engine.api.TbNodeConfiguration;
import org.thingsboard.rule.engine.api.TbNodeException;
import org.thingsboard.server.common.data.msg.TbNodeConnectionType;
import org.thingsboard.rule.engine.api.util.TbNodeUtils;
import org.thingsboard.rule.engine.external.TbAbstractExternalNode;
import org.thingsboard.server.common.data.exception.ThingsboardKafkaClientError;
import org.thingsboard.server.common.data.plugin.ComponentType;
import org.thingsboard.server.common.msg.TbMsg;
@ -56,7 +55,7 @@ import java.util.Properties;
configDirective = "tbExternalNodeKafkaConfig",
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 PARTITION = "partition";
@ -78,6 +77,7 @@ public class TbKafkaNode implements TbNode {
@Override
public void init(TbContext ctx, TbNodeConfiguration configuration) throws TbNodeException {
super.init(ctx);
this.config = TbNodeUtils.convert(configuration, TbKafkaNodeConfiguration.class);
this.initError = null;
Properties properties = new Properties();
@ -129,6 +129,7 @@ public class TbKafkaNode implements TbNode {
return null;
});
}
ackIfNeeded(ctx, msg);
} catch (Exception 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) {
if (e == null) {
TbMsg next = processResponse(ctx, msg, metadata);
ctx.tellNext(next, TbNodeConnectionType.SUCCESS);
tellSuccess(ctx, processResponse(ctx, msg, metadata));
} else {
TbMsg next = processException(ctx, msg, e);
ctx.tellFailure(next, e);
tellFailure(ctx, processException(ctx, msg, e), 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.TbContext;
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.TbNodeException;
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.msg.TbMsg;
@ -47,7 +47,7 @@ import static org.thingsboard.common.util.DonAsynchron.withCallback;
configDirective = "tbExternalNodeSendEmailConfig",
icon = "send"
)
public class TbSendEmailNode implements TbNode {
public class TbSendEmailNode extends TbAbstractExternalNode {
private static final String MAIL_PROP = "mail.";
static final String SEND_EMAIL_TYPE = "SEND_EMAIL";
@ -56,8 +56,9 @@ public class TbSendEmailNode implements TbNode {
@Override
public void init(TbContext ctx, TbNodeConfiguration configuration) throws TbNodeException {
super.init(ctx);
this.config = TbNodeUtils.convert(configuration, TbSendEmailNodeConfiguration.class);
try {
this.config = TbNodeUtils.convert(configuration, TbSendEmailNodeConfiguration.class);
if (!this.config.isUseSystemSmtpSettings()) {
mailSender = createMailSender();
}
@ -75,8 +76,9 @@ public class TbSendEmailNode implements TbNode {
sendEmail(ctx, msg, email);
return null;
}),
ok -> ctx.tellSuccess(msg),
fail -> ctx.tellFailure(msg, fail));
ok -> tellSuccess(ctx, msg),
fail -> tellFailure(ctx, msg, fail));
ackIfNeeded(ctx, msg);
} catch (Exception 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.ClientCredentials;
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.plugin.ComponentClusteringMode;
import org.thingsboard.server.common.data.plugin.ComponentType;
@ -55,7 +56,7 @@ import java.util.concurrent.TimeoutException;
configDirective = "tbExternalNodeMqttConfig",
icon = "call_split"
)
public class TbMqttNode implements TbNode {
public class TbMqttNode extends TbAbstractExternalNode {
private static final Charset UTF8 = Charset.forName("UTF-8");
@ -67,8 +68,9 @@ public class TbMqttNode implements TbNode {
@Override
public void init(TbContext ctx, TbNodeConfiguration configuration) throws TbNodeException {
super.init(ctx);
this.mqttNodeConfiguration = TbNodeUtils.convert(configuration, TbMqttNodeConfiguration.class);
try {
this.mqttNodeConfiguration = TbNodeUtils.convert(configuration, TbMqttNodeConfiguration.class);
this.mqttClient = initClient(ctx);
} catch (Exception 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())
.addListener(future -> {
if (future.isSuccess()) {
ctx.tellSuccess(msg);
tellSuccess(ctx, msg);
} else {
TbMsg next = processException(ctx, msg, future.cause());
ctx.tellFailure(next, future.cause());
tellFailure(ctx, processException(ctx, msg, future.cause()), future.cause());
}
}
);
ackIfNeeded(ctx, msg);
}
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 {
@Override
public void init(TbContext ctx, TbNodeConfiguration configuration) throws TbNodeException {
super.init(ctx);
this.mqttNodeConfiguration = TbNodeUtils.convert(configuration, TbMqttNodeConfiguration.class);
try {
this.mqttNodeConfiguration = TbNodeUtils.convert(configuration, TbMqttNodeConfiguration.class);
mqttNodeConfiguration.setPort(8883);
mqttNodeConfiguration.setCleanSession(true);
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.TbNodeException;
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.NotificationRequestConfig;
import org.thingsboard.server.common.data.notification.info.RuleEngineOriginatedNotificationInfo;
@ -42,12 +43,13 @@ import java.util.concurrent.ExecutionException;
configDirective = "tbExternalNodeNotificationConfig",
icon = "notifications"
)
public class TbNotificationNode implements TbNode {
public class TbNotificationNode extends TbAbstractExternalNode {
private TbNotificationNodeConfiguration config;
@Override
public void init(TbContext ctx, TbNodeConfiguration configuration) throws TbNodeException {
super.init(ctx);
this.config = TbNodeUtils.convert(configuration, TbNotificationNodeConfiguration.class);
}
@ -69,15 +71,16 @@ public class TbNotificationNode implements TbNode {
.originatorEntityId(ctx.getSelf().getRuleChainId())
.build();
DonAsynchron.withCallback(ctx.getNotificationExecutor().executeAsync(() -> {
return ctx.getNotificationCenter().processNotificationRequest(ctx.getTenantId(), notificationRequest, stats -> {
TbMsgMetaData metaData = msg.getMetaData().copy();
metaData.putValue("notificationRequestResult", JacksonUtil.toString(stats));
ctx.tellSuccess(TbMsg.transformMsg(msg, metaData));
});
}),
r -> {},
e -> ctx.tellFailure(msg, e));
DonAsynchron.withCallback(ctx.getNotificationExecutor().executeAsync(() ->
ctx.getNotificationCenter().processNotificationRequest(ctx.getTenantId(), notificationRequest, stats -> {
TbMsgMetaData metaData = msg.getMetaData().copy();
metaData.putValue("notificationRequestResult", JacksonUtil.toString(stats));
tellSuccess(ctx, TbMsg.transformMsg(msg, metaData));
})),
r -> {
},
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.TbNodeException;
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.msg.TbMsg;
@ -37,12 +38,13 @@ import java.util.concurrent.ExecutionException;
configDirective = "tbExternalNodeSlackConfig",
iconUrl = "data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAyNCAyNCI+PHBhdGggZD0iTTYsMTVBMiwyIDAgMCwxIDQsMTdBMiwyIDAgMCwxIDIsMTVBMiwyIDAgMCwxIDQsMTNINlYxNU03LDE1QTIsMiAwIDAsMSA5LDEzQTIsMiAwIDAsMSAxMSwxNVYyMEEyLDIgMCAwLDEgOSwyMkEyLDIgMCAwLDEgNywyMFYxNU05LDdBMiwyIDAgMCwxIDcsNUEyLDIgMCAwLDEgOSwzQTIsMiAwIDAsMSAxMSw1VjdIOU05LDhBMiwyIDAgMCwxIDExLDEwQTIsMiAwIDAsMSA5LDEySDRBMiwyIDAgMCwxIDIsMTBBMiwyIDAgMCwxIDQsOEg5TTE3LDEwQTIsMiAwIDAsMSAxOSw4QTIsMiAwIDAsMSAyMSwxMEEyLDIgMCAwLDEgMTksMTJIMTdWMTBNMTYsMTBBMiwyIDAgMCwxIDE0LDEyQTIsMiAwIDAsMSAxMiwxMFY1QTIsMiAwIDAsMSAxNCwzQTIsMiAwIDAsMSAxNiw1VjEwTTE0LDE4QTIsMiAwIDAsMSAxNiwyMEEyLDIgMCAwLDEgMTQsMjJBMiwyIDAgMCwxIDEyLDIwVjE4SDE0TTE0LDE3QTIsMiAwIDAsMSAxMiwxNUEyLDIgMCAwLDEgMTQsMTNIMTlBMiwyIDAgMCwxIDIxLDE1QTIsMiAwIDAsMSAxOSwxN0gxNFoiIC8+PC9zdmc+"
)
public class TbSlackNode implements TbNode {
public class TbSlackNode extends TbAbstractExternalNode {
private TbSlackNodeConfiguration config;
@Override
public void init(TbContext ctx, TbNodeConfiguration configuration) throws TbNodeException {
super.init(ctx);
this.config = TbNodeUtils.convert(configuration, TbSlackNodeConfiguration.class);
}
@ -62,8 +64,9 @@ public class TbSlackNode implements TbNode {
DonAsynchron.withCallback(ctx.getExternalCallExecutor().executeAsync(() -> {
ctx.getSlackService().sendMessage(ctx.getTenantId(), token, config.getConversation().getId(), message);
}),
r -> ctx.tellSuccess(msg),
e -> ctx.tellFailure(msg, e));
r -> tellSuccess(ctx, msg),
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.TbNodeException;
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.plugin.ComponentType;
import org.thingsboard.server.common.msg.TbMsg;
@ -48,7 +49,7 @@ import static org.thingsboard.common.util.DonAsynchron.withCallback;
configDirective = "tbExternalNodeRabbitMqConfig",
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");
@ -61,6 +62,7 @@ public class TbRabbitMqNode implements TbNode {
@Override
public void init(TbContext ctx, TbNodeConfiguration configuration) throws TbNodeException {
super.init(ctx);
this.config = TbNodeUtils.convert(configuration, TbRabbitMqNodeConfiguration.class);
ConnectionFactory factory = new ConnectionFactory();
factory.setHost(this.config.getHost());
@ -83,11 +85,9 @@ public class TbRabbitMqNode implements TbNode {
@Override
public void onMsg(TbContext ctx, TbMsg msg) {
withCallback(publishMessageAsync(ctx, msg),
ctx::tellSuccess,
t -> {
TbMsg next = processException(ctx, msg, t);
ctx.tellFailure(next, t);
});
m -> tellSuccess(ctx, m),
t -> tellFailure(ctx, processException(ctx, msg, t), t));
ackIfNeeded(ctx, 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.rule.engine.api.TbContext;
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.credentials.BasicCredentials;
import org.thingsboard.rule.engine.credentials.ClientCredentials;
@ -65,6 +64,7 @@ import java.util.Map;
import java.util.concurrent.ConcurrentLinkedDeque;
import java.util.concurrent.TimeUnit;
import java.util.function.BiConsumer;
import java.util.function.Consumer;
@Data
@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);
HttpHeaders headers = prepareHeaders(msg);
HttpMethod method = HttpMethod.valueOf(config.getRequestMethod());
HttpEntity<String> entity;
if(HttpMethod.GET.equals(method) || HttpMethod.HEAD.equals(method) ||
HttpMethod.OPTIONS.equals(method) || HttpMethod.TRACE.equals(method) ||
config.isIgnoreRequestBody()) {
if (HttpMethod.GET.equals(method) || HttpMethod.HEAD.equals(method) ||
HttpMethod.OPTIONS.equals(method) || HttpMethod.TRACE.equals(method) ||
config.isIgnoreRequestBody()) {
entity = new HttpEntity<>(headers);
} else {
entity = new HttpEntity<>(getData(msg), headers);
@ -198,21 +200,18 @@ public class TbHttpClient {
URI uri = buildEncodedUri(endpointUrl);
ListenableFuture<ResponseEntity<String>> future = httpClient.exchange(
uri, method, entity, String.class);
future.addCallback(new ListenableFutureCallback<ResponseEntity<String>>() {
future.addCallback(new ListenableFutureCallback<>() {
@Override
public void onFailure(Throwable throwable) {
TbMsg next = processException(ctx, msg, throwable);
ctx.tellFailure(next, throwable);
onFailure.accept(processException(ctx, msg, throwable), throwable);
}
@Override
public void onSuccess(ResponseEntity<String> responseEntity) {
if (responseEntity.getStatusCode().is2xxSuccessful()) {
TbMsg next = processResponse(ctx, msg, responseEntity);
ctx.tellSuccess(next);
onSuccess.accept(processResponse(ctx, msg, responseEntity));
} else {
TbMsg next = processFailureResponse(ctx, msg, responseEntity);
ctx.tellNext(next, TbNodeConnectionType.FAILURE);
onFailure.accept(processFailureResponse(ctx, msg, responseEntity), null);
}
}
});
@ -248,7 +247,7 @@ public class TbHttpClient {
if (config.isTrimDoubleQuotes()) {
final String dataBefore = data;
data = data.replaceAll("^\"|\"$", "");;
data = data.replaceAll("^\"|\"$", "");
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.TbNodeException;
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.msg.TbMsg;
@ -43,24 +44,26 @@ import org.thingsboard.server.common.msg.TbMsg;
configDirective = "tbExternalNodeRestApiCallConfig",
iconUrl = "data:image/svg+xml;base64,PHN2ZyBzdHlsZT0iZW5hYmxlLWJhY2tncm91bmQ6bmV3IDAgMCA1MTIgNTEyIiB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHhtbDpzcGFjZT0icHJlc2VydmUiIHZpZXdCb3g9IjAgMCA1MTIgNTEyIiB2ZXJzaW9uPSIxLjEiIHk9IjBweCIgeD0iMHB4Ij48ZyB0cmFuc2Zvcm09Im1hdHJpeCguOTQ5NzUgMCAwIC45NDk3NSAxNy4xMiAyNi40OTIpIj48cGF0aCBkPSJtMTY5LjExIDEwOC41NGMtOS45MDY2IDAuMDczNC0xOS4wMTQgNi41NzI0LTIyLjAxNCAxNi40NjlsLTY5Ljk5MyAyMzEuMDhjLTMuNjkwNCAxMi4xODEgMy4yODkyIDI1LjIyIDE1LjQ2OSAyOC45MSAyLjIyNTkgMC42NzQ4MSA0LjQ5NjkgMSA2LjcyODUgMSA5Ljk3MjEgMCAxOS4xNjUtNi41MTUzIDIyLjE4Mi0xNi40NjdhNi41MjI0IDYuNTIyNCAwIDAgMCAwLjAwMiAtMC4wMDJsNjkuOTktMjMxLjA3YTYuNTIyNCA2LjUyMjQgMCAwIDAgMCAtMC4wMDJjMy42ODU1LTEyLjE4MS0zLjI4Ny0yNS4yMjUtMTUuNDcxLTI4LjkxMi0yLjI4MjUtMC42OTE0NS00LjYxMTYtMS4wMTY5LTYuODk4NC0xem04NC45ODggMGMtOS45MDQ4IDAuMDczNC0xOS4wMTggNi41Njc1LTIyLjAxOCAxNi40NjlsLTY5Ljk4NiAyMzEuMDhjLTMuNjg5OCAxMi4xNzkgMy4yODUzIDI1LjIxNyAxNS40NjUgMjguOTA4IDIuMjI5NyAwLjY3NjQ3IDQuNTAwOCAxLjAwMiA2LjczMjQgMS4wMDIgOS45NzIxIDAgMTkuMTY1LTYuNTE1MyAyMi4xODItMTYuNDY3YTYuNTIyNCA2LjUyMjQgMCAwIDAgMC4wMDIgLTAuMDAybDY5Ljk4OC0yMzEuMDdjMy42OTA4LTEyLjE4MS0zLjI4NTItMjUuMjIzLTE1LjQ2Ny0yOC45MTItMi4yODE0LTAuNjkyMzEtNC42MTA4LTEuMDE4OS02Ljg5ODQtMS4wMDJ6bS0yMTcuMjkgNDIuMjNjLTEyLjcyOS0wLjAwMDg3LTIzLjE4OCAxMC40NTYtMjMuMTg4IDIzLjE4NiAwLjAwMSAxMi43MjggMTAuNDU5IDIzLjE4NiAyMy4xODggMjMuMTg2IDEyLjcyNy0wLjAwMSAyMy4xODMtMTAuNDU5IDIzLjE4NC0yMy4xODYgMC4wMDA4NzYtMTIuNzI4LTEwLjQ1Ni0yMy4xODUtMjMuMTg0LTIzLjE4NnptMCAxNDYuNjRjLTEyLjcyNy0wLjAwMDg3LTIzLjE4NiAxMC40NTUtMjMuMTg4IDIzLjE4NC0wLjAwMDg3MyAxMi43MjkgMTAuNDU4IDIzLjE4OCAyMy4xODggMjMuMTg4IDEyLjcyOC0wLjAwMSAyMy4xODQtMTAuNDYgMjMuMTg0LTIzLjE4OC0wLjAwMS0xMi43MjYtMTAuNDU3LTIzLjE4My0yMy4xODQtMjMuMTg0em0yNzAuNzkgNDIuMjExYy0xMi43MjcgMC0yMy4xODQgMTAuNDU3LTIzLjE4NCAyMy4xODRzMTAuNDU1IDIzLjE4OCAyMy4xODQgMjMuMTg4aDE1NC45OGMxMi43MjkgMCAyMy4xODYtMTAuNDYgMjMuMTg2LTIzLjE4OCAwLjAwMS0xMi43MjgtMTAuNDU4LTIzLjE4NC0yMy4xODYtMjMuMTg0eiIgdHJhbnNmb3JtPSJtYXRyaXgoMS4wMzc2IDAgMCAxLjAzNzYgLTcuNTY3NiAtMTQuOTI1KSIgc3Ryb2tlLXdpZHRoPSIxLjI2OTMiLz48L2c+PC9zdmc+"
)
public class TbRestApiCallNode implements TbNode {
public class TbRestApiCallNode extends TbAbstractExternalNode {
private boolean useRedisQueueForMsgPersistence;
protected TbHttpClient httpClient;
@Override
public void init(TbContext ctx, TbNodeConfiguration configuration) throws TbNodeException {
super.init(ctx);
TbRestApiCallNodeConfiguration config = TbNodeUtils.convert(configuration, TbRestApiCallNodeConfiguration.class);
httpClient = new TbHttpClient(config, ctx.getSharedEventLoop());
useRedisQueueForMsgPersistence = config.isUseRedisQueueForMsgPersistence();
if (useRedisQueueForMsgPersistence) {
if (config.isUseRedisQueueForMsgPersistence()) {
log.warn("[{}][{}] Usage of Redis Template is deprecated starting 2.5 and will have no affect", ctx.getTenantId(), ctx.getSelfId());
}
}
@Override
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

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.sms.SmsSender;
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.msg.TbMsg;
@ -39,13 +40,14 @@ import static org.thingsboard.common.util.DonAsynchron.withCallback;
configDirective = "tbExternalNodeSendSmsConfig",
icon = "sms"
)
public class TbSendSmsNode implements TbNode {
public class TbSendSmsNode extends TbAbstractExternalNode {
private TbSendSmsNodeConfiguration config;
private SmsSender smsSender;
@Override
public void init(TbContext ctx, TbNodeConfiguration configuration) throws TbNodeException {
super.init(ctx);
try {
this.config = TbNodeUtils.convert(configuration, TbSendSmsNodeConfiguration.class);
if (!this.config.isUseSystemSmsSettings()) {
@ -63,8 +65,9 @@ public class TbSendSmsNode implements TbNode {
sendSms(ctx, msg);
return null;
}),
ok -> ctx.tellSuccess(msg),
fail -> ctx.tellFailure(msg, fail));
ok -> tellSuccess(ctx, msg),
fail -> tellFailure(ctx, msg, fail));
ackIfNeeded(ctx, msg);
} catch (Exception 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()
)).thenReturn(successMsg);
httpClient.processMessage(ctx, msg);
httpClient.processMessage(ctx, msg,
m -> ctx.tellSuccess(msg),
(m, t) -> ctx.tellFailure(m, t));
Awaitility.await()
.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> {
const newDatasource = deepClone(datasource);
if (newDatasource.type === DatasourceType.device) {
newDatasource.type = DatasourceType.entity;
}
if (newDatasource.type === DatasourceType.entity || newDatasource.type === DatasourceType.entityCount
if (newDatasource.type === DatasourceType.entity
|| newDatasource.type === DatasourceType.device
|| newDatasource.type === DatasourceType.entityCount
|| newDatasource.type === DatasourceType.alarmCount) {
if (newDatasource.filterId) {
newDatasource.keyFilters = this.getKeyFilters(newDatasource.filterId);
@ -263,7 +262,8 @@ export class AliasController implements IAliasController {
if (newDatasource.type === DatasourceType.alarmCount) {
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);
if (forceFilter) {
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> {
datasources.forEach((datasource) => {
// @ts-ignore
if (datasource.type === 'device') {
datasource.type = DatasourceType.entity;
datasource.entityType = EntityType.DEVICE;
if (datasource.deviceId) {
datasource.entityId = datasource.deviceId;
} else if (datasource.deviceAliasId) {
if (datasource.type === DatasourceType.device) {
if (datasource.deviceAliasId) {
datasource.type = DatasourceType.entity;
datasource.entityAliasId = datasource.deviceAliasId;
}
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.checkToClose();
}
this.tryOpenSocket();
if (this.subscribersCount > 0) {
this.tryOpenSocket();
}
}
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 { UtilsService } from '@core/services/utils.service';
import { Widget } from '@app/shared/models/widget.models';
import { ConfirmOnExitGuard } from '@core/guards/confirm-on-exit.guard';
@Injectable()
export class WidgetEditorDashboardResolver implements Resolve<Dashboard> {
@ -59,6 +60,7 @@ const routes: Routes = [
{
path: 'dashboard/:dashboardId',
component: DashboardPageComponent,
canDeactivate: [ConfirmOnExitGuard],
data: {
breadcrumb: {
skip: true
@ -75,6 +77,7 @@ const routes: Routes = [
{
path: 'widget-editor',
component: DashboardPageComponent,
canDeactivate: [ConfirmOnExitGuard],
data: {
breadcrumb: {
skip: true

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

@ -115,7 +115,7 @@
matTooltip="{{'action.save' | translate}}"
matTooltipPosition="below">
<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>
<button fxHide.gt-md mat-icon-button (click)="toggleDashboardEditMode()"
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 { TbPopoverComponent } from '@shared/components/popover.component';
import { ResizeObserver } from '@juggle/resize-observer';
import { HasDirtyFlag } from '@core/guards/confirm-on-exit.guard';
// @dynamic
@Component({
@ -159,7 +160,15 @@ import { ResizeObserver } from '@juggle/resize-observer';
encapsulation: ViewEncapsulation.None,
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);

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
panelTitle="{{ 'widgets.table.columns' | translate }}"
addKeyTitle="{{ 'widgets.table.add-column' | translate }}"
keySettingsTitle="{{ 'widgets.table.column-settings' | translate }}"
removeKeyTitle="{{ 'widgets.table.remove-column' | translate }}"
noKeysText="{{ 'widgets.table.no-columns' | translate }}"
hideDataKeyColor
@ -61,6 +62,14 @@
</tb-color-input>
</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>{{ 'widget-config.text-color' | translate }}</div>
<div fxLayout="row" fxLayoutAlign="start center" fxLayoutGap="16px">
@ -71,7 +80,7 @@
</div>
</div>
<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">
<mat-divider vertical></mat-divider>
<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,
Datasource,
datasourcesHasAggregation,
datasourcesHasOnlyComparisonAggregation
datasourcesHasOnlyComparisonAggregation, WidgetConfig
} from '@shared/models/widget.models';
import { WidgetConfigComponent } from '@home/components/widget/widget-config.component';
import { DataKeyType } from '@shared/models/telemetry/telemetry.models';
@ -85,6 +85,7 @@ export class EntitiesTableBasicConfigComponent extends BasicWidgetConfigComponen
showTitleIcon: [configData.config.showTitleIcon, []],
titleIcon: [configData.config.titleIcon, []],
iconColor: [configData.config.iconColor, []],
cardButtons: [this.getCardButtons(configData.config), []],
color: [configData.config.color, []],
backgroundColor: [configData.config.backgroundColor, []],
actions: [configData.config.actions || {}, []]
@ -104,6 +105,7 @@ export class EntitiesTableBasicConfigComponent extends BasicWidgetConfigComponen
this.widgetConfig.config.showTitleIcon = config.showTitleIcon;
this.widgetConfig.config.titleIcon = config.titleIcon;
this.widgetConfig.config.iconColor = config.iconColor;
this.setCardButtons(config.cardButtons, this.widgetConfig.config);
this.widgetConfig.config.color = config.color;
this.widgetConfig.config.backgroundColor = config.backgroundColor;
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 }}">
</mat-form-field>
</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>
<ng-template #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;
}
}
.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,
Component,
ElementRef,
EventEmitter,
forwardRef,
Input,
OnChanges,
OnInit,
Output,
SimpleChanges,
ViewChild,
ViewEncapsulation
@ -37,7 +39,14 @@ import {
} from '@angular/forms';
import { MatDialog } from '@angular/material/dialog';
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 { DataKeyType } from '@shared/models/telemetry/telemetry.models';
import { AggregationType } from '@shared/models/time/time.models';
@ -104,6 +113,9 @@ export class DataKeyRowComponent implements ControlValueAccessor, OnInit, OnChan
@Input()
deviceId: string;
@Output()
keyRemoved = new EventEmitter();
keyFormControl: UntypedFormControl;
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);
}
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) => {};
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,
{
disableClose: true,
panelClass: ['tb-dialog', 'tb-fullscreen-dialog'],
data: {
dataKey: deepClone(this.modelValue),
dataKeyConfigMode: advanced ? DataKeyConfigMode.advanced : DataKeyConfigMode.general,
dataKeySettingsSchema: this.datakeySettingsSchema,
dataKeySettingsDirective: this.dataKeySettingsDirective,
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-actions-header"></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)">
<div cdkDrag class="tb-data-keys-table-row tb-draggable" *ngFor="let keyControl of keysFormArray().controls; trackBy: trackByKey;
let $index = index;">
<div cdkDrag [cdkDragDisabled]="!dragEnabled"
class="tb-data-keys-table-row tb-draggable"
*ngFor="let keyControl of keysFormArray().controls; trackBy: trackByKey; let $index = index;">
<tb-data-key-row fxFlex
[formControl]="keyControl"
[datasourceType]="datasourceType"
[deviceId]="deviceId"
[entityAliasId]="entityAliasId">
[entityAliasId]="entityAliasId"
(keyRemoved)="removeKey($index)">
</tb-data-key-row>
<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
type="button"
cdkDragHandle
[ngClass]="{'tb-hidden': !dragEnabled}"
matTooltip="{{ 'action.drag' | translate }}"
matTooltipPosition="above">
<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;
}
&.tb-actions-header {
width: 76px;
width: 114px;
}
}
}
@ -57,20 +57,5 @@
height: 38px;
display: flex;
flex-direction: row;
gap: 12px;
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()
addKeyTitle: string;
@Input()
keySettingsTitle: string;
@Input()
removeKeyTitle: string;
@ -112,6 +115,10 @@ export class DataKeysPanelComponent implements ControlValueAccessor, OnInit, OnC
return this.widgetConfigComponent.modelValue?.dataKeySettingsSchema;
}
get dragEnabled(): boolean {
return this.keysFormArray().controls.length > 1;
}
private propagateChange = (_val: any) => {};
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">
<h2>{{ 'datakey.configuration' | translate }}</h2>
<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
(click)="cancel()"
type="button">
@ -28,8 +32,9 @@
<mat-progress-bar color="warn" mode="indeterminate" *ngIf="isLoading$ | async">
</mat-progress-bar>
<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
[dataKeyConfigMode]="dataKeyConfigMode"
[dataKeySettingsSchema]="data.dataKeySettingsSchema"
[dataKeySettingsDirective]="data.dataKeySettingsDirective"
[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 { Store } from '@ngrx/store';
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 { 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 { DataKeyConfigComponent } from '@home/components/widget/config/data-key-config.component';
import { Dashboard } from '@shared/models/dashboard.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 {
dataKey: DataKey;
dataKeyConfigMode?: DataKeyConfigMode;
dataKeySettingsSchema: any;
dataKeySettingsDirective: string;
dashboard: Dashboard;
@ -57,6 +67,12 @@ export class DataKeyConfigDialogComponent extends DialogComponent<DataKeyConfigD
@ViewChild('dataKeyConfig', {static: true}) dataKeyConfig: DataKeyConfigComponent;
hasAdvanced = false;
dataKeyConfigHeaderOptions: ToggleHeaderOption[];
dataKeyConfigMode: DataKeyConfigMode = DataKeyConfigMode.general;
dataKeyFormGroup: UntypedFormGroup;
submitted = false;
@ -66,6 +82,7 @@ export class DataKeyConfigDialogComponent extends DialogComponent<DataKeyConfigD
@Inject(MAT_DIALOG_DATA) public data: DataKeyConfigDialogData,
@SkipSelf() private errorStateMatcher: ErrorStateMatcher,
public dialogRef: MatDialogRef<DataKeyConfigDialogComponent, DataKey>,
private translate: TranslateService,
public fb: UntypedFormBuilder) {
super(store, router, dialogRef);
}
@ -74,6 +91,23 @@ export class DataKeyConfigDialogComponent extends DialogComponent<DataKeyConfigD
this.dataKeyFormGroup = this.fb.group({
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 {

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

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

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

@ -17,18 +17,6 @@
:host {
.tb-datakey-config {
height: 100%;
.settings{
&> :first-child{
padding-top: 12px;
}
&> :nth-child(n){
padding-right: 12px;
padding-left: 12px;
}
}
.fields-group {
padding: 0 16px 8px;
margin-bottom: 10px;
@ -60,7 +48,7 @@
}
.tb-hint.after-fields {
margin-top: -1.25em;
margin-top: -0.75em;
max-width: fit-content;
line-height: 15px;
}
@ -69,15 +57,6 @@
:host ::ng-deep {
.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 {
&.tb-settings {
box-shadow: none;

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

@ -22,7 +22,7 @@ import {
ComparisonResultType,
comparisonResultTypeTranslationMap,
DataKey,
dataKeyAggregationTypeHintTranslationMap,
dataKeyAggregationTypeHintTranslationMap, DataKeyConfigMode,
Widget,
widgetType
} from '@shared/models/widget.models';
@ -73,6 +73,8 @@ import { coerceBoolean } from '@shared/decorators/coercion';
})
export class DataKeyConfigComponent extends PageComponent implements OnInit, ControlValueAccessor, Validator {
dataKeyConfigModes = DataKeyConfigMode;
dataKeyTypes = DataKeyType;
widgetTypes = widgetType;
@ -91,6 +93,9 @@ export class DataKeyConfigComponent extends PageComponent implements OnInit, Con
comparisonResultTypeTranslations = comparisonResultTypeTranslationMap;
@Input()
dataKeyConfigMode: DataKeyConfigMode;
@Input()
deviceId: string;
@ -142,7 +147,7 @@ export class DataKeyConfigComponent extends PageComponent implements OnInit, Con
@ViewChild('funcBodyEdit') funcBodyEdit: JsFuncComponent;
@ViewChild('postFuncBodyEdit') postFuncBodyEdit: JsFuncComponent;
displayAdvanced = false;
hasAdvanced = false;
modelValue: DataKey;
@ -193,7 +198,7 @@ export class DataKeyConfigComponent extends PageComponent implements OnInit, Con
}
if (this.dataKeySettingsSchema && this.dataKeySettingsSchema.schema ||
this.dataKeySettingsDirective && this.dataKeySettingsDirective.length) {
this.displayAdvanced = true;
this.hasAdvanced = true;
this.dataKeySettingsData = {
schema: this.dataKeySettingsSchema?.schema || {
type: 'object',
@ -291,7 +296,7 @@ export class DataKeyConfigComponent extends PageComponent implements OnInit, Con
}
this.dataKeyFormGroup.patchValue(this.modelValue, {emitEvent: false});
this.updateValidators();
if (this.displayAdvanced) {
if (this.hasAdvanced) {
this.dataKeySettingsData.model = this.modelValue.settings;
this.dataKeySettingsFormGroup.patchValue({
settings: this.dataKeySettingsData
@ -370,7 +375,7 @@ export class DataKeyConfigComponent extends PageComponent implements OnInit, Con
private updateModel() {
this.modelValue = {...this.modelValue, ...this.dataKeyFormGroup.value};
if (this.displayAdvanced) {
if (this.hasAdvanced) {
this.modelValue.settings = this.dataKeySettingsFormGroup.get('settings').value.model;
}
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 {
dataKeySettings: {
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.
-->
<section class="tb-widget-settings" [formGroup]="entitiesTableKeySettingsForm" fxLayout="column">
<mat-form-field fxFlex class="mat-block">
<mat-label translate>widgets.table.custom-title</mat-label>
<input matInput formControlName="customTitle">
</mat-form-field>
<mat-form-field fxFlex class="mat-block">
<mat-label translate>widgets.table.column-width</mat-label>
<input matInput formControlName="columnWidth">
</mat-form-field>
<fieldset class="fields-group fields-group-slider">
<legend class="group-title" translate>widgets.table.cell-style</legend>
<ng-container [formGroup]="entitiesTableKeySettingsForm">
<div class="tb-widget-config-panel">
<div class="tb-widget-config-panel-title" translate>widgets.table.column-settings</div>
<div class="tb-widget-config-row">
<div>{{ 'widgets.table.custom-title' | translate }}</div>
<mat-form-field fxFlex appearance="outline" subscriptSizing="dynamic">
<input matInput formControlName="customTitle" placeholder="{{ 'widget-config.set' | translate }}">
</mat-form-field>
</div>
<div class="tb-widget-config-row space-between">
<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-header fxLayout="row wrap">
<mat-expansion-panel-header fxLayout="row wrap" class="fill-width">
<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">
{{ 'widgets.table.use-cell-style-function' | translate }}
</mat-slide-toggle>
@ -48,13 +83,12 @@
</tb-js-func>
</ng-template>
</mat-expansion-panel>
</fieldset>
<fieldset class="fields-group fields-group-slider">
<legend class="group-title" translate>widgets.table.cell-content</legend>
</div>
<div class="tb-widget-config-panel tb-slide-toggle">
<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-slide-toggle formControlName="useCellContentFunction" (click)="$event.stopPropagation()"
<mat-slide-toggle class="mat-slide" formControlName="useCellContentFunction" (click)="$event.stopPropagation()"
fxLayoutAlign="center">
{{ 'widgets.table.use-cell-content-function' | translate }}
</mat-slide-toggle>
@ -73,30 +107,5 @@
</tb-js-func>
</ng-template>
</mat-expansion-panel>
</fieldset>
<mat-form-field fxFlex class="mat-block">
<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>
</div>
</ng-container>

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

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

@ -16,6 +16,10 @@
@import '../../../../../../../scss/constants';
:host {
display: flex;
flex-direction: column;
gap: 16px;
.tb-widget-settings {
.fields-group {
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-title" translate>widget-config.card-style</div>
<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">
<mat-divider vertical></mat-divider>
<tb-color-input asBoxInput
@ -92,7 +92,7 @@
</div>
</div>
<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">
<mat-divider vertical></mat-divider>
<tb-color-input asBoxInput

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

@ -331,7 +331,7 @@
</fieldset>
<div fxLayout="row" fxLayoutAlign="end center" fxLayout.xs="column" fxLayoutAlign.xs="end" fxLayoutGap="16px">
<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()">
{{'admin.send-test-mail' | translate}}
</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 {
const formValue = this.mailSettings.getRawValue() as Required<typeof this.mailSettings.value>;
delete formValue.changePassword;
if (!isDefinedAndNotNull(formValue.password)) {
delete formValue.password;
}
return formValue;
}

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

@ -251,6 +251,7 @@ const routes: Routes = [
{
path: ':dashboardId',
component: DashboardPageComponent,
canDeactivate: [ConfirmOnExitGuard],
data: {
breadcrumb: {
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 { AppState } from '@core/core.state';
import { getCurrentAuthUser } from '@core/auth/auth.selectors';
import { ConfirmOnExitGuard } from '@core/guards/confirm-on-exit.guard';
@Injectable()
export class DashboardResolver implements Resolve<Dashboard> {
@ -85,6 +86,7 @@ const routes: Routes = [
{
path: ':dashboardId',
component: DashboardPageComponent,
canDeactivate: [ConfirmOnExitGuard],
data: {
breadcrumb: {
labelFunction: dashboardBreadcumbLabelFunction,

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

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

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

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

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

@ -1130,6 +1130,7 @@
},
"datakey": {
"settings": "Settings",
"general": "General",
"advanced": "Advanced",
"key": "Key",
"label": "Label",
@ -4242,7 +4243,6 @@
"preview": "Preview",
"set": "Set",
"set-message": "Set message",
"card-title": "Card title",
"advanced-title-style": "Advanced title style",
"card-style": "Card style",
"text": "Text",
@ -5231,9 +5231,16 @@
"display-alarm-activity": "Display alarm activity",
"allow-alarms-assign": "Allow alarms assignment",
"columns": "Columns",
"column-settings": "Column settings",
"remove-column": "Remove 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",

30
ui-ngx/src/styles.scss

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

Loading…
Cancel
Save