Browse Source

Merge branch 'develop/1.5' of github.com:thingsboard/thingsboard into develop/1.5

pull/725/head
Andrew Shvayka 9 years ago
parent
commit
5aac13b527
  1. 71
      application/src/main/java/org/thingsboard/server/actors/ActorSystemContext.java
  2. 2
      application/src/main/java/org/thingsboard/server/service/component/AnnotationComponentDiscoveryService.java
  3. 2
      application/src/test/java/org/thingsboard/server/controller/AbstractRuleEngineControllerTest.java
  4. 2
      common/data/src/main/java/org/thingsboard/server/common/data/DataConstants.java
  5. 25
      dao/src/main/java/org/thingsboard/server/dao/exception/BufferLimitException.java
  6. 14
      dao/src/main/java/org/thingsboard/server/dao/nosql/RateLimitedResultSetFuture.java
  7. 24
      dao/src/main/java/org/thingsboard/server/dao/util/BufferedRateLimiter.java
  8. 32
      dao/src/test/java/org/thingsboard/server/dao/nosql/RateLimitedResultSetFutureTest.java
  9. 5
      dao/src/test/java/org/thingsboard/server/dao/util/BufferedRateLimiterTest.java
  10. 1
      pom.xml
  11. 2
      rule-engine/rule-engine-api/src/main/java/org/thingsboard/rule/engine/api/NodeDefinition.java
  12. 4
      rule-engine/rule-engine-api/src/main/java/org/thingsboard/rule/engine/api/RuleNode.java
  13. 7
      rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/filter/TbJsFilterNode.java
  14. 2
      rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/filter/TbJsFilterNodeConfiguration.java
  15. 27
      rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/filter/TbJsSwitchNode.java
  16. 8
      rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/filter/TbJsSwitchNodeConfiguration.java
  17. 4
      rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/filter/TbMsgTypeFilterNode.java
  18. 2
      rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/filter/TbMsgTypeFilterNodeConfiguration.java
  19. 18
      rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/js/NashornJsEngine.java
  20. 2
      rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/metadata/TbGetAttributesNode.java
  21. 2
      rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/metadata/TbGetCustomerAttributeNode.java
  22. 2
      rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/metadata/TbGetRelatedAttributeNode.java
  23. 2
      rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/metadata/TbGetTenantAttributeNode.java
  24. 4
      rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/transform/TbTransformMsgNode.java
  25. 2
      rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/transform/TbTransformMsgNodeConfiguration.java
  26. 2
      rule-engine/rule-engine-components/src/main/resources/public/static/rulenode/rulenode-core-config.css
  27. 2
      rule-engine/rule-engine-components/src/main/resources/public/static/rulenode/rulenode-core-config.js
  28. 14
      rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/filter/TbJsFilterNodeTest.java
  29. 52
      rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/filter/TbJsSwitchNodeTest.java
  30. 6
      rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/transform/TbTransformMsgNodeTest.java
  31. 2
      ui/package.json
  32. 20
      ui/server.js
  33. 59
      ui/src/app/api/rule-chain.service.js
  34. 30
      ui/src/app/common/types.constant.js
  35. 59
      ui/src/app/components/details-sidenav.directive.js
  36. 2
      ui/src/app/components/details-sidenav.tpl.html
  37. 34
      ui/src/app/components/js-func.directive.js
  38. 10
      ui/src/app/components/js-func.scss
  39. 15
      ui/src/app/components/js-func.tpl.html
  40. 23
      ui/src/app/components/json-object-edit.directive.js
  41. 17
      ui/src/app/event/event-content-dialog.controller.js
  42. 27
      ui/src/app/event/event-header-debug-rulenode.tpl.html
  43. 7
      ui/src/app/event/event-header.directive.js
  44. 63
      ui/src/app/event/event-row-debug-rulenode.tpl.html
  45. 16
      ui/src/app/event/event-row.directive.js
  46. 18
      ui/src/app/event/event-table.directive.js
  47. 20
      ui/src/app/locale/locale.constant.js
  48. 4
      ui/src/app/rulechain/index.js
  49. 40
      ui/src/app/rulechain/rulechain.controller.js
  50. 41
      ui/src/app/rulechain/rulechain.tpl.html
  51. 3
      ui/src/app/rulechain/rulechains.tpl.html
  52. 78
      ui/src/app/rulechain/rulenode-config.directive.js
  53. 32
      ui/src/app/rulechain/rulenode-config.tpl.html
  54. 67
      ui/src/app/rulechain/rulenode-defined-config.directive.js
  55. 38
      ui/src/app/rulechain/rulenode-fieldset.tpl.html
  56. 6
      ui/src/app/rulechain/rulenode.scss

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

1
pom.xml

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

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

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

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

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

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

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

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

@ -26,7 +26,7 @@ public class TbJsFilterNodeConfiguration implements NodeConfiguration {
@Override
public TbJsFilterNodeConfiguration defaultConfiguration() {
TbJsFilterNodeConfiguration configuration = new TbJsFilterNodeConfiguration();
configuration.setJsScript("msg.passed < 15 && msg.name === 'Vit' && meta.temp == 10 && msg.bigObj.prop == 42;");
configuration.setJsScript("return msg.passed < 15 && msg.name === 'Vit' && metadata.temp == 10 && msg.bigObj.prop == 42;");
return configuration;
}
}

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

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

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

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

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

@ -31,7 +31,9 @@ import org.thingsboard.server.common.msg.TbMsg;
configClazz = TbMsgTypeFilterNodeConfiguration.class,
nodeDescription = "Filter incoming messages by Message Type",
nodeDetails = "Evaluate incoming Message with configured JS condition. " +
"If incoming MessageType is expected - send Message via <b>Success</b> chain, otherwise <b>Failure</b> chain is used.")
"If incoming MessageType is expected - send Message via <b>Success</b> chain, otherwise <b>Failure</b> chain is used.",
uiResources = {"static/rulenode/rulenode-core-config.js", "static/rulenode/rulenode-core-config.css"},
configDirective = "tbFilterNodeMessageTypeConfig")
public class TbMsgTypeFilterNode implements TbNode {
TbMsgTypeFilterNodeConfiguration config;

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

@ -33,7 +33,7 @@ public class TbMsgTypeFilterNodeConfiguration implements NodeConfiguration {
@Override
public TbMsgTypeFilterNodeConfiguration defaultConfiguration() {
TbMsgTypeFilterNodeConfiguration configuration = new TbMsgTypeFilterNodeConfiguration();
configuration.setMessageTypes(Arrays.asList("GET_ATTRIBUTES","POST_ATTRIBUTES","POST_TELEMETRY","RPC_REQUEST"));
configuration.setMessageTypes(Arrays.asList("POST_ATTRIBUTES","POST_TELEMETRY","RPC_REQUEST"));
return configuration;
}
}

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

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

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

@ -42,7 +42,7 @@ import static org.thingsboard.server.common.data.DataConstants.*;
nodeDescription = "Add Message Originator Attributes or Latest Telemetry into Message Metadata",
nodeDetails = "If Attributes enrichment configured, <b>CLIENT/SHARED/SERVER</b> attributes are added into Message metadata " +
"with specific prefix: <i>cs/shared/ss</i>. To access those attributes in other nodes this template can be used " +
"<code>meta.cs.temperature</code> or <code>meta.shared.limit</code> " +
"<code>metadata.cs.temperature</code> or <code>metadata.shared.limit</code> " +
"If Latest Telemetry enrichment configured, latest telemetry added into metadata without prefix.")
public class TbGetAttributesNode implements TbNode {

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

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

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

@ -32,7 +32,7 @@ import org.thingsboard.server.common.data.plugin.ComponentType;
"If multiple Related Entities are found, only first Entity is used for attributes enrichment, other entities are discarded. " +
"If Attributes enrichment configured, server scope attributes are added into Message metadata. " +
"To access those attributes in other nodes this template can be used " +
"<code>meta.temperature</code>. If Latest Telemetry enrichment configured, latest telemetry added into metadata")
"<code>metadata.temperature</code>. If Latest Telemetry enrichment configured, latest telemetry added into metadata")
public class TbGetRelatedAttributeNode extends TbEntityGetAttrNode<EntityId> {
private TbGetRelatedAttrNodeConfiguration config;

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

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

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

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

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

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

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

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

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

File diff suppressed because one or more lines are too long

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

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

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

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

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

@ -51,7 +51,7 @@ public class TbTransformMsgNodeTest {
@Test
public void metadataCanBeUpdated() throws TbNodeException {
initWithScript("meta.temp = meta.temp * 10;");
initWithScript("return metadata.temp = metadata.temp * 10;");
TbMsgMetaData metaData = new TbMsgMetaData();
metaData.putValue("temp", "7");
metaData.putValue("humidity", "99");
@ -70,7 +70,7 @@ public class TbTransformMsgNodeTest {
@Test
public void metadataCanBeAdded() throws TbNodeException {
initWithScript("meta.newAttr = meta.humidity - msg.passed;");
initWithScript("return metadata.newAttr = metadata.humidity - msg.passed;");
TbMsgMetaData metaData = new TbMsgMetaData();
metaData.putValue("temp", "7");
metaData.putValue("humidity", "99");
@ -89,7 +89,7 @@ public class TbTransformMsgNodeTest {
@Test
public void payloadCanBeUpdated() throws TbNodeException {
initWithScript("msg.passed = msg.passed * meta.temp; msg.bigObj.newProp = 'Ukraine' ");
initWithScript("return msg.passed = msg.passed * metadata.temp; msg.bigObj.newProp = 'Ukraine' ");
TbMsgMetaData metaData = new TbMsgMetaData();
metaData.putValue("temp", "7");
metaData.putValue("humidity", "99");

2
ui/package.json

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

20
ui/server.js

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

@ -0,0 +1,27 @@
<!--
Copyright © 2016-2018 The Thingsboard Authors
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
-->
<div hide-xs hide-sm translate class="tb-cell" flex="30">event.event-time</div>
<div translate class="tb-cell" flex="20">event.server</div>
<div translate class="tb-cell" flex="20">event.type</div>
<div translate class="tb-cell" flex="20">event.entity</div>
<div translate class="tb-cell" flex="20">event.message-id</div>
<div translate class="tb-cell" flex="20">event.message-type</div>
<div translate class="tb-cell" flex="20">event.data-type</div>
<div translate class="tb-cell" flex="20">event.data</div>
<div translate class="tb-cell" flex="20">event.metadata</div>
<div translate class="tb-cell" flex="20">event.error</div>

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

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

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

@ -0,0 +1,63 @@
<!--
Copyright © 2016-2018 The Thingsboard Authors
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
-->
<div hide-xs hide-sm class="tb-cell" flex="30">{{event.createdTime | date : 'yyyy-MM-dd HH:mm:ss'}}</div>
<div class="tb-cell" flex="20">{{event.body.server}}</div>
<div class="tb-cell" flex="20">{{event.body.type}}</div>
<div class="tb-cell" flex="20">{{event.body.entityName}}</div>
<div class="tb-cell" flex="20">{{event.body.msgId}}</div>
<div class="tb-cell" flex="20">{{event.body.msgType}}</div>
<div class="tb-cell" flex="20">{{event.body.dataType}}</div>
<div class="tb-cell" flex="20">
<md-button ng-if="event.body.data" class="md-icon-button md-primary"
ng-click="showContent($event, event.body.data, 'event.data', event.body.msgType)"
aria-label="{{ 'action.view' | translate }}">
<md-tooltip md-direction="top">
{{ 'action.view' | translate }}
</md-tooltip>
<md-icon aria-label="{{ 'action.view' | translate }}"
class="material-icons">
more_horiz
</md-icon>
</md-button>
</div>
<div class="tb-cell" flex="20">
<md-button ng-if="event.body.metadata" class="md-icon-button md-primary"
ng-click="showContent($event, event.body.metadata, 'event.metadata', 'JSON')"
aria-label="{{ 'action.view' | translate }}">
<md-tooltip md-direction="top">
{{ 'action.view' | translate }}
</md-tooltip>
<md-icon aria-label="{{ 'action.view' | translate }}"
class="material-icons">
more_horiz
</md-icon>
</md-button>
</div>
<div class="tb-cell" flex="20">
<md-button ng-if="event.body.error" class="md-icon-button md-primary"
ng-click="showContent($event, event.body.error, 'event.error')"
aria-label="{{ 'action.view' | translate }}">
<md-tooltip md-direction="top">
{{ 'action.view' | translate }}
</md-tooltip>
<md-icon aria-label="{{ 'action.view' | translate }}"
class="material-icons">
more_horiz
</md-icon>
</md-button>
</div>

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

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

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

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

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

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

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

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

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

@ -28,7 +28,7 @@ import addRuleNodeLinkTemplate from './add-link.tpl.html';
/* eslint-enable import/no-unresolved, import/default */
/*@ngInject*/
export function RuleChainController($stateParams, $scope, $compile, $q, $mdUtil, $timeout, $mdExpansionPanel, $document, $mdDialog,
export function RuleChainController($stateParams, $scope, $compile, $q, $mdUtil, $timeout, $mdExpansionPanel, $window, $document, $mdDialog,
$filter, $translate, hotkeys, types, ruleChainService, Modelfactory, flowchartConstants,
ruleChain, ruleChainMetaData, ruleNodeComponents) {
@ -77,6 +77,8 @@ export function RuleChainController($stateParams, $scope, $compile, $q, $mdUtil,
vm.objectsSelected = objectsSelected;
vm.deleteSelected = deleteSelected;
vm.triggerResize = triggerResize;
initHotKeys();
function initHotKeys() {
@ -129,23 +131,24 @@ export function RuleChainController($stateParams, $scope, $compile, $q, $mdUtil,
}
vm.onEditRuleNodeClosed = function() {
vm.editingRuleNode = null;
//vm.editingRuleNode = null;
};
vm.onEditRuleNodeLinkClosed = function() {
vm.editingRuleNodeLink = null;
//vm.editingRuleNodeLink = null;
};
vm.saveRuleNode = function(theForm) {
theForm.$setPristine();
vm.isEditingRuleNode = false;
vm.ruleChainModel.nodes[vm.editingRuleNodeIndex] = vm.editingRuleNode;
vm.editingRuleNode = angular.copy(vm.editingRuleNode);
$scope.$broadcast('form-submit');
if (theForm.$valid) {
theForm.$setPristine();
vm.ruleChainModel.nodes[vm.editingRuleNodeIndex] = vm.editingRuleNode;
vm.editingRuleNode = angular.copy(vm.editingRuleNode);
}
};
vm.saveRuleNodeLink = function(theForm) {
theForm.$setPristine();
vm.isEditingRuleNodeLink = false;
vm.ruleChainModel.edges[vm.editingRuleNodeLinkIndex] = vm.editingRuleNodeLink;
vm.editingRuleNodeLink = angular.copy(vm.editingRuleNodeLink);
};
@ -253,6 +256,9 @@ export function RuleChainController($stateParams, $scope, $compile, $q, $mdUtil,
vm.isEditingRuleNodeLink = true;
vm.editingRuleNodeLinkIndex = vm.ruleChainModel.edges.indexOf(edge);
vm.editingRuleNodeLink = angular.copy(edge);
$mdUtil.nextTick(() => {
vm.ruleNodeLinkForm.$setPristine();
});
}
},
nodeCallbacks: {
@ -263,6 +269,9 @@ export function RuleChainController($stateParams, $scope, $compile, $q, $mdUtil,
vm.isEditingRuleNode = true;
vm.editingRuleNodeIndex = vm.ruleChainModel.nodes.indexOf(node);
vm.editingRuleNode = angular.copy(node);
$mdUtil.nextTick(() => {
vm.ruleNodeForm.$setPristine();
});
}
}
},
@ -309,7 +318,7 @@ export function RuleChainController($stateParams, $scope, $compile, $q, $mdUtil,
var componentType = ruleNodeComponent.type;
var model = vm.ruleNodeTypesModel[componentType].model;
var node = {
id: model.nodes.length,
id: 'node-lib-' + componentType + '-' + model.nodes.length,
component: ruleNodeComponent,
name: '',
nodeClass: vm.types.ruleNodeType[componentType].nodeClass,
@ -358,7 +367,7 @@ export function RuleChainController($stateParams, $scope, $compile, $q, $mdUtil,
vm.ruleChainModel.nodes.push(
{
id: vm.nextNodeID++,
id: 'rule-chain-node-' + vm.nextNodeID++,
component: types.inputNodeComponent,
name: "",
nodeClass: types.ruleNodeType.INPUT.nodeClass,
@ -389,7 +398,7 @@ export function RuleChainController($stateParams, $scope, $compile, $q, $mdUtil,
var component = ruleChainService.getRuleNodeComponentByClazz(ruleNode.type);
if (component) {
var node = {
id: vm.nextNodeID++,
id: 'rule-chain-node-' + vm.nextNodeID++,
ruleNodeId: ruleNode.id,
additionalInfo: ruleNode.additionalInfo,
configuration: ruleNode.configuration,
@ -466,7 +475,7 @@ export function RuleChainController($stateParams, $scope, $compile, $q, $mdUtil,
var ruleChainNode = ruleChainNodesMap[ruleChainConnection.additionalInfo.ruleChainNodeId];
if (!ruleChainNode) {
ruleChainNode = {
id: vm.nextNodeID++,
id: 'rule-chain-node-' + vm.nextNodeID++,
additionalInfo: ruleChainConnection.additionalInfo,
targetRuleChainId: ruleChainConnection.targetRuleChainId.id,
x: ruleChainConnection.additionalInfo.layoutX,
@ -611,7 +620,7 @@ export function RuleChainController($stateParams, $scope, $compile, $q, $mdUtil,
fullscreen: true,
targetEvent: $event
}).then(function (ruleNode) {
ruleNode.id = vm.nextNodeID++;
ruleNode.id = 'rule-chain-node-' + vm.nextNodeID++;
ruleNode.connectors = [];
if (ruleNode.component.configurationDescriptor.nodeDefinition.inEnabled) {
ruleNode.connectors.push(
@ -654,6 +663,11 @@ export function RuleChainController($stateParams, $scope, $compile, $q, $mdUtil,
function deleteSelected() {
vm.modelservice.deleteSelected();
}
function triggerResize() {
var w = angular.element($window);
w.triggerHandler('resize');
}
}
/*@ngInject*/

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

@ -65,9 +65,11 @@
</div>
<tb-details-sidenav class="tb-rulenode-details-sidenav"
header-title="{{vm.editingRuleNode.name}}"
header-subtitle="{{'rulenode.rulenode-details' | translate}}"
is-read-only="false"
header-subtitle="{{(vm.types.ruleNodeType[vm.editingRuleNode.component.type].name | translate)
+ ' - ' + vm.editingRuleNode.component.name}}"
is-read-only="vm.selectedRuleNodeTabIndex > 0"
is-open="vm.isEditingRuleNode"
tb-enable-backdrop
is-always-edit="true"
on-close-details="vm.onEditRuleNodeClosed()"
on-toggle-details-edit-mode="vm.onRevertRuleNodeEdit(vm.ruleNodeForm)"
@ -76,22 +78,37 @@
<details-buttons tb-help="vm.helpLinkIdForRuleNodeType()" help-container-id="help-container">
<div id="help-container"></div>
</details-buttons>
<form name="vm.ruleNodeForm" ng-if="vm.isEditingRuleNode">
<tb-rule-node
rule-node="vm.editingRuleNode"
rule-chain-id="vm.ruleChain.id.id"
is-edit="true"
is-read-only="false"
on-delete-rule-node="vm.deleteRuleNode(event, vm.editingRuleNode)"
the-form="vm.ruleNodeForm">
</tb-rule-node>
</form>
<md-tabs md-selected="vm.selectedRuleNodeTabIndex"
id="ruleNodeTabs" md-border-bottom flex class="tb-absolute-fill" ng-if="vm.isEditingRuleNode">
<md-tab label="{{ 'rulenode.details' | translate }}">
<form name="vm.ruleNodeForm">
<tb-rule-node
rule-node="vm.editingRuleNode"
rule-chain-id="vm.ruleChain.id.id"
is-edit="true"
is-read-only="false"
on-delete-rule-node="vm.deleteRuleNode(event, vm.editingRuleNode)"
the-form="vm.ruleNodeForm">
</tb-rule-node>
</form>
</md-tab>
<md-tab ng-if="vm.isEditingRuleNode && vm.editingRuleNode.ruleNodeId"
md-on-select="vm.triggerResize()" label="{{ 'rulenode.events' | translate }}">
<tb-event-table flex entity-type="vm.types.entityType.rulenode"
entity-id="vm.editingRuleNode.ruleNodeId.id"
tenant-id="vm.ruleChain.tenantId.id"
debug-event-types="{{vm.types.debugEventType.debugRuleNode.value}}"
default-event-type="{{vm.types.debugEventType.debugRuleNode.value}}">
</tb-event-table>
</md-tab>
</md-tabs>
</tb-details-sidenav>
<tb-details-sidenav class="tb-rulenode-link-details-sidenav"
header-title="{{vm.editingRuleNodeLink.label}}"
header-subtitle="{{'rulenode.link-details' | translate}}"
is-read-only="false"
is-open="vm.isEditingRuleNodeLink"
tb-enable-backdrop
is-always-edit="true"
on-close-details="vm.onEditRuleNodeLinkClosed()"
on-toggle-details-edit-mode="vm.onRevertRuleNodeLinkEdit(vm.ruleNodeLinkForm)"

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

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

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

@ -0,0 +1,78 @@
/*
* Copyright © 2016-2018 The Thingsboard Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
/* eslint-disable import/no-unresolved, import/default */
import ruleNodeConfigTemplate from './rulenode-config.tpl.html';
/* eslint-enable import/no-unresolved, import/default */
/*@ngInject*/
export default function RuleNodeConfigDirective($compile, $templateCache, $injector, $translate) {
var linker = function (scope, element, attrs, ngModelCtrl) {
var template = $templateCache.get(ruleNodeConfigTemplate);
element.html(template);
scope.$watch('configuration', function (newVal, prevVal) {
if (!angular.equals(newVal, prevVal)) {
ngModelCtrl.$setViewValue(scope.configuration);
}
});
ngModelCtrl.$render = function () {
scope.configuration = ngModelCtrl.$viewValue;
};
scope.useDefinedDirective = function() {
return scope.nodeDefinition &&
scope.nodeDefinition.configDirective && !scope.definedDirectiveError;
};
scope.$watch('nodeDefinition', () => {
if (scope.nodeDefinition) {
validateDefinedDirective();
}
});
function validateDefinedDirective() {
if (scope.nodeDefinition.uiResourceLoadError && scope.nodeDefinition.uiResourceLoadError.length) {
scope.definedDirectiveError = scope.nodeDefinition.uiResourceLoadError;
} else {
var definedDirective = scope.nodeDefinition.configDirective;
if (definedDirective && definedDirective.length) {
if (!$injector.has(definedDirective + 'Directive')) {
scope.definedDirectiveError = $translate.instant('rulenode.directive-is-not-loaded', {directiveName: definedDirective});
}
}
}
}
$compile(element.contents())(scope);
};
return {
restrict: "E",
require: "^ngModel",
scope: {
nodeDefinition:'=',
required:'=ngRequired',
readonly:'=ngReadonly'
},
link: linker
};
}

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

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

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

@ -0,0 +1,67 @@
/*
* Copyright © 2016-2018 The Thingsboard Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
const SNAKE_CASE_REGEXP = /[A-Z]/g;
/*@ngInject*/
export default function RuleNodeDefinedConfigDirective($compile) {
var linker = function (scope, element, attrs, ngModelCtrl) {
attrs.$observe('ruleNodeDirective', function() {
loadTemplate();
});
scope.$watch('configuration', function (newVal, prevVal) {
if (!angular.equals(newVal, prevVal)) {
ngModelCtrl.$setViewValue(scope.configuration);
}
});
ngModelCtrl.$render = function () {
scope.configuration = ngModelCtrl.$viewValue;
};
function loadTemplate() {
if (scope.ruleNodeConfigScope) {
scope.ruleNodeConfigScope.$destroy();
}
var directive = snake_case(attrs.ruleNodeDirective, '-');
var template = `<${directive} ng-model="configuration" ng-required="required" ng-readonly="readonly"></${directive}>`;
element.html(template);
scope.ruleNodeConfigScope = scope.$new();
$compile(element.contents())(scope.ruleNodeConfigScope);
}
function snake_case(name, separator) {
separator = separator || '_';
return name.replace(SNAKE_CASE_REGEXP, function(letter, pos) {
return (pos ? separator : '') + letter.toLowerCase();
});
}
};
return {
restrict: "E",
require: "^ngModel",
scope: {
required:'=ngRequired',
readonly:'=ngReadonly'
},
link: linker
};
}

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

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

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

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