thingsboard-memoizeit

Changes

pom.xml 1(+1 -0)

ui/package.json 2(+1 -1)

ui/server.js 20(+20 -0)

Details

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 a5a20b8..9e02946 100644
--- a/application/src/main/java/org/thingsboard/server/actors/ActorSystemContext.java
+++ b/application/src/main/java/org/thingsboard/server/actors/ActorSystemContext.java
@@ -25,6 +25,7 @@ import com.typesafe.config.Config;
 import com.typesafe.config.ConfigFactory;
 import lombok.Getter;
 import lombok.Setter;
+import lombok.extern.slf4j.Slf4j;
 import org.springframework.beans.factory.annotation.Autowired;
 import org.springframework.beans.factory.annotation.Value;
 import org.springframework.stereotype.Component;
@@ -38,6 +39,7 @@ import org.thingsboard.server.common.data.id.EntityId;
 import org.thingsboard.server.common.data.id.TenantId;
 import org.thingsboard.server.common.data.plugin.ComponentLifecycleEvent;
 import org.thingsboard.server.common.msg.TbMsg;
+import org.thingsboard.server.common.msg.TbMsgDataType;
 import org.thingsboard.server.common.msg.cluster.ServerAddress;
 import org.thingsboard.server.common.transport.auth.DeviceAuthService;
 import org.thingsboard.server.controller.plugin.PluginWebSocketMsgEndpoint;
@@ -60,11 +62,13 @@ import org.thingsboard.server.service.cluster.routing.ClusterRoutingService;
 import org.thingsboard.server.service.cluster.rpc.ClusterRpcService;
 import org.thingsboard.server.service.component.ComponentDiscoveryService;
 
+import java.io.IOException;
 import java.io.PrintWriter;
 import java.io.StringWriter;
 import java.nio.charset.StandardCharsets;
 import java.util.Optional;
 
+@Slf4j
 @Component
 public class ActorSystemContext {
     private static final String AKKA_CONF_FILE_NAME = "actor-system.conf";
@@ -292,38 +296,49 @@ public class ActorSystemContext {
     }
 
     private void persistDebug(TenantId tenantId, EntityId entityId, String type, TbMsg tbMsg, Throwable error) {
-        Event event = new Event();
-        event.setTenantId(tenantId);
-        event.setEntityId(entityId);
-        event.setType(DataConstants.DEBUG);
-
-        ObjectNode node = mapper.createObjectNode()
-                .put("type", type)
-                .put("server", getServerAddress())
-                .put("entityId", tbMsg.getOriginator().getId().toString())
-                .put("entityName", tbMsg.getOriginator().getEntityType().name())
-                .put("msgId", tbMsg.getId().toString())
-                .put("msgType", tbMsg.getType())
-                .put("dataType", tbMsg.getDataType().name());
-
-        ObjectNode mdNode = node.putObject("metadata");
-        tbMsg.getMetaData().getData().forEach(mdNode::put);
+        try {
+            Event event = new Event();
+            event.setTenantId(tenantId);
+            event.setEntityId(entityId);
+            event.setType(DataConstants.DEBUG_RULE_NODE);
+
+            String metadata = mapper.writeValueAsString(tbMsg.getMetaData().getData());
+
+            ObjectNode node = mapper.createObjectNode()
+                    .put("type", type)
+                    .put("server", getServerAddress())
+                    .put("entityId", tbMsg.getOriginator().getId().toString())
+                    .put("entityName", tbMsg.getOriginator().getEntityType().name())
+                    .put("msgId", tbMsg.getId().toString())
+                    .put("msgType", tbMsg.getType())
+                    .put("dataType", tbMsg.getDataType().name())
+                    .put("data", convertToString(tbMsg.getDataType(), tbMsg.getData()))
+                    .put("metadata", metadata);
+
+            if (error != null) {
+                node = node.put("error", toString(error));
+            }
+
+            event.setBody(node);
+            eventService.save(event);
+        } catch (IOException ex) {
+            log.warn("Failed to persist rule node debug message", ex);
+        }
+    }
 
-        switch (tbMsg.getDataType()) {
+    private String convertToString(TbMsgDataType messageType, byte[] data) {
+        if (data == null) {
+            return null;
+        }
+        switch (messageType) {
+            case JSON:
+            case TEXT:
+                return new String(data, StandardCharsets.UTF_8);
             case BINARY:
-                node.put("data", Base64Utils.encodeUrlSafe(tbMsg.getData()));
-                break;
+                return Base64Utils.encodeToString(data);
             default:
-                node.put("data", new String(tbMsg.getData(), StandardCharsets.UTF_8));
-                break;
-        }
-
-        if (error != null) {
-            node = node.put("error", toString(error));
+                throw new RuntimeException("Message type: " + messageType + " is not supported!");
         }
-
-        event.setBody(node);
-        eventService.save(event);
     }
 
     public static Exception toException(Throwable error) {
diff --git a/application/src/main/java/org/thingsboard/server/service/component/AnnotationComponentDiscoveryService.java b/application/src/main/java/org/thingsboard/server/service/component/AnnotationComponentDiscoveryService.java
index 479f424..9377756 100644
--- a/application/src/main/java/org/thingsboard/server/service/component/AnnotationComponentDiscoveryService.java
+++ b/application/src/main/java/org/thingsboard/server/service/component/AnnotationComponentDiscoveryService.java
@@ -192,6 +192,8 @@ public class AnnotationComponentDiscoveryService implements ComponentDiscoverySe
         NodeConfiguration config = configClazz.newInstance();
         NodeConfiguration defaultConfiguration = config.defaultConfiguration();
         nodeDefinition.setDefaultConfiguration(mapper.valueToTree(defaultConfiguration));
+        nodeDefinition.setUiResources(nodeAnnotation.uiResources());
+        nodeDefinition.setConfigDirective(nodeAnnotation.configDirective());
         return nodeDefinition;
     }
 
diff --git a/application/src/test/java/org/thingsboard/server/controller/AbstractRuleEngineControllerTest.java b/application/src/test/java/org/thingsboard/server/controller/AbstractRuleEngineControllerTest.java
index bbcb98f..93fe767 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<TimePageData<Event>>() {
-                }, 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 659a242..7d4e480 100644
--- a/common/data/src/main/java/org/thingsboard/server/common/data/DataConstants.java
+++ b/common/data/src/main/java/org/thingsboard/server/common/data/DataConstants.java
@@ -37,7 +37,7 @@ public class DataConstants {
     public static final String ERROR = "ERROR";
     public static final String LC_EVENT = "LC_EVENT";
     public static final String STATS = "STATS";
-    public static final String DEBUG = "DEBUG";
+    public static final String DEBUG_RULE_NODE = "DEBUG_RULE_NODE";
 
     public static final String ONEWAY = "ONEWAY";
     public static final String TWOWAY = "TWOWAY";
diff --git a/dao/src/main/java/org/thingsboard/server/dao/exception/BufferLimitException.java b/dao/src/main/java/org/thingsboard/server/dao/exception/BufferLimitException.java
new file mode 100644
index 0000000..3334dc6
--- /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 2674c6d..d250563 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<Void> rateLimitFuture;
 
     public RateLimitedResultSetFuture(Session session, AsyncRateLimiter rateLimiter, Statement statement) {
-        this.rateLimitFuture = rateLimiter.acquireAsync();
+        this.rateLimitFuture = Futures.withFallback(rateLimiter.acquireAsync(), t -> {
+            if (!(t instanceof BufferLimitException)) {
+                rateLimiter.release();
+            }
+            return Futures.immediateFailedFuture(t);
+        });
         this.originalFuture = Futures.transform(rateLimitFuture,
                 (Function<Void, ResultSetFuture>) i -> executeAsyncWithRelease(rateLimiter, session, statement));
+
     }
 
     @Override
@@ -108,10 +115,7 @@ public class RateLimitedResultSetFuture implements ResultSetFuture {
             try {
                 ResultSetFuture resultSetFuture = Uninterruptibles.getUninterruptibly(originalFuture);
                 resultSetFuture.addListener(listener, executor);
-            } catch (CancellationException e) {
-                cancel(false);
-                return;
-            } catch (ExecutionException e) {
+            } catch (CancellationException | ExecutionException e) {
                 Futures.immediateFailedFuture(e).addListener(listener, executor);
             }
         }, executor);
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 2acd623..0419668 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<Void> acquireAsync() {
+        totalRequested.incrementAndGet();
         if (queue.isEmpty()) {
             if (permits.incrementAndGet() <= permitsLimit) {
                 if (permits.get() > maxGrantedPermissions.get()) {
                     maxGrantedPermissions.set(permits.get());
                 }
+                totalGranted.incrementAndGet();
                 return Futures.immediateFuture(null);
             }
             permits.decrementAndGet();
@@ -69,6 +75,7 @@ public class BufferedRateLimiter implements AsyncRateLimiter {
     @Override
     public void release() {
         permits.decrementAndGet();
+        totalReleased.incrementAndGet();
         reprocessQueue();
     }
 
@@ -80,6 +87,7 @@ public class BufferedRateLimiter implements AsyncRateLimiter {
                 }
                 LockedFuture lockedFuture = queue.poll();
                 if (lockedFuture != null) {
+                    totalGranted.incrementAndGet();
                     lockedFuture.latch.countDown();
                 } else {
                     permits.decrementAndGet();
@@ -112,17 +120,20 @@ public class BufferedRateLimiter implements AsyncRateLimiter {
                 LockedFuture lockedFuture = createLockedFuture();
                 if (!queue.offer(lockedFuture, 1, TimeUnit.SECONDS)) {
                     lockedFuture.cancelFuture();
-                    return Futures.immediateFailedFuture(new IllegalStateException("Rate Limit Buffer is full. Reject"));
+                    return Futures.immediateFailedFuture(new BufferLimitException());
+                }
+                if(permits.get() < permitsLimit) {
+                    reprocessQueue();
                 }
                 if(permits.get() < permitsLimit) {
                     reprocessQueue();
                 }
                 return lockedFuture.future;
             } catch (InterruptedException e) {
-                return Futures.immediateFailedFuture(new IllegalStateException("Rate Limit Task interrupted. Reject"));
+                return Futures.immediateFailedFuture(new BufferLimitException());
             }
         }
-        return Futures.immediateFailedFuture(new IllegalStateException("Rate Limit Buffer is full. Reject"));
+        return Futures.immediateFailedFuture(new BufferLimitException());
     }
 
     @Scheduled(fixedDelayString = "${cassandra.query.rate_limit_print_interval_ms}")
@@ -134,8 +145,11 @@ public class BufferedRateLimiter implements AsyncRateLimiter {
                 expiredCount++;
             }
         }
-        log.info("Permits maxBuffer is [{}] max concurrent [{}] expired [{}] current granted [{}]", maxQueueSize.getAndSet(0),
-                maxGrantedPermissions.getAndSet(0), expiredCount, permits.get());
+        log.info("Permits maxBuffer [{}] maxPermits [{}] expired [{}] currPermits [{}] currBuffer [{}] " +
+                        "totalPermits [{}] totalRequests [{}] totalReleased [{}]",
+                maxQueueSize.getAndSet(0), maxGrantedPermissions.getAndSet(0), expiredCount,
+                permits.get(), queue.size(),
+                totalGranted.getAndSet(0), totalRequested.getAndSet(0), totalReleased.getAndSet(0));
     }
 
     private class LockedFuture {
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 fa62c2b..f49668d 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<Void> future = MoreExecutors.listeningDecorator(Executors.newFixedThreadPool(1)).submit(() -> {
+            latch.await();
+            return null;
+        });
+        when(rateLimiter.acquireAsync()).thenReturn(future);
+        resultSetFuture = new RateLimitedResultSetFuture(session, rateLimiter, statement);
+
+        ListenableFuture<Row> transform = Futures.transform(resultSetFuture, ResultSet::one);
+//        TimeUnit.MILLISECONDS.sleep(200);
+        future.cancel(false);
+        latch.countDown();
+
+        try {
+            transform.get();
+            fail();
+        } catch (Exception e) {
+            assertTrue(e instanceof ExecutionException);
+        }
+        verify(rateLimiter, times(1)).acquireAsync();
+        verify(rateLimiter, times(1)).release();
+    }
+
 }
\ 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 5bfc3b6..67c3ce8 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());
         }
     }
 

pom.xml 1(+1 -0)

diff --git a/pom.xml b/pom.xml
index f0c915a..a90a8aa 100755
--- a/pom.xml
+++ b/pom.xml
@@ -284,6 +284,7 @@
                             <exclude>src/sh/**</exclude>
                             <exclude>src/main/scripts/control/**</exclude>
                             <exclude>src/main/scripts/windows/**</exclude>
+                            <exclude>src/main/resources/public/static/rulenode/**</exclude>
                         </excludes>
                         <mapping>
                             <proto>JAVADOC_STYLE</proto>
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 6c57d92..18b2b94 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 1617034..eea92ed 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 07b166d..eb8941c 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 <b>True</b> - send Message via <b>True</b> chain, otherwise <b>False</b> chain is used." +
                 "Message payload can be accessed via <code>msg</code> property. For example <code>msg.temperature < 10;</code>" +
-                "Message metadata can be accessed via <code>meta</code> property. For example <code>meta.customerName === 'John';</code>")
+                "Message metadata can be accessed via <code>metadata</code> property. For example <code>metadata.customerName === 'John';</code>",
+        uiResources = {"static/rulenode/rulenode-core-config.js"},
+        configDirective = "tbFilterNodeScriptConfig")
+
 public class TbJsFilterNode implements TbNode {
 
     private TbJsFilterNodeConfiguration config;
@@ -44,7 +47,7 @@ public class TbJsFilterNode implements TbNode {
     @Override
     public void init(TbNodeConfiguration configuration, TbNodeState state) throws TbNodeException {
         this.config = TbNodeUtils.convert(configuration, TbJsFilterNodeConfiguration.class);
-        this.jsEngine = new NashornJsEngine(config.getJsScript());
+        this.jsEngine = new NashornJsEngine(config.getJsScript(), "Filter");
     }
 
     @Override
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 3b19c7c..2d776ce 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 c1236a4..98f0ebc 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 <code>msg</code> property. For example <code>msg.temperature < 10;</code> " +
-                "Message metadata can be accessed via <code>meta</code> property. For example <code>meta.customerName === 'John';</code>")
+                "Message metadata can be accessed via <code>metadata</code> property. For example <code>metadata.customerName === 'John';</code>",
+        uiResources = {"static/rulenode/rulenode-core-config.js"},
+        configDirective = "tbFilterNodeSwitchConfig")
 public class TbJsSwitchNode implements TbNode {
 
     private TbJsSwitchNodeConfiguration config;
@@ -45,22 +47,11 @@ public class TbJsSwitchNode implements TbNode {
     @Override
     public void init(TbNodeConfiguration configuration, TbNodeState state) throws TbNodeException {
         this.config = TbNodeUtils.convert(configuration, TbJsSwitchNodeConfiguration.class);
-        if (config.getAllowedRelations().size() < 1) {
-            String message = "Switch node should have at least 1 relation";
-            log.error(message);
-            throw new IllegalStateException(message);
-        }
-        if (!config.isRouteToAllWithNoCheck()) {
-            this.jsEngine = new NashornJsEngine(config.getJsScript());
-        }
+        this.jsEngine = new NashornJsEngine(config.getJsScript(), "Switch");
     }
 
     @Override
     public void onMsg(TbContext ctx, TbMsg msg) {
-        if (config.isRouteToAllWithNoCheck()) {
-            ctx.tellNext(msg, config.getAllowedRelations());
-            return;
-        }
         ListeningExecutor jsExecutor = ctx.getJsExecutor();
         withCallback(jsExecutor.executeAsync(() -> jsEngine.executeSwitch(toBindings(msg))),
                 result -> processSwitch(ctx, msg, result),
@@ -68,15 +59,7 @@ public class TbJsSwitchNode implements TbNode {
     }
 
     private void processSwitch(TbContext ctx, TbMsg msg, Set<String> nextRelations) {
-        if (validateRelations(nextRelations)) {
-            ctx.tellNext(msg, nextRelations);
-        } else {
-            ctx.tellError(msg, new IllegalStateException("Unsupported relation for switch " + nextRelations));
-        }
-    }
-
-    private boolean validateRelations(Set<String> nextRelations) {
-        return config.getAllowedRelations().containsAll(nextRelations);
+        ctx.tellNext(msg, nextRelations);
     }
 
     private Bindings toBindings(TbMsg msg) {
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 b354c71..3a4856c 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<String> allowedRelations;
-    private boolean routeToAllWithNoCheck;
 
     @Override
     public TbJsSwitchNodeConfiguration defaultConfiguration() {
         TbJsSwitchNodeConfiguration configuration = new TbJsSwitchNodeConfiguration();
-        configuration.setJsScript("function nextRelation(meta, msg) {\n" +
+        configuration.setJsScript("function nextRelation(metadata, msg) {\n" +
                 "    return ['one','nine'];" +
                 "};\n" +
                 "\n" +
-                "nextRelation(meta, msg);");
-        configuration.setAllowedRelations(Sets.newHashSet("one", "two"));
-        configuration.setRouteToAllWithNoCheck(false);
+                "return nextRelation(metadata, msg);");
         return configuration;
     }
 }
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 3a86c25..8d72a7b 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 <b>Success</b> chain, otherwise <b>Failure</b> chain is used.")
+                "If incoming MessageType is expected - send Message via <b>Success</b> chain, otherwise <b>Failure</b> chain is used.",
+        uiResources = {"static/rulenode/rulenode-core-config.js", "static/rulenode/rulenode-core-config.css"},
+        configDirective = "tbFilterNodeMessageTypeConfig")
 public class TbMsgTypeFilterNode implements TbNode {
 
     TbMsgTypeFilterNodeConfiguration config;
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 a2e1b17..aafd2ed 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 082535f..a4add40 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 69ee9d7..4fc0f3e 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, <b>CLIENT/SHARED/SERVER</b> attributes are added into Message metadata " +
                 "with specific prefix: <i>cs/shared/ss</i>. To access those attributes in other nodes this template can be used " +
-                "<code>meta.cs.temperature</code> or <code>meta.shared.limit</code> " +
+                "<code>metadata.cs.temperature</code> or <code>metadata.shared.limit</code> " +
                 "If Latest Telemetry enrichment configured, latest telemetry added into metadata without prefix.")
 public class TbGetAttributesNode implements TbNode {
 
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 cc6d6a1..c59a65e 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 " +
-                "<code>meta.temperature</code>. If Latest Telemetry enrichment configured, latest telemetry added into metadata")
+                "<code>metadata.temperature</code>. If Latest Telemetry enrichment configured, latest telemetry added into metadata")
 public class TbGetCustomerAttributeNode extends TbEntityGetAttrNode<CustomerId> {
 
     @Override
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 22c0b9f..4cd5cd5 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 " +
-                "<code>meta.temperature</code>. If Latest Telemetry enrichment configured, latest telemetry added into metadata")
+                "<code>metadata.temperature</code>. If Latest Telemetry enrichment configured, latest telemetry added into metadata")
 public class TbGetRelatedAttributeNode extends TbEntityGetAttrNode<EntityId> {
 
     private TbGetRelatedAttrNodeConfiguration config;
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 b5f5e02..3165385 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 " +
-                "<code>meta.temperature</code>. If Latest Telemetry enrichment configured, latest telemetry added into metadata")
+                "<code>metadata.temperature</code>. If Latest Telemetry enrichment configured, latest telemetry added into metadata")
 public class TbGetTenantAttributeNode extends TbEntityGetAttrNode<TenantId> {
 
     @Override
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 626790f..e47ea0f 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.<br/> " +
-                "<code>meta</code> - is a Message metadata.<br/>" +
+                "<code>metadata</code> - is a Message metadata.<br/>" +
                 "<code>msg</code> - is a Message payload.<br/>Any properties can be changed/removed/added in those objects.")
 public class TbTransformMsgNode extends TbAbstractTransformNode {
 
@@ -40,7 +40,7 @@ public class TbTransformMsgNode extends TbAbstractTransformNode {
     @Override
     public void init(TbNodeConfiguration configuration, TbNodeState state) throws TbNodeException {
         this.config = TbNodeUtils.convert(configuration, TbTransformMsgNodeConfiguration.class);
-        this.jsEngine = new NashornJsEngine(config.getJsScript());
+        this.jsEngine = new NashornJsEngine(config.getJsScript(), "Transform");
         setConfig(config);
     }
 
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 4f9e9eb..09d5ac4 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 0000000..a6103c1
--- /dev/null
+++ b/rule-engine/rule-engine-components/src/main/resources/public/static/rulenode/rulenode-core-config.css
@@ -0,0 +1,2 @@
+.tb-message-type-autocomplete .tb-not-found{display:block;line-height:1.5;height:48px}.tb-message-type-autocomplete .tb-not-found .tb-no-entries{line-height:48px}.tb-message-type-autocomplete li{height:auto!important;white-space:normal!important}
+/*# sourceMappingURL=rulenode-core-config.css.map*/
\ No newline at end of file
diff --git a/rule-engine/rule-engine-components/src/main/resources/public/static/rulenode/rulenode-core-config.js b/rule-engine/rule-engine-components/src/main/resources/public/static/rulenode/rulenode-core-config.js
new file mode 100644
index 0000000..3fe859d
--- /dev/null
+++ b/rule-engine/rule-engine-components/src/main/resources/public/static/rulenode/rulenode-core-config.js
@@ -0,0 +1,2 @@
+!function(e){function t(s){if(a[s])return a[s].exports;var n=a[s]={exports:{},id:s,loaded:!1};return e[s].call(n.exports,n,n.exports,t),n.loaded=!0,n.exports}var a={};return t.m=e,t.c=a,t.p="/static/",t(0)}([function(e,t,a){e.exports=a(8)},function(e,t){},function(e,t){e.exports=' <section layout=column> <label translate class="tb-title no-padding" ng-class="{\'tb-required\': required}">tb.rulenode.message-types-filter</label> <md-chips id=message_type_chips ng-required=required readonly=readonly ng-model=messageTypes md-autocomplete-snap md-transform-chip=transformMessageTypeChip($chip) md-require-match=false> <md-autocomplete id=message_type md-no-cache=true md-selected-item=selectedMessageType md-search-text=messageTypeSearchText md-items="item in messageTypesSearch(messageTypeSearchText)" md-item-text=item.name md-min-length=0 placeholder="{{\'tb.rulenode.message-type\' | translate }}" md-menu-class=tb-message-type-autocomplete> <span md-highlight-text=messageTypeSearchText md-highlight-flags=^i>{{item}}</span> <md-not-found> <div class=tb-not-found> <div class=tb-no-entries ng-if="!messageTypeSearchText || !messageTypeSearchText.length"> <span translate>tb.rulenode.no-message-types-found</span> </div> <div ng-if="messageTypeSearchText && messageTypeSearchText.length"> <span translate translate-values=\'{ messageType: "{{messageTypeSearchText | truncate:true:6:&apos;...&apos;}}" }\'>tb.rulenode.no-message-type-matching</span> <span> <a translate ng-click="createMessageType($event, \'#message_type_chips\')">tb.rulenode.create-new-message-type</a> </span> </div> </div> </md-not-found> </md-autocomplete> <md-chip-template> <span>{{$chip.name}}</span> </md-chip-template> </md-chips> <div class=tb-error-messages ng-messages=ngModelCtrl.$error role=alert> <div translate ng-message=messageTypes class=tb-error-message>tb.rulenode.message-types-required</div> </div> </section>'},function(e,t){e.exports=" <section layout=column> <label translate class=\"tb-title no-padding\">tb.rulenode.filter</label> <tb-js-func ng-model=configuration.jsScript function-name=Filter function-args=\"{{ ['msg', 'metadata'] }}\" no-validate=true> </tb-js-func> </section> "},function(e,t){e.exports=" <section layout=column> <label translate class=\"tb-title no-padding\">tb.rulenode.switch</label> <tb-js-func ng-model=configuration.jsScript function-name=Switch function-args=\"{{ ['msg', 'metadata'] }}\" no-validate=true> </tb-js-func> </section> "},function(e,t,a){"use strict";function s(e){return e&&e.__esModule?e:{default:e}}function n(e,t,a){var s=function(s,n,r,l){function u(){if(l.$viewValue){for(var e=[],t=0;t<s.messageTypes.length;t++)e.push(s.messageTypes[t].value);l.$viewValue.messageTypes=e,o()}}function o(){if(s.required){var e=!(!l.$viewValue.messageTypes||!l.$viewValue.messageTypes.length);l.$setValidity("messageTypes",e)}else l.$setValidity("messageTypes",!0)}var c=i.default;n.html(c),s.selectedMessageType=null,s.messageTypeSearchText=null,s.ngModelCtrl=l;var d=[];for(var p in a.messageType){var m={name:a.messageType[p].name,value:a.messageType[p].value};d.push(m)}s.transformMessageTypeChip=function(e){var a,s=t("filter")(d,{name:e},!0);return a=s&&s.length?angular.copy(s[0]):{name:e,value:e}},s.messageTypesSearch=function(e){var a=e?t("filter")(d,{name:e}):d;return a.map(function(e){return e.name})},s.createMessageType=function(e,t){var a=angular.element(t,n)[0].firstElementChild,s=angular.element(a),r=s.scope().$mdChipsCtrl.getChipBuffer();e.preventDefault(),e.stopPropagation(),s.scope().$mdChipsCtrl.appendChip(r.trim()),s.scope().$mdChipsCtrl.resetChipBuffer()},l.$render=function(){var e=l.$viewValue,t=[];if(e&&e.messageTypes)for(var n=0;n<e.messageTypes.length;n++){var r=e.messageTypes[n];a.messageType[r]?t.push(angular.copy(a.messageType[r])):t.push({name:r,value:r})}s.messageTypes=t,s.$watch("messageTypes",function(e,t){angular.equals(e,t)||u()},!0)},e(n.contents())(s)};return{restrict:"E",require:"^ngModel",scope:{required:"=ngRequired",readonly:"=ngReadonly"},link:s}}n.$inject=["$compile","$filter","ruleNodeTypes"],Object.defineProperty(t,"__esModule",{value:!0}),t.default=n,a(1);var r=a(2),i=s(r)},function(e,t,a){"use strict";function s(e){return e&&e.__esModule?e:{default:e}}function n(e){var t=function(t,a,s,n){var r=i.default;a.html(r),t.$watch("configuration",function(e,a){angular.equals(e,a)||n.$setViewValue(t.configuration)}),n.$render=function(){t.configuration=n.$viewValue},e(a.contents())(t)};return{restrict:"E",require:"^ngModel",scope:{},link:t}}n.$inject=["$compile"],Object.defineProperty(t,"__esModule",{value:!0}),t.default=n;var r=a(3),i=s(r)},function(e,t,a){"use strict";function s(e){return e&&e.__esModule?e:{default:e}}function n(e){var t=function(t,a,s,n){var r=i.default;a.html(r),t.$watch("configuration",function(e,a){angular.equals(e,a)||n.$setViewValue(t.configuration)}),n.$render=function(){t.configuration=n.$viewValue},e(a.contents())(t)};return{restrict:"E",require:"^ngModel",scope:{},link:t}}n.$inject=["$compile"],Object.defineProperty(t,"__esModule",{value:!0}),t.default=n;var r=a(4),i=s(r)},function(e,t,a){"use strict";function s(e){return e&&e.__esModule?e:{default:e}}Object.defineProperty(t,"__esModule",{value:!0});var n=a(11),r=s(n),i=a(6),l=s(i),u=a(5),o=s(u),c=a(7),d=s(c),p=a(10),m=s(p);t.default=angular.module("thingsboard.ruleChain.config",[r.default]).directive("tbFilterNodeScriptConfig",l.default).directive("tbFilterNodeMessageTypeConfig",o.default).directive("tbFilterNodeSwitchConfig",d.default).config(m.default).name},function(e,t){"use strict";function a(e){var t={tb:{rulenode:{filter:"Filter",switch:"Switch","message-type":"Message type","message-types-filter":"Message types filter","no-message-types-found":"No message types found","no-message-type-matching":"'{{messageType}}' not found.","create-new-message-type":"Create a new one!","message-types-required":"Message types are required."}}};angular.merge(e.en_US,t)}Object.defineProperty(t,"__esModule",{value:!0}),t.default=a},function(e,t,a){"use strict";function s(e){return e&&e.__esModule?e:{default:e}}function n(e,t){(0,i.default)(t);for(var a in t){var s=t[a];e.translations(a,s)}}n.$inject=["$translateProvider","locales"],Object.defineProperty(t,"__esModule",{value:!0}),t.default=n;var r=a(9),i=s(r)},function(e,t){"use strict";Object.defineProperty(t,"__esModule",{value:!0}),t.default=angular.module("thingsboard.ruleChain.config.types",[]).constant("ruleNodeTypes",{messageType:{POST_ATTRIBUTES:{name:"Post attributes",value:"POST_ATTRIBUTES"},POST_TELEMETRY:{name:"Post telemetry",value:"POST_TELEMETRY"},RPC_REQUEST:{name:"RPC Request",value:"RPC_REQUEST"}}}).name}]);
+//# sourceMappingURL=rulenode-core-config.js.map
\ No newline at end of file
diff --git a/rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/filter/TbJsFilterNodeTest.java b/rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/filter/TbJsFilterNodeTest.java
index 96f7032..4c44886 100644
--- a/rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/filter/TbJsFilterNodeTest.java
+++ b/rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/filter/TbJsFilterNodeTest.java
@@ -51,7 +51,7 @@ public class TbJsFilterNodeTest {
 
     @Test
     public void falseEvaluationDoNotSendMsg() throws TbNodeException {
-        initWithScript("10 > 15;");
+        initWithScript("return 10 > 15;");
         TbMsg msg = new TbMsg(UUIDs.timeBased(), "USER", null, new TbMsgMetaData(), "{}".getBytes());
 
         mockJsExecutor();
@@ -64,7 +64,7 @@ public class TbJsFilterNodeTest {
 
     @Test
     public void notValidMsgDataThrowsException() throws TbNodeException {
-        initWithScript("10 > 15;");
+        initWithScript("return 10 > 15;");
         TbMsg msg = new TbMsg(UUIDs.timeBased(), "USER", null, new TbMsgMetaData(), new byte[4]);
 
         when(ctx.getJsExecutor()).thenReturn(executor);
@@ -77,7 +77,7 @@ public class TbJsFilterNodeTest {
 
     @Test
     public void exceptionInJsThrowsException() throws TbNodeException {
-        initWithScript("meta.temp.curr < 15;");
+        initWithScript("return metadata.temp.curr < 15;");
         TbMsgMetaData metaData = new TbMsgMetaData();
         TbMsg msg = new TbMsg(UUIDs.timeBased(), "USER", null, metaData, "{}".getBytes());
         mockJsExecutor();
@@ -89,12 +89,12 @@ public class TbJsFilterNodeTest {
 
     @Test(expected = IllegalArgumentException.class)
     public void notValidScriptThrowsException() throws TbNodeException {
-        initWithScript("10 > 15 asdq out");
+        initWithScript("return 10 > 15 asdq out");
     }
 
     @Test
     public void metadataConditionCanBeFalse() throws TbNodeException {
-        initWithScript("meta.humidity < 15;");
+        initWithScript("return metadata.humidity < 15;");
         TbMsgMetaData metaData = new TbMsgMetaData();
         metaData.putValue("temp", "10");
         metaData.putValue("humidity", "99");
@@ -109,7 +109,7 @@ public class TbJsFilterNodeTest {
 
     @Test
     public void metadataConditionCanBeTrue() throws TbNodeException {
-        initWithScript("meta.temp < 15;");
+        initWithScript("return metadata.temp < 15;");
         TbMsgMetaData metaData = new TbMsgMetaData();
         metaData.putValue("temp", "10");
         metaData.putValue("humidity", "99");
@@ -123,7 +123,7 @@ public class TbJsFilterNodeTest {
 
     @Test
     public void msgJsonParsedAndBinded() throws TbNodeException {
-        initWithScript("msg.passed < 15 && msg.name === 'Vit' && meta.temp == 10 && msg.bigObj.prop == 42;");
+        initWithScript("return msg.passed < 15 && msg.name === 'Vit' && metadata.temp == 10 && msg.bigObj.prop == 42;");
         TbMsgMetaData metaData = new TbMsgMetaData();
         metaData.putValue("temp", "10");
         metaData.putValue("humidity", "99");
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 e70d4e1..01227f4 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
@@ -53,27 +53,16 @@ public class TbJsSwitchNodeTest {
     private ListeningExecutor executor;
 
     @Test
-    public void routeToAllDoNotEvaluatesJs() throws TbNodeException {
-        HashSet<String> relations = Sets.newHashSet("one", "two");
-        initWithScript("test qwerty", relations, true);
-        TbMsg msg = new TbMsg(UUIDs.timeBased(), "USER", null, new TbMsgMetaData(), "{}".getBytes());
-
-        node.onMsg(ctx, msg);
-        verify(ctx).tellNext(msg, relations);
-        verifyNoMoreInteractions(ctx, executor);
-    }
-
-    @Test
     public void multipleRoutesAreAllowed() throws TbNodeException {
-        String jsCode = "function nextRelation(meta, msg) {\n" +
-                "    if(msg.passed == 5 && meta.temp == 10)\n" +
+        String jsCode = "function nextRelation(metadata, msg) {\n" +
+                "    if(msg.passed == 5 && metadata.temp == 10)\n" +
                 "        return ['three', 'one']\n" +
                 "    else\n" +
                 "        return 'two';\n" +
                 "};\n" +
                 "\n" +
-                "nextRelation(meta, msg);";
-        initWithScript(jsCode, Sets.newHashSet("one", "two", "three"), false);
+                "return nextRelation(metadata, msg);";
+        initWithScript(jsCode);
         TbMsgMetaData metaData = new TbMsgMetaData();
         metaData.putValue("temp", "10");
         metaData.putValue("humidity", "99");
@@ -89,15 +78,15 @@ public class TbJsSwitchNodeTest {
 
     @Test
     public void allowedRelationPassed() throws TbNodeException {
-        String jsCode = "function nextRelation(meta, msg) {\n" +
-                "    if(msg.passed == 5 && meta.temp == 10)\n" +
+        String jsCode = "function nextRelation(metadata, msg) {\n" +
+                "    if(msg.passed == 5 && metadata.temp == 10)\n" +
                 "        return 'one'\n" +
                 "    else\n" +
                 "        return 'two';\n" +
                 "};\n" +
                 "\n" +
-                "nextRelation(meta, msg);";
-        initWithScript(jsCode, Sets.newHashSet("one", "two"), false);
+                "return nextRelation(metadata, msg);";
+        initWithScript(jsCode);
         TbMsgMetaData metaData = new TbMsgMetaData();
         metaData.putValue("temp", "10");
         metaData.putValue("humidity", "99");
@@ -111,32 +100,9 @@ public class TbJsSwitchNodeTest {
         verify(ctx).tellNext(msg, Sets.newHashSet("one"));
     }
 
-    @Test
-    public void unknownRelationThrowsException() throws TbNodeException {
-        String jsCode = "function nextRelation(meta, msg) {\n" +
-                "    return ['one','nine'];" +
-                "};\n" +
-                "\n" +
-                "nextRelation(meta, msg);";
-        initWithScript(jsCode, Sets.newHashSet("one", "two"), false);
-        TbMsgMetaData metaData = new TbMsgMetaData();
-        metaData.putValue("temp", "10");
-        metaData.putValue("humidity", "99");
-        String rawJson = "{\"name\": \"Vit\", \"passed\": 5}";
-
-        TbMsg msg = new TbMsg(UUIDs.timeBased(), "USER", null, metaData, rawJson.getBytes());
-        mockJsExecutor();
-
-        node.onMsg(ctx, msg);
-        verify(ctx).getJsExecutor();
-        verifyError(msg, "Unsupported relation for switch [nine, one]", IllegalStateException.class);
-    }
-
-    private void initWithScript(String script, Set<String> relations, boolean routeToAll) throws TbNodeException {
+    private void initWithScript(String script) throws TbNodeException {
         TbJsSwitchNodeConfiguration config = new TbJsSwitchNodeConfiguration();
         config.setJsScript(script);
-        config.setAllowedRelations(relations);
-        config.setRouteToAllWithNoCheck(routeToAll);
         ObjectMapper mapper = new ObjectMapper();
         TbNodeConfiguration nodeConfiguration = new TbNodeConfiguration(mapper.valueToTree(config));
 
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 d69bad8..c6b3441 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");

ui/package.json 2(+1 -1)

diff --git a/ui/package.json b/ui/package.json
index e68aa34..ec3f880 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",

ui/server.js 20(+20 -0)

diff --git a/ui/server.js b/ui/server.js
index fae132f..65a2bc7 100644
--- a/ui/server.js
+++ b/ui/server.js
@@ -30,6 +30,9 @@ const httpProxy = require('http-proxy');
 const forwardHost = 'localhost';
 const forwardPort = 8080;
 
+const ruleNodeUiforwardHost = 'localhost';
+const ruleNodeUiforwardPort = 8080;
+
 const app = express();
 const server = http.createServer(app);
 
@@ -52,17 +55,34 @@ const apiProxy = httpProxy.createProxyServer({
     }
 });
 
+const ruleNodeUiApiProxy = httpProxy.createProxyServer({
+    target: {
+        host: ruleNodeUiforwardHost,
+        port: ruleNodeUiforwardPort
+    }
+});
+
 apiProxy.on('error', function (err, req, res) {
     console.warn('API proxy error: ' + err);
     res.end('Error.');
 });
 
+ruleNodeUiApiProxy.on('error', function (err, req, res) {
+    console.warn('RuleNode UI API proxy error: ' + err);
+    res.end('Error.');
+});
+
 console.info(`Forwarding API requests to http://${forwardHost}:${forwardPort}`);
+console.info(`Forwarding Rule Node UI requests to http://${ruleNodeUiforwardHost}:${ruleNodeUiforwardPort}`);
 
 app.all('/api/*', (req, res) => {
     apiProxy.web(req, res);
 });
 
+app.all('/static/rulenode/*', (req, res) => {
+    ruleNodeUiApiProxy.web(req, res);
+});
+
 app.get('*', function(req, res) {
     res.sendFile(path.join(__dirname, 'src/index.html'));
 });
diff --git a/ui/src/app/api/rule-chain.service.js b/ui/src/app/api/rule-chain.service.js
index ebc48fa..af14a3f 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<components.length;i++) {
+            var component = components[i];
+            tasks.push(resolveRuleNodeComponentUiResources(component));
+        }
+        $q.all(tasks).then(
+            (components) => {
+                deferred.resolve(components);
+            },
+            () => {
+                deferred.resolve(components);
+            }
+        );
+        return deferred.promise;
+    }
+
+    function resolveRuleNodeComponentUiResources(component) {
+        var deferred = $q.defer();
+        var uiResources = component.configurationDescriptor.nodeDefinition.uiResources;
+        if (uiResources && uiResources.length) {
+            var tasks = [];
+            for (var i=0;i<uiResources.length;i++) {
+                var uiResource = uiResources[i];
+                tasks.push($ocLazyLoad.load(uiResource));
+            }
+            $q.all(tasks).then(
+                () => {
+                    deferred.resolve(component);
+                },
+                () => {
+                    component.configurationDescriptor.nodeDefinition.uiResourceLoadError = $translate.instant('rulenode.ui-resources-load-error');
+                    deferred.resolve(component);
+                }
+            )
+        } else {
+            deferred.resolve(component);
+        }
+        return deferred.promise;
+    }
+
     function getRuleNodeComponentByClazz(clazz) {
         var res = $filter('filter')(ruleNodeComponents, {clazz: clazz}, true);
         if (res && res.length) {
diff --git a/ui/src/app/common/types.constant.js b/ui/src/app/common/types.constant.js
index 8026115..2186508 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 e455a80..2516134 100644
--- a/ui/src/app/components/details-sidenav.directive.js
+++ b/ui/src/app/components/details-sidenav.directive.js
@@ -26,7 +26,7 @@ export default angular.module('thingsboard.directives.detailsSidenav', [])
     .name;
 
 /*@ngInject*/
-function DetailsSidenav($timeout) {
+function DetailsSidenav($timeout, $mdUtil, $q, $animate) {
 
     var linker = function (scope, element, attrs) {
 
@@ -42,6 +42,63 @@ function DetailsSidenav($timeout) {
             scope.isEdit = true;
         }
 
+        var backdrop;
+        var previousContainerStyles;
+
+        if (attrs.hasOwnProperty('tbEnableBackdrop')) {
+            backdrop = $mdUtil.createBackdrop(scope, "md-sidenav-backdrop md-opaque ng-enter");
+            element.on('$destroy', function() {
+                backdrop && backdrop.remove();
+            });
+            scope.$on('$destroy', function(){
+                backdrop && backdrop.remove();
+            });
+            scope.$watch('isOpen', updateIsOpen);
+        }
+
+        function updateIsOpen(isOpen) {
+            backdrop[isOpen ? 'on' : 'off']('click', (ev)=>{
+                ev.preventDefault();
+                scope.isOpen = false;
+                scope.$apply();
+            });
+            var parent = element.parent();
+            var restorePositioning = updateContainerPositions(parent, isOpen);
+
+            return $q.all([
+                isOpen && backdrop ? $animate.enter(backdrop, parent) : backdrop ?
+                    $animate.leave(backdrop) : $q.when(true)
+            ]).then(function() {
+                restorePositioning && restorePositioning();
+            });
+        }
+
+        function updateContainerPositions(parent, willOpen) {
+            var drawerEl = element[0];
+            var scrollTop = parent[0].scrollTop;
+            if (willOpen && scrollTop) {
+                previousContainerStyles = {
+                    top: drawerEl.style.top,
+                    bottom: drawerEl.style.bottom,
+                    height: drawerEl.style.height
+                };
+                var positionStyle = {
+                    top: scrollTop + 'px',
+                    bottom: 'auto',
+                    height: parent[0].clientHeight + 'px'
+                };
+                backdrop.css(positionStyle);
+            }
+            if (!willOpen && previousContainerStyles) {
+                return function() {
+                    backdrop[0].style.top = null;
+                    backdrop[0].style.bottom = null;
+                    backdrop[0].style.height = null;
+                    previousContainerStyles = null;
+                };
+            }
+        }
+
         scope.toggleDetailsEditMode = function () {
             if (!scope.isAlwaysEdit) {
                 if (!scope.isEdit) {
diff --git a/ui/src/app/components/details-sidenav.tpl.html b/ui/src/app/components/details-sidenav.tpl.html
index c504a24..763bc22 100644
--- a/ui/src/app/components/details-sidenav.tpl.html
+++ b/ui/src/app/components/details-sidenav.tpl.html
@@ -16,7 +16,7 @@
 
 -->
 <md-sidenav class="md-sidenav-right md-whiteframe-4dp tb-sidenav-details"
-      md-disable-backdrop="true"
+      md-disable-backdrop
       md-is-open="isOpen"
       md-component-id="right"
       layout="column">
diff --git a/ui/src/app/components/js-func.directive.js b/ui/src/app/components/js-func.directive.js
index 33cebde..deb5626 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<scope.validationArgs.length;i++) {
@@ -200,9 +210,19 @@ function JsFunc($compile, $templateCache, toast, utils, $translate) {
             }
         };
 
-        scope.$on('form-submit', function () {
-            scope.functionValid = scope.validate();
-            scope.updateValidity();
+        scope.$on('form-submit', function (event, args) {
+            if (!args || scope.validationTriggerArg && scope.validationTriggerArg == args) {
+                scope.validationArgs = scope.$eval(attrs.validationArgs);
+                scope.cleanupJsErrors();
+                scope.functionValid = true;
+                scope.updateValidity();
+                scope.functionValid = scope.validate();
+                scope.updateValidity();
+            }
+        });
+
+        scope.$on('update-ace-editor-size', function () {
+            updateEditorSize();
         });
 
         $compile(element.contents())(scope);
@@ -211,7 +231,11 @@ function JsFunc($compile, $templateCache, toast, utils, $translate) {
     return {
         restrict: "E",
         require: "^ngModel",
-        scope: {},
+        scope: {
+            disabled:'=ngDisabled',
+            noValidate: '=?',
+            fillHeight:'=?'
+        },
         link: linker
     };
 }
diff --git a/ui/src/app/components/js-func.scss b/ui/src/app/components/js-func.scss
index 2bd5df1..e1072be 100644
--- a/ui/src/app/components/js-func.scss
+++ b/ui/src/app/components/js-func.scss
@@ -15,6 +15,12 @@
  */
 tb-js-func {
   position: relative;
+  .tb-disabled {
+    color: rgba(0,0,0,0.38);
+  }
+  .fill-height {
+    height: 100%;
+  }
 }
 
 .tb-js-func-panel {
@@ -23,8 +29,10 @@ tb-js-func {
   height: 100%;
   #tb-javascript-input {
     min-width: 200px;
-    min-height: 200px;
     width: 100%;
     height: 100%;
+    &:not(.fill-height) {
+      min-height: 200px;
+    }
   }
 }
diff --git a/ui/src/app/components/js-func.tpl.html b/ui/src/app/components/js-func.tpl.html
index 806de4a..93043d4 100644
--- a/ui/src/app/components/js-func.tpl.html
+++ b/ui/src/app/components/js-func.tpl.html
@@ -15,19 +15,20 @@
     limitations under the License.
 
 -->
-<div style="background: #fff;" tb-expand-fullscreen fullscreen-zindex="100" expand-button-id="expand-button" on-fullscreen-changed="onFullscreenChanged()" layout="column">
+<div style="background: #fff;" ng-class="{'tb-disabled': disabled, 'fill-height': fillHeight}" tb-expand-fullscreen fullscreen-zindex="100" expand-button-id="expand-button" on-fullscreen-changed="onFullscreenChanged()" layout="column">
 	<div layout="row" layout-align="start center" style="height: 40px;">
-		<span style="font-style: italic;">function({{ functionArgsString }}) {</span>
+		<label class="tb-title no-padding">function {{ functionName }}({{ functionArgsString }}) {</label>
 		<span flex></span>
 		<div id="expand-button" layout="column" aria-label="Fullscreen" class="md-button md-icon-button tb-md-32 tb-fullscreen-button-style"></div>
 	</div>
 	<div flex id="tb-javascript-panel" class="tb-js-func-panel" layout="column">
-		<div flex id="tb-javascript-input"
-			 ui-ace="jsEditorOptions" 
+		<div flex id="tb-javascript-input" ng-class="{'fill-height': fillHeight}"
+			 ui-ace="jsEditorOptions"
+			 ng-readonly="disabled"
 			 ng-model="functionBody">
 		</div>
 	</div>
 	<div layout="row" layout-align="start center"  style="height: 40px;">
-		<span style="font-style: italic;">}</span>
-	</div>	   
-</div>
\ No newline at end of file
+		<label class="tb-title no-padding">}</label>
+	</div>
+</div>
diff --git a/ui/src/app/components/json-object-edit.directive.js b/ui/src/app/components/json-object-edit.directive.js
index db0aa60..215b7b9 100644
--- a/ui/src/app/components/json-object-edit.directive.js
+++ b/ui/src/app/components/json-object-edit.directive.js
@@ -84,17 +84,32 @@ function JsonObjectEdit($compile, $templateCache, $document, toast, utils) {
         scope.$watch('contentBody', function (newVal, prevVal) {
             if (!angular.equals(newVal, prevVal)) {
                 var object = scope.validate();
-                ngModelCtrl.$setViewValue(object);
+                if (scope.objectValid) {
+                    if (object == null) {
+                        scope.object = null;
+                    } else {
+                        if (scope.object == null) {
+                            scope.object = {};
+                        }
+                        Object.keys(scope.object).forEach(function (key) {
+                            delete scope.object[key];
+                        });
+                        Object.keys(object).forEach(function (key) {
+                            scope.object[key] = object[key];
+                        });
+                    }
+                    ngModelCtrl.$setViewValue(scope.object);
+                }
                 scope.updateValidity();
             }
         });
 
         ngModelCtrl.$render = function () {
-            var object = ngModelCtrl.$viewValue;
+            scope.object = ngModelCtrl.$viewValue;
             var content = '';
             try {
-                if (object) {
-                    content = angular.toJson(object, true);
+                if (scope.object) {
+                    content = angular.toJson(scope.object, true);
                 }
             } catch (e) {
                 //
diff --git a/ui/src/app/event/event-content-dialog.controller.js b/ui/src/app/event/event-content-dialog.controller.js
index 108f95e..8d13f96 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.directive.js b/ui/src/app/event/event-header.directive.js
index afac804..bc4cdbe 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-header-debug-rulenode.tpl.html b/ui/src/app/event/event-header-debug-rulenode.tpl.html
new file mode 100644
index 0000000..b412a0c
--- /dev/null
+++ b/ui/src/app/event/event-header-debug-rulenode.tpl.html
@@ -0,0 +1,27 @@
+<!--
+
+    Copyright © 2016-2018 The Thingsboard Authors
+
+    Licensed under the Apache License, Version 2.0 (the "License");
+    you may not use this file except in compliance with the License.
+    You may obtain a copy of the License at
+
+        http://www.apache.org/licenses/LICENSE-2.0
+
+    Unless required by applicable law or agreed to in writing, software
+    distributed under the License is distributed on an "AS IS" BASIS,
+    WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+    See the License for the specific language governing permissions and
+    limitations under the License.
+
+-->
+<div hide-xs hide-sm translate class="tb-cell" flex="30">event.event-time</div>
+<div translate class="tb-cell" flex="20">event.server</div>
+<div translate class="tb-cell" flex="20">event.type</div>
+<div translate class="tb-cell" flex="20">event.entity</div>
+<div translate class="tb-cell" flex="20">event.message-id</div>
+<div translate class="tb-cell" flex="20">event.message-type</div>
+<div translate class="tb-cell" flex="20">event.data-type</div>
+<div translate class="tb-cell" flex="20">event.data</div>
+<div translate class="tb-cell" flex="20">event.metadata</div>
+<div translate class="tb-cell" flex="20">event.error</div>
diff --git a/ui/src/app/event/event-row.directive.js b/ui/src/app/event/event-row.directive.js
index f005542..4643761 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-row-debug-rulenode.tpl.html b/ui/src/app/event/event-row-debug-rulenode.tpl.html
new file mode 100644
index 0000000..ec00b39
--- /dev/null
+++ b/ui/src/app/event/event-row-debug-rulenode.tpl.html
@@ -0,0 +1,63 @@
+<!--
+
+    Copyright © 2016-2018 The Thingsboard Authors
+
+    Licensed under the Apache License, Version 2.0 (the "License");
+    you may not use this file except in compliance with the License.
+    You may obtain a copy of the License at
+
+        http://www.apache.org/licenses/LICENSE-2.0
+
+    Unless required by applicable law or agreed to in writing, software
+    distributed under the License is distributed on an "AS IS" BASIS,
+    WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+    See the License for the specific language governing permissions and
+    limitations under the License.
+
+-->
+<div hide-xs hide-sm class="tb-cell" flex="30">{{event.createdTime | date :  'yyyy-MM-dd HH:mm:ss'}}</div>
+<div class="tb-cell" flex="20">{{event.body.server}}</div>
+<div class="tb-cell" flex="20">{{event.body.type}}</div>
+<div class="tb-cell" flex="20">{{event.body.entityName}}</div>
+<div class="tb-cell" flex="20">{{event.body.msgId}}</div>
+<div class="tb-cell" flex="20">{{event.body.msgType}}</div>
+<div class="tb-cell" flex="20">{{event.body.dataType}}</div>
+<div class="tb-cell" flex="20">
+    <md-button ng-if="event.body.data" class="md-icon-button md-primary"
+               ng-click="showContent($event, event.body.data, 'event.data', event.body.msgType)"
+               aria-label="{{ 'action.view' | translate }}">
+        <md-tooltip md-direction="top">
+            {{ 'action.view' | translate }}
+        </md-tooltip>
+        <md-icon aria-label="{{ 'action.view' | translate }}"
+                 class="material-icons">
+            more_horiz
+        </md-icon>
+    </md-button>
+</div>
+<div class="tb-cell" flex="20">
+    <md-button ng-if="event.body.metadata" class="md-icon-button md-primary"
+               ng-click="showContent($event, event.body.metadata, 'event.metadata', 'JSON')"
+               aria-label="{{ 'action.view' | translate }}">
+        <md-tooltip md-direction="top">
+            {{ 'action.view' | translate }}
+        </md-tooltip>
+        <md-icon aria-label="{{ 'action.view' | translate }}"
+                 class="material-icons">
+            more_horiz
+        </md-icon>
+    </md-button>
+</div>
+<div class="tb-cell" flex="20">
+    <md-button ng-if="event.body.error" class="md-icon-button md-primary"
+               ng-click="showContent($event, event.body.error, 'event.error')"
+               aria-label="{{ 'action.view' | translate }}">
+        <md-tooltip md-direction="top">
+            {{ 'action.view' | translate }}
+        </md-tooltip>
+        <md-icon aria-label="{{ 'action.view' | translate }}"
+                 class="material-icons">
+            more_horiz
+        </md-icon>
+    </md-button>
+</div>
diff --git a/ui/src/app/event/event-table.directive.js b/ui/src/app/event/event-table.directive.js
index 4291014..c61078d 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<disabledEventTypes.length;i++) {
+                    if (eventType.value === disabledEventTypes[i]) {
                         enabled = false;
                         break;
                     }
@@ -47,7 +47,19 @@ export default function EventTableDirective($compile, $templateCache, $rootScope
                 }
             }
         } else {
-            scope.eventTypes = types.eventType;
+            scope.eventTypes = angular.copy(types.eventType);
+        }
+
+        if (attrs.debugEventTypes) {
+            var debugEventTypes = attrs.debugEventTypes.split(',');
+            for (i=0;i<debugEventTypes.length;i++) {
+                for (type in types.debugEventType) {
+                    eventType = types.debugEventType[type];
+                    if (eventType.value === debugEventTypes[i]) {
+                        scope.eventTypes[type] = eventType;
+                    }
+                }
+            }
         }
 
         scope.eventType = attrs.defaultEventType;
diff --git a/ui/src/app/locale/locale.constant.js b/ui/src/app/locale/locale.constant.js
index 5dce787..0a17e39 100644
--- a/ui/src/app/locale/locale.constant.js
+++ b/ui/src/app/locale/locale.constant.js
@@ -341,6 +341,11 @@ export default angular.module('thingsboard.locale', [])
                     "enter-password": "Enter password",
                     "enter-search": "Enter search"
                 },
+                "content-type": {
+                    "json": "Json",
+                    "text": "Text",
+                    "binary": "Binary (Base64)"
+                },
                 "customer": {
                     "customer": "Customer",
                     "customers": "Customers",
@@ -762,6 +767,8 @@ export default angular.module('thingsboard.locale', [])
                     "type-error": "Error",
                     "type-lc-event": "Lifecycle event",
                     "type-stats": "Statistics",
+                    "type-debug-rule-node": "Debug",
+                    "type-debug-rule-chain": "Debug",
                     "no-events-prompt": "No events found",
                     "error": "Error",
                     "alarm": "Alarm",
@@ -769,6 +776,13 @@ export default angular.module('thingsboard.locale', [])
                     "server": "Server",
                     "body": "Body",
                     "method": "Method",
+                    "type": "Type",
+                    "entity": "Entity",
+                    "message-id": "Message Id",
+                    "message-type": "Message Type",
+                    "data-type": "Data Type",
+                    "metadata": "Metadata",
+                    "data": "Data",
                     "event": "Event",
                     "status": "Status",
                     "success": "Success",
@@ -1171,6 +1185,8 @@ export default angular.module('thingsboard.locale', [])
                     "debug-mode": "Debug mode"
                 },
                 "rulenode": {
+                    "details": "Details",
+                    "events": "Events",
                     "add": "Add rule node",
                     "name": "Name",
                     "name-required": "Name is required.",
@@ -1198,7 +1214,9 @@ export default angular.module('thingsboard.locale', [])
                     "type-action": "Action",
                     "type-action-details": "Perform special action",
                     "type-rule-chain": "Rule Chain",
-                    "type-rule-chain-details": "Forwards incoming messages to specified Rule Chain"
+                    "type-rule-chain-details": "Forwards incoming messages to specified Rule Chain",
+                    "directive-is-not-loaded": "Defined configuration directive '{{directiveName}}' is not available.",
+                    "ui-resources-load-error": "Failed to load configuration ui resources."
                 },
                 "rule-plugin": {
                     "management": "Rules and plugins management"
diff --git a/ui/src/app/rulechain/index.js b/ui/src/app/rulechain/index.js
index 7306762..c674467 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 4eba5b2..83bca32 100644
--- a/ui/src/app/rulechain/rulechain.controller.js
+++ b/ui/src/app/rulechain/rulechain.controller.js
@@ -28,7 +28,7 @@ import addRuleNodeLinkTemplate from './add-link.tpl.html';
 /* eslint-enable import/no-unresolved, import/default */
 
 /*@ngInject*/
-export function RuleChainController($stateParams, $scope, $compile, $q, $mdUtil, $timeout, $mdExpansionPanel, $document, $mdDialog,
+export function RuleChainController($stateParams, $scope, $compile, $q, $mdUtil, $timeout, $mdExpansionPanel, $window, $document, $mdDialog,
                                     $filter, $translate, hotkeys, types, ruleChainService, Modelfactory, flowchartConstants,
                                     ruleChain, ruleChainMetaData, ruleNodeComponents) {
 
@@ -77,6 +77,8 @@ export function RuleChainController($stateParams, $scope, $compile, $q, $mdUtil,
     vm.objectsSelected = objectsSelected;
     vm.deleteSelected = deleteSelected;
 
+    vm.triggerResize = triggerResize;
+
     initHotKeys();
 
     function initHotKeys() {
@@ -129,23 +131,24 @@ export function RuleChainController($stateParams, $scope, $compile, $q, $mdUtil,
     }
 
     vm.onEditRuleNodeClosed = function() {
-        vm.editingRuleNode = null;
+        //vm.editingRuleNode = null;
     };
 
     vm.onEditRuleNodeLinkClosed = function() {
-        vm.editingRuleNodeLink = null;
+        //vm.editingRuleNodeLink = null;
     };
 
     vm.saveRuleNode = function(theForm) {
-        theForm.$setPristine();
-        vm.isEditingRuleNode = false;
-        vm.ruleChainModel.nodes[vm.editingRuleNodeIndex] = vm.editingRuleNode;
-        vm.editingRuleNode = angular.copy(vm.editingRuleNode);
+        $scope.$broadcast('form-submit');
+        if (theForm.$valid) {
+            theForm.$setPristine();
+            vm.ruleChainModel.nodes[vm.editingRuleNodeIndex] = vm.editingRuleNode;
+            vm.editingRuleNode = angular.copy(vm.editingRuleNode);
+        }
     };
 
     vm.saveRuleNodeLink = function(theForm) {
         theForm.$setPristine();
-        vm.isEditingRuleNodeLink = false;
         vm.ruleChainModel.edges[vm.editingRuleNodeLinkIndex] = vm.editingRuleNodeLink;
         vm.editingRuleNodeLink = angular.copy(vm.editingRuleNodeLink);
     };
@@ -253,6 +256,9 @@ export function RuleChainController($stateParams, $scope, $compile, $q, $mdUtil,
                 vm.isEditingRuleNodeLink = true;
                 vm.editingRuleNodeLinkIndex = vm.ruleChainModel.edges.indexOf(edge);
                 vm.editingRuleNodeLink = angular.copy(edge);
+                $mdUtil.nextTick(() => {
+                    vm.ruleNodeLinkForm.$setPristine();
+                });
             }
         },
         nodeCallbacks: {
@@ -263,6 +269,9 @@ export function RuleChainController($stateParams, $scope, $compile, $q, $mdUtil,
                     vm.isEditingRuleNode = true;
                     vm.editingRuleNodeIndex = vm.ruleChainModel.nodes.indexOf(node);
                     vm.editingRuleNode = angular.copy(node);
+                    $mdUtil.nextTick(() => {
+                        vm.ruleNodeForm.$setPristine();
+                    });
                 }
             }
         },
@@ -309,7 +318,7 @@ export function RuleChainController($stateParams, $scope, $compile, $q, $mdUtil,
             var componentType = ruleNodeComponent.type;
             var model = vm.ruleNodeTypesModel[componentType].model;
             var node = {
-                id: model.nodes.length,
+                id: 'node-lib-' + componentType + '-' + model.nodes.length,
                 component: ruleNodeComponent,
                 name: '',
                 nodeClass: vm.types.ruleNodeType[componentType].nodeClass,
@@ -358,7 +367,7 @@ export function RuleChainController($stateParams, $scope, $compile, $q, $mdUtil,
 
         vm.ruleChainModel.nodes.push(
             {
-                id: vm.nextNodeID++,
+                id: 'rule-chain-node-' + vm.nextNodeID++,
                 component: types.inputNodeComponent,
                 name: "",
                 nodeClass: types.ruleNodeType.INPUT.nodeClass,
@@ -389,7 +398,7 @@ export function RuleChainController($stateParams, $scope, $compile, $q, $mdUtil,
             var component = ruleChainService.getRuleNodeComponentByClazz(ruleNode.type);
             if (component) {
                 var node = {
-                    id: vm.nextNodeID++,
+                    id: 'rule-chain-node-' + vm.nextNodeID++,
                     ruleNodeId: ruleNode.id,
                     additionalInfo: ruleNode.additionalInfo,
                     configuration: ruleNode.configuration,
@@ -466,7 +475,7 @@ export function RuleChainController($stateParams, $scope, $compile, $q, $mdUtil,
                     var ruleChainNode = ruleChainNodesMap[ruleChainConnection.additionalInfo.ruleChainNodeId];
                     if (!ruleChainNode) {
                         ruleChainNode = {
-                            id: vm.nextNodeID++,
+                            id: 'rule-chain-node-' + vm.nextNodeID++,
                             additionalInfo: ruleChainConnection.additionalInfo,
                             targetRuleChainId: ruleChainConnection.targetRuleChainId.id,
                             x: ruleChainConnection.additionalInfo.layoutX,
@@ -611,7 +620,7 @@ export function RuleChainController($stateParams, $scope, $compile, $q, $mdUtil,
             fullscreen: true,
             targetEvent: $event
         }).then(function (ruleNode) {
-            ruleNode.id = vm.nextNodeID++;
+            ruleNode.id = 'rule-chain-node-' + vm.nextNodeID++;
             ruleNode.connectors = [];
             if (ruleNode.component.configurationDescriptor.nodeDefinition.inEnabled) {
                 ruleNode.connectors.push(
@@ -654,6 +663,11 @@ export function RuleChainController($stateParams, $scope, $compile, $q, $mdUtil,
     function deleteSelected() {
         vm.modelservice.deleteSelected();
     }
+
+    function triggerResize() {
+        var w = angular.element($window);
+        w.triggerHandler('resize');
+    }
 }
 
 /*@ngInject*/
diff --git a/ui/src/app/rulechain/rulechain.tpl.html b/ui/src/app/rulechain/rulechain.tpl.html
index d23f920..b25447e 100644
--- a/ui/src/app/rulechain/rulechain.tpl.html
+++ b/ui/src/app/rulechain/rulechain.tpl.html
@@ -65,9 +65,11 @@
         </div>
         <tb-details-sidenav class="tb-rulenode-details-sidenav"
                             header-title="{{vm.editingRuleNode.name}}"
-                            header-subtitle="{{'rulenode.rulenode-details' | translate}}"
-                            is-read-only="false"
+                            header-subtitle="{{(vm.types.ruleNodeType[vm.editingRuleNode.component.type].name | translate)
+                            + ' - ' + vm.editingRuleNode.component.name}}"
+                            is-read-only="vm.selectedRuleNodeTabIndex > 0"
                             is-open="vm.isEditingRuleNode"
+                            tb-enable-backdrop
                             is-always-edit="true"
                             on-close-details="vm.onEditRuleNodeClosed()"
                             on-toggle-details-edit-mode="vm.onRevertRuleNodeEdit(vm.ruleNodeForm)"
@@ -76,22 +78,37 @@
             <details-buttons tb-help="vm.helpLinkIdForRuleNodeType()" help-container-id="help-container">
                 <div id="help-container"></div>
             </details-buttons>
-            <form name="vm.ruleNodeForm" ng-if="vm.isEditingRuleNode">
-                <tb-rule-node
-                        rule-node="vm.editingRuleNode"
-                        rule-chain-id="vm.ruleChain.id.id"
-                        is-edit="true"
-                        is-read-only="false"
-                        on-delete-rule-node="vm.deleteRuleNode(event, vm.editingRuleNode)"
-                        the-form="vm.ruleNodeForm">
-                </tb-rule-node>
-            </form>
+            <md-tabs md-selected="vm.selectedRuleNodeTabIndex"
+                     id="ruleNodeTabs" md-border-bottom flex class="tb-absolute-fill" ng-if="vm.isEditingRuleNode">
+                <md-tab label="{{ 'rulenode.details' | translate }}">
+                    <form name="vm.ruleNodeForm">
+                        <tb-rule-node
+                                rule-node="vm.editingRuleNode"
+                                rule-chain-id="vm.ruleChain.id.id"
+                                is-edit="true"
+                                is-read-only="false"
+                                on-delete-rule-node="vm.deleteRuleNode(event, vm.editingRuleNode)"
+                                the-form="vm.ruleNodeForm">
+                        </tb-rule-node>
+                    </form>
+                </md-tab>
+                <md-tab ng-if="vm.isEditingRuleNode && vm.editingRuleNode.ruleNodeId"
+                        md-on-select="vm.triggerResize()" label="{{ 'rulenode.events' | translate }}">
+                    <tb-event-table flex entity-type="vm.types.entityType.rulenode"
+                                    entity-id="vm.editingRuleNode.ruleNodeId.id"
+                                    tenant-id="vm.ruleChain.tenantId.id"
+                                    debug-event-types="{{vm.types.debugEventType.debugRuleNode.value}}"
+                                    default-event-type="{{vm.types.debugEventType.debugRuleNode.value}}">
+                    </tb-event-table>
+                </md-tab>
+            </md-tabs>
         </tb-details-sidenav>
         <tb-details-sidenav class="tb-rulenode-link-details-sidenav"
                             header-title="{{vm.editingRuleNodeLink.label}}"
                             header-subtitle="{{'rulenode.link-details' | translate}}"
                             is-read-only="false"
                             is-open="vm.isEditingRuleNodeLink"
+                            tb-enable-backdrop
                             is-always-edit="true"
                             on-close-details="vm.onEditRuleNodeLinkClosed()"
                             on-toggle-details-edit-mode="vm.onRevertRuleNodeLinkEdit(vm.ruleNodeLinkForm)"
diff --git a/ui/src/app/rulechain/rulechains.tpl.html b/ui/src/app/rulechain/rulechains.tpl.html
index a4fbd79..cf9d256 100644
--- a/ui/src/app/rulechain/rulechains.tpl.html
+++ b/ui/src/app/rulechain/rulechains.tpl.html
@@ -55,7 +55,8 @@
             <tb-event-table flex entity-type="vm.types.entityType.rulechain"
                             entity-id="vm.grid.operatingItem().id.id"
                             tenant-id="vm.grid.operatingItem().tenantId.id"
-                            default-event-type="{{vm.types.eventType.lcEvent.value}}">
+                            debug-event-types="{{vm.types.debugEventType.debugRuleChain.value}}"
+                            default-event-type="{{vm.types.debugEventType.debugRuleChain.value}}">
             </tb-event-table>
         </md-tab>
         <md-tab ng-if="!vm.grid.detailsConfig.isDetailsEditMode && vm.isRuleChainEditable(vm.grid.operatingItem())" md-on-select="vm.grid.triggerResize()" label="{{ 'relation.relations' | translate }}">
diff --git a/ui/src/app/rulechain/rulenode.scss b/ui/src/app/rulechain/rulenode.scss
index febc637..0466673 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
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 0000000..9bb8c48
--- /dev/null
+++ b/ui/src/app/rulechain/rulenode-config.directive.js
@@ -0,0 +1,78 @@
+/*
+ * Copyright © 2016-2018 The Thingsboard Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+/* eslint-disable import/no-unresolved, import/default */
+
+import ruleNodeConfigTemplate from './rulenode-config.tpl.html';
+
+/* eslint-enable import/no-unresolved, import/default */
+
+/*@ngInject*/
+export default function RuleNodeConfigDirective($compile, $templateCache, $injector, $translate) {
+
+    var linker = function (scope, element, attrs, ngModelCtrl) {
+        var template = $templateCache.get(ruleNodeConfigTemplate);
+        element.html(template);
+
+        scope.$watch('configuration', function (newVal, prevVal) {
+            if (!angular.equals(newVal, prevVal)) {
+                ngModelCtrl.$setViewValue(scope.configuration);
+            }
+        });
+
+        ngModelCtrl.$render = function () {
+            scope.configuration = ngModelCtrl.$viewValue;
+        };
+
+        scope.useDefinedDirective = function() {
+            return scope.nodeDefinition &&
+                scope.nodeDefinition.configDirective && !scope.definedDirectiveError;
+        };
+
+        scope.$watch('nodeDefinition', () => {
+            if (scope.nodeDefinition) {
+                validateDefinedDirective();
+            }
+        });
+
+        function validateDefinedDirective() {
+            if (scope.nodeDefinition.uiResourceLoadError && scope.nodeDefinition.uiResourceLoadError.length) {
+                scope.definedDirectiveError = scope.nodeDefinition.uiResourceLoadError;
+            } else {
+                var definedDirective = scope.nodeDefinition.configDirective;
+                if (definedDirective && definedDirective.length) {
+                    if (!$injector.has(definedDirective + 'Directive')) {
+                        scope.definedDirectiveError = $translate.instant('rulenode.directive-is-not-loaded', {directiveName: definedDirective});
+                    }
+                }
+            }
+        }
+
+        $compile(element.contents())(scope);
+    };
+
+    return {
+        restrict: "E",
+        require: "^ngModel",
+        scope: {
+            nodeDefinition:'=',
+            required:'=ngRequired',
+            readonly:'=ngReadonly'
+        },
+        link: linker
+    };
+
+}
diff --git a/ui/src/app/rulechain/rulenode-config.tpl.html b/ui/src/app/rulechain/rulenode-config.tpl.html
new file mode 100644
index 0000000..32d5347
--- /dev/null
+++ b/ui/src/app/rulechain/rulenode-config.tpl.html
@@ -0,0 +1,32 @@
+<!--
+
+    Copyright © 2016-2018 The Thingsboard Authors
+
+    Licensed under the Apache License, Version 2.0 (the "License");
+    you may not use this file except in compliance with the License.
+    You may obtain a copy of the License at
+
+        http://www.apache.org/licenses/LICENSE-2.0
+
+    Unless required by applicable law or agreed to in writing, software
+    distributed under the License is distributed on an "AS IS" BASIS,
+    WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+    See the License for the specific language governing permissions and
+    limitations under the License.
+
+-->
+
+<tb-rule-node-defined-config ng-if="useDefinedDirective()"
+                             ng-model="configuration"
+                             rule-node-directive="{{nodeDefinition.configDirective}}"
+                             ng-required="required"
+                             ng-readonly="readonly">
+</tb-rule-node-defined-config>
+<div class="tb-rulenode-directive-error" ng-if="definedDirectiveError">{{definedDirectiveError}}</div>
+<tb-json-object-edit ng-if="!useDefinedDirective()"
+                     class="tb-rule-node-configuration-json"
+                     ng-model="configuration"
+                     label="{{ 'rulenode.configuration' | translate }}"
+                     ng-required="required"
+                     fill-height="true">
+</tb-json-object-edit>
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 0000000..5100fbb
--- /dev/null
+++ b/ui/src/app/rulechain/rulenode-defined-config.directive.js
@@ -0,0 +1,67 @@
+/*
+ * Copyright © 2016-2018 The Thingsboard Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+const SNAKE_CASE_REGEXP = /[A-Z]/g;
+
+/*@ngInject*/
+export default function RuleNodeDefinedConfigDirective($compile) {
+
+    var linker = function (scope, element, attrs, ngModelCtrl) {
+
+        attrs.$observe('ruleNodeDirective', function() {
+            loadTemplate();
+        });
+
+        scope.$watch('configuration', function (newVal, prevVal) {
+            if (!angular.equals(newVal, prevVal)) {
+                ngModelCtrl.$setViewValue(scope.configuration);
+            }
+        });
+
+        ngModelCtrl.$render = function () {
+            scope.configuration = ngModelCtrl.$viewValue;
+        };
+
+        function loadTemplate() {
+            if (scope.ruleNodeConfigScope) {
+                scope.ruleNodeConfigScope.$destroy();
+            }
+            var directive = snake_case(attrs.ruleNodeDirective, '-');
+            var template = `<${directive} ng-model="configuration" ng-required="required" ng-readonly="readonly"></${directive}>`;
+            element.html(template);
+            scope.ruleNodeConfigScope = scope.$new();
+            $compile(element.contents())(scope.ruleNodeConfigScope);
+        }
+
+        function snake_case(name, separator) {
+            separator = separator || '_';
+            return name.replace(SNAKE_CASE_REGEXP, function(letter, pos) {
+                return (pos ? separator : '') + letter.toLowerCase();
+            });
+        }
+    };
+
+    return {
+        restrict: "E",
+        require: "^ngModel",
+        scope: {
+            required:'=ngRequired',
+            readonly:'=ngReadonly'
+        },
+        link: linker
+    };
+
+}
diff --git a/ui/src/app/rulechain/rulenode-fieldset.tpl.html b/ui/src/app/rulechain/rulenode-fieldset.tpl.html
index 30cf075..7b0fae5 100644
--- a/ui/src/app/rulechain/rulenode-fieldset.tpl.html
+++ b/ui/src/app/rulechain/rulenode-fieldset.tpl.html
@@ -21,28 +21,26 @@
 
 <md-content class="md-padding tb-rulenode" layout="column">
     <fieldset ng-disabled="$root.loading || !isEdit || isReadOnly">
-        <md-input-container class="md-block">
-            <label translate>rulenode.type</label>
-            <input readonly name="type" ng-model="ruleNode.component.name">
-        </md-input-container>
         <section ng-if="ruleNode.component.type != types.ruleNodeType.RULE_CHAIN.value">
-            <md-input-container class="md-block">
-                <label translate>rulenode.name</label>
-                <input required name="name" ng-model="ruleNode.name">
-                <div ng-messages="theForm.name.$error">
-                    <div translate ng-message="required">rulenode.name-required</div>
-                </div>
-            </md-input-container>
-            <md-input-container class="md-block">
-                <md-checkbox ng-disabled="$root.loading || !isEdit" aria-label="{{ 'rulenode.debug-mode' | translate }}"
-                             ng-model="ruleNode.debugMode">{{ 'rulenode.debug-mode' | translate }}
-                </md-checkbox>
-            </md-input-container>
-            <tb-json-object-edit class="tb-rule-node-configuration-json" ng-model="ruleNode.configuration"
-                                 label="{{ 'rulenode.configuration' | translate }}"
+            <section layout="column" layout-gt-sm="row">
+                <md-input-container flex class="md-block">
+                    <label translate>rulenode.name</label>
+                    <input required name="name" ng-model="ruleNode.name">
+                    <div ng-messages="theForm.name.$error">
+                        <div translate ng-message="required">rulenode.name-required</div>
+                    </div>
+                </md-input-container>
+                <md-input-container class="md-block">
+                    <md-checkbox ng-disabled="$root.loading || !isEdit" aria-label="{{ 'rulenode.debug-mode' | translate }}"
+                                 ng-model="ruleNode.debugMode">{{ 'rulenode.debug-mode' | translate }}
+                    </md-checkbox>
+                </md-input-container>
+            </section>
+            <tb-rule-node-config ng-model="ruleNode.configuration"
                                  ng-required="true"
-                                 fill-height="true">
-            </tb-json-object-edit>
+                                 node-definition="ruleNode.component.configurationDescriptor.nodeDefinition"
+                                 ng-readonly="$root.loading || !isEdit || isReadOnly">
+            </tb-rule-node-config>
             <md-input-container class="md-block">
                 <label translate>rulenode.description</label>
                 <textarea ng-model="ruleNode.additionalInfo.description" rows="2"></textarea>