Commit b38a7d7bda4d7175391973cc685efdb8a0dbfdae

Authored by Andrew Shvayka
2 parents 0fb9f21e 76dd0a45

Merge branch 'develop/1.5' of github.com:thingsboard/thingsboard into develop/1.5

Showing 27 changed files with 570 additions and 173 deletions
... ... @@ -69,14 +69,18 @@ public class RuleNodeActorMessageProcessor extends ComponentMsgProcessor<RuleNod
69 69 && ruleNode.getConfiguration().equals(newRuleNode.getConfiguration()));
70 70 this.ruleNode = newRuleNode;
71 71 if (restartRequired) {
72   - tbNode.destroy();
  72 + if (tbNode != null) {
  73 + tbNode.destroy();
  74 + }
73 75 start(context);
74 76 }
75 77 }
76 78
77 79 @Override
78 80 public void stop(ActorContext context) throws Exception {
79   - tbNode.destroy();
  81 + if (tbNode != null) {
  82 + tbNode.destroy();
  83 + }
80 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 43 nodeDetails = "If Attributes enrichment configured, <b>CLIENT/SHARED/SERVER</b> attributes are added into Message metadata " +
44 44 "with specific prefix: <i>cs/shared/ss</i>. To access those attributes in other nodes this template can be used " +
45 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 49 public class TbGetAttributesNode implements TbNode {
48 50
49 51 private TbGetAttributesNodeConfiguration config;
... ...
... ... @@ -30,7 +30,9 @@ import org.thingsboard.server.common.data.plugin.ComponentType;
30 30 nodeDescription = "Add Originators Customer Attributes or Latest Telemetry into Message Metadata",
31 31 nodeDetails = "If Attributes enrichment configured, server scope attributes are added into Message metadata. " +
32 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 36 public class TbGetCustomerAttributeNode extends TbEntityGetAttrNode<CustomerId> {
35 37
36 38 @Override
... ...
... ... @@ -16,18 +16,19 @@
16 16 package org.thingsboard.rule.engine.metadata;
17 17
18 18 import lombok.Data;
19   -import org.thingsboard.rule.engine.api.NodeConfiguration;
  19 +import org.thingsboard.rule.engine.data.RelationsQuery;
20 20 import org.thingsboard.server.common.data.relation.EntityRelation;
21 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 25 import java.util.HashMap;
24 26 import java.util.Map;
25 27
26 28 @Data
27 29 public class TbGetRelatedAttrNodeConfiguration extends TbGetEntityAttrNodeConfiguration {
28 30
29   - private String relationType;
30   - private EntitySearchDirection direction;
  31 + private RelationsQuery relationsQuery;
31 32
32 33 @Override
33 34 public TbGetRelatedAttrNodeConfiguration defaultConfiguration() {
... ... @@ -36,8 +37,14 @@ public class TbGetRelatedAttrNodeConfiguration extends TbGetEntityAttrNodeConfig
36 37 attrMapping.putIfAbsent("temperature", "tempo");
37 38 configuration.setAttrMapping(attrMapping);
38 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 48 return configuration;
42 49 }
43 50 }
... ...
... ... @@ -32,7 +32,10 @@ import org.thingsboard.server.common.data.plugin.ComponentType;
32 32 "If multiple Related Entities are found, only first Entity is used for attributes enrichment, other entities are discarded. " +
33 33 "If Attributes enrichment configured, server scope attributes are added into Message metadata. " +
34 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 39 public class TbGetRelatedAttributeNode extends TbEntityGetAttrNode<EntityId> {
37 40
38 41 private TbGetRelatedAttrNodeConfiguration config;
... ... @@ -45,6 +48,6 @@ public class TbGetRelatedAttributeNode extends TbEntityGetAttrNode<EntityId> {
45 48
46 49 @Override
47 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 32 nodeDescription = "Add Originators Tenant Attributes or Latest Telemetry into Message Metadata",
33 33 nodeDetails = "If Attributes enrichment configured, server scope attributes are added into Message metadata. " +
34 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 38 public class TbGetTenantAttributeNode extends TbEntityGetAttrNode<TenantId> {
37 39
38 40 @Override
... ...
... ... @@ -39,7 +39,9 @@ import java.util.HashSet;
39 39 configClazz = TbChangeOriginatorNodeConfiguration.class,
40 40 nodeDescription = "Change Message Originator To Tenant/Customer/Related Entity",
41 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 45 public class TbChangeOriginatorNode extends TbAbstractTransformNode {
44 46
45 47 protected static final String CUSTOMER_SOURCE = "CUSTOMER";
... ... @@ -68,7 +70,7 @@ public class TbChangeOriginatorNode extends TbAbstractTransformNode {
68 70 case TENANT_SOURCE:
69 71 return EntitiesTenantIdAsyncLoader.findEntityIdAsync(ctx, original);
70 72 case RELATED_SOURCE:
71   - return EntitiesRelatedEntityIdAsyncLoader.findEntityAsync(ctx, original, config.getDirection(), config.getRelationType());
  73 + return EntitiesRelatedEntityIdAsyncLoader.findEntityAsync(ctx, original, config.getRelationsQuery());
72 74 default:
73 75 return Futures.immediateFailedFuture(new IllegalStateException("Unexpected originator source " + config.getOriginatorSource()));
74 76 }
... ... @@ -82,9 +84,9 @@ public class TbChangeOriginatorNode extends TbAbstractTransformNode {
82 84 }
83 85
84 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 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 17
18 18 import lombok.Data;
19 19 import org.thingsboard.rule.engine.api.NodeConfiguration;
  20 +import org.thingsboard.rule.engine.data.RelationsQuery;
20 21 import org.thingsboard.server.common.data.relation.EntityRelation;
21 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 27 @Data
24 28 public class TbChangeOriginatorNodeConfiguration extends TbTransformNodeConfiguration implements NodeConfiguration {
25 29
26 30 private String originatorSource;
27   - private EntitySearchDirection direction;
28   - private String relationType;
  31 +
  32 + private RelationsQuery relationsQuery;
29 33
30 34 @Override
31 35 public TbChangeOriginatorNodeConfiguration defaultConfiguration() {
32 36 TbChangeOriginatorNodeConfiguration configuration = new TbChangeOriginatorNodeConfiguration();
33 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 46 configuration.setStartNewChain(false);
37 47 return configuration;
38 48 }
... ...
... ... @@ -31,7 +31,9 @@ import javax.script.Bindings;
31 31 nodeDescription = "Change Message payload and Metadata using JavaScript",
32 32 nodeDetails = "JavaScript function recieve 2 input parameters that can be changed inside.<br/> " +
33 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 37 public class TbTransformMsgNode extends TbAbstractTransformNode {
36 38
37 39 private TbTransformMsgNodeConfiguration config;
... ...
... ... @@ -20,32 +20,41 @@ import com.google.common.util.concurrent.Futures;
20 20 import com.google.common.util.concurrent.ListenableFuture;
21 21 import org.apache.commons.collections.CollectionUtils;
22 22 import org.thingsboard.rule.engine.api.TbContext;
  23 +import org.thingsboard.rule.engine.data.RelationsQuery;
23 24 import org.thingsboard.server.common.data.id.EntityId;
24 25 import org.thingsboard.server.common.data.relation.EntityRelation;
  26 +import org.thingsboard.server.common.data.relation.EntityRelationsQuery;
25 27 import org.thingsboard.server.common.data.relation.EntitySearchDirection;
  28 +import org.thingsboard.server.common.data.relation.RelationsSearchParameters;
26 29 import org.thingsboard.server.dao.relation.RelationService;
27 30
28 31 import java.util.List;
29 32
30   -import static org.thingsboard.server.common.data.relation.RelationTypeGroup.COMMON;
31   -
32 33 public class EntitiesRelatedEntityIdAsyncLoader {
33 34
34 35 public static ListenableFuture<EntityId> findEntityAsync(TbContext ctx, EntityId originator,
35   - EntitySearchDirection direction, String relationType) {
  36 + RelationsQuery relationsQuery) {
36 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 41 return Futures.transform(asyncRelation, (AsyncFunction<? super List<EntityRelation>, EntityId>)
40 42 r -> CollectionUtils.isNotEmpty(r) ? Futures.immediateFuture(r.get(0).getTo())
41 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 45 return Futures.transform(asyncRelation, (AsyncFunction<? super List<EntityRelation>, EntityId>)
45 46 r -> CollectionUtils.isNotEmpty(r) ? Futures.immediateFuture(r.get(0).getFrom())
46 47 : Futures.immediateFailedFuture(new IllegalStateException("Relation not found")));
47 48 }
48   -
49 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 2 /*# sourceMappingURL=rulenode-core-config.css.map*/
\ No newline at end of file
... ...
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:&apos;...&apos;}}" }\'>tb.rulenode.no-message-type-matching</span> <span> <a translate ng-click="createMessageType($event, \'#message_type_chips\')">tb.rulenode.create-new-message-type</a> </span> </div> </div> </md-not-found> </md-autocomplete> <md-chip-template> <span>{{$chip.name}}</span> </md-chip-template> </md-chips> <div class=tb-error-messages ng-messages=ngModelCtrl.$error role=alert> <div translate ng-message=messageTypes class=tb-error-message>tb.rulenode.message-types-required</div> </div> </section>'},function(e,t){e.exports=" <section layout=column> <label translate class=\"tb-title no-padding\">tb.rulenode.filter</label> <tb-js-func ng-model=configuration.jsScript function-name=Filter function-args=\"{{ ['msg', 'metadata'] }}\" no-validate=true> </tb-js-func> </section> "},function(e,t){e.exports=" <section layout=column> <label translate class=\"tb-title no-padding\">tb.rulenode.switch</label> <tb-js-func ng-model=configuration.jsScript function-name=Switch function-args=\"{{ ['msg', 'metadata'] }}\" no-validate=true> </tb-js-func> </section> "},function(e,t,a){"use strict";function s(e){return e&&e.__esModule?e:{default:e}}function n(e,t,a){var s=function(s,n,r,l){function u(){if(l.$viewValue){for(var e=[],t=0;t<s.messageTypes.length;t++)e.push(s.messageTypes[t].value);l.$viewValue.messageTypes=e,o()}}function o(){if(s.required){var e=!(!l.$viewValue.messageTypes||!l.$viewValue.messageTypes.length);l.$setValidity("messageTypes",e)}else l.$setValidity("messageTypes",!0)}var c=i.default;n.html(c),s.selectedMessageType=null,s.messageTypeSearchText=null,s.ngModelCtrl=l;var d=[];for(var p in a.messageType){var m={name:a.messageType[p].name,value:a.messageType[p].value};d.push(m)}s.transformMessageTypeChip=function(e){var a,s=t("filter")(d,{name:e},!0);return a=s&&s.length?angular.copy(s[0]):{name:e,value:e}},s.messageTypesSearch=function(e){var a=e?t("filter")(d,{name:e}):d;return a.map(function(e){return e.name})},s.createMessageType=function(e,t){var a=angular.element(t,n)[0].firstElementChild,s=angular.element(a),r=s.scope().$mdChipsCtrl.getChipBuffer();e.preventDefault(),e.stopPropagation(),s.scope().$mdChipsCtrl.appendChip(r.trim()),s.scope().$mdChipsCtrl.resetChipBuffer()},l.$render=function(){var e=l.$viewValue,t=[];if(e&&e.messageTypes)for(var n=0;n<e.messageTypes.length;n++){var r=e.messageTypes[n];a.messageType[r]?t.push(angular.copy(a.messageType[r])):t.push({name:r,value:r})}s.messageTypes=t,s.$watch("messageTypes",function(e,t){angular.equals(e,t)||u()},!0)},e(n.contents())(s)};return{restrict:"E",require:"^ngModel",scope:{required:"=ngRequired",readonly:"=ngReadonly"},link:s}}n.$inject=["$compile","$filter","ruleNodeTypes"],Object.defineProperty(t,"__esModule",{value:!0}),t.default=n,a(1);var r=a(2),i=s(r)},function(e,t,a){"use strict";function s(e){return e&&e.__esModule?e:{default:e}}function n(e){var t=function(t,a,s,n){var r=i.default;a.html(r),t.$watch("configuration",function(e,a){angular.equals(e,a)||n.$setViewValue(t.configuration)}),n.$render=function(){t.configuration=n.$viewValue},e(a.contents())(t)};return{restrict:"E",require:"^ngModel",scope:{},link:t}}n.$inject=["$compile"],Object.defineProperty(t,"__esModule",{value:!0}),t.default=n;var r=a(3),i=s(r)},function(e,t,a){"use strict";function s(e){return e&&e.__esModule?e:{default:e}}function n(e){var t=function(t,a,s,n){var r=i.default;a.html(r),t.$watch("configuration",function(e,a){angular.equals(e,a)||n.$setViewValue(t.configuration)}),n.$render=function(){t.configuration=n.$viewValue},e(a.contents())(t)};return{restrict:"E",require:"^ngModel",scope:{},link:t}}n.$inject=["$compile"],Object.defineProperty(t,"__esModule",{value:!0}),t.default=n;var r=a(4),i=s(r)},function(e,t,a){"use strict";function s(e){return e&&e.__esModule?e:{default:e}}Object.defineProperty(t,"__esModule",{value:!0});var n=a(11),r=s(n),i=a(6),l=s(i),u=a(5),o=s(u),c=a(7),d=s(c),p=a(10),m=s(p);t.default=angular.module("thingsboard.ruleChain.config",[r.default]).directive("tbFilterNodeScriptConfig",l.default).directive("tbFilterNodeMessageTypeConfig",o.default).directive("tbFilterNodeSwitchConfig",d.default).config(m.default).name},function(e,t){"use strict";function a(e){var t={tb:{rulenode:{filter:"Filter",switch:"Switch","message-type":"Message type","message-types-filter":"Message types filter","no-message-types-found":"No message types found","no-message-type-matching":"'{{messageType}}' not found.","create-new-message-type":"Create a new one!","message-types-required":"Message types are required."}}};angular.merge(e.en_US,t)}Object.defineProperty(t,"__esModule",{value:!0}),t.default=a},function(e,t,a){"use strict";function s(e){return e&&e.__esModule?e:{default:e}}function n(e,t){(0,i.default)(t);for(var a in t){var s=t[a];e.translations(a,s)}}n.$inject=["$translateProvider","locales"],Object.defineProperty(t,"__esModule",{value:!0}),t.default=n;var r=a(9),i=s(r)},function(e,t){"use strict";Object.defineProperty(t,"__esModule",{value:!0}),t.default=angular.module("thingsboard.ruleChain.config.types",[]).constant("ruleNodeTypes",{messageType:{POST_ATTRIBUTES:{name:"Post attributes",value:"POST_ATTRIBUTES"},POST_TELEMETRY:{name:"Post telemetry",value:"POST_TELEMETRY"},RPC_REQUEST:{name:"RPC Request",value:"RPC_REQUEST"}}}).name}]);
  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:&apos;...&apos;}}" }\'>tb.rulenode.no-message-type-matching</span> <span> <a translate ng-click="createMessageType($event, \'#message_type_chips\')">tb.rulenode.create-new-message-type</a> </span> </div> </div> </md-not-found> </md-autocomplete> <md-chip-template> <span>{{$chip.name}}</span> </md-chip-template> </md-chips> <div class=tb-error-messages ng-messages=ngModelCtrl.$error role=alert> <div translate ng-message=messageTypes class=tb-error-message>tb.rulenode.message-types-required</div> </div> </section>'},function(e,t){e.exports=" <section layout=column> <label translate class=\"tb-title no-padding\">tb.rulenode.filter</label> <tb-js-func ng-model=configuration.jsScript function-name=Filter function-args=\"{{ ['msg', 'metadata'] }}\" no-validate=true> </tb-js-func> </section> "},function(e,t){e.exports=" <section layout=column> <label translate class=\"tb-title no-padding\">tb.rulenode.switch</label> <tb-js-func ng-model=configuration.jsScript function-name=Switch function-args=\"{{ ['msg', 'metadata'] }}\" no-validate=true> </tb-js-func> </section> "},function(e,t){e.exports=' <section class=tb-kv-map-config layout=column> <div class=header flex layout=row> <span class=cell flex translate>{{ keyText }}</span> <span class=cell flex translate>{{ valText }}</span> <span ng-show=!disabled style=width:52px>&nbsp</span> </div> <div class=body> <div class=row ng-form name=kvForm flex layout=row layout-align="start center" ng-repeat="keyVal in kvList track by $index"> <md-input-container class="cell md-block" flex md-no-float> <input placeholder="{{ keyText | translate }}" ng-required=true name=key ng-model=keyVal.key> <div ng-messages=kvForm.key.$error> <div translate ng-message=required>{{keyRequiredText}}</div> </div> </md-input-container> <md-input-container class="cell md-block" flex md-no-float> <input placeholder="{{ valText | translate }}" ng-required=true name=value ng-model=keyVal.value> <div ng-messages=kvForm.value.$error> <div translate ng-message=required>{{valRequiredText}}</div> </div> </md-input-container> <md-button ng-show=!disabled ng-disabled=loading class="md-icon-button md-primary" ng-click=removeKeyVal($index) aria-label="{{ \'action.remove\' | translate }}"> <md-tooltip md-direction=top> {{ \'tb.key-val.remove-entry\' | translate }} </md-tooltip> <md-icon aria-label="{{ \'action.delete\' | translate }}" class=material-icons> close </md-icon> </md-button> </div> </div> <div class=tb-error-messages ng-messages=ngModelCtrl.$error role=alert> <div translate ng-message=kvMap class=tb-error-message>{{requiredText}}</div> </div> <div> <md-button ng-show=!disabled ng-disabled=loading class="md-primary md-raised" ng-click=addKeyVal() aria-label="{{ \'action.add\' | translate }}"> <md-tooltip md-direction=top> {{ \'tb.key-val.add-entry\' | translate }} </md-tooltip> <md-icon aria-label="{{ \'action.add\' | translate }}" class=material-icons> add </md-icon> {{ \'action.add\' | translate }} </md-button> </div> </section> '},function(e,t){e.exports=" <section layout=column> <div flex layout=row> <md-input-container class=md-block style=min-width:100px> <label translate>relation.direction</label> <md-select required ng-model=query.direction> <md-option ng-repeat=\"direction in types.entitySearchDirection\" ng-value=direction> {{ ('relation.search-direction.' + direction) | translate}} </md-option> </md-select> </md-input-container> <md-input-container class=md-block> <label translate>tb.rulenode.max-relation-level</label> <input name=maxRelationLevel type=number min=1 step=1 placeholder=\"{{ 'tb.rulenode.unlimited-level' | translate }}\" ng-model=query.maxLevel aria-label=\"{{ 'tb.rulenode.max-relation-level' | translate }}\"> </md-input-container> </div> <div class=md-caption style=padding-bottom:10px;color:rgba(0,0,0,.57) translate>relation.relation-filters</div> <tb-relation-filters ng-model=query.filters> </tb-relation-filters> </section> "},function(e,t){e.exports=' <section layout=column> <md-input-container class=md-block> <label translate>tb.rulenode.originator-source</label> <md-select required ng-model=configuration.originatorSource> <md-option ng-repeat="source in ruleNodeTypes.originatorSource" ng-value=source.value> {{ source.name | translate}} </md-option> </md-select> </md-input-container> <section layout=column ng-if="configuration.originatorSource == ruleNodeTypes.originatorSource.RELATED.value"> <label translate class="tb-title tb-required">tb.rulenode.relations-query</label> <tb-relations-query-config style=padding-bottom:15px ng-model=configuration.relationsQuery> </tb-relations-query-config> </section> <md-checkbox aria-label="{{ \'tb.rulenode.clone-message\' | translate }}" ng-model=configuration.startNewChain>{{ \'tb.rulenode.clone-message\' | translate }} </md-checkbox> </section> '},function(e,t){e.exports=" <section layout=column> <label translate class=\"tb-title no-padding\">tb.rulenode.transform</label> <tb-js-func ng-model=configuration.jsScript function-name=Transform function-args=\"{{ ['msg', 'metadata'] }}\" no-validate=true> </tb-js-func> <md-checkbox aria-label=\"{{ 'tb.rulenode.clone-message' | translate }}\" ng-model=configuration.startNewChain>{{ 'tb.rulenode.clone-message' | translate }} </md-checkbox> </section> "},function(e,t,n){"use strict";function a(e){return e&&e.__esModule?e:{default:e}}function r(e){var t=function(t,n,a,r){var i=l.default;n.html(i),t.$watch("configuration",function(e,n){angular.equals(e,n)||r.$setViewValue(t.configuration)}),r.$render=function(){t.configuration=r.$viewValue},e(n.contents())(t)};return{restrict:"E",require:"^ngModel",scope:{},link:t}}r.$inject=["$compile"],Object.defineProperty(t,"__esModule",{value:!0}),t.default=r;var i=n(3),l=a(i)},function(e,t,n){"use strict";function a(e){return e&&e.__esModule?e:{default:e}}Object.defineProperty(t,"__esModule",{value:!0});var r=n(16),i=a(r),l=n(17),o=a(l),s=n(14),u=a(s),d=n(18),c=a(d);t.default=angular.module("thingsboard.ruleChain.config.enrichment",[]).directive("tbEnrichmentNodeOriginatorAttributesConfig",i.default).directive("tbEnrichmentNodeRelatedAttributesConfig",o.default).directive("tbEnrichmentNodeCustomerAttributesConfig",u.default).directive("tbEnrichmentNodeTenantAttributesConfig",c.default).name},function(e,t,n){"use strict";function a(e){return e&&e.__esModule?e:{default:e}}function r(e,t){var n=function(n,a,r,i){var o=l.default;a.html(o);var s=186;n.separatorKeys=[t.KEY_CODE.ENTER,t.KEY_CODE.COMMA,s],n.$watch("configuration",function(e,t){angular.equals(e,t)||i.$setViewValue(n.configuration)}),i.$render=function(){n.configuration=i.$viewValue},e(a.contents())(n)};return{restrict:"E",require:"^ngModel",scope:{},link:n}}r.$inject=["$compile","$mdConstant"],Object.defineProperty(t,"__esModule",{value:!0}),t.default=r;var i=n(4),l=a(i)},function(e,t,n){"use strict";function a(e){return e&&e.__esModule?e:{default:e}}function r(e){var t=function(t,n,a,r){var i=l.default;n.html(i),t.$watch("configuration",function(e,n){angular.equals(e,n)||r.$setViewValue(t.configuration)}),r.$render=function(){t.configuration=r.$viewValue},e(n.contents())(t)};return{restrict:"E",require:"^ngModel",scope:{},link:t}}r.$inject=["$compile"],Object.defineProperty(t,"__esModule",{value:!0}),t.default=r;var i=n(5),l=a(i)},function(e,t,n){"use strict";function a(e){return e&&e.__esModule?e:{default:e}}function r(e){var t=function(t,n,a,r){var i=l.default;n.html(i),t.$watch("configuration",function(e,n){angular.equals(e,n)||r.$setViewValue(t.configuration)}),r.$render=function(){t.configuration=r.$viewValue},e(n.contents())(t)};return{restrict:"E",require:"^ngModel",scope:{},link:t}}r.$inject=["$compile"],Object.defineProperty(t,"__esModule",{value:!0}),t.default=r;var i=n(6),l=a(i)},function(e,t,n){"use strict";function a(e){return e&&e.__esModule?e:{default:e}}Object.defineProperty(t,"__esModule",{value:!0});var r=n(21),i=a(r),l=n(20),o=a(l),s=n(22),u=a(s);t.default=angular.module("thingsboard.ruleChain.config.filter",[]).directive("tbFilterNodeScriptConfig",i.default).directive("tbFilterNodeMessageTypeConfig",o.default).directive("tbFilterNodeSwitchConfig",u.default).name},function(e,t,n){"use strict";function a(e){return e&&e.__esModule?e:{default:e}}function r(e,t,n){var a=function(a,r,i,o){function s(){if(o.$viewValue){for(var e=[],t=0;t<a.messageTypes.length;t++)e.push(a.messageTypes[t].value);o.$viewValue.messageTypes=e,u()}}function u(){if(a.required){var e=!(!o.$viewValue.messageTypes||!o.$viewValue.messageTypes.length);o.$setValidity("messageTypes",e)}else o.$setValidity("messageTypes",!0)}var d=l.default;r.html(d),a.selectedMessageType=null,a.messageTypeSearchText=null,a.ngModelCtrl=o;var c=[];for(var m in n.messageType){var f={name:n.messageType[m].name,value:n.messageType[m].value};c.push(f)}a.transformMessageTypeChip=function(e){var n,a=t("filter")(c,{name:e},!0);return n=a&&a.length?angular.copy(a[0]):{name:e,value:e}},a.messageTypesSearch=function(e){var n=e?t("filter")(c,{name:e}):c;return n.map(function(e){return e.name})},a.createMessageType=function(e,t){var n=angular.element(t,r)[0].firstElementChild,a=angular.element(n),i=a.scope().$mdChipsCtrl.getChipBuffer();e.preventDefault(),e.stopPropagation(),a.scope().$mdChipsCtrl.appendChip(i.trim()),a.scope().$mdChipsCtrl.resetChipBuffer()},o.$render=function(){var e=o.$viewValue,t=[];if(e&&e.messageTypes)for(var r=0;r<e.messageTypes.length;r++){var i=e.messageTypes[r];n.messageType[i]?t.push(angular.copy(n.messageType[i])):t.push({name:i,value:i})}a.messageTypes=t,a.$watch("messageTypes",function(e,t){angular.equals(e,t)||s()},!0)},e(r.contents())(a)};return{restrict:"E",require:"^ngModel",scope:{required:"=ngRequired",readonly:"=ngReadonly"},link:a}}r.$inject=["$compile","$filter","ruleNodeTypes"],Object.defineProperty(t,"__esModule",{value:!0}),t.default=r,n(1);var i=n(7),l=a(i)},[32,8],function(e,t,n){"use strict";function a(e){return e&&e.__esModule?e:{default:e}}function r(e){var t=function(t,n,a,r){var i=l.default;n.html(i),t.$watch("configuration",function(e,n){angular.equals(e,n)||r.$setViewValue(t.configuration)}),r.$render=function(){t.configuration=r.$viewValue},e(n.contents())(t)};return{restrict:"E",require:"^ngModel",scope:{},link:t}}r.$inject=["$compile"],Object.defineProperty(t,"__esModule",{value:!0}),t.default=r;var i=n(9),l=a(i)},function(e,t,n){"use strict";function a(e){return e&&e.__esModule?e:{default:e}}function r(e){var t=function(t,n,a,r){function i(e){e>-1&&t.kvList.splice(e,1)}function o(){t.kvList||(t.kvList=[]),t.kvList.push({key:"",value:""})}function s(){var e={};t.kvList.forEach(function(t){t.key&&(e[t.key]=t.value)}),r.$setViewValue(e),u()}function u(){var e=!0;t.required&&!t.kvList.length&&(e=!1),r.$setValidity("kvMap",e)}var d=l.default;n.html(d),t.ngModelCtrl=r,t.removeKeyVal=i,t.addKeyVal=o,t.kvList=[],t.$watch("query",function(e,n){angular.equals(e,n)||r.$setViewValue(t.query)}),r.$render=function(){if(r.$viewValue){var e=r.$viewValue;t.kvList.length=0;for(var n in e)t.kvList.push({key:n,value:e[n]})}t.$watch("kvList",function(e,t){angular.equals(e,t)||s()},!0),u()},e(n.contents())(t)};return{restrict:"E",require:"^ngModel",scope:{required:"=ngRequired",disabled:"=ngDisabled",requiredText:"=",keyText:"=",keyRequiredText:"=",valText:"=",valRequiredText:"="},link:t}}r.$inject=["$compile"],Object.defineProperty(t,"__esModule",{value:!0}),t.default=r;var i=n(10),l=a(i);n(2)},function(e,t,n){"use strict";function a(e){return e&&e.__esModule?e:{default:e}}function r(e,t){var n=function(n,a,r,i){var o=l.default;a.html(o),n.types=t,n.$watch("query",function(e,t){angular.equals(e,t)||i.$setViewValue(n.query)}),i.$render=function(){n.query=i.$viewValue},e(a.contents())(n)};return{restrict:"E",require:"^ngModel",scope:{},link:n}}r.$inject=["$compile","types"],Object.defineProperty(t,"__esModule",{value:!0}),t.default=r;var i=n(11),l=a(i)},function(e,t,n){"use strict";function a(e){return e&&e.__esModule?e:{default:e}}function r(e,t){var n=function(n,a,r,i){var o=l.default;a.html(o),n.ruleNodeTypes=t,n.$watch("configuration",function(e,t){angular.equals(e,t)||i.$setViewValue(n.configuration)}),i.$render=function(){n.configuration=i.$viewValue},e(a.contents())(n)};return{restrict:"E",require:"^ngModel",scope:{},link:n}}r.$inject=["$compile","ruleNodeTypes"],Object.defineProperty(t,"__esModule",{value:!0}),t.default=r;var i=n(12),l=a(i)},function(e,t,n){"use strict";function a(e){return e&&e.__esModule?e:{default:e}}Object.defineProperty(t,"__esModule",{value:!0});var r=n(25),i=a(r),l=n(27),o=a(l);t.default=angular.module("thingsboard.ruleChain.config.filter",[]).directive("tbTransformationNodeChangeOriginatorConfig",i.default).directive("tbTransformationNodeScriptConfig",o.default).name},[32,13],function(e,t,n){"use strict";function a(e){return e&&e.__esModule?e:{default:e}}Object.defineProperty(t,"__esModule",{value:!0});var r=n(31),i=a(r),l=n(19),o=a(l),s=n(15),u=a(s),d=n(26),c=a(d),m=n(24),f=a(m),g=n(23),p=a(g),b=n(30),y=a(b);t.default=angular.module("thingsboard.ruleChain.config",[i.default,o.default,u.default,c.default]).directive("tbRelationsQueryConfig",f.default).directive("tbKvMapConfig",p.default).config(y.default).name},function(e,t){"use strict";function n(e){var t={tb:{rulenode:{filter:"Filter",switch:"Switch","message-type":"Message type","message-types-filter":"Message types filter","no-message-types-found":"No message types found","no-message-type-matching":"'{{messageType}}' not found.","create-new-message-type":"Create a new one!","message-types-required":"Message types are required.","client-attributes":"Client attributes","shared-attributes":"Shared attributes","server-attributes":"Server attributes","latest-timeseries":"Latest timeseries","relations-query":"Relations query","max-relation-level":"Max relation level","unlimited-level":"Unlimited level","latest-telemetry":"Latest telemetry","attr-mapping":"Attributes mapping","source-attribute":"Source attribute","source-attribute-required":"Source attribute is required.","source-telemetry":"Source telemetry","source-telemetry-required":"Source telemetry is required.","target-attribute":"Target attribute","target-attribute-required":"Target attribute is required.","attr-mapping-required":"At least one attribute mapping should be specified.","originator-source":"Originator source","originator-customer":"Customer","originator-tenant":"Tenant","originator-related":"Related","clone-message":"Clone message",transform:"Transform"},"key-val":{key:"Key",value:"Value","remove-entry":"Remove entry","add-entry":"Add entry"}}};angular.merge(e.en_US,t)}Object.defineProperty(t,"__esModule",{value:!0}),t.default=n},function(e,t,n){"use strict";function a(e){return e&&e.__esModule?e:{default:e}}function r(e,t){(0,l.default)(t);for(var n in t){var a=t[n];e.translations(n,a)}}r.$inject=["$translateProvider","locales"],Object.defineProperty(t,"__esModule",{value:!0}),t.default=r;var i=n(29),l=a(i)},function(e,t){"use strict";Object.defineProperty(t,"__esModule",{value:!0}),t.default=angular.module("thingsboard.ruleChain.config.types",[]).constant("ruleNodeTypes",{messageType:{POST_ATTRIBUTES:{name:"Post attributes",value:"POST_ATTRIBUTES"},POST_TELEMETRY:{name:"Post telemetry",value:"POST_TELEMETRY"},RPC_REQUEST:{name:"RPC Request",value:"RPC_REQUEST"}},originatorSource:{CUSTOMER:{name:"tb.rulenode.originator-customer",value:"CUSTOMER"},TENANT:{name:"tb.rulenode.originator-tenant",value:"TENANT"},RELATED:{name:"tb.rulenode.originator-related",value:"RELATED"}}}).name},function(e,t,n,a){"use strict";function r(e){return e&&e.__esModule?e:{default:e}}function i(e){var t=function(t,n,a,r){var i=o.default;n.html(i),t.$watch("configuration",function(e,n){angular.equals(e,n)||r.$setViewValue(t.configuration)}),r.$render=function(){t.configuration=r.$viewValue},e(n.contents())(t)};return{restrict:"E",require:"^ngModel",scope:{},link:t}}i.$inject=["$compile"],Object.defineProperty(t,"__esModule",{value:!0}),t.default=i;var l=n(a),o=r(l)}]));
2 2 //# sourceMappingURL=rulenode-core-config.js.map
\ No newline at end of file
... ...
... ... @@ -253,7 +253,7 @@ function RuleChainService($http, $q, $filter, $ocLazyLoad, $translate, types, co
253 253 if (ruleChainConnections && ruleChainConnections.length) {
254 254 var tasks = [];
255 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 258 $q.all(tasks).then(
259 259 (ruleChains) => {
... ... @@ -273,6 +273,21 @@ function RuleChainService($http, $q, $filter, $ocLazyLoad, $translate, types, co
273 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 291 function loadRuleNodeComponents() {
277 292 return componentDescriptorService.getComponentDescriptorsByTypes(types.ruleNodeTypeComponentTypes);
278 293 }
... ...
... ... @@ -46,6 +46,7 @@ export default function RelationFilters($compile, $templateCache) {
46 46 ngModelCtrl.$render = function () {
47 47 if (ngModelCtrl.$viewValue) {
48 48 var value = ngModelCtrl.$viewValue;
  49 + scope.relationFilters.length = 0;
49 50 value.forEach(function (filter) {
50 51 scope.relationFilters.push(filter);
51 52 });
... ...
... ... @@ -15,13 +15,13 @@
15 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 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 22 <div translate class="tb-cell" flex="20">event.message-id</div>
23 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 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 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 26 <md-button ng-if="event.body.data" class="md-icon-button md-primary"
27 27 ng-click="showContent($event, event.body.data, 'event.data', event.body.dataType)"
28 28 aria-label="{{ 'action.view' | translate }}">
... ... @@ -35,7 +35,7 @@
35 35 </md-icon>
36 36 </md-button>
37 37 </div>
38   -<div class="tb-cell" flex="20">
  38 +<div class="tb-cell" flex="10">
39 39 <md-button ng-if="event.body.metadata" class="md-icon-button md-primary"
40 40 ng-click="showContent($event, event.body.metadata, 'event.metadata', 'JSON')"
41 41 aria-label="{{ 'action.view' | translate }}">
... ... @@ -48,7 +48,7 @@
48 48 </md-icon>
49 49 </md-button>
50 50 </div>
51   -<div class="tb-cell" flex="20">
  51 +<div class="tb-cell" flex="10">
52 52 <md-button ng-if="event.body.error" class="md-icon-button md-primary"
53 53 ng-click="showContent($event, event.body.error, 'event.error')"
54 54 aria-label="{{ 'action.view' | translate }}">
... ...
... ... @@ -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 97 $compile(element.contents())(scope);
90 98 }
91 99
... ...
... ... @@ -24,6 +24,17 @@ md-list.tb-event-table {
24 24 height: 48px;
25 25 padding: 0px;
26 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 40 .tb-row:hover {
... ... @@ -39,13 +50,19 @@ md-list.tb-event-table {
39 50 color: rgba(0,0,0,.54);
40 51 font-size: 12px;
41 52 font-weight: 700;
42   - white-space: nowrap;
43 53 background: none;
  54 + white-space: nowrap;
44 55 }
45 56 }
46 57
47 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 66 margin: auto 0;
50 67 color: rgba(0,0,0,.87);
51 68 font-size: 13px;
... ... @@ -53,8 +70,8 @@ md-list.tb-event-table {
53 70 text-align: left;
54 71 overflow: hidden;
55 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 281
282 282 function exportRuleChain(ruleChainId) {
283 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 332 function importRuleChain($event) {
301 333 var deferred = $q.defer();
302 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 337 toast.showError($translate.instant('rulechain.invalid-rulechain-file-error'));
306 338 deferred.reject();
307 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 343 function fail() {
... ... @@ -323,10 +347,14 @@ export default function ImportExport($log, $translate, $q, $mdDialog, $document,
323 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 358 return false;
331 359 }
332 360 return true;
... ...
... ... @@ -43,6 +43,7 @@ export default angular.module('thingsboard.locale', [])
43 43 "update": "Update",
44 44 "remove": "Remove",
45 45 "search": "Search",
  46 + "clear-search": "Clear search",
46 47 "assign": "Assign",
47 48 "unassign": "Unassign",
48 49 "share": "Share",
... ... @@ -1174,7 +1175,7 @@ export default angular.module('thingsboard.locale', [])
1174 1175 "export": "Export rule chain",
1175 1176 "export-failed-error": "Unable to export rule chain: {{error}}",
1176 1177 "create-new-rulechain": "Create new rule chain",
1177   - "rule-file": "Rule chain file",
  1178 + "rulechain-file": "Rule chain file",
1178 1179 "invalid-rulechain-file-error": "Unable to import rule chain: Invalid rule chain data structure.",
1179 1180 "copyId": "Copy rule chain Id",
1180 1181 "idCopiedMessage": "Rule chain Id has been copied to clipboard",
... ... @@ -1188,6 +1189,7 @@ export default angular.module('thingsboard.locale', [])
1188 1189 "details": "Details",
1189 1190 "events": "Events",
1190 1191 "search": "Search nodes",
  1192 + "open-node-library": "Open node library",
1191 1193 "add": "Add rule node",
1192 1194 "name": "Name",
1193 1195 "name-required": "Name is required.",
... ... @@ -1217,7 +1219,8 @@ export default angular.module('thingsboard.locale', [])
1217 1219 "type-rule-chain": "Rule Chain",
1218 1220 "type-rule-chain-details": "Forwards incoming messages to specified Rule Chain",
1219 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 1225 "rule-plugin": {
1223 1226 "management": "Rules and plugins management"
... ...
... ... @@ -28,7 +28,7 @@ import addRuleNodeLinkTemplate from './add-link.tpl.html';
28 28 /* eslint-enable import/no-unresolved, import/default */
29 29
30 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 32 $filter, $translate, hotkeys, types, ruleChainService, Modelfactory, flowchartConstants,
33 33 ruleChain, ruleChainMetaData, ruleNodeComponents) {
34 34
... ... @@ -37,6 +37,24 @@ export function RuleChainController($stateParams, $scope, $compile, $q, $mdUtil,
37 37 vm.$mdExpansionPanel = $mdExpansionPanel;
38 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 58 vm.editingRuleNode = null;
41 59 vm.isEditingRuleNode = false;
42 60
... ... @@ -57,6 +75,7 @@ export function RuleChainController($stateParams, $scope, $compile, $q, $mdUtil,
57 75 };
58 76
59 77 vm.ruleNodeTypesModel = {};
  78 + vm.ruleNodeTypesCanvasControl = {};
60 79 vm.ruleChainLibraryLoaded = false;
61 80 for (var type in types.ruleNodeType) {
62 81 if (!types.ruleNodeType[type].special) {
... ... @@ -67,9 +86,12 @@ export function RuleChainController($stateParams, $scope, $compile, $q, $mdUtil,
67 86 },
68 87 selectedObjects: []
69 88 };
  89 + vm.ruleNodeTypesCanvasControl[type] = {};
70 90 }
71 91 }
72 92
  93 +
  94 +
73 95 vm.selectedObjects = [];
74 96
75 97 vm.modelservice = Modelfactory(vm.ruleChainModel, vm.selectedObjects);
... ... @@ -145,8 +167,12 @@ export function RuleChainController($stateParams, $scope, $compile, $q, $mdUtil,
145 167 $scope.$broadcast('form-submit');
146 168 if (theForm.$valid) {
147 169 theForm.$setPristine();
  170 + if (vm.editingRuleNode.error) {
  171 + delete vm.editingRuleNode.error;
  172 + }
148 173 vm.ruleChainModel.nodes[vm.editingRuleNodeIndex] = vm.editingRuleNode;
149 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 229 }
204 230 var instances = angular.element.tooltipster.instances();
205 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 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 345 vm.editCallbacks = {
253 346 edgeDoubleClick: function (event, edge) {
254 347 var sourceNode = vm.modelservice.nodes.getNodeByConnectorId(edge.source);
... ... @@ -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 428 for (var i=0;i<ruleNodeComponents.length;i++) {
320 429 var ruleNodeComponent = ruleNodeComponents[i];
321   - var componentType = ruleNodeComponent.type;
  430 + componentType = ruleNodeComponent.type;
322 431 var model = vm.ruleNodeTypesModel[componentType].model;
323 432 var node = {
324 433 id: 'node-lib-' + componentType + '-' + model.nodes.length,
... ... @@ -349,7 +458,26 @@ export function RuleChainController($stateParams, $scope, $compile, $q, $mdUtil,
349 458 model.nodes.push(node);
350 459 }
351 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 483 function prepareRuleChain() {
... ... @@ -480,11 +608,9 @@ export function RuleChainController($stateParams, $scope, $compile, $q, $mdUtil,
480 608 ruleChainNode = {
481 609 id: 'rule-chain-node-' + vm.nextNodeID++,
482 610 additionalInfo: ruleChainConnection.additionalInfo,
483   - targetRuleChainId: ruleChainConnection.targetRuleChainId.id,
484 611 x: ruleChainConnection.additionalInfo.layoutX,
485 612 y: ruleChainConnection.additionalInfo.layoutY,
486 613 component: types.ruleChainNodeComponent,
487   - name: ruleChain.name,
488 614 nodeClass: vm.types.ruleNodeType.RULE_CHAIN.nodeClass,
489 615 icon: vm.types.ruleNodeType.RULE_CHAIN.icon,
490 616 connectors: [
... ... @@ -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 631 ruleChainNodesMap[ruleChainConnection.additionalInfo.ruleChainNodeId] = ruleChainNode;
498 632 vm.ruleChainModel.nodes.push(ruleChainNode);
499 633 }
... ... @@ -519,89 +653,141 @@ export function RuleChainController($stateParams, $scope, $compile, $q, $mdUtil,
519 653
520 654 vm.isDirty = false;
521 655
  656 + updateRuleNodesHighlight();
  657 +
  658 + validate();
  659 +
522 660 $mdUtil.nextTick(() => {
523 661 vm.ruleChainWatch = $scope.$watch('vm.ruleChainModel',
524 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 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 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 800
615 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 805 $mdDialog.show({
618 806 controller: 'AddRuleNodeController',
619 807 controllerAs: 'vm',
620 808 templateUrl: addRuleNodeTemplate,
621 809 parent: angular.element($document[0].body),
622   - locals: {ruleNode: ruleNode, ruleChainId: vm.ruleChain.id.id},
  810 + locals: {ruleNode: ruleNode, ruleChainId: ruleChainId},
623 811 fullscreen: true,
624 812 targetEvent: $event
625 813 }).then(function (ruleNode) {
... ... @@ -642,6 +830,7 @@ export function RuleChainController($stateParams, $scope, $compile, $q, $mdUtil,
642 830 );
643 831 }
644 832 vm.ruleChainModel.nodes.push(ruleNode);
  833 + updateRuleNodesHighlight();
645 834 }, function () {
646 835 });
647 836 }
... ...
... ... @@ -76,11 +76,52 @@ export default function RuleChainRoutes($stateProvider, NodeTemplatePathProvider
76 76 }
77 77 },
78 78 data: {
79   - searchEnabled: false,
  79 + import: false,
  80 + searchEnabled: true,
80 81 pageTitle: 'rulechain.rulechain'
81 82 },
82 83 ncyBreadcrumb: {
83 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 125 color: #333;
126 126 border: solid 1px #777;
127 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 138 &.tb-input-type {
129 139 background-color: #a3eaa9;
130 140 user-select: none;
... ... @@ -156,7 +166,7 @@
156 166
157 167 }
158 168 .tb-node-title {
159   - font-weight: 600;
  169 + font-weight: 500;
160 170 }
161 171 .tb-node-type, .tb-node-title {
162 172 overflow: hidden;
... ... @@ -380,6 +390,14 @@
380 390 font-size: 14px;
381 391 width: 300px;
382 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 401 #tooltip-content {
384 402 .tb-node-title {
385 403 font-weight: 600;
... ...
... ... @@ -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 20 expand-tooltip-direction="bottom" layout="column" class="tb-rulechain"
21 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 23 <section class="tb-rulechain-container" flex layout="column">
24 24 <div class="tb-rulechain-layout" flex layout="row">
25 25 <section layout="row" layout-wrap
26 26 class="tb-header-buttons md-fab tb-library-open">
27 27 <md-button ng-show="!vm.isLibraryOpen"
28 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 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 33 </md-tooltip>
34 34 <ng-md-icon icon="menu"></ng-md-icon>
35 35 </md-button>
... ... @@ -43,7 +43,7 @@
43 43 <div class="md-toolbar-tools">
44 44 <md-button class="md-icon-button tb-small" aria-label="{{ 'action.search' | translate }}">
45 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 47 {{'rulenode.search' | translate}}
48 48 </md-tooltip>
49 49 </md-button>
... ... @@ -53,15 +53,17 @@
53 53 <input ng-model="vm.ruleNodeSearch" placeholder="{{'rulenode.search' | translate}}"/>
54 54 </md-input-container>
55 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 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 62 </md-tooltip>
61 63 </md-button>
62 64 <md-button class="md-icon-button tb-small" aria-label="Close" ng-click="vm.isLibraryOpen = false">
63 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 67 {{ 'action.close' | translate }}
66 68 </md-tooltip>
67 69 </md-button>
... ... @@ -90,6 +92,7 @@
90 92 callbacks="vm.nodeLibCallbacks"
91 93 node-width="170"
92 94 node-height="50"
  95 + control="vm.ruleNodeTypesCanvasControl[typeId]"
93 96 drop-target-id="'tb-rulchain-canvas'"></fc-canvas>
94 97 </md-expansion-panel-content>
95 98 </md-expansion-panel-expanded>
... ... @@ -182,7 +185,7 @@
182 185 </md-tooltip>
183 186 <ng-md-icon icon="delete"></ng-md-icon>
184 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 189 class="tb-btn-footer md-accent md-hue-2 md-fab"
187 190 aria-label="{{ 'action.apply' | translate }}"
188 191 ng-click="vm.saveRuleChain()">
... ...
... ... @@ -63,8 +63,8 @@ export default function RuleChainsController(ruleChainService, userService, impo
63 63 {
64 64 onAction: function ($event) {
65 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 23 ng-mouseenter="callbacks.mouseEnter($event, node)"
24 24 ng-mouseleave="callbacks.mouseLeave($event, node)">
25 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 27 <md-icon aria-label="node-type-icon" flex="15"
28 28 class="material-icons">{{node.icon}}</md-icon>
29 29 <div layout="column" flex="85" layout-align="center">
... ...