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()));
this.ruleNode = newRuleNode;
if (restartRequired) {
tbNode.destroy();
if (tbNode != null) {
tbNode.destroy();
}
start(context);
}
}
@Override
public void stop(ActorContext context) throws Exception {
tbNode.destroy();
if (tbNode != null) {
tbNode.destroy();
}
context.stop(self);
}

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 " +
"with specific prefix: <i>cs/shared/ss</i>. To access those attributes in other nodes this template can be used " +
"<code>metadata.cs.temperature</code> or <code>metadata.shared.limit</code> " +
"If Latest Telemetry enrichment configured, latest telemetry added into metadata without prefix.")
"If Latest Telemetry enrichment configured, latest telemetry added into metadata without prefix.",
uiResources = {"static/rulenode/rulenode-core-config.js"},
configDirective = "tbEnrichmentNodeOriginatorAttributesConfig")
public class TbGetAttributesNode implements TbNode {
private TbGetAttributesNodeConfiguration config;

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",
nodeDetails = "If Attributes enrichment configured, server scope attributes are added into Message metadata. " +
"To access those attributes in other nodes this template can be used " +
"<code>metadata.temperature</code>. If Latest Telemetry enrichment configured, latest telemetry added into metadata")
"<code>metadata.temperature</code>. If Latest Telemetry enrichment configured, latest telemetry added into metadata",
uiResources = {"static/rulenode/rulenode-core-config.js", "static/rulenode/rulenode-core-config.css"},
configDirective = "tbEnrichmentNodeCustomerAttributesConfig")
public class TbGetCustomerAttributeNode extends TbEntityGetAttrNode<CustomerId> {
@Override

View File

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

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 Attributes enrichment configured, server scope attributes are added into Message metadata. " +
"To access those attributes in other nodes this template can be used " +
"<code>metadata.temperature</code>. If Latest Telemetry enrichment configured, latest telemetry added into metadata")
"<code>metadata.temperature</code>. If Latest Telemetry enrichment configured, latest telemetry added into metadata",
uiResources = {"static/rulenode/rulenode-core-config.js", "static/rulenode/rulenode-core-config.css"},
configDirective = "tbEnrichmentNodeRelatedAttributesConfig")
public class TbGetRelatedAttributeNode extends TbEntityGetAttrNode<EntityId> {
private TbGetRelatedAttrNodeConfiguration config;
@ -45,6 +48,6 @@ public class TbGetRelatedAttributeNode extends TbEntityGetAttrNode<EntityId> {
@Override
protected ListenableFuture<EntityId> findEntityAsync(TbContext ctx, EntityId originator) {
return EntitiesRelatedEntityIdAsyncLoader.findEntityAsync(ctx, originator, config.getDirection(), config.getRelationType());
return EntitiesRelatedEntityIdAsyncLoader.findEntityAsync(ctx, originator, config.getRelationsQuery());
}
}

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",
nodeDetails = "If Attributes enrichment configured, server scope attributes are added into Message metadata. " +
"To access those attributes in other nodes this template can be used " +
"<code>metadata.temperature</code>. If Latest Telemetry enrichment configured, latest telemetry added into metadata")
"<code>metadata.temperature</code>. If Latest Telemetry enrichment configured, latest telemetry added into metadata",
uiResources = {"static/rulenode/rulenode-core-config.js", "static/rulenode/rulenode-core-config.css"},
configDirective = "tbEnrichmentNodeTenantAttributesConfig")
public class TbGetTenantAttributeNode extends TbEntityGetAttrNode<TenantId> {
@Override

View File

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

View File

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

View File

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

View File

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

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*/

View File

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

View File

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

View File

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

View File

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

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);
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

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

View File

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

View File

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