thingsboard-memoizeit
Changes
application/src/main/java/org/thingsboard/server/actors/ruleChain/RuleNodeActorMessageProcessor.java 8(+6 -2)
rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/data/RelationsQuery.java 31(+31 -0)
rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/metadata/TbGetAttributesNode.java 4(+3 -1)
rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/metadata/TbGetCustomerAttributeNode.java 4(+3 -1)
rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/metadata/TbGetRelatedAttributeNode.java 7(+5 -2)
rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/metadata/TbGetRelatedAttrNodeConfiguration.java 17(+12 -5)
rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/metadata/TbGetTenantAttributeNode.java 4(+3 -1)
rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/transform/TbChangeOriginatorNode.java 12(+7 -5)
rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/transform/TbChangeOriginatorNodeConfiguration.java 18(+14 -4)
rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/transform/TbTransformMsgNode.java 4(+3 -1)
rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/util/EntitiesRelatedEntityIdAsyncLoader.java 25(+17 -8)
rule-engine/rule-engine-components/src/main/resources/public/static/rulenode/rulenode-core-config.css 2(+1 -1)
rule-engine/rule-engine-components/src/main/resources/public/static/rulenode/rulenode-core-config.js 2(+1 -1)
ui/src/app/api/rule-chain.service.js 17(+16 -1)
ui/src/app/event/event.scss 25(+21 -4)
ui/src/app/locale/locale.constant.js 7(+5 -2)
ui/src/app/rulechain/rulechain.controller.js 341(+265 -76)
ui/src/app/rulechain/rulechain.routes.js 43(+42 -1)
ui/src/app/rulechain/rulechain.scss 20(+19 -1)
ui/src/app/rulechain/rulechain.tpl.html 25(+14 -11)
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:'...'}}" }\'>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:'...'}}" }\'>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> </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
ui/src/app/api/rule-chain.service.js 17(+16 -1)
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);
});
ui/src/app/event/event.scss 25(+21 -4)
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;
ui/src/app/locale/locale.constant.js 7(+5 -2)
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"
ui/src/app/rulechain/rulechain.controller.js 341(+265 -76)
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 () {
});
}
ui/src/app/rulechain/rulechain.routes.js 43(+42 -1)
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"}'
+ }
});
}
ui/src/app/rulechain/rulechain.scss 20(+19 -1)
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;
ui/src/app/rulechain/rulechain.tpl.html 25(+14 -11)
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">