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