From 8b637c9e9456963e1e24ec0e6306903d2e765c1f Mon Sep 17 00:00:00 2001 From: vparomskiy Date: Fri, 23 Mar 2018 16:54:48 +0200 Subject: [PATCH 1/7] add cassandra properties in test suite --- .../server/actors/plugin/PluginActorMessageProcessor.java | 1 + application/src/main/resources/thingsboard.yml | 4 ++-- .../quota/inmemory/HostRequestIntervalRegistry.java | 5 +++-- .../server/dao/timeseries/CassandraBaseTimeseriesDao.java | 3 --- .../thingsboard/server/dao/util/BufferedRateLimiter.java | 8 ++++++-- dao/src/test/resources/cassandra-test.properties | 5 +++++ 6 files changed, 17 insertions(+), 9 deletions(-) diff --git a/application/src/main/java/org/thingsboard/server/actors/plugin/PluginActorMessageProcessor.java b/application/src/main/java/org/thingsboard/server/actors/plugin/PluginActorMessageProcessor.java index 7dfe9a5d81..2af30d4b49 100644 --- a/application/src/main/java/org/thingsboard/server/actors/plugin/PluginActorMessageProcessor.java +++ b/application/src/main/java/org/thingsboard/server/actors/plugin/PluginActorMessageProcessor.java @@ -106,6 +106,7 @@ public class PluginActorMessageProcessor extends ComponentMsgProcessor try { pluginImpl.process(trustedCtx, msg.getRuleTenantId(), msg.getRuleId(), msg.getMsg()); } catch (Exception ex) { + logger.debug("[{}] Failed to process RuleToPlugin msg: [{}] [{}]", tenantId, msg.getMsg(), ex); RuleToPluginMsg ruleMsg = msg.getMsg(); MsgType responceMsgType = MsgType.RULE_ENGINE_ERROR; Integer requestId = 0; diff --git a/application/src/main/resources/thingsboard.yml b/application/src/main/resources/thingsboard.yml index 07face1059..c47fc28fbd 100644 --- a/application/src/main/resources/thingsboard.yml +++ b/application/src/main/resources/thingsboard.yml @@ -133,7 +133,7 @@ quota: intervalMin: 2 database: - type: "${DATABASE_TYPE:cassandra}" # cassandra OR sql + type: "${DATABASE_TYPE:sql}" # cassandra OR sql # Cassandra driver configuration parameters cassandra: @@ -226,7 +226,7 @@ caffeine: specs: relations: timeToLiveInMinutes: 1440 - maxSize: 0 + maxSize: 100000 deviceCredentials: timeToLiveInMinutes: 1440 maxSize: 100000 diff --git a/common/transport/src/main/java/org/thingsboard/server/common/transport/quota/inmemory/HostRequestIntervalRegistry.java b/common/transport/src/main/java/org/thingsboard/server/common/transport/quota/inmemory/HostRequestIntervalRegistry.java index 8d254a08c6..3782ed22ed 100644 --- a/common/transport/src/main/java/org/thingsboard/server/common/transport/quota/inmemory/HostRequestIntervalRegistry.java +++ b/common/transport/src/main/java/org/thingsboard/server/common/transport/quota/inmemory/HostRequestIntervalRegistry.java @@ -61,13 +61,14 @@ public class HostRequestIntervalRegistry { } public long tick(String clientHostId) { + IntervalCount intervalCount = hostCounts.computeIfAbsent(clientHostId, s -> new IntervalCount(intervalDurationMs)); + long currentCount = intervalCount.resetIfExpiredAndTick(); if (whiteList.contains(clientHostId)) { return 0; } else if (blackList.contains(clientHostId)) { return Long.MAX_VALUE; } - IntervalCount intervalCount = hostCounts.computeIfAbsent(clientHostId, s -> new IntervalCount(intervalDurationMs)); - return intervalCount.resetIfExpiredAndTick(); + return currentCount; } public void clean() { diff --git a/dao/src/main/java/org/thingsboard/server/dao/timeseries/CassandraBaseTimeseriesDao.java b/dao/src/main/java/org/thingsboard/server/dao/timeseries/CassandraBaseTimeseriesDao.java index cf141711a5..cda4b1669b 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/timeseries/CassandraBaseTimeseriesDao.java +++ b/dao/src/main/java/org/thingsboard/server/dao/timeseries/CassandraBaseTimeseriesDao.java @@ -439,8 +439,6 @@ public class CassandraBaseTimeseriesDao extends CassandraAbstractAsyncDao implem private PreparedStatement getLatestStmt() { if (latestInsertStmt == null) { -// latestInsertStmt = new PreparedStatement[DataType.values().length]; -// for (DataType type : DataType.values()) { latestInsertStmt = prepare(INSERT_INTO + ModelConstants.TS_KV_LATEST_CF + "(" + ModelConstants.ENTITY_TYPE_COLUMN + "," + ModelConstants.ENTITY_ID_COLUMN + @@ -451,7 +449,6 @@ public class CassandraBaseTimeseriesDao extends CassandraAbstractAsyncDao implem "," + ModelConstants.LONG_VALUE_COLUMN + "," + ModelConstants.DOUBLE_VALUE_COLUMN + ")" + " VALUES(?, ?, ?, ?, ?, ?, ?, ?)"); -// } } return latestInsertStmt; } 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 de07dbfa47..2acd623a37 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 @@ -29,6 +29,7 @@ import java.util.concurrent.atomic.AtomicInteger; @Component @Slf4j +@NoSqlDao public class BufferedRateLimiter implements AsyncRateLimiter { private final ListeningExecutorService pool = MoreExecutors.listeningDecorator(Executors.newFixedThreadPool(10)); @@ -113,6 +114,9 @@ public class BufferedRateLimiter implements AsyncRateLimiter { lockedFuture.cancelFuture(); return Futures.immediateFailedFuture(new IllegalStateException("Rate Limit Buffer is full. Reject")); } + if(permits.get() < permitsLimit) { + reprocessQueue(); + } return lockedFuture.future; } catch (InterruptedException e) { return Futures.immediateFailedFuture(new IllegalStateException("Rate Limit Task interrupted. Reject")); @@ -130,8 +134,8 @@ public class BufferedRateLimiter implements AsyncRateLimiter { expiredCount++; } } - log.info("Permits maxBuffer is [{}] max concurrent [{}] expired [{}]", maxQueueSize.getAndSet(0), - maxGrantedPermissions.getAndSet(0), expiredCount); + log.info("Permits maxBuffer is [{}] max concurrent [{}] expired [{}] current granted [{}]", maxQueueSize.getAndSet(0), + maxGrantedPermissions.getAndSet(0), expiredCount, permits.get()); } private class LockedFuture { diff --git a/dao/src/test/resources/cassandra-test.properties b/dao/src/test/resources/cassandra-test.properties index 82fcbe1949..737687f053 100644 --- a/dao/src/test/resources/cassandra-test.properties +++ b/dao/src/test/resources/cassandra-test.properties @@ -47,3 +47,8 @@ cassandra.query.default_fetch_size=2000 cassandra.query.ts_key_value_partitioning=HOURS cassandra.query.max_limit_per_request=1000 +cassandra.query.buffer_size=100000 +cassandra.query.concurrent_limit=1000 +cassandra.query.permit_max_wait_time=20000 +cassandra.query.rate_limit_print_interval_ms=30000 + From 54b272c04f9370a9f4e106cade3c511959fc9a18 Mon Sep 17 00:00:00 2001 From: vparomskiy Date: Tue, 27 Mar 2018 13:16:48 +0300 Subject: [PATCH 2/7] return permit if request expired/canceled --- .../dao/exception/BufferLimitException.java | 25 +++++++++++++++ .../dao/nosql/RateLimitedResultSetFuture.java | 14 +++++--- .../server/dao/util/BufferedRateLimiter.java | 21 +++++++++--- .../nosql/RateLimitedResultSetFutureTest.java | 32 +++++++++++++++++-- .../dao/util/BufferedRateLimiterTest.java | 5 +-- 5 files changed, 82 insertions(+), 15 deletions(-) create mode 100644 dao/src/main/java/org/thingsboard/server/dao/exception/BufferLimitException.java 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..03eb46f1ab 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,17 @@ 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(); } 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 +142,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()); } } From 2f6995fcedb0f795429a3d4b0ad1c7fb687314c7 Mon Sep 17 00:00:00 2001 From: Andrew Shvayka Date: Tue, 27 Mar 2018 16:59:41 +0300 Subject: [PATCH 3/7] Fix for UI build. --- ui/package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ui/package.json b/ui/package.json index ad95ef4a7a..ad9a7a67a3 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", From fd1199ee1c3927c53377d40e750aaae7d8b72e06 Mon Sep 17 00:00:00 2001 From: Igor Kulikov Date: Wed, 28 Mar 2018 11:03:11 +0300 Subject: [PATCH 4/7] RuleNode Config UI --- .../AnnotationComponentDiscoveryService.java | 2 + pom.xml | 1 + .../rule/engine/api/NodeDefinition.java | 2 + .../thingsboard/rule/engine/api/RuleNode.java | 4 + .../rule/engine/filter/TbJsFilterNode.java | 5 +- .../static/rulenode/rulenode-core-config.js | 2 + ui/server.js | 21 ++++++ ui/src/app/api/rule-chain.service.js | 59 +++++++++++++-- ui/src/app/components/js-func.directive.js | 34 +++++++-- ui/src/app/components/js-func.scss | 10 ++- ui/src/app/components/js-func.tpl.html | 15 ++-- ui/src/app/locale/locale.constant.js | 4 +- ui/src/app/rulechain/index.js | 4 + ui/src/app/rulechain/rulechain.controller.js | 21 +++--- .../rulechain/rulenode-config.directive.js | 73 +++++++++++++++++++ ui/src/app/rulechain/rulenode-config.tpl.html | 32 ++++++++ .../rulenode-defined-config.directive.js | 63 ++++++++++++++++ .../app/rulechain/rulenode-fieldset.tpl.html | 9 ++- ui/src/app/rulechain/rulenode.scss | 6 ++ 19 files changed, 336 insertions(+), 31 deletions(-) create mode 100644 rule-engine/rule-engine-components/src/main/resources/public/static/rulenode/rulenode-core-config.js create mode 100644 ui/src/app/rulechain/rulenode-config.directive.js create mode 100644 ui/src/app/rulechain/rulenode-config.tpl.html create mode 100644 ui/src/app/rulechain/rulenode-defined-config.directive.js 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/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..c684b200c5 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 meta property. For example meta.customerName === 'John';", + uiResources = {"static/rulenode/rulenode-core-config.js"}, + configDirective = "tbFilterNodeScriptConfig") + public class TbJsFilterNode implements TbNode { private TbJsFilterNodeConfiguration config; 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..f254cf599d --- /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(r){if(n[r])return n[r].exports;var u=n[r]={exports:{},id:r,loaded:!1};return e[r].call(u.exports,u,u.exports,t),u.loaded=!0,u.exports}var n={};return t.m=e,t.c=n,t.p="/static/",t(0)}([function(e,t,n){e.exports=n(3)},function(e,t){e.exports='
'},function(e,t,n){"use strict";function r(e){return e&&e.__esModule?e:{default:e}}function u(e){var t=function(t,n,r,u){var o=i.default;n.html(o),t.$watch("configuration",function(e,n){angular.equals(e,n)||u.$setViewValue(t.configuration)}),u.$render=function(){t.configuration=u.$viewValue},e(n.contents())(t)};return{restrict:"E",require:"^ngModel",scope:{},link:t}}u.$inject=["$compile"],Object.defineProperty(t,"__esModule",{value:!0}),t.default=u;var o=n(1),i=r(o)},function(e,t,n){"use strict";function r(e){return e&&e.__esModule?e:{default:e}}Object.defineProperty(t,"__esModule",{value:!0});var u=n(2),o=r(u),i=n(5),a=r(i);t.default=angular.module("thingsboard.ruleChain.config",[]).directive("tbFilterNodeScriptConfig",o.default).config(a.default).name},function(e,t){"use strict";function n(e){var t={tb:{rulenode:{filter:"Filter"}}};angular.merge(e.en_US,t)}Object.defineProperty(t,"__esModule",{value:!0}),t.default=n},function(e,t,n){"use strict";function r(e){return e&&e.__esModule?e:{default:e}}function u(e,t){(0,i.default)(t);for(var n in t){var r=t[n];e.translations(n,r)}}u.$inject=["$translateProvider","locales"],Object.defineProperty(t,"__esModule",{value:!0}),t.default=u;var o=n(4),i=r(o)}]); +//# sourceMappingURL=rulenode-core-config.js.map \ No newline at end of file diff --git a/ui/server.js b/ui/server.js index fae132f02f..0513600afd 100644 --- a/ui/server.js +++ b/ui/server.js @@ -30,6 +30,10 @@ const httpProxy = require('http-proxy'); const forwardHost = 'localhost'; const forwardPort = 8080; +const ruleNodeUiforwardHost = 'localhost'; +const ruleNodeUiforwardPort = 8080; +//const ruleNodeUiforwardPort = 5000; + const app = express(); const server = http.createServer(app); @@ -52,17 +56,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/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/locale/locale.constant.js b/ui/src/app/locale/locale.constant.js index 5dce7872e8..b511b56c33 100644 --- a/ui/src/app/locale/locale.constant.js +++ b/ui/src/app/locale/locale.constant.js @@ -1198,7 +1198,9 @@ export default angular.module('thingsboard.locale', []) "type-action": "Action", "type-action-details": "Perform special action", "type-rule-chain": "Rule Chain", - "type-rule-chain-details": "Forwards incoming messages to specified Rule Chain" + "type-rule-chain-details": "Forwards incoming messages to specified Rule Chain", + "directive-is-not-loaded": "Defined configuration directive '{{directiveName}}' is not available.", + "ui-resources-load-error": "Failed to load configuration ui resources." }, "rule-plugin": { "management": "Rules and plugins management" diff --git a/ui/src/app/rulechain/index.js b/ui/src/app/rulechain/index.js index 7306762572..c674467d35 100644 --- a/ui/src/app/rulechain/index.js +++ b/ui/src/app/rulechain/index.js @@ -18,6 +18,8 @@ import RuleChainRoutes from './rulechain.routes'; import RuleChainsController from './rulechains.controller'; import {RuleChainController, AddRuleNodeController, AddRuleNodeLinkController} from './rulechain.controller'; import RuleChainDirective from './rulechain.directive'; +import RuleNodeDefinedConfigDirective from './rulenode-defined-config.directive'; +import RuleNodeConfigDirective from './rulenode-config.directive'; import RuleNodeDirective from './rulenode.directive'; import LinkDirective from './link.directive'; @@ -28,6 +30,8 @@ export default angular.module('thingsboard.ruleChain', []) .controller('AddRuleNodeController', AddRuleNodeController) .controller('AddRuleNodeLinkController', AddRuleNodeLinkController) .directive('tbRuleChain', RuleChainDirective) + .directive('tbRuleNodeDefinedConfig', RuleNodeDefinedConfigDirective) + .directive('tbRuleNodeConfig', RuleNodeConfigDirective) .directive('tbRuleNode', RuleNodeDirective) .directive('tbRuleNodeLink', LinkDirective) .name; diff --git a/ui/src/app/rulechain/rulechain.controller.js b/ui/src/app/rulechain/rulechain.controller.js index 4eba5b2c52..dd48bb007a 100644 --- a/ui/src/app/rulechain/rulechain.controller.js +++ b/ui/src/app/rulechain/rulechain.controller.js @@ -137,10 +137,13 @@ export function RuleChainController($stateParams, $scope, $compile, $q, $mdUtil, }; vm.saveRuleNode = function(theForm) { - theForm.$setPristine(); - vm.isEditingRuleNode = false; - vm.ruleChainModel.nodes[vm.editingRuleNodeIndex] = vm.editingRuleNode; - vm.editingRuleNode = angular.copy(vm.editingRuleNode); + $scope.$broadcast('form-submit'); + if (theForm.$valid) { + theForm.$setPristine(); + vm.isEditingRuleNode = false; + vm.ruleChainModel.nodes[vm.editingRuleNodeIndex] = vm.editingRuleNode; + vm.editingRuleNode = angular.copy(vm.editingRuleNode); + } }; vm.saveRuleNodeLink = function(theForm) { @@ -309,7 +312,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 +361,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 +392,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 +469,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 +614,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( 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..4b75c793f1 --- /dev/null +++ b/ui/src/app/rulechain/rulenode-config.directive.js @@ -0,0 +1,73 @@ +/* + * 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.configDirective && !scope.definedDirectiveError; + }; + + 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..5ec2620aaf --- /dev/null +++ b/ui/src/app/rulechain/rulenode-defined-config.directive.js @@ -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. + */ + +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() { + var directive = snake_case(attrs.ruleNodeDirective, '-'); + var template = `<${directive} ng-model="configuration" ng-required="required" ng-readonly="readonly">`; + element.html(template); + $compile(element.contents())(scope); + } + + 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..ad109ef6cb 100644 --- a/ui/src/app/rulechain/rulenode-fieldset.tpl.html +++ b/ui/src/app/rulechain/rulenode-fieldset.tpl.html @@ -38,11 +38,16 @@ ng-model="ruleNode.debugMode">{{ 'rulenode.debug-mode' | translate }} - + + 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 From 784de0836fca6c6ea95de8061dd4532623639332 Mon Sep 17 00:00:00 2001 From: Igor Kulikov Date: Thu, 29 Mar 2018 10:57:28 +0300 Subject: [PATCH 5/7] Filter nodes UI configuration. --- .../rule/engine/filter/TbJsFilterNode.java | 4 +- .../filter/TbJsFilterNodeConfiguration.java | 2 +- .../rule/engine/filter/TbJsSwitchNode.java | 27 ++-------- .../filter/TbJsSwitchNodeConfiguration.java | 8 +-- .../engine/filter/TbMsgTypeFilterNode.java | 4 +- .../TbMsgTypeFilterNodeConfiguration.java | 2 +- .../rule/engine/js/NashornJsEngine.java | 18 ++++--- .../engine/metadata/TbGetAttributesNode.java | 2 +- .../metadata/TbGetCustomerAttributeNode.java | 2 +- .../metadata/TbGetRelatedAttributeNode.java | 2 +- .../metadata/TbGetTenantAttributeNode.java | 2 +- .../engine/transform/TbTransformMsgNode.java | 4 +- .../TbTransformMsgNodeConfiguration.java | 2 +- .../static/rulenode/rulenode-core-config.css | 2 + .../static/rulenode/rulenode-core-config.js | 2 +- .../engine/filter/TbJsFilterNodeTest.java | 14 ++--- .../engine/filter/TbJsSwitchNodeTest.java | 52 ++++--------------- .../transform/TbTransformMsgNodeTest.java | 6 +-- ui/server.js | 1 - .../components/json-object-edit.directive.js | 23 ++++++-- ui/src/app/locale/locale.constant.js | 1 + ui/src/app/rulechain/rulechain.controller.js | 6 +++ ui/src/app/rulechain/rulechain.tpl.html | 27 ++++++---- .../rulechain/rulenode-config.directive.js | 9 +++- .../rulenode-defined-config.directive.js | 6 ++- .../app/rulechain/rulenode-fieldset.tpl.html | 35 +++++-------- 26 files changed, 123 insertions(+), 140 deletions(-) create mode 100644 rule-engine/rule-engine-components/src/main/resources/public/static/rulenode/rulenode-core-config.css 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 c684b200c5..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,7 @@ 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") @@ -47,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 index f254cf599d..3fe859d90f 100644 --- 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 @@ -1,2 +1,2 @@ -!function(e){function t(r){if(n[r])return n[r].exports;var u=n[r]={exports:{},id:r,loaded:!1};return e[r].call(u.exports,u,u.exports,t),u.loaded=!0,u.exports}var n={};return t.m=e,t.c=n,t.p="/static/",t(0)}([function(e,t,n){e.exports=n(3)},function(e,t){e.exports='
'},function(e,t,n){"use strict";function r(e){return e&&e.__esModule?e:{default:e}}function u(e){var t=function(t,n,r,u){var o=i.default;n.html(o),t.$watch("configuration",function(e,n){angular.equals(e,n)||u.$setViewValue(t.configuration)}),u.$render=function(){t.configuration=u.$viewValue},e(n.contents())(t)};return{restrict:"E",require:"^ngModel",scope:{},link:t}}u.$inject=["$compile"],Object.defineProperty(t,"__esModule",{value:!0}),t.default=u;var o=n(1),i=r(o)},function(e,t,n){"use strict";function r(e){return e&&e.__esModule?e:{default:e}}Object.defineProperty(t,"__esModule",{value:!0});var u=n(2),o=r(u),i=n(5),a=r(i);t.default=angular.module("thingsboard.ruleChain.config",[]).directive("tbFilterNodeScriptConfig",o.default).config(a.default).name},function(e,t){"use strict";function n(e){var t={tb:{rulenode:{filter:"Filter"}}};angular.merge(e.en_US,t)}Object.defineProperty(t,"__esModule",{value:!0}),t.default=n},function(e,t,n){"use strict";function r(e){return e&&e.__esModule?e:{default:e}}function u(e,t){(0,i.default)(t);for(var n in t){var r=t[n];e.translations(n,r)}}u.$inject=["$translateProvider","locales"],Object.defineProperty(t,"__esModule",{value:!0}),t.default=u;var o=n(4),i=r(o)}]); +!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/server.js b/ui/server.js index 0513600afd..65a2bc7f45 100644 --- a/ui/server.js +++ b/ui/server.js @@ -32,7 +32,6 @@ const forwardPort = 8080; const ruleNodeUiforwardHost = 'localhost'; const ruleNodeUiforwardPort = 8080; -//const ruleNodeUiforwardPort = 5000; const app = express(); const server = http.createServer(app); 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/locale/locale.constant.js b/ui/src/app/locale/locale.constant.js index b511b56c33..a70ca14c5f 100644 --- a/ui/src/app/locale/locale.constant.js +++ b/ui/src/app/locale/locale.constant.js @@ -1171,6 +1171,7 @@ export default angular.module('thingsboard.locale', []) "debug-mode": "Debug mode" }, "rulenode": { + "details": "Details", "add": "Add rule node", "name": "Name", "name-required": "Name is required.", diff --git a/ui/src/app/rulechain/rulechain.controller.js b/ui/src/app/rulechain/rulechain.controller.js index dd48bb007a..fbdda227fb 100644 --- a/ui/src/app/rulechain/rulechain.controller.js +++ b/ui/src/app/rulechain/rulechain.controller.js @@ -256,6 +256,9 @@ export function RuleChainController($stateParams, $scope, $compile, $q, $mdUtil, vm.isEditingRuleNodeLink = true; vm.editingRuleNodeLinkIndex = vm.ruleChainModel.edges.indexOf(edge); vm.editingRuleNodeLink = angular.copy(edge); + $mdUtil.nextTick(() => { + vm.ruleNodeLinkForm.$setPristine(); + }); } }, nodeCallbacks: { @@ -266,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(); + }); } } }, diff --git a/ui/src/app/rulechain/rulechain.tpl.html b/ui/src/app/rulechain/rulechain.tpl.html index d23f920e0f..3083f83835 100644 --- a/ui/src/app/rulechain/rulechain.tpl.html +++ b/ui/src/app/rulechain/rulechain.tpl.html @@ -65,7 +65,8 @@
-
- - -
+ + +
+ + +
+
+
{ + if (scope.nodeDefinition) { + validateDefinedDirective(); + } + }); function validateDefinedDirective() { if (scope.nodeDefinition.uiResourceLoadError && scope.nodeDefinition.uiResourceLoadError.length) { diff --git a/ui/src/app/rulechain/rulenode-defined-config.directive.js b/ui/src/app/rulechain/rulenode-defined-config.directive.js index 5ec2620aaf..5100fbb96b 100644 --- a/ui/src/app/rulechain/rulenode-defined-config.directive.js +++ b/ui/src/app/rulechain/rulenode-defined-config.directive.js @@ -36,10 +36,14 @@ export default function RuleNodeDefinedConfigDirective($compile) { }; 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); - $compile(element.contents())(scope); + scope.ruleNodeConfigScope = scope.$new(); + $compile(element.contents())(scope.ruleNodeConfigScope); } function snake_case(name, separator) { diff --git a/ui/src/app/rulechain/rulenode-fieldset.tpl.html b/ui/src/app/rulechain/rulenode-fieldset.tpl.html index ad109ef6cb..7b0fae5a5f 100644 --- a/ui/src/app/rulechain/rulenode-fieldset.tpl.html +++ b/ui/src/app/rulechain/rulenode-fieldset.tpl.html @@ -21,33 +21,26 @@
- - - -
- - - -
-
rulenode.name-required
-
-
- - {{ 'rulenode.debug-mode' | translate }} - - +
+ + + +
+
rulenode.name-required
+
+
+ + {{ 'rulenode.debug-mode' | translate }} + + +
- From 4bbcffdf637f7fca0aa2caa03c514f31a3872561 Mon Sep 17 00:00:00 2001 From: Igor Kulikov Date: Thu, 29 Mar 2018 20:25:47 +0300 Subject: [PATCH 6/7] Rule Node Debug UI --- .../server/actors/ActorSystemContext.java | 71 +++++++++++-------- .../AbstractRuleEngineControllerTest.java | 2 +- .../server/common/data/DataConstants.java | 2 +- ui/src/app/common/types.constant.js | 30 +++++++- .../components/details-sidenav.directive.js | 19 ++++- .../app/components/details-sidenav.tpl.html | 1 + .../event/event-content-dialog.controller.js | 17 ++++- .../event-header-debug-rulenode.tpl.html | 27 +++++++ ui/src/app/event/event-header.directive.js | 7 ++ .../event/event-row-debug-rulenode.tpl.html | 63 ++++++++++++++++ ui/src/app/event/event-row.directive.js | 16 ++++- ui/src/app/event/event-table.directive.js | 18 ++++- ui/src/app/locale/locale.constant.js | 15 ++++ ui/src/app/rulechain/rulechain.controller.js | 15 ++-- ui/src/app/rulechain/rulechain.tpl.html | 18 ++++- ui/src/app/rulechain/rulechains.tpl.html | 3 +- 16 files changed, 276 insertions(+), 48 deletions(-) create mode 100644 ui/src/app/event/event-header-debug-rulenode.tpl.html create mode 100644 ui/src/app/event/event-row-debug-rulenode.tpl.html 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/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/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..a25374b099 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, $window) { var linker = function (scope, element, attrs) { @@ -42,6 +42,23 @@ function DetailsSidenav($timeout) { scope.isEdit = true; } + if (angular.isDefined(attrs.closeOnClickOutside && attrs.closeOnClickOutside)) { + scope.closeOnClickOutside = true; + var clickOutsideHandler = function() { + scope.closeDetails(); + }; + angular.element($window).click(clickOutsideHandler); + scope.$on("$destroy", function () { + angular.element($window).unbind('click', clickOutsideHandler); + }); + } + + scope.onClick = function($event) { + if (scope.closeOnClickOutside) { + $event.stopPropagation(); + } + }; + 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..a0032ff435 100644 --- a/ui/src/app/components/details-sidenav.tpl.html +++ b/ui/src/app/components/details-sidenav.tpl.html @@ -19,6 +19,7 @@ md-disable-backdrop="true" md-is-open="isOpen" md-component-id="right" + ng-click="onClick($event)" layout="column">
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
- + -
+
+ + + +
+ debug-event-types="{{vm.types.debugEventType.debugRuleChain.value}}" + default-event-type="{{vm.types.debugEventType.debugRuleChain.value}}"> From 3bcd596ed7fc33c6f71432408a61a08553f82318 Mon Sep 17 00:00:00 2001 From: Igor Kulikov Date: Fri, 30 Mar 2018 11:10:06 +0300 Subject: [PATCH 7/7] Details sidenav backdrop support --- .../components/details-sidenav.directive.js | 66 +++++++++++++++---- .../app/components/details-sidenav.tpl.html | 3 +- ui/src/app/rulechain/rulechain.tpl.html | 4 +- 3 files changed, 56 insertions(+), 17 deletions(-) diff --git a/ui/src/app/components/details-sidenav.directive.js b/ui/src/app/components/details-sidenav.directive.js index a25374b099..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, $window) { +function DetailsSidenav($timeout, $mdUtil, $q, $animate) { var linker = function (scope, element, attrs) { @@ -42,22 +42,62 @@ function DetailsSidenav($timeout, $window) { scope.isEdit = true; } - if (angular.isDefined(attrs.closeOnClickOutside && attrs.closeOnClickOutside)) { - scope.closeOnClickOutside = true; - var clickOutsideHandler = function() { - scope.closeDetails(); - }; - angular.element($window).click(clickOutsideHandler); - scope.$on("$destroy", function () { - angular.element($window).unbind('click', clickOutsideHandler); + 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(); }); } - scope.onClick = function($event) { - if (scope.closeOnClickOutside) { - $event.stopPropagation(); + 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) { diff --git a/ui/src/app/components/details-sidenav.tpl.html b/ui/src/app/components/details-sidenav.tpl.html index a0032ff435..763bc2295a 100644 --- a/ui/src/app/components/details-sidenav.tpl.html +++ b/ui/src/app/components/details-sidenav.tpl.html @@ -16,10 +16,9 @@ -->
diff --git a/ui/src/app/rulechain/rulechain.tpl.html b/ui/src/app/rulechain/rulechain.tpl.html index 0d557719cc..b25447e175 100644 --- a/ui/src/app/rulechain/rulechain.tpl.html +++ b/ui/src/app/rulechain/rulechain.tpl.html @@ -69,7 +69,7 @@ + ' - ' + vm.editingRuleNode.component.name}}" is-read-only="vm.selectedRuleNodeTabIndex > 0" is-open="vm.isEditingRuleNode" - close-on-click-outside="true" + tb-enable-backdrop is-always-edit="true" on-close-details="vm.onEditRuleNodeClosed()" on-toggle-details-edit-mode="vm.onRevertRuleNodeEdit(vm.ruleNodeForm)" @@ -108,7 +108,7 @@ header-subtitle="{{'rulenode.link-details' | translate}}" is-read-only="false" is-open="vm.isEditingRuleNodeLink" - close-on-click-outside="true" + tb-enable-backdrop is-always-edit="true" on-close-details="vm.onEditRuleNodeLinkClosed()" on-toggle-details-edit-mode="vm.onRevertRuleNodeLinkEdit(vm.ruleNodeLinkForm)"