Commit b38a7d7bda4d7175391973cc685efdb8a0dbfdae
Merge branch 'develop/1.5' of github.com:thingsboard/thingsboard into develop/1.5
Showing
27 changed files
with
570 additions
and
173 deletions
application/src/main/java/org/thingsboard/server/actors/ruleChain/RuleNodeActorMessageProcessor.java
@@ -69,14 +69,18 @@ public class RuleNodeActorMessageProcessor extends ComponentMsgProcessor<RuleNod | @@ -69,14 +69,18 @@ public class RuleNodeActorMessageProcessor extends ComponentMsgProcessor<RuleNod | ||
69 | && ruleNode.getConfiguration().equals(newRuleNode.getConfiguration())); | 69 | && ruleNode.getConfiguration().equals(newRuleNode.getConfiguration())); |
70 | this.ruleNode = newRuleNode; | 70 | this.ruleNode = newRuleNode; |
71 | if (restartRequired) { | 71 | if (restartRequired) { |
72 | - tbNode.destroy(); | 72 | + if (tbNode != null) { |
73 | + tbNode.destroy(); | ||
74 | + } | ||
73 | start(context); | 75 | start(context); |
74 | } | 76 | } |
75 | } | 77 | } |
76 | 78 | ||
77 | @Override | 79 | @Override |
78 | public void stop(ActorContext context) throws Exception { | 80 | public void stop(ActorContext context) throws Exception { |
79 | - tbNode.destroy(); | 81 | + if (tbNode != null) { |
82 | + tbNode.destroy(); | ||
83 | + } | ||
80 | context.stop(self); | 84 | context.stop(self); |
81 | } | 85 | } |
82 | 86 |
1 | +/** | ||
2 | + * Copyright © 2016-2018 The Thingsboard Authors | ||
3 | + * | ||
4 | + * Licensed under the Apache License, Version 2.0 (the "License"); | ||
5 | + * you may not use this file except in compliance with the License. | ||
6 | + * You may obtain a copy of the License at | ||
7 | + * | ||
8 | + * http://www.apache.org/licenses/LICENSE-2.0 | ||
9 | + * | ||
10 | + * Unless required by applicable law or agreed to in writing, software | ||
11 | + * distributed under the License is distributed on an "AS IS" BASIS, | ||
12 | + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | ||
13 | + * See the License for the specific language governing permissions and | ||
14 | + * limitations under the License. | ||
15 | + */ | ||
16 | +package org.thingsboard.rule.engine.data; | ||
17 | + | ||
18 | +import lombok.Data; | ||
19 | +import org.thingsboard.server.common.data.relation.EntitySearchDirection; | ||
20 | +import org.thingsboard.server.common.data.relation.EntityTypeFilter; | ||
21 | + | ||
22 | +import java.util.List; | ||
23 | + | ||
24 | +@Data | ||
25 | +public class RelationsQuery { | ||
26 | + | ||
27 | + private EntitySearchDirection direction; | ||
28 | + private int maxLevel = 1; | ||
29 | + private List<EntityTypeFilter> filters; | ||
30 | + | ||
31 | +} |
@@ -43,7 +43,9 @@ import static org.thingsboard.server.common.data.DataConstants.*; | @@ -43,7 +43,9 @@ import static org.thingsboard.server.common.data.DataConstants.*; | ||
43 | nodeDetails = "If Attributes enrichment configured, <b>CLIENT/SHARED/SERVER</b> attributes are added into Message metadata " + | 43 | nodeDetails = "If Attributes enrichment configured, <b>CLIENT/SHARED/SERVER</b> attributes are added into Message metadata " + |
44 | "with specific prefix: <i>cs/shared/ss</i>. To access those attributes in other nodes this template can be used " + | 44 | "with specific prefix: <i>cs/shared/ss</i>. To access those attributes in other nodes this template can be used " + |
45 | "<code>metadata.cs.temperature</code> or <code>metadata.shared.limit</code> " + | 45 | "<code>metadata.cs.temperature</code> or <code>metadata.shared.limit</code> " + |
46 | - "If Latest Telemetry enrichment configured, latest telemetry added into metadata without prefix.") | 46 | + "If Latest Telemetry enrichment configured, latest telemetry added into metadata without prefix.", |
47 | + uiResources = {"static/rulenode/rulenode-core-config.js"}, | ||
48 | + configDirective = "tbEnrichmentNodeOriginatorAttributesConfig") | ||
47 | public class TbGetAttributesNode implements TbNode { | 49 | public class TbGetAttributesNode implements TbNode { |
48 | 50 | ||
49 | private TbGetAttributesNodeConfiguration config; | 51 | private TbGetAttributesNodeConfiguration config; |
@@ -30,7 +30,9 @@ import org.thingsboard.server.common.data.plugin.ComponentType; | @@ -30,7 +30,9 @@ import org.thingsboard.server.common.data.plugin.ComponentType; | ||
30 | nodeDescription = "Add Originators Customer Attributes or Latest Telemetry into Message Metadata", | 30 | nodeDescription = "Add Originators Customer Attributes or Latest Telemetry into Message Metadata", |
31 | nodeDetails = "If Attributes enrichment configured, server scope attributes are added into Message metadata. " + | 31 | nodeDetails = "If Attributes enrichment configured, server scope attributes are added into Message metadata. " + |
32 | "To access those attributes in other nodes this template can be used " + | 32 | "To access those attributes in other nodes this template can be used " + |
33 | - "<code>metadata.temperature</code>. If Latest Telemetry enrichment configured, latest telemetry added into metadata") | 33 | + "<code>metadata.temperature</code>. If Latest Telemetry enrichment configured, latest telemetry added into metadata", |
34 | + uiResources = {"static/rulenode/rulenode-core-config.js", "static/rulenode/rulenode-core-config.css"}, | ||
35 | + configDirective = "tbEnrichmentNodeCustomerAttributesConfig") | ||
34 | public class TbGetCustomerAttributeNode extends TbEntityGetAttrNode<CustomerId> { | 36 | public class TbGetCustomerAttributeNode extends TbEntityGetAttrNode<CustomerId> { |
35 | 37 | ||
36 | @Override | 38 | @Override |
@@ -16,18 +16,19 @@ | @@ -16,18 +16,19 @@ | ||
16 | package org.thingsboard.rule.engine.metadata; | 16 | package org.thingsboard.rule.engine.metadata; |
17 | 17 | ||
18 | import lombok.Data; | 18 | import lombok.Data; |
19 | -import org.thingsboard.rule.engine.api.NodeConfiguration; | 19 | +import org.thingsboard.rule.engine.data.RelationsQuery; |
20 | import org.thingsboard.server.common.data.relation.EntityRelation; | 20 | import org.thingsboard.server.common.data.relation.EntityRelation; |
21 | import org.thingsboard.server.common.data.relation.EntitySearchDirection; | 21 | import org.thingsboard.server.common.data.relation.EntitySearchDirection; |
22 | +import org.thingsboard.server.common.data.relation.EntityTypeFilter; | ||
22 | 23 | ||
24 | +import java.util.Collections; | ||
23 | import java.util.HashMap; | 25 | import java.util.HashMap; |
24 | import java.util.Map; | 26 | import java.util.Map; |
25 | 27 | ||
26 | @Data | 28 | @Data |
27 | public class TbGetRelatedAttrNodeConfiguration extends TbGetEntityAttrNodeConfiguration { | 29 | public class TbGetRelatedAttrNodeConfiguration extends TbGetEntityAttrNodeConfiguration { |
28 | 30 | ||
29 | - private String relationType; | ||
30 | - private EntitySearchDirection direction; | 31 | + private RelationsQuery relationsQuery; |
31 | 32 | ||
32 | @Override | 33 | @Override |
33 | public TbGetRelatedAttrNodeConfiguration defaultConfiguration() { | 34 | public TbGetRelatedAttrNodeConfiguration defaultConfiguration() { |
@@ -36,8 +37,14 @@ public class TbGetRelatedAttrNodeConfiguration extends TbGetEntityAttrNodeConfig | @@ -36,8 +37,14 @@ public class TbGetRelatedAttrNodeConfiguration extends TbGetEntityAttrNodeConfig | ||
36 | attrMapping.putIfAbsent("temperature", "tempo"); | 37 | attrMapping.putIfAbsent("temperature", "tempo"); |
37 | configuration.setAttrMapping(attrMapping); | 38 | configuration.setAttrMapping(attrMapping); |
38 | configuration.setTelemetry(true); | 39 | configuration.setTelemetry(true); |
39 | - configuration.setRelationType(EntityRelation.CONTAINS_TYPE); | ||
40 | - configuration.setDirection(EntitySearchDirection.FROM); | 40 | + |
41 | + RelationsQuery relationsQuery = new RelationsQuery(); | ||
42 | + relationsQuery.setDirection(EntitySearchDirection.FROM); | ||
43 | + relationsQuery.setMaxLevel(1); | ||
44 | + EntityTypeFilter entityTypeFilter = new EntityTypeFilter(EntityRelation.CONTAINS_TYPE, Collections.emptyList()); | ||
45 | + relationsQuery.setFilters(Collections.singletonList(entityTypeFilter)); | ||
46 | + configuration.setRelationsQuery(relationsQuery); | ||
47 | + | ||
41 | return configuration; | 48 | return configuration; |
42 | } | 49 | } |
43 | } | 50 | } |
@@ -32,7 +32,10 @@ import org.thingsboard.server.common.data.plugin.ComponentType; | @@ -32,7 +32,10 @@ import org.thingsboard.server.common.data.plugin.ComponentType; | ||
32 | "If multiple Related Entities are found, only first Entity is used for attributes enrichment, other entities are discarded. " + | 32 | "If multiple Related Entities are found, only first Entity is used for attributes enrichment, other entities are discarded. " + |
33 | "If Attributes enrichment configured, server scope attributes are added into Message metadata. " + | 33 | "If Attributes enrichment configured, server scope attributes are added into Message metadata. " + |
34 | "To access those attributes in other nodes this template can be used " + | 34 | "To access those attributes in other nodes this template can be used " + |
35 | - "<code>metadata.temperature</code>. If Latest Telemetry enrichment configured, latest telemetry added into metadata") | 35 | + "<code>metadata.temperature</code>. If Latest Telemetry enrichment configured, latest telemetry added into metadata", |
36 | + uiResources = {"static/rulenode/rulenode-core-config.js", "static/rulenode/rulenode-core-config.css"}, | ||
37 | + configDirective = "tbEnrichmentNodeRelatedAttributesConfig") | ||
38 | + | ||
36 | public class TbGetRelatedAttributeNode extends TbEntityGetAttrNode<EntityId> { | 39 | public class TbGetRelatedAttributeNode extends TbEntityGetAttrNode<EntityId> { |
37 | 40 | ||
38 | private TbGetRelatedAttrNodeConfiguration config; | 41 | private TbGetRelatedAttrNodeConfiguration config; |
@@ -45,6 +48,6 @@ public class TbGetRelatedAttributeNode extends TbEntityGetAttrNode<EntityId> { | @@ -45,6 +48,6 @@ public class TbGetRelatedAttributeNode extends TbEntityGetAttrNode<EntityId> { | ||
45 | 48 | ||
46 | @Override | 49 | @Override |
47 | protected ListenableFuture<EntityId> findEntityAsync(TbContext ctx, EntityId originator) { | 50 | protected ListenableFuture<EntityId> findEntityAsync(TbContext ctx, EntityId originator) { |
48 | - return EntitiesRelatedEntityIdAsyncLoader.findEntityAsync(ctx, originator, config.getDirection(), config.getRelationType()); | 51 | + return EntitiesRelatedEntityIdAsyncLoader.findEntityAsync(ctx, originator, config.getRelationsQuery()); |
49 | } | 52 | } |
50 | } | 53 | } |
@@ -32,7 +32,9 @@ import org.thingsboard.server.common.data.plugin.ComponentType; | @@ -32,7 +32,9 @@ import org.thingsboard.server.common.data.plugin.ComponentType; | ||
32 | nodeDescription = "Add Originators Tenant Attributes or Latest Telemetry into Message Metadata", | 32 | nodeDescription = "Add Originators Tenant Attributes or Latest Telemetry into Message Metadata", |
33 | nodeDetails = "If Attributes enrichment configured, server scope attributes are added into Message metadata. " + | 33 | nodeDetails = "If Attributes enrichment configured, server scope attributes are added into Message metadata. " + |
34 | "To access those attributes in other nodes this template can be used " + | 34 | "To access those attributes in other nodes this template can be used " + |
35 | - "<code>metadata.temperature</code>. If Latest Telemetry enrichment configured, latest telemetry added into metadata") | 35 | + "<code>metadata.temperature</code>. If Latest Telemetry enrichment configured, latest telemetry added into metadata", |
36 | + uiResources = {"static/rulenode/rulenode-core-config.js", "static/rulenode/rulenode-core-config.css"}, | ||
37 | + configDirective = "tbEnrichmentNodeTenantAttributesConfig") | ||
36 | public class TbGetTenantAttributeNode extends TbEntityGetAttrNode<TenantId> { | 38 | public class TbGetTenantAttributeNode extends TbEntityGetAttrNode<TenantId> { |
37 | 39 | ||
38 | @Override | 40 | @Override |
@@ -39,7 +39,9 @@ import java.util.HashSet; | @@ -39,7 +39,9 @@ import java.util.HashSet; | ||
39 | configClazz = TbChangeOriginatorNodeConfiguration.class, | 39 | configClazz = TbChangeOriginatorNodeConfiguration.class, |
40 | nodeDescription = "Change Message Originator To Tenant/Customer/Related Entity", | 40 | nodeDescription = "Change Message Originator To Tenant/Customer/Related Entity", |
41 | nodeDetails = "Related Entity found using configured relation direction and Relation Type. " + | 41 | nodeDetails = "Related Entity found using configured relation direction and Relation Type. " + |
42 | - "If multiple Related Entities are found, only first Entity is used as new Originator, other entities are discarded. ") | 42 | + "If multiple Related Entities are found, only first Entity is used as new Originator, other entities are discarded. ", |
43 | + uiResources = {"static/rulenode/rulenode-core-config.js", "static/rulenode/rulenode-core-config.css"}, | ||
44 | + configDirective = "tbTransformationNodeChangeOriginatorConfig") | ||
43 | public class TbChangeOriginatorNode extends TbAbstractTransformNode { | 45 | public class TbChangeOriginatorNode extends TbAbstractTransformNode { |
44 | 46 | ||
45 | protected static final String CUSTOMER_SOURCE = "CUSTOMER"; | 47 | protected static final String CUSTOMER_SOURCE = "CUSTOMER"; |
@@ -68,7 +70,7 @@ public class TbChangeOriginatorNode extends TbAbstractTransformNode { | @@ -68,7 +70,7 @@ public class TbChangeOriginatorNode extends TbAbstractTransformNode { | ||
68 | case TENANT_SOURCE: | 70 | case TENANT_SOURCE: |
69 | return EntitiesTenantIdAsyncLoader.findEntityIdAsync(ctx, original); | 71 | return EntitiesTenantIdAsyncLoader.findEntityIdAsync(ctx, original); |
70 | case RELATED_SOURCE: | 72 | case RELATED_SOURCE: |
71 | - return EntitiesRelatedEntityIdAsyncLoader.findEntityAsync(ctx, original, config.getDirection(), config.getRelationType()); | 73 | + return EntitiesRelatedEntityIdAsyncLoader.findEntityAsync(ctx, original, config.getRelationsQuery()); |
72 | default: | 74 | default: |
73 | return Futures.immediateFailedFuture(new IllegalStateException("Unexpected originator source " + config.getOriginatorSource())); | 75 | return Futures.immediateFailedFuture(new IllegalStateException("Unexpected originator source " + config.getOriginatorSource())); |
74 | } | 76 | } |
@@ -82,9 +84,9 @@ public class TbChangeOriginatorNode extends TbAbstractTransformNode { | @@ -82,9 +84,9 @@ public class TbChangeOriginatorNode extends TbAbstractTransformNode { | ||
82 | } | 84 | } |
83 | 85 | ||
84 | if (conf.getOriginatorSource().equals(RELATED_SOURCE)) { | 86 | if (conf.getOriginatorSource().equals(RELATED_SOURCE)) { |
85 | - if (conf.getDirection() == null || StringUtils.isBlank(conf.getRelationType())) { | ||
86 | - log.error("Related source for TbChangeOriginatorNode should have direction and relationType. Actual [{}] [{}]", | ||
87 | - conf.getDirection(), conf.getRelationType()); | 87 | + if (conf.getRelationsQuery() == null) { |
88 | + log.error("Related source for TbChangeOriginatorNode should have relations query. Actual [{}]", | ||
89 | + conf.getRelationsQuery()); | ||
88 | throw new IllegalArgumentException("Wrong config for RElated Source in TbChangeOriginatorNode" + conf.getOriginatorSource()); | 90 | throw new IllegalArgumentException("Wrong config for RElated Source in TbChangeOriginatorNode" + conf.getOriginatorSource()); |
89 | } | 91 | } |
90 | } | 92 | } |
@@ -17,22 +17,32 @@ package org.thingsboard.rule.engine.transform; | @@ -17,22 +17,32 @@ package org.thingsboard.rule.engine.transform; | ||
17 | 17 | ||
18 | import lombok.Data; | 18 | import lombok.Data; |
19 | import org.thingsboard.rule.engine.api.NodeConfiguration; | 19 | import org.thingsboard.rule.engine.api.NodeConfiguration; |
20 | +import org.thingsboard.rule.engine.data.RelationsQuery; | ||
20 | import org.thingsboard.server.common.data.relation.EntityRelation; | 21 | import org.thingsboard.server.common.data.relation.EntityRelation; |
21 | import org.thingsboard.server.common.data.relation.EntitySearchDirection; | 22 | import org.thingsboard.server.common.data.relation.EntitySearchDirection; |
23 | +import org.thingsboard.server.common.data.relation.EntityTypeFilter; | ||
24 | + | ||
25 | +import java.util.Collections; | ||
22 | 26 | ||
23 | @Data | 27 | @Data |
24 | public class TbChangeOriginatorNodeConfiguration extends TbTransformNodeConfiguration implements NodeConfiguration { | 28 | public class TbChangeOriginatorNodeConfiguration extends TbTransformNodeConfiguration implements NodeConfiguration { |
25 | 29 | ||
26 | private String originatorSource; | 30 | private String originatorSource; |
27 | - private EntitySearchDirection direction; | ||
28 | - private String relationType; | 31 | + |
32 | + private RelationsQuery relationsQuery; | ||
29 | 33 | ||
30 | @Override | 34 | @Override |
31 | public TbChangeOriginatorNodeConfiguration defaultConfiguration() { | 35 | public TbChangeOriginatorNodeConfiguration defaultConfiguration() { |
32 | TbChangeOriginatorNodeConfiguration configuration = new TbChangeOriginatorNodeConfiguration(); | 36 | TbChangeOriginatorNodeConfiguration configuration = new TbChangeOriginatorNodeConfiguration(); |
33 | configuration.setOriginatorSource(TbChangeOriginatorNode.CUSTOMER_SOURCE); | 37 | configuration.setOriginatorSource(TbChangeOriginatorNode.CUSTOMER_SOURCE); |
34 | - configuration.setDirection(EntitySearchDirection.FROM); | ||
35 | - configuration.setRelationType(EntityRelation.CONTAINS_TYPE); | 38 | + |
39 | + RelationsQuery relationsQuery = new RelationsQuery(); | ||
40 | + relationsQuery.setDirection(EntitySearchDirection.FROM); | ||
41 | + relationsQuery.setMaxLevel(1); | ||
42 | + EntityTypeFilter entityTypeFilter = new EntityTypeFilter(EntityRelation.CONTAINS_TYPE, Collections.emptyList()); | ||
43 | + relationsQuery.setFilters(Collections.singletonList(entityTypeFilter)); | ||
44 | + configuration.setRelationsQuery(relationsQuery); | ||
45 | + | ||
36 | configuration.setStartNewChain(false); | 46 | configuration.setStartNewChain(false); |
37 | return configuration; | 47 | return configuration; |
38 | } | 48 | } |
@@ -31,7 +31,9 @@ import javax.script.Bindings; | @@ -31,7 +31,9 @@ import javax.script.Bindings; | ||
31 | nodeDescription = "Change Message payload and Metadata using JavaScript", | 31 | nodeDescription = "Change Message payload and Metadata using JavaScript", |
32 | nodeDetails = "JavaScript function recieve 2 input parameters that can be changed inside.<br/> " + | 32 | nodeDetails = "JavaScript function recieve 2 input parameters that can be changed inside.<br/> " + |
33 | "<code>metadata</code> - is a Message metadata.<br/>" + | 33 | "<code>metadata</code> - is a Message metadata.<br/>" + |
34 | - "<code>msg</code> - is a Message payload.<br/>Any properties can be changed/removed/added in those objects.") | 34 | + "<code>msg</code> - is a Message payload.<br/>Any properties can be changed/removed/added in those objects.", |
35 | + uiResources = {"static/rulenode/rulenode-core-config.js", "static/rulenode/rulenode-core-config.css"}, | ||
36 | + configDirective = "tbTransformationNodeScriptConfig") | ||
35 | public class TbTransformMsgNode extends TbAbstractTransformNode { | 37 | public class TbTransformMsgNode extends TbAbstractTransformNode { |
36 | 38 | ||
37 | private TbTransformMsgNodeConfiguration config; | 39 | private TbTransformMsgNodeConfiguration config; |
@@ -20,32 +20,41 @@ import com.google.common.util.concurrent.Futures; | @@ -20,32 +20,41 @@ import com.google.common.util.concurrent.Futures; | ||
20 | import com.google.common.util.concurrent.ListenableFuture; | 20 | import com.google.common.util.concurrent.ListenableFuture; |
21 | import org.apache.commons.collections.CollectionUtils; | 21 | import org.apache.commons.collections.CollectionUtils; |
22 | import org.thingsboard.rule.engine.api.TbContext; | 22 | import org.thingsboard.rule.engine.api.TbContext; |
23 | +import org.thingsboard.rule.engine.data.RelationsQuery; | ||
23 | import org.thingsboard.server.common.data.id.EntityId; | 24 | import org.thingsboard.server.common.data.id.EntityId; |
24 | import org.thingsboard.server.common.data.relation.EntityRelation; | 25 | import org.thingsboard.server.common.data.relation.EntityRelation; |
26 | +import org.thingsboard.server.common.data.relation.EntityRelationsQuery; | ||
25 | import org.thingsboard.server.common.data.relation.EntitySearchDirection; | 27 | import org.thingsboard.server.common.data.relation.EntitySearchDirection; |
28 | +import org.thingsboard.server.common.data.relation.RelationsSearchParameters; | ||
26 | import org.thingsboard.server.dao.relation.RelationService; | 29 | import org.thingsboard.server.dao.relation.RelationService; |
27 | 30 | ||
28 | import java.util.List; | 31 | import java.util.List; |
29 | 32 | ||
30 | -import static org.thingsboard.server.common.data.relation.RelationTypeGroup.COMMON; | ||
31 | - | ||
32 | public class EntitiesRelatedEntityIdAsyncLoader { | 33 | public class EntitiesRelatedEntityIdAsyncLoader { |
33 | 34 | ||
34 | public static ListenableFuture<EntityId> findEntityAsync(TbContext ctx, EntityId originator, | 35 | public static ListenableFuture<EntityId> findEntityAsync(TbContext ctx, EntityId originator, |
35 | - EntitySearchDirection direction, String relationType) { | 36 | + RelationsQuery relationsQuery) { |
36 | RelationService relationService = ctx.getRelationService(); | 37 | RelationService relationService = ctx.getRelationService(); |
37 | - if (direction == EntitySearchDirection.FROM) { | ||
38 | - ListenableFuture<List<EntityRelation>> asyncRelation = relationService.findByFromAndTypeAsync(originator, relationType, COMMON); | 38 | + EntityRelationsQuery query = buildQuery(originator, relationsQuery); |
39 | + ListenableFuture<List<EntityRelation>> asyncRelation = relationService.findByQuery(query); | ||
40 | + if (relationsQuery.getDirection() == EntitySearchDirection.FROM) { | ||
39 | return Futures.transform(asyncRelation, (AsyncFunction<? super List<EntityRelation>, EntityId>) | 41 | return Futures.transform(asyncRelation, (AsyncFunction<? super List<EntityRelation>, EntityId>) |
40 | r -> CollectionUtils.isNotEmpty(r) ? Futures.immediateFuture(r.get(0).getTo()) | 42 | r -> CollectionUtils.isNotEmpty(r) ? Futures.immediateFuture(r.get(0).getTo()) |
41 | : Futures.immediateFailedFuture(new IllegalStateException("Relation not found"))); | 43 | : Futures.immediateFailedFuture(new IllegalStateException("Relation not found"))); |
42 | - } else if (direction == EntitySearchDirection.TO) { | ||
43 | - ListenableFuture<List<EntityRelation>> asyncRelation = relationService.findByToAndTypeAsync(originator, relationType, COMMON); | 44 | + } else if (relationsQuery.getDirection() == EntitySearchDirection.TO) { |
44 | return Futures.transform(asyncRelation, (AsyncFunction<? super List<EntityRelation>, EntityId>) | 45 | return Futures.transform(asyncRelation, (AsyncFunction<? super List<EntityRelation>, EntityId>) |
45 | r -> CollectionUtils.isNotEmpty(r) ? Futures.immediateFuture(r.get(0).getFrom()) | 46 | r -> CollectionUtils.isNotEmpty(r) ? Futures.immediateFuture(r.get(0).getFrom()) |
46 | : Futures.immediateFailedFuture(new IllegalStateException("Relation not found"))); | 47 | : Futures.immediateFailedFuture(new IllegalStateException("Relation not found"))); |
47 | } | 48 | } |
48 | - | ||
49 | return Futures.immediateFailedFuture(new IllegalStateException("Unknown direction")); | 49 | return Futures.immediateFailedFuture(new IllegalStateException("Unknown direction")); |
50 | } | 50 | } |
51 | + | ||
52 | + private static EntityRelationsQuery buildQuery(EntityId originator, RelationsQuery relationsQuery) { | ||
53 | + EntityRelationsQuery query = new EntityRelationsQuery(); | ||
54 | + RelationsSearchParameters parameters = new RelationsSearchParameters(originator, | ||
55 | + relationsQuery.getDirection(), relationsQuery.getMaxLevel()); | ||
56 | + query.setParameters(parameters); | ||
57 | + query.setFilters(relationsQuery.getFilters()); | ||
58 | + return query; | ||
59 | + } | ||
51 | } | 60 | } |
1 | -.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} | 1 | +.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} |
2 | /*# sourceMappingURL=rulenode-core-config.css.map*/ | 2 | /*# sourceMappingURL=rulenode-core-config.css.map*/ |
rule-engine/rule-engine-components/src/main/resources/public/static/rulenode/rulenode-core-config.js
1 | -!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}]); | 1 | +!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)}])); |
2 | //# sourceMappingURL=rulenode-core-config.js.map | 2 | //# sourceMappingURL=rulenode-core-config.js.map |
@@ -253,7 +253,7 @@ function RuleChainService($http, $q, $filter, $ocLazyLoad, $translate, types, co | @@ -253,7 +253,7 @@ function RuleChainService($http, $q, $filter, $ocLazyLoad, $translate, types, co | ||
253 | if (ruleChainConnections && ruleChainConnections.length) { | 253 | if (ruleChainConnections && ruleChainConnections.length) { |
254 | var tasks = []; | 254 | var tasks = []; |
255 | for (var i = 0; i < ruleChainConnections.length; i++) { | 255 | for (var i = 0; i < ruleChainConnections.length; i++) { |
256 | - tasks.push(getRuleChain(ruleChainConnections[i].targetRuleChainId.id)); | 256 | + tasks.push(resolveRuleChain(ruleChainConnections[i].targetRuleChainId.id)); |
257 | } | 257 | } |
258 | $q.all(tasks).then( | 258 | $q.all(tasks).then( |
259 | (ruleChains) => { | 259 | (ruleChains) => { |
@@ -273,6 +273,21 @@ function RuleChainService($http, $q, $filter, $ocLazyLoad, $translate, types, co | @@ -273,6 +273,21 @@ function RuleChainService($http, $q, $filter, $ocLazyLoad, $translate, types, co | ||
273 | return deferred.promise; | 273 | return deferred.promise; |
274 | } | 274 | } |
275 | 275 | ||
276 | + function resolveRuleChain(ruleChainId) { | ||
277 | + var deferred = $q.defer(); | ||
278 | + getRuleChain(ruleChainId, {ignoreErrors: true}).then( | ||
279 | + (ruleChain) => { | ||
280 | + deferred.resolve(ruleChain); | ||
281 | + }, | ||
282 | + () => { | ||
283 | + deferred.resolve({ | ||
284 | + id: {id: ruleChainId, entityType: types.entityType.rulechain} | ||
285 | + }); | ||
286 | + } | ||
287 | + ); | ||
288 | + return deferred.promise; | ||
289 | + } | ||
290 | + | ||
276 | function loadRuleNodeComponents() { | 291 | function loadRuleNodeComponents() { |
277 | return componentDescriptorService.getComponentDescriptorsByTypes(types.ruleNodeTypeComponentTypes); | 292 | return componentDescriptorService.getComponentDescriptorsByTypes(types.ruleNodeTypeComponentTypes); |
278 | } | 293 | } |
@@ -46,6 +46,7 @@ export default function RelationFilters($compile, $templateCache) { | @@ -46,6 +46,7 @@ export default function RelationFilters($compile, $templateCache) { | ||
46 | ngModelCtrl.$render = function () { | 46 | ngModelCtrl.$render = function () { |
47 | if (ngModelCtrl.$viewValue) { | 47 | if (ngModelCtrl.$viewValue) { |
48 | var value = ngModelCtrl.$viewValue; | 48 | var value = ngModelCtrl.$viewValue; |
49 | + scope.relationFilters.length = 0; | ||
49 | value.forEach(function (filter) { | 50 | value.forEach(function (filter) { |
50 | scope.relationFilters.push(filter); | 51 | scope.relationFilters.push(filter); |
51 | }); | 52 | }); |
@@ -15,13 +15,13 @@ | @@ -15,13 +15,13 @@ | ||
15 | limitations under the License. | 15 | limitations under the License. |
16 | 16 | ||
17 | --> | 17 | --> |
18 | -<div hide-xs hide-sm translate class="tb-cell" flex="30">event.event-time</div> | 18 | +<div hide-xs hide-sm translate class="tb-cell" flex="25">event.event-time</div> |
19 | <div translate class="tb-cell" flex="20">event.server</div> | 19 | <div translate class="tb-cell" flex="20">event.server</div> |
20 | -<div translate class="tb-cell" flex="20">event.type</div> | ||
21 | -<div translate class="tb-cell" flex="20">event.entity</div> | 20 | +<div translate class="tb-cell" flex="10">event.type</div> |
21 | +<div translate class="tb-cell" flex="15">event.entity</div> | ||
22 | <div translate class="tb-cell" flex="20">event.message-id</div> | 22 | <div translate class="tb-cell" flex="20">event.message-id</div> |
23 | <div translate class="tb-cell" flex="20">event.message-type</div> | 23 | <div translate class="tb-cell" flex="20">event.message-type</div> |
24 | -<div translate class="tb-cell" flex="20">event.data-type</div> | ||
25 | -<div translate class="tb-cell" flex="20">event.data</div> | ||
26 | -<div translate class="tb-cell" flex="20">event.metadata</div> | ||
27 | -<div translate class="tb-cell" flex="20">event.error</div> | 24 | +<div translate class="tb-cell" flex="15">event.data-type</div> |
25 | +<div translate class="tb-cell" flex="10">event.data</div> | ||
26 | +<div translate class="tb-cell" flex="10">event.metadata</div> | ||
27 | +<div translate class="tb-cell" flex="10">event.error</div> |
@@ -15,14 +15,14 @@ | @@ -15,14 +15,14 @@ | ||
15 | limitations under the License. | 15 | limitations under the License. |
16 | 16 | ||
17 | --> | 17 | --> |
18 | -<div hide-xs hide-sm class="tb-cell" flex="30">{{event.createdTime | date : 'yyyy-MM-dd HH:mm:ss'}}</div> | 18 | +<div hide-xs hide-sm class="tb-cell" flex="25">{{event.createdTime | date : 'yyyy-MM-dd HH:mm:ss'}}</div> |
19 | <div class="tb-cell" flex="20">{{event.body.server}}</div> | 19 | <div class="tb-cell" flex="20">{{event.body.server}}</div> |
20 | -<div class="tb-cell" flex="20">{{event.body.type}}</div> | ||
21 | -<div class="tb-cell" flex="20">{{event.body.entityName}}</div> | ||
22 | -<div class="tb-cell" flex="20">{{event.body.msgId}}</div> | ||
23 | -<div class="tb-cell" flex="20">{{event.body.msgType}}</div> | ||
24 | -<div class="tb-cell" flex="20">{{event.body.dataType}}</div> | ||
25 | -<div class="tb-cell" flex="20"> | 20 | +<div class="tb-cell" flex="10">{{event.body.type}}</div> |
21 | +<div class="tb-cell" flex="15">{{event.body.entityName}}</div> | ||
22 | +<div class="tb-cell tb-nowrap" flex="20" ng-mouseenter="checkTooltip($event)">{{event.body.msgId}}</div> | ||
23 | +<div class="tb-cell" flex="20" ng-mouseenter="checkTooltip($event)">{{event.body.msgType}}</div> | ||
24 | +<div class="tb-cell" flex="15">{{event.body.dataType}}</div> | ||
25 | +<div class="tb-cell" flex="10"> | ||
26 | <md-button ng-if="event.body.data" class="md-icon-button md-primary" | 26 | <md-button ng-if="event.body.data" class="md-icon-button md-primary" |
27 | ng-click="showContent($event, event.body.data, 'event.data', event.body.dataType)" | 27 | ng-click="showContent($event, event.body.data, 'event.data', event.body.dataType)" |
28 | aria-label="{{ 'action.view' | translate }}"> | 28 | aria-label="{{ 'action.view' | translate }}"> |
@@ -35,7 +35,7 @@ | @@ -35,7 +35,7 @@ | ||
35 | </md-icon> | 35 | </md-icon> |
36 | </md-button> | 36 | </md-button> |
37 | </div> | 37 | </div> |
38 | -<div class="tb-cell" flex="20"> | 38 | +<div class="tb-cell" flex="10"> |
39 | <md-button ng-if="event.body.metadata" class="md-icon-button md-primary" | 39 | <md-button ng-if="event.body.metadata" class="md-icon-button md-primary" |
40 | ng-click="showContent($event, event.body.metadata, 'event.metadata', 'JSON')" | 40 | ng-click="showContent($event, event.body.metadata, 'event.metadata', 'JSON')" |
41 | aria-label="{{ 'action.view' | translate }}"> | 41 | aria-label="{{ 'action.view' | translate }}"> |
@@ -48,7 +48,7 @@ | @@ -48,7 +48,7 @@ | ||
48 | </md-icon> | 48 | </md-icon> |
49 | </md-button> | 49 | </md-button> |
50 | </div> | 50 | </div> |
51 | -<div class="tb-cell" flex="20"> | 51 | +<div class="tb-cell" flex="10"> |
52 | <md-button ng-if="event.body.error" class="md-icon-button md-primary" | 52 | <md-button ng-if="event.body.error" class="md-icon-button md-primary" |
53 | ng-click="showContent($event, event.body.error, 'event.error')" | 53 | ng-click="showContent($event, event.body.error, 'event.error')" |
54 | aria-label="{{ 'action.view' | translate }}"> | 54 | aria-label="{{ 'action.view' | translate }}"> |
@@ -86,6 +86,14 @@ export default function EventRowDirective($compile, $templateCache, $mdDialog, $ | @@ -86,6 +86,14 @@ export default function EventRowDirective($compile, $templateCache, $mdDialog, $ | ||
86 | }); | 86 | }); |
87 | } | 87 | } |
88 | 88 | ||
89 | + scope.checkTooltip = function($event) { | ||
90 | + var el = $event.target; | ||
91 | + var $el = angular.element(el); | ||
92 | + if(el.offsetWidth < el.scrollWidth && !$el.attr('title')){ | ||
93 | + $el.attr('title', $el.text()); | ||
94 | + } | ||
95 | + } | ||
96 | + | ||
89 | $compile(element.contents())(scope); | 97 | $compile(element.contents())(scope); |
90 | } | 98 | } |
91 | 99 |
@@ -24,6 +24,17 @@ md-list.tb-event-table { | @@ -24,6 +24,17 @@ md-list.tb-event-table { | ||
24 | height: 48px; | 24 | height: 48px; |
25 | padding: 0px; | 25 | padding: 0px; |
26 | overflow: hidden; | 26 | overflow: hidden; |
27 | + .tb-cell { | ||
28 | + text-overflow: ellipsis; | ||
29 | + &.tb-scroll { | ||
30 | + white-space: nowrap; | ||
31 | + overflow-y: hidden; | ||
32 | + overflow-x: auto; | ||
33 | + } | ||
34 | + &.tb-nowrap { | ||
35 | + white-space: nowrap; | ||
36 | + } | ||
37 | + } | ||
27 | } | 38 | } |
28 | 39 | ||
29 | .tb-row:hover { | 40 | .tb-row:hover { |
@@ -39,13 +50,19 @@ md-list.tb-event-table { | @@ -39,13 +50,19 @@ md-list.tb-event-table { | ||
39 | color: rgba(0,0,0,.54); | 50 | color: rgba(0,0,0,.54); |
40 | font-size: 12px; | 51 | font-size: 12px; |
41 | font-weight: 700; | 52 | font-weight: 700; |
42 | - white-space: nowrap; | ||
43 | background: none; | 53 | background: none; |
54 | + white-space: nowrap; | ||
44 | } | 55 | } |
45 | } | 56 | } |
46 | 57 | ||
47 | .tb-cell { | 58 | .tb-cell { |
48 | - padding: 0 24px; | 59 | + &:first-child { |
60 | + padding-left: 14px; | ||
61 | + } | ||
62 | + &:last-child { | ||
63 | + padding-right: 14px; | ||
64 | + } | ||
65 | + padding: 0 6px; | ||
49 | margin: auto 0; | 66 | margin: auto 0; |
50 | color: rgba(0,0,0,.87); | 67 | color: rgba(0,0,0,.87); |
51 | font-size: 13px; | 68 | font-size: 13px; |
@@ -53,8 +70,8 @@ md-list.tb-event-table { | @@ -53,8 +70,8 @@ md-list.tb-event-table { | ||
53 | text-align: left; | 70 | text-align: left; |
54 | overflow: hidden; | 71 | overflow: hidden; |
55 | .md-button { | 72 | .md-button { |
56 | - padding: 0; | ||
57 | - margin: 0; | 73 | + padding: 0; |
74 | + margin: 0; | ||
58 | } | 75 | } |
59 | } | 76 | } |
60 | 77 |
@@ -281,39 +281,63 @@ export default function ImportExport($log, $translate, $q, $mdDialog, $document, | @@ -281,39 +281,63 @@ export default function ImportExport($log, $translate, $q, $mdDialog, $document, | ||
281 | 281 | ||
282 | function exportRuleChain(ruleChainId) { | 282 | function exportRuleChain(ruleChainId) { |
283 | ruleChainService.getRuleChain(ruleChainId).then( | 283 | ruleChainService.getRuleChain(ruleChainId).then( |
284 | - function success(ruleChain) { | ||
285 | - var name = ruleChain.name; | ||
286 | - name = name.toLowerCase().replace(/\W/g,"_"); | ||
287 | - exportToPc(prepareExport(ruleChain), name + '.json'); | ||
288 | - //TODO: metadata | 284 | + (ruleChain) => { |
285 | + ruleChainService.getRuleChainMetaData(ruleChainId).then( | ||
286 | + (ruleChainMetaData) => { | ||
287 | + var ruleChainExport = { | ||
288 | + ruleChain: prepareRuleChain(ruleChain), | ||
289 | + metadata: prepareRuleChainMetaData(ruleChainMetaData) | ||
290 | + }; | ||
291 | + var name = ruleChain.name; | ||
292 | + name = name.toLowerCase().replace(/\W/g,"_"); | ||
293 | + exportToPc(ruleChainExport, name + '.json'); | ||
294 | + }, | ||
295 | + (rejection) => { | ||
296 | + processExportRuleChainRejection(rejection); | ||
297 | + } | ||
298 | + ); | ||
289 | }, | 299 | }, |
290 | - function fail(rejection) { | ||
291 | - var message = rejection; | ||
292 | - if (!message) { | ||
293 | - message = $translate.instant('error.unknown-error'); | ||
294 | - } | ||
295 | - toast.showError($translate.instant('rulechain.export-failed-error', {error: message})); | 300 | + (rejection) => { |
301 | + processExportRuleChainRejection(rejection); | ||
296 | } | 302 | } |
297 | ); | 303 | ); |
298 | } | 304 | } |
299 | 305 | ||
306 | + function prepareRuleChain(ruleChain) { | ||
307 | + ruleChain = prepareExport(ruleChain); | ||
308 | + if (ruleChain.firstRuleNodeId) { | ||
309 | + ruleChain.firstRuleNodeId = null; | ||
310 | + } | ||
311 | + ruleChain.root = false; | ||
312 | + return ruleChain; | ||
313 | + } | ||
314 | + | ||
315 | + function prepareRuleChainMetaData(ruleChainMetaData) { | ||
316 | + delete ruleChainMetaData.ruleChainId; | ||
317 | + for (var i=0;i<ruleChainMetaData.nodes.length;i++) { | ||
318 | + var node = ruleChainMetaData.nodes[i]; | ||
319 | + ruleChainMetaData.nodes[i] = prepareExport(node); | ||
320 | + } | ||
321 | + return ruleChainMetaData; | ||
322 | + } | ||
323 | + | ||
324 | + function processExportRuleChainRejection(rejection) { | ||
325 | + var message = rejection; | ||
326 | + if (!message) { | ||
327 | + message = $translate.instant('error.unknown-error'); | ||
328 | + } | ||
329 | + toast.showError($translate.instant('rulechain.export-failed-error', {error: message})); | ||
330 | + } | ||
331 | + | ||
300 | function importRuleChain($event) { | 332 | function importRuleChain($event) { |
301 | var deferred = $q.defer(); | 333 | var deferred = $q.defer(); |
302 | openImportDialog($event, 'rulechain.import', 'rulechain.rulechain-file').then( | 334 | openImportDialog($event, 'rulechain.import', 'rulechain.rulechain-file').then( |
303 | - function success(ruleChain) { | ||
304 | - if (!validateImportedRuleChain(ruleChain)) { | 335 | + function success(ruleChainImport) { |
336 | + if (!validateImportedRuleChain(ruleChainImport)) { | ||
305 | toast.showError($translate.instant('rulechain.invalid-rulechain-file-error')); | 337 | toast.showError($translate.instant('rulechain.invalid-rulechain-file-error')); |
306 | deferred.reject(); | 338 | deferred.reject(); |
307 | } else { | 339 | } else { |
308 | - //TODO: rulechain metadata | ||
309 | - ruleChainService.saveRuleChain(ruleChain).then( | ||
310 | - function success() { | ||
311 | - deferred.resolve(); | ||
312 | - }, | ||
313 | - function fail() { | ||
314 | - deferred.reject(); | ||
315 | - } | ||
316 | - ); | 340 | + deferred.resolve(ruleChainImport); |
317 | } | 341 | } |
318 | }, | 342 | }, |
319 | function fail() { | 343 | function fail() { |
@@ -323,10 +347,14 @@ export default function ImportExport($log, $translate, $q, $mdDialog, $document, | @@ -323,10 +347,14 @@ export default function ImportExport($log, $translate, $q, $mdDialog, $document, | ||
323 | return deferred.promise; | 347 | return deferred.promise; |
324 | } | 348 | } |
325 | 349 | ||
326 | - function validateImportedRuleChain(ruleChain) { | ||
327 | - //TODO: rulechain metadata | ||
328 | - if (angular.isUndefined(ruleChain.name)) | ||
329 | - { | 350 | + function validateImportedRuleChain(ruleChainImport) { |
351 | + if (angular.isUndefined(ruleChainImport.ruleChain)) { | ||
352 | + return false; | ||
353 | + } | ||
354 | + if (angular.isUndefined(ruleChainImport.metadata)) { | ||
355 | + return false; | ||
356 | + } | ||
357 | + if (angular.isUndefined(ruleChainImport.ruleChain.name)) { | ||
330 | return false; | 358 | return false; |
331 | } | 359 | } |
332 | return true; | 360 | return true; |
@@ -43,6 +43,7 @@ export default angular.module('thingsboard.locale', []) | @@ -43,6 +43,7 @@ export default angular.module('thingsboard.locale', []) | ||
43 | "update": "Update", | 43 | "update": "Update", |
44 | "remove": "Remove", | 44 | "remove": "Remove", |
45 | "search": "Search", | 45 | "search": "Search", |
46 | + "clear-search": "Clear search", | ||
46 | "assign": "Assign", | 47 | "assign": "Assign", |
47 | "unassign": "Unassign", | 48 | "unassign": "Unassign", |
48 | "share": "Share", | 49 | "share": "Share", |
@@ -1174,7 +1175,7 @@ export default angular.module('thingsboard.locale', []) | @@ -1174,7 +1175,7 @@ export default angular.module('thingsboard.locale', []) | ||
1174 | "export": "Export rule chain", | 1175 | "export": "Export rule chain", |
1175 | "export-failed-error": "Unable to export rule chain: {{error}}", | 1176 | "export-failed-error": "Unable to export rule chain: {{error}}", |
1176 | "create-new-rulechain": "Create new rule chain", | 1177 | "create-new-rulechain": "Create new rule chain", |
1177 | - "rule-file": "Rule chain file", | 1178 | + "rulechain-file": "Rule chain file", |
1178 | "invalid-rulechain-file-error": "Unable to import rule chain: Invalid rule chain data structure.", | 1179 | "invalid-rulechain-file-error": "Unable to import rule chain: Invalid rule chain data structure.", |
1179 | "copyId": "Copy rule chain Id", | 1180 | "copyId": "Copy rule chain Id", |
1180 | "idCopiedMessage": "Rule chain Id has been copied to clipboard", | 1181 | "idCopiedMessage": "Rule chain Id has been copied to clipboard", |
@@ -1188,6 +1189,7 @@ export default angular.module('thingsboard.locale', []) | @@ -1188,6 +1189,7 @@ export default angular.module('thingsboard.locale', []) | ||
1188 | "details": "Details", | 1189 | "details": "Details", |
1189 | "events": "Events", | 1190 | "events": "Events", |
1190 | "search": "Search nodes", | 1191 | "search": "Search nodes", |
1192 | + "open-node-library": "Open node library", | ||
1191 | "add": "Add rule node", | 1193 | "add": "Add rule node", |
1192 | "name": "Name", | 1194 | "name": "Name", |
1193 | "name-required": "Name is required.", | 1195 | "name-required": "Name is required.", |
@@ -1217,7 +1219,8 @@ export default angular.module('thingsboard.locale', []) | @@ -1217,7 +1219,8 @@ export default angular.module('thingsboard.locale', []) | ||
1217 | "type-rule-chain": "Rule Chain", | 1219 | "type-rule-chain": "Rule Chain", |
1218 | "type-rule-chain-details": "Forwards incoming messages to specified Rule Chain", | 1220 | "type-rule-chain-details": "Forwards incoming messages to specified Rule Chain", |
1219 | "directive-is-not-loaded": "Defined configuration directive '{{directiveName}}' is not available.", | 1221 | "directive-is-not-loaded": "Defined configuration directive '{{directiveName}}' is not available.", |
1220 | - "ui-resources-load-error": "Failed to load configuration ui resources." | 1222 | + "ui-resources-load-error": "Failed to load configuration ui resources.", |
1223 | + "invalid-target-rulechain": "Unable to resolve target rule chain!" | ||
1221 | }, | 1224 | }, |
1222 | "rule-plugin": { | 1225 | "rule-plugin": { |
1223 | "management": "Rules and plugins management" | 1226 | "management": "Rules and plugins management" |
@@ -28,7 +28,7 @@ import addRuleNodeLinkTemplate from './add-link.tpl.html'; | @@ -28,7 +28,7 @@ import addRuleNodeLinkTemplate from './add-link.tpl.html'; | ||
28 | /* eslint-enable import/no-unresolved, import/default */ | 28 | /* eslint-enable import/no-unresolved, import/default */ |
29 | 29 | ||
30 | /*@ngInject*/ | 30 | /*@ngInject*/ |
31 | -export function RuleChainController($stateParams, $scope, $compile, $q, $mdUtil, $timeout, $mdExpansionPanel, $window, $document, $mdDialog, | 31 | +export function RuleChainController($state, $scope, $compile, $q, $mdUtil, $timeout, $mdExpansionPanel, $window, $document, $mdDialog, |
32 | $filter, $translate, hotkeys, types, ruleChainService, Modelfactory, flowchartConstants, | 32 | $filter, $translate, hotkeys, types, ruleChainService, Modelfactory, flowchartConstants, |
33 | ruleChain, ruleChainMetaData, ruleNodeComponents) { | 33 | ruleChain, ruleChainMetaData, ruleNodeComponents) { |
34 | 34 | ||
@@ -37,6 +37,24 @@ export function RuleChainController($stateParams, $scope, $compile, $q, $mdUtil, | @@ -37,6 +37,24 @@ export function RuleChainController($stateParams, $scope, $compile, $q, $mdUtil, | ||
37 | vm.$mdExpansionPanel = $mdExpansionPanel; | 37 | vm.$mdExpansionPanel = $mdExpansionPanel; |
38 | vm.types = types; | 38 | vm.types = types; |
39 | 39 | ||
40 | + if ($state.current.data.import && !ruleChain) { | ||
41 | + $state.go('home.ruleChains'); | ||
42 | + return; | ||
43 | + } | ||
44 | + | ||
45 | + vm.isImport = $state.current.data.import; | ||
46 | + vm.isConfirmOnExit = false; | ||
47 | + | ||
48 | + $scope.$watch(function() { | ||
49 | + return vm.isDirty || vm.isImport; | ||
50 | + }, (val) => { | ||
51 | + vm.isConfirmOnExit = val; | ||
52 | + }); | ||
53 | + | ||
54 | + vm.errorTooltips = {}; | ||
55 | + | ||
56 | + vm.isFullscreen = false; | ||
57 | + | ||
40 | vm.editingRuleNode = null; | 58 | vm.editingRuleNode = null; |
41 | vm.isEditingRuleNode = false; | 59 | vm.isEditingRuleNode = false; |
42 | 60 | ||
@@ -57,6 +75,7 @@ export function RuleChainController($stateParams, $scope, $compile, $q, $mdUtil, | @@ -57,6 +75,7 @@ export function RuleChainController($stateParams, $scope, $compile, $q, $mdUtil, | ||
57 | }; | 75 | }; |
58 | 76 | ||
59 | vm.ruleNodeTypesModel = {}; | 77 | vm.ruleNodeTypesModel = {}; |
78 | + vm.ruleNodeTypesCanvasControl = {}; | ||
60 | vm.ruleChainLibraryLoaded = false; | 79 | vm.ruleChainLibraryLoaded = false; |
61 | for (var type in types.ruleNodeType) { | 80 | for (var type in types.ruleNodeType) { |
62 | if (!types.ruleNodeType[type].special) { | 81 | if (!types.ruleNodeType[type].special) { |
@@ -67,9 +86,12 @@ export function RuleChainController($stateParams, $scope, $compile, $q, $mdUtil, | @@ -67,9 +86,12 @@ export function RuleChainController($stateParams, $scope, $compile, $q, $mdUtil, | ||
67 | }, | 86 | }, |
68 | selectedObjects: [] | 87 | selectedObjects: [] |
69 | }; | 88 | }; |
89 | + vm.ruleNodeTypesCanvasControl[type] = {}; | ||
70 | } | 90 | } |
71 | } | 91 | } |
72 | 92 | ||
93 | + | ||
94 | + | ||
73 | vm.selectedObjects = []; | 95 | vm.selectedObjects = []; |
74 | 96 | ||
75 | vm.modelservice = Modelfactory(vm.ruleChainModel, vm.selectedObjects); | 97 | vm.modelservice = Modelfactory(vm.ruleChainModel, vm.selectedObjects); |
@@ -145,8 +167,12 @@ export function RuleChainController($stateParams, $scope, $compile, $q, $mdUtil, | @@ -145,8 +167,12 @@ export function RuleChainController($stateParams, $scope, $compile, $q, $mdUtil, | ||
145 | $scope.$broadcast('form-submit'); | 167 | $scope.$broadcast('form-submit'); |
146 | if (theForm.$valid) { | 168 | if (theForm.$valid) { |
147 | theForm.$setPristine(); | 169 | theForm.$setPristine(); |
170 | + if (vm.editingRuleNode.error) { | ||
171 | + delete vm.editingRuleNode.error; | ||
172 | + } | ||
148 | vm.ruleChainModel.nodes[vm.editingRuleNodeIndex] = vm.editingRuleNode; | 173 | vm.ruleChainModel.nodes[vm.editingRuleNodeIndex] = vm.editingRuleNode; |
149 | vm.editingRuleNode = angular.copy(vm.editingRuleNode); | 174 | vm.editingRuleNode = angular.copy(vm.editingRuleNode); |
175 | + updateRuleNodesHighlight(); | ||
150 | } | 176 | } |
151 | }; | 177 | }; |
152 | 178 | ||
@@ -203,7 +229,9 @@ export function RuleChainController($stateParams, $scope, $compile, $q, $mdUtil, | @@ -203,7 +229,9 @@ export function RuleChainController($stateParams, $scope, $compile, $q, $mdUtil, | ||
203 | } | 229 | } |
204 | var instances = angular.element.tooltipster.instances(); | 230 | var instances = angular.element.tooltipster.instances(); |
205 | instances.forEach((instance) => { | 231 | instances.forEach((instance) => { |
206 | - instance.destroy(); | 232 | + if (!instance.isErrorTooltip) { |
233 | + instance.destroy(); | ||
234 | + } | ||
207 | }); | 235 | }); |
208 | } | 236 | } |
209 | 237 | ||
@@ -249,6 +277,71 @@ export function RuleChainController($stateParams, $scope, $compile, $q, $mdUtil, | @@ -249,6 +277,71 @@ export function RuleChainController($stateParams, $scope, $compile, $q, $mdUtil, | ||
249 | }, 500); | 277 | }, 500); |
250 | } | 278 | } |
251 | 279 | ||
280 | + function updateNodeErrorTooltip(node) { | ||
281 | + if (node.error) { | ||
282 | + var element = angular.element('#' + node.id); | ||
283 | + var tooltip = vm.errorTooltips[node.id]; | ||
284 | + if (!tooltip || !element.hasClass("tooltipstered")) { | ||
285 | + element.tooltipster( | ||
286 | + { | ||
287 | + theme: 'tooltipster-shadow', | ||
288 | + delay: 0, | ||
289 | + animationDuration: 0, | ||
290 | + trigger: 'custom', | ||
291 | + triggerOpen: { | ||
292 | + click: false, | ||
293 | + tap: false | ||
294 | + }, | ||
295 | + triggerClose: { | ||
296 | + click: false, | ||
297 | + tap: false, | ||
298 | + scroll: false | ||
299 | + }, | ||
300 | + side: 'top', | ||
301 | + trackOrigin: true | ||
302 | + } | ||
303 | + ); | ||
304 | + var content = '<div class="tb-rule-node-error-tooltip">' + | ||
305 | + '<div id="tooltip-content" layout="column">' + | ||
306 | + '<div class="tb-node-details">' + node.error + '</div>' + | ||
307 | + '</div>' + | ||
308 | + '</div>'; | ||
309 | + var contentElement = angular.element(content); | ||
310 | + $compile(contentElement)($scope); | ||
311 | + tooltip = element.tooltipster('instance'); | ||
312 | + tooltip.isErrorTooltip = true; | ||
313 | + tooltip.content(contentElement); | ||
314 | + vm.errorTooltips[node.id] = tooltip; | ||
315 | + } | ||
316 | + $mdUtil.nextTick(() => { | ||
317 | + tooltip.open(); | ||
318 | + }); | ||
319 | + } else { | ||
320 | + if (vm.errorTooltips[node.id]) { | ||
321 | + tooltip = vm.errorTooltips[node.id]; | ||
322 | + tooltip.destroy(); | ||
323 | + delete vm.errorTooltips[node.id]; | ||
324 | + } | ||
325 | + } | ||
326 | + } | ||
327 | + | ||
328 | + function updateErrorTooltips(hide) { | ||
329 | + for (var nodeId in vm.errorTooltips) { | ||
330 | + var tooltip = vm.errorTooltips[nodeId]; | ||
331 | + if (hide) { | ||
332 | + tooltip.close(); | ||
333 | + } else { | ||
334 | + tooltip.open(); | ||
335 | + } | ||
336 | + } | ||
337 | + } | ||
338 | + | ||
339 | + $scope.$watch(function() { | ||
340 | + return vm.isEditingRuleNode || vm.isEditingRuleNodeLink; | ||
341 | + }, (val) => { | ||
342 | + updateErrorTooltips(val); | ||
343 | + }); | ||
344 | + | ||
252 | vm.editCallbacks = { | 345 | vm.editCallbacks = { |
253 | edgeDoubleClick: function (event, edge) { | 346 | edgeDoubleClick: function (event, edge) { |
254 | var sourceNode = vm.modelservice.nodes.getNodeByConnectorId(edge.source); | 347 | var sourceNode = vm.modelservice.nodes.getNodeByConnectorId(edge.source); |
@@ -313,12 +406,28 @@ export function RuleChainController($stateParams, $scope, $compile, $q, $mdUtil, | @@ -313,12 +406,28 @@ export function RuleChainController($stateParams, $scope, $compile, $q, $mdUtil, | ||
313 | } | 406 | } |
314 | }; | 407 | }; |
315 | 408 | ||
316 | - loadRuleChainLibrary(); | 409 | + loadRuleChainLibrary(ruleNodeComponents, true); |
410 | + | ||
411 | + $scope.$watch('vm.ruleNodeSearch', | ||
412 | + function (newVal, oldVal) { | ||
413 | + if (!angular.equals(newVal, oldVal)) { | ||
414 | + var res = $filter('filter')(ruleNodeComponents, {name: vm.ruleNodeSearch}); | ||
415 | + loadRuleChainLibrary(res); | ||
416 | + } | ||
417 | + } | ||
418 | + ); | ||
419 | + | ||
420 | + $scope.$on('searchTextUpdated', function () { | ||
421 | + updateRuleNodesHighlight(); | ||
422 | + }); | ||
317 | 423 | ||
318 | - function loadRuleChainLibrary() { | 424 | + function loadRuleChainLibrary(ruleNodeComponents, loadRuleChain) { |
425 | + for (var componentType in vm.ruleNodeTypesModel) { | ||
426 | + vm.ruleNodeTypesModel[componentType].model.nodes.length = 0; | ||
427 | + } | ||
319 | for (var i=0;i<ruleNodeComponents.length;i++) { | 428 | for (var i=0;i<ruleNodeComponents.length;i++) { |
320 | var ruleNodeComponent = ruleNodeComponents[i]; | 429 | var ruleNodeComponent = ruleNodeComponents[i]; |
321 | - var componentType = ruleNodeComponent.type; | 430 | + componentType = ruleNodeComponent.type; |
322 | var model = vm.ruleNodeTypesModel[componentType].model; | 431 | var model = vm.ruleNodeTypesModel[componentType].model; |
323 | var node = { | 432 | var node = { |
324 | id: 'node-lib-' + componentType + '-' + model.nodes.length, | 433 | id: 'node-lib-' + componentType + '-' + model.nodes.length, |
@@ -349,7 +458,26 @@ export function RuleChainController($stateParams, $scope, $compile, $q, $mdUtil, | @@ -349,7 +458,26 @@ export function RuleChainController($stateParams, $scope, $compile, $q, $mdUtil, | ||
349 | model.nodes.push(node); | 458 | model.nodes.push(node); |
350 | } | 459 | } |
351 | vm.ruleChainLibraryLoaded = true; | 460 | vm.ruleChainLibraryLoaded = true; |
352 | - prepareRuleChain(); | 461 | + if (loadRuleChain) { |
462 | + prepareRuleChain(); | ||
463 | + } | ||
464 | + $mdUtil.nextTick(() => { | ||
465 | + for (componentType in vm.ruleNodeTypesCanvasControl) { | ||
466 | + if (vm.ruleNodeTypesCanvasControl[componentType].adjustCanvasSize) { | ||
467 | + vm.ruleNodeTypesCanvasControl[componentType].adjustCanvasSize(true); | ||
468 | + } | ||
469 | + } | ||
470 | + for (componentType in vm.ruleNodeTypesModel) { | ||
471 | + var panel = vm.$mdExpansionPanel(componentType); | ||
472 | + if (panel) { | ||
473 | + if (!vm.ruleNodeTypesModel[componentType].model.nodes.length) { | ||
474 | + panel.collapse(); | ||
475 | + } else { | ||
476 | + panel.expand(); | ||
477 | + } | ||
478 | + } | ||
479 | + } | ||
480 | + }); | ||
353 | } | 481 | } |
354 | 482 | ||
355 | function prepareRuleChain() { | 483 | function prepareRuleChain() { |
@@ -480,11 +608,9 @@ export function RuleChainController($stateParams, $scope, $compile, $q, $mdUtil, | @@ -480,11 +608,9 @@ export function RuleChainController($stateParams, $scope, $compile, $q, $mdUtil, | ||
480 | ruleChainNode = { | 608 | ruleChainNode = { |
481 | id: 'rule-chain-node-' + vm.nextNodeID++, | 609 | id: 'rule-chain-node-' + vm.nextNodeID++, |
482 | additionalInfo: ruleChainConnection.additionalInfo, | 610 | additionalInfo: ruleChainConnection.additionalInfo, |
483 | - targetRuleChainId: ruleChainConnection.targetRuleChainId.id, | ||
484 | x: ruleChainConnection.additionalInfo.layoutX, | 611 | x: ruleChainConnection.additionalInfo.layoutX, |
485 | y: ruleChainConnection.additionalInfo.layoutY, | 612 | y: ruleChainConnection.additionalInfo.layoutY, |
486 | component: types.ruleChainNodeComponent, | 613 | component: types.ruleChainNodeComponent, |
487 | - name: ruleChain.name, | ||
488 | nodeClass: vm.types.ruleNodeType.RULE_CHAIN.nodeClass, | 614 | nodeClass: vm.types.ruleNodeType.RULE_CHAIN.nodeClass, |
489 | icon: vm.types.ruleNodeType.RULE_CHAIN.icon, | 615 | icon: vm.types.ruleNodeType.RULE_CHAIN.icon, |
490 | connectors: [ | 616 | connectors: [ |
@@ -494,6 +620,14 @@ export function RuleChainController($stateParams, $scope, $compile, $q, $mdUtil, | @@ -494,6 +620,14 @@ export function RuleChainController($stateParams, $scope, $compile, $q, $mdUtil, | ||
494 | } | 620 | } |
495 | ] | 621 | ] |
496 | }; | 622 | }; |
623 | + if (ruleChain.name) { | ||
624 | + ruleChainNode.name = ruleChain.name; | ||
625 | + ruleChainNode.targetRuleChainId = ruleChainConnection.targetRuleChainId.id; | ||
626 | + } else { | ||
627 | + ruleChainNode.name = "Unresolved"; | ||
628 | + ruleChainNode.targetRuleChainId = null; | ||
629 | + ruleChainNode.error = $translate.instant('rulenode.invalid-target-rulechain'); | ||
630 | + } | ||
497 | ruleChainNodesMap[ruleChainConnection.additionalInfo.ruleChainNodeId] = ruleChainNode; | 631 | ruleChainNodesMap[ruleChainConnection.additionalInfo.ruleChainNodeId] = ruleChainNode; |
498 | vm.ruleChainModel.nodes.push(ruleChainNode); | 632 | vm.ruleChainModel.nodes.push(ruleChainNode); |
499 | } | 633 | } |
@@ -519,89 +653,141 @@ export function RuleChainController($stateParams, $scope, $compile, $q, $mdUtil, | @@ -519,89 +653,141 @@ export function RuleChainController($stateParams, $scope, $compile, $q, $mdUtil, | ||
519 | 653 | ||
520 | vm.isDirty = false; | 654 | vm.isDirty = false; |
521 | 655 | ||
656 | + updateRuleNodesHighlight(); | ||
657 | + | ||
658 | + validate(); | ||
659 | + | ||
522 | $mdUtil.nextTick(() => { | 660 | $mdUtil.nextTick(() => { |
523 | vm.ruleChainWatch = $scope.$watch('vm.ruleChainModel', | 661 | vm.ruleChainWatch = $scope.$watch('vm.ruleChainModel', |
524 | function (newVal, oldVal) { | 662 | function (newVal, oldVal) { |
525 | - if (!vm.isDirty && !angular.equals(newVal, oldVal)) { | ||
526 | - vm.isDirty = true; | 663 | + if (!angular.equals(newVal, oldVal)) { |
664 | + validate(); | ||
665 | + if (!vm.isDirty) { | ||
666 | + vm.isDirty = true; | ||
667 | + } | ||
527 | } | 668 | } |
528 | }, true | 669 | }, true |
529 | ); | 670 | ); |
530 | }); | 671 | }); |
531 | } | 672 | } |
532 | 673 | ||
674 | + function updateRuleNodesHighlight() { | ||
675 | + for (var i = 0; i < vm.ruleChainModel.nodes.length; i++) { | ||
676 | + vm.ruleChainModel.nodes[i].highlighted = false; | ||
677 | + } | ||
678 | + if ($scope.searchConfig.searchText) { | ||
679 | + var res = $filter('filter')(vm.ruleChainModel.nodes, {name: $scope.searchConfig.searchText}); | ||
680 | + if (res) { | ||
681 | + for (i = 0; i < res.length; i++) { | ||
682 | + res[i].highlighted = true; | ||
683 | + } | ||
684 | + } | ||
685 | + } | ||
686 | + } | ||
687 | + | ||
688 | + function validate() { | ||
689 | + $mdUtil.nextTick(() => { | ||
690 | + vm.isInvalid = false; | ||
691 | + for (var i = 0; i < vm.ruleChainModel.nodes.length; i++) { | ||
692 | + if (vm.ruleChainModel.nodes[i].error) { | ||
693 | + vm.isInvalid = true; | ||
694 | + } | ||
695 | + updateNodeErrorTooltip(vm.ruleChainModel.nodes[i]); | ||
696 | + } | ||
697 | + }); | ||
698 | + } | ||
699 | + | ||
533 | function saveRuleChain() { | 700 | function saveRuleChain() { |
534 | - var ruleChainMetaData = { | ||
535 | - ruleChainId: vm.ruleChain.id, | ||
536 | - nodes: [], | ||
537 | - connections: [], | ||
538 | - ruleChainConnections: [] | ||
539 | - }; | 701 | + var saveRuleChainPromise; |
702 | + if (vm.isImport) { | ||
703 | + saveRuleChainPromise = ruleChainService.saveRuleChain(vm.ruleChain); | ||
704 | + } else { | ||
705 | + saveRuleChainPromise = $q.when(vm.ruleChain); | ||
706 | + } | ||
707 | + saveRuleChainPromise.then( | ||
708 | + (ruleChain) => { | ||
709 | + vm.ruleChain = ruleChain; | ||
710 | + var ruleChainMetaData = { | ||
711 | + ruleChainId: vm.ruleChain.id, | ||
712 | + nodes: [], | ||
713 | + connections: [], | ||
714 | + ruleChainConnections: [] | ||
715 | + }; | ||
540 | 716 | ||
541 | - var nodes = []; | 717 | + var nodes = []; |
542 | 718 | ||
543 | - for (var i=0;i<vm.ruleChainModel.nodes.length;i++) { | ||
544 | - var node = vm.ruleChainModel.nodes[i]; | ||
545 | - if (node.component.type != types.ruleNodeType.INPUT.value && node.component.type != types.ruleNodeType.RULE_CHAIN.value) { | ||
546 | - var ruleNode = {}; | ||
547 | - if (node.ruleNodeId) { | ||
548 | - ruleNode.id = node.ruleNodeId; | 719 | + for (var i=0;i<vm.ruleChainModel.nodes.length;i++) { |
720 | + var node = vm.ruleChainModel.nodes[i]; | ||
721 | + if (node.component.type != types.ruleNodeType.INPUT.value && node.component.type != types.ruleNodeType.RULE_CHAIN.value) { | ||
722 | + var ruleNode = {}; | ||
723 | + if (node.ruleNodeId) { | ||
724 | + ruleNode.id = node.ruleNodeId; | ||
725 | + } | ||
726 | + ruleNode.type = node.component.clazz; | ||
727 | + ruleNode.name = node.name; | ||
728 | + ruleNode.configuration = node.configuration; | ||
729 | + ruleNode.additionalInfo = node.additionalInfo; | ||
730 | + ruleNode.debugMode = node.debugMode; | ||
731 | + if (!ruleNode.additionalInfo) { | ||
732 | + ruleNode.additionalInfo = {}; | ||
733 | + } | ||
734 | + ruleNode.additionalInfo.layoutX = node.x; | ||
735 | + ruleNode.additionalInfo.layoutY = node.y; | ||
736 | + ruleChainMetaData.nodes.push(ruleNode); | ||
737 | + nodes.push(node); | ||
738 | + } | ||
549 | } | 739 | } |
550 | - ruleNode.type = node.component.clazz; | ||
551 | - ruleNode.name = node.name; | ||
552 | - ruleNode.configuration = node.configuration; | ||
553 | - ruleNode.additionalInfo = node.additionalInfo; | ||
554 | - ruleNode.debugMode = node.debugMode; | ||
555 | - if (!ruleNode.additionalInfo) { | ||
556 | - ruleNode.additionalInfo = {}; | 740 | + var res = $filter('filter')(vm.ruleChainModel.edges, {source: vm.inputConnectorId}); |
741 | + if (res && res.length) { | ||
742 | + var firstNodeEdge = res[0]; | ||
743 | + var firstNode = vm.modelservice.nodes.getNodeByConnectorId(firstNodeEdge.destination); | ||
744 | + ruleChainMetaData.firstNodeIndex = nodes.indexOf(firstNode); | ||
557 | } | 745 | } |
558 | - ruleNode.additionalInfo.layoutX = node.x; | ||
559 | - ruleNode.additionalInfo.layoutY = node.y; | ||
560 | - ruleChainMetaData.nodes.push(ruleNode); | ||
561 | - nodes.push(node); | ||
562 | - } | ||
563 | - } | ||
564 | - var res = $filter('filter')(vm.ruleChainModel.edges, {source: vm.inputConnectorId}); | ||
565 | - if (res && res.length) { | ||
566 | - var firstNodeEdge = res[0]; | ||
567 | - var firstNode = vm.modelservice.nodes.getNodeByConnectorId(firstNodeEdge.destination); | ||
568 | - ruleChainMetaData.firstNodeIndex = nodes.indexOf(firstNode); | ||
569 | - } | ||
570 | - for (i=0;i<vm.ruleChainModel.edges.length;i++) { | ||
571 | - var edge = vm.ruleChainModel.edges[i]; | ||
572 | - var sourceNode = vm.modelservice.nodes.getNodeByConnectorId(edge.source); | ||
573 | - var destNode = vm.modelservice.nodes.getNodeByConnectorId(edge.destination); | ||
574 | - if (sourceNode.component.type != types.ruleNodeType.INPUT.value) { | ||
575 | - var fromIndex = nodes.indexOf(sourceNode); | ||
576 | - if (destNode.component.type == types.ruleNodeType.RULE_CHAIN.value) { | ||
577 | - var ruleChainConnection = { | ||
578 | - fromIndex: fromIndex, | ||
579 | - targetRuleChainId: {entityType: vm.types.entityType.rulechain, id: destNode.targetRuleChainId}, | ||
580 | - additionalInfo: destNode.additionalInfo, | ||
581 | - type: edge.label | ||
582 | - }; | ||
583 | - if (!ruleChainConnection.additionalInfo) { | ||
584 | - ruleChainConnection.additionalInfo = {}; | 746 | + for (i=0;i<vm.ruleChainModel.edges.length;i++) { |
747 | + var edge = vm.ruleChainModel.edges[i]; | ||
748 | + var sourceNode = vm.modelservice.nodes.getNodeByConnectorId(edge.source); | ||
749 | + var destNode = vm.modelservice.nodes.getNodeByConnectorId(edge.destination); | ||
750 | + if (sourceNode.component.type != types.ruleNodeType.INPUT.value) { | ||
751 | + var fromIndex = nodes.indexOf(sourceNode); | ||
752 | + if (destNode.component.type == types.ruleNodeType.RULE_CHAIN.value) { | ||
753 | + var ruleChainConnection = { | ||
754 | + fromIndex: fromIndex, | ||
755 | + targetRuleChainId: {entityType: vm.types.entityType.rulechain, id: destNode.targetRuleChainId}, | ||
756 | + additionalInfo: destNode.additionalInfo, | ||
757 | + type: edge.label | ||
758 | + }; | ||
759 | + if (!ruleChainConnection.additionalInfo) { | ||
760 | + ruleChainConnection.additionalInfo = {}; | ||
761 | + } | ||
762 | + ruleChainConnection.additionalInfo.layoutX = destNode.x; | ||
763 | + ruleChainConnection.additionalInfo.layoutY = destNode.y; | ||
764 | + ruleChainConnection.additionalInfo.ruleChainNodeId = destNode.id; | ||
765 | + ruleChainMetaData.ruleChainConnections.push(ruleChainConnection); | ||
766 | + } else { | ||
767 | + var toIndex = nodes.indexOf(destNode); | ||
768 | + var nodeConnection = { | ||
769 | + fromIndex: fromIndex, | ||
770 | + toIndex: toIndex, | ||
771 | + type: edge.label | ||
772 | + }; | ||
773 | + ruleChainMetaData.connections.push(nodeConnection); | ||
774 | + } | ||
585 | } | 775 | } |
586 | - ruleChainConnection.additionalInfo.layoutX = destNode.x; | ||
587 | - ruleChainConnection.additionalInfo.layoutY = destNode.y; | ||
588 | - ruleChainConnection.additionalInfo.ruleChainNodeId = destNode.id; | ||
589 | - ruleChainMetaData.ruleChainConnections.push(ruleChainConnection); | ||
590 | - } else { | ||
591 | - var toIndex = nodes.indexOf(destNode); | ||
592 | - var nodeConnection = { | ||
593 | - fromIndex: fromIndex, | ||
594 | - toIndex: toIndex, | ||
595 | - type: edge.label | ||
596 | - }; | ||
597 | - ruleChainMetaData.connections.push(nodeConnection); | ||
598 | } | 776 | } |
599 | - } | ||
600 | - } | ||
601 | - ruleChainService.saveRuleChainMetaData(ruleChainMetaData).then( | ||
602 | - (ruleChainMetaData) => { | ||
603 | - vm.ruleChainMetaData = ruleChainMetaData; | ||
604 | - prepareRuleChain(); | 777 | + ruleChainService.saveRuleChainMetaData(ruleChainMetaData).then( |
778 | + (ruleChainMetaData) => { | ||
779 | + vm.ruleChainMetaData = ruleChainMetaData; | ||
780 | + if (vm.isImport) { | ||
781 | + vm.isDirty = false; | ||
782 | + vm.isImport = false; | ||
783 | + $mdUtil.nextTick(() => { | ||
784 | + $state.go('home.ruleChains.ruleChain', {ruleChainId: vm.ruleChain.id.id}); | ||
785 | + }); | ||
786 | + } else { | ||
787 | + prepareRuleChain(); | ||
788 | + } | ||
789 | + } | ||
790 | + ); | ||
605 | } | 791 | } |
606 | ); | 792 | ); |
607 | } | 793 | } |
@@ -614,12 +800,14 @@ export function RuleChainController($stateParams, $scope, $compile, $q, $mdUtil, | @@ -614,12 +800,14 @@ export function RuleChainController($stateParams, $scope, $compile, $q, $mdUtil, | ||
614 | 800 | ||
615 | ruleNode.configuration = angular.copy(ruleNode.component.configurationDescriptor.nodeDefinition.defaultConfiguration); | 801 | ruleNode.configuration = angular.copy(ruleNode.component.configurationDescriptor.nodeDefinition.defaultConfiguration); |
616 | 802 | ||
803 | + var ruleChainId = vm.ruleChain.id ? vm.ruleChain.id.id : null; | ||
804 | + | ||
617 | $mdDialog.show({ | 805 | $mdDialog.show({ |
618 | controller: 'AddRuleNodeController', | 806 | controller: 'AddRuleNodeController', |
619 | controllerAs: 'vm', | 807 | controllerAs: 'vm', |
620 | templateUrl: addRuleNodeTemplate, | 808 | templateUrl: addRuleNodeTemplate, |
621 | parent: angular.element($document[0].body), | 809 | parent: angular.element($document[0].body), |
622 | - locals: {ruleNode: ruleNode, ruleChainId: vm.ruleChain.id.id}, | 810 | + locals: {ruleNode: ruleNode, ruleChainId: ruleChainId}, |
623 | fullscreen: true, | 811 | fullscreen: true, |
624 | targetEvent: $event | 812 | targetEvent: $event |
625 | }).then(function (ruleNode) { | 813 | }).then(function (ruleNode) { |
@@ -642,6 +830,7 @@ export function RuleChainController($stateParams, $scope, $compile, $q, $mdUtil, | @@ -642,6 +830,7 @@ export function RuleChainController($stateParams, $scope, $compile, $q, $mdUtil, | ||
642 | ); | 830 | ); |
643 | } | 831 | } |
644 | vm.ruleChainModel.nodes.push(ruleNode); | 832 | vm.ruleChainModel.nodes.push(ruleNode); |
833 | + updateRuleNodesHighlight(); | ||
645 | }, function () { | 834 | }, function () { |
646 | }); | 835 | }); |
647 | } | 836 | } |
@@ -76,11 +76,52 @@ export default function RuleChainRoutes($stateProvider, NodeTemplatePathProvider | @@ -76,11 +76,52 @@ export default function RuleChainRoutes($stateProvider, NodeTemplatePathProvider | ||
76 | } | 76 | } |
77 | }, | 77 | }, |
78 | data: { | 78 | data: { |
79 | - searchEnabled: false, | 79 | + import: false, |
80 | + searchEnabled: true, | ||
80 | pageTitle: 'rulechain.rulechain' | 81 | pageTitle: 'rulechain.rulechain' |
81 | }, | 82 | }, |
82 | ncyBreadcrumb: { | 83 | ncyBreadcrumb: { |
83 | label: '{"icon": "settings_ethernet", "label": "{{ vm.ruleChain.name }}", "translate": "false"}' | 84 | label: '{"icon": "settings_ethernet", "label": "{{ vm.ruleChain.name }}", "translate": "false"}' |
84 | } | 85 | } |
86 | + }).state('home.ruleChains.importRuleChain', { | ||
87 | + url: '/ruleChain/import', | ||
88 | + reloadOnSearch: false, | ||
89 | + module: 'private', | ||
90 | + auth: ['SYS_ADMIN', 'TENANT_ADMIN'], | ||
91 | + views: { | ||
92 | + "content@home": { | ||
93 | + templateUrl: ruleChainTemplate, | ||
94 | + controller: 'RuleChainController', | ||
95 | + controllerAs: 'vm' | ||
96 | + } | ||
97 | + }, | ||
98 | + params: { | ||
99 | + ruleChainImport: {} | ||
100 | + }, | ||
101 | + resolve: { | ||
102 | + ruleChain: | ||
103 | + /*@ngInject*/ | ||
104 | + function($stateParams) { | ||
105 | + return $stateParams.ruleChainImport.ruleChain; | ||
106 | + }, | ||
107 | + ruleChainMetaData: | ||
108 | + /*@ngInject*/ | ||
109 | + function($stateParams) { | ||
110 | + return $stateParams.ruleChainImport.metadata; | ||
111 | + }, | ||
112 | + ruleNodeComponents: | ||
113 | + /*@ngInject*/ | ||
114 | + function($stateParams, ruleChainService) { | ||
115 | + return ruleChainService.getRuleNodeComponents(); | ||
116 | + } | ||
117 | + }, | ||
118 | + data: { | ||
119 | + import: true, | ||
120 | + searchEnabled: true, | ||
121 | + pageTitle: 'rulechain.rulechain' | ||
122 | + }, | ||
123 | + ncyBreadcrumb: { | ||
124 | + label: '{"icon": "settings_ethernet", "label": "{{ (\'rulechain.import\' | translate) + \': \'+ vm.ruleChain.name }}", "translate": "false"}' | ||
125 | + } | ||
85 | }); | 126 | }); |
86 | } | 127 | } |
@@ -125,6 +125,16 @@ | @@ -125,6 +125,16 @@ | ||
125 | color: #333; | 125 | color: #333; |
126 | border: solid 1px #777; | 126 | border: solid 1px #777; |
127 | font-size: 12px; | 127 | font-size: 12px; |
128 | + &.tb-rule-node-highlighted:not(.tb-rule-node-invalid) { | ||
129 | + box-shadow: 0 0 10px 6px #51cbee; | ||
130 | + .tb-node-title { | ||
131 | + text-decoration: underline; | ||
132 | + font-weight: bold; | ||
133 | + } | ||
134 | + } | ||
135 | + &.tb-rule-node-invalid { | ||
136 | + box-shadow: 0 0 10px 6px #ff5c50; | ||
137 | + } | ||
128 | &.tb-input-type { | 138 | &.tb-input-type { |
129 | background-color: #a3eaa9; | 139 | background-color: #a3eaa9; |
130 | user-select: none; | 140 | user-select: none; |
@@ -156,7 +166,7 @@ | @@ -156,7 +166,7 @@ | ||
156 | 166 | ||
157 | } | 167 | } |
158 | .tb-node-title { | 168 | .tb-node-title { |
159 | - font-weight: 600; | 169 | + font-weight: 500; |
160 | } | 170 | } |
161 | .tb-node-type, .tb-node-title { | 171 | .tb-node-type, .tb-node-title { |
162 | overflow: hidden; | 172 | overflow: hidden; |
@@ -380,6 +390,14 @@ | @@ -380,6 +390,14 @@ | ||
380 | font-size: 14px; | 390 | font-size: 14px; |
381 | width: 300px; | 391 | width: 300px; |
382 | color: #333; | 392 | color: #333; |
393 | +} | ||
394 | + | ||
395 | +.tb-rule-node-error-tooltip { | ||
396 | + font-size: 16px; | ||
397 | + color: #ea0d0d; | ||
398 | +} | ||
399 | + | ||
400 | +.tb-rule-node-tooltip, .tb-rule-node-error-tooltip { | ||
383 | #tooltip-content { | 401 | #tooltip-content { |
384 | .tb-node-title { | 402 | .tb-node-title { |
385 | font-weight: 600; | 403 | font-weight: 600; |
@@ -16,20 +16,20 @@ | @@ -16,20 +16,20 @@ | ||
16 | 16 | ||
17 | --> | 17 | --> |
18 | 18 | ||
19 | -<md-content flex tb-expand-fullscreen tb-confirm-on-exit is-dirty="vm.isDirty" | 19 | +<md-content flex tb-expand-fullscreen tb-confirm-on-exit is-dirty="vm.isConfirmOnExit" |
20 | expand-tooltip-direction="bottom" layout="column" class="tb-rulechain" | 20 | expand-tooltip-direction="bottom" layout="column" class="tb-rulechain" |
21 | ng-keydown="vm.keyDown($event)" | 21 | ng-keydown="vm.keyDown($event)" |
22 | - ng-keyup="vm.keyUp($event)"> | 22 | + ng-keyup="vm.keyUp($event)" on-fullscreen-changed="vm.isFullscreen = expanded"> |
23 | <section class="tb-rulechain-container" flex layout="column"> | 23 | <section class="tb-rulechain-container" flex layout="column"> |
24 | <div class="tb-rulechain-layout" flex layout="row"> | 24 | <div class="tb-rulechain-layout" flex layout="row"> |
25 | <section layout="row" layout-wrap | 25 | <section layout="row" layout-wrap |
26 | class="tb-header-buttons md-fab tb-library-open"> | 26 | class="tb-header-buttons md-fab tb-library-open"> |
27 | <md-button ng-show="!vm.isLibraryOpen" | 27 | <md-button ng-show="!vm.isLibraryOpen" |
28 | class="tb-btn-header tb-btn-open-library md-primary md-fab md-fab-top-left" | 28 | class="tb-btn-header tb-btn-open-library md-primary md-fab md-fab-top-left" |
29 | - aria-label="{{ 'action.apply' | translate }}" | 29 | + aria-label="{{ 'rulenode.open-node-library' | translate }}" |
30 | ng-click="vm.isLibraryOpen = true"> | 30 | ng-click="vm.isLibraryOpen = true"> |
31 | - <md-tooltip md-direction="top"> | ||
32 | - {{ 'action.apply-changes' | translate }} | 31 | + <md-tooltip md-direction="{{vm.isFullscreen ? 'bottom' : 'top'}}"> |
32 | + {{ 'rulenode.open-node-library' | translate }} | ||
33 | </md-tooltip> | 33 | </md-tooltip> |
34 | <ng-md-icon icon="menu"></ng-md-icon> | 34 | <ng-md-icon icon="menu"></ng-md-icon> |
35 | </md-button> | 35 | </md-button> |
@@ -43,7 +43,7 @@ | @@ -43,7 +43,7 @@ | ||
43 | <div class="md-toolbar-tools"> | 43 | <div class="md-toolbar-tools"> |
44 | <md-button class="md-icon-button tb-small" aria-label="{{ 'action.search' | translate }}"> | 44 | <md-button class="md-icon-button tb-small" aria-label="{{ 'action.search' | translate }}"> |
45 | <md-icon aria-label="{{ 'action.search' | translate }}" class="material-icons">search</md-icon> | 45 | <md-icon aria-label="{{ 'action.search' | translate }}" class="material-icons">search</md-icon> |
46 | - <md-tooltip md-direction="top"> | 46 | + <md-tooltip md-direction="{{vm.isFullscreen ? 'bottom' : 'top'}}"> |
47 | {{'rulenode.search' | translate}} | 47 | {{'rulenode.search' | translate}} |
48 | </md-tooltip> | 48 | </md-tooltip> |
49 | </md-button> | 49 | </md-button> |
@@ -53,15 +53,17 @@ | @@ -53,15 +53,17 @@ | ||
53 | <input ng-model="vm.ruleNodeSearch" placeholder="{{'rulenode.search' | translate}}"/> | 53 | <input ng-model="vm.ruleNodeSearch" placeholder="{{'rulenode.search' | translate}}"/> |
54 | </md-input-container> | 54 | </md-input-container> |
55 | </div> | 55 | </div> |
56 | - <md-button class="md-icon-button tb-small" aria-label="Close" ng-click="vm.ruleNodeSearch = ''"> | 56 | + <md-button class="md-icon-button tb-small" aria-label="Close" |
57 | + ng-show="vm.ruleNodeSearch" | ||
58 | + ng-click="vm.ruleNodeSearch = ''"> | ||
57 | <md-icon aria-label="Close" class="material-icons">close</md-icon> | 59 | <md-icon aria-label="Close" class="material-icons">close</md-icon> |
58 | - <md-tooltip md-direction="top"> | ||
59 | - {{ 'action.close' | translate }} | 60 | + <md-tooltip md-direction="{{vm.isFullscreen ? 'bottom' : 'top'}}"> |
61 | + {{ 'action.clear-search' | translate }} | ||
60 | </md-tooltip> | 62 | </md-tooltip> |
61 | </md-button> | 63 | </md-button> |
62 | <md-button class="md-icon-button tb-small" aria-label="Close" ng-click="vm.isLibraryOpen = false"> | 64 | <md-button class="md-icon-button tb-small" aria-label="Close" ng-click="vm.isLibraryOpen = false"> |
63 | <md-icon aria-label="Close" class="material-icons">chevron_left</md-icon> | 65 | <md-icon aria-label="Close" class="material-icons">chevron_left</md-icon> |
64 | - <md-tooltip md-direction="top"> | 66 | + <md-tooltip md-direction="{{vm.isFullscreen ? 'bottom' : 'top'}}"> |
65 | {{ 'action.close' | translate }} | 67 | {{ 'action.close' | translate }} |
66 | </md-tooltip> | 68 | </md-tooltip> |
67 | </md-button> | 69 | </md-button> |
@@ -90,6 +92,7 @@ | @@ -90,6 +92,7 @@ | ||
90 | callbacks="vm.nodeLibCallbacks" | 92 | callbacks="vm.nodeLibCallbacks" |
91 | node-width="170" | 93 | node-width="170" |
92 | node-height="50" | 94 | node-height="50" |
95 | + control="vm.ruleNodeTypesCanvasControl[typeId]" | ||
93 | drop-target-id="'tb-rulchain-canvas'"></fc-canvas> | 96 | drop-target-id="'tb-rulchain-canvas'"></fc-canvas> |
94 | </md-expansion-panel-content> | 97 | </md-expansion-panel-content> |
95 | </md-expansion-panel-expanded> | 98 | </md-expansion-panel-expanded> |
@@ -182,7 +185,7 @@ | @@ -182,7 +185,7 @@ | ||
182 | </md-tooltip> | 185 | </md-tooltip> |
183 | <ng-md-icon icon="delete"></ng-md-icon> | 186 | <ng-md-icon icon="delete"></ng-md-icon> |
184 | </md-button> | 187 | </md-button> |
185 | - <md-button ng-disabled="$root.loading || !vm.isDirty" | 188 | + <md-button ng-disabled="$root.loading || vm.isInvalid || (!vm.isDirty && !vm.isImport)" |
186 | class="tb-btn-footer md-accent md-hue-2 md-fab" | 189 | class="tb-btn-footer md-accent md-hue-2 md-fab" |
187 | aria-label="{{ 'action.apply' | translate }}" | 190 | aria-label="{{ 'action.apply' | translate }}" |
188 | ng-click="vm.saveRuleChain()"> | 191 | ng-click="vm.saveRuleChain()"> |
@@ -63,8 +63,8 @@ export default function RuleChainsController(ruleChainService, userService, impo | @@ -63,8 +63,8 @@ export default function RuleChainsController(ruleChainService, userService, impo | ||
63 | { | 63 | { |
64 | onAction: function ($event) { | 64 | onAction: function ($event) { |
65 | importExport.importRuleChain($event).then( | 65 | importExport.importRuleChain($event).then( |
66 | - function() { | ||
67 | - vm.grid.refreshList(); | 66 | + function(ruleChainImport) { |
67 | + $state.go('home.ruleChains.importRuleChain', {ruleChainImport:ruleChainImport}); | ||
68 | } | 68 | } |
69 | ); | 69 | ); |
70 | }, | 70 | }, |
@@ -23,7 +23,7 @@ | @@ -23,7 +23,7 @@ | ||
23 | ng-mouseenter="callbacks.mouseEnter($event, node)" | 23 | ng-mouseenter="callbacks.mouseEnter($event, node)" |
24 | ng-mouseleave="callbacks.mouseLeave($event, node)"> | 24 | ng-mouseleave="callbacks.mouseLeave($event, node)"> |
25 | <div class="{{flowchartConstants.nodeOverlayClass}}"></div> | 25 | <div class="{{flowchartConstants.nodeOverlayClass}}"></div> |
26 | - <div class="tb-rule-node {{node.nodeClass}}"> | 26 | + <div class="tb-rule-node {{node.nodeClass}}" ng-class="{'tb-rule-node-highlighted' : node.highlighted, 'tb-rule-node-invalid': node.error }"> |
27 | <md-icon aria-label="node-type-icon" flex="15" | 27 | <md-icon aria-label="node-type-icon" flex="15" |
28 | class="material-icons">{{node.icon}}</md-icon> | 28 | class="material-icons">{{node.icon}}</md-icon> |
29 | <div layout="column" flex="85" layout-align="center"> | 29 | <div layout="column" flex="85" layout-align="center"> |