Browse Source

Merge branch 'develop/1.5' into develop/1.5-no-more-plugins

pull/725/head
Andrew Shvayka 9 years ago
parent
commit
b16a02d97f
  1. 71
      application/src/main/java/org/thingsboard/server/actors/ActorSystemContext.java
  2. 17
      application/src/main/java/org/thingsboard/server/actors/ruleChain/DefaultTbContext.java
  3. 13
      application/src/main/java/org/thingsboard/server/actors/ruleChain/RuleNodeActor.java
  4. 18
      application/src/main/java/org/thingsboard/server/actors/ruleChain/RuleNodeActorMessageProcessor.java
  5. 37
      application/src/main/java/org/thingsboard/server/actors/ruleChain/RuleNodeToSelfMsg.java
  6. 2
      application/src/main/java/org/thingsboard/server/service/component/AnnotationComponentDiscoveryService.java
  7. 2
      application/src/test/java/org/thingsboard/server/controller/AbstractRuleEngineControllerTest.java
  8. 2
      common/data/src/main/java/org/thingsboard/server/common/data/DataConstants.java
  9. 5
      common/message/src/main/java/org/thingsboard/server/common/msg/MsgType.java
  10. 25
      dao/src/main/java/org/thingsboard/server/dao/exception/BufferLimitException.java
  11. 14
      dao/src/main/java/org/thingsboard/server/dao/nosql/RateLimitedResultSetFuture.java
  12. 24
      dao/src/main/java/org/thingsboard/server/dao/util/BufferedRateLimiter.java
  13. 32
      dao/src/test/java/org/thingsboard/server/dao/nosql/RateLimitedResultSetFutureTest.java
  14. 5
      dao/src/test/java/org/thingsboard/server/dao/util/BufferedRateLimiterTest.java
  15. 1
      pom.xml
  16. 4
      rule-engine/rule-engine-api/src/main/java/org/thingsboard/rule/engine/api/NodeConfiguration.java
  17. 2
      rule-engine/rule-engine-api/src/main/java/org/thingsboard/rule/engine/api/NodeDefinition.java
  18. 4
      rule-engine/rule-engine-api/src/main/java/org/thingsboard/rule/engine/api/RuleNode.java
  19. 3
      rule-engine/rule-engine-api/src/main/java/org/thingsboard/rule/engine/api/TbContext.java
  20. 2
      rule-engine/rule-engine-api/src/main/java/org/thingsboard/rule/engine/api/TbNode.java
  21. 83
      rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/debug/TbMsgGeneratorNode.java
  22. 40
      rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/debug/TbMsgGeneratorNodeConfiguration.java
  23. 11
      rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/filter/TbJsFilterNode.java
  24. 4
      rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/filter/TbJsFilterNodeConfiguration.java
  25. 29
      rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/filter/TbJsSwitchNode.java
  26. 10
      rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/filter/TbJsSwitchNodeConfiguration.java
  27. 7
      rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/filter/TbMsgTypeFilterNode.java
  28. 4
      rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/filter/TbMsgTypeFilterNodeConfiguration.java
  29. 18
      rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/js/NashornJsEngine.java
  30. 2
      rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/metadata/TbEntityGetAttrNode.java
  31. 4
      rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/metadata/TbGetAttributesNode.java
  32. 2
      rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/metadata/TbGetAttributesNodeConfiguration.java
  33. 2
      rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/metadata/TbGetCustomerAttributeNode.java
  34. 2
      rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/metadata/TbGetEntityAttrNodeConfiguration.java
  35. 4
      rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/metadata/TbGetRelatedAttributeNode.java
  36. 2
      rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/metadata/TbGetTenantAttributeNode.java
  37. 2
      rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/transform/TbAbstractTransformNode.java
  38. 2
      rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/transform/TbChangeOriginatorNode.java
  39. 6
      rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/transform/TbTransformMsgNode.java
  40. 2
      rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/transform/TbTransformMsgNodeConfiguration.java
  41. 2
      rule-engine/rule-engine-components/src/main/resources/public/static/rulenode/rulenode-core-config.css
  42. 2
      rule-engine/rule-engine-components/src/main/resources/public/static/rulenode/rulenode-core-config.js
  43. 16
      rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/filter/TbJsFilterNodeTest.java
  44. 54
      rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/filter/TbJsSwitchNodeTest.java
  45. 4
      rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/metadata/TbGetCustomerAttributeNodeTest.java
  46. 2
      rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/transform/TbChangeOriginatorNodeTest.java
  47. 8
      rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/transform/TbTransformMsgNodeTest.java
  48. 2
      ui/package.json
  49. 20
      ui/server.js
  50. 59
      ui/src/app/api/rule-chain.service.js
  51. 30
      ui/src/app/common/types.constant.js
  52. 19
      ui/src/app/components/confirm-on-exit.directive.js
  53. 59
      ui/src/app/components/details-sidenav.directive.js
  54. 2
      ui/src/app/components/details-sidenav.tpl.html
  55. 34
      ui/src/app/components/js-func.directive.js
  56. 10
      ui/src/app/components/js-func.scss
  57. 15
      ui/src/app/components/js-func.tpl.html
  58. 23
      ui/src/app/components/json-object-edit.directive.js
  59. 17
      ui/src/app/event/event-content-dialog.controller.js
  60. 27
      ui/src/app/event/event-header-debug-rulenode.tpl.html
  61. 7
      ui/src/app/event/event-header.directive.js
  62. 63
      ui/src/app/event/event-row-debug-rulenode.tpl.html
  63. 16
      ui/src/app/event/event-row.directive.js
  64. 18
      ui/src/app/event/event-table.directive.js
  65. 24
      ui/src/app/locale/locale.constant.js
  66. 4
      ui/src/app/rulechain/index.js
  67. 197
      ui/src/app/rulechain/rulechain.controller.js
  68. 5
      ui/src/app/rulechain/rulechain.routes.js
  69. 143
      ui/src/app/rulechain/rulechain.scss
  70. 108
      ui/src/app/rulechain/rulechain.tpl.html
  71. 3
      ui/src/app/rulechain/rulechains.tpl.html
  72. 78
      ui/src/app/rulechain/rulenode-config.directive.js
  73. 32
      ui/src/app/rulechain/rulenode-config.tpl.html
  74. 67
      ui/src/app/rulechain/rulenode-defined-config.directive.js
  75. 38
      ui/src/app/rulechain/rulenode-fieldset.tpl.html
  76. 6
      ui/src/app/rulechain/rulenode.scss
  77. 1
      ui/src/app/rulechain/rulenode.tpl.html

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

@ -25,6 +25,7 @@ import com.typesafe.config.Config;
import com.typesafe.config.ConfigFactory; import com.typesafe.config.ConfigFactory;
import lombok.Getter; import lombok.Getter;
import lombok.Setter; import lombok.Setter;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value; import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component; import org.springframework.stereotype.Component;
@ -38,6 +39,7 @@ import org.thingsboard.server.common.data.id.EntityId;
import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.common.data.id.TenantId;
import org.thingsboard.server.common.data.plugin.ComponentLifecycleEvent; import org.thingsboard.server.common.data.plugin.ComponentLifecycleEvent;
import org.thingsboard.server.common.msg.TbMsg; import org.thingsboard.server.common.msg.TbMsg;
import org.thingsboard.server.common.msg.TbMsgDataType;
import org.thingsboard.server.common.msg.cluster.ServerAddress; import org.thingsboard.server.common.msg.cluster.ServerAddress;
import org.thingsboard.server.common.transport.auth.DeviceAuthService; import org.thingsboard.server.common.transport.auth.DeviceAuthService;
import org.thingsboard.server.controller.plugin.PluginWebSocketMsgEndpoint; import org.thingsboard.server.controller.plugin.PluginWebSocketMsgEndpoint;
@ -60,11 +62,13 @@ import org.thingsboard.server.service.cluster.routing.ClusterRoutingService;
import org.thingsboard.server.service.cluster.rpc.ClusterRpcService; import org.thingsboard.server.service.cluster.rpc.ClusterRpcService;
import org.thingsboard.server.service.component.ComponentDiscoveryService; import org.thingsboard.server.service.component.ComponentDiscoveryService;
import java.io.IOException;
import java.io.PrintWriter; import java.io.PrintWriter;
import java.io.StringWriter; import java.io.StringWriter;
import java.nio.charset.StandardCharsets; import java.nio.charset.StandardCharsets;
import java.util.Optional; import java.util.Optional;
@Slf4j
@Component @Component
public class ActorSystemContext { public class ActorSystemContext {
private static final String AKKA_CONF_FILE_NAME = "actor-system.conf"; private static final String AKKA_CONF_FILE_NAME = "actor-system.conf";
@ -292,38 +296,49 @@ public class ActorSystemContext {
} }
private void persistDebug(TenantId tenantId, EntityId entityId, String type, TbMsg tbMsg, Throwable error) { private void persistDebug(TenantId tenantId, EntityId entityId, String type, TbMsg tbMsg, Throwable error) {
Event event = new Event(); try {
event.setTenantId(tenantId); Event event = new Event();
event.setEntityId(entityId); event.setTenantId(tenantId);
event.setType(DataConstants.DEBUG); event.setEntityId(entityId);
event.setType(DataConstants.DEBUG_RULE_NODE);
ObjectNode node = mapper.createObjectNode()
.put("type", type) String metadata = mapper.writeValueAsString(tbMsg.getMetaData().getData());
.put("server", getServerAddress())
.put("entityId", tbMsg.getOriginator().getId().toString()) ObjectNode node = mapper.createObjectNode()
.put("entityName", tbMsg.getOriginator().getEntityType().name()) .put("type", type)
.put("msgId", tbMsg.getId().toString()) .put("server", getServerAddress())
.put("msgType", tbMsg.getType()) .put("entityId", tbMsg.getOriginator().getId().toString())
.put("dataType", tbMsg.getDataType().name()); .put("entityName", tbMsg.getOriginator().getEntityType().name())
.put("msgId", tbMsg.getId().toString())
ObjectNode mdNode = node.putObject("metadata"); .put("msgType", tbMsg.getType())
tbMsg.getMetaData().getData().forEach(mdNode::put); .put("dataType", tbMsg.getDataType().name())
.put("data", convertToString(tbMsg.getDataType(), tbMsg.getData()))
.put("metadata", metadata);
if (error != null) {
node = node.put("error", toString(error));
}
event.setBody(node);
eventService.save(event);
} catch (IOException ex) {
log.warn("Failed to persist rule node debug message", ex);
}
}
switch (tbMsg.getDataType()) { private String convertToString(TbMsgDataType messageType, byte[] data) {
if (data == null) {
return null;
}
switch (messageType) {
case JSON:
case TEXT:
return new String(data, StandardCharsets.UTF_8);
case BINARY: case BINARY:
node.put("data", Base64Utils.encodeUrlSafe(tbMsg.getData())); return Base64Utils.encodeToString(data);
break;
default: default:
node.put("data", new String(tbMsg.getData(), StandardCharsets.UTF_8)); throw new RuntimeException("Message type: " + messageType + " is not supported!");
break;
}
if (error != null) {
node = node.put("error", toString(error));
} }
event.setBody(node);
eventService.save(event);
} }
public static Exception toException(Throwable error) { public static Exception toException(Throwable error) {

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

@ -15,9 +15,12 @@
*/ */
package org.thingsboard.server.actors.ruleChain; package org.thingsboard.server.actors.ruleChain;
import akka.actor.ActorContext;
import akka.actor.ActorRef;
import org.thingsboard.rule.engine.api.ListeningExecutor; import org.thingsboard.rule.engine.api.ListeningExecutor;
import org.thingsboard.rule.engine.api.TbContext; import org.thingsboard.rule.engine.api.TbContext;
import org.thingsboard.server.actors.ActorSystemContext; import org.thingsboard.server.actors.ActorSystemContext;
import org.thingsboard.server.common.data.id.RuleNodeId;
import org.thingsboard.server.common.msg.TbMsg; import org.thingsboard.server.common.msg.TbMsg;
import org.thingsboard.server.common.msg.cluster.ServerAddress; import org.thingsboard.server.common.msg.cluster.ServerAddress;
import org.thingsboard.server.dao.alarm.AlarmService; import org.thingsboard.server.dao.alarm.AlarmService;
@ -30,8 +33,10 @@ import org.thingsboard.server.dao.relation.RelationService;
import org.thingsboard.server.dao.rule.RuleChainService; import org.thingsboard.server.dao.rule.RuleChainService;
import org.thingsboard.server.dao.timeseries.TimeseriesService; import org.thingsboard.server.dao.timeseries.TimeseriesService;
import org.thingsboard.server.dao.user.UserService; import org.thingsboard.server.dao.user.UserService;
import scala.concurrent.duration.Duration;
import java.util.Set; import java.util.Set;
import java.util.concurrent.TimeUnit;
/** /**
* Created by ashvayka on 19.03.18. * Created by ashvayka on 19.03.18.
@ -61,7 +66,12 @@ class DefaultTbContext implements TbContext {
@Override @Override
public void tellSelf(TbMsg msg, long delayMs) { public void tellSelf(TbMsg msg, long delayMs) {
throw new RuntimeException("Not Implemented!"); //TODO: add persistence layer
scheduleMsgWithDelay(new RuleNodeToSelfMsg(msg), delayMs, nodeCtx.getSelfActor());
}
private void scheduleMsgWithDelay(Object msg, long delayInMs, ActorRef target) {
mainCtx.getScheduler().scheduleOnce(Duration.create(delayInMs, TimeUnit.MILLISECONDS), target, msg, mainCtx.getActorSystem().dispatcher(), nodeCtx.getSelfActor());
} }
@Override @Override
@ -92,6 +102,11 @@ class DefaultTbContext implements TbContext {
nodeCtx.getSelfActor().tell(new RuleNodeToSelfErrorMsg(msg, th), nodeCtx.getSelfActor()); nodeCtx.getSelfActor().tell(new RuleNodeToSelfErrorMsg(msg, th), nodeCtx.getSelfActor());
} }
@Override
public RuleNodeId getSelfId() {
return nodeCtx.getSelf().getId();
}
@Override @Override
public void tellNext(TbMsg msg, Set<String> relationTypes) { public void tellNext(TbMsg msg, Set<String> relationTypes) {
relationTypes.forEach(type -> tellNext(msg, type)); relationTypes.forEach(type -> tellNext(msg, type));

13
application/src/main/java/org/thingsboard/server/actors/ruleChain/RuleNodeActor.java

@ -47,12 +47,25 @@ public class RuleNodeActor extends ComponentActor<RuleNodeId, RuleNodeActorMessa
case RULE_TO_SELF_ERROR_MSG: case RULE_TO_SELF_ERROR_MSG:
onRuleNodeToSelfErrorMsg((RuleNodeToSelfErrorMsg) msg); onRuleNodeToSelfErrorMsg((RuleNodeToSelfErrorMsg) msg);
break; break;
case RULE_TO_SELF_MSG:
onRuleNodeToSelfMsg((RuleNodeToSelfMsg) msg);
break;
default: default:
return false; return false;
} }
return true; return true;
} }
private void onRuleNodeToSelfMsg(RuleNodeToSelfMsg msg) {
logger.debug("[{}] Going to process rule msg: {}", id, msg.getMsg());
try {
processor.onRuleToSelfMsg(msg);
increaseMessagesProcessedCount();
} catch (Exception e) {
logAndPersist("onRuleMsg", e);
}
}
private void onRuleChainToRuleNodeMsg(RuleChainToRuleNodeMsg msg) { private void onRuleChainToRuleNodeMsg(RuleChainToRuleNodeMsg msg) {
logger.debug("[{}] Going to process rule msg: {}", id, msg.getMsg()); logger.debug("[{}] Going to process rule msg: {}", id, msg.getMsg());
try { try {

18
application/src/main/java/org/thingsboard/server/actors/ruleChain/RuleNodeActorMessageProcessor.java

@ -18,9 +18,10 @@ package org.thingsboard.server.actors.ruleChain;
import akka.actor.ActorContext; import akka.actor.ActorContext;
import akka.actor.ActorRef; import akka.actor.ActorRef;
import akka.event.LoggingAdapter; import akka.event.LoggingAdapter;
import org.thingsboard.rule.engine.api.TbContext;
import org.thingsboard.rule.engine.api.TbNode; import org.thingsboard.rule.engine.api.TbNode;
import org.thingsboard.rule.engine.api.TbNodeConfiguration; import org.thingsboard.rule.engine.api.TbNodeConfiguration;
import org.thingsboard.rule.engine.api.TbNodeState; import org.thingsboard.rule.engine.api.TbNodeException;
import org.thingsboard.server.actors.ActorSystemContext; import org.thingsboard.server.actors.ActorSystemContext;
import org.thingsboard.server.actors.shared.ComponentMsgProcessor; import org.thingsboard.server.actors.shared.ComponentMsgProcessor;
import org.thingsboard.server.common.data.id.RuleChainId; import org.thingsboard.server.common.data.id.RuleChainId;
@ -31,6 +32,8 @@ import org.thingsboard.server.common.data.rule.RuleNode;
import org.thingsboard.server.common.msg.cluster.ClusterEventMsg; import org.thingsboard.server.common.msg.cluster.ClusterEventMsg;
import org.thingsboard.server.dao.rule.RuleChainService; import org.thingsboard.server.dao.rule.RuleChainService;
import java.util.concurrent.ExecutionException;
/** /**
* @author Andrew Shvayka * @author Andrew Shvayka
*/ */
@ -41,6 +44,7 @@ public class RuleNodeActorMessageProcessor extends ComponentMsgProcessor<RuleNod
private final RuleChainService service; private final RuleChainService service;
private RuleNode ruleNode; private RuleNode ruleNode;
private TbNode tbNode; private TbNode tbNode;
private TbContext defaultCtx;
RuleNodeActorMessageProcessor(TenantId tenantId, RuleChainId ruleChainId, RuleNodeId ruleNodeId, ActorSystemContext systemContext RuleNodeActorMessageProcessor(TenantId tenantId, RuleChainId ruleChainId, RuleNodeId ruleNodeId, ActorSystemContext systemContext
, LoggingAdapter logger, ActorRef parent, ActorRef self) { , LoggingAdapter logger, ActorRef parent, ActorRef self) {
@ -49,6 +53,7 @@ public class RuleNodeActorMessageProcessor extends ComponentMsgProcessor<RuleNod
this.self = self; this.self = self;
this.service = systemContext.getRuleChainService(); this.service = systemContext.getRuleChainService();
this.ruleNode = systemContext.getRuleChainService().findRuleNodeById(entityId); this.ruleNode = systemContext.getRuleChainService().findRuleNodeById(entityId);
this.defaultCtx = new DefaultTbContext(systemContext, new RuleNodeCtx(tenantId, parent, self, ruleNode));
} }
@Override @Override
@ -80,6 +85,14 @@ public class RuleNodeActorMessageProcessor extends ComponentMsgProcessor<RuleNod
} }
public void onRuleToSelfMsg(RuleNodeToSelfMsg msg) throws Exception {
checkActive();
if (ruleNode.isDebugMode()) {
systemContext.persistDebugInput(tenantId, entityId, msg.getMsg());
}
tbNode.onMsg(defaultCtx, msg.getMsg());
}
void onRuleChainToRuleNodeMsg(RuleChainToRuleNodeMsg msg) throws Exception { void onRuleChainToRuleNodeMsg(RuleChainToRuleNodeMsg msg) throws Exception {
checkActive(); checkActive();
if (ruleNode.isDebugMode()) { if (ruleNode.isDebugMode()) {
@ -91,9 +104,8 @@ public class RuleNodeActorMessageProcessor extends ComponentMsgProcessor<RuleNod
private TbNode initComponent(RuleNode ruleNode) throws Exception { private TbNode initComponent(RuleNode ruleNode) throws Exception {
Class<?> componentClazz = Class.forName(ruleNode.getType()); Class<?> componentClazz = Class.forName(ruleNode.getType());
TbNode tbNode = (TbNode) (componentClazz.newInstance()); TbNode tbNode = (TbNode) (componentClazz.newInstance());
tbNode.init(new TbNodeConfiguration(ruleNode.getConfiguration()), new TbNodeState()); tbNode.init(defaultCtx, new TbNodeConfiguration(ruleNode.getConfiguration()));
return tbNode; return tbNode;
} }
} }

37
application/src/main/java/org/thingsboard/server/actors/ruleChain/RuleNodeToSelfMsg.java

@ -0,0 +1,37 @@
/**
* Copyright © 2016-2018 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.ruleChain;
import lombok.Data;
import org.thingsboard.server.common.data.id.RuleNodeId;
import org.thingsboard.server.common.msg.MsgType;
import org.thingsboard.server.common.msg.TbActorMsg;
import org.thingsboard.server.common.msg.TbMsg;
/**
* Created by ashvayka on 19.03.18.
*/
@Data
final class RuleNodeToSelfMsg implements TbActorMsg {
private final TbMsg msg;
@Override
public MsgType getMsgType() {
return MsgType.RULE_TO_SELF_MSG;
}
}

2
application/src/main/java/org/thingsboard/server/service/component/AnnotationComponentDiscoveryService.java

@ -192,6 +192,8 @@ public class AnnotationComponentDiscoveryService implements ComponentDiscoverySe
NodeConfiguration config = configClazz.newInstance(); NodeConfiguration config = configClazz.newInstance();
NodeConfiguration defaultConfiguration = config.defaultConfiguration(); NodeConfiguration defaultConfiguration = config.defaultConfiguration();
nodeDefinition.setDefaultConfiguration(mapper.valueToTree(defaultConfiguration)); nodeDefinition.setDefaultConfiguration(mapper.valueToTree(defaultConfiguration));
nodeDefinition.setUiResources(nodeAnnotation.uiResources());
nodeDefinition.setConfigDirective(nodeAnnotation.configDirective());
return nodeDefinition; return nodeDefinition;
} }

2
application/src/test/java/org/thingsboard/server/controller/AbstractRuleEngineControllerTest.java

@ -51,6 +51,6 @@ public class AbstractRuleEngineControllerTest extends AbstractControllerTest {
TimePageLink pageLink = new TimePageLink(limit); TimePageLink pageLink = new TimePageLink(limit);
return doGetTypedWithTimePageLink("/api/events/{entityType}/{entityId}/{eventType}?tenantId={tenantId}&", return doGetTypedWithTimePageLink("/api/events/{entityType}/{entityId}/{eventType}?tenantId={tenantId}&",
new TypeReference<TimePageData<Event>>() { new TypeReference<TimePageData<Event>>() {
}, pageLink, entityId.getEntityType(), entityId.getId(), DataConstants.DEBUG, tenantId.getId()); }, pageLink, entityId.getEntityType(), entityId.getId(), DataConstants.DEBUG_RULE_NODE, tenantId.getId());
} }
} }

2
common/data/src/main/java/org/thingsboard/server/common/data/DataConstants.java

@ -37,7 +37,7 @@ public class DataConstants {
public static final String ERROR = "ERROR"; public static final String ERROR = "ERROR";
public static final String LC_EVENT = "LC_EVENT"; public static final String LC_EVENT = "LC_EVENT";
public static final String STATS = "STATS"; public static final String STATS = "STATS";
public static final String DEBUG = "DEBUG"; public static final String DEBUG_RULE_NODE = "DEBUG_RULE_NODE";
public static final String ONEWAY = "ONEWAY"; public static final String ONEWAY = "ONEWAY";
public static final String TWOWAY = "TWOWAY"; public static final String TWOWAY = "TWOWAY";

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

@ -54,4 +54,9 @@ public enum MsgType {
*/ */
RULE_TO_SELF_ERROR_MSG, RULE_TO_SELF_ERROR_MSG,
/**
* Message that is sent by RuleActor implementation to RuleActor itself to process the message.
*/
RULE_TO_SELF_MSG,
} }

25
dao/src/main/java/org/thingsboard/server/dao/exception/BufferLimitException.java

@ -0,0 +1,25 @@
/**
* Copyright © 2016-2018 The Thingsboard Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.thingsboard.server.dao.exception;
public class BufferLimitException extends RuntimeException {
private static final long serialVersionUID = 4513762009041887588L;
public BufferLimitException() {
super("Rate Limit Buffer is full");
}
}

14
dao/src/main/java/org/thingsboard/server/dao/nosql/RateLimitedResultSetFuture.java

@ -24,6 +24,7 @@ import com.google.common.util.concurrent.FutureCallback;
import com.google.common.util.concurrent.Futures; import com.google.common.util.concurrent.Futures;
import com.google.common.util.concurrent.ListenableFuture; import com.google.common.util.concurrent.ListenableFuture;
import com.google.common.util.concurrent.Uninterruptibles; import com.google.common.util.concurrent.Uninterruptibles;
import org.thingsboard.server.dao.exception.BufferLimitException;
import org.thingsboard.server.dao.util.AsyncRateLimiter; import org.thingsboard.server.dao.util.AsyncRateLimiter;
import javax.annotation.Nullable; import javax.annotation.Nullable;
@ -35,9 +36,15 @@ public class RateLimitedResultSetFuture implements ResultSetFuture {
private final ListenableFuture<Void> rateLimitFuture; private final ListenableFuture<Void> rateLimitFuture;
public RateLimitedResultSetFuture(Session session, AsyncRateLimiter rateLimiter, Statement statement) { public RateLimitedResultSetFuture(Session session, AsyncRateLimiter rateLimiter, Statement statement) {
this.rateLimitFuture = rateLimiter.acquireAsync(); this.rateLimitFuture = Futures.withFallback(rateLimiter.acquireAsync(), t -> {
if (!(t instanceof BufferLimitException)) {
rateLimiter.release();
}
return Futures.immediateFailedFuture(t);
});
this.originalFuture = Futures.transform(rateLimitFuture, this.originalFuture = Futures.transform(rateLimitFuture,
(Function<Void, ResultSetFuture>) i -> executeAsyncWithRelease(rateLimiter, session, statement)); (Function<Void, ResultSetFuture>) i -> executeAsyncWithRelease(rateLimiter, session, statement));
} }
@Override @Override
@ -108,10 +115,7 @@ public class RateLimitedResultSetFuture implements ResultSetFuture {
try { try {
ResultSetFuture resultSetFuture = Uninterruptibles.getUninterruptibly(originalFuture); ResultSetFuture resultSetFuture = Uninterruptibles.getUninterruptibly(originalFuture);
resultSetFuture.addListener(listener, executor); resultSetFuture.addListener(listener, executor);
} catch (CancellationException e) { } catch (CancellationException | ExecutionException e) {
cancel(false);
return;
} catch (ExecutionException e) {
Futures.immediateFailedFuture(e).addListener(listener, executor); Futures.immediateFailedFuture(e).addListener(listener, executor);
} }
}, executor); }, executor);

24
dao/src/main/java/org/thingsboard/server/dao/util/BufferedRateLimiter.java

@ -23,6 +23,7 @@ import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Value; import org.springframework.beans.factory.annotation.Value;
import org.springframework.scheduling.annotation.Scheduled; import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Component; import org.springframework.stereotype.Component;
import org.thingsboard.server.dao.exception.BufferLimitException;
import java.util.concurrent.*; import java.util.concurrent.*;
import java.util.concurrent.atomic.AtomicInteger; import java.util.concurrent.atomic.AtomicInteger;
@ -41,6 +42,9 @@ public class BufferedRateLimiter implements AsyncRateLimiter {
private final AtomicInteger maxQueueSize = new AtomicInteger(); private final AtomicInteger maxQueueSize = new AtomicInteger();
private final AtomicInteger maxGrantedPermissions = new AtomicInteger(); private final AtomicInteger maxGrantedPermissions = new AtomicInteger();
private final AtomicInteger totalGranted = new AtomicInteger();
private final AtomicInteger totalReleased = new AtomicInteger();
private final AtomicInteger totalRequested = new AtomicInteger();
public BufferedRateLimiter(@Value("${cassandra.query.buffer_size}") int queueLimit, public BufferedRateLimiter(@Value("${cassandra.query.buffer_size}") int queueLimit,
@Value("${cassandra.query.concurrent_limit}") int permitsLimit, @Value("${cassandra.query.concurrent_limit}") int permitsLimit,
@ -53,11 +57,13 @@ public class BufferedRateLimiter implements AsyncRateLimiter {
@Override @Override
public ListenableFuture<Void> acquireAsync() { public ListenableFuture<Void> acquireAsync() {
totalRequested.incrementAndGet();
if (queue.isEmpty()) { if (queue.isEmpty()) {
if (permits.incrementAndGet() <= permitsLimit) { if (permits.incrementAndGet() <= permitsLimit) {
if (permits.get() > maxGrantedPermissions.get()) { if (permits.get() > maxGrantedPermissions.get()) {
maxGrantedPermissions.set(permits.get()); maxGrantedPermissions.set(permits.get());
} }
totalGranted.incrementAndGet();
return Futures.immediateFuture(null); return Futures.immediateFuture(null);
} }
permits.decrementAndGet(); permits.decrementAndGet();
@ -69,6 +75,7 @@ public class BufferedRateLimiter implements AsyncRateLimiter {
@Override @Override
public void release() { public void release() {
permits.decrementAndGet(); permits.decrementAndGet();
totalReleased.incrementAndGet();
reprocessQueue(); reprocessQueue();
} }
@ -80,6 +87,7 @@ public class BufferedRateLimiter implements AsyncRateLimiter {
} }
LockedFuture lockedFuture = queue.poll(); LockedFuture lockedFuture = queue.poll();
if (lockedFuture != null) { if (lockedFuture != null) {
totalGranted.incrementAndGet();
lockedFuture.latch.countDown(); lockedFuture.latch.countDown();
} else { } else {
permits.decrementAndGet(); permits.decrementAndGet();
@ -112,17 +120,20 @@ public class BufferedRateLimiter implements AsyncRateLimiter {
LockedFuture lockedFuture = createLockedFuture(); LockedFuture lockedFuture = createLockedFuture();
if (!queue.offer(lockedFuture, 1, TimeUnit.SECONDS)) { if (!queue.offer(lockedFuture, 1, TimeUnit.SECONDS)) {
lockedFuture.cancelFuture(); lockedFuture.cancelFuture();
return Futures.immediateFailedFuture(new IllegalStateException("Rate Limit Buffer is full. Reject")); return Futures.immediateFailedFuture(new BufferLimitException());
}
if(permits.get() < permitsLimit) {
reprocessQueue();
} }
if(permits.get() < permitsLimit) { if(permits.get() < permitsLimit) {
reprocessQueue(); reprocessQueue();
} }
return lockedFuture.future; return lockedFuture.future;
} catch (InterruptedException e) { } catch (InterruptedException e) {
return Futures.immediateFailedFuture(new IllegalStateException("Rate Limit Task interrupted. Reject")); return Futures.immediateFailedFuture(new BufferLimitException());
} }
} }
return Futures.immediateFailedFuture(new IllegalStateException("Rate Limit Buffer is full. Reject")); return Futures.immediateFailedFuture(new BufferLimitException());
} }
@Scheduled(fixedDelayString = "${cassandra.query.rate_limit_print_interval_ms}") @Scheduled(fixedDelayString = "${cassandra.query.rate_limit_print_interval_ms}")
@ -134,8 +145,11 @@ public class BufferedRateLimiter implements AsyncRateLimiter {
expiredCount++; expiredCount++;
} }
} }
log.info("Permits maxBuffer is [{}] max concurrent [{}] expired [{}] current granted [{}]", maxQueueSize.getAndSet(0), log.info("Permits maxBuffer [{}] maxPermits [{}] expired [{}] currPermits [{}] currBuffer [{}] " +
maxGrantedPermissions.getAndSet(0), expiredCount, permits.get()); "totalPermits [{}] totalRequests [{}] totalReleased [{}]",
maxQueueSize.getAndSet(0), maxGrantedPermissions.getAndSet(0), expiredCount,
permits.get(), queue.size(),
totalGranted.getAndSet(0), totalRequested.getAndSet(0), totalReleased.getAndSet(0));
} }
private class LockedFuture { private class LockedFuture {

32
dao/src/test/java/org/thingsboard/server/dao/nosql/RateLimitedResultSetFutureTest.java

@ -19,16 +19,17 @@ import com.datastax.driver.core.*;
import com.datastax.driver.core.exceptions.UnsupportedFeatureException; import com.datastax.driver.core.exceptions.UnsupportedFeatureException;
import com.google.common.util.concurrent.Futures; import com.google.common.util.concurrent.Futures;
import com.google.common.util.concurrent.ListenableFuture; import com.google.common.util.concurrent.ListenableFuture;
import com.google.common.util.concurrent.MoreExecutors;
import org.junit.Test; import org.junit.Test;
import org.junit.runner.RunWith; import org.junit.runner.RunWith;
import org.mockito.Mock; import org.mockito.Mock;
import org.mockito.Mockito; import org.mockito.Mockito;
import org.mockito.runners.MockitoJUnitRunner; import org.mockito.runners.MockitoJUnitRunner;
import org.mockito.stubbing.Answer; import org.mockito.stubbing.Answer;
import org.thingsboard.server.dao.exception.BufferLimitException;
import org.thingsboard.server.dao.util.AsyncRateLimiter; import org.thingsboard.server.dao.util.AsyncRateLimiter;
import java.util.concurrent.ExecutionException; import java.util.concurrent.*;
import java.util.concurrent.TimeoutException;
import static org.junit.Assert.*; import static org.junit.Assert.*;
import static org.mockito.Mockito.*; import static org.mockito.Mockito.*;
@ -53,7 +54,7 @@ public class RateLimitedResultSetFutureTest {
@Test @Test
public void doNotReleasePermissionIfRateLimitFutureFailed() throws InterruptedException { public void doNotReleasePermissionIfRateLimitFutureFailed() throws InterruptedException {
when(rateLimiter.acquireAsync()).thenReturn(Futures.immediateFailedFuture(new IllegalArgumentException())); when(rateLimiter.acquireAsync()).thenReturn(Futures.immediateFailedFuture(new BufferLimitException()));
resultSetFuture = new RateLimitedResultSetFuture(session, rateLimiter, statement); resultSetFuture = new RateLimitedResultSetFuture(session, rateLimiter, statement);
Thread.sleep(1000L); Thread.sleep(1000L);
verify(rateLimiter).acquireAsync(); verify(rateLimiter).acquireAsync();
@ -153,4 +154,29 @@ public class RateLimitedResultSetFutureTest {
verify(rateLimiter, times(1)).release(); verify(rateLimiter, times(1)).release();
} }
@Test
public void expiredQueryReturnPermit() throws InterruptedException, ExecutionException {
CountDownLatch latch = new CountDownLatch(1);
ListenableFuture<Void> future = MoreExecutors.listeningDecorator(Executors.newFixedThreadPool(1)).submit(() -> {
latch.await();
return null;
});
when(rateLimiter.acquireAsync()).thenReturn(future);
resultSetFuture = new RateLimitedResultSetFuture(session, rateLimiter, statement);
ListenableFuture<Row> transform = Futures.transform(resultSetFuture, ResultSet::one);
// TimeUnit.MILLISECONDS.sleep(200);
future.cancel(false);
latch.countDown();
try {
transform.get();
fail();
} catch (Exception e) {
assertTrue(e instanceof ExecutionException);
}
verify(rateLimiter, times(1)).acquireAsync();
verify(rateLimiter, times(1)).release();
}
} }

5
dao/src/test/java/org/thingsboard/server/dao/util/BufferedRateLimiterTest.java

@ -17,6 +17,7 @@ package org.thingsboard.server.dao.util;
import com.google.common.util.concurrent.*; import com.google.common.util.concurrent.*;
import org.junit.Test; import org.junit.Test;
import org.thingsboard.server.dao.exception.BufferLimitException;
import javax.annotation.Nullable; import javax.annotation.Nullable;
import java.util.concurrent.ExecutionException; import java.util.concurrent.ExecutionException;
@ -61,8 +62,8 @@ public class BufferedRateLimiterTest {
} catch (Exception e) { } catch (Exception e) {
assertTrue(e instanceof ExecutionException); assertTrue(e instanceof ExecutionException);
Throwable actualCause = e.getCause(); Throwable actualCause = e.getCause();
assertTrue(actualCause instanceof IllegalStateException); assertTrue(actualCause instanceof BufferLimitException);
assertEquals("Rate Limit Buffer is full. Reject", actualCause.getMessage()); assertEquals("Rate Limit Buffer is full", actualCause.getMessage());
} }
} }

1
pom.xml

@ -284,6 +284,7 @@
<exclude>src/sh/**</exclude> <exclude>src/sh/**</exclude>
<exclude>src/main/scripts/control/**</exclude> <exclude>src/main/scripts/control/**</exclude>
<exclude>src/main/scripts/windows/**</exclude> <exclude>src/main/scripts/windows/**</exclude>
<exclude>src/main/resources/public/static/rulenode/**</exclude>
</excludes> </excludes>
<mapping> <mapping>
<proto>JAVADOC_STYLE</proto> <proto>JAVADOC_STYLE</proto>

4
rule-engine/rule-engine-api/src/main/java/org/thingsboard/rule/engine/api/NodeConfiguration.java

@ -15,8 +15,8 @@
*/ */
package org.thingsboard.rule.engine.api; package org.thingsboard.rule.engine.api;
public interface NodeConfiguration { public interface NodeConfiguration<T extends NodeConfiguration> {
NodeConfiguration defaultConfiguration(); T defaultConfiguration();
} }

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

@ -29,5 +29,7 @@ public class NodeDefinition {
String[] relationTypes; String[] relationTypes;
boolean customRelations; boolean customRelations;
JsonNode defaultConfiguration; JsonNode defaultConfiguration;
String[] uiResources;
String configDirective;
} }

4
rule-engine/rule-engine-api/src/main/java/org/thingsboard/rule/engine/api/RuleNode.java

@ -45,6 +45,10 @@ public @interface RuleNode {
String[] relationTypes() default {"Success", "Failure"}; String[] relationTypes() default {"Success", "Failure"};
String[] uiResources() default {};
String configDirective() default "";
boolean customRelations() default false; boolean customRelations() default false;
} }

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

@ -15,6 +15,7 @@
*/ */
package org.thingsboard.rule.engine.api; package org.thingsboard.rule.engine.api;
import org.thingsboard.server.common.data.id.RuleNodeId;
import org.thingsboard.server.common.msg.TbMsg; import org.thingsboard.server.common.msg.TbMsg;
import org.thingsboard.server.common.msg.cluster.ServerAddress; import org.thingsboard.server.common.msg.cluster.ServerAddress;
import org.thingsboard.server.dao.alarm.AlarmService; import org.thingsboard.server.dao.alarm.AlarmService;
@ -55,6 +56,8 @@ public interface TbContext {
void tellError(TbMsg msg, Throwable th); void tellError(TbMsg msg, Throwable th);
RuleNodeId getSelfId();
AttributesService getAttributesService(); AttributesService getAttributesService();
CustomerService getCustomerService(); CustomerService getCustomerService();

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

@ -24,7 +24,7 @@ import java.util.concurrent.ExecutionException;
*/ */
public interface TbNode { public interface TbNode {
void init(TbNodeConfiguration configuration, TbNodeState state) throws TbNodeException; void init(TbContext ctx, TbNodeConfiguration configuration) throws TbNodeException;
void onMsg(TbContext ctx, TbMsg msg) throws ExecutionException, InterruptedException, TbNodeException; void onMsg(TbContext ctx, TbMsg msg) throws ExecutionException, InterruptedException, TbNodeException;

83
rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/debug/TbMsgGeneratorNode.java

@ -0,0 +1,83 @@
/**
* Copyright © 2016-2018 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.debug;
import com.datastax.driver.core.utils.UUIDs;
import lombok.extern.slf4j.Slf4j;
import org.thingsboard.rule.engine.TbNodeUtils;
import org.thingsboard.rule.engine.api.ListeningExecutor;
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.filter.TbJsFilterNodeConfiguration;
import org.thingsboard.rule.engine.js.NashornJsEngine;
import org.thingsboard.server.common.data.plugin.ComponentType;
import org.thingsboard.server.common.msg.TbMsg;
import org.thingsboard.server.common.msg.TbMsgMetaData;
import javax.script.Bindings;
import java.nio.charset.StandardCharsets;
import java.util.concurrent.TimeUnit;
import static org.thingsboard.rule.engine.DonAsynchron.withCallback;
@Slf4j
@RuleNode(
type = ComponentType.ACTION,
name = "generator",
configClazz = TbMsgGeneratorNodeConfiguration.class,
nodeDescription = "Periodically generates messages",
nodeDetails = "Generates messages with configurable period. ",
inEnabled = false
)
public class TbMsgGeneratorNode implements TbNode {
public static final String TB_MSG_GENERATOR_NODE_MSG = "TbMsgGeneratorNodeMsg";
private TbMsgGeneratorNodeConfiguration config;
private long delay;
@Override
public void init(TbContext ctx, TbNodeConfiguration configuration) throws TbNodeException {
this.config = TbNodeUtils.convert(configuration, TbMsgGeneratorNodeConfiguration.class);
this.delay = TimeUnit.SECONDS.toMillis(config.getPeriodInSeconds());
ctx.tellSelf(newTickMsg(ctx), delay);
}
@Override
public void onMsg(TbContext ctx, TbMsg msg) {
if (msg.getType().equals(TB_MSG_GENERATOR_NODE_MSG)) {
TbMsgMetaData metaData = new TbMsgMetaData();
if (config.getMsgMetaData() != null) {
config.getMsgMetaData().forEach(metaData::putValue);
}
ctx.tellNext(new TbMsg(UUIDs.timeBased(), config.getMsgType(), ctx.getSelfId(), metaData, config.getMsgBody().getBytes(StandardCharsets.UTF_8)));
ctx.tellSelf(newTickMsg(ctx), delay);
}
}
private TbMsg newTickMsg(TbContext ctx) {
return new TbMsg(UUIDs.timeBased(), TB_MSG_GENERATOR_NODE_MSG, ctx.getSelfId(), new TbMsgMetaData(), new byte[]{});
}
@Override
public void destroy() {
}
}

40
rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/debug/TbMsgGeneratorNodeConfiguration.java

@ -0,0 +1,40 @@
/**
* Copyright © 2016-2018 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.debug;
import lombok.Data;
import org.thingsboard.rule.engine.api.NodeConfiguration;
import java.util.Map;
@Data
public class TbMsgGeneratorNodeConfiguration implements NodeConfiguration<TbMsgGeneratorNodeConfiguration> {
private int msgCount;
private int periodInSeconds;
private String msgType;
private String msgBody;
private Map<String, String> msgMetaData;
@Override
public TbMsgGeneratorNodeConfiguration defaultConfiguration() {
TbMsgGeneratorNodeConfiguration configuration = new TbMsgGeneratorNodeConfiguration();
configuration.setMsgCount(0);
configuration.setPeriodInSeconds(1);
configuration.setMsgType("DebugMsg");
configuration.setMsgBody("{}");
return configuration;
}
}

11
rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/filter/TbJsFilterNode.java

@ -29,22 +29,25 @@ import static org.thingsboard.rule.engine.DonAsynchron.withCallback;
@Slf4j @Slf4j
@RuleNode( @RuleNode(
type = ComponentType.FILTER, type = ComponentType.FILTER,
name = "script", relationTypes = {"True", "False", "Failure"}, name = "script", relationTypes = {"True", "False"},
configClazz = TbJsFilterNodeConfiguration.class, configClazz = TbJsFilterNodeConfiguration.class,
nodeDescription = "Filter incoming messages using JS script", nodeDescription = "Filter incoming messages using JS script",
nodeDetails = "Evaluate incoming Message with configured JS condition. " + nodeDetails = "Evaluate incoming Message with configured JS condition. " +
"If <b>True</b> - send Message via <b>True</b> chain, otherwise <b>False</b> chain is used." + "If <b>True</b> - send Message via <b>True</b> chain, otherwise <b>False</b> chain is used." +
"Message payload can be accessed via <code>msg</code> property. For example <code>msg.temperature < 10;</code>" + "Message payload can be accessed via <code>msg</code> property. For example <code>msg.temperature < 10;</code>" +
"Message metadata can be accessed via <code>meta</code> property. For example <code>meta.customerName === 'John';</code>") "Message metadata can be accessed via <code>metadata</code> property. For example <code>metadata.customerName === 'John';</code>",
uiResources = {"static/rulenode/rulenode-core-config.js"},
configDirective = "tbFilterNodeScriptConfig")
public class TbJsFilterNode implements TbNode { public class TbJsFilterNode implements TbNode {
private TbJsFilterNodeConfiguration config; private TbJsFilterNodeConfiguration config;
private NashornJsEngine jsEngine; private NashornJsEngine jsEngine;
@Override @Override
public void init(TbNodeConfiguration configuration, TbNodeState state) throws TbNodeException { public void init(TbContext ctx, TbNodeConfiguration configuration) throws TbNodeException {
this.config = TbNodeUtils.convert(configuration, TbJsFilterNodeConfiguration.class); this.config = TbNodeUtils.convert(configuration, TbJsFilterNodeConfiguration.class);
this.jsEngine = new NashornJsEngine(config.getJsScript()); this.jsEngine = new NashornJsEngine(config.getJsScript(), "Filter");
} }
@Override @Override

4
rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/filter/TbJsFilterNodeConfiguration.java

@ -19,14 +19,14 @@ import lombok.Data;
import org.thingsboard.rule.engine.api.NodeConfiguration; import org.thingsboard.rule.engine.api.NodeConfiguration;
@Data @Data
public class TbJsFilterNodeConfiguration implements NodeConfiguration { public class TbJsFilterNodeConfiguration implements NodeConfiguration<TbJsFilterNodeConfiguration> {
private String jsScript; private String jsScript;
@Override @Override
public TbJsFilterNodeConfiguration defaultConfiguration() { public TbJsFilterNodeConfiguration defaultConfiguration() {
TbJsFilterNodeConfiguration configuration = new TbJsFilterNodeConfiguration(); TbJsFilterNodeConfiguration configuration = new TbJsFilterNodeConfiguration();
configuration.setJsScript("msg.passed < 15 && msg.name === 'Vit' && meta.temp == 10 && msg.bigObj.prop == 42;"); configuration.setJsScript("return msg.passed < 15 && msg.name === 'Vit' && metadata.temp == 10 && msg.bigObj.prop == 42;");
return configuration; return configuration;
} }
} }

29
rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/filter/TbJsSwitchNode.java

@ -36,31 +36,22 @@ import static org.thingsboard.rule.engine.DonAsynchron.withCallback;
nodeDetails = "Node executes configured JS script. Script should return array of next Chain names where Message should be routed. " + nodeDetails = "Node executes configured JS script. Script should return array of next Chain names where Message should be routed. " +
"If Array is empty - message not routed to next Node. " + "If Array is empty - message not routed to next Node. " +
"Message payload can be accessed via <code>msg</code> property. For example <code>msg.temperature < 10;</code> " + "Message payload can be accessed via <code>msg</code> property. For example <code>msg.temperature < 10;</code> " +
"Message metadata can be accessed via <code>meta</code> property. For example <code>meta.customerName === 'John';</code>") "Message metadata can be accessed via <code>metadata</code> property. For example <code>metadata.customerName === 'John';</code>",
uiResources = {"static/rulenode/rulenode-core-config.js"},
configDirective = "tbFilterNodeSwitchConfig")
public class TbJsSwitchNode implements TbNode { public class TbJsSwitchNode implements TbNode {
private TbJsSwitchNodeConfiguration config; private TbJsSwitchNodeConfiguration config;
private NashornJsEngine jsEngine; private NashornJsEngine jsEngine;
@Override @Override
public void init(TbNodeConfiguration configuration, TbNodeState state) throws TbNodeException { public void init(TbContext ctx, TbNodeConfiguration configuration) throws TbNodeException {
this.config = TbNodeUtils.convert(configuration, TbJsSwitchNodeConfiguration.class); this.config = TbNodeUtils.convert(configuration, TbJsSwitchNodeConfiguration.class);
if (config.getAllowedRelations().size() < 1) { this.jsEngine = new NashornJsEngine(config.getJsScript(), "Switch");
String message = "Switch node should have at least 1 relation";
log.error(message);
throw new IllegalStateException(message);
}
if (!config.isRouteToAllWithNoCheck()) {
this.jsEngine = new NashornJsEngine(config.getJsScript());
}
} }
@Override @Override
public void onMsg(TbContext ctx, TbMsg msg) { public void onMsg(TbContext ctx, TbMsg msg) {
if (config.isRouteToAllWithNoCheck()) {
ctx.tellNext(msg, config.getAllowedRelations());
return;
}
ListeningExecutor jsExecutor = ctx.getJsExecutor(); ListeningExecutor jsExecutor = ctx.getJsExecutor();
withCallback(jsExecutor.executeAsync(() -> jsEngine.executeSwitch(toBindings(msg))), withCallback(jsExecutor.executeAsync(() -> jsEngine.executeSwitch(toBindings(msg))),
result -> processSwitch(ctx, msg, result), result -> processSwitch(ctx, msg, result),
@ -68,15 +59,7 @@ public class TbJsSwitchNode implements TbNode {
} }
private void processSwitch(TbContext ctx, TbMsg msg, Set<String> nextRelations) { private void processSwitch(TbContext ctx, TbMsg msg, Set<String> nextRelations) {
if (validateRelations(nextRelations)) { ctx.tellNext(msg, nextRelations);
ctx.tellNext(msg, nextRelations);
} else {
ctx.tellError(msg, new IllegalStateException("Unsupported relation for switch " + nextRelations));
}
}
private boolean validateRelations(Set<String> nextRelations) {
return config.getAllowedRelations().containsAll(nextRelations);
} }
private Bindings toBindings(TbMsg msg) { private Bindings toBindings(TbMsg msg) {

10
rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/filter/TbJsSwitchNodeConfiguration.java

@ -22,22 +22,18 @@ import org.thingsboard.rule.engine.api.NodeConfiguration;
import java.util.Set; import java.util.Set;
@Data @Data
public class TbJsSwitchNodeConfiguration implements NodeConfiguration { public class TbJsSwitchNodeConfiguration implements NodeConfiguration<TbJsSwitchNodeConfiguration> {
private String jsScript; private String jsScript;
private Set<String> allowedRelations;
private boolean routeToAllWithNoCheck;
@Override @Override
public TbJsSwitchNodeConfiguration defaultConfiguration() { public TbJsSwitchNodeConfiguration defaultConfiguration() {
TbJsSwitchNodeConfiguration configuration = new TbJsSwitchNodeConfiguration(); TbJsSwitchNodeConfiguration configuration = new TbJsSwitchNodeConfiguration();
configuration.setJsScript("function nextRelation(meta, msg) {\n" + configuration.setJsScript("function nextRelation(metadata, msg) {\n" +
" return ['one','nine'];" + " return ['one','nine'];" +
"};\n" + "};\n" +
"\n" + "\n" +
"nextRelation(meta, msg);"); "return nextRelation(metadata, msg);");
configuration.setAllowedRelations(Sets.newHashSet("one", "two"));
configuration.setRouteToAllWithNoCheck(false);
return configuration; return configuration;
} }
} }

7
rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/filter/TbMsgTypeFilterNode.java

@ -29,15 +29,18 @@ import org.thingsboard.server.common.msg.TbMsg;
type = ComponentType.FILTER, type = ComponentType.FILTER,
name = "message type", name = "message type",
configClazz = TbMsgTypeFilterNodeConfiguration.class, configClazz = TbMsgTypeFilterNodeConfiguration.class,
relationTypes = {"True", "False"},
nodeDescription = "Filter incoming messages by Message Type", nodeDescription = "Filter incoming messages by Message Type",
nodeDetails = "Evaluate incoming Message with configured JS condition. " + nodeDetails = "Evaluate incoming Message with configured JS condition. " +
"If incoming MessageType is expected - send Message via <b>Success</b> chain, otherwise <b>Failure</b> chain is used.") "If incoming MessageType is expected - send Message via <b>Success</b> chain, otherwise <b>Failure</b> chain is used.",
uiResources = {"static/rulenode/rulenode-core-config.js", "static/rulenode/rulenode-core-config.css"},
configDirective = "tbFilterNodeMessageTypeConfig")
public class TbMsgTypeFilterNode implements TbNode { public class TbMsgTypeFilterNode implements TbNode {
TbMsgTypeFilterNodeConfiguration config; TbMsgTypeFilterNodeConfiguration config;
@Override @Override
public void init(TbNodeConfiguration configuration, TbNodeState state) throws TbNodeException { public void init(TbContext ctx, TbNodeConfiguration configuration) throws TbNodeException {
this.config = TbNodeUtils.convert(configuration, TbMsgTypeFilterNodeConfiguration.class); this.config = TbNodeUtils.convert(configuration, TbMsgTypeFilterNodeConfiguration.class);
} }

4
rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/filter/TbMsgTypeFilterNodeConfiguration.java

@ -26,14 +26,14 @@ import java.util.List;
* Created by ashvayka on 19.01.18. * Created by ashvayka on 19.01.18.
*/ */
@Data @Data
public class TbMsgTypeFilterNodeConfiguration implements NodeConfiguration { public class TbMsgTypeFilterNodeConfiguration implements NodeConfiguration<TbMsgTypeFilterNodeConfiguration> {
private List<String> messageTypes; private List<String> messageTypes;
@Override @Override
public TbMsgTypeFilterNodeConfiguration defaultConfiguration() { public TbMsgTypeFilterNodeConfiguration defaultConfiguration() {
TbMsgTypeFilterNodeConfiguration configuration = new TbMsgTypeFilterNodeConfiguration(); TbMsgTypeFilterNodeConfiguration configuration = new TbMsgTypeFilterNodeConfiguration();
configuration.setMessageTypes(Arrays.asList("GET_ATTRIBUTES","POST_ATTRIBUTES","POST_TELEMETRY","RPC_REQUEST")); configuration.setMessageTypes(Arrays.asList("POST_ATTRIBUTES","POST_TELEMETRY","RPC_REQUEST"));
return configuration; return configuration;
} }
} }

18
rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/js/NashornJsEngine.java

@ -34,14 +34,20 @@ import java.util.Set;
@Slf4j @Slf4j
public class NashornJsEngine { public class NashornJsEngine {
public static final String METADATA = "meta"; public static final String METADATA = "metadata";
public static final String DATA = "msg"; public static final String DATA = "msg";
private static final String JS_WRAPPER_PREFIX_TEMPLATE = "function %s(msg, metadata) { ";
private static final String JS_WRAPPER_SUFFIX_TEMPLATE = "}\n %s(msg, metadata);";
private static NashornScriptEngineFactory factory = new NashornScriptEngineFactory(); private static NashornScriptEngineFactory factory = new NashornScriptEngineFactory();
private CompiledScript engine; private CompiledScript engine;
public NashornJsEngine(String script) { public NashornJsEngine(String script, String functionName) {
engine = compileScript(script); String jsWrapperPrefix = String.format(JS_WRAPPER_PREFIX_TEMPLATE, functionName);
String jsWrapperSuffix = String.format(JS_WRAPPER_SUFFIX_TEMPLATE, functionName);
engine = compileScript(jsWrapperPrefix + script + jsWrapperSuffix);
} }
private static CompiledScript compileScript(String script) { private static CompiledScript compileScript(String script) {
@ -58,15 +64,15 @@ public class NashornJsEngine {
public static Bindings bindMsg(TbMsg msg) { public static Bindings bindMsg(TbMsg msg) {
try { try {
Bindings bindings = new SimpleBindings(); Bindings bindings = new SimpleBindings();
bindings.put(METADATA, msg.getMetaData().getData());
if (ArrayUtils.isNotEmpty(msg.getData())) { if (ArrayUtils.isNotEmpty(msg.getData())) {
ObjectMapper mapper = new ObjectMapper(); ObjectMapper mapper = new ObjectMapper();
JsonNode jsonNode = mapper.readTree(msg.getData()); JsonNode jsonNode = mapper.readTree(msg.getData());
Map map = mapper.treeToValue(jsonNode, Map.class); Map map = mapper.treeToValue(jsonNode, Map.class);
bindings.put(DATA, map); bindings.put(DATA, map);
} else {
bindings.put(DATA, Collections.emptyMap());
} }
bindings.put(METADATA, msg.getMetaData().getData());
return bindings; return bindings;
} catch (Throwable th) { } catch (Throwable th) {
throw new IllegalArgumentException("Cannot bind js args", th); throw new IllegalArgumentException("Cannot bind js args", th);

2
rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/metadata/TbEntityGetAttrNode.java

@ -37,7 +37,7 @@ public abstract class TbEntityGetAttrNode<T extends EntityId> implements TbNode
private TbGetEntityAttrNodeConfiguration config; private TbGetEntityAttrNodeConfiguration config;
@Override @Override
public void init(TbNodeConfiguration configuration, TbNodeState state) throws TbNodeException { public void init(TbContext ctx, TbNodeConfiguration configuration) throws TbNodeException {
this.config = TbNodeUtils.convert(configuration, TbGetEntityAttrNodeConfiguration.class); this.config = TbNodeUtils.convert(configuration, TbGetEntityAttrNodeConfiguration.class);
} }

4
rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/metadata/TbGetAttributesNode.java

@ -42,14 +42,14 @@ import static org.thingsboard.server.common.data.DataConstants.*;
nodeDescription = "Add Message Originator Attributes or Latest Telemetry into Message Metadata", nodeDescription = "Add Message Originator Attributes or Latest Telemetry into Message Metadata",
nodeDetails = "If Attributes enrichment configured, <b>CLIENT/SHARED/SERVER</b> attributes are added into Message metadata " + nodeDetails = "If Attributes enrichment configured, <b>CLIENT/SHARED/SERVER</b> attributes are added into Message metadata " +
"with specific prefix: <i>cs/shared/ss</i>. To access those attributes in other nodes this template can be used " + "with specific prefix: <i>cs/shared/ss</i>. To access those attributes in other nodes this template can be used " +
"<code>meta.cs.temperature</code> or <code>meta.shared.limit</code> " + "<code>metadata.cs.temperature</code> or <code>metadata.shared.limit</code> " +
"If Latest Telemetry enrichment configured, latest telemetry added into metadata without prefix.") "If Latest Telemetry enrichment configured, latest telemetry added into metadata without prefix.")
public class TbGetAttributesNode implements TbNode { public class TbGetAttributesNode implements TbNode {
private TbGetAttributesNodeConfiguration config; private TbGetAttributesNodeConfiguration config;
@Override @Override
public void init(TbNodeConfiguration configuration, TbNodeState state) throws TbNodeException { public void init(TbContext ctx, TbNodeConfiguration configuration) throws TbNodeException {
this.config = TbNodeUtils.convert(configuration, TbGetAttributesNodeConfiguration.class); this.config = TbNodeUtils.convert(configuration, TbGetAttributesNodeConfiguration.class);
} }

2
rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/metadata/TbGetAttributesNodeConfiguration.java

@ -25,7 +25,7 @@ import java.util.List;
* Created by ashvayka on 19.01.18. * Created by ashvayka on 19.01.18.
*/ */
@Data @Data
public class TbGetAttributesNodeConfiguration implements NodeConfiguration { public class TbGetAttributesNodeConfiguration implements NodeConfiguration<TbGetAttributesNodeConfiguration> {
private List<String> clientAttributeNames; private List<String> clientAttributeNames;
private List<String> sharedAttributeNames; private List<String> sharedAttributeNames;

2
rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/metadata/TbGetCustomerAttributeNode.java

@ -30,7 +30,7 @@ import org.thingsboard.server.common.data.plugin.ComponentType;
nodeDescription = "Add Originators Customer Attributes or Latest Telemetry into Message Metadata", nodeDescription = "Add Originators Customer Attributes or Latest Telemetry into Message Metadata",
nodeDetails = "If Attributes enrichment configured, server scope attributes are added into Message metadata. " + nodeDetails = "If Attributes enrichment configured, server scope attributes are added into Message metadata. " +
"To access those attributes in other nodes this template can be used " + "To access those attributes in other nodes this template can be used " +
"<code>meta.temperature</code>. If Latest Telemetry enrichment configured, latest telemetry added into metadata") "<code>metadata.temperature</code>. If Latest Telemetry enrichment configured, latest telemetry added into metadata")
public class TbGetCustomerAttributeNode extends TbEntityGetAttrNode<CustomerId> { public class TbGetCustomerAttributeNode extends TbEntityGetAttrNode<CustomerId> {
@Override @Override

2
rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/metadata/TbGetEntityAttrNodeConfiguration.java

@ -23,7 +23,7 @@ import java.util.Map;
import java.util.Optional; import java.util.Optional;
@Data @Data
public class TbGetEntityAttrNodeConfiguration implements NodeConfiguration { public class TbGetEntityAttrNodeConfiguration implements NodeConfiguration<TbGetEntityAttrNodeConfiguration> {
private Map<String, String> attrMapping; private Map<String, String> attrMapping;
private boolean isTelemetry = false; private boolean isTelemetry = false;

4
rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/metadata/TbGetRelatedAttributeNode.java

@ -32,13 +32,13 @@ import org.thingsboard.server.common.data.plugin.ComponentType;
"If multiple Related Entities are found, only first Entity is used for attributes enrichment, other entities are discarded. " + "If multiple Related Entities are found, only first Entity is used for attributes enrichment, other entities are discarded. " +
"If Attributes enrichment configured, server scope attributes are added into Message metadata. " + "If Attributes enrichment configured, server scope attributes are added into Message metadata. " +
"To access those attributes in other nodes this template can be used " + "To access those attributes in other nodes this template can be used " +
"<code>meta.temperature</code>. If Latest Telemetry enrichment configured, latest telemetry added into metadata") "<code>metadata.temperature</code>. If Latest Telemetry enrichment configured, latest telemetry added into metadata")
public class TbGetRelatedAttributeNode extends TbEntityGetAttrNode<EntityId> { public class TbGetRelatedAttributeNode extends TbEntityGetAttrNode<EntityId> {
private TbGetRelatedAttrNodeConfiguration config; private TbGetRelatedAttrNodeConfiguration config;
@Override @Override
public void init(TbNodeConfiguration configuration, TbNodeState state) throws TbNodeException { public void init(TbContext context, TbNodeConfiguration configuration) throws TbNodeException {
this.config = TbNodeUtils.convert(configuration, TbGetRelatedAttrNodeConfiguration.class); this.config = TbNodeUtils.convert(configuration, TbGetRelatedAttrNodeConfiguration.class);
setConfig(config); setConfig(config);
} }

2
rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/metadata/TbGetTenantAttributeNode.java

@ -32,7 +32,7 @@ import org.thingsboard.server.common.data.plugin.ComponentType;
nodeDescription = "Add Originators Tenant Attributes or Latest Telemetry into Message Metadata", nodeDescription = "Add Originators Tenant Attributes or Latest Telemetry into Message Metadata",
nodeDetails = "If Attributes enrichment configured, server scope attributes are added into Message metadata. " + nodeDetails = "If Attributes enrichment configured, server scope attributes are added into Message metadata. " +
"To access those attributes in other nodes this template can be used " + "To access those attributes in other nodes this template can be used " +
"<code>meta.temperature</code>. If Latest Telemetry enrichment configured, latest telemetry added into metadata") "<code>metadata.temperature</code>. If Latest Telemetry enrichment configured, latest telemetry added into metadata")
public class TbGetTenantAttributeNode extends TbEntityGetAttrNode<TenantId> { public class TbGetTenantAttributeNode extends TbEntityGetAttrNode<TenantId> {
@Override @Override

2
rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/transform/TbAbstractTransformNode.java

@ -32,7 +32,7 @@ public abstract class TbAbstractTransformNode implements TbNode {
private TbTransformNodeConfiguration config; private TbTransformNodeConfiguration config;
@Override @Override
public void init(TbNodeConfiguration configuration, TbNodeState state) throws TbNodeException { public void init(TbContext context, TbNodeConfiguration configuration) throws TbNodeException {
this.config = TbNodeUtils.convert(configuration, TbTransformNodeConfiguration.class); this.config = TbNodeUtils.convert(configuration, TbTransformNodeConfiguration.class);
} }

2
rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/transform/TbChangeOriginatorNode.java

@ -49,7 +49,7 @@ public class TbChangeOriginatorNode extends TbAbstractTransformNode {
private TbChangeOriginatorNodeConfiguration config; private TbChangeOriginatorNodeConfiguration config;
@Override @Override
public void init(TbNodeConfiguration configuration, TbNodeState state) throws TbNodeException { public void init(TbContext ctx, TbNodeConfiguration configuration) throws TbNodeException {
this.config = TbNodeUtils.convert(configuration, TbChangeOriginatorNodeConfiguration.class); this.config = TbNodeUtils.convert(configuration, TbChangeOriginatorNodeConfiguration.class);
validateConfig(config); validateConfig(config);
setConfig(config); setConfig(config);

6
rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/transform/TbTransformMsgNode.java

@ -30,7 +30,7 @@ import javax.script.Bindings;
configClazz = TbTransformMsgNodeConfiguration.class, configClazz = TbTransformMsgNodeConfiguration.class,
nodeDescription = "Change Message payload and Metadata using JavaScript", nodeDescription = "Change Message payload and Metadata using JavaScript",
nodeDetails = "JavaScript function recieve 2 input parameters that can be changed inside.<br/> " + nodeDetails = "JavaScript function recieve 2 input parameters that can be changed inside.<br/> " +
"<code>meta</code> - is a Message metadata.<br/>" + "<code>metadata</code> - is a Message metadata.<br/>" +
"<code>msg</code> - is a Message payload.<br/>Any properties can be changed/removed/added in those objects.") "<code>msg</code> - is a Message payload.<br/>Any properties can be changed/removed/added in those objects.")
public class TbTransformMsgNode extends TbAbstractTransformNode { public class TbTransformMsgNode extends TbAbstractTransformNode {
@ -38,9 +38,9 @@ public class TbTransformMsgNode extends TbAbstractTransformNode {
private NashornJsEngine jsEngine; private NashornJsEngine jsEngine;
@Override @Override
public void init(TbNodeConfiguration configuration, TbNodeState state) throws TbNodeException { public void init(TbContext ctx, TbNodeConfiguration configuration) throws TbNodeException {
this.config = TbNodeUtils.convert(configuration, TbTransformMsgNodeConfiguration.class); this.config = TbNodeUtils.convert(configuration, TbTransformMsgNodeConfiguration.class);
this.jsEngine = new NashornJsEngine(config.getJsScript()); this.jsEngine = new NashornJsEngine(config.getJsScript(), "Transform");
setConfig(config); setConfig(config);
} }

2
rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/transform/TbTransformMsgNodeConfiguration.java

@ -27,7 +27,7 @@ public class TbTransformMsgNodeConfiguration extends TbTransformNodeConfiguratio
public TbTransformMsgNodeConfiguration defaultConfiguration() { public TbTransformMsgNodeConfiguration defaultConfiguration() {
TbTransformMsgNodeConfiguration configuration = new TbTransformMsgNodeConfiguration(); TbTransformMsgNodeConfiguration configuration = new TbTransformMsgNodeConfiguration();
configuration.setStartNewChain(false); configuration.setStartNewChain(false);
configuration.setJsScript("msg.passed = msg.passed * meta.temp; msg.bigObj.newProp = 'Ukraine' "); configuration.setJsScript("return msg.passed = msg.passed * metadata.temp; msg.bigObj.newProp = 'Ukraine' ");
return configuration; return configuration;
} }
} }

2
rule-engine/rule-engine-components/src/main/resources/public/static/rulenode/rulenode-core-config.css

@ -0,0 +1,2 @@
.tb-message-type-autocomplete .tb-not-found{display:block;line-height:1.5;height:48px}.tb-message-type-autocomplete .tb-not-found .tb-no-entries{line-height:48px}.tb-message-type-autocomplete li{height:auto!important;white-space:normal!important}
/*# sourceMappingURL=rulenode-core-config.css.map*/

2
rule-engine/rule-engine-components/src/main/resources/public/static/rulenode/rulenode-core-config.js

File diff suppressed because one or more lines are too long

16
rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/filter/TbJsFilterNodeTest.java

@ -51,7 +51,7 @@ public class TbJsFilterNodeTest {
@Test @Test
public void falseEvaluationDoNotSendMsg() throws TbNodeException { public void falseEvaluationDoNotSendMsg() throws TbNodeException {
initWithScript("10 > 15;"); initWithScript("return 10 > 15;");
TbMsg msg = new TbMsg(UUIDs.timeBased(), "USER", null, new TbMsgMetaData(), "{}".getBytes()); TbMsg msg = new TbMsg(UUIDs.timeBased(), "USER", null, new TbMsgMetaData(), "{}".getBytes());
mockJsExecutor(); mockJsExecutor();
@ -64,7 +64,7 @@ public class TbJsFilterNodeTest {
@Test @Test
public void notValidMsgDataThrowsException() throws TbNodeException { public void notValidMsgDataThrowsException() throws TbNodeException {
initWithScript("10 > 15;"); initWithScript("return 10 > 15;");
TbMsg msg = new TbMsg(UUIDs.timeBased(), "USER", null, new TbMsgMetaData(), new byte[4]); TbMsg msg = new TbMsg(UUIDs.timeBased(), "USER", null, new TbMsgMetaData(), new byte[4]);
when(ctx.getJsExecutor()).thenReturn(executor); when(ctx.getJsExecutor()).thenReturn(executor);
@ -77,7 +77,7 @@ public class TbJsFilterNodeTest {
@Test @Test
public void exceptionInJsThrowsException() throws TbNodeException { public void exceptionInJsThrowsException() throws TbNodeException {
initWithScript("meta.temp.curr < 15;"); initWithScript("return metadata.temp.curr < 15;");
TbMsgMetaData metaData = new TbMsgMetaData(); TbMsgMetaData metaData = new TbMsgMetaData();
TbMsg msg = new TbMsg(UUIDs.timeBased(), "USER", null, metaData, "{}".getBytes()); TbMsg msg = new TbMsg(UUIDs.timeBased(), "USER", null, metaData, "{}".getBytes());
mockJsExecutor(); mockJsExecutor();
@ -89,12 +89,12 @@ public class TbJsFilterNodeTest {
@Test(expected = IllegalArgumentException.class) @Test(expected = IllegalArgumentException.class)
public void notValidScriptThrowsException() throws TbNodeException { public void notValidScriptThrowsException() throws TbNodeException {
initWithScript("10 > 15 asdq out"); initWithScript("return 10 > 15 asdq out");
} }
@Test @Test
public void metadataConditionCanBeFalse() throws TbNodeException { public void metadataConditionCanBeFalse() throws TbNodeException {
initWithScript("meta.humidity < 15;"); initWithScript("return metadata.humidity < 15;");
TbMsgMetaData metaData = new TbMsgMetaData(); TbMsgMetaData metaData = new TbMsgMetaData();
metaData.putValue("temp", "10"); metaData.putValue("temp", "10");
metaData.putValue("humidity", "99"); metaData.putValue("humidity", "99");
@ -109,7 +109,7 @@ public class TbJsFilterNodeTest {
@Test @Test
public void metadataConditionCanBeTrue() throws TbNodeException { public void metadataConditionCanBeTrue() throws TbNodeException {
initWithScript("meta.temp < 15;"); initWithScript("return metadata.temp < 15;");
TbMsgMetaData metaData = new TbMsgMetaData(); TbMsgMetaData metaData = new TbMsgMetaData();
metaData.putValue("temp", "10"); metaData.putValue("temp", "10");
metaData.putValue("humidity", "99"); metaData.putValue("humidity", "99");
@ -123,7 +123,7 @@ public class TbJsFilterNodeTest {
@Test @Test
public void msgJsonParsedAndBinded() throws TbNodeException { public void msgJsonParsedAndBinded() throws TbNodeException {
initWithScript("msg.passed < 15 && msg.name === 'Vit' && meta.temp == 10 && msg.bigObj.prop == 42;"); initWithScript("return msg.passed < 15 && msg.name === 'Vit' && metadata.temp == 10 && msg.bigObj.prop == 42;");
TbMsgMetaData metaData = new TbMsgMetaData(); TbMsgMetaData metaData = new TbMsgMetaData();
metaData.putValue("temp", "10"); metaData.putValue("temp", "10");
metaData.putValue("humidity", "99"); metaData.putValue("humidity", "99");
@ -144,7 +144,7 @@ public class TbJsFilterNodeTest {
TbNodeConfiguration nodeConfiguration = new TbNodeConfiguration(mapper.valueToTree(config)); TbNodeConfiguration nodeConfiguration = new TbNodeConfiguration(mapper.valueToTree(config));
node = new TbJsFilterNode(); node = new TbJsFilterNode();
node.init(nodeConfiguration, null); node.init(null, nodeConfiguration);
} }
private void mockJsExecutor() { private void mockJsExecutor() {

54
rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/filter/TbJsSwitchNodeTest.java

@ -52,28 +52,17 @@ public class TbJsSwitchNodeTest {
@Mock @Mock
private ListeningExecutor executor; private ListeningExecutor executor;
@Test
public void routeToAllDoNotEvaluatesJs() throws TbNodeException {
HashSet<String> relations = Sets.newHashSet("one", "two");
initWithScript("test qwerty", relations, true);
TbMsg msg = new TbMsg(UUIDs.timeBased(), "USER", null, new TbMsgMetaData(), "{}".getBytes());
node.onMsg(ctx, msg);
verify(ctx).tellNext(msg, relations);
verifyNoMoreInteractions(ctx, executor);
}
@Test @Test
public void multipleRoutesAreAllowed() throws TbNodeException { public void multipleRoutesAreAllowed() throws TbNodeException {
String jsCode = "function nextRelation(meta, msg) {\n" + String jsCode = "function nextRelation(metadata, msg) {\n" +
" if(msg.passed == 5 && meta.temp == 10)\n" + " if(msg.passed == 5 && metadata.temp == 10)\n" +
" return ['three', 'one']\n" + " return ['three', 'one']\n" +
" else\n" + " else\n" +
" return 'two';\n" + " return 'two';\n" +
"};\n" + "};\n" +
"\n" + "\n" +
"nextRelation(meta, msg);"; "return nextRelation(metadata, msg);";
initWithScript(jsCode, Sets.newHashSet("one", "two", "three"), false); initWithScript(jsCode);
TbMsgMetaData metaData = new TbMsgMetaData(); TbMsgMetaData metaData = new TbMsgMetaData();
metaData.putValue("temp", "10"); metaData.putValue("temp", "10");
metaData.putValue("humidity", "99"); metaData.putValue("humidity", "99");
@ -89,15 +78,15 @@ public class TbJsSwitchNodeTest {
@Test @Test
public void allowedRelationPassed() throws TbNodeException { public void allowedRelationPassed() throws TbNodeException {
String jsCode = "function nextRelation(meta, msg) {\n" + String jsCode = "function nextRelation(metadata, msg) {\n" +
" if(msg.passed == 5 && meta.temp == 10)\n" + " if(msg.passed == 5 && metadata.temp == 10)\n" +
" return 'one'\n" + " return 'one'\n" +
" else\n" + " else\n" +
" return 'two';\n" + " return 'two';\n" +
"};\n" + "};\n" +
"\n" + "\n" +
"nextRelation(meta, msg);"; "return nextRelation(metadata, msg);";
initWithScript(jsCode, Sets.newHashSet("one", "two"), false); initWithScript(jsCode);
TbMsgMetaData metaData = new TbMsgMetaData(); TbMsgMetaData metaData = new TbMsgMetaData();
metaData.putValue("temp", "10"); metaData.putValue("temp", "10");
metaData.putValue("humidity", "99"); metaData.putValue("humidity", "99");
@ -111,37 +100,14 @@ public class TbJsSwitchNodeTest {
verify(ctx).tellNext(msg, Sets.newHashSet("one")); verify(ctx).tellNext(msg, Sets.newHashSet("one"));
} }
@Test private void initWithScript(String script) throws TbNodeException {
public void unknownRelationThrowsException() throws TbNodeException {
String jsCode = "function nextRelation(meta, msg) {\n" +
" return ['one','nine'];" +
"};\n" +
"\n" +
"nextRelation(meta, msg);";
initWithScript(jsCode, Sets.newHashSet("one", "two"), false);
TbMsgMetaData metaData = new TbMsgMetaData();
metaData.putValue("temp", "10");
metaData.putValue("humidity", "99");
String rawJson = "{\"name\": \"Vit\", \"passed\": 5}";
TbMsg msg = new TbMsg(UUIDs.timeBased(), "USER", null, metaData, rawJson.getBytes());
mockJsExecutor();
node.onMsg(ctx, msg);
verify(ctx).getJsExecutor();
verifyError(msg, "Unsupported relation for switch [nine, one]", IllegalStateException.class);
}
private void initWithScript(String script, Set<String> relations, boolean routeToAll) throws TbNodeException {
TbJsSwitchNodeConfiguration config = new TbJsSwitchNodeConfiguration(); TbJsSwitchNodeConfiguration config = new TbJsSwitchNodeConfiguration();
config.setJsScript(script); config.setJsScript(script);
config.setAllowedRelations(relations);
config.setRouteToAllWithNoCheck(routeToAll);
ObjectMapper mapper = new ObjectMapper(); ObjectMapper mapper = new ObjectMapper();
TbNodeConfiguration nodeConfiguration = new TbNodeConfiguration(mapper.valueToTree(config)); TbNodeConfiguration nodeConfiguration = new TbNodeConfiguration(mapper.valueToTree(config));
node = new TbJsSwitchNode(); node = new TbJsSwitchNode();
node.init(nodeConfiguration, null); node.init(null, nodeConfiguration);
} }
private void mockJsExecutor() { private void mockJsExecutor() {

4
rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/metadata/TbGetCustomerAttributeNodeTest.java

@ -88,7 +88,7 @@ public class TbGetCustomerAttributeNodeTest {
TbNodeConfiguration nodeConfiguration = new TbNodeConfiguration(mapper.valueToTree(config)); TbNodeConfiguration nodeConfiguration = new TbNodeConfiguration(mapper.valueToTree(config));
node = new TbGetCustomerAttributeNode(); node = new TbGetCustomerAttributeNode();
node.init(nodeConfiguration, null); node.init(null, nodeConfiguration);
} }
@Test @Test
@ -226,7 +226,7 @@ public class TbGetCustomerAttributeNodeTest {
TbNodeConfiguration nodeConfiguration = new TbNodeConfiguration(mapper.valueToTree(config)); TbNodeConfiguration nodeConfiguration = new TbNodeConfiguration(mapper.valueToTree(config));
node = new TbGetCustomerAttributeNode(); node = new TbGetCustomerAttributeNode();
node.init(nodeConfiguration, null); node.init(null, nodeConfiguration);
DeviceId deviceId = new DeviceId(UUIDs.timeBased()); DeviceId deviceId = new DeviceId(UUIDs.timeBased());

2
rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/transform/TbChangeOriginatorNodeTest.java

@ -119,6 +119,6 @@ public class TbChangeOriginatorNodeTest {
TbNodeConfiguration nodeConfiguration = new TbNodeConfiguration(mapper.valueToTree(config)); TbNodeConfiguration nodeConfiguration = new TbNodeConfiguration(mapper.valueToTree(config));
node = new TbChangeOriginatorNode(); node = new TbChangeOriginatorNode();
node.init(nodeConfiguration, null); node.init(null, nodeConfiguration);
} }
} }

8
rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/transform/TbTransformMsgNodeTest.java

@ -51,7 +51,7 @@ public class TbTransformMsgNodeTest {
@Test @Test
public void metadataCanBeUpdated() throws TbNodeException { public void metadataCanBeUpdated() throws TbNodeException {
initWithScript("meta.temp = meta.temp * 10;"); initWithScript("return metadata.temp = metadata.temp * 10;");
TbMsgMetaData metaData = new TbMsgMetaData(); TbMsgMetaData metaData = new TbMsgMetaData();
metaData.putValue("temp", "7"); metaData.putValue("temp", "7");
metaData.putValue("humidity", "99"); metaData.putValue("humidity", "99");
@ -70,7 +70,7 @@ public class TbTransformMsgNodeTest {
@Test @Test
public void metadataCanBeAdded() throws TbNodeException { public void metadataCanBeAdded() throws TbNodeException {
initWithScript("meta.newAttr = meta.humidity - msg.passed;"); initWithScript("return metadata.newAttr = metadata.humidity - msg.passed;");
TbMsgMetaData metaData = new TbMsgMetaData(); TbMsgMetaData metaData = new TbMsgMetaData();
metaData.putValue("temp", "7"); metaData.putValue("temp", "7");
metaData.putValue("humidity", "99"); metaData.putValue("humidity", "99");
@ -89,7 +89,7 @@ public class TbTransformMsgNodeTest {
@Test @Test
public void payloadCanBeUpdated() throws TbNodeException { public void payloadCanBeUpdated() throws TbNodeException {
initWithScript("msg.passed = msg.passed * meta.temp; msg.bigObj.newProp = 'Ukraine' "); initWithScript("return msg.passed = msg.passed * metadata.temp; msg.bigObj.newProp = 'Ukraine' ");
TbMsgMetaData metaData = new TbMsgMetaData(); TbMsgMetaData metaData = new TbMsgMetaData();
metaData.putValue("temp", "7"); metaData.putValue("temp", "7");
metaData.putValue("humidity", "99"); metaData.putValue("humidity", "99");
@ -114,7 +114,7 @@ public class TbTransformMsgNodeTest {
TbNodeConfiguration nodeConfiguration = new TbNodeConfiguration(mapper.valueToTree(config)); TbNodeConfiguration nodeConfiguration = new TbNodeConfiguration(mapper.valueToTree(config));
node = new TbTransformMsgNode(); node = new TbTransformMsgNode();
node.init(nodeConfiguration, null); node.init(null, nodeConfiguration);
} }
private void mockJsExecutor() { private void mockJsExecutor() {

2
ui/package.json

@ -15,7 +15,7 @@
}, },
"dependencies": { "dependencies": {
"@flowjs/ng-flow": "^2.7.1", "@flowjs/ng-flow": "^2.7.1",
"ace-builds": "^1.2.5", "ace-builds": "1.3.1",
"angular": "1.5.8", "angular": "1.5.8",
"angular-animate": "1.5.8", "angular-animate": "1.5.8",
"angular-aria": "1.5.8", "angular-aria": "1.5.8",

20
ui/server.js

@ -30,6 +30,9 @@ const httpProxy = require('http-proxy');
const forwardHost = 'localhost'; const forwardHost = 'localhost';
const forwardPort = 8080; const forwardPort = 8080;
const ruleNodeUiforwardHost = 'localhost';
const ruleNodeUiforwardPort = 8080;
const app = express(); const app = express();
const server = http.createServer(app); const server = http.createServer(app);
@ -52,17 +55,34 @@ const apiProxy = httpProxy.createProxyServer({
} }
}); });
const ruleNodeUiApiProxy = httpProxy.createProxyServer({
target: {
host: ruleNodeUiforwardHost,
port: ruleNodeUiforwardPort
}
});
apiProxy.on('error', function (err, req, res) { apiProxy.on('error', function (err, req, res) {
console.warn('API proxy error: ' + err); console.warn('API proxy error: ' + err);
res.end('Error.'); res.end('Error.');
}); });
ruleNodeUiApiProxy.on('error', function (err, req, res) {
console.warn('RuleNode UI API proxy error: ' + err);
res.end('Error.');
});
console.info(`Forwarding API requests to http://${forwardHost}:${forwardPort}`); console.info(`Forwarding API requests to http://${forwardHost}:${forwardPort}`);
console.info(`Forwarding Rule Node UI requests to http://${ruleNodeUiforwardHost}:${ruleNodeUiforwardPort}`);
app.all('/api/*', (req, res) => { app.all('/api/*', (req, res) => {
apiProxy.web(req, res); apiProxy.web(req, res);
}); });
app.all('/static/rulenode/*', (req, res) => {
ruleNodeUiApiProxy.web(req, res);
});
app.get('*', function(req, res) { app.get('*', function(req, res) {
res.sendFile(path.join(__dirname, 'src/index.html')); res.sendFile(path.join(__dirname, 'src/index.html'));
}); });

59
ui/src/app/api/rule-chain.service.js

@ -17,7 +17,7 @@ export default angular.module('thingsboard.api.ruleChain', [])
.factory('ruleChainService', RuleChainService).name; .factory('ruleChainService', RuleChainService).name;
/*@ngInject*/ /*@ngInject*/
function RuleChainService($http, $q, $filter, types, componentDescriptorService) { function RuleChainService($http, $q, $filter, $ocLazyLoad, $translate, types, componentDescriptorService) {
var ruleNodeComponents = null; var ruleNodeComponents = null;
@ -177,11 +177,18 @@ function RuleChainService($http, $q, $filter, types, componentDescriptorService)
} else { } else {
loadRuleNodeComponents().then( loadRuleNodeComponents().then(
(components) => { (components) => {
ruleNodeComponents = components; resolveRuleNodeComponentsUiResources(components).then(
ruleNodeComponents.push( (components) => {
types.ruleChainNodeComponent ruleNodeComponents = components;
ruleNodeComponents.push(
types.ruleChainNodeComponent
);
deferred.resolve(ruleNodeComponents);
},
() => {
deferred.reject();
}
); );
deferred.resolve(ruleNodeComponents);
}, },
() => { () => {
deferred.reject(); deferred.reject();
@ -191,6 +198,48 @@ function RuleChainService($http, $q, $filter, types, componentDescriptorService)
return deferred.promise; return deferred.promise;
} }
function resolveRuleNodeComponentsUiResources(components) {
var deferred = $q.defer();
var tasks = [];
for (var i=0;i<components.length;i++) {
var component = components[i];
tasks.push(resolveRuleNodeComponentUiResources(component));
}
$q.all(tasks).then(
(components) => {
deferred.resolve(components);
},
() => {
deferred.resolve(components);
}
);
return deferred.promise;
}
function resolveRuleNodeComponentUiResources(component) {
var deferred = $q.defer();
var uiResources = component.configurationDescriptor.nodeDefinition.uiResources;
if (uiResources && uiResources.length) {
var tasks = [];
for (var i=0;i<uiResources.length;i++) {
var uiResource = uiResources[i];
tasks.push($ocLazyLoad.load(uiResource));
}
$q.all(tasks).then(
() => {
deferred.resolve(component);
},
() => {
component.configurationDescriptor.nodeDefinition.uiResourceLoadError = $translate.instant('rulenode.ui-resources-load-error');
deferred.resolve(component);
}
)
} else {
deferred.resolve(component);
}
return deferred.promise;
}
function getRuleNodeComponentByClazz(clazz) { function getRuleNodeComponentByClazz(clazz) {
var res = $filter('filter')(ruleNodeComponents, {clazz: clazz}, true); var res = $filter('filter')(ruleNodeComponents, {clazz: clazz}, true);
if (res && res.length) { if (res && res.length) {

30
ui/src/app/common/types.constant.js

@ -279,6 +279,23 @@ export default angular.module('thingsboard.types', [])
function: "function", function: "function",
alarm: "alarm" alarm: "alarm"
}, },
contentType: {
"JSON": {
value: "JSON",
name: "content-type.json",
code: "json"
},
"TEXT": {
value: "TEXT",
name: "content-type.text",
code: "text"
},
"BINARY": {
value: "BINARY",
name: "content-type.binary",
code: "text"
}
},
componentType: { componentType: {
filter: "FILTER", filter: "FILTER",
processor: "PROCESSOR", processor: "PROCESSOR",
@ -295,7 +312,8 @@ export default angular.module('thingsboard.types', [])
user: "USER", user: "USER",
dashboard: "DASHBOARD", dashboard: "DASHBOARD",
alarm: "ALARM", alarm: "ALARM",
rulechain: "RULE_CHAIN" rulechain: "RULE_CHAIN",
rulenode: "RULE_NODE"
}, },
aliasEntityType: { aliasEntityType: {
current_customer: "CURRENT_CUSTOMER" current_customer: "CURRENT_CUSTOMER"
@ -388,6 +406,16 @@ export default angular.module('thingsboard.types', [])
name: "event.type-stats" name: "event.type-stats"
} }
}, },
debugEventType: {
debugRuleNode: {
value: "DEBUG_RULE_NODE",
name: "event.type-debug-rule-node"
},
debugRuleChain: {
value: "DEBUG_RULE_CHAIN",
name: "event.type-debug-rule-chain"
}
},
extensionType: { extensionType: {
http: "HTTP", http: "HTTP",
mqtt: "MQTT", mqtt: "MQTT",

19
ui/src/app/components/confirm-on-exit.directive.js

@ -18,17 +18,17 @@ export default angular.module('thingsboard.directives.confirmOnExit', [])
.name; .name;
/*@ngInject*/ /*@ngInject*/
function ConfirmOnExit($state, $mdDialog, $window, $filter, userService) { function ConfirmOnExit($state, $mdDialog, $window, $filter, $parse, userService) {
return { return {
link: function ($scope) { link: function ($scope, $element, $attributes) {
$scope.confirmForm = $scope.$eval($attributes.confirmForm);
$window.onbeforeunload = function () { $window.onbeforeunload = function () {
if (userService.isAuthenticated() && (($scope.confirmForm && $scope.confirmForm.$dirty) || $scope.isDirty)) { if (userService.isAuthenticated() && (($scope.confirmForm && $scope.confirmForm.$dirty) || $scope.$eval($attributes.isDirty))) {
return $filter('translate')('confirm-on-exit.message'); return $filter('translate')('confirm-on-exit.message');
} }
} }
$scope.$on('$stateChangeStart', function (event, next, current, params) { $scope.$on('$stateChangeStart', function (event, next, current, params) {
if (userService.isAuthenticated() && (($scope.confirmForm && $scope.confirmForm.$dirty) || $scope.isDirty)) { if (userService.isAuthenticated() && (($scope.confirmForm && $scope.confirmForm.$dirty) || $scope.$eval($attributes.isDirty))) {
event.preventDefault(); event.preventDefault();
var confirm = $mdDialog.confirm() var confirm = $mdDialog.confirm()
.title($filter('translate')('confirm-on-exit.title')) .title($filter('translate')('confirm-on-exit.title'))
@ -40,7 +40,9 @@ function ConfirmOnExit($state, $mdDialog, $window, $filter, userService) {
if ($scope.confirmForm) { if ($scope.confirmForm) {
$scope.confirmForm.$setPristine(); $scope.confirmForm.$setPristine();
} else { } else {
$scope.isDirty = false; var remoteSetter = $parse($attributes.isDirty).assign;
remoteSetter($scope, false);
//$scope.isDirty = false;
} }
$state.go(next.name, params); $state.go(next.name, params);
}, function () { }, function () {
@ -48,9 +50,6 @@ function ConfirmOnExit($state, $mdDialog, $window, $filter, userService) {
} }
}); });
}, },
scope: { scope: false
confirmForm: '=',
isDirty: '='
}
}; };
} }

59
ui/src/app/components/details-sidenav.directive.js

@ -26,7 +26,7 @@ export default angular.module('thingsboard.directives.detailsSidenav', [])
.name; .name;
/*@ngInject*/ /*@ngInject*/
function DetailsSidenav($timeout) { function DetailsSidenav($timeout, $mdUtil, $q, $animate) {
var linker = function (scope, element, attrs) { var linker = function (scope, element, attrs) {
@ -42,6 +42,63 @@ function DetailsSidenav($timeout) {
scope.isEdit = true; scope.isEdit = true;
} }
var backdrop;
var previousContainerStyles;
if (attrs.hasOwnProperty('tbEnableBackdrop')) {
backdrop = $mdUtil.createBackdrop(scope, "md-sidenav-backdrop md-opaque ng-enter");
element.on('$destroy', function() {
backdrop && backdrop.remove();
});
scope.$on('$destroy', function(){
backdrop && backdrop.remove();
});
scope.$watch('isOpen', updateIsOpen);
}
function updateIsOpen(isOpen) {
backdrop[isOpen ? 'on' : 'off']('click', (ev)=>{
ev.preventDefault();
scope.isOpen = false;
scope.$apply();
});
var parent = element.parent();
var restorePositioning = updateContainerPositions(parent, isOpen);
return $q.all([
isOpen && backdrop ? $animate.enter(backdrop, parent) : backdrop ?
$animate.leave(backdrop) : $q.when(true)
]).then(function() {
restorePositioning && restorePositioning();
});
}
function updateContainerPositions(parent, willOpen) {
var drawerEl = element[0];
var scrollTop = parent[0].scrollTop;
if (willOpen && scrollTop) {
previousContainerStyles = {
top: drawerEl.style.top,
bottom: drawerEl.style.bottom,
height: drawerEl.style.height
};
var positionStyle = {
top: scrollTop + 'px',
bottom: 'auto',
height: parent[0].clientHeight + 'px'
};
backdrop.css(positionStyle);
}
if (!willOpen && previousContainerStyles) {
return function() {
backdrop[0].style.top = null;
backdrop[0].style.bottom = null;
backdrop[0].style.height = null;
previousContainerStyles = null;
};
}
}
scope.toggleDetailsEditMode = function () { scope.toggleDetailsEditMode = function () {
if (!scope.isAlwaysEdit) { if (!scope.isAlwaysEdit) {
if (!scope.isEdit) { if (!scope.isEdit) {

2
ui/src/app/components/details-sidenav.tpl.html

@ -16,7 +16,7 @@
--> -->
<md-sidenav class="md-sidenav-right md-whiteframe-4dp tb-sidenav-details" <md-sidenav class="md-sidenav-right md-whiteframe-4dp tb-sidenav-details"
md-disable-backdrop="true" md-disable-backdrop
md-is-open="isOpen" md-is-open="isOpen"
md-component-id="right" md-component-id="right"
layout="column"> layout="column">

34
ui/src/app/components/js-func.directive.js

@ -43,6 +43,7 @@ function JsFunc($compile, $templateCache, toast, utils, $translate) {
var template = $templateCache.get(jsFuncTemplate); var template = $templateCache.get(jsFuncTemplate);
element.html(template); element.html(template);
scope.functionName = attrs.functionName;
scope.functionArgs = scope.$eval(attrs.functionArgs); scope.functionArgs = scope.$eval(attrs.functionArgs);
scope.validationArgs = scope.$eval(attrs.validationArgs); scope.validationArgs = scope.$eval(attrs.validationArgs);
scope.resultType = attrs.resultType; scope.resultType = attrs.resultType;
@ -50,6 +51,8 @@ function JsFunc($compile, $templateCache, toast, utils, $translate) {
scope.resultType = "nocheck"; scope.resultType = "nocheck";
} }
scope.validationTriggerArg = attrs.validationTriggerArg;
scope.functionValid = true; scope.functionValid = true;
var Range = ace.acequire("ace/range").Range; var Range = ace.acequire("ace/range").Range;
@ -66,11 +69,15 @@ function JsFunc($compile, $templateCache, toast, utils, $translate) {
} }
scope.onFullscreenChanged = function () { scope.onFullscreenChanged = function () {
updateEditorSize();
};
function updateEditorSize() {
if (scope.js_editor) { if (scope.js_editor) {
scope.js_editor.resize(); scope.js_editor.resize();
scope.js_editor.renderer.updateFull(); scope.js_editor.renderer.updateFull();
} }
}; }
scope.jsEditorOptions = { scope.jsEditorOptions = {
useWrapMode: true, useWrapMode: true,
@ -131,6 +138,9 @@ function JsFunc($compile, $templateCache, toast, utils, $translate) {
scope.validate = function () { scope.validate = function () {
try { try {
var toValidate = new Function(scope.functionArgsString, scope.functionBody); var toValidate = new Function(scope.functionArgsString, scope.functionBody);
if (scope.noValidate) {
return true;
}
var res; var res;
var validationError; var validationError;
for (var i=0;i<scope.validationArgs.length;i++) { for (var i=0;i<scope.validationArgs.length;i++) {
@ -200,9 +210,19 @@ function JsFunc($compile, $templateCache, toast, utils, $translate) {
} }
}; };
scope.$on('form-submit', function () { scope.$on('form-submit', function (event, args) {
scope.functionValid = scope.validate(); if (!args || scope.validationTriggerArg && scope.validationTriggerArg == args) {
scope.updateValidity(); scope.validationArgs = scope.$eval(attrs.validationArgs);
scope.cleanupJsErrors();
scope.functionValid = true;
scope.updateValidity();
scope.functionValid = scope.validate();
scope.updateValidity();
}
});
scope.$on('update-ace-editor-size', function () {
updateEditorSize();
}); });
$compile(element.contents())(scope); $compile(element.contents())(scope);
@ -211,7 +231,11 @@ function JsFunc($compile, $templateCache, toast, utils, $translate) {
return { return {
restrict: "E", restrict: "E",
require: "^ngModel", require: "^ngModel",
scope: {}, scope: {
disabled:'=ngDisabled',
noValidate: '=?',
fillHeight:'=?'
},
link: linker link: linker
}; };
} }

10
ui/src/app/components/js-func.scss

@ -15,6 +15,12 @@
*/ */
tb-js-func { tb-js-func {
position: relative; position: relative;
.tb-disabled {
color: rgba(0,0,0,0.38);
}
.fill-height {
height: 100%;
}
} }
.tb-js-func-panel { .tb-js-func-panel {
@ -23,8 +29,10 @@ tb-js-func {
height: 100%; height: 100%;
#tb-javascript-input { #tb-javascript-input {
min-width: 200px; min-width: 200px;
min-height: 200px;
width: 100%; width: 100%;
height: 100%; height: 100%;
&:not(.fill-height) {
min-height: 200px;
}
} }
} }

15
ui/src/app/components/js-func.tpl.html

@ -15,19 +15,20 @@
limitations under the License. limitations under the License.
--> -->
<div style="background: #fff;" tb-expand-fullscreen fullscreen-zindex="100" expand-button-id="expand-button" on-fullscreen-changed="onFullscreenChanged()" layout="column"> <div style="background: #fff;" ng-class="{'tb-disabled': disabled, 'fill-height': fillHeight}" tb-expand-fullscreen fullscreen-zindex="100" expand-button-id="expand-button" on-fullscreen-changed="onFullscreenChanged()" layout="column">
<div layout="row" layout-align="start center" style="height: 40px;"> <div layout="row" layout-align="start center" style="height: 40px;">
<span style="font-style: italic;">function({{ functionArgsString }}) {</span> <label class="tb-title no-padding">function {{ functionName }}({{ functionArgsString }}) {</label>
<span flex></span> <span flex></span>
<div id="expand-button" layout="column" aria-label="Fullscreen" class="md-button md-icon-button tb-md-32 tb-fullscreen-button-style"></div> <div id="expand-button" layout="column" aria-label="Fullscreen" class="md-button md-icon-button tb-md-32 tb-fullscreen-button-style"></div>
</div> </div>
<div flex id="tb-javascript-panel" class="tb-js-func-panel" layout="column"> <div flex id="tb-javascript-panel" class="tb-js-func-panel" layout="column">
<div flex id="tb-javascript-input" <div flex id="tb-javascript-input" ng-class="{'fill-height': fillHeight}"
ui-ace="jsEditorOptions" ui-ace="jsEditorOptions"
ng-readonly="disabled"
ng-model="functionBody"> ng-model="functionBody">
</div> </div>
</div> </div>
<div layout="row" layout-align="start center" style="height: 40px;"> <div layout="row" layout-align="start center" style="height: 40px;">
<span style="font-style: italic;">}</span> <label class="tb-title no-padding">}</label>
</div> </div>
</div> </div>

23
ui/src/app/components/json-object-edit.directive.js

@ -84,17 +84,32 @@ function JsonObjectEdit($compile, $templateCache, $document, toast, utils) {
scope.$watch('contentBody', function (newVal, prevVal) { scope.$watch('contentBody', function (newVal, prevVal) {
if (!angular.equals(newVal, prevVal)) { if (!angular.equals(newVal, prevVal)) {
var object = scope.validate(); var object = scope.validate();
ngModelCtrl.$setViewValue(object); if (scope.objectValid) {
if (object == null) {
scope.object = null;
} else {
if (scope.object == null) {
scope.object = {};
}
Object.keys(scope.object).forEach(function (key) {
delete scope.object[key];
});
Object.keys(object).forEach(function (key) {
scope.object[key] = object[key];
});
}
ngModelCtrl.$setViewValue(scope.object);
}
scope.updateValidity(); scope.updateValidity();
} }
}); });
ngModelCtrl.$render = function () { ngModelCtrl.$render = function () {
var object = ngModelCtrl.$viewValue; scope.object = ngModelCtrl.$viewValue;
var content = ''; var content = '';
try { try {
if (object) { if (scope.object) {
content = angular.toJson(object, true); content = angular.toJson(scope.object, true);
} }
} catch (e) { } catch (e) {
// //

17
ui/src/app/event/event-content-dialog.controller.js

@ -17,11 +17,14 @@ import $ from 'jquery';
import 'brace/ext/language_tools'; import 'brace/ext/language_tools';
import 'brace/mode/java'; import 'brace/mode/java';
import 'brace/theme/github'; import 'brace/theme/github';
import beautify from 'js-beautify';
/* eslint-disable angular/angularelement */ /* eslint-disable angular/angularelement */
const js_beautify = beautify.js;
/*@ngInject*/ /*@ngInject*/
export default function EventContentDialogController($mdDialog, content, title, showingCallback) { export default function EventContentDialogController($mdDialog, types, content, contentType, title, showingCallback) {
var vm = this; var vm = this;
@ -32,9 +35,19 @@ export default function EventContentDialogController($mdDialog, content, title,
vm.content = content; vm.content = content;
vm.title = title; vm.title = title;
var mode;
if (contentType) {
mode = types.contentType[contentType].code;
if (contentType == types.contentType.JSON.value && vm.content) {
vm.content = js_beautify(vm.content, {indent_size: 4});
}
} else {
mode = 'java';
}
vm.contentOptions = { vm.contentOptions = {
useWrapMode: false, useWrapMode: false,
mode: 'java', mode: mode,
showGutter: false, showGutter: false,
showPrintMargin: false, showPrintMargin: false,
theme: 'github', theme: 'github',

27
ui/src/app/event/event-header-debug-rulenode.tpl.html

@ -0,0 +1,27 @@
<!--
Copyright © 2016-2018 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.
-->
<div hide-xs hide-sm translate class="tb-cell" flex="30">event.event-time</div>
<div translate class="tb-cell" flex="20">event.server</div>
<div translate class="tb-cell" flex="20">event.type</div>
<div translate class="tb-cell" flex="20">event.entity</div>
<div translate class="tb-cell" flex="20">event.message-id</div>
<div translate class="tb-cell" flex="20">event.message-type</div>
<div translate class="tb-cell" flex="20">event.data-type</div>
<div translate class="tb-cell" flex="20">event.data</div>
<div translate class="tb-cell" flex="20">event.metadata</div>
<div translate class="tb-cell" flex="20">event.error</div>

7
ui/src/app/event/event-header.directive.js

@ -18,6 +18,7 @@
import eventHeaderLcEventTemplate from './event-header-lc-event.tpl.html'; import eventHeaderLcEventTemplate from './event-header-lc-event.tpl.html';
import eventHeaderStatsTemplate from './event-header-stats.tpl.html'; import eventHeaderStatsTemplate from './event-header-stats.tpl.html';
import eventHeaderErrorTemplate from './event-header-error.tpl.html'; import eventHeaderErrorTemplate from './event-header-error.tpl.html';
import eventHeaderDebugRuleNodeTemplate from './event-header-debug-rulenode.tpl.html';
/* eslint-enable import/no-unresolved, import/default */ /* eslint-enable import/no-unresolved, import/default */
@ -38,6 +39,12 @@ export default function EventHeaderDirective($compile, $templateCache, types) {
case types.eventType.error.value: case types.eventType.error.value:
template = eventHeaderErrorTemplate; template = eventHeaderErrorTemplate;
break; break;
case types.debugEventType.debugRuleNode.value:
template = eventHeaderDebugRuleNodeTemplate;
break;
case types.debugEventType.debugRuleChain.value:
template = eventHeaderDebugRuleNodeTemplate;
break;
} }
return $templateCache.get(template); return $templateCache.get(template);
} }

63
ui/src/app/event/event-row-debug-rulenode.tpl.html

@ -0,0 +1,63 @@
<!--
Copyright © 2016-2018 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.
-->
<div hide-xs hide-sm class="tb-cell" flex="30">{{event.createdTime | date : 'yyyy-MM-dd HH:mm:ss'}}</div>
<div class="tb-cell" flex="20">{{event.body.server}}</div>
<div class="tb-cell" flex="20">{{event.body.type}}</div>
<div class="tb-cell" flex="20">{{event.body.entityName}}</div>
<div class="tb-cell" flex="20">{{event.body.msgId}}</div>
<div class="tb-cell" flex="20">{{event.body.msgType}}</div>
<div class="tb-cell" flex="20">{{event.body.dataType}}</div>
<div class="tb-cell" flex="20">
<md-button ng-if="event.body.data" class="md-icon-button md-primary"
ng-click="showContent($event, event.body.data, 'event.data', event.body.dataType)"
aria-label="{{ 'action.view' | translate }}">
<md-tooltip md-direction="top">
{{ 'action.view' | translate }}
</md-tooltip>
<md-icon aria-label="{{ 'action.view' | translate }}"
class="material-icons">
more_horiz
</md-icon>
</md-button>
</div>
<div class="tb-cell" flex="20">
<md-button ng-if="event.body.metadata" class="md-icon-button md-primary"
ng-click="showContent($event, event.body.metadata, 'event.metadata', 'JSON')"
aria-label="{{ 'action.view' | translate }}">
<md-tooltip md-direction="top">
{{ 'action.view' | translate }}
</md-tooltip>
<md-icon aria-label="{{ 'action.view' | translate }}"
class="material-icons">
more_horiz
</md-icon>
</md-button>
</div>
<div class="tb-cell" flex="20">
<md-button ng-if="event.body.error" class="md-icon-button md-primary"
ng-click="showContent($event, event.body.error, 'event.error')"
aria-label="{{ 'action.view' | translate }}">
<md-tooltip md-direction="top">
{{ 'action.view' | translate }}
</md-tooltip>
<md-icon aria-label="{{ 'action.view' | translate }}"
class="material-icons">
more_horiz
</md-icon>
</md-button>
</div>

16
ui/src/app/event/event-row.directive.js

@ -20,6 +20,7 @@ import eventErrorDialogTemplate from './event-content-dialog.tpl.html';
import eventRowLcEventTemplate from './event-row-lc-event.tpl.html'; import eventRowLcEventTemplate from './event-row-lc-event.tpl.html';
import eventRowStatsTemplate from './event-row-stats.tpl.html'; import eventRowStatsTemplate from './event-row-stats.tpl.html';
import eventRowErrorTemplate from './event-row-error.tpl.html'; import eventRowErrorTemplate from './event-row-error.tpl.html';
import eventRowDebugRuleNodeTemplate from './event-row-debug-rulenode.tpl.html';
/* eslint-enable import/no-unresolved, import/default */ /* eslint-enable import/no-unresolved, import/default */
@ -40,6 +41,12 @@ export default function EventRowDirective($compile, $templateCache, $mdDialog, $
case types.eventType.error.value: case types.eventType.error.value:
template = eventRowErrorTemplate; template = eventRowErrorTemplate;
break; break;
case types.debugEventType.debugRuleNode.value:
template = eventRowDebugRuleNodeTemplate;
break;
case types.debugEventType.debugRuleChain.value:
template = eventRowDebugRuleNodeTemplate;
break;
} }
return $templateCache.get(template); return $templateCache.get(template);
} }
@ -53,17 +60,22 @@ export default function EventRowDirective($compile, $templateCache, $mdDialog, $
scope.loadTemplate(); scope.loadTemplate();
}); });
scope.types = types;
scope.event = attrs.event; scope.event = attrs.event;
scope.showContent = function($event, content, title) { scope.showContent = function($event, content, title, contentType) {
var onShowingCallback = { var onShowingCallback = {
onShowing: function(){} onShowing: function(){}
} }
if (!contentType) {
contentType = null;
}
$mdDialog.show({ $mdDialog.show({
controller: 'EventContentDialogController', controller: 'EventContentDialogController',
controllerAs: 'vm', controllerAs: 'vm',
templateUrl: eventErrorDialogTemplate, templateUrl: eventErrorDialogTemplate,
locals: {content: content, title: title, showingCallback: onShowingCallback}, locals: {content: content, title: title, contentType: contentType, showingCallback: onShowingCallback},
parent: angular.element($document[0].body), parent: angular.element($document[0].body),
fullscreen: true, fullscreen: true,
targetEvent: $event, targetEvent: $event,

18
ui/src/app/event/event-table.directive.js

@ -36,8 +36,8 @@ export default function EventTableDirective($compile, $templateCache, $rootScope
for (var type in types.eventType) { for (var type in types.eventType) {
var eventType = types.eventType[type]; var eventType = types.eventType[type];
var enabled = true; var enabled = true;
for (var disabledType in disabledEventTypes) { for (var i=0;i<disabledEventTypes.length;i++) {
if (eventType.value === disabledEventTypes[disabledType]) { if (eventType.value === disabledEventTypes[i]) {
enabled = false; enabled = false;
break; break;
} }
@ -47,7 +47,19 @@ export default function EventTableDirective($compile, $templateCache, $rootScope
} }
} }
} else { } else {
scope.eventTypes = types.eventType; scope.eventTypes = angular.copy(types.eventType);
}
if (attrs.debugEventTypes) {
var debugEventTypes = attrs.debugEventTypes.split(',');
for (i=0;i<debugEventTypes.length;i++) {
for (type in types.debugEventType) {
eventType = types.debugEventType[type];
if (eventType.value === debugEventTypes[i]) {
scope.eventTypes[type] = eventType;
}
}
}
} }
scope.eventType = attrs.defaultEventType; scope.eventType = attrs.defaultEventType;

24
ui/src/app/locale/locale.constant.js

@ -341,6 +341,11 @@ export default angular.module('thingsboard.locale', [])
"enter-password": "Enter password", "enter-password": "Enter password",
"enter-search": "Enter search" "enter-search": "Enter search"
}, },
"content-type": {
"json": "Json",
"text": "Text",
"binary": "Binary (Base64)"
},
"customer": { "customer": {
"customer": "Customer", "customer": "Customer",
"customers": "Customers", "customers": "Customers",
@ -762,6 +767,8 @@ export default angular.module('thingsboard.locale', [])
"type-error": "Error", "type-error": "Error",
"type-lc-event": "Lifecycle event", "type-lc-event": "Lifecycle event",
"type-stats": "Statistics", "type-stats": "Statistics",
"type-debug-rule-node": "Debug",
"type-debug-rule-chain": "Debug",
"no-events-prompt": "No events found", "no-events-prompt": "No events found",
"error": "Error", "error": "Error",
"alarm": "Alarm", "alarm": "Alarm",
@ -769,6 +776,13 @@ export default angular.module('thingsboard.locale', [])
"server": "Server", "server": "Server",
"body": "Body", "body": "Body",
"method": "Method", "method": "Method",
"type": "Type",
"entity": "Entity",
"message-id": "Message Id",
"message-type": "Message Type",
"data-type": "Data Type",
"metadata": "Metadata",
"data": "Data",
"event": "Event", "event": "Event",
"status": "Status", "status": "Status",
"success": "Success", "success": "Success",
@ -1171,12 +1185,18 @@ export default angular.module('thingsboard.locale', [])
"debug-mode": "Debug mode" "debug-mode": "Debug mode"
}, },
"rulenode": { "rulenode": {
"details": "Details",
"events": "Events",
"search": "Search nodes",
"add": "Add rule node", "add": "Add rule node",
"name": "Name", "name": "Name",
"name-required": "Name is required.", "name-required": "Name is required.",
"type": "Type", "type": "Type",
"description": "Description", "description": "Description",
"delete": "Delete rule node", "delete": "Delete rule node",
"select-all": "Select all nodes and connections",
"deselect-all": "Deselect all nodes and connections",
"delete-selected-objects": "Delete selected nodes and connections",
"rulenode-details": "Rule node details", "rulenode-details": "Rule node details",
"debug-mode": "Debug mode", "debug-mode": "Debug mode",
"configuration": "Configuration", "configuration": "Configuration",
@ -1195,7 +1215,9 @@ export default angular.module('thingsboard.locale', [])
"type-action": "Action", "type-action": "Action",
"type-action-details": "Perform special action", "type-action-details": "Perform special action",
"type-rule-chain": "Rule Chain", "type-rule-chain": "Rule Chain",
"type-rule-chain-details": "Forwards incoming messages to specified Rule Chain" "type-rule-chain-details": "Forwards incoming messages to specified Rule Chain",
"directive-is-not-loaded": "Defined configuration directive '{{directiveName}}' is not available.",
"ui-resources-load-error": "Failed to load configuration ui resources."
}, },
"rule-plugin": { "rule-plugin": {
"management": "Rules and plugins management" "management": "Rules and plugins management"

4
ui/src/app/rulechain/index.js

@ -18,6 +18,8 @@ import RuleChainRoutes from './rulechain.routes';
import RuleChainsController from './rulechains.controller'; import RuleChainsController from './rulechains.controller';
import {RuleChainController, AddRuleNodeController, AddRuleNodeLinkController} from './rulechain.controller'; import {RuleChainController, AddRuleNodeController, AddRuleNodeLinkController} from './rulechain.controller';
import RuleChainDirective from './rulechain.directive'; import RuleChainDirective from './rulechain.directive';
import RuleNodeDefinedConfigDirective from './rulenode-defined-config.directive';
import RuleNodeConfigDirective from './rulenode-config.directive';
import RuleNodeDirective from './rulenode.directive'; import RuleNodeDirective from './rulenode.directive';
import LinkDirective from './link.directive'; import LinkDirective from './link.directive';
@ -28,6 +30,8 @@ export default angular.module('thingsboard.ruleChain', [])
.controller('AddRuleNodeController', AddRuleNodeController) .controller('AddRuleNodeController', AddRuleNodeController)
.controller('AddRuleNodeLinkController', AddRuleNodeLinkController) .controller('AddRuleNodeLinkController', AddRuleNodeLinkController)
.directive('tbRuleChain', RuleChainDirective) .directive('tbRuleChain', RuleChainDirective)
.directive('tbRuleNodeDefinedConfig', RuleNodeDefinedConfigDirective)
.directive('tbRuleNodeConfig', RuleNodeConfigDirective)
.directive('tbRuleNode', RuleNodeDirective) .directive('tbRuleNode', RuleNodeDirective)
.directive('tbRuleNodeLink', LinkDirective) .directive('tbRuleNodeLink', LinkDirective)
.name; .name;

197
ui/src/app/rulechain/rulechain.controller.js

@ -27,15 +27,10 @@ import addRuleNodeLinkTemplate from './add-link.tpl.html';
/* eslint-enable import/no-unresolved, import/default */ /* eslint-enable import/no-unresolved, import/default */
const deleteKeyCode = 46;
const ctrlKeyCode = 17;
const aKeyCode = 65;
const escKeyCode = 27;
/*@ngInject*/ /*@ngInject*/
export function RuleChainController($stateParams, $scope, $compile, $q, $mdUtil, $timeout, $mdExpansionPanel, $document, $mdDialog, export function RuleChainController($stateParams, $scope, $compile, $q, $mdUtil, $timeout, $mdExpansionPanel, $window, $document, $mdDialog,
$filter, $translate, types, ruleChainService, Modelfactory, flowchartConstants, ruleChain, ruleChainMetaData) { $filter, $translate, hotkeys, types, ruleChainService, Modelfactory, flowchartConstants,
ruleChain, ruleChainMetaData, ruleNodeComponents) {
var vm = this; var vm = this;
@ -48,6 +43,9 @@ export function RuleChainController($stateParams, $scope, $compile, $q, $mdUtil,
vm.editingRuleNodeLink = null; vm.editingRuleNodeLink = null;
vm.isEditingRuleNodeLink = false; vm.isEditingRuleNodeLink = false;
vm.isLibraryOpen = true;
vm.ruleNodeSearch = '';
vm.ruleChain = ruleChain; vm.ruleChain = ruleChain;
vm.ruleChainMetaData = ruleChainMetaData; vm.ruleChainMetaData = ruleChainMetaData;
@ -76,39 +74,64 @@ export function RuleChainController($stateParams, $scope, $compile, $q, $mdUtil,
vm.modelservice = Modelfactory(vm.ruleChainModel, vm.selectedObjects); vm.modelservice = Modelfactory(vm.ruleChainModel, vm.selectedObjects);
vm.ctrlDown = false;
vm.saveRuleChain = saveRuleChain; vm.saveRuleChain = saveRuleChain;
vm.revertRuleChain = revertRuleChain; vm.revertRuleChain = revertRuleChain;
vm.keyDown = function (evt) { vm.objectsSelected = objectsSelected;
if (evt.keyCode === ctrlKeyCode) { vm.deleteSelected = deleteSelected;
vm.ctrlDown = true;
evt.stopPropagation();
evt.preventDefault();
}
};
vm.keyUp = function (evt) {
if (evt.keyCode === deleteKeyCode) { vm.triggerResize = triggerResize;
vm.modelservice.deleteSelected();
}
if (evt.keyCode == aKeyCode && vm.ctrlDown) {
vm.modelservice.selectAll();
}
if (evt.keyCode == escKeyCode) { initHotKeys();
vm.modelservice.deselectAll();
}
if (evt.keyCode === ctrlKeyCode) { function initHotKeys() {
vm.ctrlDown = false; hotkeys.bindTo($scope)
evt.stopPropagation(); .add({
evt.preventDefault(); combo: 'ctrl+a',
} description: $translate.instant('rulenode.select-all'),
}; allowIn: ['INPUT', 'SELECT', 'TEXTAREA'],
callback: function (event) {
event.preventDefault();
vm.modelservice.selectAll();
}
})
.add({
combo: 'esc',
description: $translate.instant('rulenode.deselect-all'),
allowIn: ['INPUT', 'SELECT', 'TEXTAREA'],
callback: function (event) {
event.preventDefault();
vm.modelservice.deselectAll();
}
})
.add({
combo: 'ctrl+s',
description: $translate.instant('action.apply'),
allowIn: ['INPUT', 'SELECT', 'TEXTAREA'],
callback: function (event) {
event.preventDefault();
vm.saveRuleChain();
}
})
.add({
combo: 'ctrl+z',
description: $translate.instant('action.decline-changes'),
allowIn: ['INPUT', 'SELECT', 'TEXTAREA'],
callback: function (event) {
event.preventDefault();
vm.revertRuleChain();
}
})
.add({
combo: 'del',
description: $translate.instant('rulenode.delete-selected-objects'),
allowIn: ['INPUT', 'SELECT', 'TEXTAREA'],
callback: function (event) {
event.preventDefault();
vm.modelservice.deleteSelected();
}
})
}
vm.onEditRuleNodeClosed = function() { vm.onEditRuleNodeClosed = function() {
vm.editingRuleNode = null; vm.editingRuleNode = null;
@ -119,15 +142,16 @@ export function RuleChainController($stateParams, $scope, $compile, $q, $mdUtil,
}; };
vm.saveRuleNode = function(theForm) { vm.saveRuleNode = function(theForm) {
theForm.$setPristine(); $scope.$broadcast('form-submit');
vm.isEditingRuleNode = false; if (theForm.$valid) {
vm.ruleChainModel.nodes[vm.editingRuleNodeIndex] = vm.editingRuleNode; theForm.$setPristine();
vm.editingRuleNode = angular.copy(vm.editingRuleNode); vm.ruleChainModel.nodes[vm.editingRuleNodeIndex] = vm.editingRuleNode;
vm.editingRuleNode = angular.copy(vm.editingRuleNode);
}
}; };
vm.saveRuleNodeLink = function(theForm) { vm.saveRuleNodeLink = function(theForm) {
theForm.$setPristine(); theForm.$setPristine();
vm.isEditingRuleNodeLink = false;
vm.ruleChainModel.edges[vm.editingRuleNodeLinkIndex] = vm.editingRuleNodeLink; vm.ruleChainModel.edges[vm.editingRuleNodeLinkIndex] = vm.editingRuleNodeLink;
vm.editingRuleNodeLink = angular.copy(vm.editingRuleNodeLink); vm.editingRuleNodeLink = angular.copy(vm.editingRuleNodeLink);
}; };
@ -235,6 +259,9 @@ export function RuleChainController($stateParams, $scope, $compile, $q, $mdUtil,
vm.isEditingRuleNodeLink = true; vm.isEditingRuleNodeLink = true;
vm.editingRuleNodeLinkIndex = vm.ruleChainModel.edges.indexOf(edge); vm.editingRuleNodeLinkIndex = vm.ruleChainModel.edges.indexOf(edge);
vm.editingRuleNodeLink = angular.copy(edge); vm.editingRuleNodeLink = angular.copy(edge);
$mdUtil.nextTick(() => {
vm.ruleNodeLinkForm.$setPristine();
});
} }
}, },
nodeCallbacks: { nodeCallbacks: {
@ -245,6 +272,9 @@ export function RuleChainController($stateParams, $scope, $compile, $q, $mdUtil,
vm.isEditingRuleNode = true; vm.isEditingRuleNode = true;
vm.editingRuleNodeIndex = vm.ruleChainModel.nodes.indexOf(node); vm.editingRuleNodeIndex = vm.ruleChainModel.nodes.indexOf(node);
vm.editingRuleNode = angular.copy(node); vm.editingRuleNode = angular.copy(node);
$mdUtil.nextTick(() => {
vm.ruleNodeForm.$setPristine();
});
} }
} }
}, },
@ -286,44 +316,40 @@ export function RuleChainController($stateParams, $scope, $compile, $q, $mdUtil,
loadRuleChainLibrary(); loadRuleChainLibrary();
function loadRuleChainLibrary() { function loadRuleChainLibrary() {
ruleChainService.getRuleNodeComponents().then( for (var i=0;i<ruleNodeComponents.length;i++) {
(ruleNodeComponents) => { var ruleNodeComponent = ruleNodeComponents[i];
for (var i=0;i<ruleNodeComponents.length;i++) { var componentType = ruleNodeComponent.type;
var ruleNodeComponent = ruleNodeComponents[i]; var model = vm.ruleNodeTypesModel[componentType].model;
var componentType = ruleNodeComponent.type; var node = {
var model = vm.ruleNodeTypesModel[componentType].model; id: 'node-lib-' + componentType + '-' + model.nodes.length,
var node = { component: ruleNodeComponent,
id: model.nodes.length, name: '',
component: ruleNodeComponent, nodeClass: vm.types.ruleNodeType[componentType].nodeClass,
name: '', icon: vm.types.ruleNodeType[componentType].icon,
nodeClass: vm.types.ruleNodeType[componentType].nodeClass, x: 30,
icon: vm.types.ruleNodeType[componentType].icon, y: 10+50*model.nodes.length,
x: 30, connectors: []
y: 10+50*model.nodes.length, };
connectors: [] if (ruleNodeComponent.configurationDescriptor.nodeDefinition.inEnabled) {
}; node.connectors.push(
if (ruleNodeComponent.configurationDescriptor.nodeDefinition.inEnabled) { {
node.connectors.push( type: flowchartConstants.leftConnectorType,
{ id: model.nodes.length * 2
type: flowchartConstants.leftConnectorType,
id: model.nodes.length * 2
}
);
} }
if (ruleNodeComponent.configurationDescriptor.nodeDefinition.outEnabled) { );
node.connectors.push( }
{ if (ruleNodeComponent.configurationDescriptor.nodeDefinition.outEnabled) {
type: flowchartConstants.rightConnectorType, node.connectors.push(
id: model.nodes.length * 2 + 1 {
} type: flowchartConstants.rightConnectorType,
); id: model.nodes.length * 2 + 1
} }
model.nodes.push(node); );
}
vm.ruleChainLibraryLoaded = true;
prepareRuleChain();
} }
); model.nodes.push(node);
}
vm.ruleChainLibraryLoaded = true;
prepareRuleChain();
} }
function prepareRuleChain() { function prepareRuleChain() {
@ -344,7 +370,7 @@ export function RuleChainController($stateParams, $scope, $compile, $q, $mdUtil,
vm.ruleChainModel.nodes.push( vm.ruleChainModel.nodes.push(
{ {
id: vm.nextNodeID++, id: 'rule-chain-node-' + vm.nextNodeID++,
component: types.inputNodeComponent, component: types.inputNodeComponent,
name: "", name: "",
nodeClass: types.ruleNodeType.INPUT.nodeClass, nodeClass: types.ruleNodeType.INPUT.nodeClass,
@ -375,7 +401,7 @@ export function RuleChainController($stateParams, $scope, $compile, $q, $mdUtil,
var component = ruleChainService.getRuleNodeComponentByClazz(ruleNode.type); var component = ruleChainService.getRuleNodeComponentByClazz(ruleNode.type);
if (component) { if (component) {
var node = { var node = {
id: vm.nextNodeID++, id: 'rule-chain-node-' + vm.nextNodeID++,
ruleNodeId: ruleNode.id, ruleNodeId: ruleNode.id,
additionalInfo: ruleNode.additionalInfo, additionalInfo: ruleNode.additionalInfo,
configuration: ruleNode.configuration, configuration: ruleNode.configuration,
@ -452,7 +478,7 @@ export function RuleChainController($stateParams, $scope, $compile, $q, $mdUtil,
var ruleChainNode = ruleChainNodesMap[ruleChainConnection.additionalInfo.ruleChainNodeId]; var ruleChainNode = ruleChainNodesMap[ruleChainConnection.additionalInfo.ruleChainNodeId];
if (!ruleChainNode) { if (!ruleChainNode) {
ruleChainNode = { ruleChainNode = {
id: vm.nextNodeID++, id: 'rule-chain-node-' + vm.nextNodeID++,
additionalInfo: ruleChainConnection.additionalInfo, additionalInfo: ruleChainConnection.additionalInfo,
targetRuleChainId: ruleChainConnection.targetRuleChainId.id, targetRuleChainId: ruleChainConnection.targetRuleChainId.id,
x: ruleChainConnection.additionalInfo.layoutX, x: ruleChainConnection.additionalInfo.layoutX,
@ -597,7 +623,7 @@ export function RuleChainController($stateParams, $scope, $compile, $q, $mdUtil,
fullscreen: true, fullscreen: true,
targetEvent: $event targetEvent: $event
}).then(function (ruleNode) { }).then(function (ruleNode) {
ruleNode.id = vm.nextNodeID++; ruleNode.id = 'rule-chain-node-' + vm.nextNodeID++;
ruleNode.connectors = []; ruleNode.connectors = [];
if (ruleNode.component.configurationDescriptor.nodeDefinition.inEnabled) { if (ruleNode.component.configurationDescriptor.nodeDefinition.inEnabled) {
ruleNode.connectors.push( ruleNode.connectors.push(
@ -632,6 +658,19 @@ export function RuleChainController($stateParams, $scope, $compile, $q, $mdUtil,
}); });
} }
function objectsSelected() {
return vm.modelservice.nodes.getSelectedNodes().length > 0 ||
vm.modelservice.edges.getSelectedEdges().length > 0
}
function deleteSelected() {
vm.modelservice.deleteSelected();
}
function triggerResize() {
var w = angular.element($window);
w.triggerHandler('resize');
}
} }
/*@ngInject*/ /*@ngInject*/

5
ui/src/app/rulechain/rulechain.routes.js

@ -68,6 +68,11 @@ export default function RuleChainRoutes($stateProvider, NodeTemplatePathProvider
/*@ngInject*/ /*@ngInject*/
function($stateParams, ruleChainService) { function($stateParams, ruleChainService) {
return ruleChainService.getRuleChainMetaData($stateParams.ruleChainId); return ruleChainService.getRuleChainMetaData($stateParams.ruleChainId);
},
ruleNodeComponents:
/*@ngInject*/
function($stateParams, ruleChainService) {
return ruleChainService.getRuleNodeComponents();
} }
}, },
data: { data: {

143
ui/src/app/rulechain/rulechain.scss

@ -18,13 +18,58 @@
.tb-fullscreen-button-style { .tb-fullscreen-button-style {
z-index: 1; z-index: 1;
} }
section.tb-header-buttons.tb-library-open {
pointer-events: none;
position: absolute;
left: 0px;
top: 0px;
z-index: 1;
.md-button.tb-btn-open-library {
left: 0px;
top: 0px;
line-height: 36px;
width: 36px;
height: 36px;
margin: 4px 0 0 4px;
opacity: 0.5;
}
}
.tb-rulechain-library { .tb-rulechain-library {
width: 250px; width: 250px;
min-width: 250px; min-width: 250px;
overflow-y: auto; z-index: 1;
overflow-x: hidden; md-toolbar {
min-height: 48px;
height: 48px;
.md-toolbar-tools>.md-button:last-child {
margin-right: 0px;
}
.md-toolbar-tools {
font-size: 14px;
padding: 0px 6px;
.md-button.md-icon-button {
margin: 0px;
&.tb-small {
height: 32px;
min-height: 32px;
line-height: 20px;
padding: 6px;
width: 32px;
md-icon {
line-height: 20px;
font-size: 20px;
height: 20px;
width: 20px;
min-height: 20px;
min-width: 20px;
}
}
}
}
}
.tb-rulechain-library-panel-group { .tb-rulechain-library-panel-group {
overflow-y: auto;
overflow-x: hidden;
.tb-panel-title { .tb-panel-title {
-webkit-user-select: none; -webkit-user-select: none;
-moz-user-select: none; -moz-user-select: none;
@ -33,7 +78,7 @@
min-width: 180px; min-width: 180px;
} }
.fc-canvas { .fc-canvas {
background: none; background: #f9f9f9;
} }
md-icon.md-expansion-panel-icon { md-icon.md-expansion-panel-icon {
margin-right: 0px; margin-right: 0px;
@ -55,6 +100,7 @@
} }
} }
.tb-rulechain-graph { .tb-rulechain-graph {
z-index: 0;
overflow: auto; overflow: auto;
} }
} }
@ -75,6 +121,7 @@
padding: 5px 10px; padding: 5px 10px;
border-radius: 5px; border-radius: 5px;
background-color: #F15B26; background-color: #F15B26;
pointer-events: none;
color: #333; color: #333;
border: solid 1px #777; border: solid 1px #777;
font-size: 12px; font-size: 12px;
@ -121,10 +168,6 @@
.fc-node { .fc-node {
z-index: 1; z-index: 1;
outline: none; outline: none;
&.fc-hover, &.fc-selected {
-webkit-filter: brightness(70%);
filter: brightness(70%);
}
&.fc-dragging { &.fc-dragging {
z-index: 10; z-index: 10;
} }
@ -132,6 +175,26 @@
padding: 0 15px; padding: 0 15px;
text-align: center; text-align: center;
} }
.fc-node-overlay {
position: absolute;
pointer-events: none;
left: 0;
top: 0;
right: 0;
bottom: 0;
background-color: #000;
opacity: 0;
}
&.fc-hover {
.fc-node-overlay {
opacity: 0.25;
}
}
&.fc-selected {
.fc-node-overlay {
opacity: 0.25;
}
}
} }
.fc-leftConnectors, .fc-rightConnectors { .fc-leftConnectors, .fc-rightConnectors {
@ -170,17 +233,33 @@
margin: 10px; margin: 10px;
border-radius: 5px; border-radius: 5px;
background-color: #ccc; background-color: #ccc;
pointer-events: all;
} }
.fc-connector.fc-hover { .fc-connector.fc-hover {
background-color: #000; background-color: #000;
} }
.fc-arrow-marker {
polygon {
stroke: gray;
fill: gray;
}
}
.fc-arrow-marker-selected {
polygon {
stroke: red;
fill: red;
}
}
.fc-edge { .fc-edge {
outline: none; outline: none;
stroke: gray; stroke: gray;
stroke-width: 4; stroke-width: 4;
fill: transparent; fill: transparent;
transition: stroke-width .2s;
&.fc-selected { &.fc-selected {
stroke: red; stroke: red;
stroke-width: 4; stroke-width: 4;
@ -229,24 +308,53 @@
cursor: pointer; cursor: pointer;
} }
.fc-noselect {
-webkit-touch-callout: none; /* iOS Safari */
-webkit-user-select: none; /* Safari */
-khtml-user-select: none; /* Konqueror HTML */
-moz-user-select: none; /* Firefox */
-ms-user-select: none; /* Internet Explorer/Edge */
user-select: none; /* Non-prefixed version, currently
supported by Chrome and Opera */
}
.fc-edge-label { .fc-edge-label {
position: absolute; position: absolute;
user-select: none; transition: transform .2s;
pointer-events: none;
opacity: 0.8; opacity: 0.8;
&.ng-leave {
transition: 0s none;
}
&.fc-hover {
transform: scale(1.25);
}
&.fc-selected {
.fc-edge-label-text {
span {
border: solid red;
color: red;
}
}
}
.fc-nodedelete {
right: -13px;
top: -30px;
}
&:focus {
outline: 0;
}
} }
.fc-edge-label-text { .fc-edge-label-text {
position: absolute; position: absolute;
left: 50%; -webkit-transform: translate(-50%, -50%);
-webkit-transform: translateX(-50%); transform: translate(-50%, -50%);
transform: translateX(-50%);
white-space: nowrap; white-space: nowrap;
text-align: center; text-align: center;
font-size: 14px; font-size: 14px;
font-weight: 600; font-weight: 600;
top: 5px;
span { span {
cursor: default;
border: solid 2px #003a79; border: solid 2px #003a79;
border-radius: 10px; border-radius: 10px;
color: #003a79; color: #003a79;
@ -255,6 +363,13 @@
} }
} }
.fc-select-rectangle {
border: 2px dashed #5262ff;
position: absolute;
background: rgba(20,125,255,0.1);
z-index: 2;
}
@keyframes dash { @keyframes dash {
from { from {
stroke-dashoffset: 500; stroke-dashoffset: 500;

108
ui/src/app/rulechain/rulechain.tpl.html

@ -16,12 +16,60 @@
--> -->
<md-content flex tb-expand-fullscreen <md-content flex tb-expand-fullscreen tb-confirm-on-exit is-dirty="vm.isDirty"
expand-tooltip-direction="bottom" layout="column" class="tb-rulechain"> expand-tooltip-direction="bottom" layout="column" class="tb-rulechain"
ng-keydown="vm.keyDown($event)"
ng-keyup="vm.keyUp($event)">
<section class="tb-rulechain-container" flex layout="column"> <section class="tb-rulechain-container" flex layout="column">
<div class="tb-rulechain-layout" flex layout="row"> <div class="tb-rulechain-layout" flex layout="row">
<div class="tb-rulechain-library"> <section layout="row" layout-wrap
<md-expansion-panel-group ng-if="vm.ruleChainLibraryLoaded" class="tb-rulechain-library-panel-group" md-component-id="libraryPanelGroup" auto-expand="true" multiple> class="tb-header-buttons md-fab tb-library-open">
<md-button ng-show="!vm.isLibraryOpen"
class="tb-btn-header tb-btn-open-library md-primary md-fab md-fab-top-left"
aria-label="{{ 'action.apply' | translate }}"
ng-click="vm.isLibraryOpen = true">
<md-tooltip md-direction="top">
{{ 'action.apply-changes' | translate }}
</md-tooltip>
<ng-md-icon icon="menu"></ng-md-icon>
</md-button>
</section>
<md-sidenav class="tb-rulechain-library md-sidenav-left md-whiteframe-4dp"
md-disable-backdrop
md-is-locked-open="vm.isLibraryOpen"
md-is-open="vm.isLibraryOpen"
md-component-id="rulechain-library-sidenav" layout="column">
<md-toolbar>
<div class="md-toolbar-tools">
<md-button class="md-icon-button tb-small" aria-label="{{ 'action.search' | translate }}">
<md-icon aria-label="{{ 'action.search' | translate }}" class="material-icons">search</md-icon>
<md-tooltip md-direction="top">
{{'rulenode.search' | translate}}
</md-tooltip>
</md-button>
<div layout="row" md-theme="tb-dark" flex>
<md-input-container flex>
<label>&nbsp;</label>
<input ng-model="vm.ruleNodeSearch" placeholder="{{'rulenode.search' | translate}}"/>
</md-input-container>
</div>
<md-button class="md-icon-button tb-small" aria-label="Close" ng-click="vm.ruleNodeSearch = ''">
<md-icon aria-label="Close" class="material-icons">close</md-icon>
<md-tooltip md-direction="top">
{{ 'action.close' | translate }}
</md-tooltip>
</md-button>
<md-button class="md-icon-button tb-small" aria-label="Close" ng-click="vm.isLibraryOpen = false">
<md-icon aria-label="Close" class="material-icons">chevron_left</md-icon>
<md-tooltip md-direction="top">
{{ 'action.close' | translate }}
</md-tooltip>
</md-button>
</div>
</md-toolbar>
<md-expansion-panel-group flex
ng-if="vm.ruleChainLibraryLoaded" class="tb-rulechain-library-panel-group"
md-component-id="libraryPanelGroup" auto-expand="true" multiple>
<md-expansion-panel md-component-id="{{typeId}}" id="{{typeId}}" ng-repeat="(typeId, typeModel) in vm.ruleNodeTypesModel"> <md-expansion-panel md-component-id="{{typeId}}" id="{{typeId}}" ng-repeat="(typeId, typeModel) in vm.ruleNodeTypesModel">
<md-expansion-panel-collapsed ng-mouseenter="vm.typeHeaderMouseEnter($event, typeId)" <md-expansion-panel-collapsed ng-mouseenter="vm.typeHeaderMouseEnter($event, typeId)"
ng-mouseleave="vm.destroyTooltips()"> ng-mouseleave="vm.destroyTooltips()">
@ -47,11 +95,9 @@
</md-expansion-panel-expanded> </md-expansion-panel-expanded>
</md-expansion-panel> </md-expansion-panel>
</md-expansion-panel-group> </md-expansion-panel-group>
</div> </md-sidenav>
<div flex class="tb-rulechain-graph"> <div flex class="tb-rulechain-graph">
<fc-canvas id="tb-rulchain-canvas" <fc-canvas id="tb-rulchain-canvas"
ng-keydown="vm.keyDown($event)"
ng-keyup="vm.keyUp($event)"
model="vm.ruleChainModel" model="vm.ruleChainModel"
selected-objects="vm.selectedObjects" selected-objects="vm.selectedObjects"
edge-style="curved" edge-style="curved"
@ -65,9 +111,11 @@
</div> </div>
<tb-details-sidenav class="tb-rulenode-details-sidenav" <tb-details-sidenav class="tb-rulenode-details-sidenav"
header-title="{{vm.editingRuleNode.name}}" header-title="{{vm.editingRuleNode.name}}"
header-subtitle="{{'rulenode.rulenode-details' | translate}}" header-subtitle="{{(vm.types.ruleNodeType[vm.editingRuleNode.component.type].name | translate)
is-read-only="false" + ' - ' + vm.editingRuleNode.component.name}}"
is-read-only="vm.selectedRuleNodeTabIndex > 0"
is-open="vm.isEditingRuleNode" is-open="vm.isEditingRuleNode"
tb-enable-backdrop
is-always-edit="true" is-always-edit="true"
on-close-details="vm.onEditRuleNodeClosed()" on-close-details="vm.onEditRuleNodeClosed()"
on-toggle-details-edit-mode="vm.onRevertRuleNodeEdit(vm.ruleNodeForm)" on-toggle-details-edit-mode="vm.onRevertRuleNodeEdit(vm.ruleNodeForm)"
@ -76,22 +124,37 @@
<details-buttons tb-help="vm.helpLinkIdForRuleNodeType()" help-container-id="help-container"> <details-buttons tb-help="vm.helpLinkIdForRuleNodeType()" help-container-id="help-container">
<div id="help-container"></div> <div id="help-container"></div>
</details-buttons> </details-buttons>
<form name="vm.ruleNodeForm" ng-if="vm.isEditingRuleNode"> <md-tabs md-selected="vm.selectedRuleNodeTabIndex"
<tb-rule-node id="ruleNodeTabs" md-border-bottom flex class="tb-absolute-fill" ng-if="vm.isEditingRuleNode">
rule-node="vm.editingRuleNode" <md-tab label="{{ 'rulenode.details' | translate }}">
rule-chain-id="vm.ruleChain.id.id" <form name="vm.ruleNodeForm">
is-edit="true" <tb-rule-node
is-read-only="false" rule-node="vm.editingRuleNode"
on-delete-rule-node="vm.deleteRuleNode(event, vm.editingRuleNode)" rule-chain-id="vm.ruleChain.id.id"
the-form="vm.ruleNodeForm"> is-edit="true"
</tb-rule-node> is-read-only="false"
</form> on-delete-rule-node="vm.deleteRuleNode(event, vm.editingRuleNode)"
the-form="vm.ruleNodeForm">
</tb-rule-node>
</form>
</md-tab>
<md-tab ng-if="vm.isEditingRuleNode && vm.editingRuleNode.ruleNodeId"
md-on-select="vm.triggerResize()" label="{{ 'rulenode.events' | translate }}">
<tb-event-table flex entity-type="vm.types.entityType.rulenode"
entity-id="vm.editingRuleNode.ruleNodeId.id"
tenant-id="vm.ruleChain.tenantId.id"
debug-event-types="{{vm.types.debugEventType.debugRuleNode.value}}"
default-event-type="{{vm.types.debugEventType.debugRuleNode.value}}">
</tb-event-table>
</md-tab>
</md-tabs>
</tb-details-sidenav> </tb-details-sidenav>
<tb-details-sidenav class="tb-rulenode-link-details-sidenav" <tb-details-sidenav class="tb-rulenode-link-details-sidenav"
header-title="{{vm.editingRuleNodeLink.label}}" header-title="{{vm.editingRuleNodeLink.label}}"
header-subtitle="{{'rulenode.link-details' | translate}}" header-subtitle="{{'rulenode.link-details' | translate}}"
is-read-only="false" is-read-only="false"
is-open="vm.isEditingRuleNodeLink" is-open="vm.isEditingRuleNodeLink"
tb-enable-backdrop
is-always-edit="true" is-always-edit="true"
on-close-details="vm.onEditRuleNodeLinkClosed()" on-close-details="vm.onEditRuleNodeLinkClosed()"
on-toggle-details-edit-mode="vm.onRevertRuleNodeLinkEdit(vm.ruleNodeLinkForm)" on-toggle-details-edit-mode="vm.onRevertRuleNodeLinkEdit(vm.ruleNodeLinkForm)"
@ -112,6 +175,13 @@
</tb-details-sidenav> </tb-details-sidenav>
</section> </section>
<section layout="row" layout-wrap class="tb-footer-buttons md-fab" layout-align="start end"> <section layout="row" layout-wrap class="tb-footer-buttons md-fab" layout-align="start end">
<md-button ng-disabled="$root.loading" ng-show="vm.objectsSelected()" class="tb-btn-footer md-accent md-hue-2 md-fab"
ng-click="vm.deleteSelected()" aria-label="{{ 'action.delete' | translate }}">
<md-tooltip md-direction="top">
{{ 'rulenode.delete-selected-objects' | translate }}
</md-tooltip>
<ng-md-icon icon="delete"></ng-md-icon>
</md-button>
<md-button ng-disabled="$root.loading || !vm.isDirty" <md-button ng-disabled="$root.loading || !vm.isDirty"
class="tb-btn-footer md-accent md-hue-2 md-fab" class="tb-btn-footer md-accent md-hue-2 md-fab"
aria-label="{{ 'action.apply' | translate }}" aria-label="{{ 'action.apply' | translate }}"

3
ui/src/app/rulechain/rulechains.tpl.html

@ -55,7 +55,8 @@
<tb-event-table flex entity-type="vm.types.entityType.rulechain" <tb-event-table flex entity-type="vm.types.entityType.rulechain"
entity-id="vm.grid.operatingItem().id.id" entity-id="vm.grid.operatingItem().id.id"
tenant-id="vm.grid.operatingItem().tenantId.id" tenant-id="vm.grid.operatingItem().tenantId.id"
default-event-type="{{vm.types.eventType.lcEvent.value}}"> debug-event-types="{{vm.types.debugEventType.debugRuleChain.value}}"
default-event-type="{{vm.types.debugEventType.debugRuleChain.value}}">
</tb-event-table> </tb-event-table>
</md-tab> </md-tab>
<md-tab ng-if="!vm.grid.detailsConfig.isDetailsEditMode && vm.isRuleChainEditable(vm.grid.operatingItem())" md-on-select="vm.grid.triggerResize()" label="{{ 'relation.relations' | translate }}"> <md-tab ng-if="!vm.grid.detailsConfig.isDetailsEditMode && vm.isRuleChainEditable(vm.grid.operatingItem())" md-on-select="vm.grid.triggerResize()" label="{{ 'relation.relations' | translate }}">

78
ui/src/app/rulechain/rulenode-config.directive.js

@ -0,0 +1,78 @@
/*
* Copyright © 2016-2018 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.
*/
/* eslint-disable import/no-unresolved, import/default */
import ruleNodeConfigTemplate from './rulenode-config.tpl.html';
/* eslint-enable import/no-unresolved, import/default */
/*@ngInject*/
export default function RuleNodeConfigDirective($compile, $templateCache, $injector, $translate) {
var linker = function (scope, element, attrs, ngModelCtrl) {
var template = $templateCache.get(ruleNodeConfigTemplate);
element.html(template);
scope.$watch('configuration', function (newVal, prevVal) {
if (!angular.equals(newVal, prevVal)) {
ngModelCtrl.$setViewValue(scope.configuration);
}
});
ngModelCtrl.$render = function () {
scope.configuration = ngModelCtrl.$viewValue;
};
scope.useDefinedDirective = function() {
return scope.nodeDefinition &&
scope.nodeDefinition.configDirective && !scope.definedDirectiveError;
};
scope.$watch('nodeDefinition', () => {
if (scope.nodeDefinition) {
validateDefinedDirective();
}
});
function validateDefinedDirective() {
if (scope.nodeDefinition.uiResourceLoadError && scope.nodeDefinition.uiResourceLoadError.length) {
scope.definedDirectiveError = scope.nodeDefinition.uiResourceLoadError;
} else {
var definedDirective = scope.nodeDefinition.configDirective;
if (definedDirective && definedDirective.length) {
if (!$injector.has(definedDirective + 'Directive')) {
scope.definedDirectiveError = $translate.instant('rulenode.directive-is-not-loaded', {directiveName: definedDirective});
}
}
}
}
$compile(element.contents())(scope);
};
return {
restrict: "E",
require: "^ngModel",
scope: {
nodeDefinition:'=',
required:'=ngRequired',
readonly:'=ngReadonly'
},
link: linker
};
}

32
ui/src/app/rulechain/rulenode-config.tpl.html

@ -0,0 +1,32 @@
<!--
Copyright © 2016-2018 The Thingsboard Authors
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
-->
<tb-rule-node-defined-config ng-if="useDefinedDirective()"
ng-model="configuration"
rule-node-directive="{{nodeDefinition.configDirective}}"
ng-required="required"
ng-readonly="readonly">
</tb-rule-node-defined-config>
<div class="tb-rulenode-directive-error" ng-if="definedDirectiveError">{{definedDirectiveError}}</div>
<tb-json-object-edit ng-if="!useDefinedDirective()"
class="tb-rule-node-configuration-json"
ng-model="configuration"
label="{{ 'rulenode.configuration' | translate }}"
ng-required="required"
fill-height="true">
</tb-json-object-edit>

67
ui/src/app/rulechain/rulenode-defined-config.directive.js

@ -0,0 +1,67 @@
/*
* Copyright © 2016-2018 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.
*/
const SNAKE_CASE_REGEXP = /[A-Z]/g;
/*@ngInject*/
export default function RuleNodeDefinedConfigDirective($compile) {
var linker = function (scope, element, attrs, ngModelCtrl) {
attrs.$observe('ruleNodeDirective', function() {
loadTemplate();
});
scope.$watch('configuration', function (newVal, prevVal) {
if (!angular.equals(newVal, prevVal)) {
ngModelCtrl.$setViewValue(scope.configuration);
}
});
ngModelCtrl.$render = function () {
scope.configuration = ngModelCtrl.$viewValue;
};
function loadTemplate() {
if (scope.ruleNodeConfigScope) {
scope.ruleNodeConfigScope.$destroy();
}
var directive = snake_case(attrs.ruleNodeDirective, '-');
var template = `<${directive} ng-model="configuration" ng-required="required" ng-readonly="readonly"></${directive}>`;
element.html(template);
scope.ruleNodeConfigScope = scope.$new();
$compile(element.contents())(scope.ruleNodeConfigScope);
}
function snake_case(name, separator) {
separator = separator || '_';
return name.replace(SNAKE_CASE_REGEXP, function(letter, pos) {
return (pos ? separator : '') + letter.toLowerCase();
});
}
};
return {
restrict: "E",
require: "^ngModel",
scope: {
required:'=ngRequired',
readonly:'=ngReadonly'
},
link: linker
};
}

38
ui/src/app/rulechain/rulenode-fieldset.tpl.html

@ -21,28 +21,26 @@
<md-content class="md-padding tb-rulenode" layout="column"> <md-content class="md-padding tb-rulenode" layout="column">
<fieldset ng-disabled="$root.loading || !isEdit || isReadOnly"> <fieldset ng-disabled="$root.loading || !isEdit || isReadOnly">
<md-input-container class="md-block">
<label translate>rulenode.type</label>
<input readonly name="type" ng-model="ruleNode.component.name">
</md-input-container>
<section ng-if="ruleNode.component.type != types.ruleNodeType.RULE_CHAIN.value"> <section ng-if="ruleNode.component.type != types.ruleNodeType.RULE_CHAIN.value">
<md-input-container class="md-block"> <section layout="column" layout-gt-sm="row">
<label translate>rulenode.name</label> <md-input-container flex class="md-block">
<input required name="name" ng-model="ruleNode.name"> <label translate>rulenode.name</label>
<div ng-messages="theForm.name.$error"> <input required name="name" ng-model="ruleNode.name">
<div translate ng-message="required">rulenode.name-required</div> <div ng-messages="theForm.name.$error">
</div> <div translate ng-message="required">rulenode.name-required</div>
</md-input-container> </div>
<md-input-container class="md-block"> </md-input-container>
<md-checkbox ng-disabled="$root.loading || !isEdit" aria-label="{{ 'rulenode.debug-mode' | translate }}" <md-input-container class="md-block">
ng-model="ruleNode.debugMode">{{ 'rulenode.debug-mode' | translate }} <md-checkbox ng-disabled="$root.loading || !isEdit" aria-label="{{ 'rulenode.debug-mode' | translate }}"
</md-checkbox> ng-model="ruleNode.debugMode">{{ 'rulenode.debug-mode' | translate }}
</md-input-container> </md-checkbox>
<tb-json-object-edit class="tb-rule-node-configuration-json" ng-model="ruleNode.configuration" </md-input-container>
label="{{ 'rulenode.configuration' | translate }}" </section>
<tb-rule-node-config ng-model="ruleNode.configuration"
ng-required="true" ng-required="true"
fill-height="true"> node-definition="ruleNode.component.configurationDescriptor.nodeDefinition"
</tb-json-object-edit> ng-readonly="$root.loading || !isEdit || isReadOnly">
</tb-rule-node-config>
<md-input-container class="md-block"> <md-input-container class="md-block">
<label translate>rulenode.description</label> <label translate>rulenode.description</label>
<textarea ng-model="ruleNode.additionalInfo.description" rows="2"></textarea> <textarea ng-model="ruleNode.additionalInfo.description" rows="2"></textarea>

6
ui/src/app/rulechain/rulenode.scss

@ -19,4 +19,10 @@
height: 300px; height: 300px;
display: block; display: block;
} }
}
.tb-rulenode-directive-error {
color: rgb(221,44,0);
font-size: 13px;
font-weight: 400;
} }

1
ui/src/app/rulechain/rulenode.tpl.html

@ -22,6 +22,7 @@
ng-mousedown="callbacks.mouseDown($event, node)" ng-mousedown="callbacks.mouseDown($event, node)"
ng-mouseenter="callbacks.mouseEnter($event, node)" ng-mouseenter="callbacks.mouseEnter($event, node)"
ng-mouseleave="callbacks.mouseLeave($event, node)"> ng-mouseleave="callbacks.mouseLeave($event, node)">
<div class="{{flowchartConstants.nodeOverlayClass}}"></div>
<div class="tb-rule-node {{node.nodeClass}}"> <div class="tb-rule-node {{node.nodeClass}}">
<md-icon aria-label="node-type-icon" flex="15" <md-icon aria-label="node-type-icon" flex="15"
class="material-icons">{{node.icon}}</md-icon> class="material-icons">{{node.icon}}</md-icon>

Loading…
Cancel
Save