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

This commit is contained in:
Andrew Shvayka 2018-04-03 08:13:29 +03:00
commit b38a7d7bda
27 changed files with 573 additions and 176 deletions

View File

@ -69,14 +69,18 @@ public class RuleNodeActorMessageProcessor extends ComponentMsgProcessor<RuleNod
&& ruleNode.getConfiguration().equals(newRuleNode.getConfiguration())); && ruleNode.getConfiguration().equals(newRuleNode.getConfiguration()));
this.ruleNode = newRuleNode; this.ruleNode = newRuleNode;
if (restartRequired) { if (restartRequired) {
if (tbNode != null) {
tbNode.destroy(); tbNode.destroy();
}
start(context); start(context);
} }
} }
@Override @Override
public void stop(ActorContext context) throws Exception { public void stop(ActorContext context) throws Exception {
if (tbNode != null) {
tbNode.destroy(); tbNode.destroy();
}
context.stop(self); context.stop(self);
} }

View File

@ -0,0 +1,31 @@
/**
* Copyright © 2016-2018 The Thingsboard Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.thingsboard.rule.engine.data;
import lombok.Data;
import org.thingsboard.server.common.data.relation.EntitySearchDirection;
import org.thingsboard.server.common.data.relation.EntityTypeFilter;
import java.util.List;
@Data
public class RelationsQuery {
private EntitySearchDirection direction;
private int maxLevel = 1;
private List<EntityTypeFilter> filters;
}

View File

@ -43,7 +43,9 @@ import static org.thingsboard.server.common.data.DataConstants.*;
nodeDetails = "If Attributes enrichment configured, <b>CLIENT/SHARED/SERVER</b> attributes are added into Message metadata " + nodeDetails = "If Attributes enrichment configured, <b>CLIENT/SHARED/SERVER</b> attributes are added into Message metadata " +
"with specific prefix: <i>cs/shared/ss</i>. To access those attributes in other nodes this template can be used " + "with specific prefix: <i>cs/shared/ss</i>. To access those attributes in other nodes this template can be used " +
"<code>metadata.cs.temperature</code> or <code>metadata.shared.limit</code> " + "<code>metadata.cs.temperature</code> or <code>metadata.shared.limit</code> " +
"If Latest Telemetry enrichment configured, latest telemetry added into metadata without prefix.") "If Latest Telemetry enrichment configured, latest telemetry added into metadata without prefix.",
uiResources = {"static/rulenode/rulenode-core-config.js"},
configDirective = "tbEnrichmentNodeOriginatorAttributesConfig")
public class TbGetAttributesNode implements TbNode { public class TbGetAttributesNode implements TbNode {
private TbGetAttributesNodeConfiguration config; private TbGetAttributesNodeConfiguration config;

View File

@ -30,7 +30,9 @@ import org.thingsboard.server.common.data.plugin.ComponentType;
nodeDescription = "Add Originators Customer Attributes or Latest Telemetry into Message Metadata", nodeDescription = "Add Originators Customer Attributes or Latest Telemetry into Message Metadata",
nodeDetails = "If Attributes enrichment configured, server scope attributes are added into Message metadata. " + nodeDetails = "If Attributes enrichment configured, server scope attributes are added into Message metadata. " +
"To access those attributes in other nodes this template can be used " + "To access those attributes in other nodes this template can be used " +
"<code>metadata.temperature</code>. If Latest Telemetry enrichment configured, latest telemetry added into metadata") "<code>metadata.temperature</code>. If Latest Telemetry enrichment configured, latest telemetry added into metadata",
uiResources = {"static/rulenode/rulenode-core-config.js", "static/rulenode/rulenode-core-config.css"},
configDirective = "tbEnrichmentNodeCustomerAttributesConfig")
public class TbGetCustomerAttributeNode extends TbEntityGetAttrNode<CustomerId> { public class TbGetCustomerAttributeNode extends TbEntityGetAttrNode<CustomerId> {
@Override @Override

View File

@ -16,18 +16,19 @@
package org.thingsboard.rule.engine.metadata; package org.thingsboard.rule.engine.metadata;
import lombok.Data; import lombok.Data;
import org.thingsboard.rule.engine.api.NodeConfiguration; import org.thingsboard.rule.engine.data.RelationsQuery;
import org.thingsboard.server.common.data.relation.EntityRelation; import org.thingsboard.server.common.data.relation.EntityRelation;
import org.thingsboard.server.common.data.relation.EntitySearchDirection; import org.thingsboard.server.common.data.relation.EntitySearchDirection;
import org.thingsboard.server.common.data.relation.EntityTypeFilter;
import java.util.Collections;
import java.util.HashMap; import java.util.HashMap;
import java.util.Map; import java.util.Map;
@Data @Data
public class TbGetRelatedAttrNodeConfiguration extends TbGetEntityAttrNodeConfiguration { public class TbGetRelatedAttrNodeConfiguration extends TbGetEntityAttrNodeConfiguration {
private String relationType; private RelationsQuery relationsQuery;
private EntitySearchDirection direction;
@Override @Override
public TbGetRelatedAttrNodeConfiguration defaultConfiguration() { public TbGetRelatedAttrNodeConfiguration defaultConfiguration() {
@ -36,8 +37,14 @@ public class TbGetRelatedAttrNodeConfiguration extends TbGetEntityAttrNodeConfig
attrMapping.putIfAbsent("temperature", "tempo"); attrMapping.putIfAbsent("temperature", "tempo");
configuration.setAttrMapping(attrMapping); configuration.setAttrMapping(attrMapping);
configuration.setTelemetry(true); configuration.setTelemetry(true);
configuration.setRelationType(EntityRelation.CONTAINS_TYPE);
configuration.setDirection(EntitySearchDirection.FROM); RelationsQuery relationsQuery = new RelationsQuery();
relationsQuery.setDirection(EntitySearchDirection.FROM);
relationsQuery.setMaxLevel(1);
EntityTypeFilter entityTypeFilter = new EntityTypeFilter(EntityRelation.CONTAINS_TYPE, Collections.emptyList());
relationsQuery.setFilters(Collections.singletonList(entityTypeFilter));
configuration.setRelationsQuery(relationsQuery);
return configuration; return configuration;
} }
} }

View File

@ -32,7 +32,10 @@ import org.thingsboard.server.common.data.plugin.ComponentType;
"If multiple Related Entities are found, only first Entity is used for attributes enrichment, other entities are discarded. " + "If multiple Related Entities are found, only first Entity is used for attributes enrichment, other entities are discarded. " +
"If Attributes enrichment configured, server scope attributes are added into Message metadata. " + "If Attributes enrichment configured, server scope attributes are added into Message metadata. " +
"To access those attributes in other nodes this template can be used " + "To access those attributes in other nodes this template can be used " +
"<code>metadata.temperature</code>. If Latest Telemetry enrichment configured, latest telemetry added into metadata") "<code>metadata.temperature</code>. If Latest Telemetry enrichment configured, latest telemetry added into metadata",
uiResources = {"static/rulenode/rulenode-core-config.js", "static/rulenode/rulenode-core-config.css"},
configDirective = "tbEnrichmentNodeRelatedAttributesConfig")
public class TbGetRelatedAttributeNode extends TbEntityGetAttrNode<EntityId> { public class TbGetRelatedAttributeNode extends TbEntityGetAttrNode<EntityId> {
private TbGetRelatedAttrNodeConfiguration config; private TbGetRelatedAttrNodeConfiguration config;
@ -45,6 +48,6 @@ public class TbGetRelatedAttributeNode extends TbEntityGetAttrNode<EntityId> {
@Override @Override
protected ListenableFuture<EntityId> findEntityAsync(TbContext ctx, EntityId originator) { protected ListenableFuture<EntityId> findEntityAsync(TbContext ctx, EntityId originator) {
return EntitiesRelatedEntityIdAsyncLoader.findEntityAsync(ctx, originator, config.getDirection(), config.getRelationType()); return EntitiesRelatedEntityIdAsyncLoader.findEntityAsync(ctx, originator, config.getRelationsQuery());
} }
} }

View File

@ -32,7 +32,9 @@ import org.thingsboard.server.common.data.plugin.ComponentType;
nodeDescription = "Add Originators Tenant Attributes or Latest Telemetry into Message Metadata", nodeDescription = "Add Originators Tenant Attributes or Latest Telemetry into Message Metadata",
nodeDetails = "If Attributes enrichment configured, server scope attributes are added into Message metadata. " + nodeDetails = "If Attributes enrichment configured, server scope attributes are added into Message metadata. " +
"To access those attributes in other nodes this template can be used " + "To access those attributes in other nodes this template can be used " +
"<code>metadata.temperature</code>. If Latest Telemetry enrichment configured, latest telemetry added into metadata") "<code>metadata.temperature</code>. If Latest Telemetry enrichment configured, latest telemetry added into metadata",
uiResources = {"static/rulenode/rulenode-core-config.js", "static/rulenode/rulenode-core-config.css"},
configDirective = "tbEnrichmentNodeTenantAttributesConfig")
public class TbGetTenantAttributeNode extends TbEntityGetAttrNode<TenantId> { public class TbGetTenantAttributeNode extends TbEntityGetAttrNode<TenantId> {
@Override @Override

View File

@ -39,7 +39,9 @@ import java.util.HashSet;
configClazz = TbChangeOriginatorNodeConfiguration.class, configClazz = TbChangeOriginatorNodeConfiguration.class,
nodeDescription = "Change Message Originator To Tenant/Customer/Related Entity", nodeDescription = "Change Message Originator To Tenant/Customer/Related Entity",
nodeDetails = "Related Entity found using configured relation direction and Relation Type. " + nodeDetails = "Related Entity found using configured relation direction and Relation Type. " +
"If multiple Related Entities are found, only first Entity is used as new Originator, other entities are discarded. ") "If multiple Related Entities are found, only first Entity is used as new Originator, other entities are discarded. ",
uiResources = {"static/rulenode/rulenode-core-config.js", "static/rulenode/rulenode-core-config.css"},
configDirective = "tbTransformationNodeChangeOriginatorConfig")
public class TbChangeOriginatorNode extends TbAbstractTransformNode { public class TbChangeOriginatorNode extends TbAbstractTransformNode {
protected static final String CUSTOMER_SOURCE = "CUSTOMER"; protected static final String CUSTOMER_SOURCE = "CUSTOMER";
@ -68,7 +70,7 @@ public class TbChangeOriginatorNode extends TbAbstractTransformNode {
case TENANT_SOURCE: case TENANT_SOURCE:
return EntitiesTenantIdAsyncLoader.findEntityIdAsync(ctx, original); return EntitiesTenantIdAsyncLoader.findEntityIdAsync(ctx, original);
case RELATED_SOURCE: case RELATED_SOURCE:
return EntitiesRelatedEntityIdAsyncLoader.findEntityAsync(ctx, original, config.getDirection(), config.getRelationType()); return EntitiesRelatedEntityIdAsyncLoader.findEntityAsync(ctx, original, config.getRelationsQuery());
default: default:
return Futures.immediateFailedFuture(new IllegalStateException("Unexpected originator source " + config.getOriginatorSource())); return Futures.immediateFailedFuture(new IllegalStateException("Unexpected originator source " + config.getOriginatorSource()));
} }
@ -82,9 +84,9 @@ public class TbChangeOriginatorNode extends TbAbstractTransformNode {
} }
if (conf.getOriginatorSource().equals(RELATED_SOURCE)) { if (conf.getOriginatorSource().equals(RELATED_SOURCE)) {
if (conf.getDirection() == null || StringUtils.isBlank(conf.getRelationType())) { if (conf.getRelationsQuery() == null) {
log.error("Related source for TbChangeOriginatorNode should have direction and relationType. Actual [{}] [{}]", log.error("Related source for TbChangeOriginatorNode should have relations query. Actual [{}]",
conf.getDirection(), conf.getRelationType()); conf.getRelationsQuery());
throw new IllegalArgumentException("Wrong config for RElated Source in TbChangeOriginatorNode" + conf.getOriginatorSource()); throw new IllegalArgumentException("Wrong config for RElated Source in TbChangeOriginatorNode" + conf.getOriginatorSource());
} }
} }

View File

@ -17,22 +17,32 @@ package org.thingsboard.rule.engine.transform;
import lombok.Data; import lombok.Data;
import org.thingsboard.rule.engine.api.NodeConfiguration; import org.thingsboard.rule.engine.api.NodeConfiguration;
import org.thingsboard.rule.engine.data.RelationsQuery;
import org.thingsboard.server.common.data.relation.EntityRelation; import org.thingsboard.server.common.data.relation.EntityRelation;
import org.thingsboard.server.common.data.relation.EntitySearchDirection; import org.thingsboard.server.common.data.relation.EntitySearchDirection;
import org.thingsboard.server.common.data.relation.EntityTypeFilter;
import java.util.Collections;
@Data @Data
public class TbChangeOriginatorNodeConfiguration extends TbTransformNodeConfiguration implements NodeConfiguration { public class TbChangeOriginatorNodeConfiguration extends TbTransformNodeConfiguration implements NodeConfiguration {
private String originatorSource; private String originatorSource;
private EntitySearchDirection direction;
private String relationType; private RelationsQuery relationsQuery;
@Override @Override
public TbChangeOriginatorNodeConfiguration defaultConfiguration() { public TbChangeOriginatorNodeConfiguration defaultConfiguration() {
TbChangeOriginatorNodeConfiguration configuration = new TbChangeOriginatorNodeConfiguration(); TbChangeOriginatorNodeConfiguration configuration = new TbChangeOriginatorNodeConfiguration();
configuration.setOriginatorSource(TbChangeOriginatorNode.CUSTOMER_SOURCE); configuration.setOriginatorSource(TbChangeOriginatorNode.CUSTOMER_SOURCE);
configuration.setDirection(EntitySearchDirection.FROM);
configuration.setRelationType(EntityRelation.CONTAINS_TYPE); RelationsQuery relationsQuery = new RelationsQuery();
relationsQuery.setDirection(EntitySearchDirection.FROM);
relationsQuery.setMaxLevel(1);
EntityTypeFilter entityTypeFilter = new EntityTypeFilter(EntityRelation.CONTAINS_TYPE, Collections.emptyList());
relationsQuery.setFilters(Collections.singletonList(entityTypeFilter));
configuration.setRelationsQuery(relationsQuery);
configuration.setStartNewChain(false); configuration.setStartNewChain(false);
return configuration; return configuration;
} }

View File

@ -31,7 +31,9 @@ import javax.script.Bindings;
nodeDescription = "Change Message payload and Metadata using JavaScript", nodeDescription = "Change Message payload and Metadata using JavaScript",
nodeDetails = "JavaScript function recieve 2 input parameters that can be changed inside.<br/> " + nodeDetails = "JavaScript function recieve 2 input parameters that can be changed inside.<br/> " +
"<code>metadata</code> - is a Message metadata.<br/>" + "<code>metadata</code> - is a Message metadata.<br/>" +
"<code>msg</code> - is a Message payload.<br/>Any properties can be changed/removed/added in those objects.") "<code>msg</code> - is a Message payload.<br/>Any properties can be changed/removed/added in those objects.",
uiResources = {"static/rulenode/rulenode-core-config.js", "static/rulenode/rulenode-core-config.css"},
configDirective = "tbTransformationNodeScriptConfig")
public class TbTransformMsgNode extends TbAbstractTransformNode { public class TbTransformMsgNode extends TbAbstractTransformNode {
private TbTransformMsgNodeConfiguration config; private TbTransformMsgNodeConfiguration config;

View File

@ -20,32 +20,41 @@ import com.google.common.util.concurrent.Futures;
import com.google.common.util.concurrent.ListenableFuture; import com.google.common.util.concurrent.ListenableFuture;
import org.apache.commons.collections.CollectionUtils; import org.apache.commons.collections.CollectionUtils;
import org.thingsboard.rule.engine.api.TbContext; import org.thingsboard.rule.engine.api.TbContext;
import org.thingsboard.rule.engine.data.RelationsQuery;
import org.thingsboard.server.common.data.id.EntityId; import org.thingsboard.server.common.data.id.EntityId;
import org.thingsboard.server.common.data.relation.EntityRelation; import org.thingsboard.server.common.data.relation.EntityRelation;
import org.thingsboard.server.common.data.relation.EntityRelationsQuery;
import org.thingsboard.server.common.data.relation.EntitySearchDirection; import org.thingsboard.server.common.data.relation.EntitySearchDirection;
import org.thingsboard.server.common.data.relation.RelationsSearchParameters;
import org.thingsboard.server.dao.relation.RelationService; import org.thingsboard.server.dao.relation.RelationService;
import java.util.List; import java.util.List;
import static org.thingsboard.server.common.data.relation.RelationTypeGroup.COMMON;
public class EntitiesRelatedEntityIdAsyncLoader { public class EntitiesRelatedEntityIdAsyncLoader {
public static ListenableFuture<EntityId> findEntityAsync(TbContext ctx, EntityId originator, public static ListenableFuture<EntityId> findEntityAsync(TbContext ctx, EntityId originator,
EntitySearchDirection direction, String relationType) { RelationsQuery relationsQuery) {
RelationService relationService = ctx.getRelationService(); RelationService relationService = ctx.getRelationService();
if (direction == EntitySearchDirection.FROM) { EntityRelationsQuery query = buildQuery(originator, relationsQuery);
ListenableFuture<List<EntityRelation>> asyncRelation = relationService.findByFromAndTypeAsync(originator, relationType, COMMON); ListenableFuture<List<EntityRelation>> asyncRelation = relationService.findByQuery(query);
if (relationsQuery.getDirection() == EntitySearchDirection.FROM) {
return Futures.transform(asyncRelation, (AsyncFunction<? super List<EntityRelation>, EntityId>) return Futures.transform(asyncRelation, (AsyncFunction<? super List<EntityRelation>, EntityId>)
r -> CollectionUtils.isNotEmpty(r) ? Futures.immediateFuture(r.get(0).getTo()) r -> CollectionUtils.isNotEmpty(r) ? Futures.immediateFuture(r.get(0).getTo())
: Futures.immediateFailedFuture(new IllegalStateException("Relation not found"))); : Futures.immediateFailedFuture(new IllegalStateException("Relation not found")));
} else if (direction == EntitySearchDirection.TO) { } else if (relationsQuery.getDirection() == EntitySearchDirection.TO) {
ListenableFuture<List<EntityRelation>> asyncRelation = relationService.findByToAndTypeAsync(originator, relationType, COMMON);
return Futures.transform(asyncRelation, (AsyncFunction<? super List<EntityRelation>, EntityId>) return Futures.transform(asyncRelation, (AsyncFunction<? super List<EntityRelation>, EntityId>)
r -> CollectionUtils.isNotEmpty(r) ? Futures.immediateFuture(r.get(0).getFrom()) r -> CollectionUtils.isNotEmpty(r) ? Futures.immediateFuture(r.get(0).getFrom())
: Futures.immediateFailedFuture(new IllegalStateException("Relation not found"))); : Futures.immediateFailedFuture(new IllegalStateException("Relation not found")));
} }
return Futures.immediateFailedFuture(new IllegalStateException("Unknown direction")); return Futures.immediateFailedFuture(new IllegalStateException("Unknown direction"));
} }
private static EntityRelationsQuery buildQuery(EntityId originator, RelationsQuery relationsQuery) {
EntityRelationsQuery query = new EntityRelationsQuery();
RelationsSearchParameters parameters = new RelationsSearchParameters(originator,
relationsQuery.getDirection(), relationsQuery.getMaxLevel());
query.setParameters(parameters);
query.setFilters(relationsQuery.getFilters());
return query;
}
} }

View File

@ -1,2 +1,2 @@
.tb-message-type-autocomplete .tb-not-found{display:block;line-height:1.5;height:48px}.tb-message-type-autocomplete .tb-not-found .tb-no-entries{line-height:48px}.tb-message-type-autocomplete li{height:auto!important;white-space:normal!important} .tb-message-type-autocomplete .tb-not-found{display:block;line-height:1.5;height:48px}.tb-message-type-autocomplete .tb-not-found .tb-no-entries{line-height:48px}.tb-message-type-autocomplete li{height:auto!important;white-space:normal!important}.tb-kv-map-config .header{padding-left:5px;padding-right:5px;padding-bottom:5px}.tb-kv-map-config .header .cell{padding-left:5px;padding-right:5px;color:rgba(0,0,0,.54);font-size:12px;font-weight:700;white-space:nowrap}.tb-kv-map-config .body{padding-left:5px;padding-right:5px;padding-bottom:20px;max-height:300px;overflow:auto}.tb-kv-map-config .body .row{padding-top:5px;max-height:40px}.tb-kv-map-config .body .cell{padding-left:5px;padding-right:5px}.tb-kv-map-config .body md-input-container.cell{margin:0;max-height:40px}.tb-kv-map-config .body .md-button{margin:0}
/*# sourceMappingURL=rulenode-core-config.css.map*/ /*# sourceMappingURL=rulenode-core-config.css.map*/

View File

@ -253,7 +253,7 @@ function RuleChainService($http, $q, $filter, $ocLazyLoad, $translate, types, co
if (ruleChainConnections && ruleChainConnections.length) { if (ruleChainConnections && ruleChainConnections.length) {
var tasks = []; var tasks = [];
for (var i = 0; i < ruleChainConnections.length; i++) { for (var i = 0; i < ruleChainConnections.length; i++) {
tasks.push(getRuleChain(ruleChainConnections[i].targetRuleChainId.id)); tasks.push(resolveRuleChain(ruleChainConnections[i].targetRuleChainId.id));
} }
$q.all(tasks).then( $q.all(tasks).then(
(ruleChains) => { (ruleChains) => {
@ -273,6 +273,21 @@ function RuleChainService($http, $q, $filter, $ocLazyLoad, $translate, types, co
return deferred.promise; return deferred.promise;
} }
function resolveRuleChain(ruleChainId) {
var deferred = $q.defer();
getRuleChain(ruleChainId, {ignoreErrors: true}).then(
(ruleChain) => {
deferred.resolve(ruleChain);
},
() => {
deferred.resolve({
id: {id: ruleChainId, entityType: types.entityType.rulechain}
});
}
);
return deferred.promise;
}
function loadRuleNodeComponents() { function loadRuleNodeComponents() {
return componentDescriptorService.getComponentDescriptorsByTypes(types.ruleNodeTypeComponentTypes); return componentDescriptorService.getComponentDescriptorsByTypes(types.ruleNodeTypeComponentTypes);
} }

View File

@ -46,6 +46,7 @@ export default function RelationFilters($compile, $templateCache) {
ngModelCtrl.$render = function () { ngModelCtrl.$render = function () {
if (ngModelCtrl.$viewValue) { if (ngModelCtrl.$viewValue) {
var value = ngModelCtrl.$viewValue; var value = ngModelCtrl.$viewValue;
scope.relationFilters.length = 0;
value.forEach(function (filter) { value.forEach(function (filter) {
scope.relationFilters.push(filter); scope.relationFilters.push(filter);
}); });

View File

@ -15,13 +15,13 @@
limitations under the License. limitations under the License.
--> -->
<div hide-xs hide-sm translate class="tb-cell" flex="30">event.event-time</div> <div hide-xs hide-sm translate class="tb-cell" flex="25">event.event-time</div>
<div translate class="tb-cell" flex="20">event.server</div> <div translate class="tb-cell" flex="20">event.server</div>
<div translate class="tb-cell" flex="20">event.type</div> <div translate class="tb-cell" flex="10">event.type</div>
<div translate class="tb-cell" flex="20">event.entity</div> <div translate class="tb-cell" flex="15">event.entity</div>
<div translate class="tb-cell" flex="20">event.message-id</div> <div translate class="tb-cell" flex="20">event.message-id</div>
<div translate class="tb-cell" flex="20">event.message-type</div> <div translate class="tb-cell" flex="20">event.message-type</div>
<div translate class="tb-cell" flex="20">event.data-type</div> <div translate class="tb-cell" flex="15">event.data-type</div>
<div translate class="tb-cell" flex="20">event.data</div> <div translate class="tb-cell" flex="10">event.data</div>
<div translate class="tb-cell" flex="20">event.metadata</div> <div translate class="tb-cell" flex="10">event.metadata</div>
<div translate class="tb-cell" flex="20">event.error</div> <div translate class="tb-cell" flex="10">event.error</div>

View File

@ -15,14 +15,14 @@
limitations under the License. limitations under the License.
--> -->
<div hide-xs hide-sm class="tb-cell" flex="30">{{event.createdTime | date : 'yyyy-MM-dd HH:mm:ss'}}</div> <div hide-xs hide-sm class="tb-cell" flex="25">{{event.createdTime | date : 'yyyy-MM-dd HH:mm:ss'}}</div>
<div class="tb-cell" flex="20">{{event.body.server}}</div> <div class="tb-cell" flex="20">{{event.body.server}}</div>
<div class="tb-cell" flex="20">{{event.body.type}}</div> <div class="tb-cell" flex="10">{{event.body.type}}</div>
<div class="tb-cell" flex="20">{{event.body.entityName}}</div> <div class="tb-cell" flex="15">{{event.body.entityName}}</div>
<div class="tb-cell" flex="20">{{event.body.msgId}}</div> <div class="tb-cell tb-nowrap" flex="20" ng-mouseenter="checkTooltip($event)">{{event.body.msgId}}</div>
<div class="tb-cell" flex="20">{{event.body.msgType}}</div> <div class="tb-cell" flex="20" ng-mouseenter="checkTooltip($event)">{{event.body.msgType}}</div>
<div class="tb-cell" flex="20">{{event.body.dataType}}</div> <div class="tb-cell" flex="15">{{event.body.dataType}}</div>
<div class="tb-cell" flex="20"> <div class="tb-cell" flex="10">
<md-button ng-if="event.body.data" class="md-icon-button md-primary" <md-button ng-if="event.body.data" class="md-icon-button md-primary"
ng-click="showContent($event, event.body.data, 'event.data', event.body.dataType)" ng-click="showContent($event, event.body.data, 'event.data', event.body.dataType)"
aria-label="{{ 'action.view' | translate }}"> aria-label="{{ 'action.view' | translate }}">
@ -35,7 +35,7 @@
</md-icon> </md-icon>
</md-button> </md-button>
</div> </div>
<div class="tb-cell" flex="20"> <div class="tb-cell" flex="10">
<md-button ng-if="event.body.metadata" class="md-icon-button md-primary" <md-button ng-if="event.body.metadata" class="md-icon-button md-primary"
ng-click="showContent($event, event.body.metadata, 'event.metadata', 'JSON')" ng-click="showContent($event, event.body.metadata, 'event.metadata', 'JSON')"
aria-label="{{ 'action.view' | translate }}"> aria-label="{{ 'action.view' | translate }}">
@ -48,7 +48,7 @@
</md-icon> </md-icon>
</md-button> </md-button>
</div> </div>
<div class="tb-cell" flex="20"> <div class="tb-cell" flex="10">
<md-button ng-if="event.body.error" class="md-icon-button md-primary" <md-button ng-if="event.body.error" class="md-icon-button md-primary"
ng-click="showContent($event, event.body.error, 'event.error')" ng-click="showContent($event, event.body.error, 'event.error')"
aria-label="{{ 'action.view' | translate }}"> aria-label="{{ 'action.view' | translate }}">

View File

@ -86,6 +86,14 @@ export default function EventRowDirective($compile, $templateCache, $mdDialog, $
}); });
} }
scope.checkTooltip = function($event) {
var el = $event.target;
var $el = angular.element(el);
if(el.offsetWidth < el.scrollWidth && !$el.attr('title')){
$el.attr('title', $el.text());
}
}
$compile(element.contents())(scope); $compile(element.contents())(scope);
} }

View File

@ -24,6 +24,17 @@ md-list.tb-event-table {
height: 48px; height: 48px;
padding: 0px; padding: 0px;
overflow: hidden; overflow: hidden;
.tb-cell {
text-overflow: ellipsis;
&.tb-scroll {
white-space: nowrap;
overflow-y: hidden;
overflow-x: auto;
}
&.tb-nowrap {
white-space: nowrap;
}
}
} }
.tb-row:hover { .tb-row:hover {
@ -39,13 +50,19 @@ md-list.tb-event-table {
color: rgba(0,0,0,.54); color: rgba(0,0,0,.54);
font-size: 12px; font-size: 12px;
font-weight: 700; font-weight: 700;
white-space: nowrap;
background: none; background: none;
white-space: nowrap;
} }
} }
.tb-cell { .tb-cell {
padding: 0 24px; &:first-child {
padding-left: 14px;
}
&:last-child {
padding-right: 14px;
}
padding: 0 6px;
margin: auto 0; margin: auto 0;
color: rgba(0,0,0,.87); color: rgba(0,0,0,.87);
font-size: 13px; font-size: 13px;

View File

@ -281,39 +281,63 @@ export default function ImportExport($log, $translate, $q, $mdDialog, $document,
function exportRuleChain(ruleChainId) { function exportRuleChain(ruleChainId) {
ruleChainService.getRuleChain(ruleChainId).then( ruleChainService.getRuleChain(ruleChainId).then(
function success(ruleChain) { (ruleChain) => {
ruleChainService.getRuleChainMetaData(ruleChainId).then(
(ruleChainMetaData) => {
var ruleChainExport = {
ruleChain: prepareRuleChain(ruleChain),
metadata: prepareRuleChainMetaData(ruleChainMetaData)
};
var name = ruleChain.name; var name = ruleChain.name;
name = name.toLowerCase().replace(/\W/g,"_"); name = name.toLowerCase().replace(/\W/g,"_");
exportToPc(prepareExport(ruleChain), name + '.json'); exportToPc(ruleChainExport, name + '.json');
//TODO: metadata
}, },
function fail(rejection) { (rejection) => {
processExportRuleChainRejection(rejection);
}
);
},
(rejection) => {
processExportRuleChainRejection(rejection);
}
);
}
function prepareRuleChain(ruleChain) {
ruleChain = prepareExport(ruleChain);
if (ruleChain.firstRuleNodeId) {
ruleChain.firstRuleNodeId = null;
}
ruleChain.root = false;
return ruleChain;
}
function prepareRuleChainMetaData(ruleChainMetaData) {
delete ruleChainMetaData.ruleChainId;
for (var i=0;i<ruleChainMetaData.nodes.length;i++) {
var node = ruleChainMetaData.nodes[i];
ruleChainMetaData.nodes[i] = prepareExport(node);
}
return ruleChainMetaData;
}
function processExportRuleChainRejection(rejection) {
var message = rejection; var message = rejection;
if (!message) { if (!message) {
message = $translate.instant('error.unknown-error'); message = $translate.instant('error.unknown-error');
} }
toast.showError($translate.instant('rulechain.export-failed-error', {error: message})); toast.showError($translate.instant('rulechain.export-failed-error', {error: message}));
} }
);
}
function importRuleChain($event) { function importRuleChain($event) {
var deferred = $q.defer(); var deferred = $q.defer();
openImportDialog($event, 'rulechain.import', 'rulechain.rulechain-file').then( openImportDialog($event, 'rulechain.import', 'rulechain.rulechain-file').then(
function success(ruleChain) { function success(ruleChainImport) {
if (!validateImportedRuleChain(ruleChain)) { if (!validateImportedRuleChain(ruleChainImport)) {
toast.showError($translate.instant('rulechain.invalid-rulechain-file-error')); toast.showError($translate.instant('rulechain.invalid-rulechain-file-error'));
deferred.reject(); deferred.reject();
} else { } else {
//TODO: rulechain metadata deferred.resolve(ruleChainImport);
ruleChainService.saveRuleChain(ruleChain).then(
function success() {
deferred.resolve();
},
function fail() {
deferred.reject();
}
);
} }
}, },
function fail() { function fail() {
@ -323,10 +347,14 @@ export default function ImportExport($log, $translate, $q, $mdDialog, $document,
return deferred.promise; return deferred.promise;
} }
function validateImportedRuleChain(ruleChain) { function validateImportedRuleChain(ruleChainImport) {
//TODO: rulechain metadata if (angular.isUndefined(ruleChainImport.ruleChain)) {
if (angular.isUndefined(ruleChain.name)) return false;
{ }
if (angular.isUndefined(ruleChainImport.metadata)) {
return false;
}
if (angular.isUndefined(ruleChainImport.ruleChain.name)) {
return false; return false;
} }
return true; return true;

View File

@ -43,6 +43,7 @@ export default angular.module('thingsboard.locale', [])
"update": "Update", "update": "Update",
"remove": "Remove", "remove": "Remove",
"search": "Search", "search": "Search",
"clear-search": "Clear search",
"assign": "Assign", "assign": "Assign",
"unassign": "Unassign", "unassign": "Unassign",
"share": "Share", "share": "Share",
@ -1174,7 +1175,7 @@ export default angular.module('thingsboard.locale', [])
"export": "Export rule chain", "export": "Export rule chain",
"export-failed-error": "Unable to export rule chain: {{error}}", "export-failed-error": "Unable to export rule chain: {{error}}",
"create-new-rulechain": "Create new rule chain", "create-new-rulechain": "Create new rule chain",
"rule-file": "Rule chain file", "rulechain-file": "Rule chain file",
"invalid-rulechain-file-error": "Unable to import rule chain: Invalid rule chain data structure.", "invalid-rulechain-file-error": "Unable to import rule chain: Invalid rule chain data structure.",
"copyId": "Copy rule chain Id", "copyId": "Copy rule chain Id",
"idCopiedMessage": "Rule chain Id has been copied to clipboard", "idCopiedMessage": "Rule chain Id has been copied to clipboard",
@ -1188,6 +1189,7 @@ export default angular.module('thingsboard.locale', [])
"details": "Details", "details": "Details",
"events": "Events", "events": "Events",
"search": "Search nodes", "search": "Search nodes",
"open-node-library": "Open node library",
"add": "Add rule node", "add": "Add rule node",
"name": "Name", "name": "Name",
"name-required": "Name is required.", "name-required": "Name is required.",
@ -1217,7 +1219,8 @@ export default angular.module('thingsboard.locale', [])
"type-rule-chain": "Rule Chain", "type-rule-chain": "Rule Chain",
"type-rule-chain-details": "Forwards incoming messages to specified Rule Chain", "type-rule-chain-details": "Forwards incoming messages to specified Rule Chain",
"directive-is-not-loaded": "Defined configuration directive '{{directiveName}}' is not available.", "directive-is-not-loaded": "Defined configuration directive '{{directiveName}}' is not available.",
"ui-resources-load-error": "Failed to load configuration ui resources." "ui-resources-load-error": "Failed to load configuration ui resources.",
"invalid-target-rulechain": "Unable to resolve target rule chain!"
}, },
"rule-plugin": { "rule-plugin": {
"management": "Rules and plugins management" "management": "Rules and plugins management"

View File

@ -28,7 +28,7 @@ import addRuleNodeLinkTemplate from './add-link.tpl.html';
/* eslint-enable import/no-unresolved, import/default */ /* eslint-enable import/no-unresolved, import/default */
/*@ngInject*/ /*@ngInject*/
export function RuleChainController($stateParams, $scope, $compile, $q, $mdUtil, $timeout, $mdExpansionPanel, $window, $document, $mdDialog, export function RuleChainController($state, $scope, $compile, $q, $mdUtil, $timeout, $mdExpansionPanel, $window, $document, $mdDialog,
$filter, $translate, hotkeys, types, ruleChainService, Modelfactory, flowchartConstants, $filter, $translate, hotkeys, types, ruleChainService, Modelfactory, flowchartConstants,
ruleChain, ruleChainMetaData, ruleNodeComponents) { ruleChain, ruleChainMetaData, ruleNodeComponents) {
@ -37,6 +37,24 @@ export function RuleChainController($stateParams, $scope, $compile, $q, $mdUtil,
vm.$mdExpansionPanel = $mdExpansionPanel; vm.$mdExpansionPanel = $mdExpansionPanel;
vm.types = types; vm.types = types;
if ($state.current.data.import && !ruleChain) {
$state.go('home.ruleChains');
return;
}
vm.isImport = $state.current.data.import;
vm.isConfirmOnExit = false;
$scope.$watch(function() {
return vm.isDirty || vm.isImport;
}, (val) => {
vm.isConfirmOnExit = val;
});
vm.errorTooltips = {};
vm.isFullscreen = false;
vm.editingRuleNode = null; vm.editingRuleNode = null;
vm.isEditingRuleNode = false; vm.isEditingRuleNode = false;
@ -57,6 +75,7 @@ export function RuleChainController($stateParams, $scope, $compile, $q, $mdUtil,
}; };
vm.ruleNodeTypesModel = {}; vm.ruleNodeTypesModel = {};
vm.ruleNodeTypesCanvasControl = {};
vm.ruleChainLibraryLoaded = false; vm.ruleChainLibraryLoaded = false;
for (var type in types.ruleNodeType) { for (var type in types.ruleNodeType) {
if (!types.ruleNodeType[type].special) { if (!types.ruleNodeType[type].special) {
@ -67,9 +86,12 @@ export function RuleChainController($stateParams, $scope, $compile, $q, $mdUtil,
}, },
selectedObjects: [] selectedObjects: []
}; };
vm.ruleNodeTypesCanvasControl[type] = {};
} }
} }
vm.selectedObjects = []; vm.selectedObjects = [];
vm.modelservice = Modelfactory(vm.ruleChainModel, vm.selectedObjects); vm.modelservice = Modelfactory(vm.ruleChainModel, vm.selectedObjects);
@ -145,8 +167,12 @@ export function RuleChainController($stateParams, $scope, $compile, $q, $mdUtil,
$scope.$broadcast('form-submit'); $scope.$broadcast('form-submit');
if (theForm.$valid) { if (theForm.$valid) {
theForm.$setPristine(); theForm.$setPristine();
if (vm.editingRuleNode.error) {
delete vm.editingRuleNode.error;
}
vm.ruleChainModel.nodes[vm.editingRuleNodeIndex] = vm.editingRuleNode; vm.ruleChainModel.nodes[vm.editingRuleNodeIndex] = vm.editingRuleNode;
vm.editingRuleNode = angular.copy(vm.editingRuleNode); vm.editingRuleNode = angular.copy(vm.editingRuleNode);
updateRuleNodesHighlight();
} }
}; };
@ -203,7 +229,9 @@ export function RuleChainController($stateParams, $scope, $compile, $q, $mdUtil,
} }
var instances = angular.element.tooltipster.instances(); var instances = angular.element.tooltipster.instances();
instances.forEach((instance) => { instances.forEach((instance) => {
if (!instance.isErrorTooltip) {
instance.destroy(); instance.destroy();
}
}); });
} }
@ -249,6 +277,71 @@ export function RuleChainController($stateParams, $scope, $compile, $q, $mdUtil,
}, 500); }, 500);
} }
function updateNodeErrorTooltip(node) {
if (node.error) {
var element = angular.element('#' + node.id);
var tooltip = vm.errorTooltips[node.id];
if (!tooltip || !element.hasClass("tooltipstered")) {
element.tooltipster(
{
theme: 'tooltipster-shadow',
delay: 0,
animationDuration: 0,
trigger: 'custom',
triggerOpen: {
click: false,
tap: false
},
triggerClose: {
click: false,
tap: false,
scroll: false
},
side: 'top',
trackOrigin: true
}
);
var content = '<div class="tb-rule-node-error-tooltip">' +
'<div id="tooltip-content" layout="column">' +
'<div class="tb-node-details">' + node.error + '</div>' +
'</div>' +
'</div>';
var contentElement = angular.element(content);
$compile(contentElement)($scope);
tooltip = element.tooltipster('instance');
tooltip.isErrorTooltip = true;
tooltip.content(contentElement);
vm.errorTooltips[node.id] = tooltip;
}
$mdUtil.nextTick(() => {
tooltip.open();
});
} else {
if (vm.errorTooltips[node.id]) {
tooltip = vm.errorTooltips[node.id];
tooltip.destroy();
delete vm.errorTooltips[node.id];
}
}
}
function updateErrorTooltips(hide) {
for (var nodeId in vm.errorTooltips) {
var tooltip = vm.errorTooltips[nodeId];
if (hide) {
tooltip.close();
} else {
tooltip.open();
}
}
}
$scope.$watch(function() {
return vm.isEditingRuleNode || vm.isEditingRuleNodeLink;
}, (val) => {
updateErrorTooltips(val);
});
vm.editCallbacks = { vm.editCallbacks = {
edgeDoubleClick: function (event, edge) { edgeDoubleClick: function (event, edge) {
var sourceNode = vm.modelservice.nodes.getNodeByConnectorId(edge.source); var sourceNode = vm.modelservice.nodes.getNodeByConnectorId(edge.source);
@ -313,12 +406,28 @@ export function RuleChainController($stateParams, $scope, $compile, $q, $mdUtil,
} }
}; };
loadRuleChainLibrary(); loadRuleChainLibrary(ruleNodeComponents, true);
function loadRuleChainLibrary() { $scope.$watch('vm.ruleNodeSearch',
function (newVal, oldVal) {
if (!angular.equals(newVal, oldVal)) {
var res = $filter('filter')(ruleNodeComponents, {name: vm.ruleNodeSearch});
loadRuleChainLibrary(res);
}
}
);
$scope.$on('searchTextUpdated', function () {
updateRuleNodesHighlight();
});
function loadRuleChainLibrary(ruleNodeComponents, loadRuleChain) {
for (var componentType in vm.ruleNodeTypesModel) {
vm.ruleNodeTypesModel[componentType].model.nodes.length = 0;
}
for (var i=0;i<ruleNodeComponents.length;i++) { for (var i=0;i<ruleNodeComponents.length;i++) {
var ruleNodeComponent = ruleNodeComponents[i]; var ruleNodeComponent = ruleNodeComponents[i];
var componentType = ruleNodeComponent.type; componentType = ruleNodeComponent.type;
var model = vm.ruleNodeTypesModel[componentType].model; var model = vm.ruleNodeTypesModel[componentType].model;
var node = { var node = {
id: 'node-lib-' + componentType + '-' + model.nodes.length, id: 'node-lib-' + componentType + '-' + model.nodes.length,
@ -349,8 +458,27 @@ export function RuleChainController($stateParams, $scope, $compile, $q, $mdUtil,
model.nodes.push(node); model.nodes.push(node);
} }
vm.ruleChainLibraryLoaded = true; vm.ruleChainLibraryLoaded = true;
if (loadRuleChain) {
prepareRuleChain(); prepareRuleChain();
} }
$mdUtil.nextTick(() => {
for (componentType in vm.ruleNodeTypesCanvasControl) {
if (vm.ruleNodeTypesCanvasControl[componentType].adjustCanvasSize) {
vm.ruleNodeTypesCanvasControl[componentType].adjustCanvasSize(true);
}
}
for (componentType in vm.ruleNodeTypesModel) {
var panel = vm.$mdExpansionPanel(componentType);
if (panel) {
if (!vm.ruleNodeTypesModel[componentType].model.nodes.length) {
panel.collapse();
} else {
panel.expand();
}
}
}
});
}
function prepareRuleChain() { function prepareRuleChain() {
@ -480,11 +608,9 @@ export function RuleChainController($stateParams, $scope, $compile, $q, $mdUtil,
ruleChainNode = { ruleChainNode = {
id: 'rule-chain-node-' + vm.nextNodeID++, id: 'rule-chain-node-' + vm.nextNodeID++,
additionalInfo: ruleChainConnection.additionalInfo, additionalInfo: ruleChainConnection.additionalInfo,
targetRuleChainId: ruleChainConnection.targetRuleChainId.id,
x: ruleChainConnection.additionalInfo.layoutX, x: ruleChainConnection.additionalInfo.layoutX,
y: ruleChainConnection.additionalInfo.layoutY, y: ruleChainConnection.additionalInfo.layoutY,
component: types.ruleChainNodeComponent, component: types.ruleChainNodeComponent,
name: ruleChain.name,
nodeClass: vm.types.ruleNodeType.RULE_CHAIN.nodeClass, nodeClass: vm.types.ruleNodeType.RULE_CHAIN.nodeClass,
icon: vm.types.ruleNodeType.RULE_CHAIN.icon, icon: vm.types.ruleNodeType.RULE_CHAIN.icon,
connectors: [ connectors: [
@ -494,6 +620,14 @@ export function RuleChainController($stateParams, $scope, $compile, $q, $mdUtil,
} }
] ]
}; };
if (ruleChain.name) {
ruleChainNode.name = ruleChain.name;
ruleChainNode.targetRuleChainId = ruleChainConnection.targetRuleChainId.id;
} else {
ruleChainNode.name = "Unresolved";
ruleChainNode.targetRuleChainId = null;
ruleChainNode.error = $translate.instant('rulenode.invalid-target-rulechain');
}
ruleChainNodesMap[ruleChainConnection.additionalInfo.ruleChainNodeId] = ruleChainNode; ruleChainNodesMap[ruleChainConnection.additionalInfo.ruleChainNodeId] = ruleChainNode;
vm.ruleChainModel.nodes.push(ruleChainNode); vm.ruleChainModel.nodes.push(ruleChainNode);
} }
@ -519,18 +653,60 @@ export function RuleChainController($stateParams, $scope, $compile, $q, $mdUtil,
vm.isDirty = false; vm.isDirty = false;
updateRuleNodesHighlight();
validate();
$mdUtil.nextTick(() => { $mdUtil.nextTick(() => {
vm.ruleChainWatch = $scope.$watch('vm.ruleChainModel', vm.ruleChainWatch = $scope.$watch('vm.ruleChainModel',
function (newVal, oldVal) { function (newVal, oldVal) {
if (!vm.isDirty && !angular.equals(newVal, oldVal)) { if (!angular.equals(newVal, oldVal)) {
validate();
if (!vm.isDirty) {
vm.isDirty = true; vm.isDirty = true;
} }
}
}, true }, true
); );
}); });
} }
function updateRuleNodesHighlight() {
for (var i = 0; i < vm.ruleChainModel.nodes.length; i++) {
vm.ruleChainModel.nodes[i].highlighted = false;
}
if ($scope.searchConfig.searchText) {
var res = $filter('filter')(vm.ruleChainModel.nodes, {name: $scope.searchConfig.searchText});
if (res) {
for (i = 0; i < res.length; i++) {
res[i].highlighted = true;
}
}
}
}
function validate() {
$mdUtil.nextTick(() => {
vm.isInvalid = false;
for (var i = 0; i < vm.ruleChainModel.nodes.length; i++) {
if (vm.ruleChainModel.nodes[i].error) {
vm.isInvalid = true;
}
updateNodeErrorTooltip(vm.ruleChainModel.nodes[i]);
}
});
}
function saveRuleChain() { function saveRuleChain() {
var saveRuleChainPromise;
if (vm.isImport) {
saveRuleChainPromise = ruleChainService.saveRuleChain(vm.ruleChain);
} else {
saveRuleChainPromise = $q.when(vm.ruleChain);
}
saveRuleChainPromise.then(
(ruleChain) => {
vm.ruleChain = ruleChain;
var ruleChainMetaData = { var ruleChainMetaData = {
ruleChainId: vm.ruleChain.id, ruleChainId: vm.ruleChain.id,
nodes: [], nodes: [],
@ -601,8 +777,18 @@ export function RuleChainController($stateParams, $scope, $compile, $q, $mdUtil,
ruleChainService.saveRuleChainMetaData(ruleChainMetaData).then( ruleChainService.saveRuleChainMetaData(ruleChainMetaData).then(
(ruleChainMetaData) => { (ruleChainMetaData) => {
vm.ruleChainMetaData = ruleChainMetaData; vm.ruleChainMetaData = ruleChainMetaData;
if (vm.isImport) {
vm.isDirty = false;
vm.isImport = false;
$mdUtil.nextTick(() => {
$state.go('home.ruleChains.ruleChain', {ruleChainId: vm.ruleChain.id.id});
});
} else {
prepareRuleChain(); prepareRuleChain();
} }
}
);
}
); );
} }
@ -614,12 +800,14 @@ export function RuleChainController($stateParams, $scope, $compile, $q, $mdUtil,
ruleNode.configuration = angular.copy(ruleNode.component.configurationDescriptor.nodeDefinition.defaultConfiguration); ruleNode.configuration = angular.copy(ruleNode.component.configurationDescriptor.nodeDefinition.defaultConfiguration);
var ruleChainId = vm.ruleChain.id ? vm.ruleChain.id.id : null;
$mdDialog.show({ $mdDialog.show({
controller: 'AddRuleNodeController', controller: 'AddRuleNodeController',
controllerAs: 'vm', controllerAs: 'vm',
templateUrl: addRuleNodeTemplate, templateUrl: addRuleNodeTemplate,
parent: angular.element($document[0].body), parent: angular.element($document[0].body),
locals: {ruleNode: ruleNode, ruleChainId: vm.ruleChain.id.id}, locals: {ruleNode: ruleNode, ruleChainId: ruleChainId},
fullscreen: true, fullscreen: true,
targetEvent: $event targetEvent: $event
}).then(function (ruleNode) { }).then(function (ruleNode) {
@ -642,6 +830,7 @@ export function RuleChainController($stateParams, $scope, $compile, $q, $mdUtil,
); );
} }
vm.ruleChainModel.nodes.push(ruleNode); vm.ruleChainModel.nodes.push(ruleNode);
updateRuleNodesHighlight();
}, function () { }, function () {
}); });
} }

View File

@ -76,11 +76,52 @@ export default function RuleChainRoutes($stateProvider, NodeTemplatePathProvider
} }
}, },
data: { data: {
searchEnabled: false, import: false,
searchEnabled: true,
pageTitle: 'rulechain.rulechain' pageTitle: 'rulechain.rulechain'
}, },
ncyBreadcrumb: { ncyBreadcrumb: {
label: '{"icon": "settings_ethernet", "label": "{{ vm.ruleChain.name }}", "translate": "false"}' label: '{"icon": "settings_ethernet", "label": "{{ vm.ruleChain.name }}", "translate": "false"}'
} }
}).state('home.ruleChains.importRuleChain', {
url: '/ruleChain/import',
reloadOnSearch: false,
module: 'private',
auth: ['SYS_ADMIN', 'TENANT_ADMIN'],
views: {
"content@home": {
templateUrl: ruleChainTemplate,
controller: 'RuleChainController',
controllerAs: 'vm'
}
},
params: {
ruleChainImport: {}
},
resolve: {
ruleChain:
/*@ngInject*/
function($stateParams) {
return $stateParams.ruleChainImport.ruleChain;
},
ruleChainMetaData:
/*@ngInject*/
function($stateParams) {
return $stateParams.ruleChainImport.metadata;
},
ruleNodeComponents:
/*@ngInject*/
function($stateParams, ruleChainService) {
return ruleChainService.getRuleNodeComponents();
}
},
data: {
import: true,
searchEnabled: true,
pageTitle: 'rulechain.rulechain'
},
ncyBreadcrumb: {
label: '{"icon": "settings_ethernet", "label": "{{ (\'rulechain.import\' | translate) + \': \'+ vm.ruleChain.name }}", "translate": "false"}'
}
}); });
} }

View File

@ -125,6 +125,16 @@
color: #333; color: #333;
border: solid 1px #777; border: solid 1px #777;
font-size: 12px; font-size: 12px;
&.tb-rule-node-highlighted:not(.tb-rule-node-invalid) {
box-shadow: 0 0 10px 6px #51cbee;
.tb-node-title {
text-decoration: underline;
font-weight: bold;
}
}
&.tb-rule-node-invalid {
box-shadow: 0 0 10px 6px #ff5c50;
}
&.tb-input-type { &.tb-input-type {
background-color: #a3eaa9; background-color: #a3eaa9;
user-select: none; user-select: none;
@ -156,7 +166,7 @@
} }
.tb-node-title { .tb-node-title {
font-weight: 600; font-weight: 500;
} }
.tb-node-type, .tb-node-title { .tb-node-type, .tb-node-title {
overflow: hidden; overflow: hidden;
@ -380,6 +390,14 @@
font-size: 14px; font-size: 14px;
width: 300px; width: 300px;
color: #333; color: #333;
}
.tb-rule-node-error-tooltip {
font-size: 16px;
color: #ea0d0d;
}
.tb-rule-node-tooltip, .tb-rule-node-error-tooltip {
#tooltip-content { #tooltip-content {
.tb-node-title { .tb-node-title {
font-weight: 600; font-weight: 600;

View File

@ -16,20 +16,20 @@
--> -->
<md-content flex tb-expand-fullscreen tb-confirm-on-exit is-dirty="vm.isDirty" <md-content flex tb-expand-fullscreen tb-confirm-on-exit is-dirty="vm.isConfirmOnExit"
expand-tooltip-direction="bottom" layout="column" class="tb-rulechain" expand-tooltip-direction="bottom" layout="column" class="tb-rulechain"
ng-keydown="vm.keyDown($event)" ng-keydown="vm.keyDown($event)"
ng-keyup="vm.keyUp($event)"> ng-keyup="vm.keyUp($event)" on-fullscreen-changed="vm.isFullscreen = expanded">
<section class="tb-rulechain-container" flex layout="column"> <section class="tb-rulechain-container" flex layout="column">
<div class="tb-rulechain-layout" flex layout="row"> <div class="tb-rulechain-layout" flex layout="row">
<section layout="row" layout-wrap <section layout="row" layout-wrap
class="tb-header-buttons md-fab tb-library-open"> class="tb-header-buttons md-fab tb-library-open">
<md-button ng-show="!vm.isLibraryOpen" <md-button ng-show="!vm.isLibraryOpen"
class="tb-btn-header tb-btn-open-library md-primary md-fab md-fab-top-left" class="tb-btn-header tb-btn-open-library md-primary md-fab md-fab-top-left"
aria-label="{{ 'action.apply' | translate }}" aria-label="{{ 'rulenode.open-node-library' | translate }}"
ng-click="vm.isLibraryOpen = true"> ng-click="vm.isLibraryOpen = true">
<md-tooltip md-direction="top"> <md-tooltip md-direction="{{vm.isFullscreen ? 'bottom' : 'top'}}">
{{ 'action.apply-changes' | translate }} {{ 'rulenode.open-node-library' | translate }}
</md-tooltip> </md-tooltip>
<ng-md-icon icon="menu"></ng-md-icon> <ng-md-icon icon="menu"></ng-md-icon>
</md-button> </md-button>
@ -43,7 +43,7 @@
<div class="md-toolbar-tools"> <div class="md-toolbar-tools">
<md-button class="md-icon-button tb-small" aria-label="{{ 'action.search' | translate }}"> <md-button class="md-icon-button tb-small" aria-label="{{ 'action.search' | translate }}">
<md-icon aria-label="{{ 'action.search' | translate }}" class="material-icons">search</md-icon> <md-icon aria-label="{{ 'action.search' | translate }}" class="material-icons">search</md-icon>
<md-tooltip md-direction="top"> <md-tooltip md-direction="{{vm.isFullscreen ? 'bottom' : 'top'}}">
{{'rulenode.search' | translate}} {{'rulenode.search' | translate}}
</md-tooltip> </md-tooltip>
</md-button> </md-button>
@ -53,15 +53,17 @@
<input ng-model="vm.ruleNodeSearch" placeholder="{{'rulenode.search' | translate}}"/> <input ng-model="vm.ruleNodeSearch" placeholder="{{'rulenode.search' | translate}}"/>
</md-input-container> </md-input-container>
</div> </div>
<md-button class="md-icon-button tb-small" aria-label="Close" ng-click="vm.ruleNodeSearch = ''"> <md-button class="md-icon-button tb-small" aria-label="Close"
ng-show="vm.ruleNodeSearch"
ng-click="vm.ruleNodeSearch = ''">
<md-icon aria-label="Close" class="material-icons">close</md-icon> <md-icon aria-label="Close" class="material-icons">close</md-icon>
<md-tooltip md-direction="top"> <md-tooltip md-direction="{{vm.isFullscreen ? 'bottom' : 'top'}}">
{{ 'action.close' | translate }} {{ 'action.clear-search' | translate }}
</md-tooltip> </md-tooltip>
</md-button> </md-button>
<md-button class="md-icon-button tb-small" aria-label="Close" ng-click="vm.isLibraryOpen = false"> <md-button class="md-icon-button tb-small" aria-label="Close" ng-click="vm.isLibraryOpen = false">
<md-icon aria-label="Close" class="material-icons">chevron_left</md-icon> <md-icon aria-label="Close" class="material-icons">chevron_left</md-icon>
<md-tooltip md-direction="top"> <md-tooltip md-direction="{{vm.isFullscreen ? 'bottom' : 'top'}}">
{{ 'action.close' | translate }} {{ 'action.close' | translate }}
</md-tooltip> </md-tooltip>
</md-button> </md-button>
@ -90,6 +92,7 @@
callbacks="vm.nodeLibCallbacks" callbacks="vm.nodeLibCallbacks"
node-width="170" node-width="170"
node-height="50" node-height="50"
control="vm.ruleNodeTypesCanvasControl[typeId]"
drop-target-id="'tb-rulchain-canvas'"></fc-canvas> drop-target-id="'tb-rulchain-canvas'"></fc-canvas>
</md-expansion-panel-content> </md-expansion-panel-content>
</md-expansion-panel-expanded> </md-expansion-panel-expanded>
@ -182,7 +185,7 @@
</md-tooltip> </md-tooltip>
<ng-md-icon icon="delete"></ng-md-icon> <ng-md-icon icon="delete"></ng-md-icon>
</md-button> </md-button>
<md-button ng-disabled="$root.loading || !vm.isDirty" <md-button ng-disabled="$root.loading || vm.isInvalid || (!vm.isDirty && !vm.isImport)"
class="tb-btn-footer md-accent md-hue-2 md-fab" class="tb-btn-footer md-accent md-hue-2 md-fab"
aria-label="{{ 'action.apply' | translate }}" aria-label="{{ 'action.apply' | translate }}"
ng-click="vm.saveRuleChain()"> ng-click="vm.saveRuleChain()">

View File

@ -63,8 +63,8 @@ export default function RuleChainsController(ruleChainService, userService, impo
{ {
onAction: function ($event) { onAction: function ($event) {
importExport.importRuleChain($event).then( importExport.importRuleChain($event).then(
function() { function(ruleChainImport) {
vm.grid.refreshList(); $state.go('home.ruleChains.importRuleChain', {ruleChainImport:ruleChainImport});
} }
); );
}, },

View File

@ -23,7 +23,7 @@
ng-mouseenter="callbacks.mouseEnter($event, node)" ng-mouseenter="callbacks.mouseEnter($event, node)"
ng-mouseleave="callbacks.mouseLeave($event, node)"> ng-mouseleave="callbacks.mouseLeave($event, node)">
<div class="{{flowchartConstants.nodeOverlayClass}}"></div> <div class="{{flowchartConstants.nodeOverlayClass}}"></div>
<div class="tb-rule-node {{node.nodeClass}}"> <div class="tb-rule-node {{node.nodeClass}}" ng-class="{'tb-rule-node-highlighted' : node.highlighted, 'tb-rule-node-invalid': node.error }">
<md-icon aria-label="node-type-icon" flex="15" <md-icon aria-label="node-type-icon" flex="15"
class="material-icons">{{node.icon}}</md-icon> class="material-icons">{{node.icon}}</md-icon>
<div layout="column" flex="85" layout-align="center"> <div layout="column" flex="85" layout-align="center">