thingsboard-memoizeit

Changes

Details

diff --git a/application/src/main/java/org/thingsboard/server/actors/ruleChain/RuleNodeActorMessageProcessor.java b/application/src/main/java/org/thingsboard/server/actors/ruleChain/RuleNodeActorMessageProcessor.java
index a08dc34..93cb5fb 100644
--- a/application/src/main/java/org/thingsboard/server/actors/ruleChain/RuleNodeActorMessageProcessor.java
+++ b/application/src/main/java/org/thingsboard/server/actors/ruleChain/RuleNodeActorMessageProcessor.java
@@ -69,14 +69,18 @@ public class RuleNodeActorMessageProcessor extends ComponentMsgProcessor<RuleNod
                 && ruleNode.getConfiguration().equals(newRuleNode.getConfiguration()));
         this.ruleNode = newRuleNode;
         if (restartRequired) {
-            tbNode.destroy();
+            if (tbNode != null) {
+                tbNode.destroy();
+            }
             start(context);
         }
     }
 
     @Override
     public void stop(ActorContext context) throws Exception {
-        tbNode.destroy();
+        if (tbNode != null) {
+            tbNode.destroy();
+        }
         context.stop(self);
     }
 
diff --git a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/data/RelationsQuery.java b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/data/RelationsQuery.java
new file mode 100644
index 0000000..1d944ec
--- /dev/null
+++ b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/data/RelationsQuery.java
@@ -0,0 +1,31 @@
+/**
+ * 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.rule.engine.data;
+
+import lombok.Data;
+import org.thingsboard.server.common.data.relation.EntitySearchDirection;
+import org.thingsboard.server.common.data.relation.EntityTypeFilter;
+
+import java.util.List;
+
+@Data
+public class RelationsQuery {
+
+    private EntitySearchDirection direction;
+    private int maxLevel = 1;
+    private List<EntityTypeFilter> filters;
+
+}
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 7bf70f4..fc65e43 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
@@ -43,7 +43,9 @@ import static org.thingsboard.server.common.data.DataConstants.*;
           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>metadata.cs.temperature</code> or <code>metadata.shared.limit</code> " +
-                "If Latest Telemetry enrichment configured, latest telemetry added into metadata without prefix.")
+                "If Latest Telemetry enrichment configured, latest telemetry added into metadata without prefix.",
+        uiResources = {"static/rulenode/rulenode-core-config.js"},
+        configDirective = "tbEnrichmentNodeOriginatorAttributesConfig")
 public class TbGetAttributesNode implements TbNode {
 
     private TbGetAttributesNodeConfiguration config;
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 c59a65e..b092bad 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,9 @@ 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>metadata.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",
+        uiResources = {"static/rulenode/rulenode-core-config.js", "static/rulenode/rulenode-core-config.css"},
+        configDirective = "tbEnrichmentNodeCustomerAttributesConfig")
 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 603746e..8f65c31 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,10 @@ 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>metadata.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",
+        uiResources = {"static/rulenode/rulenode-core-config.js", "static/rulenode/rulenode-core-config.css"},
+        configDirective = "tbEnrichmentNodeRelatedAttributesConfig")
+
 public class TbGetRelatedAttributeNode extends TbEntityGetAttrNode<EntityId> {
 
     private TbGetRelatedAttrNodeConfiguration config;
@@ -45,6 +48,6 @@ public class TbGetRelatedAttributeNode extends TbEntityGetAttrNode<EntityId> {
 
     @Override
     protected ListenableFuture<EntityId> findEntityAsync(TbContext ctx, EntityId originator) {
-        return EntitiesRelatedEntityIdAsyncLoader.findEntityAsync(ctx, originator, config.getDirection(), config.getRelationType());
+        return EntitiesRelatedEntityIdAsyncLoader.findEntityAsync(ctx, originator, config.getRelationsQuery());
     }
 }
diff --git a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/metadata/TbGetRelatedAttrNodeConfiguration.java b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/metadata/TbGetRelatedAttrNodeConfiguration.java
index 8211992..dccd878 100644
--- a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/metadata/TbGetRelatedAttrNodeConfiguration.java
+++ b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/metadata/TbGetRelatedAttrNodeConfiguration.java
@@ -16,18 +16,19 @@
 package org.thingsboard.rule.engine.metadata;
 
 import lombok.Data;
-import org.thingsboard.rule.engine.api.NodeConfiguration;
+import org.thingsboard.rule.engine.data.RelationsQuery;
 import org.thingsboard.server.common.data.relation.EntityRelation;
 import org.thingsboard.server.common.data.relation.EntitySearchDirection;
+import org.thingsboard.server.common.data.relation.EntityTypeFilter;
 
+import java.util.Collections;
 import java.util.HashMap;
 import java.util.Map;
 
 @Data
 public class TbGetRelatedAttrNodeConfiguration extends TbGetEntityAttrNodeConfiguration {
 
-    private String relationType;
-    private EntitySearchDirection direction;
+    private RelationsQuery relationsQuery;
 
     @Override
     public TbGetRelatedAttrNodeConfiguration defaultConfiguration() {
@@ -36,8 +37,14 @@ public class TbGetRelatedAttrNodeConfiguration extends TbGetEntityAttrNodeConfig
         attrMapping.putIfAbsent("temperature", "tempo");
         configuration.setAttrMapping(attrMapping);
         configuration.setTelemetry(true);
-        configuration.setRelationType(EntityRelation.CONTAINS_TYPE);
-        configuration.setDirection(EntitySearchDirection.FROM);
+
+        RelationsQuery relationsQuery = new RelationsQuery();
+        relationsQuery.setDirection(EntitySearchDirection.FROM);
+        relationsQuery.setMaxLevel(1);
+        EntityTypeFilter entityTypeFilter = new EntityTypeFilter(EntityRelation.CONTAINS_TYPE, Collections.emptyList());
+        relationsQuery.setFilters(Collections.singletonList(entityTypeFilter));
+        configuration.setRelationsQuery(relationsQuery);
+
         return configuration;
     }
 }
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 3165385..f0d28d3 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,9 @@ 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>metadata.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",
+        uiResources = {"static/rulenode/rulenode-core-config.js", "static/rulenode/rulenode-core-config.css"},
+        configDirective = "tbEnrichmentNodeTenantAttributesConfig")
 public class TbGetTenantAttributeNode extends TbEntityGetAttrNode<TenantId> {
 
     @Override
diff --git a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/transform/TbChangeOriginatorNode.java b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/transform/TbChangeOriginatorNode.java
index cf761f2..05ad49c 100644
--- a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/transform/TbChangeOriginatorNode.java
+++ b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/transform/TbChangeOriginatorNode.java
@@ -39,7 +39,9 @@ import java.util.HashSet;
         configClazz = TbChangeOriginatorNodeConfiguration.class,
         nodeDescription = "Change Message Originator To Tenant/Customer/Related Entity",
         nodeDetails = "Related Entity found using configured relation direction and Relation Type. " +
-                "If multiple Related Entities are found, only first Entity is used as new Originator, other entities are discarded. ")
+                "If multiple Related Entities are found, only first Entity is used as new Originator, other entities are discarded. ",
+        uiResources = {"static/rulenode/rulenode-core-config.js", "static/rulenode/rulenode-core-config.css"},
+        configDirective = "tbTransformationNodeChangeOriginatorConfig")
 public class TbChangeOriginatorNode extends TbAbstractTransformNode {
 
     protected static final String CUSTOMER_SOURCE = "CUSTOMER";
@@ -68,7 +70,7 @@ public class TbChangeOriginatorNode extends TbAbstractTransformNode {
             case TENANT_SOURCE:
                 return EntitiesTenantIdAsyncLoader.findEntityIdAsync(ctx, original);
             case RELATED_SOURCE:
-                return EntitiesRelatedEntityIdAsyncLoader.findEntityAsync(ctx, original, config.getDirection(), config.getRelationType());
+                return EntitiesRelatedEntityIdAsyncLoader.findEntityAsync(ctx, original, config.getRelationsQuery());
             default:
                 return Futures.immediateFailedFuture(new IllegalStateException("Unexpected originator source " + config.getOriginatorSource()));
         }
@@ -82,9 +84,9 @@ public class TbChangeOriginatorNode extends TbAbstractTransformNode {
         }
 
         if (conf.getOriginatorSource().equals(RELATED_SOURCE)) {
-            if (conf.getDirection() == null || StringUtils.isBlank(conf.getRelationType())) {
-                log.error("Related source for TbChangeOriginatorNode should have direction and relationType. Actual [{}] [{}]",
-                        conf.getDirection(), conf.getRelationType());
+            if (conf.getRelationsQuery() == null) {
+                log.error("Related source for TbChangeOriginatorNode should have relations query. Actual [{}]",
+                        conf.getRelationsQuery());
                 throw new IllegalArgumentException("Wrong config for RElated Source in TbChangeOriginatorNode" + conf.getOriginatorSource());
             }
         }
diff --git a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/transform/TbChangeOriginatorNodeConfiguration.java b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/transform/TbChangeOriginatorNodeConfiguration.java
index 3370408..7cd77bf 100644
--- a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/transform/TbChangeOriginatorNodeConfiguration.java
+++ b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/transform/TbChangeOriginatorNodeConfiguration.java
@@ -17,22 +17,32 @@ package org.thingsboard.rule.engine.transform;
 
 import lombok.Data;
 import org.thingsboard.rule.engine.api.NodeConfiguration;
+import org.thingsboard.rule.engine.data.RelationsQuery;
 import org.thingsboard.server.common.data.relation.EntityRelation;
 import org.thingsboard.server.common.data.relation.EntitySearchDirection;
+import org.thingsboard.server.common.data.relation.EntityTypeFilter;
+
+import java.util.Collections;
 
 @Data
 public class TbChangeOriginatorNodeConfiguration extends TbTransformNodeConfiguration implements NodeConfiguration {
 
     private String originatorSource;
-    private EntitySearchDirection direction;
-    private String relationType;
+
+    private RelationsQuery relationsQuery;
 
     @Override
     public TbChangeOriginatorNodeConfiguration defaultConfiguration() {
         TbChangeOriginatorNodeConfiguration configuration = new TbChangeOriginatorNodeConfiguration();
         configuration.setOriginatorSource(TbChangeOriginatorNode.CUSTOMER_SOURCE);
-        configuration.setDirection(EntitySearchDirection.FROM);
-        configuration.setRelationType(EntityRelation.CONTAINS_TYPE);
+
+        RelationsQuery relationsQuery = new RelationsQuery();
+        relationsQuery.setDirection(EntitySearchDirection.FROM);
+        relationsQuery.setMaxLevel(1);
+        EntityTypeFilter entityTypeFilter = new EntityTypeFilter(EntityRelation.CONTAINS_TYPE, Collections.emptyList());
+        relationsQuery.setFilters(Collections.singletonList(entityTypeFilter));
+        configuration.setRelationsQuery(relationsQuery);
+
         configuration.setStartNewChain(false);
         return configuration;
     }
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 27e46fe..626d057 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
@@ -31,7 +31,9 @@ import javax.script.Bindings;
         nodeDescription = "Change Message payload and Metadata using JavaScript",
         nodeDetails = "JavaScript function recieve 2 input parameters that can be changed inside.<br/> " +
                 "<code>metadata</code> - is a Message metadata.<br/>" +
-                "<code>msg</code> - is a Message payload.<br/>Any properties can be changed/removed/added in those objects.")
+                "<code>msg</code> - is a Message payload.<br/>Any properties can be changed/removed/added in those objects.",
+        uiResources = {"static/rulenode/rulenode-core-config.js", "static/rulenode/rulenode-core-config.css"},
+        configDirective = "tbTransformationNodeScriptConfig")
 public class TbTransformMsgNode extends TbAbstractTransformNode {
 
     private TbTransformMsgNodeConfiguration config;
diff --git a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/util/EntitiesRelatedEntityIdAsyncLoader.java b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/util/EntitiesRelatedEntityIdAsyncLoader.java
index ac69c5d..08ce38e 100644
--- a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/util/EntitiesRelatedEntityIdAsyncLoader.java
+++ b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/util/EntitiesRelatedEntityIdAsyncLoader.java
@@ -20,32 +20,41 @@ import com.google.common.util.concurrent.Futures;
 import com.google.common.util.concurrent.ListenableFuture;
 import org.apache.commons.collections.CollectionUtils;
 import org.thingsboard.rule.engine.api.TbContext;
+import org.thingsboard.rule.engine.data.RelationsQuery;
 import org.thingsboard.server.common.data.id.EntityId;
 import org.thingsboard.server.common.data.relation.EntityRelation;
+import org.thingsboard.server.common.data.relation.EntityRelationsQuery;
 import org.thingsboard.server.common.data.relation.EntitySearchDirection;
+import org.thingsboard.server.common.data.relation.RelationsSearchParameters;
 import org.thingsboard.server.dao.relation.RelationService;
 
 import java.util.List;
 
-import static org.thingsboard.server.common.data.relation.RelationTypeGroup.COMMON;
-
 public class EntitiesRelatedEntityIdAsyncLoader {
 
     public static ListenableFuture<EntityId> findEntityAsync(TbContext ctx, EntityId originator,
-                                                             EntitySearchDirection direction, String relationType) {
+                                                             RelationsQuery relationsQuery) {
         RelationService relationService = ctx.getRelationService();
-        if (direction == EntitySearchDirection.FROM) {
-            ListenableFuture<List<EntityRelation>> asyncRelation = relationService.findByFromAndTypeAsync(originator, relationType, COMMON);
+        EntityRelationsQuery query = buildQuery(originator, relationsQuery);
+        ListenableFuture<List<EntityRelation>> asyncRelation = relationService.findByQuery(query);
+        if (relationsQuery.getDirection() == EntitySearchDirection.FROM) {
             return Futures.transform(asyncRelation, (AsyncFunction<? super List<EntityRelation>, EntityId>)
                     r -> CollectionUtils.isNotEmpty(r) ? Futures.immediateFuture(r.get(0).getTo())
                             : Futures.immediateFailedFuture(new IllegalStateException("Relation not found")));
-        } else if (direction == EntitySearchDirection.TO) {
-            ListenableFuture<List<EntityRelation>> asyncRelation = relationService.findByToAndTypeAsync(originator, relationType, COMMON);
+        } else if (relationsQuery.getDirection() == EntitySearchDirection.TO) {
             return Futures.transform(asyncRelation, (AsyncFunction<? super List<EntityRelation>, EntityId>)
                     r -> CollectionUtils.isNotEmpty(r) ? Futures.immediateFuture(r.get(0).getFrom())
                             : Futures.immediateFailedFuture(new IllegalStateException("Relation not found")));
         }
-
         return Futures.immediateFailedFuture(new IllegalStateException("Unknown direction"));
     }
+
+    private static EntityRelationsQuery buildQuery(EntityId originator, RelationsQuery relationsQuery) {
+        EntityRelationsQuery query = new EntityRelationsQuery();
+        RelationsSearchParameters parameters = new RelationsSearchParameters(originator,
+                relationsQuery.getDirection(), relationsQuery.getMaxLevel());
+        query.setParameters(parameters);
+        query.setFilters(relationsQuery.getFilters());
+        return query;
+    }
 }
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
index a6103c1..c91d7da 100644
--- 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
@@ -1,2 +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}
+.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}.tb-kv-map-config .header{padding-left:5px;padding-right:5px;padding-bottom:5px}.tb-kv-map-config .header .cell{padding-left:5px;padding-right:5px;color:rgba(0,0,0,.54);font-size:12px;font-weight:700;white-space:nowrap}.tb-kv-map-config .body{padding-left:5px;padding-right:5px;padding-bottom:20px;max-height:300px;overflow:auto}.tb-kv-map-config .body .row{padding-top:5px;max-height:40px}.tb-kv-map-config .body .cell{padding-left:5px;padding-right:5px}.tb-kv-map-config .body md-input-container.cell{margin:0;max-height:40px}.tb-kv-map-config .body .md-button{margin:0}
 /*# sourceMappingURL=rulenode-core-config.css.map*/
\ No newline at end of file
diff --git a/rule-engine/rule-engine-components/src/main/resources/public/static/rulenode/rulenode-core-config.js b/rule-engine/rule-engine-components/src/main/resources/public/static/rulenode/rulenode-core-config.js
index 3fe859d..69ff7ae 100644
--- a/rule-engine/rule-engine-components/src/main/resources/public/static/rulenode/rulenode-core-config.js
+++ b/rule-engine/rule-engine-components/src/main/resources/public/static/rulenode/rulenode-core-config.js
@@ -1,2 +1,2 @@
-!function(e){function t(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}]);
+!function(e){function t(a){if(n[a])return n[a].exports;var r=n[a]={exports:{},id:a,loaded:!1};return e[a].call(r.exports,r,r.exports,t),r.loaded=!0,r.exports}var n={};return t.m=e,t.c=n,t.p="/static/",t(0)}(function(e){for(var t in e)if(Object.prototype.hasOwnProperty.call(e,t))switch(typeof e[t]){case"function":break;case"object":e[t]=function(t){var n=t.slice(1),a=e[t[0]];return function(e,t,r){a.apply(this,[e,t,r].concat(n))}}(e[t]);break;default:e[t]=e[e[t]]}return e}([function(e,t,n){e.exports=n(28)},function(e,t){},1,function(e,t){e.exports=" <section layout=column> <label translate class=\"tb-title tb-required\">tb.rulenode.attr-mapping</label> <md-checkbox aria-label=\"{{ 'tb.rulenode.latest-telemetry' | translate }}\" ng-model=configuration.telemetry>{{ 'tb.rulenode.latest-telemetry' | translate }} </md-checkbox> <tb-kv-map-config ng-model=configuration.attrMapping ng-required=true required-text=\"'tb.rulenode.attr-mapping-required'\" key-text=\"configuration.telemetry ? 'tb.rulenode.source-telemetry' : 'tb.rulenode.source-attribute'\" key-required-text=\"configuration.telemetry ? 'tb.rulenode.source-telemetry-required' : 'tb.rulenode.source-attribute-required'\" val-text=\"'tb.rulenode.target-attribute'\" val-required-text=\"'tb.rulenode.target-attribute-required'\"> </tb-kv-map-config> </section> "},function(e,t){e.exports=' <section layout=column> <label translate class="tb-title no-padding">tb.rulenode.client-attributes</label> <md-chips style=padding-bottom:15px ng-required=false readonly=readonly ng-model=configuration.clientAttributeNames placeholder="{{\'tb.rulenode.client-attributes\' | translate}}" md-separator-keys=separatorKeys> </md-chips> <label translate class="tb-title no-padding">tb.rulenode.shared-attributes</label> <md-chips style=padding-bottom:15px ng-required=false readonly=readonly ng-model=configuration.sharedAttributeNames placeholder="{{\'tb.rulenode.shared-attributes\' | translate}}" md-separator-keys=separatorKeys> </md-chips> <label translate class="tb-title no-padding">tb.rulenode.server-attributes</label> <md-chips style=padding-bottom:15px ng-required=false readonly=readonly ng-model=configuration.serverAttributeNames placeholder="{{\'tb.rulenode.server-attributes\' | translate}}" md-separator-keys=separatorKeys> </md-chips> <label translate class="tb-title no-padding">tb.rulenode.latest-timeseries</label> <md-chips ng-required=false readonly=readonly ng-model=configuration.latestTsKeyNames placeholder="{{\'tb.rulenode.latest-timeseries\' | translate}}" md-separator-keys=separatorKeys> </md-chips> </section> '},function(e,t){e.exports=" <section layout=column> <label translate class=\"tb-title tb-required\">tb.rulenode.relations-query</label> <tb-relations-query-config style=padding-bottom:15px ng-model=configuration.relationsQuery> </tb-relations-query-config> <label translate class=\"tb-title tb-required\">tb.rulenode.attr-mapping</label> <md-checkbox aria-label=\"{{ 'tb.rulenode.latest-telemetry' | translate }}\" ng-model=configuration.telemetry>{{ 'tb.rulenode.latest-telemetry' | translate }} </md-checkbox> <tb-kv-map-config ng-model=configuration.attrMapping ng-required=true required-text=\"'tb.rulenode.attr-mapping-required'\" key-text=\"configuration.telemetry ? 'tb.rulenode.source-telemetry' : 'tb.rulenode.source-attribute'\" key-required-text=\"configuration.telemetry ? 'tb.rulenode.source-telemetry-required' : 'tb.rulenode.source-attribute-required'\" val-text=\"'tb.rulenode.target-attribute'\" val-required-text=\"'tb.rulenode.target-attribute-required'\"> </tb-kv-map-config> </section> "},3,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){e.exports=' <section class=tb-kv-map-config layout=column> <div class=header flex layout=row> <span class=cell flex translate>{{ keyText }}</span> <span class=cell flex translate>{{ valText }}</span> <span ng-show=!disabled style=width:52px>&nbsp</span> </div> <div class=body> <div class=row ng-form name=kvForm flex layout=row layout-align="start center" ng-repeat="keyVal in kvList track by $index"> <md-input-container class="cell md-block" flex md-no-float> <input placeholder="{{ keyText | translate }}" ng-required=true name=key ng-model=keyVal.key> <div ng-messages=kvForm.key.$error> <div translate ng-message=required>{{keyRequiredText}}</div> </div> </md-input-container> <md-input-container class="cell md-block" flex md-no-float> <input placeholder="{{ valText | translate }}" ng-required=true name=value ng-model=keyVal.value> <div ng-messages=kvForm.value.$error> <div translate ng-message=required>{{valRequiredText}}</div> </div> </md-input-container> <md-button ng-show=!disabled ng-disabled=loading class="md-icon-button md-primary" ng-click=removeKeyVal($index) aria-label="{{ \'action.remove\' | translate }}"> <md-tooltip md-direction=top> {{ \'tb.key-val.remove-entry\' | translate }} </md-tooltip> <md-icon aria-label="{{ \'action.delete\' | translate }}" class=material-icons> close </md-icon> </md-button> </div> </div> <div class=tb-error-messages ng-messages=ngModelCtrl.$error role=alert> <div translate ng-message=kvMap class=tb-error-message>{{requiredText}}</div> </div> <div> <md-button ng-show=!disabled ng-disabled=loading class="md-primary md-raised" ng-click=addKeyVal() aria-label="{{ \'action.add\' | translate }}"> <md-tooltip md-direction=top> {{ \'tb.key-val.add-entry\' | translate }} </md-tooltip> <md-icon aria-label="{{ \'action.add\' | translate }}" class=material-icons> add </md-icon> {{ \'action.add\' | translate }} </md-button> </div> </section> '},function(e,t){e.exports=" <section layout=column> <div flex layout=row> <md-input-container class=md-block style=min-width:100px> <label translate>relation.direction</label> <md-select required ng-model=query.direction> <md-option ng-repeat=\"direction in types.entitySearchDirection\" ng-value=direction> {{ ('relation.search-direction.' + direction) | translate}} </md-option> </md-select> </md-input-container> <md-input-container class=md-block> <label translate>tb.rulenode.max-relation-level</label> <input name=maxRelationLevel type=number min=1 step=1 placeholder=\"{{ 'tb.rulenode.unlimited-level' | translate }}\" ng-model=query.maxLevel aria-label=\"{{ 'tb.rulenode.max-relation-level' | translate }}\"> </md-input-container> </div> <div class=md-caption style=padding-bottom:10px;color:rgba(0,0,0,.57) translate>relation.relation-filters</div> <tb-relation-filters ng-model=query.filters> </tb-relation-filters> </section> "},function(e,t){e.exports=' <section layout=column> <md-input-container class=md-block> <label translate>tb.rulenode.originator-source</label> <md-select required ng-model=configuration.originatorSource> <md-option ng-repeat="source in ruleNodeTypes.originatorSource" ng-value=source.value> {{ source.name | translate}} </md-option> </md-select> </md-input-container> <section layout=column ng-if="configuration.originatorSource == ruleNodeTypes.originatorSource.RELATED.value"> <label translate class="tb-title tb-required">tb.rulenode.relations-query</label> <tb-relations-query-config style=padding-bottom:15px ng-model=configuration.relationsQuery> </tb-relations-query-config> </section> <md-checkbox aria-label="{{ \'tb.rulenode.clone-message\' | translate }}" ng-model=configuration.startNewChain>{{ \'tb.rulenode.clone-message\' | translate }} </md-checkbox> </section> '},function(e,t){e.exports=" <section layout=column> <label translate class=\"tb-title no-padding\">tb.rulenode.transform</label> <tb-js-func ng-model=configuration.jsScript function-name=Transform function-args=\"{{ ['msg', 'metadata'] }}\" no-validate=true> </tb-js-func> <md-checkbox aria-label=\"{{ 'tb.rulenode.clone-message' | translate }}\" ng-model=configuration.startNewChain>{{ 'tb.rulenode.clone-message' | translate }} </md-checkbox> </section> "},function(e,t,n){"use strict";function a(e){return e&&e.__esModule?e:{default:e}}function r(e){var t=function(t,n,a,r){var i=l.default;n.html(i),t.$watch("configuration",function(e,n){angular.equals(e,n)||r.$setViewValue(t.configuration)}),r.$render=function(){t.configuration=r.$viewValue},e(n.contents())(t)};return{restrict:"E",require:"^ngModel",scope:{},link:t}}r.$inject=["$compile"],Object.defineProperty(t,"__esModule",{value:!0}),t.default=r;var i=n(3),l=a(i)},function(e,t,n){"use strict";function a(e){return e&&e.__esModule?e:{default:e}}Object.defineProperty(t,"__esModule",{value:!0});var r=n(16),i=a(r),l=n(17),o=a(l),s=n(14),u=a(s),d=n(18),c=a(d);t.default=angular.module("thingsboard.ruleChain.config.enrichment",[]).directive("tbEnrichmentNodeOriginatorAttributesConfig",i.default).directive("tbEnrichmentNodeRelatedAttributesConfig",o.default).directive("tbEnrichmentNodeCustomerAttributesConfig",u.default).directive("tbEnrichmentNodeTenantAttributesConfig",c.default).name},function(e,t,n){"use strict";function a(e){return e&&e.__esModule?e:{default:e}}function r(e,t){var n=function(n,a,r,i){var o=l.default;a.html(o);var s=186;n.separatorKeys=[t.KEY_CODE.ENTER,t.KEY_CODE.COMMA,s],n.$watch("configuration",function(e,t){angular.equals(e,t)||i.$setViewValue(n.configuration)}),i.$render=function(){n.configuration=i.$viewValue},e(a.contents())(n)};return{restrict:"E",require:"^ngModel",scope:{},link:n}}r.$inject=["$compile","$mdConstant"],Object.defineProperty(t,"__esModule",{value:!0}),t.default=r;var i=n(4),l=a(i)},function(e,t,n){"use strict";function a(e){return e&&e.__esModule?e:{default:e}}function r(e){var t=function(t,n,a,r){var i=l.default;n.html(i),t.$watch("configuration",function(e,n){angular.equals(e,n)||r.$setViewValue(t.configuration)}),r.$render=function(){t.configuration=r.$viewValue},e(n.contents())(t)};return{restrict:"E",require:"^ngModel",scope:{},link:t}}r.$inject=["$compile"],Object.defineProperty(t,"__esModule",{value:!0}),t.default=r;var i=n(5),l=a(i)},function(e,t,n){"use strict";function a(e){return e&&e.__esModule?e:{default:e}}function r(e){var t=function(t,n,a,r){var i=l.default;n.html(i),t.$watch("configuration",function(e,n){angular.equals(e,n)||r.$setViewValue(t.configuration)}),r.$render=function(){t.configuration=r.$viewValue},e(n.contents())(t)};return{restrict:"E",require:"^ngModel",scope:{},link:t}}r.$inject=["$compile"],Object.defineProperty(t,"__esModule",{value:!0}),t.default=r;var i=n(6),l=a(i)},function(e,t,n){"use strict";function a(e){return e&&e.__esModule?e:{default:e}}Object.defineProperty(t,"__esModule",{value:!0});var r=n(21),i=a(r),l=n(20),o=a(l),s=n(22),u=a(s);t.default=angular.module("thingsboard.ruleChain.config.filter",[]).directive("tbFilterNodeScriptConfig",i.default).directive("tbFilterNodeMessageTypeConfig",o.default).directive("tbFilterNodeSwitchConfig",u.default).name},function(e,t,n){"use strict";function a(e){return e&&e.__esModule?e:{default:e}}function r(e,t,n){var a=function(a,r,i,o){function s(){if(o.$viewValue){for(var e=[],t=0;t<a.messageTypes.length;t++)e.push(a.messageTypes[t].value);o.$viewValue.messageTypes=e,u()}}function u(){if(a.required){var e=!(!o.$viewValue.messageTypes||!o.$viewValue.messageTypes.length);o.$setValidity("messageTypes",e)}else o.$setValidity("messageTypes",!0)}var d=l.default;r.html(d),a.selectedMessageType=null,a.messageTypeSearchText=null,a.ngModelCtrl=o;var c=[];for(var m in n.messageType){var f={name:n.messageType[m].name,value:n.messageType[m].value};c.push(f)}a.transformMessageTypeChip=function(e){var n,a=t("filter")(c,{name:e},!0);return n=a&&a.length?angular.copy(a[0]):{name:e,value:e}},a.messageTypesSearch=function(e){var n=e?t("filter")(c,{name:e}):c;return n.map(function(e){return e.name})},a.createMessageType=function(e,t){var n=angular.element(t,r)[0].firstElementChild,a=angular.element(n),i=a.scope().$mdChipsCtrl.getChipBuffer();e.preventDefault(),e.stopPropagation(),a.scope().$mdChipsCtrl.appendChip(i.trim()),a.scope().$mdChipsCtrl.resetChipBuffer()},o.$render=function(){var e=o.$viewValue,t=[];if(e&&e.messageTypes)for(var r=0;r<e.messageTypes.length;r++){var i=e.messageTypes[r];n.messageType[i]?t.push(angular.copy(n.messageType[i])):t.push({name:i,value:i})}a.messageTypes=t,a.$watch("messageTypes",function(e,t){angular.equals(e,t)||s()},!0)},e(r.contents())(a)};return{restrict:"E",require:"^ngModel",scope:{required:"=ngRequired",readonly:"=ngReadonly"},link:a}}r.$inject=["$compile","$filter","ruleNodeTypes"],Object.defineProperty(t,"__esModule",{value:!0}),t.default=r,n(1);var i=n(7),l=a(i)},[32,8],function(e,t,n){"use strict";function a(e){return e&&e.__esModule?e:{default:e}}function r(e){var t=function(t,n,a,r){var i=l.default;n.html(i),t.$watch("configuration",function(e,n){angular.equals(e,n)||r.$setViewValue(t.configuration)}),r.$render=function(){t.configuration=r.$viewValue},e(n.contents())(t)};return{restrict:"E",require:"^ngModel",scope:{},link:t}}r.$inject=["$compile"],Object.defineProperty(t,"__esModule",{value:!0}),t.default=r;var i=n(9),l=a(i)},function(e,t,n){"use strict";function a(e){return e&&e.__esModule?e:{default:e}}function r(e){var t=function(t,n,a,r){function i(e){e>-1&&t.kvList.splice(e,1)}function o(){t.kvList||(t.kvList=[]),t.kvList.push({key:"",value:""})}function s(){var e={};t.kvList.forEach(function(t){t.key&&(e[t.key]=t.value)}),r.$setViewValue(e),u()}function u(){var e=!0;t.required&&!t.kvList.length&&(e=!1),r.$setValidity("kvMap",e)}var d=l.default;n.html(d),t.ngModelCtrl=r,t.removeKeyVal=i,t.addKeyVal=o,t.kvList=[],t.$watch("query",function(e,n){angular.equals(e,n)||r.$setViewValue(t.query)}),r.$render=function(){if(r.$viewValue){var e=r.$viewValue;t.kvList.length=0;for(var n in e)t.kvList.push({key:n,value:e[n]})}t.$watch("kvList",function(e,t){angular.equals(e,t)||s()},!0),u()},e(n.contents())(t)};return{restrict:"E",require:"^ngModel",scope:{required:"=ngRequired",disabled:"=ngDisabled",requiredText:"=",keyText:"=",keyRequiredText:"=",valText:"=",valRequiredText:"="},link:t}}r.$inject=["$compile"],Object.defineProperty(t,"__esModule",{value:!0}),t.default=r;var i=n(10),l=a(i);n(2)},function(e,t,n){"use strict";function a(e){return e&&e.__esModule?e:{default:e}}function r(e,t){var n=function(n,a,r,i){var o=l.default;a.html(o),n.types=t,n.$watch("query",function(e,t){angular.equals(e,t)||i.$setViewValue(n.query)}),i.$render=function(){n.query=i.$viewValue},e(a.contents())(n)};return{restrict:"E",require:"^ngModel",scope:{},link:n}}r.$inject=["$compile","types"],Object.defineProperty(t,"__esModule",{value:!0}),t.default=r;var i=n(11),l=a(i)},function(e,t,n){"use strict";function a(e){return e&&e.__esModule?e:{default:e}}function r(e,t){var n=function(n,a,r,i){var o=l.default;a.html(o),n.ruleNodeTypes=t,n.$watch("configuration",function(e,t){angular.equals(e,t)||i.$setViewValue(n.configuration)}),i.$render=function(){n.configuration=i.$viewValue},e(a.contents())(n)};return{restrict:"E",require:"^ngModel",scope:{},link:n}}r.$inject=["$compile","ruleNodeTypes"],Object.defineProperty(t,"__esModule",{value:!0}),t.default=r;var i=n(12),l=a(i)},function(e,t,n){"use strict";function a(e){return e&&e.__esModule?e:{default:e}}Object.defineProperty(t,"__esModule",{value:!0});var r=n(25),i=a(r),l=n(27),o=a(l);t.default=angular.module("thingsboard.ruleChain.config.filter",[]).directive("tbTransformationNodeChangeOriginatorConfig",i.default).directive("tbTransformationNodeScriptConfig",o.default).name},[32,13],function(e,t,n){"use strict";function a(e){return e&&e.__esModule?e:{default:e}}Object.defineProperty(t,"__esModule",{value:!0});var r=n(31),i=a(r),l=n(19),o=a(l),s=n(15),u=a(s),d=n(26),c=a(d),m=n(24),f=a(m),g=n(23),p=a(g),b=n(30),y=a(b);t.default=angular.module("thingsboard.ruleChain.config",[i.default,o.default,u.default,c.default]).directive("tbRelationsQueryConfig",f.default).directive("tbKvMapConfig",p.default).config(y.default).name},function(e,t){"use strict";function n(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.","client-attributes":"Client attributes","shared-attributes":"Shared attributes","server-attributes":"Server attributes","latest-timeseries":"Latest timeseries","relations-query":"Relations query","max-relation-level":"Max relation level","unlimited-level":"Unlimited level","latest-telemetry":"Latest telemetry","attr-mapping":"Attributes mapping","source-attribute":"Source attribute","source-attribute-required":"Source attribute is required.","source-telemetry":"Source telemetry","source-telemetry-required":"Source telemetry is required.","target-attribute":"Target attribute","target-attribute-required":"Target attribute is required.","attr-mapping-required":"At least one attribute mapping should be specified.","originator-source":"Originator source","originator-customer":"Customer","originator-tenant":"Tenant","originator-related":"Related","clone-message":"Clone message",transform:"Transform"},"key-val":{key:"Key",value:"Value","remove-entry":"Remove entry","add-entry":"Add entry"}}};angular.merge(e.en_US,t)}Object.defineProperty(t,"__esModule",{value:!0}),t.default=n},function(e,t,n){"use strict";function a(e){return e&&e.__esModule?e:{default:e}}function r(e,t){(0,l.default)(t);for(var n in t){var a=t[n];e.translations(n,a)}}r.$inject=["$translateProvider","locales"],Object.defineProperty(t,"__esModule",{value:!0}),t.default=r;var i=n(29),l=a(i)},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"}},originatorSource:{CUSTOMER:{name:"tb.rulenode.originator-customer",value:"CUSTOMER"},TENANT:{name:"tb.rulenode.originator-tenant",value:"TENANT"},RELATED:{name:"tb.rulenode.originator-related",value:"RELATED"}}}).name},function(e,t,n,a){"use strict";function r(e){return e&&e.__esModule?e:{default:e}}function i(e){var t=function(t,n,a,r){var i=o.default;n.html(i),t.$watch("configuration",function(e,n){angular.equals(e,n)||r.$setViewValue(t.configuration)}),r.$render=function(){t.configuration=r.$viewValue},e(n.contents())(t)};return{restrict:"E",require:"^ngModel",scope:{},link:t}}i.$inject=["$compile"],Object.defineProperty(t,"__esModule",{value:!0}),t.default=i;var l=n(a),o=r(l)}]));
 //# sourceMappingURL=rulenode-core-config.js.map
\ No newline at end of file
diff --git a/ui/src/app/api/rule-chain.service.js b/ui/src/app/api/rule-chain.service.js
index af14a3f..b930cf1 100644
--- a/ui/src/app/api/rule-chain.service.js
+++ b/ui/src/app/api/rule-chain.service.js
@@ -253,7 +253,7 @@ function RuleChainService($http, $q, $filter, $ocLazyLoad, $translate, types, co
         if (ruleChainConnections && ruleChainConnections.length) {
             var tasks = [];
             for (var i = 0; i < ruleChainConnections.length; i++) {
-                tasks.push(getRuleChain(ruleChainConnections[i].targetRuleChainId.id));
+                tasks.push(resolveRuleChain(ruleChainConnections[i].targetRuleChainId.id));
             }
             $q.all(tasks).then(
                 (ruleChains) => {
@@ -273,6 +273,21 @@ function RuleChainService($http, $q, $filter, $ocLazyLoad, $translate, types, co
         return deferred.promise;
     }
 
+    function resolveRuleChain(ruleChainId) {
+        var deferred = $q.defer();
+        getRuleChain(ruleChainId, {ignoreErrors: true}).then(
+            (ruleChain) => {
+                deferred.resolve(ruleChain);
+            },
+            () => {
+                deferred.resolve({
+                    id: {id: ruleChainId, entityType: types.entityType.rulechain}
+                });
+            }
+        );
+        return deferred.promise;
+    }
+
     function loadRuleNodeComponents() {
         return componentDescriptorService.getComponentDescriptorsByTypes(types.ruleNodeTypeComponentTypes);
     }
diff --git a/ui/src/app/entity/relation/relation-filters.directive.js b/ui/src/app/entity/relation/relation-filters.directive.js
index 00d3b26..9ab66ca 100644
--- a/ui/src/app/entity/relation/relation-filters.directive.js
+++ b/ui/src/app/entity/relation/relation-filters.directive.js
@@ -46,6 +46,7 @@ export default function RelationFilters($compile, $templateCache) {
         ngModelCtrl.$render = function () {
             if (ngModelCtrl.$viewValue) {
                 var value = ngModelCtrl.$viewValue;
+                scope.relationFilters.length = 0;
                 value.forEach(function (filter) {
                     scope.relationFilters.push(filter);
                 });
diff --git a/ui/src/app/event/event.scss b/ui/src/app/event/event.scss
index b3be35c..b0fc46f 100644
--- a/ui/src/app/event/event.scss
+++ b/ui/src/app/event/event.scss
@@ -24,6 +24,17 @@ md-list.tb-event-table {
     height: 48px;
     padding: 0px;
     overflow: hidden;
+    .tb-cell {
+      text-overflow: ellipsis;
+      &.tb-scroll {
+        white-space: nowrap;
+        overflow-y: hidden;
+        overflow-x: auto;
+      }
+      &.tb-nowrap {
+        white-space: nowrap;
+      }
+    }
   }
 
   .tb-row:hover {
@@ -39,13 +50,19 @@ md-list.tb-event-table {
         color: rgba(0,0,0,.54);
         font-size: 12px;
         font-weight: 700;
-        white-space: nowrap;
         background: none;
+        white-space: nowrap;
       }
   }
 
   .tb-cell {
-      padding: 0 24px;
+      &:first-child {
+        padding-left: 14px;
+      }
+      &:last-child {
+        padding-right: 14px;
+      }
+      padding: 0 6px;
       margin: auto 0;
       color: rgba(0,0,0,.87);
       font-size: 13px;
@@ -53,8 +70,8 @@ md-list.tb-event-table {
       text-align: left;
       overflow: hidden;
       .md-button {
-        padding: 0;
-        margin: 0;
+          padding: 0;
+          margin: 0;
       }
   }
 
diff --git a/ui/src/app/event/event-header-debug-rulenode.tpl.html b/ui/src/app/event/event-header-debug-rulenode.tpl.html
index b412a0c..34f4513 100644
--- a/ui/src/app/event/event-header-debug-rulenode.tpl.html
+++ b/ui/src/app/event/event-header-debug-rulenode.tpl.html
@@ -15,13 +15,13 @@
     limitations under the License.
 
 -->
-<div hide-xs hide-sm translate class="tb-cell" flex="30">event.event-time</div>
+<div hide-xs hide-sm translate class="tb-cell" flex="25">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="10">event.type</div>
+<div translate class="tb-cell" flex="15">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>
+<div translate class="tb-cell" flex="15">event.data-type</div>
+<div translate class="tb-cell" flex="10">event.data</div>
+<div translate class="tb-cell" flex="10">event.metadata</div>
+<div translate class="tb-cell" flex="10">event.error</div>
diff --git a/ui/src/app/event/event-row.directive.js b/ui/src/app/event/event-row.directive.js
index 4643761..b808fb8 100644
--- a/ui/src/app/event/event-row.directive.js
+++ b/ui/src/app/event/event-row.directive.js
@@ -86,6 +86,14 @@ export default function EventRowDirective($compile, $templateCache, $mdDialog, $
             });
         }
 
+        scope.checkTooltip = function($event) {
+            var el = $event.target;
+            var $el = angular.element(el);
+            if(el.offsetWidth < el.scrollWidth && !$el.attr('title')){
+                $el.attr('title', $el.text());
+            }
+        }
+
         $compile(element.contents())(scope);
     }
 
diff --git a/ui/src/app/event/event-row-debug-rulenode.tpl.html b/ui/src/app/event/event-row-debug-rulenode.tpl.html
index 5b96baf..bb832b1 100644
--- a/ui/src/app/event/event-row-debug-rulenode.tpl.html
+++ b/ui/src/app/event/event-row-debug-rulenode.tpl.html
@@ -15,14 +15,14 @@
     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 hide-xs hide-sm class="tb-cell" flex="25">{{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">
+<div class="tb-cell" flex="10">{{event.body.type}}</div>
+<div class="tb-cell" flex="15">{{event.body.entityName}}</div>
+<div class="tb-cell tb-nowrap" flex="20" ng-mouseenter="checkTooltip($event)">{{event.body.msgId}}</div>
+<div class="tb-cell" flex="20" ng-mouseenter="checkTooltip($event)">{{event.body.msgType}}</div>
+<div class="tb-cell" flex="15">{{event.body.dataType}}</div>
+<div class="tb-cell" flex="10">
     <md-button ng-if="event.body.data" class="md-icon-button md-primary"
                ng-click="showContent($event, event.body.data, 'event.data', event.body.dataType)"
                aria-label="{{ 'action.view' | translate }}">
@@ -35,7 +35,7 @@
         </md-icon>
     </md-button>
 </div>
-<div class="tb-cell" flex="20">
+<div class="tb-cell" flex="10">
     <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 }}">
@@ -48,7 +48,7 @@
         </md-icon>
     </md-button>
 </div>
-<div class="tb-cell" flex="20">
+<div class="tb-cell" flex="10">
     <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 }}">
diff --git a/ui/src/app/import-export/import-export.service.js b/ui/src/app/import-export/import-export.service.js
index 19c249f..88c6353 100644
--- a/ui/src/app/import-export/import-export.service.js
+++ b/ui/src/app/import-export/import-export.service.js
@@ -281,39 +281,63 @@ export default function ImportExport($log, $translate, $q, $mdDialog, $document,
 
     function exportRuleChain(ruleChainId) {
         ruleChainService.getRuleChain(ruleChainId).then(
-            function success(ruleChain) {
-                var name = ruleChain.name;
-                name = name.toLowerCase().replace(/\W/g,"_");
-                exportToPc(prepareExport(ruleChain), name + '.json');
-                //TODO: metadata
+            (ruleChain) => {
+                ruleChainService.getRuleChainMetaData(ruleChainId).then(
+                    (ruleChainMetaData) => {
+                        var ruleChainExport = {
+                            ruleChain: prepareRuleChain(ruleChain),
+                            metadata: prepareRuleChainMetaData(ruleChainMetaData)
+                        };
+                        var name = ruleChain.name;
+                        name = name.toLowerCase().replace(/\W/g,"_");
+                        exportToPc(ruleChainExport, name + '.json');
+                    },
+                    (rejection) => {
+                        processExportRuleChainRejection(rejection);
+                    }
+                );
             },
-            function fail(rejection) {
-                var message = rejection;
-                if (!message) {
-                    message = $translate.instant('error.unknown-error');
-                }
-                toast.showError($translate.instant('rulechain.export-failed-error', {error: message}));
+            (rejection) => {
+                processExportRuleChainRejection(rejection);
             }
         );
     }
 
+    function prepareRuleChain(ruleChain) {
+        ruleChain = prepareExport(ruleChain);
+        if (ruleChain.firstRuleNodeId) {
+            ruleChain.firstRuleNodeId = null;
+        }
+        ruleChain.root = false;
+        return ruleChain;
+    }
+
+    function prepareRuleChainMetaData(ruleChainMetaData) {
+        delete ruleChainMetaData.ruleChainId;
+        for (var i=0;i<ruleChainMetaData.nodes.length;i++) {
+            var node = ruleChainMetaData.nodes[i];
+            ruleChainMetaData.nodes[i] = prepareExport(node);
+        }
+        return ruleChainMetaData;
+    }
+
+    function processExportRuleChainRejection(rejection) {
+        var message = rejection;
+        if (!message) {
+            message = $translate.instant('error.unknown-error');
+        }
+        toast.showError($translate.instant('rulechain.export-failed-error', {error: message}));
+    }
+
     function importRuleChain($event) {
         var deferred = $q.defer();
         openImportDialog($event, 'rulechain.import', 'rulechain.rulechain-file').then(
-            function success(ruleChain) {
-                if (!validateImportedRuleChain(ruleChain)) {
+            function success(ruleChainImport) {
+                if (!validateImportedRuleChain(ruleChainImport)) {
                     toast.showError($translate.instant('rulechain.invalid-rulechain-file-error'));
                     deferred.reject();
                 } else {
-                    //TODO: rulechain metadata
-                    ruleChainService.saveRuleChain(ruleChain).then(
-                        function success() {
-                            deferred.resolve();
-                        },
-                        function fail() {
-                            deferred.reject();
-                        }
-                    );
+                    deferred.resolve(ruleChainImport);
                 }
             },
             function fail() {
@@ -323,10 +347,14 @@ export default function ImportExport($log, $translate, $q, $mdDialog, $document,
         return deferred.promise;
     }
 
-    function validateImportedRuleChain(ruleChain) {
-        //TODO: rulechain metadata
-        if (angular.isUndefined(ruleChain.name))
-        {
+    function validateImportedRuleChain(ruleChainImport) {
+        if (angular.isUndefined(ruleChainImport.ruleChain)) {
+            return false;
+        }
+        if (angular.isUndefined(ruleChainImport.metadata)) {
+            return false;
+        }
+        if (angular.isUndefined(ruleChainImport.ruleChain.name)) {
             return false;
         }
         return true;
diff --git a/ui/src/app/locale/locale.constant.js b/ui/src/app/locale/locale.constant.js
index f616fec..f366644 100644
--- a/ui/src/app/locale/locale.constant.js
+++ b/ui/src/app/locale/locale.constant.js
@@ -43,6 +43,7 @@ export default angular.module('thingsboard.locale', [])
                     "update": "Update",
                     "remove": "Remove",
                     "search": "Search",
+                    "clear-search": "Clear search",
                     "assign": "Assign",
                     "unassign": "Unassign",
                     "share": "Share",
@@ -1174,7 +1175,7 @@ export default angular.module('thingsboard.locale', [])
                     "export": "Export rule chain",
                     "export-failed-error": "Unable to export rule chain: {{error}}",
                     "create-new-rulechain": "Create new rule chain",
-                    "rule-file": "Rule chain file",
+                    "rulechain-file": "Rule chain file",
                     "invalid-rulechain-file-error": "Unable to import rule chain: Invalid rule chain data structure.",
                     "copyId": "Copy rule chain Id",
                     "idCopiedMessage": "Rule chain Id has been copied to clipboard",
@@ -1188,6 +1189,7 @@ export default angular.module('thingsboard.locale', [])
                     "details": "Details",
                     "events": "Events",
                     "search": "Search nodes",
+                    "open-node-library": "Open node library",
                     "add": "Add rule node",
                     "name": "Name",
                     "name-required": "Name is required.",
@@ -1217,7 +1219,8 @@ export default angular.module('thingsboard.locale', [])
                     "type-rule-chain": "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."
+                    "ui-resources-load-error": "Failed to load configuration ui resources.",
+                    "invalid-target-rulechain": "Unable to resolve target rule chain!"
                 },
                 "rule-plugin": {
                     "management": "Rules and plugins management"
diff --git a/ui/src/app/rulechain/rulechain.controller.js b/ui/src/app/rulechain/rulechain.controller.js
index 7de72c3..c20fa44 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, $window, $document, $mdDialog,
+export function RuleChainController($state, $scope, $compile, $q, $mdUtil, $timeout, $mdExpansionPanel, $window, $document, $mdDialog,
                                     $filter, $translate, hotkeys, types, ruleChainService, Modelfactory, flowchartConstants,
                                     ruleChain, ruleChainMetaData, ruleNodeComponents) {
 
@@ -37,6 +37,24 @@ export function RuleChainController($stateParams, $scope, $compile, $q, $mdUtil,
     vm.$mdExpansionPanel = $mdExpansionPanel;
     vm.types = types;
 
+    if ($state.current.data.import && !ruleChain) {
+        $state.go('home.ruleChains');
+        return;
+    }
+
+    vm.isImport = $state.current.data.import;
+    vm.isConfirmOnExit = false;
+
+    $scope.$watch(function() {
+        return vm.isDirty || vm.isImport;
+    }, (val) => {
+        vm.isConfirmOnExit = val;
+    });
+
+    vm.errorTooltips = {};
+
+    vm.isFullscreen = false;
+
     vm.editingRuleNode = null;
     vm.isEditingRuleNode = false;
 
@@ -57,6 +75,7 @@ export function RuleChainController($stateParams, $scope, $compile, $q, $mdUtil,
     };
 
     vm.ruleNodeTypesModel = {};
+    vm.ruleNodeTypesCanvasControl = {};
     vm.ruleChainLibraryLoaded = false;
     for (var type in types.ruleNodeType) {
         if (!types.ruleNodeType[type].special) {
@@ -67,9 +86,12 @@ export function RuleChainController($stateParams, $scope, $compile, $q, $mdUtil,
                 },
                 selectedObjects: []
             };
+            vm.ruleNodeTypesCanvasControl[type] = {};
         }
     }
 
+
+
     vm.selectedObjects = [];
 
     vm.modelservice = Modelfactory(vm.ruleChainModel, vm.selectedObjects);
@@ -145,8 +167,12 @@ export function RuleChainController($stateParams, $scope, $compile, $q, $mdUtil,
         $scope.$broadcast('form-submit');
         if (theForm.$valid) {
             theForm.$setPristine();
+            if (vm.editingRuleNode.error) {
+                delete vm.editingRuleNode.error;
+            }
             vm.ruleChainModel.nodes[vm.editingRuleNodeIndex] = vm.editingRuleNode;
             vm.editingRuleNode = angular.copy(vm.editingRuleNode);
+            updateRuleNodesHighlight();
         }
     };
 
@@ -203,7 +229,9 @@ export function RuleChainController($stateParams, $scope, $compile, $q, $mdUtil,
         }
         var instances = angular.element.tooltipster.instances();
         instances.forEach((instance) => {
-            instance.destroy();
+            if (!instance.isErrorTooltip) {
+                instance.destroy();
+            }
         });
     }
 
@@ -249,6 +277,71 @@ export function RuleChainController($stateParams, $scope, $compile, $q, $mdUtil,
         }, 500);
     }
 
+    function updateNodeErrorTooltip(node) {
+        if (node.error) {
+            var element = angular.element('#' + node.id);
+            var tooltip = vm.errorTooltips[node.id];
+            if (!tooltip || !element.hasClass("tooltipstered")) {
+                element.tooltipster(
+                    {
+                        theme: 'tooltipster-shadow',
+                        delay: 0,
+                        animationDuration: 0,
+                        trigger: 'custom',
+                        triggerOpen: {
+                            click: false,
+                            tap: false
+                        },
+                        triggerClose: {
+                            click: false,
+                            tap: false,
+                            scroll: false
+                        },
+                        side: 'top',
+                        trackOrigin: true
+                    }
+                );
+                var content = '<div class="tb-rule-node-error-tooltip">' +
+                    '<div id="tooltip-content" layout="column">' +
+                    '<div class="tb-node-details">' + node.error + '</div>' +
+                    '</div>' +
+                    '</div>';
+                var contentElement = angular.element(content);
+                $compile(contentElement)($scope);
+                tooltip = element.tooltipster('instance');
+                tooltip.isErrorTooltip = true;
+                tooltip.content(contentElement);
+                vm.errorTooltips[node.id] = tooltip;
+            }
+            $mdUtil.nextTick(() => {
+                tooltip.open();
+            });
+        } else {
+            if (vm.errorTooltips[node.id]) {
+                tooltip = vm.errorTooltips[node.id];
+                tooltip.destroy();
+                delete vm.errorTooltips[node.id];
+            }
+        }
+    }
+
+    function updateErrorTooltips(hide) {
+        for (var nodeId in vm.errorTooltips) {
+            var tooltip = vm.errorTooltips[nodeId];
+            if (hide) {
+                tooltip.close();
+            } else {
+                tooltip.open();
+            }
+        }
+    }
+
+    $scope.$watch(function() {
+        return vm.isEditingRuleNode || vm.isEditingRuleNodeLink;
+    }, (val) => {
+        updateErrorTooltips(val);
+    });
+
     vm.editCallbacks = {
         edgeDoubleClick: function (event, edge) {
             var sourceNode = vm.modelservice.nodes.getNodeByConnectorId(edge.source);
@@ -313,12 +406,28 @@ export function RuleChainController($stateParams, $scope, $compile, $q, $mdUtil,
         }
     };
 
-    loadRuleChainLibrary();
+    loadRuleChainLibrary(ruleNodeComponents, true);
+
+    $scope.$watch('vm.ruleNodeSearch',
+        function (newVal, oldVal) {
+            if (!angular.equals(newVal, oldVal)) {
+                var res = $filter('filter')(ruleNodeComponents, {name: vm.ruleNodeSearch});
+                loadRuleChainLibrary(res);
+            }
+        }
+    );
+
+    $scope.$on('searchTextUpdated', function () {
+        updateRuleNodesHighlight();
+    });
 
-    function loadRuleChainLibrary() {
+    function loadRuleChainLibrary(ruleNodeComponents, loadRuleChain) {
+        for (var componentType in vm.ruleNodeTypesModel) {
+            vm.ruleNodeTypesModel[componentType].model.nodes.length = 0;
+        }
         for (var i=0;i<ruleNodeComponents.length;i++) {
             var ruleNodeComponent = ruleNodeComponents[i];
-            var componentType = ruleNodeComponent.type;
+            componentType = ruleNodeComponent.type;
             var model = vm.ruleNodeTypesModel[componentType].model;
             var node = {
                 id: 'node-lib-' + componentType + '-' + model.nodes.length,
@@ -349,7 +458,26 @@ export function RuleChainController($stateParams, $scope, $compile, $q, $mdUtil,
             model.nodes.push(node);
         }
         vm.ruleChainLibraryLoaded = true;
-        prepareRuleChain();
+        if (loadRuleChain) {
+            prepareRuleChain();
+        }
+        $mdUtil.nextTick(() => {
+            for (componentType in vm.ruleNodeTypesCanvasControl) {
+                if (vm.ruleNodeTypesCanvasControl[componentType].adjustCanvasSize) {
+                    vm.ruleNodeTypesCanvasControl[componentType].adjustCanvasSize(true);
+                }
+            }
+            for (componentType in vm.ruleNodeTypesModel) {
+                var panel = vm.$mdExpansionPanel(componentType);
+                if (panel) {
+                    if (!vm.ruleNodeTypesModel[componentType].model.nodes.length) {
+                        panel.collapse();
+                    } else {
+                        panel.expand();
+                    }
+                }
+            }
+        });
     }
 
     function prepareRuleChain() {
@@ -480,11 +608,9 @@ export function RuleChainController($stateParams, $scope, $compile, $q, $mdUtil,
                         ruleChainNode = {
                             id: 'rule-chain-node-' + vm.nextNodeID++,
                             additionalInfo: ruleChainConnection.additionalInfo,
-                            targetRuleChainId: ruleChainConnection.targetRuleChainId.id,
                             x: ruleChainConnection.additionalInfo.layoutX,
                             y: ruleChainConnection.additionalInfo.layoutY,
                             component: types.ruleChainNodeComponent,
-                            name: ruleChain.name,
                             nodeClass: vm.types.ruleNodeType.RULE_CHAIN.nodeClass,
                             icon: vm.types.ruleNodeType.RULE_CHAIN.icon,
                             connectors: [
@@ -494,6 +620,14 @@ export function RuleChainController($stateParams, $scope, $compile, $q, $mdUtil,
                                 }
                             ]
                         };
+                        if (ruleChain.name) {
+                            ruleChainNode.name = ruleChain.name;
+                            ruleChainNode.targetRuleChainId = ruleChainConnection.targetRuleChainId.id;
+                        } else {
+                            ruleChainNode.name = "Unresolved";
+                            ruleChainNode.targetRuleChainId = null;
+                            ruleChainNode.error = $translate.instant('rulenode.invalid-target-rulechain');
+                        }
                         ruleChainNodesMap[ruleChainConnection.additionalInfo.ruleChainNodeId] = ruleChainNode;
                         vm.ruleChainModel.nodes.push(ruleChainNode);
                     }
@@ -519,89 +653,141 @@ export function RuleChainController($stateParams, $scope, $compile, $q, $mdUtil,
 
         vm.isDirty = false;
 
+        updateRuleNodesHighlight();
+
+        validate();
+
         $mdUtil.nextTick(() => {
             vm.ruleChainWatch = $scope.$watch('vm.ruleChainModel',
                 function (newVal, oldVal) {
-                    if (!vm.isDirty && !angular.equals(newVal, oldVal)) {
-                        vm.isDirty = true;
+                    if (!angular.equals(newVal, oldVal)) {
+                        validate();
+                        if (!vm.isDirty) {
+                            vm.isDirty = true;
+                        }
                     }
                 }, true
             );
         });
     }
 
+    function updateRuleNodesHighlight() {
+        for (var i = 0; i < vm.ruleChainModel.nodes.length; i++) {
+            vm.ruleChainModel.nodes[i].highlighted = false;
+        }
+        if ($scope.searchConfig.searchText) {
+            var res = $filter('filter')(vm.ruleChainModel.nodes, {name: $scope.searchConfig.searchText});
+            if (res) {
+                for (i = 0; i < res.length; i++) {
+                    res[i].highlighted = true;
+                }
+            }
+        }
+    }
+
+    function validate() {
+        $mdUtil.nextTick(() => {
+            vm.isInvalid = false;
+            for (var i = 0; i < vm.ruleChainModel.nodes.length; i++) {
+                if (vm.ruleChainModel.nodes[i].error) {
+                    vm.isInvalid = true;
+                }
+                updateNodeErrorTooltip(vm.ruleChainModel.nodes[i]);
+            }
+        });
+    }
+
     function saveRuleChain() {
-        var ruleChainMetaData = {
-            ruleChainId: vm.ruleChain.id,
-            nodes: [],
-            connections: [],
-            ruleChainConnections: []
-        };
+        var saveRuleChainPromise;
+        if (vm.isImport) {
+            saveRuleChainPromise = ruleChainService.saveRuleChain(vm.ruleChain);
+        } else {
+            saveRuleChainPromise = $q.when(vm.ruleChain);
+        }
+        saveRuleChainPromise.then(
+            (ruleChain) => {
+                vm.ruleChain = ruleChain;
+                var ruleChainMetaData = {
+                    ruleChainId: vm.ruleChain.id,
+                    nodes: [],
+                    connections: [],
+                    ruleChainConnections: []
+                };
 
-        var nodes = [];
+                var nodes = [];
 
-        for (var i=0;i<vm.ruleChainModel.nodes.length;i++) {
-            var node = vm.ruleChainModel.nodes[i];
-            if (node.component.type != types.ruleNodeType.INPUT.value && node.component.type != types.ruleNodeType.RULE_CHAIN.value) {
-                var ruleNode = {};
-                if (node.ruleNodeId) {
-                    ruleNode.id = node.ruleNodeId;
+                for (var i=0;i<vm.ruleChainModel.nodes.length;i++) {
+                    var node = vm.ruleChainModel.nodes[i];
+                    if (node.component.type != types.ruleNodeType.INPUT.value && node.component.type != types.ruleNodeType.RULE_CHAIN.value) {
+                        var ruleNode = {};
+                        if (node.ruleNodeId) {
+                            ruleNode.id = node.ruleNodeId;
+                        }
+                        ruleNode.type = node.component.clazz;
+                        ruleNode.name = node.name;
+                        ruleNode.configuration = node.configuration;
+                        ruleNode.additionalInfo = node.additionalInfo;
+                        ruleNode.debugMode = node.debugMode;
+                        if (!ruleNode.additionalInfo) {
+                            ruleNode.additionalInfo = {};
+                        }
+                        ruleNode.additionalInfo.layoutX = node.x;
+                        ruleNode.additionalInfo.layoutY = node.y;
+                        ruleChainMetaData.nodes.push(ruleNode);
+                        nodes.push(node);
+                    }
                 }
-                ruleNode.type = node.component.clazz;
-                ruleNode.name = node.name;
-                ruleNode.configuration = node.configuration;
-                ruleNode.additionalInfo = node.additionalInfo;
-                ruleNode.debugMode = node.debugMode;
-                if (!ruleNode.additionalInfo) {
-                    ruleNode.additionalInfo = {};
+                var res = $filter('filter')(vm.ruleChainModel.edges, {source: vm.inputConnectorId});
+                if (res && res.length) {
+                    var firstNodeEdge = res[0];
+                    var firstNode = vm.modelservice.nodes.getNodeByConnectorId(firstNodeEdge.destination);
+                    ruleChainMetaData.firstNodeIndex = nodes.indexOf(firstNode);
                 }
-                ruleNode.additionalInfo.layoutX = node.x;
-                ruleNode.additionalInfo.layoutY = node.y;
-                ruleChainMetaData.nodes.push(ruleNode);
-                nodes.push(node);
-            }
-        }
-        var res = $filter('filter')(vm.ruleChainModel.edges, {source: vm.inputConnectorId});
-        if (res && res.length) {
-            var firstNodeEdge = res[0];
-            var firstNode = vm.modelservice.nodes.getNodeByConnectorId(firstNodeEdge.destination);
-            ruleChainMetaData.firstNodeIndex = nodes.indexOf(firstNode);
-        }
-        for (i=0;i<vm.ruleChainModel.edges.length;i++) {
-            var edge = vm.ruleChainModel.edges[i];
-            var sourceNode = vm.modelservice.nodes.getNodeByConnectorId(edge.source);
-            var destNode = vm.modelservice.nodes.getNodeByConnectorId(edge.destination);
-            if (sourceNode.component.type != types.ruleNodeType.INPUT.value) {
-                var fromIndex = nodes.indexOf(sourceNode);
-                if (destNode.component.type == types.ruleNodeType.RULE_CHAIN.value) {
-                    var ruleChainConnection = {
-                        fromIndex: fromIndex,
-                        targetRuleChainId: {entityType: vm.types.entityType.rulechain, id: destNode.targetRuleChainId},
-                        additionalInfo: destNode.additionalInfo,
-                        type: edge.label
-                    };
-                    if (!ruleChainConnection.additionalInfo) {
-                        ruleChainConnection.additionalInfo = {};
+                for (i=0;i<vm.ruleChainModel.edges.length;i++) {
+                    var edge = vm.ruleChainModel.edges[i];
+                    var sourceNode = vm.modelservice.nodes.getNodeByConnectorId(edge.source);
+                    var destNode = vm.modelservice.nodes.getNodeByConnectorId(edge.destination);
+                    if (sourceNode.component.type != types.ruleNodeType.INPUT.value) {
+                        var fromIndex = nodes.indexOf(sourceNode);
+                        if (destNode.component.type == types.ruleNodeType.RULE_CHAIN.value) {
+                            var ruleChainConnection = {
+                                fromIndex: fromIndex,
+                                targetRuleChainId: {entityType: vm.types.entityType.rulechain, id: destNode.targetRuleChainId},
+                                additionalInfo: destNode.additionalInfo,
+                                type: edge.label
+                            };
+                            if (!ruleChainConnection.additionalInfo) {
+                                ruleChainConnection.additionalInfo = {};
+                            }
+                            ruleChainConnection.additionalInfo.layoutX = destNode.x;
+                            ruleChainConnection.additionalInfo.layoutY = destNode.y;
+                            ruleChainConnection.additionalInfo.ruleChainNodeId = destNode.id;
+                            ruleChainMetaData.ruleChainConnections.push(ruleChainConnection);
+                        } else {
+                            var toIndex = nodes.indexOf(destNode);
+                            var nodeConnection = {
+                                fromIndex: fromIndex,
+                                toIndex: toIndex,
+                                type: edge.label
+                            };
+                            ruleChainMetaData.connections.push(nodeConnection);
+                        }
                     }
-                    ruleChainConnection.additionalInfo.layoutX = destNode.x;
-                    ruleChainConnection.additionalInfo.layoutY = destNode.y;
-                    ruleChainConnection.additionalInfo.ruleChainNodeId = destNode.id;
-                    ruleChainMetaData.ruleChainConnections.push(ruleChainConnection);
-                } else {
-                    var toIndex = nodes.indexOf(destNode);
-                    var nodeConnection = {
-                        fromIndex: fromIndex,
-                        toIndex: toIndex,
-                        type: edge.label
-                    };
-                    ruleChainMetaData.connections.push(nodeConnection);
                 }
-            }
-        }
-        ruleChainService.saveRuleChainMetaData(ruleChainMetaData).then(
-            (ruleChainMetaData) => {
-                vm.ruleChainMetaData = ruleChainMetaData;
-                prepareRuleChain();
+                ruleChainService.saveRuleChainMetaData(ruleChainMetaData).then(
+                    (ruleChainMetaData) => {
+                        vm.ruleChainMetaData = ruleChainMetaData;
+                        if (vm.isImport) {
+                            vm.isDirty = false;
+                            vm.isImport = false;
+                            $mdUtil.nextTick(() => {
+                                $state.go('home.ruleChains.ruleChain', {ruleChainId: vm.ruleChain.id.id});
+                            });
+                        } else {
+                            prepareRuleChain();
+                        }
+                    }
+                );
             }
         );
     }
@@ -614,12 +800,14 @@ export function RuleChainController($stateParams, $scope, $compile, $q, $mdUtil,
 
         ruleNode.configuration = angular.copy(ruleNode.component.configurationDescriptor.nodeDefinition.defaultConfiguration);
 
+        var ruleChainId = vm.ruleChain.id ? vm.ruleChain.id.id : null;
+
         $mdDialog.show({
             controller: 'AddRuleNodeController',
             controllerAs: 'vm',
             templateUrl: addRuleNodeTemplate,
             parent: angular.element($document[0].body),
-            locals: {ruleNode: ruleNode, ruleChainId: vm.ruleChain.id.id},
+            locals: {ruleNode: ruleNode, ruleChainId: ruleChainId},
             fullscreen: true,
             targetEvent: $event
         }).then(function (ruleNode) {
@@ -642,6 +830,7 @@ export function RuleChainController($stateParams, $scope, $compile, $q, $mdUtil,
                 );
             }
             vm.ruleChainModel.nodes.push(ruleNode);
+            updateRuleNodesHighlight();
         }, function () {
         });
     }
diff --git a/ui/src/app/rulechain/rulechain.routes.js b/ui/src/app/rulechain/rulechain.routes.js
index f9578ef..2aefd82 100644
--- a/ui/src/app/rulechain/rulechain.routes.js
+++ b/ui/src/app/rulechain/rulechain.routes.js
@@ -76,11 +76,52 @@ export default function RuleChainRoutes($stateProvider, NodeTemplatePathProvider
                     }
             },
             data: {
-                searchEnabled: false,
+                import: false,
+                searchEnabled: true,
                 pageTitle: 'rulechain.rulechain'
             },
             ncyBreadcrumb: {
                 label: '{"icon": "settings_ethernet", "label": "{{ vm.ruleChain.name }}", "translate": "false"}'
             }
+    }).state('home.ruleChains.importRuleChain', {
+        url: '/ruleChain/import',
+        reloadOnSearch: false,
+        module: 'private',
+        auth: ['SYS_ADMIN', 'TENANT_ADMIN'],
+        views: {
+            "content@home": {
+                templateUrl: ruleChainTemplate,
+                controller: 'RuleChainController',
+                controllerAs: 'vm'
+            }
+        },
+        params: {
+            ruleChainImport: {}
+        },
+        resolve: {
+            ruleChain:
+            /*@ngInject*/
+                function($stateParams) {
+                    return $stateParams.ruleChainImport.ruleChain;
+                },
+            ruleChainMetaData:
+            /*@ngInject*/
+                function($stateParams) {
+                    return $stateParams.ruleChainImport.metadata;
+                },
+            ruleNodeComponents:
+            /*@ngInject*/
+                function($stateParams, ruleChainService) {
+                    return ruleChainService.getRuleNodeComponents();
+                }
+        },
+        data: {
+            import: true,
+            searchEnabled: true,
+            pageTitle: 'rulechain.rulechain'
+        },
+        ncyBreadcrumb: {
+            label: '{"icon": "settings_ethernet", "label": "{{ (\'rulechain.import\' | translate) + \': \'+ vm.ruleChain.name }}", "translate": "false"}'
+        }
     });
 }
diff --git a/ui/src/app/rulechain/rulechain.scss b/ui/src/app/rulechain/rulechain.scss
index 38f785a..187a722 100644
--- a/ui/src/app/rulechain/rulechain.scss
+++ b/ui/src/app/rulechain/rulechain.scss
@@ -125,6 +125,16 @@
   color: #333;
   border: solid 1px #777;
   font-size: 12px;
+  &.tb-rule-node-highlighted:not(.tb-rule-node-invalid) {
+    box-shadow: 0 0 10px 6px #51cbee;
+    .tb-node-title {
+      text-decoration: underline;
+      font-weight: bold;
+    }
+  }
+  &.tb-rule-node-invalid {
+    box-shadow: 0 0 10px 6px #ff5c50;
+  }
   &.tb-input-type {
     background-color: #a3eaa9;
     user-select: none;
@@ -156,7 +166,7 @@
 
   }
   .tb-node-title {
-    font-weight: 600;
+    font-weight: 500;
   }
   .tb-node-type, .tb-node-title {
     overflow: hidden;
@@ -380,6 +390,14 @@
   font-size: 14px;
   width: 300px;
   color: #333;
+}
+
+.tb-rule-node-error-tooltip {
+  font-size: 16px;
+  color: #ea0d0d;
+}
+
+.tb-rule-node-tooltip, .tb-rule-node-error-tooltip {
   #tooltip-content {
     .tb-node-title {
       font-weight: 600;
diff --git a/ui/src/app/rulechain/rulechain.tpl.html b/ui/src/app/rulechain/rulechain.tpl.html
index ddc1a90..37fcd9b 100644
--- a/ui/src/app/rulechain/rulechain.tpl.html
+++ b/ui/src/app/rulechain/rulechain.tpl.html
@@ -16,20 +16,20 @@
 
 -->
 
-<md-content flex tb-expand-fullscreen tb-confirm-on-exit is-dirty="vm.isDirty"
+<md-content flex tb-expand-fullscreen tb-confirm-on-exit is-dirty="vm.isConfirmOnExit"
             expand-tooltip-direction="bottom" layout="column" class="tb-rulechain"
             ng-keydown="vm.keyDown($event)"
-            ng-keyup="vm.keyUp($event)">
+            ng-keyup="vm.keyUp($event)" on-fullscreen-changed="vm.isFullscreen = expanded">
     <section class="tb-rulechain-container" flex layout="column">
         <div class="tb-rulechain-layout" flex layout="row">
             <section layout="row" layout-wrap
                      class="tb-header-buttons md-fab tb-library-open">
                 <md-button ng-show="!vm.isLibraryOpen"
                            class="tb-btn-header tb-btn-open-library md-primary md-fab md-fab-top-left"
-                           aria-label="{{ 'action.apply' | translate }}"
+                           aria-label="{{ 'rulenode.open-node-library' | translate }}"
                            ng-click="vm.isLibraryOpen = true">
-                    <md-tooltip md-direction="top">
-                        {{ 'action.apply-changes' | translate }}
+                    <md-tooltip md-direction="{{vm.isFullscreen ? 'bottom' : 'top'}}">
+                        {{ 'rulenode.open-node-library' | translate }}
                     </md-tooltip>
                     <ng-md-icon icon="menu"></ng-md-icon>
                 </md-button>
@@ -43,7 +43,7 @@
                     <div class="md-toolbar-tools">
                         <md-button class="md-icon-button tb-small" aria-label="{{ 'action.search' | translate }}">
                             <md-icon aria-label="{{ 'action.search' | translate }}" class="material-icons">search</md-icon>
-                            <md-tooltip md-direction="top">
+                            <md-tooltip md-direction="{{vm.isFullscreen ? 'bottom' : 'top'}}">
                                 {{'rulenode.search' | translate}}
                             </md-tooltip>
                         </md-button>
@@ -53,15 +53,17 @@
                                 <input ng-model="vm.ruleNodeSearch" placeholder="{{'rulenode.search' | translate}}"/>
                             </md-input-container>
                         </div>
-                        <md-button class="md-icon-button tb-small" aria-label="Close" ng-click="vm.ruleNodeSearch = ''">
+                        <md-button class="md-icon-button tb-small" aria-label="Close"
+                                   ng-show="vm.ruleNodeSearch"
+                                   ng-click="vm.ruleNodeSearch = ''">
                             <md-icon aria-label="Close" class="material-icons">close</md-icon>
-                            <md-tooltip md-direction="top">
-                                {{ 'action.close' | translate }}
+                            <md-tooltip md-direction="{{vm.isFullscreen ? 'bottom' : 'top'}}">
+                                {{ 'action.clear-search' | translate }}
                             </md-tooltip>
                         </md-button>
                         <md-button class="md-icon-button tb-small" aria-label="Close" ng-click="vm.isLibraryOpen = false">
                             <md-icon aria-label="Close" class="material-icons">chevron_left</md-icon>
-                            <md-tooltip md-direction="top">
+                            <md-tooltip md-direction="{{vm.isFullscreen ? 'bottom' : 'top'}}">
                                 {{ 'action.close' | translate }}
                             </md-tooltip>
                         </md-button>
@@ -90,6 +92,7 @@
                                            callbacks="vm.nodeLibCallbacks"
                                            node-width="170"
                                            node-height="50"
+                                           control="vm.ruleNodeTypesCanvasControl[typeId]"
                                            drop-target-id="'tb-rulchain-canvas'"></fc-canvas>
                             </md-expansion-panel-content>
                         </md-expansion-panel-expanded>
@@ -182,7 +185,7 @@
             </md-tooltip>
             <ng-md-icon icon="delete"></ng-md-icon>
         </md-button>
-        <md-button ng-disabled="$root.loading || !vm.isDirty"
+        <md-button ng-disabled="$root.loading  || vm.isInvalid || (!vm.isDirty && !vm.isImport)"
                    class="tb-btn-footer md-accent md-hue-2 md-fab"
                    aria-label="{{ 'action.apply' | translate }}"
                    ng-click="vm.saveRuleChain()">
diff --git a/ui/src/app/rulechain/rulechains.controller.js b/ui/src/app/rulechain/rulechains.controller.js
index 2a30b0c..7c857f2 100644
--- a/ui/src/app/rulechain/rulechains.controller.js
+++ b/ui/src/app/rulechain/rulechains.controller.js
@@ -63,8 +63,8 @@ export default function RuleChainsController(ruleChainService, userService, impo
         {
             onAction: function ($event) {
                 importExport.importRuleChain($event).then(
-                    function() {
-                        vm.grid.refreshList();
+                    function(ruleChainImport) {
+                        $state.go('home.ruleChains.importRuleChain', {ruleChainImport:ruleChainImport});
                     }
                 );
             },
diff --git a/ui/src/app/rulechain/rulenode.tpl.html b/ui/src/app/rulechain/rulenode.tpl.html
index 55ee3d3..973ea1f 100644
--- a/ui/src/app/rulechain/rulenode.tpl.html
+++ b/ui/src/app/rulechain/rulenode.tpl.html
@@ -23,7 +23,7 @@
         ng-mouseenter="callbacks.mouseEnter($event, node)"
         ng-mouseleave="callbacks.mouseLeave($event, node)">
     <div class="{{flowchartConstants.nodeOverlayClass}}"></div>
-    <div class="tb-rule-node {{node.nodeClass}}">
+    <div class="tb-rule-node {{node.nodeClass}}" ng-class="{'tb-rule-node-highlighted' : node.highlighted, 'tb-rule-node-invalid': node.error }">
         <md-icon aria-label="node-type-icon" flex="15"
                  class="material-icons">{{node.icon}}</md-icon>
         <div layout="column" flex="85" layout-align="center">