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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

1
pom.xml

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

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

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

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

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

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

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 @Override
public TbJsFilterNodeConfiguration defaultConfiguration() { public TbJsFilterNodeConfiguration defaultConfiguration() {
TbJsFilterNodeConfiguration configuration = new TbJsFilterNodeConfiguration(); TbJsFilterNodeConfiguration configuration = new TbJsFilterNodeConfiguration();
configuration.setJsScript("msg.passed < 15 && msg.name === 'Vit' && meta.temp == 10 && msg.bigObj.prop == 42;"); configuration.setJsScript("return msg.passed < 15 && msg.name === 'Vit' && metadata.temp == 10 && msg.bigObj.prop == 42;");
return configuration; return configuration;
} }
} }

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

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

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, configClazz = TbMsgTypeFilterNodeConfiguration.class,
nodeDescription = "Filter incoming messages by Message Type", nodeDescription = "Filter incoming messages by Message Type",
nodeDetails = "Evaluate incoming Message with configured JS condition. " + nodeDetails = "Evaluate incoming Message with configured JS condition. " +
"If incoming MessageType is expected - send Message via <b>Success</b> chain, otherwise <b>Failure</b> chain is used.") "If incoming MessageType is expected - send Message via <b>Success</b> chain, otherwise <b>Failure</b> chain is used.",
uiResources = {"static/rulenode/rulenode-core-config.js", "static/rulenode/rulenode-core-config.css"},
configDirective = "tbFilterNodeMessageTypeConfig")
public class TbMsgTypeFilterNode implements TbNode { public class TbMsgTypeFilterNode implements TbNode {
TbMsgTypeFilterNodeConfiguration config; TbMsgTypeFilterNodeConfiguration config;

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 @Override
public TbMsgTypeFilterNodeConfiguration defaultConfiguration() { public TbMsgTypeFilterNodeConfiguration defaultConfiguration() {
TbMsgTypeFilterNodeConfiguration configuration = new TbMsgTypeFilterNodeConfiguration(); TbMsgTypeFilterNodeConfiguration configuration = new TbMsgTypeFilterNodeConfiguration();
configuration.setMessageTypes(Arrays.asList("GET_ATTRIBUTES","POST_ATTRIBUTES","POST_TELEMETRY","RPC_REQUEST")); configuration.setMessageTypes(Arrays.asList("POST_ATTRIBUTES","POST_TELEMETRY","RPC_REQUEST"));
return configuration; return configuration;
} }
} }

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

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

2
rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/metadata/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", nodeDescription = "Add Message Originator Attributes or Latest Telemetry into Message Metadata",
nodeDetails = "If Attributes enrichment configured, <b>CLIENT/SHARED/SERVER</b> attributes are added into Message metadata " + nodeDetails = "If Attributes enrichment configured, <b>CLIENT/SHARED/SERVER</b> attributes are added into Message metadata " +
"with specific prefix: <i>cs/shared/ss</i>. To access those attributes in other nodes this template can be used " + "with specific prefix: <i>cs/shared/ss</i>. To access those attributes in other nodes this template can be used " +
"<code>meta.cs.temperature</code> or <code>meta.shared.limit</code> " + "<code>metadata.cs.temperature</code> or <code>metadata.shared.limit</code> " +
"If Latest Telemetry enrichment configured, latest telemetry added into metadata without prefix.") "If Latest Telemetry enrichment configured, latest telemetry added into metadata without prefix.")
public class TbGetAttributesNode implements TbNode { public class TbGetAttributesNode implements TbNode {

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

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

2
rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/metadata/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 multiple Related Entities are found, only first Entity is used for attributes enrichment, other entities are discarded. " +
"If Attributes enrichment configured, server scope attributes are added into Message metadata. " + "If Attributes enrichment configured, server scope attributes are added into Message metadata. " +
"To access those attributes in other nodes this template can be used " + "To access those attributes in other nodes this template can be used " +
"<code>meta.temperature</code>. If Latest Telemetry enrichment configured, latest telemetry added into metadata") "<code>metadata.temperature</code>. If Latest Telemetry enrichment configured, latest telemetry added into metadata")
public class TbGetRelatedAttributeNode extends TbEntityGetAttrNode<EntityId> { public class TbGetRelatedAttributeNode extends TbEntityGetAttrNode<EntityId> {
private TbGetRelatedAttrNodeConfiguration config; private TbGetRelatedAttrNodeConfiguration config;

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

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

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

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

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

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

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

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

File diff suppressed because one or more lines are too long

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

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

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

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

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

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

2
ui/package.json

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

20
ui/server.js

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

@ -0,0 +1,63 @@
<!--
Copyright © 2016-2018 The Thingsboard Authors
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
-->
<div hide-xs hide-sm class="tb-cell" flex="30">{{event.createdTime | date : 'yyyy-MM-dd HH:mm:ss'}}</div>
<div class="tb-cell" flex="20">{{event.body.server}}</div>
<div class="tb-cell" flex="20">{{event.body.type}}</div>
<div class="tb-cell" flex="20">{{event.body.entityName}}</div>
<div class="tb-cell" flex="20">{{event.body.msgId}}</div>
<div class="tb-cell" flex="20">{{event.body.msgType}}</div>
<div class="tb-cell" flex="20">{{event.body.dataType}}</div>
<div class="tb-cell" flex="20">
<md-button ng-if="event.body.data" class="md-icon-button md-primary"
ng-click="showContent($event, event.body.data, 'event.data', event.body.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 eventRowLcEventTemplate from './event-row-lc-event.tpl.html';
import eventRowStatsTemplate from './event-row-stats.tpl.html'; import eventRowStatsTemplate from './event-row-stats.tpl.html';
import eventRowErrorTemplate from './event-row-error.tpl.html'; import eventRowErrorTemplate from './event-row-error.tpl.html';
import eventRowDebugRuleNodeTemplate from './event-row-debug-rulenode.tpl.html';
/* eslint-enable import/no-unresolved, import/default */ /* eslint-enable import/no-unresolved, import/default */
@ -40,6 +41,12 @@ export default function EventRowDirective($compile, $templateCache, $mdDialog, $
case types.eventType.error.value: case types.eventType.error.value:
template = eventRowErrorTemplate; template = eventRowErrorTemplate;
break; break;
case types.debugEventType.debugRuleNode.value:
template = eventRowDebugRuleNodeTemplate;
break;
case types.debugEventType.debugRuleChain.value:
template = eventRowDebugRuleNodeTemplate;
break;
} }
return $templateCache.get(template); return $templateCache.get(template);
} }
@ -53,17 +60,22 @@ export default function EventRowDirective($compile, $templateCache, $mdDialog, $
scope.loadTemplate(); scope.loadTemplate();
}); });
scope.types = types;
scope.event = attrs.event; scope.event = attrs.event;
scope.showContent = function($event, content, title) { scope.showContent = function($event, content, title, contentType) {
var onShowingCallback = { var onShowingCallback = {
onShowing: function(){} onShowing: function(){}
} }
if (!contentType) {
contentType = null;
}
$mdDialog.show({ $mdDialog.show({
controller: 'EventContentDialogController', controller: 'EventContentDialogController',
controllerAs: 'vm', controllerAs: 'vm',
templateUrl: eventErrorDialogTemplate, templateUrl: eventErrorDialogTemplate,
locals: {content: content, title: title, showingCallback: onShowingCallback}, locals: {content: content, title: title, contentType: contentType, showingCallback: onShowingCallback},
parent: angular.element($document[0].body), parent: angular.element($document[0].body),
fullscreen: true, fullscreen: true,
targetEvent: $event, targetEvent: $event,

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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