diff --git a/application/src/main/java/org/thingsboard/server/actors/ActorSystemContext.java b/application/src/main/java/org/thingsboard/server/actors/ActorSystemContext.java index a5a20b83fa..9e02946574 100644 --- a/application/src/main/java/org/thingsboard/server/actors/ActorSystemContext.java +++ b/application/src/main/java/org/thingsboard/server/actors/ActorSystemContext.java @@ -25,6 +25,7 @@ import com.typesafe.config.Config; import com.typesafe.config.ConfigFactory; import lombok.Getter; import lombok.Setter; +import lombok.extern.slf4j.Slf4j; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Value; import org.springframework.stereotype.Component; @@ -38,6 +39,7 @@ import org.thingsboard.server.common.data.id.EntityId; import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.common.data.plugin.ComponentLifecycleEvent; import org.thingsboard.server.common.msg.TbMsg; +import org.thingsboard.server.common.msg.TbMsgDataType; import org.thingsboard.server.common.msg.cluster.ServerAddress; import org.thingsboard.server.common.transport.auth.DeviceAuthService; import org.thingsboard.server.controller.plugin.PluginWebSocketMsgEndpoint; @@ -60,11 +62,13 @@ import org.thingsboard.server.service.cluster.routing.ClusterRoutingService; import org.thingsboard.server.service.cluster.rpc.ClusterRpcService; import org.thingsboard.server.service.component.ComponentDiscoveryService; +import java.io.IOException; import java.io.PrintWriter; import java.io.StringWriter; import java.nio.charset.StandardCharsets; import java.util.Optional; +@Slf4j @Component public class ActorSystemContext { private static final String AKKA_CONF_FILE_NAME = "actor-system.conf"; @@ -292,38 +296,49 @@ public class ActorSystemContext { } private void persistDebug(TenantId tenantId, EntityId entityId, String type, TbMsg tbMsg, Throwable error) { - Event event = new Event(); - event.setTenantId(tenantId); - event.setEntityId(entityId); - event.setType(DataConstants.DEBUG); - - ObjectNode node = mapper.createObjectNode() - .put("type", type) - .put("server", getServerAddress()) - .put("entityId", tbMsg.getOriginator().getId().toString()) - .put("entityName", tbMsg.getOriginator().getEntityType().name()) - .put("msgId", tbMsg.getId().toString()) - .put("msgType", tbMsg.getType()) - .put("dataType", tbMsg.getDataType().name()); - - ObjectNode mdNode = node.putObject("metadata"); - tbMsg.getMetaData().getData().forEach(mdNode::put); + try { + Event event = new Event(); + event.setTenantId(tenantId); + event.setEntityId(entityId); + event.setType(DataConstants.DEBUG_RULE_NODE); + + String metadata = mapper.writeValueAsString(tbMsg.getMetaData().getData()); + + ObjectNode node = mapper.createObjectNode() + .put("type", type) + .put("server", getServerAddress()) + .put("entityId", tbMsg.getOriginator().getId().toString()) + .put("entityName", tbMsg.getOriginator().getEntityType().name()) + .put("msgId", tbMsg.getId().toString()) + .put("msgType", tbMsg.getType()) + .put("dataType", tbMsg.getDataType().name()) + .put("data", convertToString(tbMsg.getDataType(), tbMsg.getData())) + .put("metadata", metadata); + + if (error != null) { + node = node.put("error", toString(error)); + } + + event.setBody(node); + eventService.save(event); + } catch (IOException ex) { + log.warn("Failed to persist rule node debug message", ex); + } + } - switch (tbMsg.getDataType()) { + private String convertToString(TbMsgDataType messageType, byte[] data) { + if (data == null) { + return null; + } + switch (messageType) { + case JSON: + case TEXT: + return new String(data, StandardCharsets.UTF_8); case BINARY: - node.put("data", Base64Utils.encodeUrlSafe(tbMsg.getData())); - break; + return Base64Utils.encodeToString(data); default: - node.put("data", new String(tbMsg.getData(), StandardCharsets.UTF_8)); - break; - } - - if (error != null) { - node = node.put("error", toString(error)); + throw new RuntimeException("Message type: " + messageType + " is not supported!"); } - - event.setBody(node); - eventService.save(event); } public static Exception toException(Throwable error) { diff --git a/application/src/main/java/org/thingsboard/server/service/component/AnnotationComponentDiscoveryService.java b/application/src/main/java/org/thingsboard/server/service/component/AnnotationComponentDiscoveryService.java index 479f424e93..9377756b86 100644 --- a/application/src/main/java/org/thingsboard/server/service/component/AnnotationComponentDiscoveryService.java +++ b/application/src/main/java/org/thingsboard/server/service/component/AnnotationComponentDiscoveryService.java @@ -192,6 +192,8 @@ public class AnnotationComponentDiscoveryService implements ComponentDiscoverySe NodeConfiguration config = configClazz.newInstance(); NodeConfiguration defaultConfiguration = config.defaultConfiguration(); nodeDefinition.setDefaultConfiguration(mapper.valueToTree(defaultConfiguration)); + nodeDefinition.setUiResources(nodeAnnotation.uiResources()); + nodeDefinition.setConfigDirective(nodeAnnotation.configDirective()); return nodeDefinition; } diff --git a/application/src/test/java/org/thingsboard/server/controller/AbstractRuleEngineControllerTest.java b/application/src/test/java/org/thingsboard/server/controller/AbstractRuleEngineControllerTest.java index bbcb98ff48..93fe76752e 100644 --- a/application/src/test/java/org/thingsboard/server/controller/AbstractRuleEngineControllerTest.java +++ b/application/src/test/java/org/thingsboard/server/controller/AbstractRuleEngineControllerTest.java @@ -51,6 +51,6 @@ public class AbstractRuleEngineControllerTest extends AbstractControllerTest { TimePageLink pageLink = new TimePageLink(limit); return doGetTypedWithTimePageLink("/api/events/{entityType}/{entityId}/{eventType}?tenantId={tenantId}&", new TypeReference>() { - }, pageLink, entityId.getEntityType(), entityId.getId(), DataConstants.DEBUG, tenantId.getId()); + }, pageLink, entityId.getEntityType(), entityId.getId(), DataConstants.DEBUG_RULE_NODE, tenantId.getId()); } } diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/DataConstants.java b/common/data/src/main/java/org/thingsboard/server/common/data/DataConstants.java index 659a242882..7d4e4807c9 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/DataConstants.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/DataConstants.java @@ -37,7 +37,7 @@ public class DataConstants { public static final String ERROR = "ERROR"; public static final String LC_EVENT = "LC_EVENT"; public static final String STATS = "STATS"; - public static final String DEBUG = "DEBUG"; + public static final String DEBUG_RULE_NODE = "DEBUG_RULE_NODE"; public static final String ONEWAY = "ONEWAY"; public static final String TWOWAY = "TWOWAY"; diff --git a/dao/src/main/java/org/thingsboard/server/dao/exception/BufferLimitException.java b/dao/src/main/java/org/thingsboard/server/dao/exception/BufferLimitException.java new file mode 100644 index 0000000000..3334dc62a9 --- /dev/null +++ b/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"); + } +} diff --git a/dao/src/main/java/org/thingsboard/server/dao/nosql/RateLimitedResultSetFuture.java b/dao/src/main/java/org/thingsboard/server/dao/nosql/RateLimitedResultSetFuture.java index 2674c6ddea..d2505632d7 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/nosql/RateLimitedResultSetFuture.java +++ b/dao/src/main/java/org/thingsboard/server/dao/nosql/RateLimitedResultSetFuture.java @@ -24,6 +24,7 @@ import com.google.common.util.concurrent.FutureCallback; import com.google.common.util.concurrent.Futures; import com.google.common.util.concurrent.ListenableFuture; import com.google.common.util.concurrent.Uninterruptibles; +import org.thingsboard.server.dao.exception.BufferLimitException; import org.thingsboard.server.dao.util.AsyncRateLimiter; import javax.annotation.Nullable; @@ -35,9 +36,15 @@ public class RateLimitedResultSetFuture implements ResultSetFuture { private final ListenableFuture rateLimitFuture; public RateLimitedResultSetFuture(Session session, AsyncRateLimiter rateLimiter, Statement statement) { - this.rateLimitFuture = rateLimiter.acquireAsync(); + this.rateLimitFuture = Futures.withFallback(rateLimiter.acquireAsync(), t -> { + if (!(t instanceof BufferLimitException)) { + rateLimiter.release(); + } + return Futures.immediateFailedFuture(t); + }); this.originalFuture = Futures.transform(rateLimitFuture, (Function) i -> executeAsyncWithRelease(rateLimiter, session, statement)); + } @Override @@ -108,10 +115,7 @@ public class RateLimitedResultSetFuture implements ResultSetFuture { try { ResultSetFuture resultSetFuture = Uninterruptibles.getUninterruptibly(originalFuture); resultSetFuture.addListener(listener, executor); - } catch (CancellationException e) { - cancel(false); - return; - } catch (ExecutionException e) { + } catch (CancellationException | ExecutionException e) { Futures.immediateFailedFuture(e).addListener(listener, executor); } }, executor); diff --git a/dao/src/main/java/org/thingsboard/server/dao/util/BufferedRateLimiter.java b/dao/src/main/java/org/thingsboard/server/dao/util/BufferedRateLimiter.java index 2acd623a37..04196686c3 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/util/BufferedRateLimiter.java +++ b/dao/src/main/java/org/thingsboard/server/dao/util/BufferedRateLimiter.java @@ -23,6 +23,7 @@ import lombok.extern.slf4j.Slf4j; import org.springframework.beans.factory.annotation.Value; import org.springframework.scheduling.annotation.Scheduled; import org.springframework.stereotype.Component; +import org.thingsboard.server.dao.exception.BufferLimitException; import java.util.concurrent.*; import java.util.concurrent.atomic.AtomicInteger; @@ -41,6 +42,9 @@ public class BufferedRateLimiter implements AsyncRateLimiter { private final AtomicInteger maxQueueSize = new AtomicInteger(); private final AtomicInteger maxGrantedPermissions = new AtomicInteger(); + private final AtomicInteger totalGranted = new AtomicInteger(); + private final AtomicInteger totalReleased = new AtomicInteger(); + private final AtomicInteger totalRequested = new AtomicInteger(); public BufferedRateLimiter(@Value("${cassandra.query.buffer_size}") int queueLimit, @Value("${cassandra.query.concurrent_limit}") int permitsLimit, @@ -53,11 +57,13 @@ public class BufferedRateLimiter implements AsyncRateLimiter { @Override public ListenableFuture acquireAsync() { + totalRequested.incrementAndGet(); if (queue.isEmpty()) { if (permits.incrementAndGet() <= permitsLimit) { if (permits.get() > maxGrantedPermissions.get()) { maxGrantedPermissions.set(permits.get()); } + totalGranted.incrementAndGet(); return Futures.immediateFuture(null); } permits.decrementAndGet(); @@ -69,6 +75,7 @@ public class BufferedRateLimiter implements AsyncRateLimiter { @Override public void release() { permits.decrementAndGet(); + totalReleased.incrementAndGet(); reprocessQueue(); } @@ -80,6 +87,7 @@ public class BufferedRateLimiter implements AsyncRateLimiter { } LockedFuture lockedFuture = queue.poll(); if (lockedFuture != null) { + totalGranted.incrementAndGet(); lockedFuture.latch.countDown(); } else { permits.decrementAndGet(); @@ -112,17 +120,20 @@ public class BufferedRateLimiter implements AsyncRateLimiter { LockedFuture lockedFuture = createLockedFuture(); if (!queue.offer(lockedFuture, 1, TimeUnit.SECONDS)) { lockedFuture.cancelFuture(); - return Futures.immediateFailedFuture(new IllegalStateException("Rate Limit Buffer is full. Reject")); + return Futures.immediateFailedFuture(new BufferLimitException()); + } + if(permits.get() < permitsLimit) { + reprocessQueue(); } if(permits.get() < permitsLimit) { reprocessQueue(); } return lockedFuture.future; } catch (InterruptedException e) { - return Futures.immediateFailedFuture(new IllegalStateException("Rate Limit Task interrupted. Reject")); + return Futures.immediateFailedFuture(new BufferLimitException()); } } - return Futures.immediateFailedFuture(new IllegalStateException("Rate Limit Buffer is full. Reject")); + return Futures.immediateFailedFuture(new BufferLimitException()); } @Scheduled(fixedDelayString = "${cassandra.query.rate_limit_print_interval_ms}") @@ -134,8 +145,11 @@ public class BufferedRateLimiter implements AsyncRateLimiter { expiredCount++; } } - log.info("Permits maxBuffer is [{}] max concurrent [{}] expired [{}] current granted [{}]", maxQueueSize.getAndSet(0), - maxGrantedPermissions.getAndSet(0), expiredCount, permits.get()); + log.info("Permits maxBuffer [{}] maxPermits [{}] expired [{}] currPermits [{}] currBuffer [{}] " + + "totalPermits [{}] totalRequests [{}] totalReleased [{}]", + maxQueueSize.getAndSet(0), maxGrantedPermissions.getAndSet(0), expiredCount, + permits.get(), queue.size(), + totalGranted.getAndSet(0), totalRequested.getAndSet(0), totalReleased.getAndSet(0)); } private class LockedFuture { diff --git a/dao/src/test/java/org/thingsboard/server/dao/nosql/RateLimitedResultSetFutureTest.java b/dao/src/test/java/org/thingsboard/server/dao/nosql/RateLimitedResultSetFutureTest.java index fa62c2b9b0..f49668d3fd 100644 --- a/dao/src/test/java/org/thingsboard/server/dao/nosql/RateLimitedResultSetFutureTest.java +++ b/dao/src/test/java/org/thingsboard/server/dao/nosql/RateLimitedResultSetFutureTest.java @@ -19,16 +19,17 @@ import com.datastax.driver.core.*; import com.datastax.driver.core.exceptions.UnsupportedFeatureException; import com.google.common.util.concurrent.Futures; import com.google.common.util.concurrent.ListenableFuture; +import com.google.common.util.concurrent.MoreExecutors; import org.junit.Test; import org.junit.runner.RunWith; import org.mockito.Mock; import org.mockito.Mockito; import org.mockito.runners.MockitoJUnitRunner; import org.mockito.stubbing.Answer; +import org.thingsboard.server.dao.exception.BufferLimitException; import org.thingsboard.server.dao.util.AsyncRateLimiter; -import java.util.concurrent.ExecutionException; -import java.util.concurrent.TimeoutException; +import java.util.concurrent.*; import static org.junit.Assert.*; import static org.mockito.Mockito.*; @@ -53,7 +54,7 @@ public class RateLimitedResultSetFutureTest { @Test public void doNotReleasePermissionIfRateLimitFutureFailed() throws InterruptedException { - when(rateLimiter.acquireAsync()).thenReturn(Futures.immediateFailedFuture(new IllegalArgumentException())); + when(rateLimiter.acquireAsync()).thenReturn(Futures.immediateFailedFuture(new BufferLimitException())); resultSetFuture = new RateLimitedResultSetFuture(session, rateLimiter, statement); Thread.sleep(1000L); verify(rateLimiter).acquireAsync(); @@ -153,4 +154,29 @@ public class RateLimitedResultSetFutureTest { verify(rateLimiter, times(1)).release(); } + @Test + public void expiredQueryReturnPermit() throws InterruptedException, ExecutionException { + CountDownLatch latch = new CountDownLatch(1); + ListenableFuture future = MoreExecutors.listeningDecorator(Executors.newFixedThreadPool(1)).submit(() -> { + latch.await(); + return null; + }); + when(rateLimiter.acquireAsync()).thenReturn(future); + resultSetFuture = new RateLimitedResultSetFuture(session, rateLimiter, statement); + + ListenableFuture 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(); + } + } \ No newline at end of file diff --git a/dao/src/test/java/org/thingsboard/server/dao/util/BufferedRateLimiterTest.java b/dao/src/test/java/org/thingsboard/server/dao/util/BufferedRateLimiterTest.java index 5bfc3b6e95..67c3ce8d73 100644 --- a/dao/src/test/java/org/thingsboard/server/dao/util/BufferedRateLimiterTest.java +++ b/dao/src/test/java/org/thingsboard/server/dao/util/BufferedRateLimiterTest.java @@ -17,6 +17,7 @@ package org.thingsboard.server.dao.util; import com.google.common.util.concurrent.*; import org.junit.Test; +import org.thingsboard.server.dao.exception.BufferLimitException; import javax.annotation.Nullable; import java.util.concurrent.ExecutionException; @@ -61,8 +62,8 @@ public class BufferedRateLimiterTest { } catch (Exception e) { assertTrue(e instanceof ExecutionException); Throwable actualCause = e.getCause(); - assertTrue(actualCause instanceof IllegalStateException); - assertEquals("Rate Limit Buffer is full. Reject", actualCause.getMessage()); + assertTrue(actualCause instanceof BufferLimitException); + assertEquals("Rate Limit Buffer is full", actualCause.getMessage()); } } diff --git a/pom.xml b/pom.xml index f0c915a5b0..a90a8aa857 100755 --- a/pom.xml +++ b/pom.xml @@ -284,6 +284,7 @@ src/sh/** src/main/scripts/control/** src/main/scripts/windows/** + src/main/resources/public/static/rulenode/** JAVADOC_STYLE diff --git a/rule-engine/rule-engine-api/src/main/java/org/thingsboard/rule/engine/api/NodeDefinition.java b/rule-engine/rule-engine-api/src/main/java/org/thingsboard/rule/engine/api/NodeDefinition.java index 6c57d92a54..18b2b941ad 100644 --- a/rule-engine/rule-engine-api/src/main/java/org/thingsboard/rule/engine/api/NodeDefinition.java +++ b/rule-engine/rule-engine-api/src/main/java/org/thingsboard/rule/engine/api/NodeDefinition.java @@ -29,5 +29,7 @@ public class NodeDefinition { String[] relationTypes; boolean customRelations; JsonNode defaultConfiguration; + String[] uiResources; + String configDirective; } diff --git a/rule-engine/rule-engine-api/src/main/java/org/thingsboard/rule/engine/api/RuleNode.java b/rule-engine/rule-engine-api/src/main/java/org/thingsboard/rule/engine/api/RuleNode.java index 16170347a7..eea92ed00a 100644 --- a/rule-engine/rule-engine-api/src/main/java/org/thingsboard/rule/engine/api/RuleNode.java +++ b/rule-engine/rule-engine-api/src/main/java/org/thingsboard/rule/engine/api/RuleNode.java @@ -45,6 +45,10 @@ public @interface RuleNode { String[] relationTypes() default {"Success", "Failure"}; + String[] uiResources() default {}; + + String configDirective() default ""; + boolean customRelations() default false; } diff --git a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/filter/TbJsFilterNode.java b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/filter/TbJsFilterNode.java index 07b166db68..eb8941cc8c 100644 --- a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/filter/TbJsFilterNode.java +++ b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/filter/TbJsFilterNode.java @@ -35,7 +35,10 @@ import static org.thingsboard.rule.engine.DonAsynchron.withCallback; nodeDetails = "Evaluate incoming Message with configured JS condition. " + "If True - send Message via True chain, otherwise False chain is used." + "Message payload can be accessed via msg property. For example msg.temperature < 10;" + - "Message metadata can be accessed via meta property. For example meta.customerName === 'John';") + "Message metadata can be accessed via metadata property. For example metadata.customerName === 'John';", + uiResources = {"static/rulenode/rulenode-core-config.js"}, + configDirective = "tbFilterNodeScriptConfig") + public class TbJsFilterNode implements TbNode { private TbJsFilterNodeConfiguration config; @@ -44,7 +47,7 @@ public class TbJsFilterNode implements TbNode { @Override public void init(TbNodeConfiguration configuration, TbNodeState state) throws TbNodeException { this.config = TbNodeUtils.convert(configuration, TbJsFilterNodeConfiguration.class); - this.jsEngine = new NashornJsEngine(config.getJsScript()); + this.jsEngine = new NashornJsEngine(config.getJsScript(), "Filter"); } @Override diff --git a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/filter/TbJsFilterNodeConfiguration.java b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/filter/TbJsFilterNodeConfiguration.java index 3b19c7c641..2d776ce3e3 100644 --- a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/filter/TbJsFilterNodeConfiguration.java +++ b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/filter/TbJsFilterNodeConfiguration.java @@ -26,7 +26,7 @@ public class TbJsFilterNodeConfiguration implements NodeConfiguration { @Override public TbJsFilterNodeConfiguration defaultConfiguration() { TbJsFilterNodeConfiguration configuration = new TbJsFilterNodeConfiguration(); - configuration.setJsScript("msg.passed < 15 && msg.name === 'Vit' && meta.temp == 10 && msg.bigObj.prop == 42;"); + configuration.setJsScript("return msg.passed < 15 && msg.name === 'Vit' && metadata.temp == 10 && msg.bigObj.prop == 42;"); return configuration; } } diff --git a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/filter/TbJsSwitchNode.java b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/filter/TbJsSwitchNode.java index c1236a4653..98f0ebcd76 100644 --- a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/filter/TbJsSwitchNode.java +++ b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/filter/TbJsSwitchNode.java @@ -36,7 +36,9 @@ import static org.thingsboard.rule.engine.DonAsynchron.withCallback; nodeDetails = "Node executes configured JS script. Script should return array of next Chain names where Message should be routed. " + "If Array is empty - message not routed to next Node. " + "Message payload can be accessed via msg property. For example msg.temperature < 10; " + - "Message metadata can be accessed via meta property. For example meta.customerName === 'John';") + "Message metadata can be accessed via metadata property. For example metadata.customerName === 'John';", + uiResources = {"static/rulenode/rulenode-core-config.js"}, + configDirective = "tbFilterNodeSwitchConfig") public class TbJsSwitchNode implements TbNode { private TbJsSwitchNodeConfiguration config; @@ -45,22 +47,11 @@ public class TbJsSwitchNode implements TbNode { @Override public void init(TbNodeConfiguration configuration, TbNodeState state) throws TbNodeException { this.config = TbNodeUtils.convert(configuration, TbJsSwitchNodeConfiguration.class); - if (config.getAllowedRelations().size() < 1) { - String message = "Switch node should have at least 1 relation"; - log.error(message); - throw new IllegalStateException(message); - } - if (!config.isRouteToAllWithNoCheck()) { - this.jsEngine = new NashornJsEngine(config.getJsScript()); - } + this.jsEngine = new NashornJsEngine(config.getJsScript(), "Switch"); } @Override public void onMsg(TbContext ctx, TbMsg msg) { - if (config.isRouteToAllWithNoCheck()) { - ctx.tellNext(msg, config.getAllowedRelations()); - return; - } ListeningExecutor jsExecutor = ctx.getJsExecutor(); withCallback(jsExecutor.executeAsync(() -> jsEngine.executeSwitch(toBindings(msg))), result -> processSwitch(ctx, msg, result), @@ -68,15 +59,7 @@ public class TbJsSwitchNode implements TbNode { } private void processSwitch(TbContext ctx, TbMsg msg, Set nextRelations) { - if (validateRelations(nextRelations)) { - ctx.tellNext(msg, nextRelations); - } else { - ctx.tellError(msg, new IllegalStateException("Unsupported relation for switch " + nextRelations)); - } - } - - private boolean validateRelations(Set nextRelations) { - return config.getAllowedRelations().containsAll(nextRelations); + ctx.tellNext(msg, nextRelations); } private Bindings toBindings(TbMsg msg) { diff --git a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/filter/TbJsSwitchNodeConfiguration.java b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/filter/TbJsSwitchNodeConfiguration.java index b354c7199f..3a4856cd93 100644 --- a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/filter/TbJsSwitchNodeConfiguration.java +++ b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/filter/TbJsSwitchNodeConfiguration.java @@ -25,19 +25,15 @@ import java.util.Set; public class TbJsSwitchNodeConfiguration implements NodeConfiguration { private String jsScript; - private Set allowedRelations; - private boolean routeToAllWithNoCheck; @Override public TbJsSwitchNodeConfiguration defaultConfiguration() { TbJsSwitchNodeConfiguration configuration = new TbJsSwitchNodeConfiguration(); - configuration.setJsScript("function nextRelation(meta, msg) {\n" + + configuration.setJsScript("function nextRelation(metadata, msg) {\n" + " return ['one','nine'];" + "};\n" + "\n" + - "nextRelation(meta, msg);"); - configuration.setAllowedRelations(Sets.newHashSet("one", "two")); - configuration.setRouteToAllWithNoCheck(false); + "return nextRelation(metadata, msg);"); return configuration; } } diff --git a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/filter/TbMsgTypeFilterNode.java b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/filter/TbMsgTypeFilterNode.java index 3a86c25afb..8d72a7b711 100644 --- a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/filter/TbMsgTypeFilterNode.java +++ b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/filter/TbMsgTypeFilterNode.java @@ -31,7 +31,9 @@ import org.thingsboard.server.common.msg.TbMsg; configClazz = TbMsgTypeFilterNodeConfiguration.class, nodeDescription = "Filter incoming messages by Message Type", nodeDetails = "Evaluate incoming Message with configured JS condition. " + - "If incoming MessageType is expected - send Message via Success chain, otherwise Failure chain is used.") + "If incoming MessageType is expected - send Message via Success chain, otherwise Failure chain is used.", + uiResources = {"static/rulenode/rulenode-core-config.js", "static/rulenode/rulenode-core-config.css"}, + configDirective = "tbFilterNodeMessageTypeConfig") public class TbMsgTypeFilterNode implements TbNode { TbMsgTypeFilterNodeConfiguration config; diff --git a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/filter/TbMsgTypeFilterNodeConfiguration.java b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/filter/TbMsgTypeFilterNodeConfiguration.java index a2e1b179f0..aafd2ed7c2 100644 --- a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/filter/TbMsgTypeFilterNodeConfiguration.java +++ b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/filter/TbMsgTypeFilterNodeConfiguration.java @@ -33,7 +33,7 @@ public class TbMsgTypeFilterNodeConfiguration implements NodeConfiguration { @Override public TbMsgTypeFilterNodeConfiguration defaultConfiguration() { TbMsgTypeFilterNodeConfiguration configuration = new TbMsgTypeFilterNodeConfiguration(); - configuration.setMessageTypes(Arrays.asList("GET_ATTRIBUTES","POST_ATTRIBUTES","POST_TELEMETRY","RPC_REQUEST")); + configuration.setMessageTypes(Arrays.asList("POST_ATTRIBUTES","POST_TELEMETRY","RPC_REQUEST")); return configuration; } } diff --git a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/js/NashornJsEngine.java b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/js/NashornJsEngine.java index 082535f0fe..a4add405e7 100644 --- a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/js/NashornJsEngine.java +++ b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/js/NashornJsEngine.java @@ -34,14 +34,20 @@ import java.util.Set; @Slf4j public class NashornJsEngine { - public static final String METADATA = "meta"; + public static final String METADATA = "metadata"; public static final String DATA = "msg"; + + private static final String JS_WRAPPER_PREFIX_TEMPLATE = "function %s(msg, metadata) { "; + private static final String JS_WRAPPER_SUFFIX_TEMPLATE = "}\n %s(msg, metadata);"; + private static NashornScriptEngineFactory factory = new NashornScriptEngineFactory(); private CompiledScript engine; - public NashornJsEngine(String script) { - engine = compileScript(script); + public NashornJsEngine(String script, String functionName) { + String jsWrapperPrefix = String.format(JS_WRAPPER_PREFIX_TEMPLATE, functionName); + String jsWrapperSuffix = String.format(JS_WRAPPER_SUFFIX_TEMPLATE, functionName); + engine = compileScript(jsWrapperPrefix + script + jsWrapperSuffix); } private static CompiledScript compileScript(String script) { @@ -58,15 +64,15 @@ public class NashornJsEngine { public static Bindings bindMsg(TbMsg msg) { try { Bindings bindings = new SimpleBindings(); - bindings.put(METADATA, msg.getMetaData().getData()); - if (ArrayUtils.isNotEmpty(msg.getData())) { ObjectMapper mapper = new ObjectMapper(); JsonNode jsonNode = mapper.readTree(msg.getData()); Map map = mapper.treeToValue(jsonNode, Map.class); bindings.put(DATA, map); + } else { + bindings.put(DATA, Collections.emptyMap()); } - + bindings.put(METADATA, msg.getMetaData().getData()); return bindings; } catch (Throwable th) { throw new IllegalArgumentException("Cannot bind js args", th); diff --git a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/metadata/TbGetAttributesNode.java b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/metadata/TbGetAttributesNode.java index 69ee9d7c79..4fc0f3e48f 100644 --- a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/metadata/TbGetAttributesNode.java +++ b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/metadata/TbGetAttributesNode.java @@ -42,7 +42,7 @@ import static org.thingsboard.server.common.data.DataConstants.*; nodeDescription = "Add Message Originator Attributes or Latest Telemetry into Message Metadata", nodeDetails = "If Attributes enrichment configured, CLIENT/SHARED/SERVER attributes are added into Message metadata " + "with specific prefix: cs/shared/ss. To access those attributes in other nodes this template can be used " + - "meta.cs.temperature or meta.shared.limit " + + "metadata.cs.temperature or metadata.shared.limit " + "If Latest Telemetry enrichment configured, latest telemetry added into metadata without prefix.") public class TbGetAttributesNode implements TbNode { diff --git a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/metadata/TbGetCustomerAttributeNode.java b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/metadata/TbGetCustomerAttributeNode.java index cc6d6a1197..c59a65e27d 100644 --- a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/metadata/TbGetCustomerAttributeNode.java +++ b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/metadata/TbGetCustomerAttributeNode.java @@ -30,7 +30,7 @@ import org.thingsboard.server.common.data.plugin.ComponentType; nodeDescription = "Add Originators Customer Attributes or Latest Telemetry into Message Metadata", nodeDetails = "If Attributes enrichment configured, server scope attributes are added into Message metadata. " + "To access those attributes in other nodes this template can be used " + - "meta.temperature. If Latest Telemetry enrichment configured, latest telemetry added into metadata") + "metadata.temperature. If Latest Telemetry enrichment configured, latest telemetry added into metadata") public class TbGetCustomerAttributeNode extends TbEntityGetAttrNode { @Override diff --git a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/metadata/TbGetRelatedAttributeNode.java b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/metadata/TbGetRelatedAttributeNode.java index 22c0b9f0a8..4cd5cd5ba6 100644 --- a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/metadata/TbGetRelatedAttributeNode.java +++ b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/metadata/TbGetRelatedAttributeNode.java @@ -32,7 +32,7 @@ import org.thingsboard.server.common.data.plugin.ComponentType; "If multiple Related Entities are found, only first Entity is used for attributes enrichment, other entities are discarded. " + "If Attributes enrichment configured, server scope attributes are added into Message metadata. " + "To access those attributes in other nodes this template can be used " + - "meta.temperature. If Latest Telemetry enrichment configured, latest telemetry added into metadata") + "metadata.temperature. If Latest Telemetry enrichment configured, latest telemetry added into metadata") public class TbGetRelatedAttributeNode extends TbEntityGetAttrNode { private TbGetRelatedAttrNodeConfiguration config; diff --git a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/metadata/TbGetTenantAttributeNode.java b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/metadata/TbGetTenantAttributeNode.java index b5f5e02dbe..3165385587 100644 --- a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/metadata/TbGetTenantAttributeNode.java +++ b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/metadata/TbGetTenantAttributeNode.java @@ -32,7 +32,7 @@ import org.thingsboard.server.common.data.plugin.ComponentType; nodeDescription = "Add Originators Tenant Attributes or Latest Telemetry into Message Metadata", nodeDetails = "If Attributes enrichment configured, server scope attributes are added into Message metadata. " + "To access those attributes in other nodes this template can be used " + - "meta.temperature. If Latest Telemetry enrichment configured, latest telemetry added into metadata") + "metadata.temperature. If Latest Telemetry enrichment configured, latest telemetry added into metadata") public class TbGetTenantAttributeNode extends TbEntityGetAttrNode { @Override diff --git a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/transform/TbTransformMsgNode.java b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/transform/TbTransformMsgNode.java index 626790fcf8..e47ea0f8db 100644 --- a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/transform/TbTransformMsgNode.java +++ b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/transform/TbTransformMsgNode.java @@ -30,7 +30,7 @@ import javax.script.Bindings; configClazz = TbTransformMsgNodeConfiguration.class, nodeDescription = "Change Message payload and Metadata using JavaScript", nodeDetails = "JavaScript function recieve 2 input parameters that can be changed inside.
" + - "meta - is a Message metadata.
" + + "metadata - is a Message metadata.
" + "msg - is a Message payload.
Any properties can be changed/removed/added in those objects.") public class TbTransformMsgNode extends TbAbstractTransformNode { @@ -40,7 +40,7 @@ public class TbTransformMsgNode extends TbAbstractTransformNode { @Override public void init(TbNodeConfiguration configuration, TbNodeState state) throws TbNodeException { this.config = TbNodeUtils.convert(configuration, TbTransformMsgNodeConfiguration.class); - this.jsEngine = new NashornJsEngine(config.getJsScript()); + this.jsEngine = new NashornJsEngine(config.getJsScript(), "Transform"); setConfig(config); } diff --git a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/transform/TbTransformMsgNodeConfiguration.java b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/transform/TbTransformMsgNodeConfiguration.java index 4f9e9eb681..09d5ac43ad 100644 --- a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/transform/TbTransformMsgNodeConfiguration.java +++ b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/transform/TbTransformMsgNodeConfiguration.java @@ -27,7 +27,7 @@ public class TbTransformMsgNodeConfiguration extends TbTransformNodeConfiguratio public TbTransformMsgNodeConfiguration defaultConfiguration() { TbTransformMsgNodeConfiguration configuration = new TbTransformMsgNodeConfiguration(); configuration.setStartNewChain(false); - configuration.setJsScript("msg.passed = msg.passed * meta.temp; msg.bigObj.newProp = 'Ukraine' "); + configuration.setJsScript("return msg.passed = msg.passed * metadata.temp; msg.bigObj.newProp = 'Ukraine' "); return configuration; } } diff --git a/rule-engine/rule-engine-components/src/main/resources/public/static/rulenode/rulenode-core-config.css b/rule-engine/rule-engine-components/src/main/resources/public/static/rulenode/rulenode-core-config.css new file mode 100644 index 0000000000..a6103c1d56 --- /dev/null +++ b/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*/ \ No newline at end of file diff --git a/rule-engine/rule-engine-components/src/main/resources/public/static/rulenode/rulenode-core-config.js b/rule-engine/rule-engine-components/src/main/resources/public/static/rulenode/rulenode-core-config.js new file mode 100644 index 0000000000..3fe859d90f --- /dev/null +++ b/rule-engine/rule-engine-components/src/main/resources/public/static/rulenode/rulenode-core-config.js @@ -0,0 +1,2 @@ +!function(e){function t(s){if(a[s])return a[s].exports;var n=a[s]={exports:{},id:s,loaded:!1};return e[s].call(n.exports,n,n.exports,t),n.loaded=!0,n.exports}var a={};return t.m=e,t.c=a,t.p="/static/",t(0)}([function(e,t,a){e.exports=a(8)},function(e,t){},function(e,t){e.exports='
{{item}}
tb.rulenode.no-message-types-found
tb.rulenode.no-message-type-matching tb.rulenode.create-new-message-type
{{$chip.name}}
'},function(e,t){e.exports="
"},function(e,t){e.exports="
"},function(e,t,a){"use strict";function s(e){return e&&e.__esModule?e:{default:e}}function n(e,t,a){var s=function(s,n,r,l){function u(){if(l.$viewValue){for(var e=[],t=0;t 15;"); + initWithScript("return 10 > 15;"); TbMsg msg = new TbMsg(UUIDs.timeBased(), "USER", null, new TbMsgMetaData(), "{}".getBytes()); mockJsExecutor(); @@ -64,7 +64,7 @@ public class TbJsFilterNodeTest { @Test public void notValidMsgDataThrowsException() throws TbNodeException { - initWithScript("10 > 15;"); + initWithScript("return 10 > 15;"); TbMsg msg = new TbMsg(UUIDs.timeBased(), "USER", null, new TbMsgMetaData(), new byte[4]); when(ctx.getJsExecutor()).thenReturn(executor); @@ -77,7 +77,7 @@ public class TbJsFilterNodeTest { @Test public void exceptionInJsThrowsException() throws TbNodeException { - initWithScript("meta.temp.curr < 15;"); + initWithScript("return metadata.temp.curr < 15;"); TbMsgMetaData metaData = new TbMsgMetaData(); TbMsg msg = new TbMsg(UUIDs.timeBased(), "USER", null, metaData, "{}".getBytes()); mockJsExecutor(); @@ -89,12 +89,12 @@ public class TbJsFilterNodeTest { @Test(expected = IllegalArgumentException.class) public void notValidScriptThrowsException() throws TbNodeException { - initWithScript("10 > 15 asdq out"); + initWithScript("return 10 > 15 asdq out"); } @Test public void metadataConditionCanBeFalse() throws TbNodeException { - initWithScript("meta.humidity < 15;"); + initWithScript("return metadata.humidity < 15;"); TbMsgMetaData metaData = new TbMsgMetaData(); metaData.putValue("temp", "10"); metaData.putValue("humidity", "99"); @@ -109,7 +109,7 @@ public class TbJsFilterNodeTest { @Test public void metadataConditionCanBeTrue() throws TbNodeException { - initWithScript("meta.temp < 15;"); + initWithScript("return metadata.temp < 15;"); TbMsgMetaData metaData = new TbMsgMetaData(); metaData.putValue("temp", "10"); metaData.putValue("humidity", "99"); @@ -123,7 +123,7 @@ public class TbJsFilterNodeTest { @Test public void msgJsonParsedAndBinded() throws TbNodeException { - initWithScript("msg.passed < 15 && msg.name === 'Vit' && meta.temp == 10 && msg.bigObj.prop == 42;"); + initWithScript("return msg.passed < 15 && msg.name === 'Vit' && metadata.temp == 10 && msg.bigObj.prop == 42;"); TbMsgMetaData metaData = new TbMsgMetaData(); metaData.putValue("temp", "10"); metaData.putValue("humidity", "99"); diff --git a/rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/filter/TbJsSwitchNodeTest.java b/rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/filter/TbJsSwitchNodeTest.java index e70d4e16f4..01227f48aa 100644 --- a/rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/filter/TbJsSwitchNodeTest.java +++ b/rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/filter/TbJsSwitchNodeTest.java @@ -52,28 +52,17 @@ public class TbJsSwitchNodeTest { @Mock private ListeningExecutor executor; - @Test - public void routeToAllDoNotEvaluatesJs() throws TbNodeException { - HashSet relations = Sets.newHashSet("one", "two"); - initWithScript("test qwerty", relations, true); - TbMsg msg = new TbMsg(UUIDs.timeBased(), "USER", null, new TbMsgMetaData(), "{}".getBytes()); - - node.onMsg(ctx, msg); - verify(ctx).tellNext(msg, relations); - verifyNoMoreInteractions(ctx, executor); - } - @Test public void multipleRoutesAreAllowed() throws TbNodeException { - String jsCode = "function nextRelation(meta, msg) {\n" + - " if(msg.passed == 5 && meta.temp == 10)\n" + + String jsCode = "function nextRelation(metadata, msg) {\n" + + " if(msg.passed == 5 && metadata.temp == 10)\n" + " return ['three', 'one']\n" + " else\n" + " return 'two';\n" + "};\n" + "\n" + - "nextRelation(meta, msg);"; - initWithScript(jsCode, Sets.newHashSet("one", "two", "three"), false); + "return nextRelation(metadata, msg);"; + initWithScript(jsCode); TbMsgMetaData metaData = new TbMsgMetaData(); metaData.putValue("temp", "10"); metaData.putValue("humidity", "99"); @@ -89,15 +78,15 @@ public class TbJsSwitchNodeTest { @Test public void allowedRelationPassed() throws TbNodeException { - String jsCode = "function nextRelation(meta, msg) {\n" + - " if(msg.passed == 5 && meta.temp == 10)\n" + + String jsCode = "function nextRelation(metadata, msg) {\n" + + " if(msg.passed == 5 && metadata.temp == 10)\n" + " return 'one'\n" + " else\n" + " return 'two';\n" + "};\n" + "\n" + - "nextRelation(meta, msg);"; - initWithScript(jsCode, Sets.newHashSet("one", "two"), false); + "return nextRelation(metadata, msg);"; + initWithScript(jsCode); TbMsgMetaData metaData = new TbMsgMetaData(); metaData.putValue("temp", "10"); metaData.putValue("humidity", "99"); @@ -111,32 +100,9 @@ public class TbJsSwitchNodeTest { verify(ctx).tellNext(msg, Sets.newHashSet("one")); } - @Test - public void unknownRelationThrowsException() throws TbNodeException { - String jsCode = "function nextRelation(meta, msg) {\n" + - " return ['one','nine'];" + - "};\n" + - "\n" + - "nextRelation(meta, msg);"; - initWithScript(jsCode, Sets.newHashSet("one", "two"), false); - TbMsgMetaData metaData = new TbMsgMetaData(); - metaData.putValue("temp", "10"); - metaData.putValue("humidity", "99"); - String rawJson = "{\"name\": \"Vit\", \"passed\": 5}"; - - TbMsg msg = new TbMsg(UUIDs.timeBased(), "USER", null, metaData, rawJson.getBytes()); - mockJsExecutor(); - - node.onMsg(ctx, msg); - verify(ctx).getJsExecutor(); - verifyError(msg, "Unsupported relation for switch [nine, one]", IllegalStateException.class); - } - - private void initWithScript(String script, Set relations, boolean routeToAll) throws TbNodeException { + private void initWithScript(String script) throws TbNodeException { TbJsSwitchNodeConfiguration config = new TbJsSwitchNodeConfiguration(); config.setJsScript(script); - config.setAllowedRelations(relations); - config.setRouteToAllWithNoCheck(routeToAll); ObjectMapper mapper = new ObjectMapper(); TbNodeConfiguration nodeConfiguration = new TbNodeConfiguration(mapper.valueToTree(config)); diff --git a/rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/transform/TbTransformMsgNodeTest.java b/rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/transform/TbTransformMsgNodeTest.java index d69bad8864..c6b3441f27 100644 --- a/rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/transform/TbTransformMsgNodeTest.java +++ b/rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/transform/TbTransformMsgNodeTest.java @@ -51,7 +51,7 @@ public class TbTransformMsgNodeTest { @Test public void metadataCanBeUpdated() throws TbNodeException { - initWithScript("meta.temp = meta.temp * 10;"); + initWithScript("return metadata.temp = metadata.temp * 10;"); TbMsgMetaData metaData = new TbMsgMetaData(); metaData.putValue("temp", "7"); metaData.putValue("humidity", "99"); @@ -70,7 +70,7 @@ public class TbTransformMsgNodeTest { @Test public void metadataCanBeAdded() throws TbNodeException { - initWithScript("meta.newAttr = meta.humidity - msg.passed;"); + initWithScript("return metadata.newAttr = metadata.humidity - msg.passed;"); TbMsgMetaData metaData = new TbMsgMetaData(); metaData.putValue("temp", "7"); metaData.putValue("humidity", "99"); @@ -89,7 +89,7 @@ public class TbTransformMsgNodeTest { @Test public void payloadCanBeUpdated() throws TbNodeException { - initWithScript("msg.passed = msg.passed * meta.temp; msg.bigObj.newProp = 'Ukraine' "); + initWithScript("return msg.passed = msg.passed * metadata.temp; msg.bigObj.newProp = 'Ukraine' "); TbMsgMetaData metaData = new TbMsgMetaData(); metaData.putValue("temp", "7"); metaData.putValue("humidity", "99"); diff --git a/ui/package.json b/ui/package.json index e68aa34223..ec3f880c23 100644 --- a/ui/package.json +++ b/ui/package.json @@ -15,7 +15,7 @@ }, "dependencies": { "@flowjs/ng-flow": "^2.7.1", - "ace-builds": "^1.2.5", + "ace-builds": "1.3.1", "angular": "1.5.8", "angular-animate": "1.5.8", "angular-aria": "1.5.8", diff --git a/ui/server.js b/ui/server.js index fae132f02f..65a2bc7f45 100644 --- a/ui/server.js +++ b/ui/server.js @@ -30,6 +30,9 @@ const httpProxy = require('http-proxy'); const forwardHost = 'localhost'; const forwardPort = 8080; +const ruleNodeUiforwardHost = 'localhost'; +const ruleNodeUiforwardPort = 8080; + const app = express(); const server = http.createServer(app); @@ -52,17 +55,34 @@ const apiProxy = httpProxy.createProxyServer({ } }); +const ruleNodeUiApiProxy = httpProxy.createProxyServer({ + target: { + host: ruleNodeUiforwardHost, + port: ruleNodeUiforwardPort + } +}); + apiProxy.on('error', function (err, req, res) { console.warn('API proxy error: ' + err); res.end('Error.'); }); +ruleNodeUiApiProxy.on('error', function (err, req, res) { + console.warn('RuleNode UI API proxy error: ' + err); + res.end('Error.'); +}); + console.info(`Forwarding API requests to http://${forwardHost}:${forwardPort}`); +console.info(`Forwarding Rule Node UI requests to http://${ruleNodeUiforwardHost}:${ruleNodeUiforwardPort}`); app.all('/api/*', (req, res) => { apiProxy.web(req, res); }); +app.all('/static/rulenode/*', (req, res) => { + ruleNodeUiApiProxy.web(req, res); +}); + app.get('*', function(req, res) { res.sendFile(path.join(__dirname, 'src/index.html')); }); diff --git a/ui/src/app/api/rule-chain.service.js b/ui/src/app/api/rule-chain.service.js index ebc48fad61..af14a3f582 100644 --- a/ui/src/app/api/rule-chain.service.js +++ b/ui/src/app/api/rule-chain.service.js @@ -17,7 +17,7 @@ export default angular.module('thingsboard.api.ruleChain', []) .factory('ruleChainService', RuleChainService).name; /*@ngInject*/ -function RuleChainService($http, $q, $filter, types, componentDescriptorService) { +function RuleChainService($http, $q, $filter, $ocLazyLoad, $translate, types, componentDescriptorService) { var ruleNodeComponents = null; @@ -177,11 +177,18 @@ function RuleChainService($http, $q, $filter, types, componentDescriptorService) } else { loadRuleNodeComponents().then( (components) => { - ruleNodeComponents = components; - ruleNodeComponents.push( - types.ruleChainNodeComponent + resolveRuleNodeComponentsUiResources(components).then( + (components) => { + ruleNodeComponents = components; + ruleNodeComponents.push( + types.ruleChainNodeComponent + ); + deferred.resolve(ruleNodeComponents); + }, + () => { + deferred.reject(); + } ); - deferred.resolve(ruleNodeComponents); }, () => { deferred.reject(); @@ -191,6 +198,48 @@ function RuleChainService($http, $q, $filter, types, componentDescriptorService) return deferred.promise; } + function resolveRuleNodeComponentsUiResources(components) { + var deferred = $q.defer(); + var tasks = []; + for (var i=0;i { + 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 { + deferred.resolve(component); + }, + () => { + component.configurationDescriptor.nodeDefinition.uiResourceLoadError = $translate.instant('rulenode.ui-resources-load-error'); + deferred.resolve(component); + } + ) + } else { + deferred.resolve(component); + } + return deferred.promise; + } + function getRuleNodeComponentByClazz(clazz) { var res = $filter('filter')(ruleNodeComponents, {clazz: clazz}, true); if (res && res.length) { diff --git a/ui/src/app/common/types.constant.js b/ui/src/app/common/types.constant.js index 8026115b35..21865087cc 100644 --- a/ui/src/app/common/types.constant.js +++ b/ui/src/app/common/types.constant.js @@ -279,6 +279,23 @@ export default angular.module('thingsboard.types', []) function: "function", alarm: "alarm" }, + contentType: { + "JSON": { + value: "JSON", + name: "content-type.json", + code: "json" + }, + "TEXT": { + value: "TEXT", + name: "content-type.text", + code: "text" + }, + "BINARY": { + value: "BINARY", + name: "content-type.binary", + code: "text" + } + }, componentType: { filter: "FILTER", processor: "PROCESSOR", @@ -295,7 +312,8 @@ export default angular.module('thingsboard.types', []) user: "USER", dashboard: "DASHBOARD", alarm: "ALARM", - rulechain: "RULE_CHAIN" + rulechain: "RULE_CHAIN", + rulenode: "RULE_NODE" }, aliasEntityType: { current_customer: "CURRENT_CUSTOMER" @@ -388,6 +406,16 @@ export default angular.module('thingsboard.types', []) name: "event.type-stats" } }, + debugEventType: { + debugRuleNode: { + value: "DEBUG_RULE_NODE", + name: "event.type-debug-rule-node" + }, + debugRuleChain: { + value: "DEBUG_RULE_CHAIN", + name: "event.type-debug-rule-chain" + } + }, extensionType: { http: "HTTP", mqtt: "MQTT", diff --git a/ui/src/app/components/details-sidenav.directive.js b/ui/src/app/components/details-sidenav.directive.js index e455a80dcd..2516134d6f 100644 --- a/ui/src/app/components/details-sidenav.directive.js +++ b/ui/src/app/components/details-sidenav.directive.js @@ -26,7 +26,7 @@ export default angular.module('thingsboard.directives.detailsSidenav', []) .name; /*@ngInject*/ -function DetailsSidenav($timeout) { +function DetailsSidenav($timeout, $mdUtil, $q, $animate) { var linker = function (scope, element, attrs) { @@ -42,6 +42,63 @@ function DetailsSidenav($timeout) { scope.isEdit = true; } + var backdrop; + var previousContainerStyles; + + if (attrs.hasOwnProperty('tbEnableBackdrop')) { + backdrop = $mdUtil.createBackdrop(scope, "md-sidenav-backdrop md-opaque ng-enter"); + element.on('$destroy', function() { + backdrop && backdrop.remove(); + }); + scope.$on('$destroy', function(){ + backdrop && backdrop.remove(); + }); + scope.$watch('isOpen', updateIsOpen); + } + + function updateIsOpen(isOpen) { + backdrop[isOpen ? 'on' : 'off']('click', (ev)=>{ + ev.preventDefault(); + scope.isOpen = false; + scope.$apply(); + }); + var parent = element.parent(); + var restorePositioning = updateContainerPositions(parent, isOpen); + + return $q.all([ + isOpen && backdrop ? $animate.enter(backdrop, parent) : backdrop ? + $animate.leave(backdrop) : $q.when(true) + ]).then(function() { + restorePositioning && restorePositioning(); + }); + } + + function updateContainerPositions(parent, willOpen) { + var drawerEl = element[0]; + var scrollTop = parent[0].scrollTop; + if (willOpen && scrollTop) { + previousContainerStyles = { + top: drawerEl.style.top, + bottom: drawerEl.style.bottom, + height: drawerEl.style.height + }; + var positionStyle = { + top: scrollTop + 'px', + bottom: 'auto', + height: parent[0].clientHeight + 'px' + }; + backdrop.css(positionStyle); + } + if (!willOpen && previousContainerStyles) { + return function() { + backdrop[0].style.top = null; + backdrop[0].style.bottom = null; + backdrop[0].style.height = null; + previousContainerStyles = null; + }; + } + } + scope.toggleDetailsEditMode = function () { if (!scope.isAlwaysEdit) { if (!scope.isEdit) { diff --git a/ui/src/app/components/details-sidenav.tpl.html b/ui/src/app/components/details-sidenav.tpl.html index c504a24a79..763bc2295a 100644 --- a/ui/src/app/components/details-sidenav.tpl.html +++ b/ui/src/app/components/details-sidenav.tpl.html @@ -16,7 +16,7 @@ --> diff --git a/ui/src/app/components/js-func.directive.js b/ui/src/app/components/js-func.directive.js index 33cebdebf0..deb5626826 100644 --- a/ui/src/app/components/js-func.directive.js +++ b/ui/src/app/components/js-func.directive.js @@ -43,6 +43,7 @@ function JsFunc($compile, $templateCache, toast, utils, $translate) { var template = $templateCache.get(jsFuncTemplate); element.html(template); + scope.functionName = attrs.functionName; scope.functionArgs = scope.$eval(attrs.functionArgs); scope.validationArgs = scope.$eval(attrs.validationArgs); scope.resultType = attrs.resultType; @@ -50,6 +51,8 @@ function JsFunc($compile, $templateCache, toast, utils, $translate) { scope.resultType = "nocheck"; } + scope.validationTriggerArg = attrs.validationTriggerArg; + scope.functionValid = true; var Range = ace.acequire("ace/range").Range; @@ -66,11 +69,15 @@ function JsFunc($compile, $templateCache, toast, utils, $translate) { } scope.onFullscreenChanged = function () { + updateEditorSize(); + }; + + function updateEditorSize() { if (scope.js_editor) { scope.js_editor.resize(); scope.js_editor.renderer.updateFull(); } - }; + } scope.jsEditorOptions = { useWrapMode: true, @@ -131,6 +138,9 @@ function JsFunc($compile, $templateCache, toast, utils, $translate) { scope.validate = function () { try { var toValidate = new Function(scope.functionArgsString, scope.functionBody); + if (scope.noValidate) { + return true; + } var res; var validationError; for (var i=0;i -
+
- function({{ functionArgsString }}) { +
-
- } -
-
\ No newline at end of file + +
+ diff --git a/ui/src/app/components/json-object-edit.directive.js b/ui/src/app/components/json-object-edit.directive.js index db0aa60bb5..215b7b9fe8 100644 --- a/ui/src/app/components/json-object-edit.directive.js +++ b/ui/src/app/components/json-object-edit.directive.js @@ -84,17 +84,32 @@ function JsonObjectEdit($compile, $templateCache, $document, toast, utils) { scope.$watch('contentBody', function (newVal, prevVal) { if (!angular.equals(newVal, prevVal)) { var object = scope.validate(); - ngModelCtrl.$setViewValue(object); + if (scope.objectValid) { + if (object == null) { + scope.object = null; + } else { + if (scope.object == null) { + scope.object = {}; + } + Object.keys(scope.object).forEach(function (key) { + delete scope.object[key]; + }); + Object.keys(object).forEach(function (key) { + scope.object[key] = object[key]; + }); + } + ngModelCtrl.$setViewValue(scope.object); + } scope.updateValidity(); } }); ngModelCtrl.$render = function () { - var object = ngModelCtrl.$viewValue; + scope.object = ngModelCtrl.$viewValue; var content = ''; try { - if (object) { - content = angular.toJson(object, true); + if (scope.object) { + content = angular.toJson(scope.object, true); } } catch (e) { // diff --git a/ui/src/app/event/event-content-dialog.controller.js b/ui/src/app/event/event-content-dialog.controller.js index 108f95e323..8d13f9654a 100644 --- a/ui/src/app/event/event-content-dialog.controller.js +++ b/ui/src/app/event/event-content-dialog.controller.js @@ -17,11 +17,14 @@ import $ from 'jquery'; import 'brace/ext/language_tools'; import 'brace/mode/java'; import 'brace/theme/github'; +import beautify from 'js-beautify'; /* eslint-disable angular/angularelement */ +const js_beautify = beautify.js; + /*@ngInject*/ -export default function EventContentDialogController($mdDialog, content, title, showingCallback) { +export default function EventContentDialogController($mdDialog, types, content, contentType, title, showingCallback) { var vm = this; @@ -32,9 +35,19 @@ export default function EventContentDialogController($mdDialog, content, title, vm.content = content; vm.title = title; + var mode; + if (contentType) { + mode = types.contentType[contentType].code; + if (contentType == types.contentType.JSON.value && vm.content) { + vm.content = js_beautify(vm.content, {indent_size: 4}); + } + } else { + mode = 'java'; + } + vm.contentOptions = { useWrapMode: false, - mode: 'java', + mode: mode, showGutter: false, showPrintMargin: false, theme: 'github', diff --git a/ui/src/app/event/event-header-debug-rulenode.tpl.html b/ui/src/app/event/event-header-debug-rulenode.tpl.html new file mode 100644 index 0000000000..b412a0c50a --- /dev/null +++ b/ui/src/app/event/event-header-debug-rulenode.tpl.html @@ -0,0 +1,27 @@ + +
event.event-time
+
event.server
+
event.type
+
event.entity
+
event.message-id
+
event.message-type
+
event.data-type
+
event.data
+
event.metadata
+
event.error
diff --git a/ui/src/app/event/event-header.directive.js b/ui/src/app/event/event-header.directive.js index afac804f30..bc4cdbe9ea 100644 --- a/ui/src/app/event/event-header.directive.js +++ b/ui/src/app/event/event-header.directive.js @@ -18,6 +18,7 @@ import eventHeaderLcEventTemplate from './event-header-lc-event.tpl.html'; import eventHeaderStatsTemplate from './event-header-stats.tpl.html'; import eventHeaderErrorTemplate from './event-header-error.tpl.html'; +import eventHeaderDebugRuleNodeTemplate from './event-header-debug-rulenode.tpl.html'; /* eslint-enable import/no-unresolved, import/default */ @@ -38,6 +39,12 @@ export default function EventHeaderDirective($compile, $templateCache, types) { case types.eventType.error.value: template = eventHeaderErrorTemplate; break; + case types.debugEventType.debugRuleNode.value: + template = eventHeaderDebugRuleNodeTemplate; + break; + case types.debugEventType.debugRuleChain.value: + template = eventHeaderDebugRuleNodeTemplate; + break; } return $templateCache.get(template); } diff --git a/ui/src/app/event/event-row-debug-rulenode.tpl.html b/ui/src/app/event/event-row-debug-rulenode.tpl.html new file mode 100644 index 0000000000..ec00b39ef1 --- /dev/null +++ b/ui/src/app/event/event-row-debug-rulenode.tpl.html @@ -0,0 +1,63 @@ + +
{{event.createdTime | date : 'yyyy-MM-dd HH:mm:ss'}}
+
{{event.body.server}}
+
{{event.body.type}}
+
{{event.body.entityName}}
+
{{event.body.msgId}}
+
{{event.body.msgType}}
+
{{event.body.dataType}}
+
+ + + {{ 'action.view' | translate }} + + + more_horiz + + +
+
+ + + {{ 'action.view' | translate }} + + + more_horiz + + +
+
+ + + {{ 'action.view' | translate }} + + + more_horiz + + +
diff --git a/ui/src/app/event/event-row.directive.js b/ui/src/app/event/event-row.directive.js index f005542d40..4643761b39 100644 --- a/ui/src/app/event/event-row.directive.js +++ b/ui/src/app/event/event-row.directive.js @@ -20,6 +20,7 @@ import eventErrorDialogTemplate from './event-content-dialog.tpl.html'; import eventRowLcEventTemplate from './event-row-lc-event.tpl.html'; import eventRowStatsTemplate from './event-row-stats.tpl.html'; import eventRowErrorTemplate from './event-row-error.tpl.html'; +import eventRowDebugRuleNodeTemplate from './event-row-debug-rulenode.tpl.html'; /* eslint-enable import/no-unresolved, import/default */ @@ -40,6 +41,12 @@ export default function EventRowDirective($compile, $templateCache, $mdDialog, $ case types.eventType.error.value: template = eventRowErrorTemplate; break; + case types.debugEventType.debugRuleNode.value: + template = eventRowDebugRuleNodeTemplate; + break; + case types.debugEventType.debugRuleChain.value: + template = eventRowDebugRuleNodeTemplate; + break; } return $templateCache.get(template); } @@ -53,17 +60,22 @@ export default function EventRowDirective($compile, $templateCache, $mdDialog, $ scope.loadTemplate(); }); + scope.types = types; + scope.event = attrs.event; - scope.showContent = function($event, content, title) { + scope.showContent = function($event, content, title, contentType) { var onShowingCallback = { onShowing: function(){} } + if (!contentType) { + contentType = null; + } $mdDialog.show({ controller: 'EventContentDialogController', controllerAs: 'vm', templateUrl: eventErrorDialogTemplate, - locals: {content: content, title: title, showingCallback: onShowingCallback}, + locals: {content: content, title: title, contentType: contentType, showingCallback: onShowingCallback}, parent: angular.element($document[0].body), fullscreen: true, targetEvent: $event, diff --git a/ui/src/app/event/event-table.directive.js b/ui/src/app/event/event-table.directive.js index 4291014340..c61078dfe6 100644 --- a/ui/src/app/event/event-table.directive.js +++ b/ui/src/app/event/event-table.directive.js @@ -36,8 +36,8 @@ export default function EventTableDirective($compile, $templateCache, $rootScope for (var type in types.eventType) { var eventType = types.eventType[type]; var enabled = true; - for (var disabledType in disabledEventTypes) { - if (eventType.value === disabledEventTypes[disabledType]) { + for (var i=0;i { + vm.ruleNodeLinkForm.$setPristine(); + }); } }, nodeCallbacks: { @@ -263,6 +269,9 @@ export function RuleChainController($stateParams, $scope, $compile, $q, $mdUtil, vm.isEditingRuleNode = true; vm.editingRuleNodeIndex = vm.ruleChainModel.nodes.indexOf(node); vm.editingRuleNode = angular.copy(node); + $mdUtil.nextTick(() => { + vm.ruleNodeForm.$setPristine(); + }); } } }, @@ -309,7 +318,7 @@ export function RuleChainController($stateParams, $scope, $compile, $q, $mdUtil, var componentType = ruleNodeComponent.type; var model = vm.ruleNodeTypesModel[componentType].model; var node = { - id: model.nodes.length, + id: 'node-lib-' + componentType + '-' + model.nodes.length, component: ruleNodeComponent, name: '', nodeClass: vm.types.ruleNodeType[componentType].nodeClass, @@ -358,7 +367,7 @@ export function RuleChainController($stateParams, $scope, $compile, $q, $mdUtil, vm.ruleChainModel.nodes.push( { - id: vm.nextNodeID++, + id: 'rule-chain-node-' + vm.nextNodeID++, component: types.inputNodeComponent, name: "", nodeClass: types.ruleNodeType.INPUT.nodeClass, @@ -389,7 +398,7 @@ export function RuleChainController($stateParams, $scope, $compile, $q, $mdUtil, var component = ruleChainService.getRuleNodeComponentByClazz(ruleNode.type); if (component) { var node = { - id: vm.nextNodeID++, + id: 'rule-chain-node-' + vm.nextNodeID++, ruleNodeId: ruleNode.id, additionalInfo: ruleNode.additionalInfo, configuration: ruleNode.configuration, @@ -466,7 +475,7 @@ export function RuleChainController($stateParams, $scope, $compile, $q, $mdUtil, var ruleChainNode = ruleChainNodesMap[ruleChainConnection.additionalInfo.ruleChainNodeId]; if (!ruleChainNode) { ruleChainNode = { - id: vm.nextNodeID++, + id: 'rule-chain-node-' + vm.nextNodeID++, additionalInfo: ruleChainConnection.additionalInfo, targetRuleChainId: ruleChainConnection.targetRuleChainId.id, x: ruleChainConnection.additionalInfo.layoutX, @@ -611,7 +620,7 @@ export function RuleChainController($stateParams, $scope, $compile, $q, $mdUtil, fullscreen: true, targetEvent: $event }).then(function (ruleNode) { - ruleNode.id = vm.nextNodeID++; + ruleNode.id = 'rule-chain-node-' + vm.nextNodeID++; ruleNode.connectors = []; if (ruleNode.component.configurationDescriptor.nodeDefinition.inEnabled) { ruleNode.connectors.push( @@ -654,6 +663,11 @@ export function RuleChainController($stateParams, $scope, $compile, $q, $mdUtil, function deleteSelected() { vm.modelservice.deleteSelected(); } + + function triggerResize() { + var w = angular.element($window); + w.triggerHandler('resize'); + } } /*@ngInject*/ diff --git a/ui/src/app/rulechain/rulechain.tpl.html b/ui/src/app/rulechain/rulechain.tpl.html index d23f920e0f..b25447e175 100644 --- a/ui/src/app/rulechain/rulechain.tpl.html +++ b/ui/src/app/rulechain/rulechain.tpl.html @@ -65,9 +65,11 @@
-
- - -
+ + +
+ + +
+
+ + + + +
+ debug-event-types="{{vm.types.debugEventType.debugRuleChain.value}}" + default-event-type="{{vm.types.debugEventType.debugRuleChain.value}}"> diff --git a/ui/src/app/rulechain/rulenode-config.directive.js b/ui/src/app/rulechain/rulenode-config.directive.js new file mode 100644 index 0000000000..9bb8c48f11 --- /dev/null +++ b/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 + }; + +} diff --git a/ui/src/app/rulechain/rulenode-config.tpl.html b/ui/src/app/rulechain/rulenode-config.tpl.html new file mode 100644 index 0000000000..32d5347b43 --- /dev/null +++ b/ui/src/app/rulechain/rulenode-config.tpl.html @@ -0,0 +1,32 @@ + + + + +
{{definedDirectiveError}}
+ + diff --git a/ui/src/app/rulechain/rulenode-defined-config.directive.js b/ui/src/app/rulechain/rulenode-defined-config.directive.js new file mode 100644 index 0000000000..5100fbb96b --- /dev/null +++ b/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">`; + 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 + }; + +} diff --git a/ui/src/app/rulechain/rulenode-fieldset.tpl.html b/ui/src/app/rulechain/rulenode-fieldset.tpl.html index 30cf0752cd..7b0fae5a5f 100644 --- a/ui/src/app/rulechain/rulenode-fieldset.tpl.html +++ b/ui/src/app/rulechain/rulenode-fieldset.tpl.html @@ -21,28 +21,26 @@
- - - -
- - - -
-
rulenode.name-required
-
-
- - {{ 'rulenode.debug-mode' | translate }} - - - + + + +
+
rulenode.name-required
+
+
+ + {{ 'rulenode.debug-mode' | translate }} + + +
+ - + node-definition="ruleNode.component.configurationDescriptor.nodeDefinition" + ng-readonly="$root.loading || !isEdit || isReadOnly"> + diff --git a/ui/src/app/rulechain/rulenode.scss b/ui/src/app/rulechain/rulenode.scss index febc637a37..0466673337 100644 --- a/ui/src/app/rulechain/rulenode.scss +++ b/ui/src/app/rulechain/rulenode.scss @@ -19,4 +19,10 @@ height: 300px; display: block; } +} + +.tb-rulenode-directive-error { + color: rgb(221,44,0); + font-size: 13px; + font-weight: 400; } \ No newline at end of file