Commit b16a02d97fe6266a647ef8e02e940e2e064e97c9

Authored by Andrew Shvayka
2 parents 6823b896 39f682ce

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

Showing 77 changed files with 1385 additions and 349 deletions
@@ -25,6 +25,7 @@ import com.typesafe.config.Config; @@ -25,6 +25,7 @@ import com.typesafe.config.Config;
25 import com.typesafe.config.ConfigFactory; 25 import com.typesafe.config.ConfigFactory;
26 import lombok.Getter; 26 import lombok.Getter;
27 import lombok.Setter; 27 import lombok.Setter;
  28 +import lombok.extern.slf4j.Slf4j;
28 import org.springframework.beans.factory.annotation.Autowired; 29 import org.springframework.beans.factory.annotation.Autowired;
29 import org.springframework.beans.factory.annotation.Value; 30 import org.springframework.beans.factory.annotation.Value;
30 import org.springframework.stereotype.Component; 31 import org.springframework.stereotype.Component;
@@ -38,6 +39,7 @@ import org.thingsboard.server.common.data.id.EntityId; @@ -38,6 +39,7 @@ import org.thingsboard.server.common.data.id.EntityId;
38 import org.thingsboard.server.common.data.id.TenantId; 39 import org.thingsboard.server.common.data.id.TenantId;
39 import org.thingsboard.server.common.data.plugin.ComponentLifecycleEvent; 40 import org.thingsboard.server.common.data.plugin.ComponentLifecycleEvent;
40 import org.thingsboard.server.common.msg.TbMsg; 41 import org.thingsboard.server.common.msg.TbMsg;
  42 +import org.thingsboard.server.common.msg.TbMsgDataType;
41 import org.thingsboard.server.common.msg.cluster.ServerAddress; 43 import org.thingsboard.server.common.msg.cluster.ServerAddress;
42 import org.thingsboard.server.common.transport.auth.DeviceAuthService; 44 import org.thingsboard.server.common.transport.auth.DeviceAuthService;
43 import org.thingsboard.server.controller.plugin.PluginWebSocketMsgEndpoint; 45 import org.thingsboard.server.controller.plugin.PluginWebSocketMsgEndpoint;
@@ -60,11 +62,13 @@ import org.thingsboard.server.service.cluster.routing.ClusterRoutingService; @@ -60,11 +62,13 @@ import org.thingsboard.server.service.cluster.routing.ClusterRoutingService;
60 import org.thingsboard.server.service.cluster.rpc.ClusterRpcService; 62 import org.thingsboard.server.service.cluster.rpc.ClusterRpcService;
61 import org.thingsboard.server.service.component.ComponentDiscoveryService; 63 import org.thingsboard.server.service.component.ComponentDiscoveryService;
62 64
  65 +import java.io.IOException;
63 import java.io.PrintWriter; 66 import java.io.PrintWriter;
64 import java.io.StringWriter; 67 import java.io.StringWriter;
65 import java.nio.charset.StandardCharsets; 68 import java.nio.charset.StandardCharsets;
66 import java.util.Optional; 69 import java.util.Optional;
67 70
  71 +@Slf4j
68 @Component 72 @Component
69 public class ActorSystemContext { 73 public class ActorSystemContext {
70 private static final String AKKA_CONF_FILE_NAME = "actor-system.conf"; 74 private static final String AKKA_CONF_FILE_NAME = "actor-system.conf";
@@ -292,38 +296,49 @@ public class ActorSystemContext { @@ -292,38 +296,49 @@ public class ActorSystemContext {
292 } 296 }
293 297
294 private void persistDebug(TenantId tenantId, EntityId entityId, String type, TbMsg tbMsg, Throwable error) { 298 private void persistDebug(TenantId tenantId, EntityId entityId, String type, TbMsg tbMsg, Throwable error) {
295 - Event event = new Event();  
296 - event.setTenantId(tenantId);  
297 - event.setEntityId(entityId);  
298 - event.setType(DataConstants.DEBUG);  
299 -  
300 - ObjectNode node = mapper.createObjectNode()  
301 - .put("type", type)  
302 - .put("server", getServerAddress())  
303 - .put("entityId", tbMsg.getOriginator().getId().toString())  
304 - .put("entityName", tbMsg.getOriginator().getEntityType().name())  
305 - .put("msgId", tbMsg.getId().toString())  
306 - .put("msgType", tbMsg.getType())  
307 - .put("dataType", tbMsg.getDataType().name());  
308 -  
309 - ObjectNode mdNode = node.putObject("metadata");  
310 - tbMsg.getMetaData().getData().forEach(mdNode::put); 299 + try {
  300 + Event event = new Event();
  301 + event.setTenantId(tenantId);
  302 + event.setEntityId(entityId);
  303 + event.setType(DataConstants.DEBUG_RULE_NODE);
  304 +
  305 + String metadata = mapper.writeValueAsString(tbMsg.getMetaData().getData());
  306 +
  307 + ObjectNode node = mapper.createObjectNode()
  308 + .put("type", type)
  309 + .put("server", getServerAddress())
  310 + .put("entityId", tbMsg.getOriginator().getId().toString())
  311 + .put("entityName", tbMsg.getOriginator().getEntityType().name())
  312 + .put("msgId", tbMsg.getId().toString())
  313 + .put("msgType", tbMsg.getType())
  314 + .put("dataType", tbMsg.getDataType().name())
  315 + .put("data", convertToString(tbMsg.getDataType(), tbMsg.getData()))
  316 + .put("metadata", metadata);
  317 +
  318 + if (error != null) {
  319 + node = node.put("error", toString(error));
  320 + }
  321 +
  322 + event.setBody(node);
  323 + eventService.save(event);
  324 + } catch (IOException ex) {
  325 + log.warn("Failed to persist rule node debug message", ex);
  326 + }
  327 + }
311 328
312 - switch (tbMsg.getDataType()) { 329 + private String convertToString(TbMsgDataType messageType, byte[] data) {
  330 + if (data == null) {
  331 + return null;
  332 + }
  333 + switch (messageType) {
  334 + case JSON:
  335 + case TEXT:
  336 + return new String(data, StandardCharsets.UTF_8);
313 case BINARY: 337 case BINARY:
314 - node.put("data", Base64Utils.encodeUrlSafe(tbMsg.getData()));  
315 - break; 338 + return Base64Utils.encodeToString(data);
316 default: 339 default:
317 - node.put("data", new String(tbMsg.getData(), StandardCharsets.UTF_8));  
318 - break;  
319 - }  
320 -  
321 - if (error != null) {  
322 - node = node.put("error", toString(error)); 340 + throw new RuntimeException("Message type: " + messageType + " is not supported!");
323 } 341 }
324 -  
325 - event.setBody(node);  
326 - eventService.save(event);  
327 } 342 }
328 343
329 public static Exception toException(Throwable error) { 344 public static Exception toException(Throwable error) {
@@ -15,9 +15,12 @@ @@ -15,9 +15,12 @@
15 */ 15 */
16 package org.thingsboard.server.actors.ruleChain; 16 package org.thingsboard.server.actors.ruleChain;
17 17
  18 +import akka.actor.ActorContext;
  19 +import akka.actor.ActorRef;
18 import org.thingsboard.rule.engine.api.ListeningExecutor; 20 import org.thingsboard.rule.engine.api.ListeningExecutor;
19 import org.thingsboard.rule.engine.api.TbContext; 21 import org.thingsboard.rule.engine.api.TbContext;
20 import org.thingsboard.server.actors.ActorSystemContext; 22 import org.thingsboard.server.actors.ActorSystemContext;
  23 +import org.thingsboard.server.common.data.id.RuleNodeId;
21 import org.thingsboard.server.common.msg.TbMsg; 24 import org.thingsboard.server.common.msg.TbMsg;
22 import org.thingsboard.server.common.msg.cluster.ServerAddress; 25 import org.thingsboard.server.common.msg.cluster.ServerAddress;
23 import org.thingsboard.server.dao.alarm.AlarmService; 26 import org.thingsboard.server.dao.alarm.AlarmService;
@@ -30,8 +33,10 @@ import org.thingsboard.server.dao.relation.RelationService; @@ -30,8 +33,10 @@ import org.thingsboard.server.dao.relation.RelationService;
30 import org.thingsboard.server.dao.rule.RuleChainService; 33 import org.thingsboard.server.dao.rule.RuleChainService;
31 import org.thingsboard.server.dao.timeseries.TimeseriesService; 34 import org.thingsboard.server.dao.timeseries.TimeseriesService;
32 import org.thingsboard.server.dao.user.UserService; 35 import org.thingsboard.server.dao.user.UserService;
  36 +import scala.concurrent.duration.Duration;
33 37
34 import java.util.Set; 38 import java.util.Set;
  39 +import java.util.concurrent.TimeUnit;
35 40
36 /** 41 /**
37 * Created by ashvayka on 19.03.18. 42 * Created by ashvayka on 19.03.18.
@@ -61,7 +66,12 @@ class DefaultTbContext implements TbContext { @@ -61,7 +66,12 @@ class DefaultTbContext implements TbContext {
61 66
62 @Override 67 @Override
63 public void tellSelf(TbMsg msg, long delayMs) { 68 public void tellSelf(TbMsg msg, long delayMs) {
64 - throw new RuntimeException("Not Implemented!"); 69 + //TODO: add persistence layer
  70 + scheduleMsgWithDelay(new RuleNodeToSelfMsg(msg), delayMs, nodeCtx.getSelfActor());
  71 + }
  72 +
  73 + private void scheduleMsgWithDelay(Object msg, long delayInMs, ActorRef target) {
  74 + mainCtx.getScheduler().scheduleOnce(Duration.create(delayInMs, TimeUnit.MILLISECONDS), target, msg, mainCtx.getActorSystem().dispatcher(), nodeCtx.getSelfActor());
65 } 75 }
66 76
67 @Override 77 @Override
@@ -93,6 +103,11 @@ class DefaultTbContext implements TbContext { @@ -93,6 +103,11 @@ class DefaultTbContext implements TbContext {
93 } 103 }
94 104
95 @Override 105 @Override
  106 + public RuleNodeId getSelfId() {
  107 + return nodeCtx.getSelf().getId();
  108 + }
  109 +
  110 + @Override
96 public void tellNext(TbMsg msg, Set<String> relationTypes) { 111 public void tellNext(TbMsg msg, Set<String> relationTypes) {
97 relationTypes.forEach(type -> tellNext(msg, type)); 112 relationTypes.forEach(type -> tellNext(msg, type));
98 } 113 }
@@ -47,12 +47,25 @@ public class RuleNodeActor extends ComponentActor<RuleNodeId, RuleNodeActorMessa @@ -47,12 +47,25 @@ public class RuleNodeActor extends ComponentActor<RuleNodeId, RuleNodeActorMessa
47 case RULE_TO_SELF_ERROR_MSG: 47 case RULE_TO_SELF_ERROR_MSG:
48 onRuleNodeToSelfErrorMsg((RuleNodeToSelfErrorMsg) msg); 48 onRuleNodeToSelfErrorMsg((RuleNodeToSelfErrorMsg) msg);
49 break; 49 break;
  50 + case RULE_TO_SELF_MSG:
  51 + onRuleNodeToSelfMsg((RuleNodeToSelfMsg) msg);
  52 + break;
50 default: 53 default:
51 return false; 54 return false;
52 } 55 }
53 return true; 56 return true;
54 } 57 }
55 58
  59 + private void onRuleNodeToSelfMsg(RuleNodeToSelfMsg msg) {
  60 + logger.debug("[{}] Going to process rule msg: {}", id, msg.getMsg());
  61 + try {
  62 + processor.onRuleToSelfMsg(msg);
  63 + increaseMessagesProcessedCount();
  64 + } catch (Exception e) {
  65 + logAndPersist("onRuleMsg", e);
  66 + }
  67 + }
  68 +
56 private void onRuleChainToRuleNodeMsg(RuleChainToRuleNodeMsg msg) { 69 private void onRuleChainToRuleNodeMsg(RuleChainToRuleNodeMsg msg) {
57 logger.debug("[{}] Going to process rule msg: {}", id, msg.getMsg()); 70 logger.debug("[{}] Going to process rule msg: {}", id, msg.getMsg());
58 try { 71 try {
@@ -18,9 +18,10 @@ package org.thingsboard.server.actors.ruleChain; @@ -18,9 +18,10 @@ package org.thingsboard.server.actors.ruleChain;
18 import akka.actor.ActorContext; 18 import akka.actor.ActorContext;
19 import akka.actor.ActorRef; 19 import akka.actor.ActorRef;
20 import akka.event.LoggingAdapter; 20 import akka.event.LoggingAdapter;
  21 +import org.thingsboard.rule.engine.api.TbContext;
21 import org.thingsboard.rule.engine.api.TbNode; 22 import org.thingsboard.rule.engine.api.TbNode;
22 import org.thingsboard.rule.engine.api.TbNodeConfiguration; 23 import org.thingsboard.rule.engine.api.TbNodeConfiguration;
23 -import org.thingsboard.rule.engine.api.TbNodeState; 24 +import org.thingsboard.rule.engine.api.TbNodeException;
24 import org.thingsboard.server.actors.ActorSystemContext; 25 import org.thingsboard.server.actors.ActorSystemContext;
25 import org.thingsboard.server.actors.shared.ComponentMsgProcessor; 26 import org.thingsboard.server.actors.shared.ComponentMsgProcessor;
26 import org.thingsboard.server.common.data.id.RuleChainId; 27 import org.thingsboard.server.common.data.id.RuleChainId;
@@ -31,6 +32,8 @@ import org.thingsboard.server.common.data.rule.RuleNode; @@ -31,6 +32,8 @@ import org.thingsboard.server.common.data.rule.RuleNode;
31 import org.thingsboard.server.common.msg.cluster.ClusterEventMsg; 32 import org.thingsboard.server.common.msg.cluster.ClusterEventMsg;
32 import org.thingsboard.server.dao.rule.RuleChainService; 33 import org.thingsboard.server.dao.rule.RuleChainService;
33 34
  35 +import java.util.concurrent.ExecutionException;
  36 +
34 /** 37 /**
35 * @author Andrew Shvayka 38 * @author Andrew Shvayka
36 */ 39 */
@@ -41,6 +44,7 @@ public class RuleNodeActorMessageProcessor extends ComponentMsgProcessor<RuleNod @@ -41,6 +44,7 @@ public class RuleNodeActorMessageProcessor extends ComponentMsgProcessor<RuleNod
41 private final RuleChainService service; 44 private final RuleChainService service;
42 private RuleNode ruleNode; 45 private RuleNode ruleNode;
43 private TbNode tbNode; 46 private TbNode tbNode;
  47 + private TbContext defaultCtx;
44 48
45 RuleNodeActorMessageProcessor(TenantId tenantId, RuleChainId ruleChainId, RuleNodeId ruleNodeId, ActorSystemContext systemContext 49 RuleNodeActorMessageProcessor(TenantId tenantId, RuleChainId ruleChainId, RuleNodeId ruleNodeId, ActorSystemContext systemContext
46 , LoggingAdapter logger, ActorRef parent, ActorRef self) { 50 , LoggingAdapter logger, ActorRef parent, ActorRef self) {
@@ -49,6 +53,7 @@ public class RuleNodeActorMessageProcessor extends ComponentMsgProcessor<RuleNod @@ -49,6 +53,7 @@ public class RuleNodeActorMessageProcessor extends ComponentMsgProcessor<RuleNod
49 this.self = self; 53 this.self = self;
50 this.service = systemContext.getRuleChainService(); 54 this.service = systemContext.getRuleChainService();
51 this.ruleNode = systemContext.getRuleChainService().findRuleNodeById(entityId); 55 this.ruleNode = systemContext.getRuleChainService().findRuleNodeById(entityId);
  56 + this.defaultCtx = new DefaultTbContext(systemContext, new RuleNodeCtx(tenantId, parent, self, ruleNode));
52 } 57 }
53 58
54 @Override 59 @Override
@@ -80,6 +85,14 @@ public class RuleNodeActorMessageProcessor extends ComponentMsgProcessor<RuleNod @@ -80,6 +85,14 @@ public class RuleNodeActorMessageProcessor extends ComponentMsgProcessor<RuleNod
80 85
81 } 86 }
82 87
  88 + public void onRuleToSelfMsg(RuleNodeToSelfMsg msg) throws Exception {
  89 + checkActive();
  90 + if (ruleNode.isDebugMode()) {
  91 + systemContext.persistDebugInput(tenantId, entityId, msg.getMsg());
  92 + }
  93 + tbNode.onMsg(defaultCtx, msg.getMsg());
  94 + }
  95 +
83 void onRuleChainToRuleNodeMsg(RuleChainToRuleNodeMsg msg) throws Exception { 96 void onRuleChainToRuleNodeMsg(RuleChainToRuleNodeMsg msg) throws Exception {
84 checkActive(); 97 checkActive();
85 if (ruleNode.isDebugMode()) { 98 if (ruleNode.isDebugMode()) {
@@ -91,9 +104,8 @@ public class RuleNodeActorMessageProcessor extends ComponentMsgProcessor<RuleNod @@ -91,9 +104,8 @@ public class RuleNodeActorMessageProcessor extends ComponentMsgProcessor<RuleNod
91 private TbNode initComponent(RuleNode ruleNode) throws Exception { 104 private TbNode initComponent(RuleNode ruleNode) throws Exception {
92 Class<?> componentClazz = Class.forName(ruleNode.getType()); 105 Class<?> componentClazz = Class.forName(ruleNode.getType());
93 TbNode tbNode = (TbNode) (componentClazz.newInstance()); 106 TbNode tbNode = (TbNode) (componentClazz.newInstance());
94 - tbNode.init(new TbNodeConfiguration(ruleNode.getConfiguration()), new TbNodeState()); 107 + tbNode.init(defaultCtx, new TbNodeConfiguration(ruleNode.getConfiguration()));
95 return tbNode; 108 return tbNode;
96 } 109 }
97 110
98 -  
99 } 111 }
  1 +/**
  2 + * Copyright © 2016-2018 The Thingsboard Authors
  3 + *
  4 + * Licensed under the Apache License, Version 2.0 (the "License");
  5 + * you may not use this file except in compliance with the License.
  6 + * You may obtain a copy of the License at
  7 + *
  8 + * http://www.apache.org/licenses/LICENSE-2.0
  9 + *
  10 + * Unless required by applicable law or agreed to in writing, software
  11 + * distributed under the License is distributed on an "AS IS" BASIS,
  12 + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  13 + * See the License for the specific language governing permissions and
  14 + * limitations under the License.
  15 + */
  16 +package org.thingsboard.server.actors.ruleChain;
  17 +
  18 +import lombok.Data;
  19 +import org.thingsboard.server.common.data.id.RuleNodeId;
  20 +import org.thingsboard.server.common.msg.MsgType;
  21 +import org.thingsboard.server.common.msg.TbActorMsg;
  22 +import org.thingsboard.server.common.msg.TbMsg;
  23 +
  24 +/**
  25 + * Created by ashvayka on 19.03.18.
  26 + */
  27 +@Data
  28 +final class RuleNodeToSelfMsg implements TbActorMsg {
  29 +
  30 + private final TbMsg msg;
  31 +
  32 + @Override
  33 + public MsgType getMsgType() {
  34 + return MsgType.RULE_TO_SELF_MSG;
  35 + }
  36 +
  37 +}
@@ -192,6 +192,8 @@ public class AnnotationComponentDiscoveryService implements ComponentDiscoverySe @@ -192,6 +192,8 @@ public class AnnotationComponentDiscoveryService implements ComponentDiscoverySe
192 NodeConfiguration config = configClazz.newInstance(); 192 NodeConfiguration config = configClazz.newInstance();
193 NodeConfiguration defaultConfiguration = config.defaultConfiguration(); 193 NodeConfiguration defaultConfiguration = config.defaultConfiguration();
194 nodeDefinition.setDefaultConfiguration(mapper.valueToTree(defaultConfiguration)); 194 nodeDefinition.setDefaultConfiguration(mapper.valueToTree(defaultConfiguration));
  195 + nodeDefinition.setUiResources(nodeAnnotation.uiResources());
  196 + nodeDefinition.setConfigDirective(nodeAnnotation.configDirective());
195 return nodeDefinition; 197 return nodeDefinition;
196 } 198 }
197 199
@@ -51,6 +51,6 @@ public class AbstractRuleEngineControllerTest extends AbstractControllerTest { @@ -51,6 +51,6 @@ public class AbstractRuleEngineControllerTest extends AbstractControllerTest {
51 TimePageLink pageLink = new TimePageLink(limit); 51 TimePageLink pageLink = new TimePageLink(limit);
52 return doGetTypedWithTimePageLink("/api/events/{entityType}/{entityId}/{eventType}?tenantId={tenantId}&", 52 return doGetTypedWithTimePageLink("/api/events/{entityType}/{entityId}/{eventType}?tenantId={tenantId}&",
53 new TypeReference<TimePageData<Event>>() { 53 new TypeReference<TimePageData<Event>>() {
54 - }, pageLink, entityId.getEntityType(), entityId.getId(), DataConstants.DEBUG, tenantId.getId()); 54 + }, pageLink, entityId.getEntityType(), entityId.getId(), DataConstants.DEBUG_RULE_NODE, tenantId.getId());
55 } 55 }
56 } 56 }
@@ -37,7 +37,7 @@ public class DataConstants { @@ -37,7 +37,7 @@ public class DataConstants {
37 public static final String ERROR = "ERROR"; 37 public static final String ERROR = "ERROR";
38 public static final String LC_EVENT = "LC_EVENT"; 38 public static final String LC_EVENT = "LC_EVENT";
39 public static final String STATS = "STATS"; 39 public static final String STATS = "STATS";
40 - public static final String DEBUG = "DEBUG"; 40 + public static final String DEBUG_RULE_NODE = "DEBUG_RULE_NODE";
41 41
42 public static final String ONEWAY = "ONEWAY"; 42 public static final String ONEWAY = "ONEWAY";
43 public static final String TWOWAY = "TWOWAY"; 43 public static final String TWOWAY = "TWOWAY";
@@ -54,4 +54,9 @@ public enum MsgType { @@ -54,4 +54,9 @@ public enum MsgType {
54 */ 54 */
55 RULE_TO_SELF_ERROR_MSG, 55 RULE_TO_SELF_ERROR_MSG,
56 56
  57 + /**
  58 + * Message that is sent by RuleActor implementation to RuleActor itself to process the message.
  59 + */
  60 + RULE_TO_SELF_MSG,
  61 +
57 } 62 }
  1 +/**
  2 + * Copyright © 2016-2018 The Thingsboard Authors
  3 + *
  4 + * Licensed under the Apache License, Version 2.0 (the "License");
  5 + * you may not use this file except in compliance with the License.
  6 + * You may obtain a copy of the License at
  7 + *
  8 + * http://www.apache.org/licenses/LICENSE-2.0
  9 + *
  10 + * Unless required by applicable law or agreed to in writing, software
  11 + * distributed under the License is distributed on an "AS IS" BASIS,
  12 + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  13 + * See the License for the specific language governing permissions and
  14 + * limitations under the License.
  15 + */
  16 +package org.thingsboard.server.dao.exception;
  17 +
  18 +public class BufferLimitException extends RuntimeException {
  19 +
  20 + private static final long serialVersionUID = 4513762009041887588L;
  21 +
  22 + public BufferLimitException() {
  23 + super("Rate Limit Buffer is full");
  24 + }
  25 +}
@@ -24,6 +24,7 @@ import com.google.common.util.concurrent.FutureCallback; @@ -24,6 +24,7 @@ import com.google.common.util.concurrent.FutureCallback;
24 import com.google.common.util.concurrent.Futures; 24 import com.google.common.util.concurrent.Futures;
25 import com.google.common.util.concurrent.ListenableFuture; 25 import com.google.common.util.concurrent.ListenableFuture;
26 import com.google.common.util.concurrent.Uninterruptibles; 26 import com.google.common.util.concurrent.Uninterruptibles;
  27 +import org.thingsboard.server.dao.exception.BufferLimitException;
27 import org.thingsboard.server.dao.util.AsyncRateLimiter; 28 import org.thingsboard.server.dao.util.AsyncRateLimiter;
28 29
29 import javax.annotation.Nullable; 30 import javax.annotation.Nullable;
@@ -35,9 +36,15 @@ public class RateLimitedResultSetFuture implements ResultSetFuture { @@ -35,9 +36,15 @@ public class RateLimitedResultSetFuture implements ResultSetFuture {
35 private final ListenableFuture<Void> rateLimitFuture; 36 private final ListenableFuture<Void> rateLimitFuture;
36 37
37 public RateLimitedResultSetFuture(Session session, AsyncRateLimiter rateLimiter, Statement statement) { 38 public RateLimitedResultSetFuture(Session session, AsyncRateLimiter rateLimiter, Statement statement) {
38 - this.rateLimitFuture = rateLimiter.acquireAsync(); 39 + this.rateLimitFuture = Futures.withFallback(rateLimiter.acquireAsync(), t -> {
  40 + if (!(t instanceof BufferLimitException)) {
  41 + rateLimiter.release();
  42 + }
  43 + return Futures.immediateFailedFuture(t);
  44 + });
39 this.originalFuture = Futures.transform(rateLimitFuture, 45 this.originalFuture = Futures.transform(rateLimitFuture,
40 (Function<Void, ResultSetFuture>) i -> executeAsyncWithRelease(rateLimiter, session, statement)); 46 (Function<Void, ResultSetFuture>) i -> executeAsyncWithRelease(rateLimiter, session, statement));
  47 +
41 } 48 }
42 49
43 @Override 50 @Override
@@ -108,10 +115,7 @@ public class RateLimitedResultSetFuture implements ResultSetFuture { @@ -108,10 +115,7 @@ public class RateLimitedResultSetFuture implements ResultSetFuture {
108 try { 115 try {
109 ResultSetFuture resultSetFuture = Uninterruptibles.getUninterruptibly(originalFuture); 116 ResultSetFuture resultSetFuture = Uninterruptibles.getUninterruptibly(originalFuture);
110 resultSetFuture.addListener(listener, executor); 117 resultSetFuture.addListener(listener, executor);
111 - } catch (CancellationException e) {  
112 - cancel(false);  
113 - return;  
114 - } catch (ExecutionException e) { 118 + } catch (CancellationException | ExecutionException e) {
115 Futures.immediateFailedFuture(e).addListener(listener, executor); 119 Futures.immediateFailedFuture(e).addListener(listener, executor);
116 } 120 }
117 }, executor); 121 }, executor);
@@ -23,6 +23,7 @@ import lombok.extern.slf4j.Slf4j; @@ -23,6 +23,7 @@ import lombok.extern.slf4j.Slf4j;
23 import org.springframework.beans.factory.annotation.Value; 23 import org.springframework.beans.factory.annotation.Value;
24 import org.springframework.scheduling.annotation.Scheduled; 24 import org.springframework.scheduling.annotation.Scheduled;
25 import org.springframework.stereotype.Component; 25 import org.springframework.stereotype.Component;
  26 +import org.thingsboard.server.dao.exception.BufferLimitException;
26 27
27 import java.util.concurrent.*; 28 import java.util.concurrent.*;
28 import java.util.concurrent.atomic.AtomicInteger; 29 import java.util.concurrent.atomic.AtomicInteger;
@@ -41,6 +42,9 @@ public class BufferedRateLimiter implements AsyncRateLimiter { @@ -41,6 +42,9 @@ public class BufferedRateLimiter implements AsyncRateLimiter {
41 42
42 private final AtomicInteger maxQueueSize = new AtomicInteger(); 43 private final AtomicInteger maxQueueSize = new AtomicInteger();
43 private final AtomicInteger maxGrantedPermissions = new AtomicInteger(); 44 private final AtomicInteger maxGrantedPermissions = new AtomicInteger();
  45 + private final AtomicInteger totalGranted = new AtomicInteger();
  46 + private final AtomicInteger totalReleased = new AtomicInteger();
  47 + private final AtomicInteger totalRequested = new AtomicInteger();
44 48
45 public BufferedRateLimiter(@Value("${cassandra.query.buffer_size}") int queueLimit, 49 public BufferedRateLimiter(@Value("${cassandra.query.buffer_size}") int queueLimit,
46 @Value("${cassandra.query.concurrent_limit}") int permitsLimit, 50 @Value("${cassandra.query.concurrent_limit}") int permitsLimit,
@@ -53,11 +57,13 @@ public class BufferedRateLimiter implements AsyncRateLimiter { @@ -53,11 +57,13 @@ public class BufferedRateLimiter implements AsyncRateLimiter {
53 57
54 @Override 58 @Override
55 public ListenableFuture<Void> acquireAsync() { 59 public ListenableFuture<Void> acquireAsync() {
  60 + totalRequested.incrementAndGet();
56 if (queue.isEmpty()) { 61 if (queue.isEmpty()) {
57 if (permits.incrementAndGet() <= permitsLimit) { 62 if (permits.incrementAndGet() <= permitsLimit) {
58 if (permits.get() > maxGrantedPermissions.get()) { 63 if (permits.get() > maxGrantedPermissions.get()) {
59 maxGrantedPermissions.set(permits.get()); 64 maxGrantedPermissions.set(permits.get());
60 } 65 }
  66 + totalGranted.incrementAndGet();
61 return Futures.immediateFuture(null); 67 return Futures.immediateFuture(null);
62 } 68 }
63 permits.decrementAndGet(); 69 permits.decrementAndGet();
@@ -69,6 +75,7 @@ public class BufferedRateLimiter implements AsyncRateLimiter { @@ -69,6 +75,7 @@ public class BufferedRateLimiter implements AsyncRateLimiter {
69 @Override 75 @Override
70 public void release() { 76 public void release() {
71 permits.decrementAndGet(); 77 permits.decrementAndGet();
  78 + totalReleased.incrementAndGet();
72 reprocessQueue(); 79 reprocessQueue();
73 } 80 }
74 81
@@ -80,6 +87,7 @@ public class BufferedRateLimiter implements AsyncRateLimiter { @@ -80,6 +87,7 @@ public class BufferedRateLimiter implements AsyncRateLimiter {
80 } 87 }
81 LockedFuture lockedFuture = queue.poll(); 88 LockedFuture lockedFuture = queue.poll();
82 if (lockedFuture != null) { 89 if (lockedFuture != null) {
  90 + totalGranted.incrementAndGet();
83 lockedFuture.latch.countDown(); 91 lockedFuture.latch.countDown();
84 } else { 92 } else {
85 permits.decrementAndGet(); 93 permits.decrementAndGet();
@@ -112,17 +120,20 @@ public class BufferedRateLimiter implements AsyncRateLimiter { @@ -112,17 +120,20 @@ public class BufferedRateLimiter implements AsyncRateLimiter {
112 LockedFuture lockedFuture = createLockedFuture(); 120 LockedFuture lockedFuture = createLockedFuture();
113 if (!queue.offer(lockedFuture, 1, TimeUnit.SECONDS)) { 121 if (!queue.offer(lockedFuture, 1, TimeUnit.SECONDS)) {
114 lockedFuture.cancelFuture(); 122 lockedFuture.cancelFuture();
115 - return Futures.immediateFailedFuture(new IllegalStateException("Rate Limit Buffer is full. Reject")); 123 + return Futures.immediateFailedFuture(new BufferLimitException());
  124 + }
  125 + if(permits.get() < permitsLimit) {
  126 + reprocessQueue();
116 } 127 }
117 if(permits.get() < permitsLimit) { 128 if(permits.get() < permitsLimit) {
118 reprocessQueue(); 129 reprocessQueue();
119 } 130 }
120 return lockedFuture.future; 131 return lockedFuture.future;
121 } catch (InterruptedException e) { 132 } catch (InterruptedException e) {
122 - return Futures.immediateFailedFuture(new IllegalStateException("Rate Limit Task interrupted. Reject")); 133 + return Futures.immediateFailedFuture(new BufferLimitException());
123 } 134 }
124 } 135 }
125 - return Futures.immediateFailedFuture(new IllegalStateException("Rate Limit Buffer is full. Reject")); 136 + return Futures.immediateFailedFuture(new BufferLimitException());
126 } 137 }
127 138
128 @Scheduled(fixedDelayString = "${cassandra.query.rate_limit_print_interval_ms}") 139 @Scheduled(fixedDelayString = "${cassandra.query.rate_limit_print_interval_ms}")
@@ -134,8 +145,11 @@ public class BufferedRateLimiter implements AsyncRateLimiter { @@ -134,8 +145,11 @@ public class BufferedRateLimiter implements AsyncRateLimiter {
134 expiredCount++; 145 expiredCount++;
135 } 146 }
136 } 147 }
137 - log.info("Permits maxBuffer is [{}] max concurrent [{}] expired [{}] current granted [{}]", maxQueueSize.getAndSet(0),  
138 - maxGrantedPermissions.getAndSet(0), expiredCount, permits.get()); 148 + log.info("Permits maxBuffer [{}] maxPermits [{}] expired [{}] currPermits [{}] currBuffer [{}] " +
  149 + "totalPermits [{}] totalRequests [{}] totalReleased [{}]",
  150 + maxQueueSize.getAndSet(0), maxGrantedPermissions.getAndSet(0), expiredCount,
  151 + permits.get(), queue.size(),
  152 + totalGranted.getAndSet(0), totalRequested.getAndSet(0), totalReleased.getAndSet(0));
139 } 153 }
140 154
141 private class LockedFuture { 155 private class LockedFuture {
@@ -19,16 +19,17 @@ import com.datastax.driver.core.*; @@ -19,16 +19,17 @@ import com.datastax.driver.core.*;
19 import com.datastax.driver.core.exceptions.UnsupportedFeatureException; 19 import com.datastax.driver.core.exceptions.UnsupportedFeatureException;
20 import com.google.common.util.concurrent.Futures; 20 import com.google.common.util.concurrent.Futures;
21 import com.google.common.util.concurrent.ListenableFuture; 21 import com.google.common.util.concurrent.ListenableFuture;
  22 +import com.google.common.util.concurrent.MoreExecutors;
22 import org.junit.Test; 23 import org.junit.Test;
23 import org.junit.runner.RunWith; 24 import org.junit.runner.RunWith;
24 import org.mockito.Mock; 25 import org.mockito.Mock;
25 import org.mockito.Mockito; 26 import org.mockito.Mockito;
26 import org.mockito.runners.MockitoJUnitRunner; 27 import org.mockito.runners.MockitoJUnitRunner;
27 import org.mockito.stubbing.Answer; 28 import org.mockito.stubbing.Answer;
  29 +import org.thingsboard.server.dao.exception.BufferLimitException;
28 import org.thingsboard.server.dao.util.AsyncRateLimiter; 30 import org.thingsboard.server.dao.util.AsyncRateLimiter;
29 31
30 -import java.util.concurrent.ExecutionException;  
31 -import java.util.concurrent.TimeoutException; 32 +import java.util.concurrent.*;
32 33
33 import static org.junit.Assert.*; 34 import static org.junit.Assert.*;
34 import static org.mockito.Mockito.*; 35 import static org.mockito.Mockito.*;
@@ -53,7 +54,7 @@ public class RateLimitedResultSetFutureTest { @@ -53,7 +54,7 @@ public class RateLimitedResultSetFutureTest {
53 54
54 @Test 55 @Test
55 public void doNotReleasePermissionIfRateLimitFutureFailed() throws InterruptedException { 56 public void doNotReleasePermissionIfRateLimitFutureFailed() throws InterruptedException {
56 - when(rateLimiter.acquireAsync()).thenReturn(Futures.immediateFailedFuture(new IllegalArgumentException())); 57 + when(rateLimiter.acquireAsync()).thenReturn(Futures.immediateFailedFuture(new BufferLimitException()));
57 resultSetFuture = new RateLimitedResultSetFuture(session, rateLimiter, statement); 58 resultSetFuture = new RateLimitedResultSetFuture(session, rateLimiter, statement);
58 Thread.sleep(1000L); 59 Thread.sleep(1000L);
59 verify(rateLimiter).acquireAsync(); 60 verify(rateLimiter).acquireAsync();
@@ -153,4 +154,29 @@ public class RateLimitedResultSetFutureTest { @@ -153,4 +154,29 @@ public class RateLimitedResultSetFutureTest {
153 verify(rateLimiter, times(1)).release(); 154 verify(rateLimiter, times(1)).release();
154 } 155 }
155 156
  157 + @Test
  158 + public void expiredQueryReturnPermit() throws InterruptedException, ExecutionException {
  159 + CountDownLatch latch = new CountDownLatch(1);
  160 + ListenableFuture<Void> future = MoreExecutors.listeningDecorator(Executors.newFixedThreadPool(1)).submit(() -> {
  161 + latch.await();
  162 + return null;
  163 + });
  164 + when(rateLimiter.acquireAsync()).thenReturn(future);
  165 + resultSetFuture = new RateLimitedResultSetFuture(session, rateLimiter, statement);
  166 +
  167 + ListenableFuture<Row> transform = Futures.transform(resultSetFuture, ResultSet::one);
  168 +// TimeUnit.MILLISECONDS.sleep(200);
  169 + future.cancel(false);
  170 + latch.countDown();
  171 +
  172 + try {
  173 + transform.get();
  174 + fail();
  175 + } catch (Exception e) {
  176 + assertTrue(e instanceof ExecutionException);
  177 + }
  178 + verify(rateLimiter, times(1)).acquireAsync();
  179 + verify(rateLimiter, times(1)).release();
  180 + }
  181 +
156 } 182 }
@@ -17,6 +17,7 @@ package org.thingsboard.server.dao.util; @@ -17,6 +17,7 @@ package org.thingsboard.server.dao.util;
17 17
18 import com.google.common.util.concurrent.*; 18 import com.google.common.util.concurrent.*;
19 import org.junit.Test; 19 import org.junit.Test;
  20 +import org.thingsboard.server.dao.exception.BufferLimitException;
20 21
21 import javax.annotation.Nullable; 22 import javax.annotation.Nullable;
22 import java.util.concurrent.ExecutionException; 23 import java.util.concurrent.ExecutionException;
@@ -61,8 +62,8 @@ public class BufferedRateLimiterTest { @@ -61,8 +62,8 @@ public class BufferedRateLimiterTest {
61 } catch (Exception e) { 62 } catch (Exception e) {
62 assertTrue(e instanceof ExecutionException); 63 assertTrue(e instanceof ExecutionException);
63 Throwable actualCause = e.getCause(); 64 Throwable actualCause = e.getCause();
64 - assertTrue(actualCause instanceof IllegalStateException);  
65 - assertEquals("Rate Limit Buffer is full. Reject", actualCause.getMessage()); 65 + assertTrue(actualCause instanceof BufferLimitException);
  66 + assertEquals("Rate Limit Buffer is full", actualCause.getMessage());
66 } 67 }
67 } 68 }
68 69
@@ -284,6 +284,7 @@ @@ -284,6 +284,7 @@
284 <exclude>src/sh/**</exclude> 284 <exclude>src/sh/**</exclude>
285 <exclude>src/main/scripts/control/**</exclude> 285 <exclude>src/main/scripts/control/**</exclude>
286 <exclude>src/main/scripts/windows/**</exclude> 286 <exclude>src/main/scripts/windows/**</exclude>
  287 + <exclude>src/main/resources/public/static/rulenode/**</exclude>
287 </excludes> 288 </excludes>
288 <mapping> 289 <mapping>
289 <proto>JAVADOC_STYLE</proto> 290 <proto>JAVADOC_STYLE</proto>
@@ -15,8 +15,8 @@ @@ -15,8 +15,8 @@
15 */ 15 */
16 package org.thingsboard.rule.engine.api; 16 package org.thingsboard.rule.engine.api;
17 17
18 -public interface NodeConfiguration { 18 +public interface NodeConfiguration<T extends NodeConfiguration> {
19 19
20 - NodeConfiguration defaultConfiguration(); 20 + T defaultConfiguration();
21 21
22 } 22 }
@@ -29,5 +29,7 @@ public class NodeDefinition { @@ -29,5 +29,7 @@ public class NodeDefinition {
29 String[] relationTypes; 29 String[] relationTypes;
30 boolean customRelations; 30 boolean customRelations;
31 JsonNode defaultConfiguration; 31 JsonNode defaultConfiguration;
  32 + String[] uiResources;
  33 + String configDirective;
32 34
33 } 35 }
@@ -45,6 +45,10 @@ public @interface RuleNode { @@ -45,6 +45,10 @@ public @interface RuleNode {
45 45
46 String[] relationTypes() default {"Success", "Failure"}; 46 String[] relationTypes() default {"Success", "Failure"};
47 47
  48 + String[] uiResources() default {};
  49 +
  50 + String configDirective() default "";
  51 +
48 boolean customRelations() default false; 52 boolean customRelations() default false;
49 53
50 } 54 }
@@ -15,6 +15,7 @@ @@ -15,6 +15,7 @@
15 */ 15 */
16 package org.thingsboard.rule.engine.api; 16 package org.thingsboard.rule.engine.api;
17 17
  18 +import org.thingsboard.server.common.data.id.RuleNodeId;
18 import org.thingsboard.server.common.msg.TbMsg; 19 import org.thingsboard.server.common.msg.TbMsg;
19 import org.thingsboard.server.common.msg.cluster.ServerAddress; 20 import org.thingsboard.server.common.msg.cluster.ServerAddress;
20 import org.thingsboard.server.dao.alarm.AlarmService; 21 import org.thingsboard.server.dao.alarm.AlarmService;
@@ -55,6 +56,8 @@ public interface TbContext { @@ -55,6 +56,8 @@ public interface TbContext {
55 56
56 void tellError(TbMsg msg, Throwable th); 57 void tellError(TbMsg msg, Throwable th);
57 58
  59 + RuleNodeId getSelfId();
  60 +
58 AttributesService getAttributesService(); 61 AttributesService getAttributesService();
59 62
60 CustomerService getCustomerService(); 63 CustomerService getCustomerService();
@@ -24,7 +24,7 @@ import java.util.concurrent.ExecutionException; @@ -24,7 +24,7 @@ import java.util.concurrent.ExecutionException;
24 */ 24 */
25 public interface TbNode { 25 public interface TbNode {
26 26
27 - void init(TbNodeConfiguration configuration, TbNodeState state) throws TbNodeException; 27 + void init(TbContext ctx, TbNodeConfiguration configuration) throws TbNodeException;
28 28
29 void onMsg(TbContext ctx, TbMsg msg) throws ExecutionException, InterruptedException, TbNodeException; 29 void onMsg(TbContext ctx, TbMsg msg) throws ExecutionException, InterruptedException, TbNodeException;
30 30
  1 +/**
  2 + * Copyright © 2016-2018 The Thingsboard Authors
  3 + *
  4 + * Licensed under the Apache License, Version 2.0 (the "License");
  5 + * you may not use this file except in compliance with the License.
  6 + * You may obtain a copy of the License at
  7 + *
  8 + * http://www.apache.org/licenses/LICENSE-2.0
  9 + *
  10 + * Unless required by applicable law or agreed to in writing, software
  11 + * distributed under the License is distributed on an "AS IS" BASIS,
  12 + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  13 + * See the License for the specific language governing permissions and
  14 + * limitations under the License.
  15 + */
  16 +package org.thingsboard.rule.engine.debug;
  17 +
  18 +import com.datastax.driver.core.utils.UUIDs;
  19 +import lombok.extern.slf4j.Slf4j;
  20 +import org.thingsboard.rule.engine.TbNodeUtils;
  21 +import org.thingsboard.rule.engine.api.ListeningExecutor;
  22 +import org.thingsboard.rule.engine.api.RuleNode;
  23 +import org.thingsboard.rule.engine.api.TbContext;
  24 +import org.thingsboard.rule.engine.api.TbNode;
  25 +import org.thingsboard.rule.engine.api.TbNodeConfiguration;
  26 +import org.thingsboard.rule.engine.api.TbNodeException;
  27 +import org.thingsboard.rule.engine.filter.TbJsFilterNodeConfiguration;
  28 +import org.thingsboard.rule.engine.js.NashornJsEngine;
  29 +import org.thingsboard.server.common.data.plugin.ComponentType;
  30 +import org.thingsboard.server.common.msg.TbMsg;
  31 +import org.thingsboard.server.common.msg.TbMsgMetaData;
  32 +
  33 +import javax.script.Bindings;
  34 +
  35 +import java.nio.charset.StandardCharsets;
  36 +import java.util.concurrent.TimeUnit;
  37 +
  38 +import static org.thingsboard.rule.engine.DonAsynchron.withCallback;
  39 +
  40 +@Slf4j
  41 +@RuleNode(
  42 + type = ComponentType.ACTION,
  43 + name = "generator",
  44 + configClazz = TbMsgGeneratorNodeConfiguration.class,
  45 + nodeDescription = "Periodically generates messages",
  46 + nodeDetails = "Generates messages with configurable period. ",
  47 + inEnabled = false
  48 +)
  49 +
  50 +public class TbMsgGeneratorNode implements TbNode {
  51 +
  52 + public static final String TB_MSG_GENERATOR_NODE_MSG = "TbMsgGeneratorNodeMsg";
  53 +
  54 + private TbMsgGeneratorNodeConfiguration config;
  55 + private long delay;
  56 +
  57 + @Override
  58 + public void init(TbContext ctx, TbNodeConfiguration configuration) throws TbNodeException {
  59 + this.config = TbNodeUtils.convert(configuration, TbMsgGeneratorNodeConfiguration.class);
  60 + this.delay = TimeUnit.SECONDS.toMillis(config.getPeriodInSeconds());
  61 + ctx.tellSelf(newTickMsg(ctx), delay);
  62 + }
  63 +
  64 + @Override
  65 + public void onMsg(TbContext ctx, TbMsg msg) {
  66 + if (msg.getType().equals(TB_MSG_GENERATOR_NODE_MSG)) {
  67 + TbMsgMetaData metaData = new TbMsgMetaData();
  68 + if (config.getMsgMetaData() != null) {
  69 + config.getMsgMetaData().forEach(metaData::putValue);
  70 + }
  71 + ctx.tellNext(new TbMsg(UUIDs.timeBased(), config.getMsgType(), ctx.getSelfId(), metaData, config.getMsgBody().getBytes(StandardCharsets.UTF_8)));
  72 + ctx.tellSelf(newTickMsg(ctx), delay);
  73 + }
  74 + }
  75 +
  76 + private TbMsg newTickMsg(TbContext ctx) {
  77 + return new TbMsg(UUIDs.timeBased(), TB_MSG_GENERATOR_NODE_MSG, ctx.getSelfId(), new TbMsgMetaData(), new byte[]{});
  78 + }
  79 +
  80 + @Override
  81 + public void destroy() {
  82 + }
  83 +}
  1 +/**
  2 + * Copyright © 2016-2018 The Thingsboard Authors
  3 + *
  4 + * Licensed under the Apache License, Version 2.0 (the "License");
  5 + * you may not use this file except in compliance with the License.
  6 + * You may obtain a copy of the License at
  7 + *
  8 + * http://www.apache.org/licenses/LICENSE-2.0
  9 + *
  10 + * Unless required by applicable law or agreed to in writing, software
  11 + * distributed under the License is distributed on an "AS IS" BASIS,
  12 + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  13 + * See the License for the specific language governing permissions and
  14 + * limitations under the License.
  15 + */
  16 +package org.thingsboard.rule.engine.debug;
  17 +
  18 +import lombok.Data;
  19 +import org.thingsboard.rule.engine.api.NodeConfiguration;
  20 +import java.util.Map;
  21 +
  22 +@Data
  23 +public class TbMsgGeneratorNodeConfiguration implements NodeConfiguration<TbMsgGeneratorNodeConfiguration> {
  24 +
  25 + private int msgCount;
  26 + private int periodInSeconds;
  27 + private String msgType;
  28 + private String msgBody;
  29 + private Map<String, String> msgMetaData;
  30 +
  31 + @Override
  32 + public TbMsgGeneratorNodeConfiguration defaultConfiguration() {
  33 + TbMsgGeneratorNodeConfiguration configuration = new TbMsgGeneratorNodeConfiguration();
  34 + configuration.setMsgCount(0);
  35 + configuration.setPeriodInSeconds(1);
  36 + configuration.setMsgType("DebugMsg");
  37 + configuration.setMsgBody("{}");
  38 + return configuration;
  39 + }
  40 +}
@@ -29,22 +29,25 @@ import static org.thingsboard.rule.engine.DonAsynchron.withCallback; @@ -29,22 +29,25 @@ import static org.thingsboard.rule.engine.DonAsynchron.withCallback;
29 @Slf4j 29 @Slf4j
30 @RuleNode( 30 @RuleNode(
31 type = ComponentType.FILTER, 31 type = ComponentType.FILTER,
32 - name = "script", relationTypes = {"True", "False", "Failure"}, 32 + name = "script", relationTypes = {"True", "False"},
33 configClazz = TbJsFilterNodeConfiguration.class, 33 configClazz = TbJsFilterNodeConfiguration.class,
34 nodeDescription = "Filter incoming messages using JS script", 34 nodeDescription = "Filter incoming messages using JS script",
35 nodeDetails = "Evaluate incoming Message with configured JS condition. " + 35 nodeDetails = "Evaluate incoming Message with configured JS condition. " +
36 "If <b>True</b> - send Message via <b>True</b> chain, otherwise <b>False</b> chain is used." + 36 "If <b>True</b> - send Message via <b>True</b> chain, otherwise <b>False</b> chain is used." +
37 "Message payload can be accessed via <code>msg</code> property. For example <code>msg.temperature < 10;</code>" + 37 "Message payload can be accessed via <code>msg</code> property. For example <code>msg.temperature < 10;</code>" +
38 - "Message metadata can be accessed via <code>meta</code> property. For example <code>meta.customerName === 'John';</code>") 38 + "Message metadata can be accessed via <code>metadata</code> property. For example <code>metadata.customerName === 'John';</code>",
  39 + uiResources = {"static/rulenode/rulenode-core-config.js"},
  40 + configDirective = "tbFilterNodeScriptConfig")
  41 +
39 public class TbJsFilterNode implements TbNode { 42 public class TbJsFilterNode implements TbNode {
40 43
41 private TbJsFilterNodeConfiguration config; 44 private TbJsFilterNodeConfiguration config;
42 private NashornJsEngine jsEngine; 45 private NashornJsEngine jsEngine;
43 46
44 @Override 47 @Override
45 - public void init(TbNodeConfiguration configuration, TbNodeState state) throws TbNodeException { 48 + public void init(TbContext ctx, TbNodeConfiguration configuration) throws TbNodeException {
46 this.config = TbNodeUtils.convert(configuration, TbJsFilterNodeConfiguration.class); 49 this.config = TbNodeUtils.convert(configuration, TbJsFilterNodeConfiguration.class);
47 - this.jsEngine = new NashornJsEngine(config.getJsScript()); 50 + this.jsEngine = new NashornJsEngine(config.getJsScript(), "Filter");
48 } 51 }
49 52
50 @Override 53 @Override
@@ -19,14 +19,14 @@ import lombok.Data; @@ -19,14 +19,14 @@ import lombok.Data;
19 import org.thingsboard.rule.engine.api.NodeConfiguration; 19 import org.thingsboard.rule.engine.api.NodeConfiguration;
20 20
21 @Data 21 @Data
22 -public class TbJsFilterNodeConfiguration implements NodeConfiguration { 22 +public class TbJsFilterNodeConfiguration implements NodeConfiguration<TbJsFilterNodeConfiguration> {
23 23
24 private String jsScript; 24 private String jsScript;
25 25
26 @Override 26 @Override
27 public TbJsFilterNodeConfiguration defaultConfiguration() { 27 public TbJsFilterNodeConfiguration defaultConfiguration() {
28 TbJsFilterNodeConfiguration configuration = new TbJsFilterNodeConfiguration(); 28 TbJsFilterNodeConfiguration configuration = new TbJsFilterNodeConfiguration();
29 - configuration.setJsScript("msg.passed < 15 && msg.name === 'Vit' && meta.temp == 10 && msg.bigObj.prop == 42;"); 29 + configuration.setJsScript("return msg.passed < 15 && msg.name === 'Vit' && metadata.temp == 10 && msg.bigObj.prop == 42;");
30 return configuration; 30 return configuration;
31 } 31 }
32 } 32 }
@@ -36,31 +36,22 @@ import static org.thingsboard.rule.engine.DonAsynchron.withCallback; @@ -36,31 +36,22 @@ import static org.thingsboard.rule.engine.DonAsynchron.withCallback;
36 nodeDetails = "Node executes configured JS script. Script should return array of next Chain names where Message should be routed. " + 36 nodeDetails = "Node executes configured JS script. Script should return array of next Chain names where Message should be routed. " +
37 "If Array is empty - message not routed to next Node. " + 37 "If Array is empty - message not routed to next Node. " +
38 "Message payload can be accessed via <code>msg</code> property. For example <code>msg.temperature < 10;</code> " + 38 "Message payload can be accessed via <code>msg</code> property. For example <code>msg.temperature < 10;</code> " +
39 - "Message metadata can be accessed via <code>meta</code> property. For example <code>meta.customerName === 'John';</code>") 39 + "Message metadata can be accessed via <code>metadata</code> property. For example <code>metadata.customerName === 'John';</code>",
  40 + uiResources = {"static/rulenode/rulenode-core-config.js"},
  41 + configDirective = "tbFilterNodeSwitchConfig")
40 public class TbJsSwitchNode implements TbNode { 42 public class TbJsSwitchNode implements TbNode {
41 43
42 private TbJsSwitchNodeConfiguration config; 44 private TbJsSwitchNodeConfiguration config;
43 private NashornJsEngine jsEngine; 45 private NashornJsEngine jsEngine;
44 46
45 @Override 47 @Override
46 - public void init(TbNodeConfiguration configuration, TbNodeState state) throws TbNodeException { 48 + public void init(TbContext ctx, TbNodeConfiguration configuration) throws TbNodeException {
47 this.config = TbNodeUtils.convert(configuration, TbJsSwitchNodeConfiguration.class); 49 this.config = TbNodeUtils.convert(configuration, TbJsSwitchNodeConfiguration.class);
48 - if (config.getAllowedRelations().size() < 1) {  
49 - String message = "Switch node should have at least 1 relation";  
50 - log.error(message);  
51 - throw new IllegalStateException(message);  
52 - }  
53 - if (!config.isRouteToAllWithNoCheck()) {  
54 - this.jsEngine = new NashornJsEngine(config.getJsScript());  
55 - } 50 + this.jsEngine = new NashornJsEngine(config.getJsScript(), "Switch");
56 } 51 }
57 52
58 @Override 53 @Override
59 public void onMsg(TbContext ctx, TbMsg msg) { 54 public void onMsg(TbContext ctx, TbMsg msg) {
60 - if (config.isRouteToAllWithNoCheck()) {  
61 - ctx.tellNext(msg, config.getAllowedRelations());  
62 - return;  
63 - }  
64 ListeningExecutor jsExecutor = ctx.getJsExecutor(); 55 ListeningExecutor jsExecutor = ctx.getJsExecutor();
65 withCallback(jsExecutor.executeAsync(() -> jsEngine.executeSwitch(toBindings(msg))), 56 withCallback(jsExecutor.executeAsync(() -> jsEngine.executeSwitch(toBindings(msg))),
66 result -> processSwitch(ctx, msg, result), 57 result -> processSwitch(ctx, msg, result),
@@ -68,15 +59,7 @@ public class TbJsSwitchNode implements TbNode { @@ -68,15 +59,7 @@ public class TbJsSwitchNode implements TbNode {
68 } 59 }
69 60
70 private void processSwitch(TbContext ctx, TbMsg msg, Set<String> nextRelations) { 61 private void processSwitch(TbContext ctx, TbMsg msg, Set<String> nextRelations) {
71 - if (validateRelations(nextRelations)) {  
72 - ctx.tellNext(msg, nextRelations);  
73 - } else {  
74 - ctx.tellError(msg, new IllegalStateException("Unsupported relation for switch " + nextRelations));  
75 - }  
76 - }  
77 -  
78 - private boolean validateRelations(Set<String> nextRelations) {  
79 - return config.getAllowedRelations().containsAll(nextRelations); 62 + ctx.tellNext(msg, nextRelations);
80 } 63 }
81 64
82 private Bindings toBindings(TbMsg msg) { 65 private Bindings toBindings(TbMsg msg) {
@@ -22,22 +22,18 @@ import org.thingsboard.rule.engine.api.NodeConfiguration; @@ -22,22 +22,18 @@ import org.thingsboard.rule.engine.api.NodeConfiguration;
22 import java.util.Set; 22 import java.util.Set;
23 23
24 @Data 24 @Data
25 -public class TbJsSwitchNodeConfiguration implements NodeConfiguration { 25 +public class TbJsSwitchNodeConfiguration implements NodeConfiguration<TbJsSwitchNodeConfiguration> {
26 26
27 private String jsScript; 27 private String jsScript;
28 - private Set<String> allowedRelations;  
29 - private boolean routeToAllWithNoCheck;  
30 28
31 @Override 29 @Override
32 public TbJsSwitchNodeConfiguration defaultConfiguration() { 30 public TbJsSwitchNodeConfiguration defaultConfiguration() {
33 TbJsSwitchNodeConfiguration configuration = new TbJsSwitchNodeConfiguration(); 31 TbJsSwitchNodeConfiguration configuration = new TbJsSwitchNodeConfiguration();
34 - configuration.setJsScript("function nextRelation(meta, msg) {\n" + 32 + configuration.setJsScript("function nextRelation(metadata, msg) {\n" +
35 " return ['one','nine'];" + 33 " return ['one','nine'];" +
36 "};\n" + 34 "};\n" +
37 "\n" + 35 "\n" +
38 - "nextRelation(meta, msg);");  
39 - configuration.setAllowedRelations(Sets.newHashSet("one", "two"));  
40 - configuration.setRouteToAllWithNoCheck(false); 36 + "return nextRelation(metadata, msg);");
41 return configuration; 37 return configuration;
42 } 38 }
43 } 39 }
@@ -29,15 +29,18 @@ import org.thingsboard.server.common.msg.TbMsg; @@ -29,15 +29,18 @@ import org.thingsboard.server.common.msg.TbMsg;
29 type = ComponentType.FILTER, 29 type = ComponentType.FILTER,
30 name = "message type", 30 name = "message type",
31 configClazz = TbMsgTypeFilterNodeConfiguration.class, 31 configClazz = TbMsgTypeFilterNodeConfiguration.class,
  32 + relationTypes = {"True", "False"},
32 nodeDescription = "Filter incoming messages by Message Type", 33 nodeDescription = "Filter incoming messages by Message Type",
33 nodeDetails = "Evaluate incoming Message with configured JS condition. " + 34 nodeDetails = "Evaluate incoming Message with configured JS condition. " +
34 - "If incoming MessageType is expected - send Message via <b>Success</b> chain, otherwise <b>Failure</b> chain is used.") 35 + "If incoming MessageType is expected - send Message via <b>Success</b> chain, otherwise <b>Failure</b> chain is used.",
  36 + uiResources = {"static/rulenode/rulenode-core-config.js", "static/rulenode/rulenode-core-config.css"},
  37 + configDirective = "tbFilterNodeMessageTypeConfig")
35 public class TbMsgTypeFilterNode implements TbNode { 38 public class TbMsgTypeFilterNode implements TbNode {
36 39
37 TbMsgTypeFilterNodeConfiguration config; 40 TbMsgTypeFilterNodeConfiguration config;
38 41
39 @Override 42 @Override
40 - public void init(TbNodeConfiguration configuration, TbNodeState state) throws TbNodeException { 43 + public void init(TbContext ctx, TbNodeConfiguration configuration) throws TbNodeException {
41 this.config = TbNodeUtils.convert(configuration, TbMsgTypeFilterNodeConfiguration.class); 44 this.config = TbNodeUtils.convert(configuration, TbMsgTypeFilterNodeConfiguration.class);
42 } 45 }
43 46
@@ -26,14 +26,14 @@ import java.util.List; @@ -26,14 +26,14 @@ import java.util.List;
26 * Created by ashvayka on 19.01.18. 26 * Created by ashvayka on 19.01.18.
27 */ 27 */
28 @Data 28 @Data
29 -public class TbMsgTypeFilterNodeConfiguration implements NodeConfiguration { 29 +public class TbMsgTypeFilterNodeConfiguration implements NodeConfiguration<TbMsgTypeFilterNodeConfiguration> {
30 30
31 private List<String> messageTypes; 31 private List<String> messageTypes;
32 32
33 @Override 33 @Override
34 public TbMsgTypeFilterNodeConfiguration defaultConfiguration() { 34 public TbMsgTypeFilterNodeConfiguration defaultConfiguration() {
35 TbMsgTypeFilterNodeConfiguration configuration = new TbMsgTypeFilterNodeConfiguration(); 35 TbMsgTypeFilterNodeConfiguration configuration = new TbMsgTypeFilterNodeConfiguration();
36 - configuration.setMessageTypes(Arrays.asList("GET_ATTRIBUTES","POST_ATTRIBUTES","POST_TELEMETRY","RPC_REQUEST")); 36 + configuration.setMessageTypes(Arrays.asList("POST_ATTRIBUTES","POST_TELEMETRY","RPC_REQUEST"));
37 return configuration; 37 return configuration;
38 } 38 }
39 } 39 }
@@ -34,14 +34,20 @@ import java.util.Set; @@ -34,14 +34,20 @@ import java.util.Set;
34 @Slf4j 34 @Slf4j
35 public class NashornJsEngine { 35 public class NashornJsEngine {
36 36
37 - public static final String METADATA = "meta"; 37 + public static final String METADATA = "metadata";
38 public static final String DATA = "msg"; 38 public static final String DATA = "msg";
  39 +
  40 + private static final String JS_WRAPPER_PREFIX_TEMPLATE = "function %s(msg, metadata) { ";
  41 + private static final String JS_WRAPPER_SUFFIX_TEMPLATE = "}\n %s(msg, metadata);";
  42 +
39 private static NashornScriptEngineFactory factory = new NashornScriptEngineFactory(); 43 private static NashornScriptEngineFactory factory = new NashornScriptEngineFactory();
40 44
41 private CompiledScript engine; 45 private CompiledScript engine;
42 46
43 - public NashornJsEngine(String script) {  
44 - engine = compileScript(script); 47 + public NashornJsEngine(String script, String functionName) {
  48 + String jsWrapperPrefix = String.format(JS_WRAPPER_PREFIX_TEMPLATE, functionName);
  49 + String jsWrapperSuffix = String.format(JS_WRAPPER_SUFFIX_TEMPLATE, functionName);
  50 + engine = compileScript(jsWrapperPrefix + script + jsWrapperSuffix);
45 } 51 }
46 52
47 private static CompiledScript compileScript(String script) { 53 private static CompiledScript compileScript(String script) {
@@ -58,15 +64,15 @@ public class NashornJsEngine { @@ -58,15 +64,15 @@ public class NashornJsEngine {
58 public static Bindings bindMsg(TbMsg msg) { 64 public static Bindings bindMsg(TbMsg msg) {
59 try { 65 try {
60 Bindings bindings = new SimpleBindings(); 66 Bindings bindings = new SimpleBindings();
61 - bindings.put(METADATA, msg.getMetaData().getData());  
62 -  
63 if (ArrayUtils.isNotEmpty(msg.getData())) { 67 if (ArrayUtils.isNotEmpty(msg.getData())) {
64 ObjectMapper mapper = new ObjectMapper(); 68 ObjectMapper mapper = new ObjectMapper();
65 JsonNode jsonNode = mapper.readTree(msg.getData()); 69 JsonNode jsonNode = mapper.readTree(msg.getData());
66 Map map = mapper.treeToValue(jsonNode, Map.class); 70 Map map = mapper.treeToValue(jsonNode, Map.class);
67 bindings.put(DATA, map); 71 bindings.put(DATA, map);
  72 + } else {
  73 + bindings.put(DATA, Collections.emptyMap());
68 } 74 }
69 - 75 + bindings.put(METADATA, msg.getMetaData().getData());
70 return bindings; 76 return bindings;
71 } catch (Throwable th) { 77 } catch (Throwable th) {
72 throw new IllegalArgumentException("Cannot bind js args", th); 78 throw new IllegalArgumentException("Cannot bind js args", th);
@@ -37,7 +37,7 @@ public abstract class TbEntityGetAttrNode<T extends EntityId> implements TbNode @@ -37,7 +37,7 @@ public abstract class TbEntityGetAttrNode<T extends EntityId> implements TbNode
37 private TbGetEntityAttrNodeConfiguration config; 37 private TbGetEntityAttrNodeConfiguration config;
38 38
39 @Override 39 @Override
40 - public void init(TbNodeConfiguration configuration, TbNodeState state) throws TbNodeException { 40 + public void init(TbContext ctx, TbNodeConfiguration configuration) throws TbNodeException {
41 this.config = TbNodeUtils.convert(configuration, TbGetEntityAttrNodeConfiguration.class); 41 this.config = TbNodeUtils.convert(configuration, TbGetEntityAttrNodeConfiguration.class);
42 } 42 }
43 43
@@ -42,14 +42,14 @@ import static org.thingsboard.server.common.data.DataConstants.*; @@ -42,14 +42,14 @@ import static org.thingsboard.server.common.data.DataConstants.*;
42 nodeDescription = "Add Message Originator Attributes or Latest Telemetry into Message Metadata", 42 nodeDescription = "Add Message Originator Attributes or Latest Telemetry into Message Metadata",
43 nodeDetails = "If Attributes enrichment configured, <b>CLIENT/SHARED/SERVER</b> attributes are added into Message metadata " + 43 nodeDetails = "If Attributes enrichment configured, <b>CLIENT/SHARED/SERVER</b> attributes are added into Message metadata " +
44 "with specific prefix: <i>cs/shared/ss</i>. To access those attributes in other nodes this template can be used " + 44 "with specific prefix: <i>cs/shared/ss</i>. To access those attributes in other nodes this template can be used " +
45 - "<code>meta.cs.temperature</code> or <code>meta.shared.limit</code> " + 45 + "<code>metadata.cs.temperature</code> or <code>metadata.shared.limit</code> " +
46 "If Latest Telemetry enrichment configured, latest telemetry added into metadata without prefix.") 46 "If Latest Telemetry enrichment configured, latest telemetry added into metadata without prefix.")
47 public class TbGetAttributesNode implements TbNode { 47 public class TbGetAttributesNode implements TbNode {
48 48
49 private TbGetAttributesNodeConfiguration config; 49 private TbGetAttributesNodeConfiguration config;
50 50
51 @Override 51 @Override
52 - public void init(TbNodeConfiguration configuration, TbNodeState state) throws TbNodeException { 52 + public void init(TbContext ctx, TbNodeConfiguration configuration) throws TbNodeException {
53 this.config = TbNodeUtils.convert(configuration, TbGetAttributesNodeConfiguration.class); 53 this.config = TbNodeUtils.convert(configuration, TbGetAttributesNodeConfiguration.class);
54 } 54 }
55 55
@@ -25,7 +25,7 @@ import java.util.List; @@ -25,7 +25,7 @@ import java.util.List;
25 * Created by ashvayka on 19.01.18. 25 * Created by ashvayka on 19.01.18.
26 */ 26 */
27 @Data 27 @Data
28 -public class TbGetAttributesNodeConfiguration implements NodeConfiguration { 28 +public class TbGetAttributesNodeConfiguration implements NodeConfiguration<TbGetAttributesNodeConfiguration> {
29 29
30 private List<String> clientAttributeNames; 30 private List<String> clientAttributeNames;
31 private List<String> sharedAttributeNames; 31 private List<String> sharedAttributeNames;
@@ -30,7 +30,7 @@ import org.thingsboard.server.common.data.plugin.ComponentType; @@ -30,7 +30,7 @@ import org.thingsboard.server.common.data.plugin.ComponentType;
30 nodeDescription = "Add Originators Customer Attributes or Latest Telemetry into Message Metadata", 30 nodeDescription = "Add Originators Customer Attributes or Latest Telemetry into Message Metadata",
31 nodeDetails = "If Attributes enrichment configured, server scope attributes are added into Message metadata. " + 31 nodeDetails = "If Attributes enrichment configured, server scope attributes are added into Message metadata. " +
32 "To access those attributes in other nodes this template can be used " + 32 "To access those attributes in other nodes this template can be used " +
33 - "<code>meta.temperature</code>. If Latest Telemetry enrichment configured, latest telemetry added into metadata") 33 + "<code>metadata.temperature</code>. If Latest Telemetry enrichment configured, latest telemetry added into metadata")
34 public class TbGetCustomerAttributeNode extends TbEntityGetAttrNode<CustomerId> { 34 public class TbGetCustomerAttributeNode extends TbEntityGetAttrNode<CustomerId> {
35 35
36 @Override 36 @Override
@@ -23,7 +23,7 @@ import java.util.Map; @@ -23,7 +23,7 @@ import java.util.Map;
23 import java.util.Optional; 23 import java.util.Optional;
24 24
25 @Data 25 @Data
26 -public class TbGetEntityAttrNodeConfiguration implements NodeConfiguration { 26 +public class TbGetEntityAttrNodeConfiguration implements NodeConfiguration<TbGetEntityAttrNodeConfiguration> {
27 27
28 private Map<String, String> attrMapping; 28 private Map<String, String> attrMapping;
29 private boolean isTelemetry = false; 29 private boolean isTelemetry = false;
@@ -32,13 +32,13 @@ import org.thingsboard.server.common.data.plugin.ComponentType; @@ -32,13 +32,13 @@ import org.thingsboard.server.common.data.plugin.ComponentType;
32 "If multiple Related Entities are found, only first Entity is used for attributes enrichment, other entities are discarded. " + 32 "If multiple Related Entities are found, only first Entity is used for attributes enrichment, other entities are discarded. " +
33 "If Attributes enrichment configured, server scope attributes are added into Message metadata. " + 33 "If Attributes enrichment configured, server scope attributes are added into Message metadata. " +
34 "To access those attributes in other nodes this template can be used " + 34 "To access those attributes in other nodes this template can be used " +
35 - "<code>meta.temperature</code>. If Latest Telemetry enrichment configured, latest telemetry added into metadata") 35 + "<code>metadata.temperature</code>. If Latest Telemetry enrichment configured, latest telemetry added into metadata")
36 public class TbGetRelatedAttributeNode extends TbEntityGetAttrNode<EntityId> { 36 public class TbGetRelatedAttributeNode extends TbEntityGetAttrNode<EntityId> {
37 37
38 private TbGetRelatedAttrNodeConfiguration config; 38 private TbGetRelatedAttrNodeConfiguration config;
39 39
40 @Override 40 @Override
41 - public void init(TbNodeConfiguration configuration, TbNodeState state) throws TbNodeException { 41 + public void init(TbContext context, TbNodeConfiguration configuration) throws TbNodeException {
42 this.config = TbNodeUtils.convert(configuration, TbGetRelatedAttrNodeConfiguration.class); 42 this.config = TbNodeUtils.convert(configuration, TbGetRelatedAttrNodeConfiguration.class);
43 setConfig(config); 43 setConfig(config);
44 } 44 }
@@ -32,7 +32,7 @@ import org.thingsboard.server.common.data.plugin.ComponentType; @@ -32,7 +32,7 @@ import org.thingsboard.server.common.data.plugin.ComponentType;
32 nodeDescription = "Add Originators Tenant Attributes or Latest Telemetry into Message Metadata", 32 nodeDescription = "Add Originators Tenant Attributes or Latest Telemetry into Message Metadata",
33 nodeDetails = "If Attributes enrichment configured, server scope attributes are added into Message metadata. " + 33 nodeDetails = "If Attributes enrichment configured, server scope attributes are added into Message metadata. " +
34 "To access those attributes in other nodes this template can be used " + 34 "To access those attributes in other nodes this template can be used " +
35 - "<code>meta.temperature</code>. If Latest Telemetry enrichment configured, latest telemetry added into metadata") 35 + "<code>metadata.temperature</code>. If Latest Telemetry enrichment configured, latest telemetry added into metadata")
36 public class TbGetTenantAttributeNode extends TbEntityGetAttrNode<TenantId> { 36 public class TbGetTenantAttributeNode extends TbEntityGetAttrNode<TenantId> {
37 37
38 @Override 38 @Override
@@ -32,7 +32,7 @@ public abstract class TbAbstractTransformNode implements TbNode { @@ -32,7 +32,7 @@ public abstract class TbAbstractTransformNode implements TbNode {
32 private TbTransformNodeConfiguration config; 32 private TbTransformNodeConfiguration config;
33 33
34 @Override 34 @Override
35 - public void init(TbNodeConfiguration configuration, TbNodeState state) throws TbNodeException { 35 + public void init(TbContext context, TbNodeConfiguration configuration) throws TbNodeException {
36 this.config = TbNodeUtils.convert(configuration, TbTransformNodeConfiguration.class); 36 this.config = TbNodeUtils.convert(configuration, TbTransformNodeConfiguration.class);
37 } 37 }
38 38
@@ -49,7 +49,7 @@ public class TbChangeOriginatorNode extends TbAbstractTransformNode { @@ -49,7 +49,7 @@ public class TbChangeOriginatorNode extends TbAbstractTransformNode {
49 private TbChangeOriginatorNodeConfiguration config; 49 private TbChangeOriginatorNodeConfiguration config;
50 50
51 @Override 51 @Override
52 - public void init(TbNodeConfiguration configuration, TbNodeState state) throws TbNodeException { 52 + public void init(TbContext ctx, TbNodeConfiguration configuration) throws TbNodeException {
53 this.config = TbNodeUtils.convert(configuration, TbChangeOriginatorNodeConfiguration.class); 53 this.config = TbNodeUtils.convert(configuration, TbChangeOriginatorNodeConfiguration.class);
54 validateConfig(config); 54 validateConfig(config);
55 setConfig(config); 55 setConfig(config);
@@ -30,7 +30,7 @@ import javax.script.Bindings; @@ -30,7 +30,7 @@ import javax.script.Bindings;
30 configClazz = TbTransformMsgNodeConfiguration.class, 30 configClazz = TbTransformMsgNodeConfiguration.class,
31 nodeDescription = "Change Message payload and Metadata using JavaScript", 31 nodeDescription = "Change Message payload and Metadata using JavaScript",
32 nodeDetails = "JavaScript function recieve 2 input parameters that can be changed inside.<br/> " + 32 nodeDetails = "JavaScript function recieve 2 input parameters that can be changed inside.<br/> " +
33 - "<code>meta</code> - is a Message metadata.<br/>" + 33 + "<code>metadata</code> - is a Message metadata.<br/>" +
34 "<code>msg</code> - is a Message payload.<br/>Any properties can be changed/removed/added in those objects.") 34 "<code>msg</code> - is a Message payload.<br/>Any properties can be changed/removed/added in those objects.")
35 public class TbTransformMsgNode extends TbAbstractTransformNode { 35 public class TbTransformMsgNode extends TbAbstractTransformNode {
36 36
@@ -38,9 +38,9 @@ public class TbTransformMsgNode extends TbAbstractTransformNode { @@ -38,9 +38,9 @@ public class TbTransformMsgNode extends TbAbstractTransformNode {
38 private NashornJsEngine jsEngine; 38 private NashornJsEngine jsEngine;
39 39
40 @Override 40 @Override
41 - public void init(TbNodeConfiguration configuration, TbNodeState state) throws TbNodeException { 41 + public void init(TbContext ctx, TbNodeConfiguration configuration) throws TbNodeException {
42 this.config = TbNodeUtils.convert(configuration, TbTransformMsgNodeConfiguration.class); 42 this.config = TbNodeUtils.convert(configuration, TbTransformMsgNodeConfiguration.class);
43 - this.jsEngine = new NashornJsEngine(config.getJsScript()); 43 + this.jsEngine = new NashornJsEngine(config.getJsScript(), "Transform");
44 setConfig(config); 44 setConfig(config);
45 } 45 }
46 46
@@ -27,7 +27,7 @@ public class TbTransformMsgNodeConfiguration extends TbTransformNodeConfiguratio @@ -27,7 +27,7 @@ public class TbTransformMsgNodeConfiguration extends TbTransformNodeConfiguratio
27 public TbTransformMsgNodeConfiguration defaultConfiguration() { 27 public TbTransformMsgNodeConfiguration defaultConfiguration() {
28 TbTransformMsgNodeConfiguration configuration = new TbTransformMsgNodeConfiguration(); 28 TbTransformMsgNodeConfiguration configuration = new TbTransformMsgNodeConfiguration();
29 configuration.setStartNewChain(false); 29 configuration.setStartNewChain(false);
30 - configuration.setJsScript("msg.passed = msg.passed * meta.temp; msg.bigObj.newProp = 'Ukraine' "); 30 + configuration.setJsScript("return msg.passed = msg.passed * metadata.temp; msg.bigObj.newProp = 'Ukraine' ");
31 return configuration; 31 return configuration;
32 } 32 }
33 } 33 }
  1 +.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}
  2 +/*# sourceMappingURL=rulenode-core-config.css.map*/
  1 +!function(e){function t(s){if(a[s])return a[s].exports;var n=a[s]={exports:{},id:s,loaded:!1};return e[s].call(n.exports,n,n.exports,t),n.loaded=!0,n.exports}var a={};return t.m=e,t.c=a,t.p="/static/",t(0)}([function(e,t,a){e.exports=a(8)},function(e,t){},function(e,t){e.exports=' <section layout=column> <label translate class="tb-title no-padding" ng-class="{\'tb-required\': required}">tb.rulenode.message-types-filter</label> <md-chips id=message_type_chips ng-required=required readonly=readonly ng-model=messageTypes md-autocomplete-snap md-transform-chip=transformMessageTypeChip($chip) md-require-match=false> <md-autocomplete id=message_type md-no-cache=true md-selected-item=selectedMessageType md-search-text=messageTypeSearchText md-items="item in messageTypesSearch(messageTypeSearchText)" md-item-text=item.name md-min-length=0 placeholder="{{\'tb.rulenode.message-type\' | translate }}" md-menu-class=tb-message-type-autocomplete> <span md-highlight-text=messageTypeSearchText md-highlight-flags=^i>{{item}}</span> <md-not-found> <div class=tb-not-found> <div class=tb-no-entries ng-if="!messageTypeSearchText || !messageTypeSearchText.length"> <span translate>tb.rulenode.no-message-types-found</span> </div> <div ng-if="messageTypeSearchText && messageTypeSearchText.length"> <span translate translate-values=\'{ messageType: "{{messageTypeSearchText | truncate:true:6:&apos;...&apos;}}" }\'>tb.rulenode.no-message-type-matching</span> <span> <a translate ng-click="createMessageType($event, \'#message_type_chips\')">tb.rulenode.create-new-message-type</a> </span> </div> </div> </md-not-found> </md-autocomplete> <md-chip-template> <span>{{$chip.name}}</span> </md-chip-template> </md-chips> <div class=tb-error-messages ng-messages=ngModelCtrl.$error role=alert> <div translate ng-message=messageTypes class=tb-error-message>tb.rulenode.message-types-required</div> </div> </section>'},function(e,t){e.exports=" <section layout=column> <label translate class=\"tb-title no-padding\">tb.rulenode.filter</label> <tb-js-func ng-model=configuration.jsScript function-name=Filter function-args=\"{{ ['msg', 'metadata'] }}\" no-validate=true> </tb-js-func> </section> "},function(e,t){e.exports=" <section layout=column> <label translate class=\"tb-title no-padding\">tb.rulenode.switch</label> <tb-js-func ng-model=configuration.jsScript function-name=Switch function-args=\"{{ ['msg', 'metadata'] }}\" no-validate=true> </tb-js-func> </section> "},function(e,t,a){"use strict";function s(e){return e&&e.__esModule?e:{default:e}}function n(e,t,a){var s=function(s,n,r,l){function u(){if(l.$viewValue){for(var e=[],t=0;t<s.messageTypes.length;t++)e.push(s.messageTypes[t].value);l.$viewValue.messageTypes=e,o()}}function o(){if(s.required){var e=!(!l.$viewValue.messageTypes||!l.$viewValue.messageTypes.length);l.$setValidity("messageTypes",e)}else l.$setValidity("messageTypes",!0)}var c=i.default;n.html(c),s.selectedMessageType=null,s.messageTypeSearchText=null,s.ngModelCtrl=l;var d=[];for(var p in a.messageType){var m={name:a.messageType[p].name,value:a.messageType[p].value};d.push(m)}s.transformMessageTypeChip=function(e){var a,s=t("filter")(d,{name:e},!0);return a=s&&s.length?angular.copy(s[0]):{name:e,value:e}},s.messageTypesSearch=function(e){var a=e?t("filter")(d,{name:e}):d;return a.map(function(e){return e.name})},s.createMessageType=function(e,t){var a=angular.element(t,n)[0].firstElementChild,s=angular.element(a),r=s.scope().$mdChipsCtrl.getChipBuffer();e.preventDefault(),e.stopPropagation(),s.scope().$mdChipsCtrl.appendChip(r.trim()),s.scope().$mdChipsCtrl.resetChipBuffer()},l.$render=function(){var e=l.$viewValue,t=[];if(e&&e.messageTypes)for(var n=0;n<e.messageTypes.length;n++){var r=e.messageTypes[n];a.messageType[r]?t.push(angular.copy(a.messageType[r])):t.push({name:r,value:r})}s.messageTypes=t,s.$watch("messageTypes",function(e,t){angular.equals(e,t)||u()},!0)},e(n.contents())(s)};return{restrict:"E",require:"^ngModel",scope:{required:"=ngRequired",readonly:"=ngReadonly"},link:s}}n.$inject=["$compile","$filter","ruleNodeTypes"],Object.defineProperty(t,"__esModule",{value:!0}),t.default=n,a(1);var r=a(2),i=s(r)},function(e,t,a){"use strict";function s(e){return e&&e.__esModule?e:{default:e}}function n(e){var t=function(t,a,s,n){var r=i.default;a.html(r),t.$watch("configuration",function(e,a){angular.equals(e,a)||n.$setViewValue(t.configuration)}),n.$render=function(){t.configuration=n.$viewValue},e(a.contents())(t)};return{restrict:"E",require:"^ngModel",scope:{},link:t}}n.$inject=["$compile"],Object.defineProperty(t,"__esModule",{value:!0}),t.default=n;var r=a(3),i=s(r)},function(e,t,a){"use strict";function s(e){return e&&e.__esModule?e:{default:e}}function n(e){var t=function(t,a,s,n){var r=i.default;a.html(r),t.$watch("configuration",function(e,a){angular.equals(e,a)||n.$setViewValue(t.configuration)}),n.$render=function(){t.configuration=n.$viewValue},e(a.contents())(t)};return{restrict:"E",require:"^ngModel",scope:{},link:t}}n.$inject=["$compile"],Object.defineProperty(t,"__esModule",{value:!0}),t.default=n;var r=a(4),i=s(r)},function(e,t,a){"use strict";function s(e){return e&&e.__esModule?e:{default:e}}Object.defineProperty(t,"__esModule",{value:!0});var n=a(11),r=s(n),i=a(6),l=s(i),u=a(5),o=s(u),c=a(7),d=s(c),p=a(10),m=s(p);t.default=angular.module("thingsboard.ruleChain.config",[r.default]).directive("tbFilterNodeScriptConfig",l.default).directive("tbFilterNodeMessageTypeConfig",o.default).directive("tbFilterNodeSwitchConfig",d.default).config(m.default).name},function(e,t){"use strict";function a(e){var t={tb:{rulenode:{filter:"Filter",switch:"Switch","message-type":"Message type","message-types-filter":"Message types filter","no-message-types-found":"No message types found","no-message-type-matching":"'{{messageType}}' not found.","create-new-message-type":"Create a new one!","message-types-required":"Message types are required."}}};angular.merge(e.en_US,t)}Object.defineProperty(t,"__esModule",{value:!0}),t.default=a},function(e,t,a){"use strict";function s(e){return e&&e.__esModule?e:{default:e}}function n(e,t){(0,i.default)(t);for(var a in t){var s=t[a];e.translations(a,s)}}n.$inject=["$translateProvider","locales"],Object.defineProperty(t,"__esModule",{value:!0}),t.default=n;var r=a(9),i=s(r)},function(e,t){"use strict";Object.defineProperty(t,"__esModule",{value:!0}),t.default=angular.module("thingsboard.ruleChain.config.types",[]).constant("ruleNodeTypes",{messageType:{POST_ATTRIBUTES:{name:"Post attributes",value:"POST_ATTRIBUTES"},POST_TELEMETRY:{name:"Post telemetry",value:"POST_TELEMETRY"},RPC_REQUEST:{name:"RPC Request",value:"RPC_REQUEST"}}}).name}]);
  2 +//# sourceMappingURL=rulenode-core-config.js.map
@@ -51,7 +51,7 @@ public class TbJsFilterNodeTest { @@ -51,7 +51,7 @@ public class TbJsFilterNodeTest {
51 51
52 @Test 52 @Test
53 public void falseEvaluationDoNotSendMsg() throws TbNodeException { 53 public void falseEvaluationDoNotSendMsg() throws TbNodeException {
54 - initWithScript("10 > 15;"); 54 + initWithScript("return 10 > 15;");
55 TbMsg msg = new TbMsg(UUIDs.timeBased(), "USER", null, new TbMsgMetaData(), "{}".getBytes()); 55 TbMsg msg = new TbMsg(UUIDs.timeBased(), "USER", null, new TbMsgMetaData(), "{}".getBytes());
56 56
57 mockJsExecutor(); 57 mockJsExecutor();
@@ -64,7 +64,7 @@ public class TbJsFilterNodeTest { @@ -64,7 +64,7 @@ public class TbJsFilterNodeTest {
64 64
65 @Test 65 @Test
66 public void notValidMsgDataThrowsException() throws TbNodeException { 66 public void notValidMsgDataThrowsException() throws TbNodeException {
67 - initWithScript("10 > 15;"); 67 + initWithScript("return 10 > 15;");
68 TbMsg msg = new TbMsg(UUIDs.timeBased(), "USER", null, new TbMsgMetaData(), new byte[4]); 68 TbMsg msg = new TbMsg(UUIDs.timeBased(), "USER", null, new TbMsgMetaData(), new byte[4]);
69 69
70 when(ctx.getJsExecutor()).thenReturn(executor); 70 when(ctx.getJsExecutor()).thenReturn(executor);
@@ -77,7 +77,7 @@ public class TbJsFilterNodeTest { @@ -77,7 +77,7 @@ public class TbJsFilterNodeTest {
77 77
78 @Test 78 @Test
79 public void exceptionInJsThrowsException() throws TbNodeException { 79 public void exceptionInJsThrowsException() throws TbNodeException {
80 - initWithScript("meta.temp.curr < 15;"); 80 + initWithScript("return metadata.temp.curr < 15;");
81 TbMsgMetaData metaData = new TbMsgMetaData(); 81 TbMsgMetaData metaData = new TbMsgMetaData();
82 TbMsg msg = new TbMsg(UUIDs.timeBased(), "USER", null, metaData, "{}".getBytes()); 82 TbMsg msg = new TbMsg(UUIDs.timeBased(), "USER", null, metaData, "{}".getBytes());
83 mockJsExecutor(); 83 mockJsExecutor();
@@ -89,12 +89,12 @@ public class TbJsFilterNodeTest { @@ -89,12 +89,12 @@ public class TbJsFilterNodeTest {
89 89
90 @Test(expected = IllegalArgumentException.class) 90 @Test(expected = IllegalArgumentException.class)
91 public void notValidScriptThrowsException() throws TbNodeException { 91 public void notValidScriptThrowsException() throws TbNodeException {
92 - initWithScript("10 > 15 asdq out"); 92 + initWithScript("return 10 > 15 asdq out");
93 } 93 }
94 94
95 @Test 95 @Test
96 public void metadataConditionCanBeFalse() throws TbNodeException { 96 public void metadataConditionCanBeFalse() throws TbNodeException {
97 - initWithScript("meta.humidity < 15;"); 97 + initWithScript("return metadata.humidity < 15;");
98 TbMsgMetaData metaData = new TbMsgMetaData(); 98 TbMsgMetaData metaData = new TbMsgMetaData();
99 metaData.putValue("temp", "10"); 99 metaData.putValue("temp", "10");
100 metaData.putValue("humidity", "99"); 100 metaData.putValue("humidity", "99");
@@ -109,7 +109,7 @@ public class TbJsFilterNodeTest { @@ -109,7 +109,7 @@ public class TbJsFilterNodeTest {
109 109
110 @Test 110 @Test
111 public void metadataConditionCanBeTrue() throws TbNodeException { 111 public void metadataConditionCanBeTrue() throws TbNodeException {
112 - initWithScript("meta.temp < 15;"); 112 + initWithScript("return metadata.temp < 15;");
113 TbMsgMetaData metaData = new TbMsgMetaData(); 113 TbMsgMetaData metaData = new TbMsgMetaData();
114 metaData.putValue("temp", "10"); 114 metaData.putValue("temp", "10");
115 metaData.putValue("humidity", "99"); 115 metaData.putValue("humidity", "99");
@@ -123,7 +123,7 @@ public class TbJsFilterNodeTest { @@ -123,7 +123,7 @@ public class TbJsFilterNodeTest {
123 123
124 @Test 124 @Test
125 public void msgJsonParsedAndBinded() throws TbNodeException { 125 public void msgJsonParsedAndBinded() throws TbNodeException {
126 - initWithScript("msg.passed < 15 && msg.name === 'Vit' && meta.temp == 10 && msg.bigObj.prop == 42;"); 126 + initWithScript("return msg.passed < 15 && msg.name === 'Vit' && metadata.temp == 10 && msg.bigObj.prop == 42;");
127 TbMsgMetaData metaData = new TbMsgMetaData(); 127 TbMsgMetaData metaData = new TbMsgMetaData();
128 metaData.putValue("temp", "10"); 128 metaData.putValue("temp", "10");
129 metaData.putValue("humidity", "99"); 129 metaData.putValue("humidity", "99");
@@ -144,7 +144,7 @@ public class TbJsFilterNodeTest { @@ -144,7 +144,7 @@ public class TbJsFilterNodeTest {
144 TbNodeConfiguration nodeConfiguration = new TbNodeConfiguration(mapper.valueToTree(config)); 144 TbNodeConfiguration nodeConfiguration = new TbNodeConfiguration(mapper.valueToTree(config));
145 145
146 node = new TbJsFilterNode(); 146 node = new TbJsFilterNode();
147 - node.init(nodeConfiguration, null); 147 + node.init(null, nodeConfiguration);
148 } 148 }
149 149
150 private void mockJsExecutor() { 150 private void mockJsExecutor() {
@@ -53,27 +53,16 @@ public class TbJsSwitchNodeTest { @@ -53,27 +53,16 @@ public class TbJsSwitchNodeTest {
53 private ListeningExecutor executor; 53 private ListeningExecutor executor;
54 54
55 @Test 55 @Test
56 - public void routeToAllDoNotEvaluatesJs() throws TbNodeException {  
57 - HashSet<String> relations = Sets.newHashSet("one", "two");  
58 - initWithScript("test qwerty", relations, true);  
59 - TbMsg msg = new TbMsg(UUIDs.timeBased(), "USER", null, new TbMsgMetaData(), "{}".getBytes());  
60 -  
61 - node.onMsg(ctx, msg);  
62 - verify(ctx).tellNext(msg, relations);  
63 - verifyNoMoreInteractions(ctx, executor);  
64 - }  
65 -  
66 - @Test  
67 public void multipleRoutesAreAllowed() throws TbNodeException { 56 public void multipleRoutesAreAllowed() throws TbNodeException {
68 - String jsCode = "function nextRelation(meta, msg) {\n" +  
69 - " if(msg.passed == 5 && meta.temp == 10)\n" + 57 + String jsCode = "function nextRelation(metadata, msg) {\n" +
  58 + " if(msg.passed == 5 && metadata.temp == 10)\n" +
70 " return ['three', 'one']\n" + 59 " return ['three', 'one']\n" +
71 " else\n" + 60 " else\n" +
72 " return 'two';\n" + 61 " return 'two';\n" +
73 "};\n" + 62 "};\n" +
74 "\n" + 63 "\n" +
75 - "nextRelation(meta, msg);";  
76 - initWithScript(jsCode, Sets.newHashSet("one", "two", "three"), false); 64 + "return nextRelation(metadata, msg);";
  65 + initWithScript(jsCode);
77 TbMsgMetaData metaData = new TbMsgMetaData(); 66 TbMsgMetaData metaData = new TbMsgMetaData();
78 metaData.putValue("temp", "10"); 67 metaData.putValue("temp", "10");
79 metaData.putValue("humidity", "99"); 68 metaData.putValue("humidity", "99");
@@ -89,15 +78,15 @@ public class TbJsSwitchNodeTest { @@ -89,15 +78,15 @@ public class TbJsSwitchNodeTest {
89 78
90 @Test 79 @Test
91 public void allowedRelationPassed() throws TbNodeException { 80 public void allowedRelationPassed() throws TbNodeException {
92 - String jsCode = "function nextRelation(meta, msg) {\n" +  
93 - " if(msg.passed == 5 && meta.temp == 10)\n" + 81 + String jsCode = "function nextRelation(metadata, msg) {\n" +
  82 + " if(msg.passed == 5 && metadata.temp == 10)\n" +
94 " return 'one'\n" + 83 " return 'one'\n" +
95 " else\n" + 84 " else\n" +
96 " return 'two';\n" + 85 " return 'two';\n" +
97 "};\n" + 86 "};\n" +
98 "\n" + 87 "\n" +
99 - "nextRelation(meta, msg);";  
100 - initWithScript(jsCode, Sets.newHashSet("one", "two"), false); 88 + "return nextRelation(metadata, msg);";
  89 + initWithScript(jsCode);
101 TbMsgMetaData metaData = new TbMsgMetaData(); 90 TbMsgMetaData metaData = new TbMsgMetaData();
102 metaData.putValue("temp", "10"); 91 metaData.putValue("temp", "10");
103 metaData.putValue("humidity", "99"); 92 metaData.putValue("humidity", "99");
@@ -111,37 +100,14 @@ public class TbJsSwitchNodeTest { @@ -111,37 +100,14 @@ public class TbJsSwitchNodeTest {
111 verify(ctx).tellNext(msg, Sets.newHashSet("one")); 100 verify(ctx).tellNext(msg, Sets.newHashSet("one"));
112 } 101 }
113 102
114 - @Test  
115 - public void unknownRelationThrowsException() throws TbNodeException {  
116 - String jsCode = "function nextRelation(meta, msg) {\n" +  
117 - " return ['one','nine'];" +  
118 - "};\n" +  
119 - "\n" +  
120 - "nextRelation(meta, msg);";  
121 - initWithScript(jsCode, Sets.newHashSet("one", "two"), false);  
122 - TbMsgMetaData metaData = new TbMsgMetaData();  
123 - metaData.putValue("temp", "10");  
124 - metaData.putValue("humidity", "99");  
125 - String rawJson = "{\"name\": \"Vit\", \"passed\": 5}";  
126 -  
127 - TbMsg msg = new TbMsg(UUIDs.timeBased(), "USER", null, metaData, rawJson.getBytes());  
128 - mockJsExecutor();  
129 -  
130 - node.onMsg(ctx, msg);  
131 - verify(ctx).getJsExecutor();  
132 - verifyError(msg, "Unsupported relation for switch [nine, one]", IllegalStateException.class);  
133 - }  
134 -  
135 - private void initWithScript(String script, Set<String> relations, boolean routeToAll) throws TbNodeException { 103 + private void initWithScript(String script) throws TbNodeException {
136 TbJsSwitchNodeConfiguration config = new TbJsSwitchNodeConfiguration(); 104 TbJsSwitchNodeConfiguration config = new TbJsSwitchNodeConfiguration();
137 config.setJsScript(script); 105 config.setJsScript(script);
138 - config.setAllowedRelations(relations);  
139 - config.setRouteToAllWithNoCheck(routeToAll);  
140 ObjectMapper mapper = new ObjectMapper(); 106 ObjectMapper mapper = new ObjectMapper();
141 TbNodeConfiguration nodeConfiguration = new TbNodeConfiguration(mapper.valueToTree(config)); 107 TbNodeConfiguration nodeConfiguration = new TbNodeConfiguration(mapper.valueToTree(config));
142 108
143 node = new TbJsSwitchNode(); 109 node = new TbJsSwitchNode();
144 - node.init(nodeConfiguration, null); 110 + node.init(null, nodeConfiguration);
145 } 111 }
146 112
147 private void mockJsExecutor() { 113 private void mockJsExecutor() {
@@ -88,7 +88,7 @@ public class TbGetCustomerAttributeNodeTest { @@ -88,7 +88,7 @@ public class TbGetCustomerAttributeNodeTest {
88 TbNodeConfiguration nodeConfiguration = new TbNodeConfiguration(mapper.valueToTree(config)); 88 TbNodeConfiguration nodeConfiguration = new TbNodeConfiguration(mapper.valueToTree(config));
89 89
90 node = new TbGetCustomerAttributeNode(); 90 node = new TbGetCustomerAttributeNode();
91 - node.init(nodeConfiguration, null); 91 + node.init(null, nodeConfiguration);
92 } 92 }
93 93
94 @Test 94 @Test
@@ -226,7 +226,7 @@ public class TbGetCustomerAttributeNodeTest { @@ -226,7 +226,7 @@ public class TbGetCustomerAttributeNodeTest {
226 TbNodeConfiguration nodeConfiguration = new TbNodeConfiguration(mapper.valueToTree(config)); 226 TbNodeConfiguration nodeConfiguration = new TbNodeConfiguration(mapper.valueToTree(config));
227 227
228 node = new TbGetCustomerAttributeNode(); 228 node = new TbGetCustomerAttributeNode();
229 - node.init(nodeConfiguration, null); 229 + node.init(null, nodeConfiguration);
230 230
231 231
232 DeviceId deviceId = new DeviceId(UUIDs.timeBased()); 232 DeviceId deviceId = new DeviceId(UUIDs.timeBased());
@@ -119,6 +119,6 @@ public class TbChangeOriginatorNodeTest { @@ -119,6 +119,6 @@ public class TbChangeOriginatorNodeTest {
119 TbNodeConfiguration nodeConfiguration = new TbNodeConfiguration(mapper.valueToTree(config)); 119 TbNodeConfiguration nodeConfiguration = new TbNodeConfiguration(mapper.valueToTree(config));
120 120
121 node = new TbChangeOriginatorNode(); 121 node = new TbChangeOriginatorNode();
122 - node.init(nodeConfiguration, null); 122 + node.init(null, nodeConfiguration);
123 } 123 }
124 } 124 }
@@ -51,7 +51,7 @@ public class TbTransformMsgNodeTest { @@ -51,7 +51,7 @@ public class TbTransformMsgNodeTest {
51 51
52 @Test 52 @Test
53 public void metadataCanBeUpdated() throws TbNodeException { 53 public void metadataCanBeUpdated() throws TbNodeException {
54 - initWithScript("meta.temp = meta.temp * 10;"); 54 + initWithScript("return metadata.temp = metadata.temp * 10;");
55 TbMsgMetaData metaData = new TbMsgMetaData(); 55 TbMsgMetaData metaData = new TbMsgMetaData();
56 metaData.putValue("temp", "7"); 56 metaData.putValue("temp", "7");
57 metaData.putValue("humidity", "99"); 57 metaData.putValue("humidity", "99");
@@ -70,7 +70,7 @@ public class TbTransformMsgNodeTest { @@ -70,7 +70,7 @@ public class TbTransformMsgNodeTest {
70 70
71 @Test 71 @Test
72 public void metadataCanBeAdded() throws TbNodeException { 72 public void metadataCanBeAdded() throws TbNodeException {
73 - initWithScript("meta.newAttr = meta.humidity - msg.passed;"); 73 + initWithScript("return metadata.newAttr = metadata.humidity - msg.passed;");
74 TbMsgMetaData metaData = new TbMsgMetaData(); 74 TbMsgMetaData metaData = new TbMsgMetaData();
75 metaData.putValue("temp", "7"); 75 metaData.putValue("temp", "7");
76 metaData.putValue("humidity", "99"); 76 metaData.putValue("humidity", "99");
@@ -89,7 +89,7 @@ public class TbTransformMsgNodeTest { @@ -89,7 +89,7 @@ public class TbTransformMsgNodeTest {
89 89
90 @Test 90 @Test
91 public void payloadCanBeUpdated() throws TbNodeException { 91 public void payloadCanBeUpdated() throws TbNodeException {
92 - initWithScript("msg.passed = msg.passed * meta.temp; msg.bigObj.newProp = 'Ukraine' "); 92 + initWithScript("return msg.passed = msg.passed * metadata.temp; msg.bigObj.newProp = 'Ukraine' ");
93 TbMsgMetaData metaData = new TbMsgMetaData(); 93 TbMsgMetaData metaData = new TbMsgMetaData();
94 metaData.putValue("temp", "7"); 94 metaData.putValue("temp", "7");
95 metaData.putValue("humidity", "99"); 95 metaData.putValue("humidity", "99");
@@ -114,7 +114,7 @@ public class TbTransformMsgNodeTest { @@ -114,7 +114,7 @@ public class TbTransformMsgNodeTest {
114 TbNodeConfiguration nodeConfiguration = new TbNodeConfiguration(mapper.valueToTree(config)); 114 TbNodeConfiguration nodeConfiguration = new TbNodeConfiguration(mapper.valueToTree(config));
115 115
116 node = new TbTransformMsgNode(); 116 node = new TbTransformMsgNode();
117 - node.init(nodeConfiguration, null); 117 + node.init(null, nodeConfiguration);
118 } 118 }
119 119
120 private void mockJsExecutor() { 120 private void mockJsExecutor() {
@@ -15,7 +15,7 @@ @@ -15,7 +15,7 @@
15 }, 15 },
16 "dependencies": { 16 "dependencies": {
17 "@flowjs/ng-flow": "^2.7.1", 17 "@flowjs/ng-flow": "^2.7.1",
18 - "ace-builds": "^1.2.5", 18 + "ace-builds": "1.3.1",
19 "angular": "1.5.8", 19 "angular": "1.5.8",
20 "angular-animate": "1.5.8", 20 "angular-animate": "1.5.8",
21 "angular-aria": "1.5.8", 21 "angular-aria": "1.5.8",
@@ -30,6 +30,9 @@ const httpProxy = require('http-proxy'); @@ -30,6 +30,9 @@ const httpProxy = require('http-proxy');
30 const forwardHost = 'localhost'; 30 const forwardHost = 'localhost';
31 const forwardPort = 8080; 31 const forwardPort = 8080;
32 32
  33 +const ruleNodeUiforwardHost = 'localhost';
  34 +const ruleNodeUiforwardPort = 8080;
  35 +
33 const app = express(); 36 const app = express();
34 const server = http.createServer(app); 37 const server = http.createServer(app);
35 38
@@ -52,17 +55,34 @@ const apiProxy = httpProxy.createProxyServer({ @@ -52,17 +55,34 @@ const apiProxy = httpProxy.createProxyServer({
52 } 55 }
53 }); 56 });
54 57
  58 +const ruleNodeUiApiProxy = httpProxy.createProxyServer({
  59 + target: {
  60 + host: ruleNodeUiforwardHost,
  61 + port: ruleNodeUiforwardPort
  62 + }
  63 +});
  64 +
55 apiProxy.on('error', function (err, req, res) { 65 apiProxy.on('error', function (err, req, res) {
56 console.warn('API proxy error: ' + err); 66 console.warn('API proxy error: ' + err);
57 res.end('Error.'); 67 res.end('Error.');
58 }); 68 });
59 69
  70 +ruleNodeUiApiProxy.on('error', function (err, req, res) {
  71 + console.warn('RuleNode UI API proxy error: ' + err);
  72 + res.end('Error.');
  73 +});
  74 +
60 console.info(`Forwarding API requests to http://${forwardHost}:${forwardPort}`); 75 console.info(`Forwarding API requests to http://${forwardHost}:${forwardPort}`);
  76 +console.info(`Forwarding Rule Node UI requests to http://${ruleNodeUiforwardHost}:${ruleNodeUiforwardPort}`);
61 77
62 app.all('/api/*', (req, res) => { 78 app.all('/api/*', (req, res) => {
63 apiProxy.web(req, res); 79 apiProxy.web(req, res);
64 }); 80 });
65 81
  82 +app.all('/static/rulenode/*', (req, res) => {
  83 + ruleNodeUiApiProxy.web(req, res);
  84 +});
  85 +
66 app.get('*', function(req, res) { 86 app.get('*', function(req, res) {
67 res.sendFile(path.join(__dirname, 'src/index.html')); 87 res.sendFile(path.join(__dirname, 'src/index.html'));
68 }); 88 });
@@ -17,7 +17,7 @@ export default angular.module('thingsboard.api.ruleChain', []) @@ -17,7 +17,7 @@ export default angular.module('thingsboard.api.ruleChain', [])
17 .factory('ruleChainService', RuleChainService).name; 17 .factory('ruleChainService', RuleChainService).name;
18 18
19 /*@ngInject*/ 19 /*@ngInject*/
20 -function RuleChainService($http, $q, $filter, types, componentDescriptorService) { 20 +function RuleChainService($http, $q, $filter, $ocLazyLoad, $translate, types, componentDescriptorService) {
21 21
22 var ruleNodeComponents = null; 22 var ruleNodeComponents = null;
23 23
@@ -177,11 +177,18 @@ function RuleChainService($http, $q, $filter, types, componentDescriptorService) @@ -177,11 +177,18 @@ function RuleChainService($http, $q, $filter, types, componentDescriptorService)
177 } else { 177 } else {
178 loadRuleNodeComponents().then( 178 loadRuleNodeComponents().then(
179 (components) => { 179 (components) => {
180 - ruleNodeComponents = components;  
181 - ruleNodeComponents.push(  
182 - types.ruleChainNodeComponent 180 + resolveRuleNodeComponentsUiResources(components).then(
  181 + (components) => {
  182 + ruleNodeComponents = components;
  183 + ruleNodeComponents.push(
  184 + types.ruleChainNodeComponent
  185 + );
  186 + deferred.resolve(ruleNodeComponents);
  187 + },
  188 + () => {
  189 + deferred.reject();
  190 + }
183 ); 191 );
184 - deferred.resolve(ruleNodeComponents);  
185 }, 192 },
186 () => { 193 () => {
187 deferred.reject(); 194 deferred.reject();
@@ -191,6 +198,48 @@ function RuleChainService($http, $q, $filter, types, componentDescriptorService) @@ -191,6 +198,48 @@ function RuleChainService($http, $q, $filter, types, componentDescriptorService)
191 return deferred.promise; 198 return deferred.promise;
192 } 199 }
193 200
  201 + function resolveRuleNodeComponentsUiResources(components) {
  202 + var deferred = $q.defer();
  203 + var tasks = [];
  204 + for (var i=0;i<components.length;i++) {
  205 + var component = components[i];
  206 + tasks.push(resolveRuleNodeComponentUiResources(component));
  207 + }
  208 + $q.all(tasks).then(
  209 + (components) => {
  210 + deferred.resolve(components);
  211 + },
  212 + () => {
  213 + deferred.resolve(components);
  214 + }
  215 + );
  216 + return deferred.promise;
  217 + }
  218 +
  219 + function resolveRuleNodeComponentUiResources(component) {
  220 + var deferred = $q.defer();
  221 + var uiResources = component.configurationDescriptor.nodeDefinition.uiResources;
  222 + if (uiResources && uiResources.length) {
  223 + var tasks = [];
  224 + for (var i=0;i<uiResources.length;i++) {
  225 + var uiResource = uiResources[i];
  226 + tasks.push($ocLazyLoad.load(uiResource));
  227 + }
  228 + $q.all(tasks).then(
  229 + () => {
  230 + deferred.resolve(component);
  231 + },
  232 + () => {
  233 + component.configurationDescriptor.nodeDefinition.uiResourceLoadError = $translate.instant('rulenode.ui-resources-load-error');
  234 + deferred.resolve(component);
  235 + }
  236 + )
  237 + } else {
  238 + deferred.resolve(component);
  239 + }
  240 + return deferred.promise;
  241 + }
  242 +
194 function getRuleNodeComponentByClazz(clazz) { 243 function getRuleNodeComponentByClazz(clazz) {
195 var res = $filter('filter')(ruleNodeComponents, {clazz: clazz}, true); 244 var res = $filter('filter')(ruleNodeComponents, {clazz: clazz}, true);
196 if (res && res.length) { 245 if (res && res.length) {
@@ -279,6 +279,23 @@ export default angular.module('thingsboard.types', []) @@ -279,6 +279,23 @@ export default angular.module('thingsboard.types', [])
279 function: "function", 279 function: "function",
280 alarm: "alarm" 280 alarm: "alarm"
281 }, 281 },
  282 + contentType: {
  283 + "JSON": {
  284 + value: "JSON",
  285 + name: "content-type.json",
  286 + code: "json"
  287 + },
  288 + "TEXT": {
  289 + value: "TEXT",
  290 + name: "content-type.text",
  291 + code: "text"
  292 + },
  293 + "BINARY": {
  294 + value: "BINARY",
  295 + name: "content-type.binary",
  296 + code: "text"
  297 + }
  298 + },
282 componentType: { 299 componentType: {
283 filter: "FILTER", 300 filter: "FILTER",
284 processor: "PROCESSOR", 301 processor: "PROCESSOR",
@@ -295,7 +312,8 @@ export default angular.module('thingsboard.types', []) @@ -295,7 +312,8 @@ export default angular.module('thingsboard.types', [])
295 user: "USER", 312 user: "USER",
296 dashboard: "DASHBOARD", 313 dashboard: "DASHBOARD",
297 alarm: "ALARM", 314 alarm: "ALARM",
298 - rulechain: "RULE_CHAIN" 315 + rulechain: "RULE_CHAIN",
  316 + rulenode: "RULE_NODE"
299 }, 317 },
300 aliasEntityType: { 318 aliasEntityType: {
301 current_customer: "CURRENT_CUSTOMER" 319 current_customer: "CURRENT_CUSTOMER"
@@ -388,6 +406,16 @@ export default angular.module('thingsboard.types', []) @@ -388,6 +406,16 @@ export default angular.module('thingsboard.types', [])
388 name: "event.type-stats" 406 name: "event.type-stats"
389 } 407 }
390 }, 408 },
  409 + debugEventType: {
  410 + debugRuleNode: {
  411 + value: "DEBUG_RULE_NODE",
  412 + name: "event.type-debug-rule-node"
  413 + },
  414 + debugRuleChain: {
  415 + value: "DEBUG_RULE_CHAIN",
  416 + name: "event.type-debug-rule-chain"
  417 + }
  418 + },
391 extensionType: { 419 extensionType: {
392 http: "HTTP", 420 http: "HTTP",
393 mqtt: "MQTT", 421 mqtt: "MQTT",
@@ -18,17 +18,17 @@ export default angular.module('thingsboard.directives.confirmOnExit', []) @@ -18,17 +18,17 @@ export default angular.module('thingsboard.directives.confirmOnExit', [])
18 .name; 18 .name;
19 19
20 /*@ngInject*/ 20 /*@ngInject*/
21 -function ConfirmOnExit($state, $mdDialog, $window, $filter, userService) { 21 +function ConfirmOnExit($state, $mdDialog, $window, $filter, $parse, userService) {
22 return { 22 return {
23 - link: function ($scope) {  
24 - 23 + link: function ($scope, $element, $attributes) {
  24 + $scope.confirmForm = $scope.$eval($attributes.confirmForm);
25 $window.onbeforeunload = function () { 25 $window.onbeforeunload = function () {
26 - if (userService.isAuthenticated() && (($scope.confirmForm && $scope.confirmForm.$dirty) || $scope.isDirty)) { 26 + if (userService.isAuthenticated() && (($scope.confirmForm && $scope.confirmForm.$dirty) || $scope.$eval($attributes.isDirty))) {
27 return $filter('translate')('confirm-on-exit.message'); 27 return $filter('translate')('confirm-on-exit.message');
28 } 28 }
29 } 29 }
30 $scope.$on('$stateChangeStart', function (event, next, current, params) { 30 $scope.$on('$stateChangeStart', function (event, next, current, params) {
31 - if (userService.isAuthenticated() && (($scope.confirmForm && $scope.confirmForm.$dirty) || $scope.isDirty)) { 31 + if (userService.isAuthenticated() && (($scope.confirmForm && $scope.confirmForm.$dirty) || $scope.$eval($attributes.isDirty))) {
32 event.preventDefault(); 32 event.preventDefault();
33 var confirm = $mdDialog.confirm() 33 var confirm = $mdDialog.confirm()
34 .title($filter('translate')('confirm-on-exit.title')) 34 .title($filter('translate')('confirm-on-exit.title'))
@@ -40,7 +40,9 @@ function ConfirmOnExit($state, $mdDialog, $window, $filter, userService) { @@ -40,7 +40,9 @@ function ConfirmOnExit($state, $mdDialog, $window, $filter, userService) {
40 if ($scope.confirmForm) { 40 if ($scope.confirmForm) {
41 $scope.confirmForm.$setPristine(); 41 $scope.confirmForm.$setPristine();
42 } else { 42 } else {
43 - $scope.isDirty = false; 43 + var remoteSetter = $parse($attributes.isDirty).assign;
  44 + remoteSetter($scope, false);
  45 + //$scope.isDirty = false;
44 } 46 }
45 $state.go(next.name, params); 47 $state.go(next.name, params);
46 }, function () { 48 }, function () {
@@ -48,9 +50,6 @@ function ConfirmOnExit($state, $mdDialog, $window, $filter, userService) { @@ -48,9 +50,6 @@ function ConfirmOnExit($state, $mdDialog, $window, $filter, userService) {
48 } 50 }
49 }); 51 });
50 }, 52 },
51 - scope: {  
52 - confirmForm: '=',  
53 - isDirty: '='  
54 - } 53 + scope: false
55 }; 54 };
56 } 55 }
@@ -26,7 +26,7 @@ export default angular.module('thingsboard.directives.detailsSidenav', []) @@ -26,7 +26,7 @@ export default angular.module('thingsboard.directives.detailsSidenav', [])
26 .name; 26 .name;
27 27
28 /*@ngInject*/ 28 /*@ngInject*/
29 -function DetailsSidenav($timeout) { 29 +function DetailsSidenav($timeout, $mdUtil, $q, $animate) {
30 30
31 var linker = function (scope, element, attrs) { 31 var linker = function (scope, element, attrs) {
32 32
@@ -42,6 +42,63 @@ function DetailsSidenav($timeout) { @@ -42,6 +42,63 @@ function DetailsSidenav($timeout) {
42 scope.isEdit = true; 42 scope.isEdit = true;
43 } 43 }
44 44
  45 + var backdrop;
  46 + var previousContainerStyles;
  47 +
  48 + if (attrs.hasOwnProperty('tbEnableBackdrop')) {
  49 + backdrop = $mdUtil.createBackdrop(scope, "md-sidenav-backdrop md-opaque ng-enter");
  50 + element.on('$destroy', function() {
  51 + backdrop && backdrop.remove();
  52 + });
  53 + scope.$on('$destroy', function(){
  54 + backdrop && backdrop.remove();
  55 + });
  56 + scope.$watch('isOpen', updateIsOpen);
  57 + }
  58 +
  59 + function updateIsOpen(isOpen) {
  60 + backdrop[isOpen ? 'on' : 'off']('click', (ev)=>{
  61 + ev.preventDefault();
  62 + scope.isOpen = false;
  63 + scope.$apply();
  64 + });
  65 + var parent = element.parent();
  66 + var restorePositioning = updateContainerPositions(parent, isOpen);
  67 +
  68 + return $q.all([
  69 + isOpen && backdrop ? $animate.enter(backdrop, parent) : backdrop ?
  70 + $animate.leave(backdrop) : $q.when(true)
  71 + ]).then(function() {
  72 + restorePositioning && restorePositioning();
  73 + });
  74 + }
  75 +
  76 + function updateContainerPositions(parent, willOpen) {
  77 + var drawerEl = element[0];
  78 + var scrollTop = parent[0].scrollTop;
  79 + if (willOpen && scrollTop) {
  80 + previousContainerStyles = {
  81 + top: drawerEl.style.top,
  82 + bottom: drawerEl.style.bottom,
  83 + height: drawerEl.style.height
  84 + };
  85 + var positionStyle = {
  86 + top: scrollTop + 'px',
  87 + bottom: 'auto',
  88 + height: parent[0].clientHeight + 'px'
  89 + };
  90 + backdrop.css(positionStyle);
  91 + }
  92 + if (!willOpen && previousContainerStyles) {
  93 + return function() {
  94 + backdrop[0].style.top = null;
  95 + backdrop[0].style.bottom = null;
  96 + backdrop[0].style.height = null;
  97 + previousContainerStyles = null;
  98 + };
  99 + }
  100 + }
  101 +
45 scope.toggleDetailsEditMode = function () { 102 scope.toggleDetailsEditMode = function () {
46 if (!scope.isAlwaysEdit) { 103 if (!scope.isAlwaysEdit) {
47 if (!scope.isEdit) { 104 if (!scope.isEdit) {
@@ -16,7 +16,7 @@ @@ -16,7 +16,7 @@
16 16
17 --> 17 -->
18 <md-sidenav class="md-sidenav-right md-whiteframe-4dp tb-sidenav-details" 18 <md-sidenav class="md-sidenav-right md-whiteframe-4dp tb-sidenav-details"
19 - md-disable-backdrop="true" 19 + md-disable-backdrop
20 md-is-open="isOpen" 20 md-is-open="isOpen"
21 md-component-id="right" 21 md-component-id="right"
22 layout="column"> 22 layout="column">
@@ -43,6 +43,7 @@ function JsFunc($compile, $templateCache, toast, utils, $translate) { @@ -43,6 +43,7 @@ function JsFunc($compile, $templateCache, toast, utils, $translate) {
43 var template = $templateCache.get(jsFuncTemplate); 43 var template = $templateCache.get(jsFuncTemplate);
44 element.html(template); 44 element.html(template);
45 45
  46 + scope.functionName = attrs.functionName;
46 scope.functionArgs = scope.$eval(attrs.functionArgs); 47 scope.functionArgs = scope.$eval(attrs.functionArgs);
47 scope.validationArgs = scope.$eval(attrs.validationArgs); 48 scope.validationArgs = scope.$eval(attrs.validationArgs);
48 scope.resultType = attrs.resultType; 49 scope.resultType = attrs.resultType;
@@ -50,6 +51,8 @@ function JsFunc($compile, $templateCache, toast, utils, $translate) { @@ -50,6 +51,8 @@ function JsFunc($compile, $templateCache, toast, utils, $translate) {
50 scope.resultType = "nocheck"; 51 scope.resultType = "nocheck";
51 } 52 }
52 53
  54 + scope.validationTriggerArg = attrs.validationTriggerArg;
  55 +
53 scope.functionValid = true; 56 scope.functionValid = true;
54 57
55 var Range = ace.acequire("ace/range").Range; 58 var Range = ace.acequire("ace/range").Range;
@@ -66,11 +69,15 @@ function JsFunc($compile, $templateCache, toast, utils, $translate) { @@ -66,11 +69,15 @@ function JsFunc($compile, $templateCache, toast, utils, $translate) {
66 } 69 }
67 70
68 scope.onFullscreenChanged = function () { 71 scope.onFullscreenChanged = function () {
  72 + updateEditorSize();
  73 + };
  74 +
  75 + function updateEditorSize() {
69 if (scope.js_editor) { 76 if (scope.js_editor) {
70 scope.js_editor.resize(); 77 scope.js_editor.resize();
71 scope.js_editor.renderer.updateFull(); 78 scope.js_editor.renderer.updateFull();
72 } 79 }
73 - }; 80 + }
74 81
75 scope.jsEditorOptions = { 82 scope.jsEditorOptions = {
76 useWrapMode: true, 83 useWrapMode: true,
@@ -131,6 +138,9 @@ function JsFunc($compile, $templateCache, toast, utils, $translate) { @@ -131,6 +138,9 @@ function JsFunc($compile, $templateCache, toast, utils, $translate) {
131 scope.validate = function () { 138 scope.validate = function () {
132 try { 139 try {
133 var toValidate = new Function(scope.functionArgsString, scope.functionBody); 140 var toValidate = new Function(scope.functionArgsString, scope.functionBody);
  141 + if (scope.noValidate) {
  142 + return true;
  143 + }
134 var res; 144 var res;
135 var validationError; 145 var validationError;
136 for (var i=0;i<scope.validationArgs.length;i++) { 146 for (var i=0;i<scope.validationArgs.length;i++) {
@@ -200,9 +210,19 @@ function JsFunc($compile, $templateCache, toast, utils, $translate) { @@ -200,9 +210,19 @@ function JsFunc($compile, $templateCache, toast, utils, $translate) {
200 } 210 }
201 }; 211 };
202 212
203 - scope.$on('form-submit', function () {  
204 - scope.functionValid = scope.validate();  
205 - scope.updateValidity(); 213 + scope.$on('form-submit', function (event, args) {
  214 + if (!args || scope.validationTriggerArg && scope.validationTriggerArg == args) {
  215 + scope.validationArgs = scope.$eval(attrs.validationArgs);
  216 + scope.cleanupJsErrors();
  217 + scope.functionValid = true;
  218 + scope.updateValidity();
  219 + scope.functionValid = scope.validate();
  220 + scope.updateValidity();
  221 + }
  222 + });
  223 +
  224 + scope.$on('update-ace-editor-size', function () {
  225 + updateEditorSize();
206 }); 226 });
207 227
208 $compile(element.contents())(scope); 228 $compile(element.contents())(scope);
@@ -211,7 +231,11 @@ function JsFunc($compile, $templateCache, toast, utils, $translate) { @@ -211,7 +231,11 @@ function JsFunc($compile, $templateCache, toast, utils, $translate) {
211 return { 231 return {
212 restrict: "E", 232 restrict: "E",
213 require: "^ngModel", 233 require: "^ngModel",
214 - scope: {}, 234 + scope: {
  235 + disabled:'=ngDisabled',
  236 + noValidate: '=?',
  237 + fillHeight:'=?'
  238 + },
215 link: linker 239 link: linker
216 }; 240 };
217 } 241 }
@@ -15,6 +15,12 @@ @@ -15,6 +15,12 @@
15 */ 15 */
16 tb-js-func { 16 tb-js-func {
17 position: relative; 17 position: relative;
  18 + .tb-disabled {
  19 + color: rgba(0,0,0,0.38);
  20 + }
  21 + .fill-height {
  22 + height: 100%;
  23 + }
18 } 24 }
19 25
20 .tb-js-func-panel { 26 .tb-js-func-panel {
@@ -23,8 +29,10 @@ tb-js-func { @@ -23,8 +29,10 @@ tb-js-func {
23 height: 100%; 29 height: 100%;
24 #tb-javascript-input { 30 #tb-javascript-input {
25 min-width: 200px; 31 min-width: 200px;
26 - min-height: 200px;  
27 width: 100%; 32 width: 100%;
28 height: 100%; 33 height: 100%;
  34 + &:not(.fill-height) {
  35 + min-height: 200px;
  36 + }
29 } 37 }
30 } 38 }
@@ -15,19 +15,20 @@ @@ -15,19 +15,20 @@
15 limitations under the License. 15 limitations under the License.
16 16
17 --> 17 -->
18 -<div style="background: #fff;" tb-expand-fullscreen fullscreen-zindex="100" expand-button-id="expand-button" on-fullscreen-changed="onFullscreenChanged()" layout="column"> 18 +<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">
19 <div layout="row" layout-align="start center" style="height: 40px;"> 19 <div layout="row" layout-align="start center" style="height: 40px;">
20 - <span style="font-style: italic;">function({{ functionArgsString }}) {</span> 20 + <label class="tb-title no-padding">function {{ functionName }}({{ functionArgsString }}) {</label>
21 <span flex></span> 21 <span flex></span>
22 <div id="expand-button" layout="column" aria-label="Fullscreen" class="md-button md-icon-button tb-md-32 tb-fullscreen-button-style"></div> 22 <div id="expand-button" layout="column" aria-label="Fullscreen" class="md-button md-icon-button tb-md-32 tb-fullscreen-button-style"></div>
23 </div> 23 </div>
24 <div flex id="tb-javascript-panel" class="tb-js-func-panel" layout="column"> 24 <div flex id="tb-javascript-panel" class="tb-js-func-panel" layout="column">
25 - <div flex id="tb-javascript-input"  
26 - ui-ace="jsEditorOptions" 25 + <div flex id="tb-javascript-input" ng-class="{'fill-height': fillHeight}"
  26 + ui-ace="jsEditorOptions"
  27 + ng-readonly="disabled"
27 ng-model="functionBody"> 28 ng-model="functionBody">
28 </div> 29 </div>
29 </div> 30 </div>
30 <div layout="row" layout-align="start center" style="height: 40px;"> 31 <div layout="row" layout-align="start center" style="height: 40px;">
31 - <span style="font-style: italic;">}</span>  
32 - </div>  
33 -</div>  
  32 + <label class="tb-title no-padding">}</label>
  33 + </div>
  34 +</div>
@@ -84,17 +84,32 @@ function JsonObjectEdit($compile, $templateCache, $document, toast, utils) { @@ -84,17 +84,32 @@ function JsonObjectEdit($compile, $templateCache, $document, toast, utils) {
84 scope.$watch('contentBody', function (newVal, prevVal) { 84 scope.$watch('contentBody', function (newVal, prevVal) {
85 if (!angular.equals(newVal, prevVal)) { 85 if (!angular.equals(newVal, prevVal)) {
86 var object = scope.validate(); 86 var object = scope.validate();
87 - ngModelCtrl.$setViewValue(object); 87 + if (scope.objectValid) {
  88 + if (object == null) {
  89 + scope.object = null;
  90 + } else {
  91 + if (scope.object == null) {
  92 + scope.object = {};
  93 + }
  94 + Object.keys(scope.object).forEach(function (key) {
  95 + delete scope.object[key];
  96 + });
  97 + Object.keys(object).forEach(function (key) {
  98 + scope.object[key] = object[key];
  99 + });
  100 + }
  101 + ngModelCtrl.$setViewValue(scope.object);
  102 + }
88 scope.updateValidity(); 103 scope.updateValidity();
89 } 104 }
90 }); 105 });
91 106
92 ngModelCtrl.$render = function () { 107 ngModelCtrl.$render = function () {
93 - var object = ngModelCtrl.$viewValue; 108 + scope.object = ngModelCtrl.$viewValue;
94 var content = ''; 109 var content = '';
95 try { 110 try {
96 - if (object) {  
97 - content = angular.toJson(object, true); 111 + if (scope.object) {
  112 + content = angular.toJson(scope.object, true);
98 } 113 }
99 } catch (e) { 114 } catch (e) {
100 // 115 //
@@ -17,11 +17,14 @@ import $ from 'jquery'; @@ -17,11 +17,14 @@ import $ from 'jquery';
17 import 'brace/ext/language_tools'; 17 import 'brace/ext/language_tools';
18 import 'brace/mode/java'; 18 import 'brace/mode/java';
19 import 'brace/theme/github'; 19 import 'brace/theme/github';
  20 +import beautify from 'js-beautify';
20 21
21 /* eslint-disable angular/angularelement */ 22 /* eslint-disable angular/angularelement */
22 23
  24 +const js_beautify = beautify.js;
  25 +
23 /*@ngInject*/ 26 /*@ngInject*/
24 -export default function EventContentDialogController($mdDialog, content, title, showingCallback) { 27 +export default function EventContentDialogController($mdDialog, types, content, contentType, title, showingCallback) {
25 28
26 var vm = this; 29 var vm = this;
27 30
@@ -32,9 +35,19 @@ export default function EventContentDialogController($mdDialog, content, title, @@ -32,9 +35,19 @@ export default function EventContentDialogController($mdDialog, content, title,
32 vm.content = content; 35 vm.content = content;
33 vm.title = title; 36 vm.title = title;
34 37
  38 + var mode;
  39 + if (contentType) {
  40 + mode = types.contentType[contentType].code;
  41 + if (contentType == types.contentType.JSON.value && vm.content) {
  42 + vm.content = js_beautify(vm.content, {indent_size: 4});
  43 + }
  44 + } else {
  45 + mode = 'java';
  46 + }
  47 +
35 vm.contentOptions = { 48 vm.contentOptions = {
36 useWrapMode: false, 49 useWrapMode: false,
37 - mode: 'java', 50 + mode: mode,
38 showGutter: false, 51 showGutter: false,
39 showPrintMargin: false, 52 showPrintMargin: false,
40 theme: 'github', 53 theme: 'github',
  1 +<!--
  2 +
  3 + Copyright © 2016-2018 The Thingsboard Authors
  4 +
  5 + Licensed under the Apache License, Version 2.0 (the "License");
  6 + you may not use this file except in compliance with the License.
  7 + You may obtain a copy of the License at
  8 +
  9 + http://www.apache.org/licenses/LICENSE-2.0
  10 +
  11 + Unless required by applicable law or agreed to in writing, software
  12 + distributed under the License is distributed on an "AS IS" BASIS,
  13 + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  14 + See the License for the specific language governing permissions and
  15 + limitations under the License.
  16 +
  17 +-->
  18 +<div hide-xs hide-sm translate class="tb-cell" flex="30">event.event-time</div>
  19 +<div translate class="tb-cell" flex="20">event.server</div>
  20 +<div translate class="tb-cell" flex="20">event.type</div>
  21 +<div translate class="tb-cell" flex="20">event.entity</div>
  22 +<div translate class="tb-cell" flex="20">event.message-id</div>
  23 +<div translate class="tb-cell" flex="20">event.message-type</div>
  24 +<div translate class="tb-cell" flex="20">event.data-type</div>
  25 +<div translate class="tb-cell" flex="20">event.data</div>
  26 +<div translate class="tb-cell" flex="20">event.metadata</div>
  27 +<div translate class="tb-cell" flex="20">event.error</div>
@@ -18,6 +18,7 @@ @@ -18,6 +18,7 @@
18 import eventHeaderLcEventTemplate from './event-header-lc-event.tpl.html'; 18 import eventHeaderLcEventTemplate from './event-header-lc-event.tpl.html';
19 import eventHeaderStatsTemplate from './event-header-stats.tpl.html'; 19 import eventHeaderStatsTemplate from './event-header-stats.tpl.html';
20 import eventHeaderErrorTemplate from './event-header-error.tpl.html'; 20 import eventHeaderErrorTemplate from './event-header-error.tpl.html';
  21 +import eventHeaderDebugRuleNodeTemplate from './event-header-debug-rulenode.tpl.html';
21 22
22 /* eslint-enable import/no-unresolved, import/default */ 23 /* eslint-enable import/no-unresolved, import/default */
23 24
@@ -38,6 +39,12 @@ export default function EventHeaderDirective($compile, $templateCache, types) { @@ -38,6 +39,12 @@ export default function EventHeaderDirective($compile, $templateCache, types) {
38 case types.eventType.error.value: 39 case types.eventType.error.value:
39 template = eventHeaderErrorTemplate; 40 template = eventHeaderErrorTemplate;
40 break; 41 break;
  42 + case types.debugEventType.debugRuleNode.value:
  43 + template = eventHeaderDebugRuleNodeTemplate;
  44 + break;
  45 + case types.debugEventType.debugRuleChain.value:
  46 + template = eventHeaderDebugRuleNodeTemplate;
  47 + break;
41 } 48 }
42 return $templateCache.get(template); 49 return $templateCache.get(template);
43 } 50 }
  1 +<!--
  2 +
  3 + Copyright © 2016-2018 The Thingsboard Authors
  4 +
  5 + Licensed under the Apache License, Version 2.0 (the "License");
  6 + you may not use this file except in compliance with the License.
  7 + You may obtain a copy of the License at
  8 +
  9 + http://www.apache.org/licenses/LICENSE-2.0
  10 +
  11 + Unless required by applicable law or agreed to in writing, software
  12 + distributed under the License is distributed on an "AS IS" BASIS,
  13 + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  14 + See the License for the specific language governing permissions and
  15 + limitations under the License.
  16 +
  17 +-->
  18 +<div hide-xs hide-sm class="tb-cell" flex="30">{{event.createdTime | date : 'yyyy-MM-dd HH:mm:ss'}}</div>
  19 +<div class="tb-cell" flex="20">{{event.body.server}}</div>
  20 +<div class="tb-cell" flex="20">{{event.body.type}}</div>
  21 +<div class="tb-cell" flex="20">{{event.body.entityName}}</div>
  22 +<div class="tb-cell" flex="20">{{event.body.msgId}}</div>
  23 +<div class="tb-cell" flex="20">{{event.body.msgType}}</div>
  24 +<div class="tb-cell" flex="20">{{event.body.dataType}}</div>
  25 +<div class="tb-cell" flex="20">
  26 + <md-button ng-if="event.body.data" class="md-icon-button md-primary"
  27 + ng-click="showContent($event, event.body.data, 'event.data', event.body.dataType)"
  28 + aria-label="{{ 'action.view' | translate }}">
  29 + <md-tooltip md-direction="top">
  30 + {{ 'action.view' | translate }}
  31 + </md-tooltip>
  32 + <md-icon aria-label="{{ 'action.view' | translate }}"
  33 + class="material-icons">
  34 + more_horiz
  35 + </md-icon>
  36 + </md-button>
  37 +</div>
  38 +<div class="tb-cell" flex="20">
  39 + <md-button ng-if="event.body.metadata" class="md-icon-button md-primary"
  40 + ng-click="showContent($event, event.body.metadata, 'event.metadata', 'JSON')"
  41 + aria-label="{{ 'action.view' | translate }}">
  42 + <md-tooltip md-direction="top">
  43 + {{ 'action.view' | translate }}
  44 + </md-tooltip>
  45 + <md-icon aria-label="{{ 'action.view' | translate }}"
  46 + class="material-icons">
  47 + more_horiz
  48 + </md-icon>
  49 + </md-button>
  50 +</div>
  51 +<div class="tb-cell" flex="20">
  52 + <md-button ng-if="event.body.error" class="md-icon-button md-primary"
  53 + ng-click="showContent($event, event.body.error, 'event.error')"
  54 + aria-label="{{ 'action.view' | translate }}">
  55 + <md-tooltip md-direction="top">
  56 + {{ 'action.view' | translate }}
  57 + </md-tooltip>
  58 + <md-icon aria-label="{{ 'action.view' | translate }}"
  59 + class="material-icons">
  60 + more_horiz
  61 + </md-icon>
  62 + </md-button>
  63 +</div>
@@ -20,6 +20,7 @@ import eventErrorDialogTemplate from './event-content-dialog.tpl.html'; @@ -20,6 +20,7 @@ import eventErrorDialogTemplate from './event-content-dialog.tpl.html';
20 import eventRowLcEventTemplate from './event-row-lc-event.tpl.html'; 20 import eventRowLcEventTemplate from './event-row-lc-event.tpl.html';
21 import eventRowStatsTemplate from './event-row-stats.tpl.html'; 21 import eventRowStatsTemplate from './event-row-stats.tpl.html';
22 import eventRowErrorTemplate from './event-row-error.tpl.html'; 22 import eventRowErrorTemplate from './event-row-error.tpl.html';
  23 +import eventRowDebugRuleNodeTemplate from './event-row-debug-rulenode.tpl.html';
23 24
24 /* eslint-enable import/no-unresolved, import/default */ 25 /* eslint-enable import/no-unresolved, import/default */
25 26
@@ -40,6 +41,12 @@ export default function EventRowDirective($compile, $templateCache, $mdDialog, $ @@ -40,6 +41,12 @@ export default function EventRowDirective($compile, $templateCache, $mdDialog, $
40 case types.eventType.error.value: 41 case types.eventType.error.value:
41 template = eventRowErrorTemplate; 42 template = eventRowErrorTemplate;
42 break; 43 break;
  44 + case types.debugEventType.debugRuleNode.value:
  45 + template = eventRowDebugRuleNodeTemplate;
  46 + break;
  47 + case types.debugEventType.debugRuleChain.value:
  48 + template = eventRowDebugRuleNodeTemplate;
  49 + break;
43 } 50 }
44 return $templateCache.get(template); 51 return $templateCache.get(template);
45 } 52 }
@@ -53,17 +60,22 @@ export default function EventRowDirective($compile, $templateCache, $mdDialog, $ @@ -53,17 +60,22 @@ export default function EventRowDirective($compile, $templateCache, $mdDialog, $
53 scope.loadTemplate(); 60 scope.loadTemplate();
54 }); 61 });
55 62
  63 + scope.types = types;
  64 +
56 scope.event = attrs.event; 65 scope.event = attrs.event;
57 66
58 - scope.showContent = function($event, content, title) { 67 + scope.showContent = function($event, content, title, contentType) {
59 var onShowingCallback = { 68 var onShowingCallback = {
60 onShowing: function(){} 69 onShowing: function(){}
61 } 70 }
  71 + if (!contentType) {
  72 + contentType = null;
  73 + }
62 $mdDialog.show({ 74 $mdDialog.show({
63 controller: 'EventContentDialogController', 75 controller: 'EventContentDialogController',
64 controllerAs: 'vm', 76 controllerAs: 'vm',
65 templateUrl: eventErrorDialogTemplate, 77 templateUrl: eventErrorDialogTemplate,
66 - locals: {content: content, title: title, showingCallback: onShowingCallback}, 78 + locals: {content: content, title: title, contentType: contentType, showingCallback: onShowingCallback},
67 parent: angular.element($document[0].body), 79 parent: angular.element($document[0].body),
68 fullscreen: true, 80 fullscreen: true,
69 targetEvent: $event, 81 targetEvent: $event,
@@ -36,8 +36,8 @@ export default function EventTableDirective($compile, $templateCache, $rootScope @@ -36,8 +36,8 @@ export default function EventTableDirective($compile, $templateCache, $rootScope
36 for (var type in types.eventType) { 36 for (var type in types.eventType) {
37 var eventType = types.eventType[type]; 37 var eventType = types.eventType[type];
38 var enabled = true; 38 var enabled = true;
39 - for (var disabledType in disabledEventTypes) {  
40 - if (eventType.value === disabledEventTypes[disabledType]) { 39 + for (var i=0;i<disabledEventTypes.length;i++) {
  40 + if (eventType.value === disabledEventTypes[i]) {
41 enabled = false; 41 enabled = false;
42 break; 42 break;
43 } 43 }
@@ -47,7 +47,19 @@ export default function EventTableDirective($compile, $templateCache, $rootScope @@ -47,7 +47,19 @@ export default function EventTableDirective($compile, $templateCache, $rootScope
47 } 47 }
48 } 48 }
49 } else { 49 } else {
50 - scope.eventTypes = types.eventType; 50 + scope.eventTypes = angular.copy(types.eventType);
  51 + }
  52 +
  53 + if (attrs.debugEventTypes) {
  54 + var debugEventTypes = attrs.debugEventTypes.split(',');
  55 + for (i=0;i<debugEventTypes.length;i++) {
  56 + for (type in types.debugEventType) {
  57 + eventType = types.debugEventType[type];
  58 + if (eventType.value === debugEventTypes[i]) {
  59 + scope.eventTypes[type] = eventType;
  60 + }
  61 + }
  62 + }
51 } 63 }
52 64
53 scope.eventType = attrs.defaultEventType; 65 scope.eventType = attrs.defaultEventType;
@@ -341,6 +341,11 @@ export default angular.module('thingsboard.locale', []) @@ -341,6 +341,11 @@ export default angular.module('thingsboard.locale', [])
341 "enter-password": "Enter password", 341 "enter-password": "Enter password",
342 "enter-search": "Enter search" 342 "enter-search": "Enter search"
343 }, 343 },
  344 + "content-type": {
  345 + "json": "Json",
  346 + "text": "Text",
  347 + "binary": "Binary (Base64)"
  348 + },
344 "customer": { 349 "customer": {
345 "customer": "Customer", 350 "customer": "Customer",
346 "customers": "Customers", 351 "customers": "Customers",
@@ -762,6 +767,8 @@ export default angular.module('thingsboard.locale', []) @@ -762,6 +767,8 @@ export default angular.module('thingsboard.locale', [])
762 "type-error": "Error", 767 "type-error": "Error",
763 "type-lc-event": "Lifecycle event", 768 "type-lc-event": "Lifecycle event",
764 "type-stats": "Statistics", 769 "type-stats": "Statistics",
  770 + "type-debug-rule-node": "Debug",
  771 + "type-debug-rule-chain": "Debug",
765 "no-events-prompt": "No events found", 772 "no-events-prompt": "No events found",
766 "error": "Error", 773 "error": "Error",
767 "alarm": "Alarm", 774 "alarm": "Alarm",
@@ -769,6 +776,13 @@ export default angular.module('thingsboard.locale', []) @@ -769,6 +776,13 @@ export default angular.module('thingsboard.locale', [])
769 "server": "Server", 776 "server": "Server",
770 "body": "Body", 777 "body": "Body",
771 "method": "Method", 778 "method": "Method",
  779 + "type": "Type",
  780 + "entity": "Entity",
  781 + "message-id": "Message Id",
  782 + "message-type": "Message Type",
  783 + "data-type": "Data Type",
  784 + "metadata": "Metadata",
  785 + "data": "Data",
772 "event": "Event", 786 "event": "Event",
773 "status": "Status", 787 "status": "Status",
774 "success": "Success", 788 "success": "Success",
@@ -1171,12 +1185,18 @@ export default angular.module('thingsboard.locale', []) @@ -1171,12 +1185,18 @@ export default angular.module('thingsboard.locale', [])
1171 "debug-mode": "Debug mode" 1185 "debug-mode": "Debug mode"
1172 }, 1186 },
1173 "rulenode": { 1187 "rulenode": {
  1188 + "details": "Details",
  1189 + "events": "Events",
  1190 + "search": "Search nodes",
1174 "add": "Add rule node", 1191 "add": "Add rule node",
1175 "name": "Name", 1192 "name": "Name",
1176 "name-required": "Name is required.", 1193 "name-required": "Name is required.",
1177 "type": "Type", 1194 "type": "Type",
1178 "description": "Description", 1195 "description": "Description",
1179 "delete": "Delete rule node", 1196 "delete": "Delete rule node",
  1197 + "select-all": "Select all nodes and connections",
  1198 + "deselect-all": "Deselect all nodes and connections",
  1199 + "delete-selected-objects": "Delete selected nodes and connections",
1180 "rulenode-details": "Rule node details", 1200 "rulenode-details": "Rule node details",
1181 "debug-mode": "Debug mode", 1201 "debug-mode": "Debug mode",
1182 "configuration": "Configuration", 1202 "configuration": "Configuration",
@@ -1195,7 +1215,9 @@ export default angular.module('thingsboard.locale', []) @@ -1195,7 +1215,9 @@ export default angular.module('thingsboard.locale', [])
1195 "type-action": "Action", 1215 "type-action": "Action",
1196 "type-action-details": "Perform special action", 1216 "type-action-details": "Perform special action",
1197 "type-rule-chain": "Rule Chain", 1217 "type-rule-chain": "Rule Chain",
1198 - "type-rule-chain-details": "Forwards incoming messages to specified Rule Chain" 1218 + "type-rule-chain-details": "Forwards incoming messages to specified Rule Chain",
  1219 + "directive-is-not-loaded": "Defined configuration directive '{{directiveName}}' is not available.",
  1220 + "ui-resources-load-error": "Failed to load configuration ui resources."
1199 }, 1221 },
1200 "rule-plugin": { 1222 "rule-plugin": {
1201 "management": "Rules and plugins management" 1223 "management": "Rules and plugins management"
@@ -18,6 +18,8 @@ import RuleChainRoutes from './rulechain.routes'; @@ -18,6 +18,8 @@ import RuleChainRoutes from './rulechain.routes';
18 import RuleChainsController from './rulechains.controller'; 18 import RuleChainsController from './rulechains.controller';
19 import {RuleChainController, AddRuleNodeController, AddRuleNodeLinkController} from './rulechain.controller'; 19 import {RuleChainController, AddRuleNodeController, AddRuleNodeLinkController} from './rulechain.controller';
20 import RuleChainDirective from './rulechain.directive'; 20 import RuleChainDirective from './rulechain.directive';
  21 +import RuleNodeDefinedConfigDirective from './rulenode-defined-config.directive';
  22 +import RuleNodeConfigDirective from './rulenode-config.directive';
21 import RuleNodeDirective from './rulenode.directive'; 23 import RuleNodeDirective from './rulenode.directive';
22 import LinkDirective from './link.directive'; 24 import LinkDirective from './link.directive';
23 25
@@ -28,6 +30,8 @@ export default angular.module('thingsboard.ruleChain', []) @@ -28,6 +30,8 @@ export default angular.module('thingsboard.ruleChain', [])
28 .controller('AddRuleNodeController', AddRuleNodeController) 30 .controller('AddRuleNodeController', AddRuleNodeController)
29 .controller('AddRuleNodeLinkController', AddRuleNodeLinkController) 31 .controller('AddRuleNodeLinkController', AddRuleNodeLinkController)
30 .directive('tbRuleChain', RuleChainDirective) 32 .directive('tbRuleChain', RuleChainDirective)
  33 + .directive('tbRuleNodeDefinedConfig', RuleNodeDefinedConfigDirective)
  34 + .directive('tbRuleNodeConfig', RuleNodeConfigDirective)
31 .directive('tbRuleNode', RuleNodeDirective) 35 .directive('tbRuleNode', RuleNodeDirective)
32 .directive('tbRuleNodeLink', LinkDirective) 36 .directive('tbRuleNodeLink', LinkDirective)
33 .name; 37 .name;
@@ -27,15 +27,10 @@ import addRuleNodeLinkTemplate from './add-link.tpl.html'; @@ -27,15 +27,10 @@ import addRuleNodeLinkTemplate from './add-link.tpl.html';
27 27
28 /* eslint-enable import/no-unresolved, import/default */ 28 /* eslint-enable import/no-unresolved, import/default */
29 29
30 -  
31 -const deleteKeyCode = 46;  
32 -const ctrlKeyCode = 17;  
33 -const aKeyCode = 65;  
34 -const escKeyCode = 27;  
35 -  
36 /*@ngInject*/ 30 /*@ngInject*/
37 -export function RuleChainController($stateParams, $scope, $compile, $q, $mdUtil, $timeout, $mdExpansionPanel, $document, $mdDialog,  
38 - $filter, $translate, types, ruleChainService, Modelfactory, flowchartConstants, ruleChain, ruleChainMetaData) { 31 +export function RuleChainController($stateParams, $scope, $compile, $q, $mdUtil, $timeout, $mdExpansionPanel, $window, $document, $mdDialog,
  32 + $filter, $translate, hotkeys, types, ruleChainService, Modelfactory, flowchartConstants,
  33 + ruleChain, ruleChainMetaData, ruleNodeComponents) {
39 34
40 var vm = this; 35 var vm = this;
41 36
@@ -48,6 +43,9 @@ export function RuleChainController($stateParams, $scope, $compile, $q, $mdUtil, @@ -48,6 +43,9 @@ export function RuleChainController($stateParams, $scope, $compile, $q, $mdUtil,
48 vm.editingRuleNodeLink = null; 43 vm.editingRuleNodeLink = null;
49 vm.isEditingRuleNodeLink = false; 44 vm.isEditingRuleNodeLink = false;
50 45
  46 + vm.isLibraryOpen = true;
  47 + vm.ruleNodeSearch = '';
  48 +
51 vm.ruleChain = ruleChain; 49 vm.ruleChain = ruleChain;
52 vm.ruleChainMetaData = ruleChainMetaData; 50 vm.ruleChainMetaData = ruleChainMetaData;
53 51
@@ -76,39 +74,64 @@ export function RuleChainController($stateParams, $scope, $compile, $q, $mdUtil, @@ -76,39 +74,64 @@ export function RuleChainController($stateParams, $scope, $compile, $q, $mdUtil,
76 74
77 vm.modelservice = Modelfactory(vm.ruleChainModel, vm.selectedObjects); 75 vm.modelservice = Modelfactory(vm.ruleChainModel, vm.selectedObjects);
78 76
79 - vm.ctrlDown = false;  
80 -  
81 vm.saveRuleChain = saveRuleChain; 77 vm.saveRuleChain = saveRuleChain;
82 vm.revertRuleChain = revertRuleChain; 78 vm.revertRuleChain = revertRuleChain;
83 79
84 - vm.keyDown = function (evt) {  
85 - if (evt.keyCode === ctrlKeyCode) {  
86 - vm.ctrlDown = true;  
87 - evt.stopPropagation();  
88 - evt.preventDefault();  
89 - }  
90 - };  
91 -  
92 - vm.keyUp = function (evt) { 80 + vm.objectsSelected = objectsSelected;
  81 + vm.deleteSelected = deleteSelected;
93 82
94 - if (evt.keyCode === deleteKeyCode) {  
95 - vm.modelservice.deleteSelected();  
96 - }  
97 -  
98 - if (evt.keyCode == aKeyCode && vm.ctrlDown) {  
99 - vm.modelservice.selectAll();  
100 - } 83 + vm.triggerResize = triggerResize;
101 84
102 - if (evt.keyCode == escKeyCode) {  
103 - vm.modelservice.deselectAll();  
104 - } 85 + initHotKeys();
105 86
106 - if (evt.keyCode === ctrlKeyCode) {  
107 - vm.ctrlDown = false;  
108 - evt.stopPropagation();  
109 - evt.preventDefault();  
110 - }  
111 - }; 87 + function initHotKeys() {
  88 + hotkeys.bindTo($scope)
  89 + .add({
  90 + combo: 'ctrl+a',
  91 + description: $translate.instant('rulenode.select-all'),
  92 + allowIn: ['INPUT', 'SELECT', 'TEXTAREA'],
  93 + callback: function (event) {
  94 + event.preventDefault();
  95 + vm.modelservice.selectAll();
  96 + }
  97 + })
  98 + .add({
  99 + combo: 'esc',
  100 + description: $translate.instant('rulenode.deselect-all'),
  101 + allowIn: ['INPUT', 'SELECT', 'TEXTAREA'],
  102 + callback: function (event) {
  103 + event.preventDefault();
  104 + vm.modelservice.deselectAll();
  105 + }
  106 + })
  107 + .add({
  108 + combo: 'ctrl+s',
  109 + description: $translate.instant('action.apply'),
  110 + allowIn: ['INPUT', 'SELECT', 'TEXTAREA'],
  111 + callback: function (event) {
  112 + event.preventDefault();
  113 + vm.saveRuleChain();
  114 + }
  115 + })
  116 + .add({
  117 + combo: 'ctrl+z',
  118 + description: $translate.instant('action.decline-changes'),
  119 + allowIn: ['INPUT', 'SELECT', 'TEXTAREA'],
  120 + callback: function (event) {
  121 + event.preventDefault();
  122 + vm.revertRuleChain();
  123 + }
  124 + })
  125 + .add({
  126 + combo: 'del',
  127 + description: $translate.instant('rulenode.delete-selected-objects'),
  128 + allowIn: ['INPUT', 'SELECT', 'TEXTAREA'],
  129 + callback: function (event) {
  130 + event.preventDefault();
  131 + vm.modelservice.deleteSelected();
  132 + }
  133 + })
  134 + }
112 135
113 vm.onEditRuleNodeClosed = function() { 136 vm.onEditRuleNodeClosed = function() {
114 vm.editingRuleNode = null; 137 vm.editingRuleNode = null;
@@ -119,15 +142,16 @@ export function RuleChainController($stateParams, $scope, $compile, $q, $mdUtil, @@ -119,15 +142,16 @@ export function RuleChainController($stateParams, $scope, $compile, $q, $mdUtil,
119 }; 142 };
120 143
121 vm.saveRuleNode = function(theForm) { 144 vm.saveRuleNode = function(theForm) {
122 - theForm.$setPristine();  
123 - vm.isEditingRuleNode = false;  
124 - vm.ruleChainModel.nodes[vm.editingRuleNodeIndex] = vm.editingRuleNode;  
125 - vm.editingRuleNode = angular.copy(vm.editingRuleNode); 145 + $scope.$broadcast('form-submit');
  146 + if (theForm.$valid) {
  147 + theForm.$setPristine();
  148 + vm.ruleChainModel.nodes[vm.editingRuleNodeIndex] = vm.editingRuleNode;
  149 + vm.editingRuleNode = angular.copy(vm.editingRuleNode);
  150 + }
126 }; 151 };
127 152
128 vm.saveRuleNodeLink = function(theForm) { 153 vm.saveRuleNodeLink = function(theForm) {
129 theForm.$setPristine(); 154 theForm.$setPristine();
130 - vm.isEditingRuleNodeLink = false;  
131 vm.ruleChainModel.edges[vm.editingRuleNodeLinkIndex] = vm.editingRuleNodeLink; 155 vm.ruleChainModel.edges[vm.editingRuleNodeLinkIndex] = vm.editingRuleNodeLink;
132 vm.editingRuleNodeLink = angular.copy(vm.editingRuleNodeLink); 156 vm.editingRuleNodeLink = angular.copy(vm.editingRuleNodeLink);
133 }; 157 };
@@ -235,6 +259,9 @@ export function RuleChainController($stateParams, $scope, $compile, $q, $mdUtil, @@ -235,6 +259,9 @@ export function RuleChainController($stateParams, $scope, $compile, $q, $mdUtil,
235 vm.isEditingRuleNodeLink = true; 259 vm.isEditingRuleNodeLink = true;
236 vm.editingRuleNodeLinkIndex = vm.ruleChainModel.edges.indexOf(edge); 260 vm.editingRuleNodeLinkIndex = vm.ruleChainModel.edges.indexOf(edge);
237 vm.editingRuleNodeLink = angular.copy(edge); 261 vm.editingRuleNodeLink = angular.copy(edge);
  262 + $mdUtil.nextTick(() => {
  263 + vm.ruleNodeLinkForm.$setPristine();
  264 + });
238 } 265 }
239 }, 266 },
240 nodeCallbacks: { 267 nodeCallbacks: {
@@ -245,6 +272,9 @@ export function RuleChainController($stateParams, $scope, $compile, $q, $mdUtil, @@ -245,6 +272,9 @@ export function RuleChainController($stateParams, $scope, $compile, $q, $mdUtil,
245 vm.isEditingRuleNode = true; 272 vm.isEditingRuleNode = true;
246 vm.editingRuleNodeIndex = vm.ruleChainModel.nodes.indexOf(node); 273 vm.editingRuleNodeIndex = vm.ruleChainModel.nodes.indexOf(node);
247 vm.editingRuleNode = angular.copy(node); 274 vm.editingRuleNode = angular.copy(node);
  275 + $mdUtil.nextTick(() => {
  276 + vm.ruleNodeForm.$setPristine();
  277 + });
248 } 278 }
249 } 279 }
250 }, 280 },
@@ -286,44 +316,40 @@ export function RuleChainController($stateParams, $scope, $compile, $q, $mdUtil, @@ -286,44 +316,40 @@ export function RuleChainController($stateParams, $scope, $compile, $q, $mdUtil,
286 loadRuleChainLibrary(); 316 loadRuleChainLibrary();
287 317
288 function loadRuleChainLibrary() { 318 function loadRuleChainLibrary() {
289 - ruleChainService.getRuleNodeComponents().then(  
290 - (ruleNodeComponents) => {  
291 - for (var i=0;i<ruleNodeComponents.length;i++) {  
292 - var ruleNodeComponent = ruleNodeComponents[i];  
293 - var componentType = ruleNodeComponent.type;  
294 - var model = vm.ruleNodeTypesModel[componentType].model;  
295 - var node = {  
296 - id: model.nodes.length,  
297 - component: ruleNodeComponent,  
298 - name: '',  
299 - nodeClass: vm.types.ruleNodeType[componentType].nodeClass,  
300 - icon: vm.types.ruleNodeType[componentType].icon,  
301 - x: 30,  
302 - y: 10+50*model.nodes.length,  
303 - connectors: []  
304 - };  
305 - if (ruleNodeComponent.configurationDescriptor.nodeDefinition.inEnabled) {  
306 - node.connectors.push(  
307 - {  
308 - type: flowchartConstants.leftConnectorType,  
309 - id: model.nodes.length * 2  
310 - }  
311 - ); 319 + for (var i=0;i<ruleNodeComponents.length;i++) {
  320 + var ruleNodeComponent = ruleNodeComponents[i];
  321 + var componentType = ruleNodeComponent.type;
  322 + var model = vm.ruleNodeTypesModel[componentType].model;
  323 + var node = {
  324 + id: 'node-lib-' + componentType + '-' + model.nodes.length,
  325 + component: ruleNodeComponent,
  326 + name: '',
  327 + nodeClass: vm.types.ruleNodeType[componentType].nodeClass,
  328 + icon: vm.types.ruleNodeType[componentType].icon,
  329 + x: 30,
  330 + y: 10+50*model.nodes.length,
  331 + connectors: []
  332 + };
  333 + if (ruleNodeComponent.configurationDescriptor.nodeDefinition.inEnabled) {
  334 + node.connectors.push(
  335 + {
  336 + type: flowchartConstants.leftConnectorType,
  337 + id: model.nodes.length * 2
312 } 338 }
313 - if (ruleNodeComponent.configurationDescriptor.nodeDefinition.outEnabled) {  
314 - node.connectors.push(  
315 - {  
316 - type: flowchartConstants.rightConnectorType,  
317 - id: model.nodes.length * 2 + 1  
318 - }  
319 - ); 339 + );
  340 + }
  341 + if (ruleNodeComponent.configurationDescriptor.nodeDefinition.outEnabled) {
  342 + node.connectors.push(
  343 + {
  344 + type: flowchartConstants.rightConnectorType,
  345 + id: model.nodes.length * 2 + 1
320 } 346 }
321 - model.nodes.push(node);  
322 - }  
323 - vm.ruleChainLibraryLoaded = true;  
324 - prepareRuleChain(); 347 + );
325 } 348 }
326 - ); 349 + model.nodes.push(node);
  350 + }
  351 + vm.ruleChainLibraryLoaded = true;
  352 + prepareRuleChain();
327 } 353 }
328 354
329 function prepareRuleChain() { 355 function prepareRuleChain() {
@@ -344,7 +370,7 @@ export function RuleChainController($stateParams, $scope, $compile, $q, $mdUtil, @@ -344,7 +370,7 @@ export function RuleChainController($stateParams, $scope, $compile, $q, $mdUtil,
344 370
345 vm.ruleChainModel.nodes.push( 371 vm.ruleChainModel.nodes.push(
346 { 372 {
347 - id: vm.nextNodeID++, 373 + id: 'rule-chain-node-' + vm.nextNodeID++,
348 component: types.inputNodeComponent, 374 component: types.inputNodeComponent,
349 name: "", 375 name: "",
350 nodeClass: types.ruleNodeType.INPUT.nodeClass, 376 nodeClass: types.ruleNodeType.INPUT.nodeClass,
@@ -375,7 +401,7 @@ export function RuleChainController($stateParams, $scope, $compile, $q, $mdUtil, @@ -375,7 +401,7 @@ export function RuleChainController($stateParams, $scope, $compile, $q, $mdUtil,
375 var component = ruleChainService.getRuleNodeComponentByClazz(ruleNode.type); 401 var component = ruleChainService.getRuleNodeComponentByClazz(ruleNode.type);
376 if (component) { 402 if (component) {
377 var node = { 403 var node = {
378 - id: vm.nextNodeID++, 404 + id: 'rule-chain-node-' + vm.nextNodeID++,
379 ruleNodeId: ruleNode.id, 405 ruleNodeId: ruleNode.id,
380 additionalInfo: ruleNode.additionalInfo, 406 additionalInfo: ruleNode.additionalInfo,
381 configuration: ruleNode.configuration, 407 configuration: ruleNode.configuration,
@@ -452,7 +478,7 @@ export function RuleChainController($stateParams, $scope, $compile, $q, $mdUtil, @@ -452,7 +478,7 @@ export function RuleChainController($stateParams, $scope, $compile, $q, $mdUtil,
452 var ruleChainNode = ruleChainNodesMap[ruleChainConnection.additionalInfo.ruleChainNodeId]; 478 var ruleChainNode = ruleChainNodesMap[ruleChainConnection.additionalInfo.ruleChainNodeId];
453 if (!ruleChainNode) { 479 if (!ruleChainNode) {
454 ruleChainNode = { 480 ruleChainNode = {
455 - id: vm.nextNodeID++, 481 + id: 'rule-chain-node-' + vm.nextNodeID++,
456 additionalInfo: ruleChainConnection.additionalInfo, 482 additionalInfo: ruleChainConnection.additionalInfo,
457 targetRuleChainId: ruleChainConnection.targetRuleChainId.id, 483 targetRuleChainId: ruleChainConnection.targetRuleChainId.id,
458 x: ruleChainConnection.additionalInfo.layoutX, 484 x: ruleChainConnection.additionalInfo.layoutX,
@@ -597,7 +623,7 @@ export function RuleChainController($stateParams, $scope, $compile, $q, $mdUtil, @@ -597,7 +623,7 @@ export function RuleChainController($stateParams, $scope, $compile, $q, $mdUtil,
597 fullscreen: true, 623 fullscreen: true,
598 targetEvent: $event 624 targetEvent: $event
599 }).then(function (ruleNode) { 625 }).then(function (ruleNode) {
600 - ruleNode.id = vm.nextNodeID++; 626 + ruleNode.id = 'rule-chain-node-' + vm.nextNodeID++;
601 ruleNode.connectors = []; 627 ruleNode.connectors = [];
602 if (ruleNode.component.configurationDescriptor.nodeDefinition.inEnabled) { 628 if (ruleNode.component.configurationDescriptor.nodeDefinition.inEnabled) {
603 ruleNode.connectors.push( 629 ruleNode.connectors.push(
@@ -632,6 +658,19 @@ export function RuleChainController($stateParams, $scope, $compile, $q, $mdUtil, @@ -632,6 +658,19 @@ export function RuleChainController($stateParams, $scope, $compile, $q, $mdUtil,
632 }); 658 });
633 } 659 }
634 660
  661 + function objectsSelected() {
  662 + return vm.modelservice.nodes.getSelectedNodes().length > 0 ||
  663 + vm.modelservice.edges.getSelectedEdges().length > 0
  664 + }
  665 +
  666 + function deleteSelected() {
  667 + vm.modelservice.deleteSelected();
  668 + }
  669 +
  670 + function triggerResize() {
  671 + var w = angular.element($window);
  672 + w.triggerHandler('resize');
  673 + }
635 } 674 }
636 675
637 /*@ngInject*/ 676 /*@ngInject*/
@@ -68,6 +68,11 @@ export default function RuleChainRoutes($stateProvider, NodeTemplatePathProvider @@ -68,6 +68,11 @@ export default function RuleChainRoutes($stateProvider, NodeTemplatePathProvider
68 /*@ngInject*/ 68 /*@ngInject*/
69 function($stateParams, ruleChainService) { 69 function($stateParams, ruleChainService) {
70 return ruleChainService.getRuleChainMetaData($stateParams.ruleChainId); 70 return ruleChainService.getRuleChainMetaData($stateParams.ruleChainId);
  71 + },
  72 + ruleNodeComponents:
  73 + /*@ngInject*/
  74 + function($stateParams, ruleChainService) {
  75 + return ruleChainService.getRuleNodeComponents();
71 } 76 }
72 }, 77 },
73 data: { 78 data: {
@@ -18,13 +18,58 @@ @@ -18,13 +18,58 @@
18 .tb-fullscreen-button-style { 18 .tb-fullscreen-button-style {
19 z-index: 1; 19 z-index: 1;
20 } 20 }
  21 + section.tb-header-buttons.tb-library-open {
  22 + pointer-events: none;
  23 + position: absolute;
  24 + left: 0px;
  25 + top: 0px;
  26 + z-index: 1;
  27 + .md-button.tb-btn-open-library {
  28 + left: 0px;
  29 + top: 0px;
  30 + line-height: 36px;
  31 + width: 36px;
  32 + height: 36px;
  33 + margin: 4px 0 0 4px;
  34 + opacity: 0.5;
  35 + }
  36 + }
21 .tb-rulechain-library { 37 .tb-rulechain-library {
22 width: 250px; 38 width: 250px;
23 min-width: 250px; 39 min-width: 250px;
24 - overflow-y: auto;  
25 - overflow-x: hidden;  
26 - 40 + z-index: 1;
  41 + md-toolbar {
  42 + min-height: 48px;
  43 + height: 48px;
  44 + .md-toolbar-tools>.md-button:last-child {
  45 + margin-right: 0px;
  46 + }
  47 + .md-toolbar-tools {
  48 + font-size: 14px;
  49 + padding: 0px 6px;
  50 + .md-button.md-icon-button {
  51 + margin: 0px;
  52 + &.tb-small {
  53 + height: 32px;
  54 + min-height: 32px;
  55 + line-height: 20px;
  56 + padding: 6px;
  57 + width: 32px;
  58 + md-icon {
  59 + line-height: 20px;
  60 + font-size: 20px;
  61 + height: 20px;
  62 + width: 20px;
  63 + min-height: 20px;
  64 + min-width: 20px;
  65 + }
  66 + }
  67 + }
  68 + }
  69 + }
27 .tb-rulechain-library-panel-group { 70 .tb-rulechain-library-panel-group {
  71 + overflow-y: auto;
  72 + overflow-x: hidden;
28 .tb-panel-title { 73 .tb-panel-title {
29 -webkit-user-select: none; 74 -webkit-user-select: none;
30 -moz-user-select: none; 75 -moz-user-select: none;
@@ -33,7 +78,7 @@ @@ -33,7 +78,7 @@
33 min-width: 180px; 78 min-width: 180px;
34 } 79 }
35 .fc-canvas { 80 .fc-canvas {
36 - background: none; 81 + background: #f9f9f9;
37 } 82 }
38 md-icon.md-expansion-panel-icon { 83 md-icon.md-expansion-panel-icon {
39 margin-right: 0px; 84 margin-right: 0px;
@@ -55,6 +100,7 @@ @@ -55,6 +100,7 @@
55 } 100 }
56 } 101 }
57 .tb-rulechain-graph { 102 .tb-rulechain-graph {
  103 + z-index: 0;
58 overflow: auto; 104 overflow: auto;
59 } 105 }
60 } 106 }
@@ -75,6 +121,7 @@ @@ -75,6 +121,7 @@
75 padding: 5px 10px; 121 padding: 5px 10px;
76 border-radius: 5px; 122 border-radius: 5px;
77 background-color: #F15B26; 123 background-color: #F15B26;
  124 + pointer-events: none;
78 color: #333; 125 color: #333;
79 border: solid 1px #777; 126 border: solid 1px #777;
80 font-size: 12px; 127 font-size: 12px;
@@ -121,10 +168,6 @@ @@ -121,10 +168,6 @@
121 .fc-node { 168 .fc-node {
122 z-index: 1; 169 z-index: 1;
123 outline: none; 170 outline: none;
124 - &.fc-hover, &.fc-selected {  
125 - -webkit-filter: brightness(70%);  
126 - filter: brightness(70%);  
127 - }  
128 &.fc-dragging { 171 &.fc-dragging {
129 z-index: 10; 172 z-index: 10;
130 } 173 }
@@ -132,6 +175,26 @@ @@ -132,6 +175,26 @@
132 padding: 0 15px; 175 padding: 0 15px;
133 text-align: center; 176 text-align: center;
134 } 177 }
  178 + .fc-node-overlay {
  179 + position: absolute;
  180 + pointer-events: none;
  181 + left: 0;
  182 + top: 0;
  183 + right: 0;
  184 + bottom: 0;
  185 + background-color: #000;
  186 + opacity: 0;
  187 + }
  188 + &.fc-hover {
  189 + .fc-node-overlay {
  190 + opacity: 0.25;
  191 + }
  192 + }
  193 + &.fc-selected {
  194 + .fc-node-overlay {
  195 + opacity: 0.25;
  196 + }
  197 + }
135 } 198 }
136 199
137 .fc-leftConnectors, .fc-rightConnectors { 200 .fc-leftConnectors, .fc-rightConnectors {
@@ -170,17 +233,33 @@ @@ -170,17 +233,33 @@
170 margin: 10px; 233 margin: 10px;
171 border-radius: 5px; 234 border-radius: 5px;
172 background-color: #ccc; 235 background-color: #ccc;
  236 + pointer-events: all;
173 } 237 }
174 238
175 .fc-connector.fc-hover { 239 .fc-connector.fc-hover {
176 background-color: #000; 240 background-color: #000;
177 } 241 }
178 242
  243 +.fc-arrow-marker {
  244 + polygon {
  245 + stroke: gray;
  246 + fill: gray;
  247 + }
  248 +}
  249 +
  250 +.fc-arrow-marker-selected {
  251 + polygon {
  252 + stroke: red;
  253 + fill: red;
  254 + }
  255 +}
  256 +
179 .fc-edge { 257 .fc-edge {
180 outline: none; 258 outline: none;
181 stroke: gray; 259 stroke: gray;
182 stroke-width: 4; 260 stroke-width: 4;
183 fill: transparent; 261 fill: transparent;
  262 + transition: stroke-width .2s;
184 &.fc-selected { 263 &.fc-selected {
185 stroke: red; 264 stroke: red;
186 stroke-width: 4; 265 stroke-width: 4;
@@ -229,24 +308,53 @@ @@ -229,24 +308,53 @@
229 cursor: pointer; 308 cursor: pointer;
230 } 309 }
231 310
  311 +.fc-noselect {
  312 + -webkit-touch-callout: none; /* iOS Safari */
  313 + -webkit-user-select: none; /* Safari */
  314 + -khtml-user-select: none; /* Konqueror HTML */
  315 + -moz-user-select: none; /* Firefox */
  316 + -ms-user-select: none; /* Internet Explorer/Edge */
  317 + user-select: none; /* Non-prefixed version, currently
  318 + supported by Chrome and Opera */
  319 +}
  320 +
232 .fc-edge-label { 321 .fc-edge-label {
233 position: absolute; 322 position: absolute;
234 - user-select: none;  
235 - pointer-events: none; 323 + transition: transform .2s;
236 opacity: 0.8; 324 opacity: 0.8;
  325 + &.ng-leave {
  326 + transition: 0s none;
  327 + }
  328 + &.fc-hover {
  329 + transform: scale(1.25);
  330 + }
  331 + &.fc-selected {
  332 + .fc-edge-label-text {
  333 + span {
  334 + border: solid red;
  335 + color: red;
  336 + }
  337 + }
  338 + }
  339 + .fc-nodedelete {
  340 + right: -13px;
  341 + top: -30px;
  342 + }
  343 + &:focus {
  344 + outline: 0;
  345 + }
237 } 346 }
238 347
239 .fc-edge-label-text { 348 .fc-edge-label-text {
240 position: absolute; 349 position: absolute;
241 - left: 50%;  
242 - -webkit-transform: translateX(-50%);  
243 - transform: translateX(-50%); 350 + -webkit-transform: translate(-50%, -50%);
  351 + transform: translate(-50%, -50%);
244 white-space: nowrap; 352 white-space: nowrap;
245 text-align: center; 353 text-align: center;
246 font-size: 14px; 354 font-size: 14px;
247 font-weight: 600; 355 font-weight: 600;
248 - top: 5px;  
249 span { 356 span {
  357 + cursor: default;
250 border: solid 2px #003a79; 358 border: solid 2px #003a79;
251 border-radius: 10px; 359 border-radius: 10px;
252 color: #003a79; 360 color: #003a79;
@@ -255,6 +363,13 @@ @@ -255,6 +363,13 @@
255 } 363 }
256 } 364 }
257 365
  366 +.fc-select-rectangle {
  367 + border: 2px dashed #5262ff;
  368 + position: absolute;
  369 + background: rgba(20,125,255,0.1);
  370 + z-index: 2;
  371 +}
  372 +
258 @keyframes dash { 373 @keyframes dash {
259 from { 374 from {
260 stroke-dashoffset: 500; 375 stroke-dashoffset: 500;
@@ -16,12 +16,60 @@ @@ -16,12 +16,60 @@
16 16
17 --> 17 -->
18 18
19 -<md-content flex tb-expand-fullscreen  
20 - expand-tooltip-direction="bottom" layout="column" class="tb-rulechain"> 19 +<md-content flex tb-expand-fullscreen tb-confirm-on-exit is-dirty="vm.isDirty"
  20 + expand-tooltip-direction="bottom" layout="column" class="tb-rulechain"
  21 + ng-keydown="vm.keyDown($event)"
  22 + ng-keyup="vm.keyUp($event)">
21 <section class="tb-rulechain-container" flex layout="column"> 23 <section class="tb-rulechain-container" flex layout="column">
22 <div class="tb-rulechain-layout" flex layout="row"> 24 <div class="tb-rulechain-layout" flex layout="row">
23 - <div class="tb-rulechain-library">  
24 - <md-expansion-panel-group ng-if="vm.ruleChainLibraryLoaded" class="tb-rulechain-library-panel-group" md-component-id="libraryPanelGroup" auto-expand="true" multiple> 25 + <section layout="row" layout-wrap
  26 + class="tb-header-buttons md-fab tb-library-open">
  27 + <md-button ng-show="!vm.isLibraryOpen"
  28 + class="tb-btn-header tb-btn-open-library md-primary md-fab md-fab-top-left"
  29 + aria-label="{{ 'action.apply' | translate }}"
  30 + ng-click="vm.isLibraryOpen = true">
  31 + <md-tooltip md-direction="top">
  32 + {{ 'action.apply-changes' | translate }}
  33 + </md-tooltip>
  34 + <ng-md-icon icon="menu"></ng-md-icon>
  35 + </md-button>
  36 + </section>
  37 + <md-sidenav class="tb-rulechain-library md-sidenav-left md-whiteframe-4dp"
  38 + md-disable-backdrop
  39 + md-is-locked-open="vm.isLibraryOpen"
  40 + md-is-open="vm.isLibraryOpen"
  41 + md-component-id="rulechain-library-sidenav" layout="column">
  42 + <md-toolbar>
  43 + <div class="md-toolbar-tools">
  44 + <md-button class="md-icon-button tb-small" aria-label="{{ 'action.search' | translate }}">
  45 + <md-icon aria-label="{{ 'action.search' | translate }}" class="material-icons">search</md-icon>
  46 + <md-tooltip md-direction="top">
  47 + {{'rulenode.search' | translate}}
  48 + </md-tooltip>
  49 + </md-button>
  50 + <div layout="row" md-theme="tb-dark" flex>
  51 + <md-input-container flex>
  52 + <label>&nbsp;</label>
  53 + <input ng-model="vm.ruleNodeSearch" placeholder="{{'rulenode.search' | translate}}"/>
  54 + </md-input-container>
  55 + </div>
  56 + <md-button class="md-icon-button tb-small" aria-label="Close" ng-click="vm.ruleNodeSearch = ''">
  57 + <md-icon aria-label="Close" class="material-icons">close</md-icon>
  58 + <md-tooltip md-direction="top">
  59 + {{ 'action.close' | translate }}
  60 + </md-tooltip>
  61 + </md-button>
  62 + <md-button class="md-icon-button tb-small" aria-label="Close" ng-click="vm.isLibraryOpen = false">
  63 + <md-icon aria-label="Close" class="material-icons">chevron_left</md-icon>
  64 + <md-tooltip md-direction="top">
  65 + {{ 'action.close' | translate }}
  66 + </md-tooltip>
  67 + </md-button>
  68 + </div>
  69 + </md-toolbar>
  70 + <md-expansion-panel-group flex
  71 + ng-if="vm.ruleChainLibraryLoaded" class="tb-rulechain-library-panel-group"
  72 + md-component-id="libraryPanelGroup" auto-expand="true" multiple>
25 <md-expansion-panel md-component-id="{{typeId}}" id="{{typeId}}" ng-repeat="(typeId, typeModel) in vm.ruleNodeTypesModel"> 73 <md-expansion-panel md-component-id="{{typeId}}" id="{{typeId}}" ng-repeat="(typeId, typeModel) in vm.ruleNodeTypesModel">
26 <md-expansion-panel-collapsed ng-mouseenter="vm.typeHeaderMouseEnter($event, typeId)" 74 <md-expansion-panel-collapsed ng-mouseenter="vm.typeHeaderMouseEnter($event, typeId)"
27 ng-mouseleave="vm.destroyTooltips()"> 75 ng-mouseleave="vm.destroyTooltips()">
@@ -47,11 +95,9 @@ @@ -47,11 +95,9 @@
47 </md-expansion-panel-expanded> 95 </md-expansion-panel-expanded>
48 </md-expansion-panel> 96 </md-expansion-panel>
49 </md-expansion-panel-group> 97 </md-expansion-panel-group>
50 - </div> 98 + </md-sidenav>
51 <div flex class="tb-rulechain-graph"> 99 <div flex class="tb-rulechain-graph">
52 <fc-canvas id="tb-rulchain-canvas" 100 <fc-canvas id="tb-rulchain-canvas"
53 - ng-keydown="vm.keyDown($event)"  
54 - ng-keyup="vm.keyUp($event)"  
55 model="vm.ruleChainModel" 101 model="vm.ruleChainModel"
56 selected-objects="vm.selectedObjects" 102 selected-objects="vm.selectedObjects"
57 edge-style="curved" 103 edge-style="curved"
@@ -65,9 +111,11 @@ @@ -65,9 +111,11 @@
65 </div> 111 </div>
66 <tb-details-sidenav class="tb-rulenode-details-sidenav" 112 <tb-details-sidenav class="tb-rulenode-details-sidenav"
67 header-title="{{vm.editingRuleNode.name}}" 113 header-title="{{vm.editingRuleNode.name}}"
68 - header-subtitle="{{'rulenode.rulenode-details' | translate}}"  
69 - is-read-only="false" 114 + header-subtitle="{{(vm.types.ruleNodeType[vm.editingRuleNode.component.type].name | translate)
  115 + + ' - ' + vm.editingRuleNode.component.name}}"
  116 + is-read-only="vm.selectedRuleNodeTabIndex > 0"
70 is-open="vm.isEditingRuleNode" 117 is-open="vm.isEditingRuleNode"
  118 + tb-enable-backdrop
71 is-always-edit="true" 119 is-always-edit="true"
72 on-close-details="vm.onEditRuleNodeClosed()" 120 on-close-details="vm.onEditRuleNodeClosed()"
73 on-toggle-details-edit-mode="vm.onRevertRuleNodeEdit(vm.ruleNodeForm)" 121 on-toggle-details-edit-mode="vm.onRevertRuleNodeEdit(vm.ruleNodeForm)"
@@ -76,22 +124,37 @@ @@ -76,22 +124,37 @@
76 <details-buttons tb-help="vm.helpLinkIdForRuleNodeType()" help-container-id="help-container"> 124 <details-buttons tb-help="vm.helpLinkIdForRuleNodeType()" help-container-id="help-container">
77 <div id="help-container"></div> 125 <div id="help-container"></div>
78 </details-buttons> 126 </details-buttons>
79 - <form name="vm.ruleNodeForm" ng-if="vm.isEditingRuleNode">  
80 - <tb-rule-node  
81 - rule-node="vm.editingRuleNode"  
82 - rule-chain-id="vm.ruleChain.id.id"  
83 - is-edit="true"  
84 - is-read-only="false"  
85 - on-delete-rule-node="vm.deleteRuleNode(event, vm.editingRuleNode)"  
86 - the-form="vm.ruleNodeForm">  
87 - </tb-rule-node>  
88 - </form> 127 + <md-tabs md-selected="vm.selectedRuleNodeTabIndex"
  128 + id="ruleNodeTabs" md-border-bottom flex class="tb-absolute-fill" ng-if="vm.isEditingRuleNode">
  129 + <md-tab label="{{ 'rulenode.details' | translate }}">
  130 + <form name="vm.ruleNodeForm">
  131 + <tb-rule-node
  132 + rule-node="vm.editingRuleNode"
  133 + rule-chain-id="vm.ruleChain.id.id"
  134 + is-edit="true"
  135 + is-read-only="false"
  136 + on-delete-rule-node="vm.deleteRuleNode(event, vm.editingRuleNode)"
  137 + the-form="vm.ruleNodeForm">
  138 + </tb-rule-node>
  139 + </form>
  140 + </md-tab>
  141 + <md-tab ng-if="vm.isEditingRuleNode && vm.editingRuleNode.ruleNodeId"
  142 + md-on-select="vm.triggerResize()" label="{{ 'rulenode.events' | translate }}">
  143 + <tb-event-table flex entity-type="vm.types.entityType.rulenode"
  144 + entity-id="vm.editingRuleNode.ruleNodeId.id"
  145 + tenant-id="vm.ruleChain.tenantId.id"
  146 + debug-event-types="{{vm.types.debugEventType.debugRuleNode.value}}"
  147 + default-event-type="{{vm.types.debugEventType.debugRuleNode.value}}">
  148 + </tb-event-table>
  149 + </md-tab>
  150 + </md-tabs>
89 </tb-details-sidenav> 151 </tb-details-sidenav>
90 <tb-details-sidenav class="tb-rulenode-link-details-sidenav" 152 <tb-details-sidenav class="tb-rulenode-link-details-sidenav"
91 header-title="{{vm.editingRuleNodeLink.label}}" 153 header-title="{{vm.editingRuleNodeLink.label}}"
92 header-subtitle="{{'rulenode.link-details' | translate}}" 154 header-subtitle="{{'rulenode.link-details' | translate}}"
93 is-read-only="false" 155 is-read-only="false"
94 is-open="vm.isEditingRuleNodeLink" 156 is-open="vm.isEditingRuleNodeLink"
  157 + tb-enable-backdrop
95 is-always-edit="true" 158 is-always-edit="true"
96 on-close-details="vm.onEditRuleNodeLinkClosed()" 159 on-close-details="vm.onEditRuleNodeLinkClosed()"
97 on-toggle-details-edit-mode="vm.onRevertRuleNodeLinkEdit(vm.ruleNodeLinkForm)" 160 on-toggle-details-edit-mode="vm.onRevertRuleNodeLinkEdit(vm.ruleNodeLinkForm)"
@@ -112,6 +175,13 @@ @@ -112,6 +175,13 @@
112 </tb-details-sidenav> 175 </tb-details-sidenav>
113 </section> 176 </section>
114 <section layout="row" layout-wrap class="tb-footer-buttons md-fab" layout-align="start end"> 177 <section layout="row" layout-wrap class="tb-footer-buttons md-fab" layout-align="start end">
  178 + <md-button ng-disabled="$root.loading" ng-show="vm.objectsSelected()" class="tb-btn-footer md-accent md-hue-2 md-fab"
  179 + ng-click="vm.deleteSelected()" aria-label="{{ 'action.delete' | translate }}">
  180 + <md-tooltip md-direction="top">
  181 + {{ 'rulenode.delete-selected-objects' | translate }}
  182 + </md-tooltip>
  183 + <ng-md-icon icon="delete"></ng-md-icon>
  184 + </md-button>
115 <md-button ng-disabled="$root.loading || !vm.isDirty" 185 <md-button ng-disabled="$root.loading || !vm.isDirty"
116 class="tb-btn-footer md-accent md-hue-2 md-fab" 186 class="tb-btn-footer md-accent md-hue-2 md-fab"
117 aria-label="{{ 'action.apply' | translate }}" 187 aria-label="{{ 'action.apply' | translate }}"
@@ -55,7 +55,8 @@ @@ -55,7 +55,8 @@
55 <tb-event-table flex entity-type="vm.types.entityType.rulechain" 55 <tb-event-table flex entity-type="vm.types.entityType.rulechain"
56 entity-id="vm.grid.operatingItem().id.id" 56 entity-id="vm.grid.operatingItem().id.id"
57 tenant-id="vm.grid.operatingItem().tenantId.id" 57 tenant-id="vm.grid.operatingItem().tenantId.id"
58 - default-event-type="{{vm.types.eventType.lcEvent.value}}"> 58 + debug-event-types="{{vm.types.debugEventType.debugRuleChain.value}}"
  59 + default-event-type="{{vm.types.debugEventType.debugRuleChain.value}}">
59 </tb-event-table> 60 </tb-event-table>
60 </md-tab> 61 </md-tab>
61 <md-tab ng-if="!vm.grid.detailsConfig.isDetailsEditMode && vm.isRuleChainEditable(vm.grid.operatingItem())" md-on-select="vm.grid.triggerResize()" label="{{ 'relation.relations' | translate }}"> 62 <md-tab ng-if="!vm.grid.detailsConfig.isDetailsEditMode && vm.isRuleChainEditable(vm.grid.operatingItem())" md-on-select="vm.grid.triggerResize()" label="{{ 'relation.relations' | translate }}">
  1 +/*
  2 + * Copyright © 2016-2018 The Thingsboard Authors
  3 + *
  4 + * Licensed under the Apache License, Version 2.0 (the "License");
  5 + * you may not use this file except in compliance with the License.
  6 + * You may obtain a copy of the License at
  7 + *
  8 + * http://www.apache.org/licenses/LICENSE-2.0
  9 + *
  10 + * Unless required by applicable law or agreed to in writing, software
  11 + * distributed under the License is distributed on an "AS IS" BASIS,
  12 + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  13 + * See the License for the specific language governing permissions and
  14 + * limitations under the License.
  15 + */
  16 +
  17 +/* eslint-disable import/no-unresolved, import/default */
  18 +
  19 +import ruleNodeConfigTemplate from './rulenode-config.tpl.html';
  20 +
  21 +/* eslint-enable import/no-unresolved, import/default */
  22 +
  23 +/*@ngInject*/
  24 +export default function RuleNodeConfigDirective($compile, $templateCache, $injector, $translate) {
  25 +
  26 + var linker = function (scope, element, attrs, ngModelCtrl) {
  27 + var template = $templateCache.get(ruleNodeConfigTemplate);
  28 + element.html(template);
  29 +
  30 + scope.$watch('configuration', function (newVal, prevVal) {
  31 + if (!angular.equals(newVal, prevVal)) {
  32 + ngModelCtrl.$setViewValue(scope.configuration);
  33 + }
  34 + });
  35 +
  36 + ngModelCtrl.$render = function () {
  37 + scope.configuration = ngModelCtrl.$viewValue;
  38 + };
  39 +
  40 + scope.useDefinedDirective = function() {
  41 + return scope.nodeDefinition &&
  42 + scope.nodeDefinition.configDirective && !scope.definedDirectiveError;
  43 + };
  44 +
  45 + scope.$watch('nodeDefinition', () => {
  46 + if (scope.nodeDefinition) {
  47 + validateDefinedDirective();
  48 + }
  49 + });
  50 +
  51 + function validateDefinedDirective() {
  52 + if (scope.nodeDefinition.uiResourceLoadError && scope.nodeDefinition.uiResourceLoadError.length) {
  53 + scope.definedDirectiveError = scope.nodeDefinition.uiResourceLoadError;
  54 + } else {
  55 + var definedDirective = scope.nodeDefinition.configDirective;
  56 + if (definedDirective && definedDirective.length) {
  57 + if (!$injector.has(definedDirective + 'Directive')) {
  58 + scope.definedDirectiveError = $translate.instant('rulenode.directive-is-not-loaded', {directiveName: definedDirective});
  59 + }
  60 + }
  61 + }
  62 + }
  63 +
  64 + $compile(element.contents())(scope);
  65 + };
  66 +
  67 + return {
  68 + restrict: "E",
  69 + require: "^ngModel",
  70 + scope: {
  71 + nodeDefinition:'=',
  72 + required:'=ngRequired',
  73 + readonly:'=ngReadonly'
  74 + },
  75 + link: linker
  76 + };
  77 +
  78 +}
  1 +<!--
  2 +
  3 + Copyright © 2016-2018 The Thingsboard Authors
  4 +
  5 + Licensed under the Apache License, Version 2.0 (the "License");
  6 + you may not use this file except in compliance with the License.
  7 + You may obtain a copy of the License at
  8 +
  9 + http://www.apache.org/licenses/LICENSE-2.0
  10 +
  11 + Unless required by applicable law or agreed to in writing, software
  12 + distributed under the License is distributed on an "AS IS" BASIS,
  13 + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  14 + See the License for the specific language governing permissions and
  15 + limitations under the License.
  16 +
  17 +-->
  18 +
  19 +<tb-rule-node-defined-config ng-if="useDefinedDirective()"
  20 + ng-model="configuration"
  21 + rule-node-directive="{{nodeDefinition.configDirective}}"
  22 + ng-required="required"
  23 + ng-readonly="readonly">
  24 +</tb-rule-node-defined-config>
  25 +<div class="tb-rulenode-directive-error" ng-if="definedDirectiveError">{{definedDirectiveError}}</div>
  26 +<tb-json-object-edit ng-if="!useDefinedDirective()"
  27 + class="tb-rule-node-configuration-json"
  28 + ng-model="configuration"
  29 + label="{{ 'rulenode.configuration' | translate }}"
  30 + ng-required="required"
  31 + fill-height="true">
  32 +</tb-json-object-edit>
  1 +/*
  2 + * Copyright © 2016-2018 The Thingsboard Authors
  3 + *
  4 + * Licensed under the Apache License, Version 2.0 (the "License");
  5 + * you may not use this file except in compliance with the License.
  6 + * You may obtain a copy of the License at
  7 + *
  8 + * http://www.apache.org/licenses/LICENSE-2.0
  9 + *
  10 + * Unless required by applicable law or agreed to in writing, software
  11 + * distributed under the License is distributed on an "AS IS" BASIS,
  12 + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  13 + * See the License for the specific language governing permissions and
  14 + * limitations under the License.
  15 + */
  16 +
  17 +const SNAKE_CASE_REGEXP = /[A-Z]/g;
  18 +
  19 +/*@ngInject*/
  20 +export default function RuleNodeDefinedConfigDirective($compile) {
  21 +
  22 + var linker = function (scope, element, attrs, ngModelCtrl) {
  23 +
  24 + attrs.$observe('ruleNodeDirective', function() {
  25 + loadTemplate();
  26 + });
  27 +
  28 + scope.$watch('configuration', function (newVal, prevVal) {
  29 + if (!angular.equals(newVal, prevVal)) {
  30 + ngModelCtrl.$setViewValue(scope.configuration);
  31 + }
  32 + });
  33 +
  34 + ngModelCtrl.$render = function () {
  35 + scope.configuration = ngModelCtrl.$viewValue;
  36 + };
  37 +
  38 + function loadTemplate() {
  39 + if (scope.ruleNodeConfigScope) {
  40 + scope.ruleNodeConfigScope.$destroy();
  41 + }
  42 + var directive = snake_case(attrs.ruleNodeDirective, '-');
  43 + var template = `<${directive} ng-model="configuration" ng-required="required" ng-readonly="readonly"></${directive}>`;
  44 + element.html(template);
  45 + scope.ruleNodeConfigScope = scope.$new();
  46 + $compile(element.contents())(scope.ruleNodeConfigScope);
  47 + }
  48 +
  49 + function snake_case(name, separator) {
  50 + separator = separator || '_';
  51 + return name.replace(SNAKE_CASE_REGEXP, function(letter, pos) {
  52 + return (pos ? separator : '') + letter.toLowerCase();
  53 + });
  54 + }
  55 + };
  56 +
  57 + return {
  58 + restrict: "E",
  59 + require: "^ngModel",
  60 + scope: {
  61 + required:'=ngRequired',
  62 + readonly:'=ngReadonly'
  63 + },
  64 + link: linker
  65 + };
  66 +
  67 +}
@@ -21,28 +21,26 @@ @@ -21,28 +21,26 @@
21 21
22 <md-content class="md-padding tb-rulenode" layout="column"> 22 <md-content class="md-padding tb-rulenode" layout="column">
23 <fieldset ng-disabled="$root.loading || !isEdit || isReadOnly"> 23 <fieldset ng-disabled="$root.loading || !isEdit || isReadOnly">
24 - <md-input-container class="md-block">  
25 - <label translate>rulenode.type</label>  
26 - <input readonly name="type" ng-model="ruleNode.component.name">  
27 - </md-input-container>  
28 <section ng-if="ruleNode.component.type != types.ruleNodeType.RULE_CHAIN.value"> 24 <section ng-if="ruleNode.component.type != types.ruleNodeType.RULE_CHAIN.value">
29 - <md-input-container class="md-block">  
30 - <label translate>rulenode.name</label>  
31 - <input required name="name" ng-model="ruleNode.name">  
32 - <div ng-messages="theForm.name.$error">  
33 - <div translate ng-message="required">rulenode.name-required</div>  
34 - </div>  
35 - </md-input-container>  
36 - <md-input-container class="md-block">  
37 - <md-checkbox ng-disabled="$root.loading || !isEdit" aria-label="{{ 'rulenode.debug-mode' | translate }}"  
38 - ng-model="ruleNode.debugMode">{{ 'rulenode.debug-mode' | translate }}  
39 - </md-checkbox>  
40 - </md-input-container>  
41 - <tb-json-object-edit class="tb-rule-node-configuration-json" ng-model="ruleNode.configuration"  
42 - label="{{ 'rulenode.configuration' | translate }}" 25 + <section layout="column" layout-gt-sm="row">
  26 + <md-input-container flex class="md-block">
  27 + <label translate>rulenode.name</label>
  28 + <input required name="name" ng-model="ruleNode.name">
  29 + <div ng-messages="theForm.name.$error">
  30 + <div translate ng-message="required">rulenode.name-required</div>
  31 + </div>
  32 + </md-input-container>
  33 + <md-input-container class="md-block">
  34 + <md-checkbox ng-disabled="$root.loading || !isEdit" aria-label="{{ 'rulenode.debug-mode' | translate }}"
  35 + ng-model="ruleNode.debugMode">{{ 'rulenode.debug-mode' | translate }}
  36 + </md-checkbox>
  37 + </md-input-container>
  38 + </section>
  39 + <tb-rule-node-config ng-model="ruleNode.configuration"
43 ng-required="true" 40 ng-required="true"
44 - fill-height="true">  
45 - </tb-json-object-edit> 41 + node-definition="ruleNode.component.configurationDescriptor.nodeDefinition"
  42 + ng-readonly="$root.loading || !isEdit || isReadOnly">
  43 + </tb-rule-node-config>
46 <md-input-container class="md-block"> 44 <md-input-container class="md-block">
47 <label translate>rulenode.description</label> 45 <label translate>rulenode.description</label>
48 <textarea ng-model="ruleNode.additionalInfo.description" rows="2"></textarea> 46 <textarea ng-model="ruleNode.additionalInfo.description" rows="2"></textarea>
@@ -19,4 +19,10 @@ @@ -19,4 +19,10 @@
19 height: 300px; 19 height: 300px;
20 display: block; 20 display: block;
21 } 21 }
  22 +}
  23 +
  24 +.tb-rulenode-directive-error {
  25 + color: rgb(221,44,0);
  26 + font-size: 13px;
  27 + font-weight: 400;
22 } 28 }
@@ -22,6 +22,7 @@ @@ -22,6 +22,7 @@
22 ng-mousedown="callbacks.mouseDown($event, node)" 22 ng-mousedown="callbacks.mouseDown($event, node)"
23 ng-mouseenter="callbacks.mouseEnter($event, node)" 23 ng-mouseenter="callbacks.mouseEnter($event, node)"
24 ng-mouseleave="callbacks.mouseLeave($event, node)"> 24 ng-mouseleave="callbacks.mouseLeave($event, node)">
  25 + <div class="{{flowchartConstants.nodeOverlayClass}}"></div>
25 <div class="tb-rule-node {{node.nodeClass}}"> 26 <div class="tb-rule-node {{node.nodeClass}}">
26 <md-icon aria-label="node-type-icon" flex="15" 27 <md-icon aria-label="node-type-icon" flex="15"
27 class="material-icons">{{node.icon}}</md-icon> 28 class="material-icons">{{node.icon}}</md-icon>