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 25 import com.typesafe.config.ConfigFactory;
26 26 import lombok.Getter;
27 27 import lombok.Setter;
  28 +import lombok.extern.slf4j.Slf4j;
28 29 import org.springframework.beans.factory.annotation.Autowired;
29 30 import org.springframework.beans.factory.annotation.Value;
30 31 import org.springframework.stereotype.Component;
... ... @@ -38,6 +39,7 @@ import org.thingsboard.server.common.data.id.EntityId;
38 39 import org.thingsboard.server.common.data.id.TenantId;
39 40 import org.thingsboard.server.common.data.plugin.ComponentLifecycleEvent;
40 41 import org.thingsboard.server.common.msg.TbMsg;
  42 +import org.thingsboard.server.common.msg.TbMsgDataType;
41 43 import org.thingsboard.server.common.msg.cluster.ServerAddress;
42 44 import org.thingsboard.server.common.transport.auth.DeviceAuthService;
43 45 import org.thingsboard.server.controller.plugin.PluginWebSocketMsgEndpoint;
... ... @@ -60,11 +62,13 @@ import org.thingsboard.server.service.cluster.routing.ClusterRoutingService;
60 62 import org.thingsboard.server.service.cluster.rpc.ClusterRpcService;
61 63 import org.thingsboard.server.service.component.ComponentDiscoveryService;
62 64
  65 +import java.io.IOException;
63 66 import java.io.PrintWriter;
64 67 import java.io.StringWriter;
65 68 import java.nio.charset.StandardCharsets;
66 69 import java.util.Optional;
67 70
  71 +@Slf4j
68 72 @Component
69 73 public class ActorSystemContext {
70 74 private static final String AKKA_CONF_FILE_NAME = "actor-system.conf";
... ... @@ -292,38 +296,49 @@ public class ActorSystemContext {
292 296 }
293 297
294 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 337 case BINARY:
314   - node.put("data", Base64Utils.encodeUrlSafe(tbMsg.getData()));
315   - break;
  338 + return Base64Utils.encodeToString(data);
316 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 344 public static Exception toException(Throwable error) {
... ...
... ... @@ -15,9 +15,12 @@
15 15 */
16 16 package org.thingsboard.server.actors.ruleChain;
17 17
  18 +import akka.actor.ActorContext;
  19 +import akka.actor.ActorRef;
18 20 import org.thingsboard.rule.engine.api.ListeningExecutor;
19 21 import org.thingsboard.rule.engine.api.TbContext;
20 22 import org.thingsboard.server.actors.ActorSystemContext;
  23 +import org.thingsboard.server.common.data.id.RuleNodeId;
21 24 import org.thingsboard.server.common.msg.TbMsg;
22 25 import org.thingsboard.server.common.msg.cluster.ServerAddress;
23 26 import org.thingsboard.server.dao.alarm.AlarmService;
... ... @@ -30,8 +33,10 @@ import org.thingsboard.server.dao.relation.RelationService;
30 33 import org.thingsboard.server.dao.rule.RuleChainService;
31 34 import org.thingsboard.server.dao.timeseries.TimeseriesService;
32 35 import org.thingsboard.server.dao.user.UserService;
  36 +import scala.concurrent.duration.Duration;
33 37
34 38 import java.util.Set;
  39 +import java.util.concurrent.TimeUnit;
35 40
36 41 /**
37 42 * Created by ashvayka on 19.03.18.
... ... @@ -61,7 +66,12 @@ class DefaultTbContext implements TbContext {
61 66
62 67 @Override
63 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 77 @Override
... ... @@ -93,6 +103,11 @@ class DefaultTbContext implements TbContext {
93 103 }
94 104
95 105 @Override
  106 + public RuleNodeId getSelfId() {
  107 + return nodeCtx.getSelf().getId();
  108 + }
  109 +
  110 + @Override
96 111 public void tellNext(TbMsg msg, Set<String> relationTypes) {
97 112 relationTypes.forEach(type -> tellNext(msg, type));
98 113 }
... ...
... ... @@ -47,12 +47,25 @@ public class RuleNodeActor extends ComponentActor<RuleNodeId, RuleNodeActorMessa
47 47 case RULE_TO_SELF_ERROR_MSG:
48 48 onRuleNodeToSelfErrorMsg((RuleNodeToSelfErrorMsg) msg);
49 49 break;
  50 + case RULE_TO_SELF_MSG:
  51 + onRuleNodeToSelfMsg((RuleNodeToSelfMsg) msg);
  52 + break;
50 53 default:
51 54 return false;
52 55 }
53 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 69 private void onRuleChainToRuleNodeMsg(RuleChainToRuleNodeMsg msg) {
57 70 logger.debug("[{}] Going to process rule msg: {}", id, msg.getMsg());
58 71 try {
... ...
... ... @@ -18,9 +18,10 @@ package org.thingsboard.server.actors.ruleChain;
18 18 import akka.actor.ActorContext;
19 19 import akka.actor.ActorRef;
20 20 import akka.event.LoggingAdapter;
  21 +import org.thingsboard.rule.engine.api.TbContext;
21 22 import org.thingsboard.rule.engine.api.TbNode;
22 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 25 import org.thingsboard.server.actors.ActorSystemContext;
25 26 import org.thingsboard.server.actors.shared.ComponentMsgProcessor;
26 27 import org.thingsboard.server.common.data.id.RuleChainId;
... ... @@ -31,6 +32,8 @@ import org.thingsboard.server.common.data.rule.RuleNode;
31 32 import org.thingsboard.server.common.msg.cluster.ClusterEventMsg;
32 33 import org.thingsboard.server.dao.rule.RuleChainService;
33 34
  35 +import java.util.concurrent.ExecutionException;
  36 +
34 37 /**
35 38 * @author Andrew Shvayka
36 39 */
... ... @@ -41,6 +44,7 @@ public class RuleNodeActorMessageProcessor extends ComponentMsgProcessor<RuleNod
41 44 private final RuleChainService service;
42 45 private RuleNode ruleNode;
43 46 private TbNode tbNode;
  47 + private TbContext defaultCtx;
44 48
45 49 RuleNodeActorMessageProcessor(TenantId tenantId, RuleChainId ruleChainId, RuleNodeId ruleNodeId, ActorSystemContext systemContext
46 50 , LoggingAdapter logger, ActorRef parent, ActorRef self) {
... ... @@ -49,6 +53,7 @@ public class RuleNodeActorMessageProcessor extends ComponentMsgProcessor<RuleNod
49 53 this.self = self;
50 54 this.service = systemContext.getRuleChainService();
51 55 this.ruleNode = systemContext.getRuleChainService().findRuleNodeById(entityId);
  56 + this.defaultCtx = new DefaultTbContext(systemContext, new RuleNodeCtx(tenantId, parent, self, ruleNode));
52 57 }
53 58
54 59 @Override
... ... @@ -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 96 void onRuleChainToRuleNodeMsg(RuleChainToRuleNodeMsg msg) throws Exception {
84 97 checkActive();
85 98 if (ruleNode.isDebugMode()) {
... ... @@ -91,9 +104,8 @@ public class RuleNodeActorMessageProcessor extends ComponentMsgProcessor<RuleNod
91 104 private TbNode initComponent(RuleNode ruleNode) throws Exception {
92 105 Class<?> componentClazz = Class.forName(ruleNode.getType());
93 106 TbNode tbNode = (TbNode) (componentClazz.newInstance());
94   - tbNode.init(new TbNodeConfiguration(ruleNode.getConfiguration()), new TbNodeState());
  107 + tbNode.init(defaultCtx, new TbNodeConfiguration(ruleNode.getConfiguration()));
95 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 192 NodeConfiguration config = configClazz.newInstance();
193 193 NodeConfiguration defaultConfiguration = config.defaultConfiguration();
194 194 nodeDefinition.setDefaultConfiguration(mapper.valueToTree(defaultConfiguration));
  195 + nodeDefinition.setUiResources(nodeAnnotation.uiResources());
  196 + nodeDefinition.setConfigDirective(nodeAnnotation.configDirective());
195 197 return nodeDefinition;
196 198 }
197 199
... ...
... ... @@ -51,6 +51,6 @@ public class AbstractRuleEngineControllerTest extends AbstractControllerTest {
51 51 TimePageLink pageLink = new TimePageLink(limit);
52 52 return doGetTypedWithTimePageLink("/api/events/{entityType}/{entityId}/{eventType}?tenantId={tenantId}&",
53 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 37 public static final String ERROR = "ERROR";
38 38 public static final String LC_EVENT = "LC_EVENT";
39 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 42 public static final String ONEWAY = "ONEWAY";
43 43 public static final String TWOWAY = "TWOWAY";
... ...
... ... @@ -54,4 +54,9 @@ public enum MsgType {
54 54 */
55 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 24 import com.google.common.util.concurrent.Futures;
25 25 import com.google.common.util.concurrent.ListenableFuture;
26 26 import com.google.common.util.concurrent.Uninterruptibles;
  27 +import org.thingsboard.server.dao.exception.BufferLimitException;
27 28 import org.thingsboard.server.dao.util.AsyncRateLimiter;
28 29
29 30 import javax.annotation.Nullable;
... ... @@ -35,9 +36,15 @@ public class RateLimitedResultSetFuture implements ResultSetFuture {
35 36 private final ListenableFuture<Void> rateLimitFuture;
36 37
37 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 45 this.originalFuture = Futures.transform(rateLimitFuture,
40 46 (Function<Void, ResultSetFuture>) i -> executeAsyncWithRelease(rateLimiter, session, statement));
  47 +
41 48 }
42 49
43 50 @Override
... ... @@ -108,10 +115,7 @@ public class RateLimitedResultSetFuture implements ResultSetFuture {
108 115 try {
109 116 ResultSetFuture resultSetFuture = Uninterruptibles.getUninterruptibly(originalFuture);
110 117 resultSetFuture.addListener(listener, executor);
111   - } catch (CancellationException e) {
112   - cancel(false);
113   - return;
114   - } catch (ExecutionException e) {
  118 + } catch (CancellationException | ExecutionException e) {
115 119 Futures.immediateFailedFuture(e).addListener(listener, executor);
116 120 }
117 121 }, executor);
... ...
... ... @@ -23,6 +23,7 @@ import lombok.extern.slf4j.Slf4j;
23 23 import org.springframework.beans.factory.annotation.Value;
24 24 import org.springframework.scheduling.annotation.Scheduled;
25 25 import org.springframework.stereotype.Component;
  26 +import org.thingsboard.server.dao.exception.BufferLimitException;
26 27
27 28 import java.util.concurrent.*;
28 29 import java.util.concurrent.atomic.AtomicInteger;
... ... @@ -41,6 +42,9 @@ public class BufferedRateLimiter implements AsyncRateLimiter {
41 42
42 43 private final AtomicInteger maxQueueSize = new AtomicInteger();
43 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 49 public BufferedRateLimiter(@Value("${cassandra.query.buffer_size}") int queueLimit,
46 50 @Value("${cassandra.query.concurrent_limit}") int permitsLimit,
... ... @@ -53,11 +57,13 @@ public class BufferedRateLimiter implements AsyncRateLimiter {
53 57
54 58 @Override
55 59 public ListenableFuture<Void> acquireAsync() {
  60 + totalRequested.incrementAndGet();
56 61 if (queue.isEmpty()) {
57 62 if (permits.incrementAndGet() <= permitsLimit) {
58 63 if (permits.get() > maxGrantedPermissions.get()) {
59 64 maxGrantedPermissions.set(permits.get());
60 65 }
  66 + totalGranted.incrementAndGet();
61 67 return Futures.immediateFuture(null);
62 68 }
63 69 permits.decrementAndGet();
... ... @@ -69,6 +75,7 @@ public class BufferedRateLimiter implements AsyncRateLimiter {
69 75 @Override
70 76 public void release() {
71 77 permits.decrementAndGet();
  78 + totalReleased.incrementAndGet();
72 79 reprocessQueue();
73 80 }
74 81
... ... @@ -80,6 +87,7 @@ public class BufferedRateLimiter implements AsyncRateLimiter {
80 87 }
81 88 LockedFuture lockedFuture = queue.poll();
82 89 if (lockedFuture != null) {
  90 + totalGranted.incrementAndGet();
83 91 lockedFuture.latch.countDown();
84 92 } else {
85 93 permits.decrementAndGet();
... ... @@ -112,17 +120,20 @@ public class BufferedRateLimiter implements AsyncRateLimiter {
112 120 LockedFuture lockedFuture = createLockedFuture();
113 121 if (!queue.offer(lockedFuture, 1, TimeUnit.SECONDS)) {
114 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 128 if(permits.get() < permitsLimit) {
118 129 reprocessQueue();
119 130 }
120 131 return lockedFuture.future;
121 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 139 @Scheduled(fixedDelayString = "${cassandra.query.rate_limit_print_interval_ms}")
... ... @@ -134,8 +145,11 @@ public class BufferedRateLimiter implements AsyncRateLimiter {
134 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 155 private class LockedFuture {
... ...
... ... @@ -19,16 +19,17 @@ import com.datastax.driver.core.*;
19 19 import com.datastax.driver.core.exceptions.UnsupportedFeatureException;
20 20 import com.google.common.util.concurrent.Futures;
21 21 import com.google.common.util.concurrent.ListenableFuture;
  22 +import com.google.common.util.concurrent.MoreExecutors;
22 23 import org.junit.Test;
23 24 import org.junit.runner.RunWith;
24 25 import org.mockito.Mock;
25 26 import org.mockito.Mockito;
26 27 import org.mockito.runners.MockitoJUnitRunner;
27 28 import org.mockito.stubbing.Answer;
  29 +import org.thingsboard.server.dao.exception.BufferLimitException;
28 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 34 import static org.junit.Assert.*;
34 35 import static org.mockito.Mockito.*;
... ... @@ -53,7 +54,7 @@ public class RateLimitedResultSetFutureTest {
53 54
54 55 @Test
55 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 58 resultSetFuture = new RateLimitedResultSetFuture(session, rateLimiter, statement);
58 59 Thread.sleep(1000L);
59 60 verify(rateLimiter).acquireAsync();
... ... @@ -153,4 +154,29 @@ public class RateLimitedResultSetFutureTest {
153 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 }
\ No newline at end of file
... ...
... ... @@ -17,6 +17,7 @@ package org.thingsboard.server.dao.util;
17 17
18 18 import com.google.common.util.concurrent.*;
19 19 import org.junit.Test;
  20 +import org.thingsboard.server.dao.exception.BufferLimitException;
20 21
21 22 import javax.annotation.Nullable;
22 23 import java.util.concurrent.ExecutionException;
... ... @@ -61,8 +62,8 @@ public class BufferedRateLimiterTest {
61 62 } catch (Exception e) {
62 63 assertTrue(e instanceof ExecutionException);
63 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 284 <exclude>src/sh/**</exclude>
285 285 <exclude>src/main/scripts/control/**</exclude>
286 286 <exclude>src/main/scripts/windows/**</exclude>
  287 + <exclude>src/main/resources/public/static/rulenode/**</exclude>
287 288 </excludes>
288 289 <mapping>
289 290 <proto>JAVADOC_STYLE</proto>
... ...
... ... @@ -15,8 +15,8 @@
15 15 */
16 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 29 String[] relationTypes;
30 30 boolean customRelations;
31 31 JsonNode defaultConfiguration;
  32 + String[] uiResources;
  33 + String configDirective;
32 34
33 35 }
... ...
... ... @@ -45,6 +45,10 @@ public @interface RuleNode {
45 45
46 46 String[] relationTypes() default {"Success", "Failure"};
47 47
  48 + String[] uiResources() default {};
  49 +
  50 + String configDirective() default "";
  51 +
48 52 boolean customRelations() default false;
49 53
50 54 }
... ...
... ... @@ -15,6 +15,7 @@
15 15 */
16 16 package org.thingsboard.rule.engine.api;
17 17
  18 +import org.thingsboard.server.common.data.id.RuleNodeId;
18 19 import org.thingsboard.server.common.msg.TbMsg;
19 20 import org.thingsboard.server.common.msg.cluster.ServerAddress;
20 21 import org.thingsboard.server.dao.alarm.AlarmService;
... ... @@ -55,6 +56,8 @@ public interface TbContext {
55 56
56 57 void tellError(TbMsg msg, Throwable th);
57 58
  59 + RuleNodeId getSelfId();
  60 +
58 61 AttributesService getAttributesService();
59 62
60 63 CustomerService getCustomerService();
... ...
... ... @@ -24,7 +24,7 @@ import java.util.concurrent.ExecutionException;
24 24 */
25 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 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 29 @Slf4j
30 30 @RuleNode(
31 31 type = ComponentType.FILTER,
32   - name = "script", relationTypes = {"True", "False", "Failure"},
  32 + name = "script", relationTypes = {"True", "False"},
33 33 configClazz = TbJsFilterNodeConfiguration.class,
34 34 nodeDescription = "Filter incoming messages using JS script",
35 35 nodeDetails = "Evaluate incoming Message with configured JS condition. " +
36 36 "If <b>True</b> - send Message via <b>True</b> chain, otherwise <b>False</b> chain is used." +
37 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 42 public class TbJsFilterNode implements TbNode {
40 43
41 44 private TbJsFilterNodeConfiguration config;
42 45 private NashornJsEngine jsEngine;
43 46
44 47 @Override
45   - public void init(TbNodeConfiguration configuration, TbNodeState state) throws TbNodeException {
  48 + public void init(TbContext ctx, TbNodeConfiguration configuration) throws TbNodeException {
46 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 53 @Override
... ...
... ... @@ -19,14 +19,14 @@ import lombok.Data;
19 19 import org.thingsboard.rule.engine.api.NodeConfiguration;
20 20
21 21 @Data
22   -public class TbJsFilterNodeConfiguration implements NodeConfiguration {
  22 +public class TbJsFilterNodeConfiguration implements NodeConfiguration<TbJsFilterNodeConfiguration> {
23 23
24 24 private String jsScript;
25 25
26 26 @Override
27 27 public TbJsFilterNodeConfiguration defaultConfiguration() {
28 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 30 return configuration;
31 31 }
32 32 }
... ...
... ... @@ -36,31 +36,22 @@ import static org.thingsboard.rule.engine.DonAsynchron.withCallback;
36 36 nodeDetails = "Node executes configured JS script. Script should return array of next Chain names where Message should be routed. " +
37 37 "If Array is empty - message not routed to next Node. " +
38 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 42 public class TbJsSwitchNode implements TbNode {
41 43
42 44 private TbJsSwitchNodeConfiguration config;
43 45 private NashornJsEngine jsEngine;
44 46
45 47 @Override
46   - public void init(TbNodeConfiguration configuration, TbNodeState state) throws TbNodeException {
  48 + public void init(TbContext ctx, TbNodeConfiguration configuration) throws TbNodeException {
47 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 53 @Override
59 54 public void onMsg(TbContext ctx, TbMsg msg) {
60   - if (config.isRouteToAllWithNoCheck()) {
61   - ctx.tellNext(msg, config.getAllowedRelations());
62   - return;
63   - }
64 55 ListeningExecutor jsExecutor = ctx.getJsExecutor();
65 56 withCallback(jsExecutor.executeAsync(() -> jsEngine.executeSwitch(toBindings(msg))),
66 57 result -> processSwitch(ctx, msg, result),
... ... @@ -68,15 +59,7 @@ public class TbJsSwitchNode implements TbNode {
68 59 }
69 60
70 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 65 private Bindings toBindings(TbMsg msg) {
... ...
... ... @@ -22,22 +22,18 @@ import org.thingsboard.rule.engine.api.NodeConfiguration;
22 22 import java.util.Set;
23 23
24 24 @Data
25   -public class TbJsSwitchNodeConfiguration implements NodeConfiguration {
  25 +public class TbJsSwitchNodeConfiguration implements NodeConfiguration<TbJsSwitchNodeConfiguration> {
26 26
27 27 private String jsScript;
28   - private Set<String> allowedRelations;
29   - private boolean routeToAllWithNoCheck;
30 28
31 29 @Override
32 30 public TbJsSwitchNodeConfiguration defaultConfiguration() {
33 31 TbJsSwitchNodeConfiguration configuration = new TbJsSwitchNodeConfiguration();
34   - configuration.setJsScript("function nextRelation(meta, msg) {\n" +
  32 + configuration.setJsScript("function nextRelation(metadata, msg) {\n" +
35 33 " return ['one','nine'];" +
36 34 "};\n" +
37 35 "\n" +
38   - "nextRelation(meta, msg);");
39   - configuration.setAllowedRelations(Sets.newHashSet("one", "two"));
40   - configuration.setRouteToAllWithNoCheck(false);
  36 + "return nextRelation(metadata, msg);");
41 37 return configuration;
42 38 }
43 39 }
... ...
... ... @@ -29,15 +29,18 @@ import org.thingsboard.server.common.msg.TbMsg;
29 29 type = ComponentType.FILTER,
30 30 name = "message type",
31 31 configClazz = TbMsgTypeFilterNodeConfiguration.class,
  32 + relationTypes = {"True", "False"},
32 33 nodeDescription = "Filter incoming messages by Message Type",
33 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 38 public class TbMsgTypeFilterNode implements TbNode {
36 39
37 40 TbMsgTypeFilterNodeConfiguration config;
38 41
39 42 @Override
40   - public void init(TbNodeConfiguration configuration, TbNodeState state) throws TbNodeException {
  43 + public void init(TbContext ctx, TbNodeConfiguration configuration) throws TbNodeException {
41 44 this.config = TbNodeUtils.convert(configuration, TbMsgTypeFilterNodeConfiguration.class);
42 45 }
43 46
... ...
... ... @@ -26,14 +26,14 @@ import java.util.List;
26 26 * Created by ashvayka on 19.01.18.
27 27 */
28 28 @Data
29   -public class TbMsgTypeFilterNodeConfiguration implements NodeConfiguration {
  29 +public class TbMsgTypeFilterNodeConfiguration implements NodeConfiguration<TbMsgTypeFilterNodeConfiguration> {
30 30
31 31 private List<String> messageTypes;
32 32
33 33 @Override
34 34 public TbMsgTypeFilterNodeConfiguration defaultConfiguration() {
35 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 37 return configuration;
38 38 }
39 39 }
... ...
... ... @@ -34,14 +34,20 @@ import java.util.Set;
34 34 @Slf4j
35 35 public class NashornJsEngine {
36 36
37   - public static final String METADATA = "meta";
  37 + public static final String METADATA = "metadata";
38 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 43 private static NashornScriptEngineFactory factory = new NashornScriptEngineFactory();
40 44
41 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 53 private static CompiledScript compileScript(String script) {
... ... @@ -58,15 +64,15 @@ public class NashornJsEngine {
58 64 public static Bindings bindMsg(TbMsg msg) {
59 65 try {
60 66 Bindings bindings = new SimpleBindings();
61   - bindings.put(METADATA, msg.getMetaData().getData());
62   -
63 67 if (ArrayUtils.isNotEmpty(msg.getData())) {
64 68 ObjectMapper mapper = new ObjectMapper();
65 69 JsonNode jsonNode = mapper.readTree(msg.getData());
66 70 Map map = mapper.treeToValue(jsonNode, Map.class);
67 71 bindings.put(DATA, map);
  72 + } else {
  73 + bindings.put(DATA, Collections.emptyMap());
68 74 }
69   -
  75 + bindings.put(METADATA, msg.getMetaData().getData());
70 76 return bindings;
71 77 } catch (Throwable th) {
72 78 throw new IllegalArgumentException("Cannot bind js args", th);
... ...
... ... @@ -37,7 +37,7 @@ public abstract class TbEntityGetAttrNode<T extends EntityId> implements TbNode
37 37 private TbGetEntityAttrNodeConfiguration config;
38 38
39 39 @Override
40   - public void init(TbNodeConfiguration configuration, TbNodeState state) throws TbNodeException {
  40 + public void init(TbContext ctx, TbNodeConfiguration configuration) throws TbNodeException {
41 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 42 nodeDescription = "Add Message Originator Attributes or Latest Telemetry into Message Metadata",
43 43 nodeDetails = "If Attributes enrichment configured, <b>CLIENT/SHARED/SERVER</b> attributes are added into Message metadata " +
44 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 46 "If Latest Telemetry enrichment configured, latest telemetry added into metadata without prefix.")
47 47 public class TbGetAttributesNode implements TbNode {
48 48
49 49 private TbGetAttributesNodeConfiguration config;
50 50
51 51 @Override
52   - public void init(TbNodeConfiguration configuration, TbNodeState state) throws TbNodeException {
  52 + public void init(TbContext ctx, TbNodeConfiguration configuration) throws TbNodeException {
53 53 this.config = TbNodeUtils.convert(configuration, TbGetAttributesNodeConfiguration.class);
54 54 }
55 55
... ...
... ... @@ -25,7 +25,7 @@ import java.util.List;
25 25 * Created by ashvayka on 19.01.18.
26 26 */
27 27 @Data
28   -public class TbGetAttributesNodeConfiguration implements NodeConfiguration {
  28 +public class TbGetAttributesNodeConfiguration implements NodeConfiguration<TbGetAttributesNodeConfiguration> {
29 29
30 30 private List<String> clientAttributeNames;
31 31 private List<String> sharedAttributeNames;
... ...
... ... @@ -30,7 +30,7 @@ import org.thingsboard.server.common.data.plugin.ComponentType;
30 30 nodeDescription = "Add Originators Customer Attributes or Latest Telemetry into Message Metadata",
31 31 nodeDetails = "If Attributes enrichment configured, server scope attributes are added into Message metadata. " +
32 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 34 public class TbGetCustomerAttributeNode extends TbEntityGetAttrNode<CustomerId> {
35 35
36 36 @Override
... ...
... ... @@ -23,7 +23,7 @@ import java.util.Map;
23 23 import java.util.Optional;
24 24
25 25 @Data
26   -public class TbGetEntityAttrNodeConfiguration implements NodeConfiguration {
  26 +public class TbGetEntityAttrNodeConfiguration implements NodeConfiguration<TbGetEntityAttrNodeConfiguration> {
27 27
28 28 private Map<String, String> attrMapping;
29 29 private boolean isTelemetry = false;
... ...
... ... @@ -32,13 +32,13 @@ import org.thingsboard.server.common.data.plugin.ComponentType;
32 32 "If multiple Related Entities are found, only first Entity is used for attributes enrichment, other entities are discarded. " +
33 33 "If Attributes enrichment configured, server scope attributes are added into Message metadata. " +
34 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 36 public class TbGetRelatedAttributeNode extends TbEntityGetAttrNode<EntityId> {
37 37
38 38 private TbGetRelatedAttrNodeConfiguration config;
39 39
40 40 @Override
41   - public void init(TbNodeConfiguration configuration, TbNodeState state) throws TbNodeException {
  41 + public void init(TbContext context, TbNodeConfiguration configuration) throws TbNodeException {
42 42 this.config = TbNodeUtils.convert(configuration, TbGetRelatedAttrNodeConfiguration.class);
43 43 setConfig(config);
44 44 }
... ...
... ... @@ -32,7 +32,7 @@ import org.thingsboard.server.common.data.plugin.ComponentType;
32 32 nodeDescription = "Add Originators Tenant Attributes or Latest Telemetry into Message Metadata",
33 33 nodeDetails = "If Attributes enrichment configured, server scope attributes are added into Message metadata. " +
34 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 36 public class TbGetTenantAttributeNode extends TbEntityGetAttrNode<TenantId> {
37 37
38 38 @Override
... ...
... ... @@ -32,7 +32,7 @@ public abstract class TbAbstractTransformNode implements TbNode {
32 32 private TbTransformNodeConfiguration config;
33 33
34 34 @Override
35   - public void init(TbNodeConfiguration configuration, TbNodeState state) throws TbNodeException {
  35 + public void init(TbContext context, TbNodeConfiguration configuration) throws TbNodeException {
36 36 this.config = TbNodeUtils.convert(configuration, TbTransformNodeConfiguration.class);
37 37 }
38 38
... ...
... ... @@ -49,7 +49,7 @@ public class TbChangeOriginatorNode extends TbAbstractTransformNode {
49 49 private TbChangeOriginatorNodeConfiguration config;
50 50
51 51 @Override
52   - public void init(TbNodeConfiguration configuration, TbNodeState state) throws TbNodeException {
  52 + public void init(TbContext ctx, TbNodeConfiguration configuration) throws TbNodeException {
53 53 this.config = TbNodeUtils.convert(configuration, TbChangeOriginatorNodeConfiguration.class);
54 54 validateConfig(config);
55 55 setConfig(config);
... ...
... ... @@ -30,7 +30,7 @@ import javax.script.Bindings;
30 30 configClazz = TbTransformMsgNodeConfiguration.class,
31 31 nodeDescription = "Change Message payload and Metadata using JavaScript",
32 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 34 "<code>msg</code> - is a Message payload.<br/>Any properties can be changed/removed/added in those objects.")
35 35 public class TbTransformMsgNode extends TbAbstractTransformNode {
36 36
... ... @@ -38,9 +38,9 @@ public class TbTransformMsgNode extends TbAbstractTransformNode {
38 38 private NashornJsEngine jsEngine;
39 39
40 40 @Override
41   - public void init(TbNodeConfiguration configuration, TbNodeState state) throws TbNodeException {
  41 + public void init(TbContext ctx, TbNodeConfiguration configuration) throws TbNodeException {
42 42 this.config = TbNodeUtils.convert(configuration, TbTransformMsgNodeConfiguration.class);
43   - this.jsEngine = new NashornJsEngine(config.getJsScript());
  43 + this.jsEngine = new NashornJsEngine(config.getJsScript(), "Transform");
44 44 setConfig(config);
45 45 }
46 46
... ...
... ... @@ -27,7 +27,7 @@ public class TbTransformMsgNodeConfiguration extends TbTransformNodeConfiguratio
27 27 public TbTransformMsgNodeConfiguration defaultConfiguration() {
28 28 TbTransformMsgNodeConfiguration configuration = new TbTransformMsgNodeConfiguration();
29 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 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*/
\ No newline at end of file
... ...
  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
\ No newline at end of file
... ...
... ... @@ -51,7 +51,7 @@ public class TbJsFilterNodeTest {
51 51
52 52 @Test
53 53 public void falseEvaluationDoNotSendMsg() throws TbNodeException {
54   - initWithScript("10 > 15;");
  54 + initWithScript("return 10 > 15;");
55 55 TbMsg msg = new TbMsg(UUIDs.timeBased(), "USER", null, new TbMsgMetaData(), "{}".getBytes());
56 56
57 57 mockJsExecutor();
... ... @@ -64,7 +64,7 @@ public class TbJsFilterNodeTest {
64 64
65 65 @Test
66 66 public void notValidMsgDataThrowsException() throws TbNodeException {
67   - initWithScript("10 > 15;");
  67 + initWithScript("return 10 > 15;");
68 68 TbMsg msg = new TbMsg(UUIDs.timeBased(), "USER", null, new TbMsgMetaData(), new byte[4]);
69 69
70 70 when(ctx.getJsExecutor()).thenReturn(executor);
... ... @@ -77,7 +77,7 @@ public class TbJsFilterNodeTest {
77 77
78 78 @Test
79 79 public void exceptionInJsThrowsException() throws TbNodeException {
80   - initWithScript("meta.temp.curr < 15;");
  80 + initWithScript("return metadata.temp.curr < 15;");
81 81 TbMsgMetaData metaData = new TbMsgMetaData();
82 82 TbMsg msg = new TbMsg(UUIDs.timeBased(), "USER", null, metaData, "{}".getBytes());
83 83 mockJsExecutor();
... ... @@ -89,12 +89,12 @@ public class TbJsFilterNodeTest {
89 89
90 90 @Test(expected = IllegalArgumentException.class)
91 91 public void notValidScriptThrowsException() throws TbNodeException {
92   - initWithScript("10 > 15 asdq out");
  92 + initWithScript("return 10 > 15 asdq out");
93 93 }
94 94
95 95 @Test
96 96 public void metadataConditionCanBeFalse() throws TbNodeException {
97   - initWithScript("meta.humidity < 15;");
  97 + initWithScript("return metadata.humidity < 15;");
98 98 TbMsgMetaData metaData = new TbMsgMetaData();
99 99 metaData.putValue("temp", "10");
100 100 metaData.putValue("humidity", "99");
... ... @@ -109,7 +109,7 @@ public class TbJsFilterNodeTest {
109 109
110 110 @Test
111 111 public void metadataConditionCanBeTrue() throws TbNodeException {
112   - initWithScript("meta.temp < 15;");
  112 + initWithScript("return metadata.temp < 15;");
113 113 TbMsgMetaData metaData = new TbMsgMetaData();
114 114 metaData.putValue("temp", "10");
115 115 metaData.putValue("humidity", "99");
... ... @@ -123,7 +123,7 @@ public class TbJsFilterNodeTest {
123 123
124 124 @Test
125 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 127 TbMsgMetaData metaData = new TbMsgMetaData();
128 128 metaData.putValue("temp", "10");
129 129 metaData.putValue("humidity", "99");
... ... @@ -144,7 +144,7 @@ public class TbJsFilterNodeTest {
144 144 TbNodeConfiguration nodeConfiguration = new TbNodeConfiguration(mapper.valueToTree(config));
145 145
146 146 node = new TbJsFilterNode();
147   - node.init(nodeConfiguration, null);
  147 + node.init(null, nodeConfiguration);
148 148 }
149 149
150 150 private void mockJsExecutor() {
... ...
... ... @@ -53,27 +53,16 @@ public class TbJsSwitchNodeTest {
53 53 private ListeningExecutor executor;
54 54
55 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 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 59 " return ['three', 'one']\n" +
71 60 " else\n" +
72 61 " return 'two';\n" +
73 62 "};\n" +
74 63 "\n" +
75   - "nextRelation(meta, msg);";
76   - initWithScript(jsCode, Sets.newHashSet("one", "two", "three"), false);
  64 + "return nextRelation(metadata, msg);";
  65 + initWithScript(jsCode);
77 66 TbMsgMetaData metaData = new TbMsgMetaData();
78 67 metaData.putValue("temp", "10");
79 68 metaData.putValue("humidity", "99");
... ... @@ -89,15 +78,15 @@ public class TbJsSwitchNodeTest {
89 78
90 79 @Test
91 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 83 " return 'one'\n" +
95 84 " else\n" +
96 85 " return 'two';\n" +
97 86 "};\n" +
98 87 "\n" +
99   - "nextRelation(meta, msg);";
100   - initWithScript(jsCode, Sets.newHashSet("one", "two"), false);
  88 + "return nextRelation(metadata, msg);";
  89 + initWithScript(jsCode);
101 90 TbMsgMetaData metaData = new TbMsgMetaData();
102 91 metaData.putValue("temp", "10");
103 92 metaData.putValue("humidity", "99");
... ... @@ -111,37 +100,14 @@ public class TbJsSwitchNodeTest {
111 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 104 TbJsSwitchNodeConfiguration config = new TbJsSwitchNodeConfiguration();
137 105 config.setJsScript(script);
138   - config.setAllowedRelations(relations);
139   - config.setRouteToAllWithNoCheck(routeToAll);
140 106 ObjectMapper mapper = new ObjectMapper();
141 107 TbNodeConfiguration nodeConfiguration = new TbNodeConfiguration(mapper.valueToTree(config));
142 108
143 109 node = new TbJsSwitchNode();
144   - node.init(nodeConfiguration, null);
  110 + node.init(null, nodeConfiguration);
145 111 }
146 112
147 113 private void mockJsExecutor() {
... ...
... ... @@ -88,7 +88,7 @@ public class TbGetCustomerAttributeNodeTest {
88 88 TbNodeConfiguration nodeConfiguration = new TbNodeConfiguration(mapper.valueToTree(config));
89 89
90 90 node = new TbGetCustomerAttributeNode();
91   - node.init(nodeConfiguration, null);
  91 + node.init(null, nodeConfiguration);
92 92 }
93 93
94 94 @Test
... ... @@ -226,7 +226,7 @@ public class TbGetCustomerAttributeNodeTest {
226 226 TbNodeConfiguration nodeConfiguration = new TbNodeConfiguration(mapper.valueToTree(config));
227 227
228 228 node = new TbGetCustomerAttributeNode();
229   - node.init(nodeConfiguration, null);
  229 + node.init(null, nodeConfiguration);
230 230
231 231
232 232 DeviceId deviceId = new DeviceId(UUIDs.timeBased());
... ...
... ... @@ -119,6 +119,6 @@ public class TbChangeOriginatorNodeTest {
119 119 TbNodeConfiguration nodeConfiguration = new TbNodeConfiguration(mapper.valueToTree(config));
120 120
121 121 node = new TbChangeOriginatorNode();
122   - node.init(nodeConfiguration, null);
  122 + node.init(null, nodeConfiguration);
123 123 }
124 124 }
\ No newline at end of file
... ...
... ... @@ -51,7 +51,7 @@ public class TbTransformMsgNodeTest {
51 51
52 52 @Test
53 53 public void metadataCanBeUpdated() throws TbNodeException {
54   - initWithScript("meta.temp = meta.temp * 10;");
  54 + initWithScript("return metadata.temp = metadata.temp * 10;");
55 55 TbMsgMetaData metaData = new TbMsgMetaData();
56 56 metaData.putValue("temp", "7");
57 57 metaData.putValue("humidity", "99");
... ... @@ -70,7 +70,7 @@ public class TbTransformMsgNodeTest {
70 70
71 71 @Test
72 72 public void metadataCanBeAdded() throws TbNodeException {
73   - initWithScript("meta.newAttr = meta.humidity - msg.passed;");
  73 + initWithScript("return metadata.newAttr = metadata.humidity - msg.passed;");
74 74 TbMsgMetaData metaData = new TbMsgMetaData();
75 75 metaData.putValue("temp", "7");
76 76 metaData.putValue("humidity", "99");
... ... @@ -89,7 +89,7 @@ public class TbTransformMsgNodeTest {
89 89
90 90 @Test
91 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 93 TbMsgMetaData metaData = new TbMsgMetaData();
94 94 metaData.putValue("temp", "7");
95 95 metaData.putValue("humidity", "99");
... ... @@ -114,7 +114,7 @@ public class TbTransformMsgNodeTest {
114 114 TbNodeConfiguration nodeConfiguration = new TbNodeConfiguration(mapper.valueToTree(config));
115 115
116 116 node = new TbTransformMsgNode();
117   - node.init(nodeConfiguration, null);
  117 + node.init(null, nodeConfiguration);
118 118 }
119 119
120 120 private void mockJsExecutor() {
... ...
... ... @@ -15,7 +15,7 @@
15 15 },
16 16 "dependencies": {
17 17 "@flowjs/ng-flow": "^2.7.1",
18   - "ace-builds": "^1.2.5",
  18 + "ace-builds": "1.3.1",
19 19 "angular": "1.5.8",
20 20 "angular-animate": "1.5.8",
21 21 "angular-aria": "1.5.8",
... ...
... ... @@ -30,6 +30,9 @@ const httpProxy = require('http-proxy');
30 30 const forwardHost = 'localhost';
31 31 const forwardPort = 8080;
32 32
  33 +const ruleNodeUiforwardHost = 'localhost';
  34 +const ruleNodeUiforwardPort = 8080;
  35 +
33 36 const app = express();
34 37 const server = http.createServer(app);
35 38
... ... @@ -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 65 apiProxy.on('error', function (err, req, res) {
56 66 console.warn('API proxy error: ' + err);
57 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 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 78 app.all('/api/*', (req, res) => {
63 79 apiProxy.web(req, res);
64 80 });
65 81
  82 +app.all('/static/rulenode/*', (req, res) => {
  83 + ruleNodeUiApiProxy.web(req, res);
  84 +});
  85 +
66 86 app.get('*', function(req, res) {
67 87 res.sendFile(path.join(__dirname, 'src/index.html'));
68 88 });
... ...
... ... @@ -17,7 +17,7 @@ export default angular.module('thingsboard.api.ruleChain', [])
17 17 .factory('ruleChainService', RuleChainService).name;
18 18
19 19 /*@ngInject*/
20   -function RuleChainService($http, $q, $filter, types, componentDescriptorService) {
  20 +function RuleChainService($http, $q, $filter, $ocLazyLoad, $translate, types, componentDescriptorService) {
21 21
22 22 var ruleNodeComponents = null;
23 23
... ... @@ -177,11 +177,18 @@ function RuleChainService($http, $q, $filter, types, componentDescriptorService)
177 177 } else {
178 178 loadRuleNodeComponents().then(
179 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 194 deferred.reject();
... ... @@ -191,6 +198,48 @@ function RuleChainService($http, $q, $filter, types, componentDescriptorService)
191 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 243 function getRuleNodeComponentByClazz(clazz) {
195 244 var res = $filter('filter')(ruleNodeComponents, {clazz: clazz}, true);
196 245 if (res && res.length) {
... ...
... ... @@ -279,6 +279,23 @@ export default angular.module('thingsboard.types', [])
279 279 function: "function",
280 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 299 componentType: {
283 300 filter: "FILTER",
284 301 processor: "PROCESSOR",
... ... @@ -295,7 +312,8 @@ export default angular.module('thingsboard.types', [])
295 312 user: "USER",
296 313 dashboard: "DASHBOARD",
297 314 alarm: "ALARM",
298   - rulechain: "RULE_CHAIN"
  315 + rulechain: "RULE_CHAIN",
  316 + rulenode: "RULE_NODE"
299 317 },
300 318 aliasEntityType: {
301 319 current_customer: "CURRENT_CUSTOMER"
... ... @@ -388,6 +406,16 @@ export default angular.module('thingsboard.types', [])
388 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 419 extensionType: {
392 420 http: "HTTP",
393 421 mqtt: "MQTT",
... ...
... ... @@ -18,17 +18,17 @@ export default angular.module('thingsboard.directives.confirmOnExit', [])
18 18 .name;
19 19
20 20 /*@ngInject*/
21   -function ConfirmOnExit($state, $mdDialog, $window, $filter, userService) {
  21 +function ConfirmOnExit($state, $mdDialog, $window, $filter, $parse, userService) {
22 22 return {
23   - link: function ($scope) {
24   -
  23 + link: function ($scope, $element, $attributes) {
  24 + $scope.confirmForm = $scope.$eval($attributes.confirmForm);
25 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 27 return $filter('translate')('confirm-on-exit.message');
28 28 }
29 29 }
30 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 32 event.preventDefault();
33 33 var confirm = $mdDialog.confirm()
34 34 .title($filter('translate')('confirm-on-exit.title'))
... ... @@ -40,7 +40,9 @@ function ConfirmOnExit($state, $mdDialog, $window, $filter, userService) {
40 40 if ($scope.confirmForm) {
41 41 $scope.confirmForm.$setPristine();
42 42 } else {
43   - $scope.isDirty = false;
  43 + var remoteSetter = $parse($attributes.isDirty).assign;
  44 + remoteSetter($scope, false);
  45 + //$scope.isDirty = false;
44 46 }
45 47 $state.go(next.name, params);
46 48 }, function () {
... ... @@ -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 }
\ No newline at end of file
... ...
... ... @@ -26,7 +26,7 @@ export default angular.module('thingsboard.directives.detailsSidenav', [])
26 26 .name;
27 27
28 28 /*@ngInject*/
29   -function DetailsSidenav($timeout) {
  29 +function DetailsSidenav($timeout, $mdUtil, $q, $animate) {
30 30
31 31 var linker = function (scope, element, attrs) {
32 32
... ... @@ -42,6 +42,63 @@ function DetailsSidenav($timeout) {
42 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 102 scope.toggleDetailsEditMode = function () {
46 103 if (!scope.isAlwaysEdit) {
47 104 if (!scope.isEdit) {
... ...
... ... @@ -16,7 +16,7 @@
16 16
17 17 -->
18 18 <md-sidenav class="md-sidenav-right md-whiteframe-4dp tb-sidenav-details"
19   - md-disable-backdrop="true"
  19 + md-disable-backdrop
20 20 md-is-open="isOpen"
21 21 md-component-id="right"
22 22 layout="column">
... ...
... ... @@ -43,6 +43,7 @@ function JsFunc($compile, $templateCache, toast, utils, $translate) {
43 43 var template = $templateCache.get(jsFuncTemplate);
44 44 element.html(template);
45 45
  46 + scope.functionName = attrs.functionName;
46 47 scope.functionArgs = scope.$eval(attrs.functionArgs);
47 48 scope.validationArgs = scope.$eval(attrs.validationArgs);
48 49 scope.resultType = attrs.resultType;
... ... @@ -50,6 +51,8 @@ function JsFunc($compile, $templateCache, toast, utils, $translate) {
50 51 scope.resultType = "nocheck";
51 52 }
52 53
  54 + scope.validationTriggerArg = attrs.validationTriggerArg;
  55 +
53 56 scope.functionValid = true;
54 57
55 58 var Range = ace.acequire("ace/range").Range;
... ... @@ -66,11 +69,15 @@ function JsFunc($compile, $templateCache, toast, utils, $translate) {
66 69 }
67 70
68 71 scope.onFullscreenChanged = function () {
  72 + updateEditorSize();
  73 + };
  74 +
  75 + function updateEditorSize() {
69 76 if (scope.js_editor) {
70 77 scope.js_editor.resize();
71 78 scope.js_editor.renderer.updateFull();
72 79 }
73   - };
  80 + }
74 81
75 82 scope.jsEditorOptions = {
76 83 useWrapMode: true,
... ... @@ -131,6 +138,9 @@ function JsFunc($compile, $templateCache, toast, utils, $translate) {
131 138 scope.validate = function () {
132 139 try {
133 140 var toValidate = new Function(scope.functionArgsString, scope.functionBody);
  141 + if (scope.noValidate) {
  142 + return true;
  143 + }
134 144 var res;
135 145 var validationError;
136 146 for (var i=0;i<scope.validationArgs.length;i++) {
... ... @@ -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 228 $compile(element.contents())(scope);
... ... @@ -211,7 +231,11 @@ function JsFunc($compile, $templateCache, toast, utils, $translate) {
211 231 return {
212 232 restrict: "E",
213 233 require: "^ngModel",
214   - scope: {},
  234 + scope: {
  235 + disabled:'=ngDisabled',
  236 + noValidate: '=?',
  237 + fillHeight:'=?'
  238 + },
215 239 link: linker
216 240 };
217 241 }
... ...
... ... @@ -15,6 +15,12 @@
15 15 */
16 16 tb-js-func {
17 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 26 .tb-js-func-panel {
... ... @@ -23,8 +29,10 @@ tb-js-func {
23 29 height: 100%;
24 30 #tb-javascript-input {
25 31 min-width: 200px;
26   - min-height: 200px;
27 32 width: 100%;
28 33 height: 100%;
  34 + &:not(.fill-height) {
  35 + min-height: 200px;
  36 + }
29 37 }
30 38 }
... ...
... ... @@ -15,19 +15,20 @@
15 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 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 21 <span flex></span>
22 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 23 </div>
24 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 28 ng-model="functionBody">
28 29 </div>
29 30 </div>
30 31 <div layout="row" layout-align="start center" style="height: 40px;">
31   - <span style="font-style: italic;">}</span>
32   - </div>
33   -</div>
\ No newline at end of file
  32 + <label class="tb-title no-padding">}</label>
  33 + </div>
  34 +</div>
... ...
... ... @@ -84,17 +84,32 @@ function JsonObjectEdit($compile, $templateCache, $document, toast, utils) {
84 84 scope.$watch('contentBody', function (newVal, prevVal) {
85 85 if (!angular.equals(newVal, prevVal)) {
86 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 103 scope.updateValidity();
89 104 }
90 105 });
91 106
92 107 ngModelCtrl.$render = function () {
93   - var object = ngModelCtrl.$viewValue;
  108 + scope.object = ngModelCtrl.$viewValue;
94 109 var content = '';
95 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 114 } catch (e) {
100 115 //
... ...
... ... @@ -17,11 +17,14 @@ import $ from 'jquery';
17 17 import 'brace/ext/language_tools';
18 18 import 'brace/mode/java';
19 19 import 'brace/theme/github';
  20 +import beautify from 'js-beautify';
20 21
21 22 /* eslint-disable angular/angularelement */
22 23
  24 +const js_beautify = beautify.js;
  25 +
23 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 29 var vm = this;
27 30
... ... @@ -32,9 +35,19 @@ export default function EventContentDialogController($mdDialog, content, title,
32 35 vm.content = content;
33 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 48 vm.contentOptions = {
36 49 useWrapMode: false,
37   - mode: 'java',
  50 + mode: mode,
38 51 showGutter: false,
39 52 showPrintMargin: false,
40 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 18 import eventHeaderLcEventTemplate from './event-header-lc-event.tpl.html';
19 19 import eventHeaderStatsTemplate from './event-header-stats.tpl.html';
20 20 import eventHeaderErrorTemplate from './event-header-error.tpl.html';
  21 +import eventHeaderDebugRuleNodeTemplate from './event-header-debug-rulenode.tpl.html';
21 22
22 23 /* eslint-enable import/no-unresolved, import/default */
23 24
... ... @@ -38,6 +39,12 @@ export default function EventHeaderDirective($compile, $templateCache, types) {
38 39 case types.eventType.error.value:
39 40 template = eventHeaderErrorTemplate;
40 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 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 20 import eventRowLcEventTemplate from './event-row-lc-event.tpl.html';
21 21 import eventRowStatsTemplate from './event-row-stats.tpl.html';
22 22 import eventRowErrorTemplate from './event-row-error.tpl.html';
  23 +import eventRowDebugRuleNodeTemplate from './event-row-debug-rulenode.tpl.html';
23 24
24 25 /* eslint-enable import/no-unresolved, import/default */
25 26
... ... @@ -40,6 +41,12 @@ export default function EventRowDirective($compile, $templateCache, $mdDialog, $
40 41 case types.eventType.error.value:
41 42 template = eventRowErrorTemplate;
42 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 51 return $templateCache.get(template);
45 52 }
... ... @@ -53,17 +60,22 @@ export default function EventRowDirective($compile, $templateCache, $mdDialog, $
53 60 scope.loadTemplate();
54 61 });
55 62
  63 + scope.types = types;
  64 +
56 65 scope.event = attrs.event;
57 66
58   - scope.showContent = function($event, content, title) {
  67 + scope.showContent = function($event, content, title, contentType) {
59 68 var onShowingCallback = {
60 69 onShowing: function(){}
61 70 }
  71 + if (!contentType) {
  72 + contentType = null;
  73 + }
62 74 $mdDialog.show({
63 75 controller: 'EventContentDialogController',
64 76 controllerAs: 'vm',
65 77 templateUrl: eventErrorDialogTemplate,
66   - locals: {content: content, title: title, showingCallback: onShowingCallback},
  78 + locals: {content: content, title: title, contentType: contentType, showingCallback: onShowingCallback},
67 79 parent: angular.element($document[0].body),
68 80 fullscreen: true,
69 81 targetEvent: $event,
... ...
... ... @@ -36,8 +36,8 @@ export default function EventTableDirective($compile, $templateCache, $rootScope
36 36 for (var type in types.eventType) {
37 37 var eventType = types.eventType[type];
38 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 41 enabled = false;
42 42 break;
43 43 }
... ... @@ -47,7 +47,19 @@ export default function EventTableDirective($compile, $templateCache, $rootScope
47 47 }
48 48 }
49 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 65 scope.eventType = attrs.defaultEventType;
... ...
... ... @@ -341,6 +341,11 @@ export default angular.module('thingsboard.locale', [])
341 341 "enter-password": "Enter password",
342 342 "enter-search": "Enter search"
343 343 },
  344 + "content-type": {
  345 + "json": "Json",
  346 + "text": "Text",
  347 + "binary": "Binary (Base64)"
  348 + },
344 349 "customer": {
345 350 "customer": "Customer",
346 351 "customers": "Customers",
... ... @@ -762,6 +767,8 @@ export default angular.module('thingsboard.locale', [])
762 767 "type-error": "Error",
763 768 "type-lc-event": "Lifecycle event",
764 769 "type-stats": "Statistics",
  770 + "type-debug-rule-node": "Debug",
  771 + "type-debug-rule-chain": "Debug",
765 772 "no-events-prompt": "No events found",
766 773 "error": "Error",
767 774 "alarm": "Alarm",
... ... @@ -769,6 +776,13 @@ export default angular.module('thingsboard.locale', [])
769 776 "server": "Server",
770 777 "body": "Body",
771 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 786 "event": "Event",
773 787 "status": "Status",
774 788 "success": "Success",
... ... @@ -1171,12 +1185,18 @@ export default angular.module('thingsboard.locale', [])
1171 1185 "debug-mode": "Debug mode"
1172 1186 },
1173 1187 "rulenode": {
  1188 + "details": "Details",
  1189 + "events": "Events",
  1190 + "search": "Search nodes",
1174 1191 "add": "Add rule node",
1175 1192 "name": "Name",
1176 1193 "name-required": "Name is required.",
1177 1194 "type": "Type",
1178 1195 "description": "Description",
1179 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 1200 "rulenode-details": "Rule node details",
1181 1201 "debug-mode": "Debug mode",
1182 1202 "configuration": "Configuration",
... ... @@ -1195,7 +1215,9 @@ export default angular.module('thingsboard.locale', [])
1195 1215 "type-action": "Action",
1196 1216 "type-action-details": "Perform special action",
1197 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 1222 "rule-plugin": {
1201 1223 "management": "Rules and plugins management"
... ...
... ... @@ -18,6 +18,8 @@ import RuleChainRoutes from './rulechain.routes';
18 18 import RuleChainsController from './rulechains.controller';
19 19 import {RuleChainController, AddRuleNodeController, AddRuleNodeLinkController} from './rulechain.controller';
20 20 import RuleChainDirective from './rulechain.directive';
  21 +import RuleNodeDefinedConfigDirective from './rulenode-defined-config.directive';
  22 +import RuleNodeConfigDirective from './rulenode-config.directive';
21 23 import RuleNodeDirective from './rulenode.directive';
22 24 import LinkDirective from './link.directive';
23 25
... ... @@ -28,6 +30,8 @@ export default angular.module('thingsboard.ruleChain', [])
28 30 .controller('AddRuleNodeController', AddRuleNodeController)
29 31 .controller('AddRuleNodeLinkController', AddRuleNodeLinkController)
30 32 .directive('tbRuleChain', RuleChainDirective)
  33 + .directive('tbRuleNodeDefinedConfig', RuleNodeDefinedConfigDirective)
  34 + .directive('tbRuleNodeConfig', RuleNodeConfigDirective)
31 35 .directive('tbRuleNode', RuleNodeDirective)
32 36 .directive('tbRuleNodeLink', LinkDirective)
33 37 .name;
... ...
... ... @@ -27,15 +27,10 @@ import addRuleNodeLinkTemplate from './add-link.tpl.html';
27 27
28 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 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 35 var vm = this;
41 36
... ... @@ -48,6 +43,9 @@ export function RuleChainController($stateParams, $scope, $compile, $q, $mdUtil,
48 43 vm.editingRuleNodeLink = null;
49 44 vm.isEditingRuleNodeLink = false;
50 45
  46 + vm.isLibraryOpen = true;
  47 + vm.ruleNodeSearch = '';
  48 +
51 49 vm.ruleChain = ruleChain;
52 50 vm.ruleChainMetaData = ruleChainMetaData;
53 51
... ... @@ -76,39 +74,64 @@ export function RuleChainController($stateParams, $scope, $compile, $q, $mdUtil,
76 74
77 75 vm.modelservice = Modelfactory(vm.ruleChainModel, vm.selectedObjects);
78 76
79   - vm.ctrlDown = false;
80   -
81 77 vm.saveRuleChain = saveRuleChain;
82 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 136 vm.onEditRuleNodeClosed = function() {
114 137 vm.editingRuleNode = null;
... ... @@ -119,15 +142,16 @@ export function RuleChainController($stateParams, $scope, $compile, $q, $mdUtil,
119 142 };
120 143
121 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 153 vm.saveRuleNodeLink = function(theForm) {
129 154 theForm.$setPristine();
130   - vm.isEditingRuleNodeLink = false;
131 155 vm.ruleChainModel.edges[vm.editingRuleNodeLinkIndex] = vm.editingRuleNodeLink;
132 156 vm.editingRuleNodeLink = angular.copy(vm.editingRuleNodeLink);
133 157 };
... ... @@ -235,6 +259,9 @@ export function RuleChainController($stateParams, $scope, $compile, $q, $mdUtil,
235 259 vm.isEditingRuleNodeLink = true;
236 260 vm.editingRuleNodeLinkIndex = vm.ruleChainModel.edges.indexOf(edge);
237 261 vm.editingRuleNodeLink = angular.copy(edge);
  262 + $mdUtil.nextTick(() => {
  263 + vm.ruleNodeLinkForm.$setPristine();
  264 + });
238 265 }
239 266 },
240 267 nodeCallbacks: {
... ... @@ -245,6 +272,9 @@ export function RuleChainController($stateParams, $scope, $compile, $q, $mdUtil,
245 272 vm.isEditingRuleNode = true;
246 273 vm.editingRuleNodeIndex = vm.ruleChainModel.nodes.indexOf(node);
247 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 316 loadRuleChainLibrary();
287 317
288 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 355 function prepareRuleChain() {
... ... @@ -344,7 +370,7 @@ export function RuleChainController($stateParams, $scope, $compile, $q, $mdUtil,
344 370
345 371 vm.ruleChainModel.nodes.push(
346 372 {
347   - id: vm.nextNodeID++,
  373 + id: 'rule-chain-node-' + vm.nextNodeID++,
348 374 component: types.inputNodeComponent,
349 375 name: "",
350 376 nodeClass: types.ruleNodeType.INPUT.nodeClass,
... ... @@ -375,7 +401,7 @@ export function RuleChainController($stateParams, $scope, $compile, $q, $mdUtil,
375 401 var component = ruleChainService.getRuleNodeComponentByClazz(ruleNode.type);
376 402 if (component) {
377 403 var node = {
378   - id: vm.nextNodeID++,
  404 + id: 'rule-chain-node-' + vm.nextNodeID++,
379 405 ruleNodeId: ruleNode.id,
380 406 additionalInfo: ruleNode.additionalInfo,
381 407 configuration: ruleNode.configuration,
... ... @@ -452,7 +478,7 @@ export function RuleChainController($stateParams, $scope, $compile, $q, $mdUtil,
452 478 var ruleChainNode = ruleChainNodesMap[ruleChainConnection.additionalInfo.ruleChainNodeId];
453 479 if (!ruleChainNode) {
454 480 ruleChainNode = {
455   - id: vm.nextNodeID++,
  481 + id: 'rule-chain-node-' + vm.nextNodeID++,
456 482 additionalInfo: ruleChainConnection.additionalInfo,
457 483 targetRuleChainId: ruleChainConnection.targetRuleChainId.id,
458 484 x: ruleChainConnection.additionalInfo.layoutX,
... ... @@ -597,7 +623,7 @@ export function RuleChainController($stateParams, $scope, $compile, $q, $mdUtil,
597 623 fullscreen: true,
598 624 targetEvent: $event
599 625 }).then(function (ruleNode) {
600   - ruleNode.id = vm.nextNodeID++;
  626 + ruleNode.id = 'rule-chain-node-' + vm.nextNodeID++;
601 627 ruleNode.connectors = [];
602 628 if (ruleNode.component.configurationDescriptor.nodeDefinition.inEnabled) {
603 629 ruleNode.connectors.push(
... ... @@ -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 676 /*@ngInject*/
... ...
... ... @@ -68,6 +68,11 @@ export default function RuleChainRoutes($stateProvider, NodeTemplatePathProvider
68 68 /*@ngInject*/
69 69 function($stateParams, ruleChainService) {
70 70 return ruleChainService.getRuleChainMetaData($stateParams.ruleChainId);
  71 + },
  72 + ruleNodeComponents:
  73 + /*@ngInject*/
  74 + function($stateParams, ruleChainService) {
  75 + return ruleChainService.getRuleNodeComponents();
71 76 }
72 77 },
73 78 data: {
... ...
... ... @@ -18,13 +18,58 @@
18 18 .tb-fullscreen-button-style {
19 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 37 .tb-rulechain-library {
22 38 width: 250px;
23 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 70 .tb-rulechain-library-panel-group {
  71 + overflow-y: auto;
  72 + overflow-x: hidden;
28 73 .tb-panel-title {
29 74 -webkit-user-select: none;
30 75 -moz-user-select: none;
... ... @@ -33,7 +78,7 @@
33 78 min-width: 180px;
34 79 }
35 80 .fc-canvas {
36   - background: none;
  81 + background: #f9f9f9;
37 82 }
38 83 md-icon.md-expansion-panel-icon {
39 84 margin-right: 0px;
... ... @@ -55,6 +100,7 @@
55 100 }
56 101 }
57 102 .tb-rulechain-graph {
  103 + z-index: 0;
58 104 overflow: auto;
59 105 }
60 106 }
... ... @@ -75,6 +121,7 @@
75 121 padding: 5px 10px;
76 122 border-radius: 5px;
77 123 background-color: #F15B26;
  124 + pointer-events: none;
78 125 color: #333;
79 126 border: solid 1px #777;
80 127 font-size: 12px;
... ... @@ -121,10 +168,6 @@
121 168 .fc-node {
122 169 z-index: 1;
123 170 outline: none;
124   - &.fc-hover, &.fc-selected {
125   - -webkit-filter: brightness(70%);
126   - filter: brightness(70%);
127   - }
128 171 &.fc-dragging {
129 172 z-index: 10;
130 173 }
... ... @@ -132,6 +175,26 @@
132 175 padding: 0 15px;
133 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 200 .fc-leftConnectors, .fc-rightConnectors {
... ... @@ -170,17 +233,33 @@
170 233 margin: 10px;
171 234 border-radius: 5px;
172 235 background-color: #ccc;
  236 + pointer-events: all;
173 237 }
174 238
175 239 .fc-connector.fc-hover {
176 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 257 .fc-edge {
180 258 outline: none;
181 259 stroke: gray;
182 260 stroke-width: 4;
183 261 fill: transparent;
  262 + transition: stroke-width .2s;
184 263 &.fc-selected {
185 264 stroke: red;
186 265 stroke-width: 4;
... ... @@ -229,24 +308,53 @@
229 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 321 .fc-edge-label {
233 322 position: absolute;
234   - user-select: none;
235   - pointer-events: none;
  323 + transition: transform .2s;
236 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 348 .fc-edge-label-text {
240 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 352 white-space: nowrap;
245 353 text-align: center;
246 354 font-size: 14px;
247 355 font-weight: 600;
248   - top: 5px;
249 356 span {
  357 + cursor: default;
250 358 border: solid 2px #003a79;
251 359 border-radius: 10px;
252 360 color: #003a79;
... ... @@ -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 373 @keyframes dash {
259 374 from {
260 375 stroke-dashoffset: 500;
... ...
... ... @@ -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 23 <section class="tb-rulechain-container" flex layout="column">
22 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 73 <md-expansion-panel md-component-id="{{typeId}}" id="{{typeId}}" ng-repeat="(typeId, typeModel) in vm.ruleNodeTypesModel">
26 74 <md-expansion-panel-collapsed ng-mouseenter="vm.typeHeaderMouseEnter($event, typeId)"
27 75 ng-mouseleave="vm.destroyTooltips()">
... ... @@ -47,11 +95,9 @@
47 95 </md-expansion-panel-expanded>
48 96 </md-expansion-panel>
49 97 </md-expansion-panel-group>
50   - </div>
  98 + </md-sidenav>
51 99 <div flex class="tb-rulechain-graph">
52 100 <fc-canvas id="tb-rulchain-canvas"
53   - ng-keydown="vm.keyDown($event)"
54   - ng-keyup="vm.keyUp($event)"
55 101 model="vm.ruleChainModel"
56 102 selected-objects="vm.selectedObjects"
57 103 edge-style="curved"
... ... @@ -65,9 +111,11 @@
65 111 </div>
66 112 <tb-details-sidenav class="tb-rulenode-details-sidenav"
67 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 117 is-open="vm.isEditingRuleNode"
  118 + tb-enable-backdrop
71 119 is-always-edit="true"
72 120 on-close-details="vm.onEditRuleNodeClosed()"
73 121 on-toggle-details-edit-mode="vm.onRevertRuleNodeEdit(vm.ruleNodeForm)"
... ... @@ -76,22 +124,37 @@
76 124 <details-buttons tb-help="vm.helpLinkIdForRuleNodeType()" help-container-id="help-container">
77 125 <div id="help-container"></div>
78 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 151 </tb-details-sidenav>
90 152 <tb-details-sidenav class="tb-rulenode-link-details-sidenav"
91 153 header-title="{{vm.editingRuleNodeLink.label}}"
92 154 header-subtitle="{{'rulenode.link-details' | translate}}"
93 155 is-read-only="false"
94 156 is-open="vm.isEditingRuleNodeLink"
  157 + tb-enable-backdrop
95 158 is-always-edit="true"
96 159 on-close-details="vm.onEditRuleNodeLinkClosed()"
97 160 on-toggle-details-edit-mode="vm.onRevertRuleNodeLinkEdit(vm.ruleNodeLinkForm)"
... ... @@ -112,6 +175,13 @@
112 175 </tb-details-sidenav>
113 176 </section>
114 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 185 <md-button ng-disabled="$root.loading || !vm.isDirty"
116 186 class="tb-btn-footer md-accent md-hue-2 md-fab"
117 187 aria-label="{{ 'action.apply' | translate }}"
... ...
... ... @@ -55,7 +55,8 @@
55 55 <tb-event-table flex entity-type="vm.types.entityType.rulechain"
56 56 entity-id="vm.grid.operatingItem().id.id"
57 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 60 </tb-event-table>
60 61 </md-tab>
61 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 21
22 22 <md-content class="md-padding tb-rulenode" layout="column">
23 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 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 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 44 <md-input-container class="md-block">
47 45 <label translate>rulenode.description</label>
48 46 <textarea ng-model="ruleNode.additionalInfo.description" rows="2"></textarea>
... ...
... ... @@ -19,4 +19,10 @@
19 19 height: 300px;
20 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 }
\ No newline at end of file
... ...
... ... @@ -22,6 +22,7 @@
22 22 ng-mousedown="callbacks.mouseDown($event, node)"
23 23 ng-mouseenter="callbacks.mouseEnter($event, node)"
24 24 ng-mouseleave="callbacks.mouseLeave($event, node)">
  25 + <div class="{{flowchartConstants.nodeOverlayClass}}"></div>
25 26 <div class="tb-rule-node {{node.nodeClass}}">
26 27 <md-icon aria-label="node-type-icon" flex="15"
27 28 class="material-icons">{{node.icon}}</md-icon>
... ...