Commit 6ece02f88f864f5a9551b870b3f40e8a33eb20c8

Authored by Igor Kulikov
1 parent 7bc74a1c

Rule chains UI

@@ -15,6 +15,7 @@ @@ -15,6 +15,7 @@
15 */ 15 */
16 package org.thingsboard.server.common.data.rule; 16 package org.thingsboard.server.common.data.rule;
17 17
  18 +import com.fasterxml.jackson.databind.JsonNode;
18 import lombok.Data; 19 import lombok.Data;
19 import org.thingsboard.server.common.data.id.RuleChainId; 20 import org.thingsboard.server.common.data.id.RuleChainId;
20 21
@@ -47,11 +48,12 @@ public class RuleChainMetaData { @@ -47,11 +48,12 @@ public class RuleChainMetaData {
47 } 48 }
48 connections.add(connectionInfo); 49 connections.add(connectionInfo);
49 } 50 }
50 - public void addRuleChainConnectionInfo(int fromIndex, RuleChainId targetRuleChainId, String type) { 51 + public void addRuleChainConnectionInfo(int fromIndex, RuleChainId targetRuleChainId, String type, JsonNode additionalInfo) {
51 RuleChainConnectionInfo connectionInfo = new RuleChainConnectionInfo(); 52 RuleChainConnectionInfo connectionInfo = new RuleChainConnectionInfo();
52 connectionInfo.setFromIndex(fromIndex); 53 connectionInfo.setFromIndex(fromIndex);
53 connectionInfo.setTargetRuleChainId(targetRuleChainId); 54 connectionInfo.setTargetRuleChainId(targetRuleChainId);
54 connectionInfo.setType(type); 55 connectionInfo.setType(type);
  56 + connectionInfo.setAdditionalInfo(additionalInfo);
55 if (ruleChainConnections == null) { 57 if (ruleChainConnections == null) {
56 ruleChainConnections = new ArrayList<>(); 58 ruleChainConnections = new ArrayList<>();
57 } 59 }
@@ -59,16 +61,17 @@ public class RuleChainMetaData { @@ -59,16 +61,17 @@ public class RuleChainMetaData {
59 } 61 }
60 62
61 @Data 63 @Data
62 - public class NodeConnectionInfo { 64 + public static class NodeConnectionInfo {
63 private int fromIndex; 65 private int fromIndex;
64 private int toIndex; 66 private int toIndex;
65 private String type; 67 private String type;
66 } 68 }
67 69
68 @Data 70 @Data
69 - public class RuleChainConnectionInfo { 71 + public static class RuleChainConnectionInfo {
70 private int fromIndex; 72 private int fromIndex;
71 private RuleChainId targetRuleChainId; 73 private RuleChainId targetRuleChainId;
  74 + private JsonNode additionalInfo;
72 private String type; 75 private String type;
73 } 76 }
74 77
@@ -166,7 +166,7 @@ public class BaseRuleChainService extends AbstractEntityService implements RuleC @@ -166,7 +166,7 @@ public class BaseRuleChainService extends AbstractEntityService implements RuleC
166 EntityId to = nodeToRuleChainConnection.getTargetRuleChainId(); 166 EntityId to = nodeToRuleChainConnection.getTargetRuleChainId();
167 String type = nodeToRuleChainConnection.getType(); 167 String type = nodeToRuleChainConnection.getType();
168 try { 168 try {
169 - createRelation(new EntityRelation(from, to, type, RelationTypeGroup.RULE_NODE)); 169 + createRelation(new EntityRelation(from, to, type, RelationTypeGroup.RULE_NODE, nodeToRuleChainConnection.getAdditionalInfo()));
170 } catch (ExecutionException | InterruptedException e) { 170 } catch (ExecutionException | InterruptedException e) {
171 log.warn("[{}] Failed to create rule node to rule chain relation. from: [{}], to: [{}]", from, to); 171 log.warn("[{}] Failed to create rule node to rule chain relation. from: [{}], to: [{}]", from, to);
172 throw new RuntimeException(e); 172 throw new RuntimeException(e);
@@ -206,7 +206,7 @@ public class BaseRuleChainService extends AbstractEntityService implements RuleC @@ -206,7 +206,7 @@ public class BaseRuleChainService extends AbstractEntityService implements RuleC
206 ruleChainMetaData.addConnectionInfo(fromIndex, toIndex, type); 206 ruleChainMetaData.addConnectionInfo(fromIndex, toIndex, type);
207 } else if (nodeRelation.getTo().getEntityType() == EntityType.RULE_CHAIN) { 207 } else if (nodeRelation.getTo().getEntityType() == EntityType.RULE_CHAIN) {
208 RuleChainId targetRuleChainId = new RuleChainId(nodeRelation.getTo().getId()); 208 RuleChainId targetRuleChainId = new RuleChainId(nodeRelation.getTo().getId());
209 - ruleChainMetaData.addRuleChainConnectionInfo(fromIndex, targetRuleChainId, type); 209 + ruleChainMetaData.addRuleChainConnectionInfo(fromIndex, targetRuleChainId, type, nodeRelation.getAdditionalInfo());
210 } 210 }
211 } 211 }
212 } 212 }
@@ -69,6 +69,7 @@ @@ -69,6 +69,7 @@
69 "moment": "^2.15.0", 69 "moment": "^2.15.0",
70 "ngclipboard": "^1.1.1", 70 "ngclipboard": "^1.1.1",
71 "ngreact": "^0.3.0", 71 "ngreact": "^0.3.0",
  72 + "ngFlowchart": "git://github.com/ikulikov/ngFlowchart.git#master",
72 "objectpath": "^1.2.1", 73 "objectpath": "^1.2.1",
73 "oclazyload": "^1.0.9", 74 "oclazyload": "^1.0.9",
74 "raphael": "^2.2.7", 75 "raphael": "^2.2.7",
@@ -22,7 +22,7 @@ export default angular.module('thingsboard.api.entity', [thingsboardTypes]) @@ -22,7 +22,7 @@ export default angular.module('thingsboard.api.entity', [thingsboardTypes])
22 /*@ngInject*/ 22 /*@ngInject*/
23 function EntityService($http, $q, $filter, $translate, $log, userService, deviceService, 23 function EntityService($http, $q, $filter, $translate, $log, userService, deviceService,
24 assetService, tenantService, customerService, 24 assetService, tenantService, customerService,
25 - ruleService, pluginService, dashboardService, entityRelationService, attributeService, types, utils) { 25 + ruleService, pluginService, ruleChainService, dashboardService, entityRelationService, attributeService, types, utils) {
26 var service = { 26 var service = {
27 getEntity: getEntity, 27 getEntity: getEntity,
28 getEntities: getEntities, 28 getEntities: getEntities,
@@ -73,6 +73,9 @@ function EntityService($http, $q, $filter, $translate, $log, userService, device @@ -73,6 +73,9 @@ function EntityService($http, $q, $filter, $translate, $log, userService, device
73 case types.entityType.user: 73 case types.entityType.user:
74 promise = userService.getUser(entityId, true, config); 74 promise = userService.getUser(entityId, true, config);
75 break; 75 break;
  76 + case types.entityType.rulechain:
  77 + promise = ruleChainService.getRuleChain(entityId, config);
  78 + break;
76 case types.entityType.alarm: 79 case types.entityType.alarm:
77 $log.error('Get Alarm Entity is not implemented!'); 80 $log.error('Get Alarm Entity is not implemented!');
78 break; 81 break;
@@ -271,6 +274,9 @@ function EntityService($http, $q, $filter, $translate, $log, userService, device @@ -271,6 +274,9 @@ function EntityService($http, $q, $filter, $translate, $log, userService, device
271 case types.entityType.plugin: 274 case types.entityType.plugin:
272 promise = pluginService.getAllPlugins(pageLink, config); 275 promise = pluginService.getAllPlugins(pageLink, config);
273 break; 276 break;
  277 + case types.entityType.rulechain:
  278 + promise = ruleChainService.getRuleChains(pageLink, config);
  279 + break;
274 case types.entityType.dashboard: 280 case types.entityType.dashboard:
275 if (user.authority === 'CUSTOMER_USER') { 281 if (user.authority === 'CUSTOMER_USER') {
276 promise = dashboardService.getCustomerDashboards(customerId, pageLink, config); 282 promise = dashboardService.getCustomerDashboards(customerId, pageLink, config);
@@ -17,7 +17,9 @@ export default angular.module('thingsboard.api.ruleChain', []) @@ -17,7 +17,9 @@ export default angular.module('thingsboard.api.ruleChain', [])
17 .factory('ruleChainService', RuleChainService).name; 17 .factory('ruleChainService', RuleChainService).name;
18 18
19 /*@ngInject*/ 19 /*@ngInject*/
20 -function RuleChainService($http, $q) { 20 +function RuleChainService($http, $q, $filter, types) {
  21 +
  22 + var ruleNodeTypes = null;
21 23
22 var service = { 24 var service = {
23 getSystemRuleChains: getSystemRuleChains, 25 getSystemRuleChains: getSystemRuleChains,
@@ -27,7 +29,11 @@ function RuleChainService($http, $q) { @@ -27,7 +29,11 @@ function RuleChainService($http, $q) {
27 saveRuleChain: saveRuleChain, 29 saveRuleChain: saveRuleChain,
28 deleteRuleChain: deleteRuleChain, 30 deleteRuleChain: deleteRuleChain,
29 getRuleChainMetaData: getRuleChainMetaData, 31 getRuleChainMetaData: getRuleChainMetaData,
30 - saveRuleChainMetaData: saveRuleChainMetaData 32 + saveRuleChainMetaData: saveRuleChainMetaData,
  33 + getRuleNodeTypes: getRuleNodeTypes,
  34 + getRuleNodeComponentType: getRuleNodeComponentType,
  35 + getRuleNodeSupportedLinks: getRuleNodeSupportedLinks,
  36 + resolveTargetRuleChains: resolveTargetRuleChains
31 }; 37 };
32 38
33 return service; 39 return service;
@@ -147,4 +153,131 @@ function RuleChainService($http, $q) { @@ -147,4 +153,131 @@ function RuleChainService($http, $q) {
147 return deferred.promise; 153 return deferred.promise;
148 } 154 }
149 155
  156 + function getRuleNodeSupportedLinks(nodeType) { //eslint-disable-line
  157 + //TODO:
  158 + var deferred = $q.defer();
  159 + var linkLabels = [
  160 + { name: 'Success', custom: false },
  161 + { name: 'Fail', custom: false },
  162 + { name: 'Custom', custom: true },
  163 + ];
  164 + deferred.resolve(linkLabels);
  165 + return deferred.promise;
  166 + }
  167 +
  168 + function getRuleNodeTypes() {
  169 + var deferred = $q.defer();
  170 + if (ruleNodeTypes) {
  171 + deferred.resolve(ruleNodeTypes);
  172 + } else {
  173 + loadRuleNodeTypes().then(
  174 + (nodeTypes) => {
  175 + ruleNodeTypes = nodeTypes;
  176 + ruleNodeTypes.push(
  177 + {
  178 + nodeType: types.ruleNodeType.RULE_CHAIN.value,
  179 + type: 'Rule chain'
  180 + }
  181 + );
  182 + deferred.resolve(ruleNodeTypes);
  183 + },
  184 + () => {
  185 + deferred.reject();
  186 + }
  187 + );
  188 + }
  189 + return deferred.promise;
  190 + }
  191 +
  192 + function getRuleNodeComponentType(type) {
  193 + var res = $filter('filter')(ruleNodeTypes, {type: type}, true);
  194 + if (res && res.length) {
  195 + return res[0].nodeType;
  196 + }
  197 + return null;
  198 + }
  199 +
  200 + function resolveTargetRuleChains(ruleChainConnections) {
  201 + var deferred = $q.defer();
  202 + if (ruleChainConnections && ruleChainConnections.length) {
  203 + var tasks = [];
  204 + for (var i = 0; i < ruleChainConnections.length; i++) {
  205 + tasks.push(getRuleChain(ruleChainConnections[i].targetRuleChainId.id));
  206 + }
  207 + $q.all(tasks).then(
  208 + (ruleChains) => {
  209 + var ruleChainsMap = {};
  210 + for (var i = 0; i < ruleChains.length; i++) {
  211 + ruleChainsMap[ruleChains[i].id.id] = ruleChains[i];
  212 + }
  213 + deferred.resolve(ruleChainsMap);
  214 + },
  215 + () => {
  216 + deferred.reject();
  217 + }
  218 + );
  219 + } else {
  220 + deferred.resolve({});
  221 + }
  222 + return deferred.promise;
  223 + }
  224 +
  225 + function loadRuleNodeTypes() {
  226 + var deferred = $q.defer();
  227 + deferred.resolve(
  228 + [
  229 + {
  230 + nodeType: types.ruleNodeType.FILTER.value,
  231 + type: 'Filter'
  232 + },
  233 + {
  234 + nodeType: types.ruleNodeType.FILTER.value,
  235 + type: 'Switch'
  236 + },
  237 + {
  238 + nodeType: types.ruleNodeType.ENRICHMENT.value,
  239 + type: 'Self'
  240 + },
  241 + {
  242 + nodeType: types.ruleNodeType.ENRICHMENT.value,
  243 + type: 'Tenant/Customer'
  244 + },
  245 + {
  246 + nodeType: types.ruleNodeType.ENRICHMENT.value,
  247 + type: 'Related Entity'
  248 + },
  249 + {
  250 + nodeType: types.ruleNodeType.ENRICHMENT.value,
  251 + type: 'Last Telemetry'
  252 + },
  253 + {
  254 + nodeType: types.ruleNodeType.TRANSFORMATION.value,
  255 + type: 'Modify'
  256 + },
  257 + {
  258 + nodeType: types.ruleNodeType.TRANSFORMATION.value,
  259 + type: 'New/Update'
  260 + },
  261 + {
  262 + nodeType: types.ruleNodeType.ACTION.value,
  263 + type: 'Telemetry'
  264 + },
  265 + {
  266 + nodeType: types.ruleNodeType.ACTION.value,
  267 + type: 'RPC call'
  268 + },
  269 + {
  270 + nodeType: types.ruleNodeType.ACTION.value,
  271 + type: 'Send email'
  272 + },
  273 + {
  274 + nodeType: types.ruleNodeType.ACTION.value,
  275 + type: 'Alarm'
  276 + }
  277 + ]
  278 + );
  279 + return deferred.promise;
  280 + }
  281 +
  282 +
150 } 283 }
@@ -49,6 +49,7 @@ import 'material-ui'; @@ -49,6 +49,7 @@ import 'material-ui';
49 import 'react-schema-form'; 49 import 'react-schema-form';
50 import react from 'ngreact'; 50 import react from 'ngreact';
51 import '@flowjs/ng-flow/dist/ng-flow-standalone.min'; 51 import '@flowjs/ng-flow/dist/ng-flow-standalone.min';
  52 +import 'ngFlowchart/dist/ngFlowchart';
52 53
53 import thingsboardLocales from './locale/locale.constant'; 54 import thingsboardLocales from './locale/locale.constant';
54 import thingsboardLogin from './login'; 55 import thingsboardLogin from './login';
@@ -86,6 +87,7 @@ import 'mdPickers/dist/mdPickers.min.css'; @@ -86,6 +87,7 @@ import 'mdPickers/dist/mdPickers.min.css';
86 import 'angular-hotkeys/build/hotkeys.min.css'; 87 import 'angular-hotkeys/build/hotkeys.min.css';
87 import 'angular-carousel/dist/angular-carousel.min.css'; 88 import 'angular-carousel/dist/angular-carousel.min.css';
88 import 'angular-material-expansion-panel/dist/md-expansion-panel.min.css'; 89 import 'angular-material-expansion-panel/dist/md-expansion-panel.min.css';
  90 +import 'ngFlowchart/dist/flowchart.css';
89 import '../scss/main.scss'; 91 import '../scss/main.scss';
90 92
91 import AppConfig from './app.config'; 93 import AppConfig from './app.config';
@@ -113,6 +115,7 @@ angular.module('thingsboard', [ @@ -113,6 +115,7 @@ angular.module('thingsboard', [
113 'ngclipboard', 115 'ngclipboard',
114 react.name, 116 react.name,
115 'flow', 117 'flow',
  118 + 'flowchart',
116 thingsboardLocales, 119 thingsboardLocales,
117 thingsboardLogin, 120 thingsboardLogin,
118 thingsboardDialogs, 121 thingsboardDialogs,
@@ -457,6 +457,44 @@ export default angular.module('thingsboard.types', []) @@ -457,6 +457,44 @@ export default angular.module('thingsboard.types', [])
457 clientSide: false 457 clientSide: false
458 } 458 }
459 }, 459 },
  460 + ruleNodeType: {
  461 + FILTER: {
  462 + value: "FILTER",
  463 + name: "rulenode.type-filter",
  464 + nodeClass: "tb-filter-type",
  465 + icon: "filter_list"
  466 + },
  467 + ENRICHMENT: {
  468 + value: "ENRICHMENT",
  469 + name: "rulenode.type-enrichment",
  470 + nodeClass: "tb-enrichment-type",
  471 + icon: "playlist_add"
  472 + },
  473 + TRANSFORMATION: {
  474 + value: "TRANSFORMATION",
  475 + name: "rulenode.type-transformation",
  476 + nodeClass: "tb-transformation-type",
  477 + icon: "transform"
  478 + },
  479 + ACTION: {
  480 + value: "ACTION",
  481 + name: "rulenode.type-action",
  482 + nodeClass: "tb-action-type",
  483 + icon: "flash_on"
  484 + },
  485 + RULE_CHAIN: {
  486 + value: "RULE_CHAIN",
  487 + name: "rulenode.type-rule-chain",
  488 + nodeClass: "tb-rule-chain-type",
  489 + icon: "settings_ethernet"
  490 + },
  491 + INPUT: {
  492 + value: "INPUT",
  493 + nodeClass: "tb-input-type",
  494 + icon: "input",
  495 + special: true
  496 + }
  497 + },
460 valueType: { 498 valueType: {
461 string: { 499 string: {
462 value: "string", 500 value: "string",
@@ -143,6 +143,12 @@ export default function EntityAutocomplete($compile, $templateCache, $q, $filter @@ -143,6 +143,12 @@ export default function EntityAutocomplete($compile, $templateCache, $q, $filter
143 scope.noEntitiesMatchingText = 'plugin.no-plugins-matching'; 143 scope.noEntitiesMatchingText = 'plugin.no-plugins-matching';
144 scope.entityRequiredText = 'plugin.plugin-required'; 144 scope.entityRequiredText = 'plugin.plugin-required';
145 break; 145 break;
  146 + case types.entityType.rulechain:
  147 + scope.selectEntityText = 'rulechain.select-rulechain';
  148 + scope.entityText = 'rulechain.rulechain';
  149 + scope.noEntitiesMatchingText = 'rulechain.no-rulechains-matching';
  150 + scope.entityRequiredText = 'rulechain.rulechain-required';
  151 + break;
146 case types.entityType.tenant: 152 case types.entityType.tenant:
147 scope.selectEntityText = 'tenant.select-tenant'; 153 scope.selectEntityText = 'tenant.select-tenant';
148 scope.entityText = 'tenant.tenant'; 154 scope.entityText = 'tenant.tenant';
@@ -1169,6 +1169,26 @@ export default angular.module('thingsboard.locale', []) @@ -1169,6 +1169,26 @@ export default angular.module('thingsboard.locale', [])
1169 "rulechain-required": "Rule chain is required", 1169 "rulechain-required": "Rule chain is required",
1170 "management": "Rules management" 1170 "management": "Rules management"
1171 }, 1171 },
  1172 + "rulenode": {
  1173 + "add": "Add rule node",
  1174 + "name": "Name",
  1175 + "name-required": "Name is required.",
  1176 + "type": "Type",
  1177 + "description": "Description",
  1178 + "delete": "Delete rule node",
  1179 + "rulenode-details": "Rule node details",
  1180 + "link-details": "Rule node link details",
  1181 + "add-link": "Add link",
  1182 + "link-label": "Link label",
  1183 + "link-label-required": "Link label is required.",
  1184 + "custom-link-label": "Custom link label",
  1185 + "custom-link-label-required": "Custom link label is required.",
  1186 + "type-filter": "Filter",
  1187 + "type-enrichment": "Enrichment",
  1188 + "type-transformation": "Transformation",
  1189 + "type-action": "Action",
  1190 + "type-rule-chain": "Rule Chain"
  1191 + },
1172 "rule-plugin": { 1192 "rule-plugin": {
1173 "management": "Rules and plugins management" 1193 "management": "Rules and plugins management"
1174 }, 1194 },
  1 +<!--
  2 +
  3 + Copyright © 2016-2018 The Thingsboard Authors
  4 +
  5 + Licensed under the Apache License, Version 2.0 (the "License");
  6 + you may not use this file except in compliance with the License.
  7 + You may obtain a copy of the License at
  8 +
  9 + http://www.apache.org/licenses/LICENSE-2.0
  10 +
  11 + Unless required by applicable law or agreed to in writing, software
  12 + distributed under the License is distributed on an "AS IS" BASIS,
  13 + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  14 + See the License for the specific language governing permissions and
  15 + limitations under the License.
  16 +
  17 +-->
  18 +<md-dialog aria-label="{{ 'rulenode.add-link' | translate }}" tb-help="'rulechains'" help-container-id="help-container">
  19 + <form name="theForm" ng-submit="vm.add()">
  20 + <md-toolbar>
  21 + <div class="md-toolbar-tools">
  22 + <h2 translate>rulenode.add-link</h2>
  23 + <span flex></span>
  24 + <div id="help-container"></div>
  25 + <md-button class="md-icon-button" ng-click="vm.cancel()">
  26 + <ng-md-icon icon="close" aria-label="{{ 'dialog.close' | translate }}"></ng-md-icon>
  27 + </md-button>
  28 + </div>
  29 + </md-toolbar>
  30 + <md-progress-linear class="md-warn" md-mode="indeterminate" ng-disabled="!$root.loading" ng-show="$root.loading"></md-progress-linear>
  31 + <span style="min-height: 5px;" flex="" ng-show="!$root.loading"></span>
  32 + <md-dialog-content>
  33 + <div class="md-dialog-content">
  34 + <tb-rule-node-link link="vm.link" labels="vm.labels" is-edit="true" the-form="theForm"></tb-rule-node-link>
  35 + </div>
  36 + </md-dialog-content>
  37 + <md-dialog-actions layout="row">
  38 + <span flex></span>
  39 + <md-button ng-disabled="$root.loading || theForm.$invalid || !theForm.$dirty" type="submit"
  40 + class="md-raised md-primary">
  41 + {{ 'action.add' | translate }}
  42 + </md-button>
  43 + <md-button ng-disabled="$root.loading" ng-click="vm.cancel()" style="margin-right:20px;">{{ 'action.cancel' |
  44 + translate }}
  45 + </md-button>
  46 + </md-dialog-actions>
  47 + </form>
  48 +</md-dialog>
  1 +<!--
  2 +
  3 + Copyright © 2016-2018 The Thingsboard Authors
  4 +
  5 + Licensed under the Apache License, Version 2.0 (the "License");
  6 + you may not use this file except in compliance with the License.
  7 + You may obtain a copy of the License at
  8 +
  9 + http://www.apache.org/licenses/LICENSE-2.0
  10 +
  11 + Unless required by applicable law or agreed to in writing, software
  12 + distributed under the License is distributed on an "AS IS" BASIS,
  13 + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  14 + See the License for the specific language governing permissions and
  15 + limitations under the License.
  16 +
  17 +-->
  18 +<md-dialog aria-label="{{ 'rulenode.add' | translate }}" tb-help="'rulechains'" help-container-id="help-container" style="min-width: 650px;">
  19 + <form name="theForm" ng-submit="vm.add()">
  20 + <md-toolbar>
  21 + <div class="md-toolbar-tools">
  22 + <h2 translate>rulenode.add</h2>
  23 + <span flex></span>
  24 + <div id="help-container"></div>
  25 + <md-button class="md-icon-button" ng-click="vm.cancel()">
  26 + <ng-md-icon icon="close" aria-label="{{ 'dialog.close' | translate }}"></ng-md-icon>
  27 + </md-button>
  28 + </div>
  29 + </md-toolbar>
  30 + <md-progress-linear class="md-warn" md-mode="indeterminate" ng-disabled="!$root.loading" ng-show="$root.loading"></md-progress-linear>
  31 + <span style="min-height: 5px;" flex="" ng-show="!$root.loading"></span>
  32 + <md-dialog-content>
  33 + <div class="md-dialog-content">
  34 + <tb-rule-node rule-node="vm.ruleNode" rule-chain-id="vm.ruleChainId" is-edit="true" the-form="theForm"></tb-rule-node>
  35 + </div>
  36 + </md-dialog-content>
  37 + <md-dialog-actions layout="row">
  38 + <span flex></span>
  39 + <md-button ng-disabled="$root.loading || theForm.$invalid || !theForm.$dirty" type="submit"
  40 + class="md-raised md-primary">
  41 + {{ 'action.add' | translate }}
  42 + </md-button>
  43 + <md-button ng-disabled="$root.loading" ng-click="vm.cancel()" style="margin-right:20px;">{{ 'action.cancel' |
  44 + translate }}
  45 + </md-button>
  46 + </md-dialog-actions>
  47 + </form>
  48 +</md-dialog>
@@ -15,11 +15,19 @@ @@ -15,11 +15,19 @@
15 */ 15 */
16 16
17 import RuleChainRoutes from './rulechain.routes'; 17 import RuleChainRoutes from './rulechain.routes';
18 -import RuleChainController from './rulechain.controller'; 18 +import RuleChainsController from './rulechains.controller';
  19 +import {RuleChainController, AddRuleNodeController, AddRuleNodeLinkController} from './rulechain.controller';
19 import RuleChainDirective from './rulechain.directive'; 20 import RuleChainDirective from './rulechain.directive';
  21 +import RuleNodeDirective from './rulenode.directive';
  22 +import LinkDirective from './link.directive';
20 23
21 export default angular.module('thingsboard.ruleChain', []) 24 export default angular.module('thingsboard.ruleChain', [])
22 .config(RuleChainRoutes) 25 .config(RuleChainRoutes)
  26 + .controller('RuleChainsController', RuleChainsController)
23 .controller('RuleChainController', RuleChainController) 27 .controller('RuleChainController', RuleChainController)
  28 + .controller('AddRuleNodeController', AddRuleNodeController)
  29 + .controller('AddRuleNodeLinkController', AddRuleNodeLinkController)
24 .directive('tbRuleChain', RuleChainDirective) 30 .directive('tbRuleChain', RuleChainDirective)
  31 + .directive('tbRuleNode', RuleNodeDirective)
  32 + .directive('tbRuleNodeLink', LinkDirective)
25 .name; 33 .name;
  1 +<!--
  2 +
  3 + Copyright © 2016-2018 The Thingsboard Authors
  4 +
  5 + Licensed under the Apache License, Version 2.0 (the "License");
  6 + you may not use this file except in compliance with the License.
  7 + You may obtain a copy of the License at
  8 +
  9 + http://www.apache.org/licenses/LICENSE-2.0
  10 +
  11 + Unless required by applicable law or agreed to in writing, software
  12 + distributed under the License is distributed on an "AS IS" BASIS,
  13 + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  14 + See the License for the specific language governing permissions and
  15 + limitations under the License.
  16 +
  17 +-->
  18 +<md-content class="md-padding tb-link" layout="column">
  19 + <fieldset ng-disabled="$root.loading || !isEdit || isReadOnly">
  20 + <md-input-container class="md-block">
  21 + <label translate>rulenode.link-label</label>
  22 + <md-select ng-model="selectedLabel" ng-change="selectedLabelChanged()">
  23 + <md-option ng-repeat="label in labels" ng-value="label">
  24 + {{label.name}}
  25 + </md-option>
  26 + </md-select>
  27 + <div ng-messages="theForm.linkLabel.$error">
  28 + <div translate ng-message="required">rulenode.link-label-required</div>
  29 + </div>
  30 + </md-input-container>
  31 + <md-input-container ng-if="selectedLabel.custom" class="md-block">
  32 + <label translate>rulenode.link-label</label>
  33 + <input required name="customLinkLabel" ng-model="link.label">
  34 + <div ng-messages="theForm.customLinkLabel.$error">
  35 + <div translate ng-message="required">rulenode.custom-link-label-required</div>
  36 + </div>
  37 + </md-input-container>
  38 + </fieldset>
  39 +</md-content>
  1 +/*
  2 + * Copyright © 2016-2018 The Thingsboard Authors
  3 + *
  4 + * Licensed under the Apache License, Version 2.0 (the "License");
  5 + * you may not use this file except in compliance with the License.
  6 + * You may obtain a copy of the License at
  7 + *
  8 + * http://www.apache.org/licenses/LICENSE-2.0
  9 + *
  10 + * Unless required by applicable law or agreed to in writing, software
  11 + * distributed under the License is distributed on an "AS IS" BASIS,
  12 + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  13 + * See the License for the specific language governing permissions and
  14 + * limitations under the License.
  15 + */
  16 +
  17 +/* eslint-disable import/no-unresolved, import/default */
  18 +
  19 +import linkFieldsetTemplate from './link-fieldset.tpl.html';
  20 +
  21 +/* eslint-enable import/no-unresolved, import/default */
  22 +
  23 +/*@ngInject*/
  24 +export default function LinkDirective($compile, $templateCache, $filter) {
  25 + var linker = function (scope, element) {
  26 + var template = $templateCache.get(linkFieldsetTemplate);
  27 + element.html(template);
  28 +
  29 + scope.selectedLabel = null;
  30 +
  31 + scope.$watch('link', function() {
  32 + scope.selectedLabel = null;
  33 + if (scope.link && scope.labels) {
  34 + if (scope.link.label) {
  35 + var result = $filter('filter')(scope.labels, {name: scope.link.label});
  36 + if (result && result.length) {
  37 + scope.selectedLabel = result[0];
  38 + } else {
  39 + result = $filter('filter')(scope.labels, {custom: true});
  40 + if (result && result.length && result[0].custom) {
  41 + scope.selectedLabel = result[0];
  42 + }
  43 + }
  44 + }
  45 + }
  46 + });
  47 +
  48 + scope.selectedLabelChanged = function() {
  49 + if (scope.link && scope.selectedLabel) {
  50 + if (!scope.selectedLabel.custom) {
  51 + scope.link.label = scope.selectedLabel.name;
  52 + } else {
  53 + scope.link.label = "";
  54 + }
  55 + }
  56 + };
  57 +
  58 + $compile(element.contents())(scope);
  59 + }
  60 + return {
  61 + restrict: "E",
  62 + link: linker,
  63 + scope: {
  64 + link: '=',
  65 + labels: '=',
  66 + isEdit: '=',
  67 + isReadOnly: '=',
  68 + theForm: '='
  69 + }
  70 + };
  71 +}
@@ -32,7 +32,7 @@ @@ -32,7 +32,7 @@
32 </md-button> 32 </md-button>
33 </div> 33 </div>
34 34
35 -<md-content class="md-padding tb-rulechain" layout="column"> 35 +<md-content class="md-padding tb-rulechain-fieldset" layout="column">
36 <fieldset ng-disabled="$root.loading || !isEdit || isReadOnly"> 36 <fieldset ng-disabled="$root.loading || !isEdit || isReadOnly">
37 <md-input-container class="md-block"> 37 <md-input-container class="md-block">
38 <label translate>rulechain.name</label> 38 <label translate>rulechain.name</label>
@@ -13,160 +13,582 @@ @@ -13,160 +13,582 @@
13 * See the License for the specific language governing permissions and 13 * See the License for the specific language governing permissions and
14 * limitations under the License. 14 * limitations under the License.
15 */ 15 */
  16 +
  17 +import './rulechain.scss';
  18 +
16 /* eslint-disable import/no-unresolved, import/default */ 19 /* eslint-disable import/no-unresolved, import/default */
17 20
18 -import addRuleChainTemplate from './add-rulechain.tpl.html';  
19 -import ruleChainCard from './rulechain-card.tpl.html'; 21 +import addRuleNodeTemplate from './add-rulenode.tpl.html';
  22 +import addRuleNodeLinkTemplate from './add-link.tpl.html';
20 23
21 /* eslint-enable import/no-unresolved, import/default */ 24 /* eslint-enable import/no-unresolved, import/default */
22 25
  26 +
  27 +const deleteKeyCode = 46;
  28 +const ctrlKeyCode = 17;
  29 +const aKeyCode = 65;
  30 +const escKeyCode = 27;
  31 +
23 /*@ngInject*/ 32 /*@ngInject*/
24 -export default function RuleChainController(ruleChainService, userService, importExport, $state, $stateParams, $filter, $translate, types) {  
25 -  
26 - var ruleChainActionsList = [  
27 - {  
28 - onAction: function ($event, item) {  
29 - exportRuleChain($event, item);  
30 - },  
31 - name: function() { $translate.instant('action.export') },  
32 - details: function() { return $translate.instant('rulechain.export') },  
33 - icon: "file_download"  
34 - },  
35 - {  
36 - onAction: function ($event, item) {  
37 - vm.grid.deleteItem($event, item);  
38 - },  
39 - name: function() { return $translate.instant('action.delete') },  
40 - details: function() { return $translate.instant('rulechain.delete') },  
41 - icon: "delete",  
42 - isEnabled: isRuleChainEditable  
43 - }  
44 - ];  
45 -  
46 - var ruleChainAddItemActionsList = [  
47 - {  
48 - onAction: function ($event) {  
49 - vm.grid.addItem($event);  
50 - },  
51 - name: function() { return $translate.instant('action.create') },  
52 - details: function() { return $translate.instant('rulechain.create-new-rulechain') },  
53 - icon: "insert_drive_file"  
54 - },  
55 - {  
56 - onAction: function ($event) {  
57 - importExport.importRuleChain($event).then(  
58 - function() {  
59 - vm.grid.refreshList();  
60 - }  
61 - );  
62 - },  
63 - name: function() { return $translate.instant('action.import') },  
64 - details: function() { return $translate.instant('rulechain.import') },  
65 - icon: "file_upload"  
66 - }  
67 - ]; 33 +export function RuleChainController($stateParams, $scope, $q, $mdUtil, $mdExpansionPanel, $document, $mdDialog, $filter, types, ruleChainService, Modelfactory, flowchartConstants, ruleChain, ruleChainMetaData) {
68 34
69 var vm = this; 35 var vm = this;
70 36
  37 + vm.$mdExpansionPanel = $mdExpansionPanel;
71 vm.types = types; 38 vm.types = types;
72 39
73 - vm.ruleChainGridConfig = { 40 + vm.editingRuleNode = null;
  41 + vm.isEditingRuleNode = false;
  42 +
  43 + vm.editingRuleNodeLink = null;
  44 + vm.isEditingRuleNodeLink = false;
74 45
75 - refreshParamsFunc: null, 46 + vm.ruleChain = ruleChain;
  47 + vm.ruleChainMetaData = ruleChainMetaData;
76 48
77 - deleteItemTitleFunc: deleteRuleChainTitle,  
78 - deleteItemContentFunc: deleteRuleChainText,  
79 - deleteItemsTitleFunc: deleteRuleChainsTitle,  
80 - deleteItemsActionTitleFunc: deleteRuleChainsActionTitle,  
81 - deleteItemsContentFunc: deleteRuleChainsText, 49 + vm.canvasControl = {};
82 50
83 - fetchItemsFunc: fetchRuleChains,  
84 - saveItemFunc: saveRuleChain,  
85 - deleteItemFunc: deleteRuleChain, 51 + vm.ruleChainModel = {
  52 + nodes: [],
  53 + edges: []
  54 + };
  55 +
  56 + vm.ruleNodeTypesModel = {};
  57 + for (var type in types.ruleNodeType) {
  58 + if (!types.ruleNodeType[type].special) {
  59 + vm.ruleNodeTypesModel[type] = {
  60 + model: {
  61 + nodes: [],
  62 + edges: []
  63 + },
  64 + selectedObjects: []
  65 + };
  66 + }
  67 + }
86 68
87 - getItemTitleFunc: getRuleChainTitle,  
88 - itemCardTemplateUrl: ruleChainCard,  
89 - parentCtl: vm, 69 + vm.selectedObjects = [];
90 70
91 - actionsList: ruleChainActionsList,  
92 - addItemActions: ruleChainAddItemActionsList, 71 + vm.modelservice = Modelfactory(vm.ruleChainModel, vm.selectedObjects);
93 72
94 - onGridInited: gridInited, 73 + vm.ctrlDown = false;
95 74
96 - addItemTemplateUrl: addRuleChainTemplate, 75 + vm.saveRuleChain = saveRuleChain;
  76 + vm.revertRuleChain = revertRuleChain;
97 77
98 - addItemText: function() { return $translate.instant('rulechain.add-rulechain-text') },  
99 - noItemsText: function() { return $translate.instant('rulechain.no-rulechains-text') },  
100 - itemDetailsText: function() { return $translate.instant('rulechain.rulechain-details') },  
101 - isSelectionEnabled: isRuleChainEditable,  
102 - isDetailsReadOnly: function(ruleChain) {  
103 - return !isRuleChainEditable(ruleChain); 78 + vm.keyDown = function (evt) {
  79 + if (evt.keyCode === ctrlKeyCode) {
  80 + vm.ctrlDown = true;
  81 + evt.stopPropagation();
  82 + evt.preventDefault();
104 } 83 }
105 }; 84 };
106 85
107 - if (angular.isDefined($stateParams.items) && $stateParams.items !== null) {  
108 - vm.ruleChainGridConfig.items = $stateParams.items;  
109 - } 86 + vm.keyUp = function (evt) {
110 87
111 - if (angular.isDefined($stateParams.topIndex) && $stateParams.topIndex > 0) {  
112 - vm.ruleChainGridConfig.topIndex = $stateParams.topIndex;  
113 - } 88 + if (evt.keyCode === deleteKeyCode) {
  89 + vm.modelservice.deleteSelected();
  90 + }
114 91
115 - vm.isRuleChainEditable = isRuleChainEditable; 92 + if (evt.keyCode == aKeyCode && vm.ctrlDown) {
  93 + vm.modelservice.selectAll();
  94 + }
116 95
117 - vm.exportRuleChain = exportRuleChain; 96 + if (evt.keyCode == escKeyCode) {
  97 + vm.modelservice.deselectAll();
  98 + }
118 99
119 - function deleteRuleChainTitle(ruleChain) {  
120 - return $translate.instant('rulechain.delete-rulechain-title', {ruleChainName: ruleChain.name});  
121 - } 100 + if (evt.keyCode === ctrlKeyCode) {
  101 + vm.ctrlDown = false;
  102 + evt.stopPropagation();
  103 + evt.preventDefault();
  104 + }
  105 + };
122 106
123 - function deleteRuleChainText() {  
124 - return $translate.instant('rulechain.delete-rulechain-text');  
125 - } 107 + vm.onEditRuleNodeClosed = function() {
  108 + vm.editingRuleNode = null;
  109 + };
  110 +
  111 + vm.onEditRuleNodeLinkClosed = function() {
  112 + vm.editingRuleNodeLink = null;
  113 + };
  114 +
  115 + vm.saveRuleNode = function(theForm) {
  116 + theForm.$setPristine();
  117 + vm.isEditingRuleNode = false;
  118 + vm.ruleChainModel.nodes[vm.editingRuleNodeIndex] = vm.editingRuleNode;
  119 + vm.editingRuleNode = angular.copy(vm.editingRuleNode);
  120 + };
  121 +
  122 + vm.saveRuleNodeLink = function(theForm) {
  123 + theForm.$setPristine();
  124 + vm.isEditingRuleNodeLink = false;
  125 + vm.ruleChainModel.edges[vm.editingRuleNodeLinkIndex] = vm.editingRuleNodeLink;
  126 + vm.editingRuleNodeLink = angular.copy(vm.editingRuleNodeLink);
  127 + };
  128 +
  129 + vm.onRevertRuleNodeEdit = function(theForm) {
  130 + theForm.$setPristine();
  131 + var node = vm.ruleChainModel.nodes[vm.editingRuleNodeIndex];
  132 + vm.editingRuleNode = angular.copy(node);
  133 + };
  134 +
  135 + vm.onRevertRuleNodeLinkEdit = function(theForm) {
  136 + theForm.$setPristine();
  137 + var edge = vm.ruleChainModel.edges[vm.editingRuleNodeLinkIndex];
  138 + vm.editingRuleNodeLink = angular.copy(edge);
  139 + };
  140 +
  141 + vm.editCallbacks = {
  142 + edgeDoubleClick: function (event, edge) {
  143 + var sourceNode = vm.modelservice.nodes.getNodeByConnectorId(edge.source);
  144 + if (sourceNode.nodeType != types.ruleNodeType.INPUT.value) {
  145 + ruleChainService.getRuleNodeSupportedLinks(sourceNode.type).then(
  146 + (labels) => {
  147 + vm.isEditingRuleNode = false;
  148 + vm.editingRuleNode = null;
  149 + vm.editingRuleNodeLinkLabels = labels;
  150 + vm.isEditingRuleNodeLink = true;
  151 + vm.editingRuleNodeLinkIndex = vm.ruleChainModel.edges.indexOf(edge);
  152 + vm.editingRuleNodeLink = angular.copy(edge);
  153 + }
  154 + );
  155 + }
  156 + },
  157 + nodeCallbacks: {
  158 + 'doubleClick': function (event, node) {
  159 + if (node.nodeType != types.ruleNodeType.INPUT.value) {
  160 + vm.isEditingRuleNodeLink = false;
  161 + vm.editingRuleNodeLink = null;
  162 + vm.isEditingRuleNode = true;
  163 + vm.editingRuleNodeIndex = vm.ruleChainModel.nodes.indexOf(node);
  164 + vm.editingRuleNode = angular.copy(node);
  165 + }
  166 + }
  167 + },
  168 + isValidEdge: function (source, destination) {
  169 + return source.type === flowchartConstants.rightConnectorType && destination.type === flowchartConstants.leftConnectorType;
  170 + },
  171 + createEdge: function (event, edge) {
  172 + var deferred = $q.defer();
  173 + var sourceNode = vm.modelservice.nodes.getNodeByConnectorId(edge.source);
  174 + if (sourceNode.nodeType == types.ruleNodeType.INPUT.value) {
  175 + var destNode = vm.modelservice.nodes.getNodeByConnectorId(edge.destination);
  176 + if (destNode.nodeType == types.ruleNodeType.RULE_CHAIN.value) {
  177 + deferred.reject();
  178 + } else {
  179 + var res = $filter('filter')(vm.ruleChainModel.edges, {source: vm.inputConnectorId});
  180 + if (res && res.length) {
  181 + vm.modelservice.edges.delete(res[0]);
  182 + }
  183 + deferred.resolve(edge);
  184 + }
  185 + } else {
  186 + ruleChainService.getRuleNodeSupportedLinks(sourceNode.type).then(
  187 + (labels) => {
  188 + addRuleNodeLink(event, edge, labels).then(
  189 + (link) => {
  190 + deferred.resolve(link);
  191 + },
  192 + () => {
  193 + deferred.reject();
  194 + }
  195 + );
  196 + },
  197 + () => {
  198 + deferred.reject();
  199 + }
  200 + );
  201 + }
  202 + return deferred.promise;
  203 + },
  204 + dropNode: function (event, node) {
  205 + addRuleNode(event, node);
  206 + }
  207 + };
126 208
127 - function deleteRuleChainsTitle(selectedCount) {  
128 - return $translate.instant('rulechain.delete-rulechains-title', {count: selectedCount}, 'messageformat'); 209 + loadRuleChainLibrary();
  210 +
  211 + function loadRuleChainLibrary() {
  212 + ruleChainService.getRuleNodeTypes().then(
  213 + (ruleNodeTypes) => {
  214 + for (var i=0;i<ruleNodeTypes.length;i++) {
  215 + var ruleNodeType = ruleNodeTypes[i];
  216 + var nodeType = ruleNodeType.nodeType;
  217 + var model = vm.ruleNodeTypesModel[nodeType].model;
  218 + var node = {
  219 + id: model.nodes.length,
  220 + nodeType: nodeType,
  221 + type: ruleNodeType.type,
  222 + name: '',
  223 + nodeClass: vm.types.ruleNodeType[nodeType].nodeClass,
  224 + icon: vm.types.ruleNodeType[nodeType].icon,
  225 + x: 30,
  226 + y: 10+50*model.nodes.length,
  227 + connectors: []
  228 + };
  229 + if (nodeType == types.ruleNodeType.RULE_CHAIN.value) {
  230 + node.connectors.push(
  231 + {
  232 + type: flowchartConstants.leftConnectorType,
  233 + id: model.nodes.length
  234 + }
  235 + );
  236 + } else {
  237 + node.connectors.push(
  238 + {
  239 + type: flowchartConstants.leftConnectorType,
  240 + id: model.nodes.length*2
  241 + }
  242 + );
  243 + node.connectors.push(
  244 + {
  245 + type: flowchartConstants.rightConnectorType,
  246 + id: model.nodes.length*2+1
  247 + }
  248 + );
  249 + }
  250 + model.nodes.push(node);
  251 + }
  252 + prepareRuleChain();
  253 + }
  254 + );
129 } 255 }
130 256
131 - function deleteRuleChainsActionTitle(selectedCount) {  
132 - return $translate.instant('rulechain.delete-rulechains-action-title', {count: selectedCount}, 'messageformat'); 257 + function prepareRuleChain() {
  258 +
  259 + if (vm.ruleChainWatch) {
  260 + vm.ruleChainWatch();
  261 + vm.ruleChainWatch = null;
  262 + }
  263 +
  264 + vm.nextNodeID = 1;
  265 + vm.nextConnectorID = 1;
  266 +
  267 + vm.selectedObjects.length = 0;
  268 + vm.ruleChainModel.nodes.length = 0;
  269 + vm.ruleChainModel.edges.length = 0;
  270 +
  271 + vm.inputConnectorId = vm.nextConnectorID++;
  272 +
  273 + vm.ruleChainModel.nodes.push(
  274 + {
  275 + id: vm.nextNodeID++,
  276 + type: "Input",
  277 + name: "",
  278 + nodeType: types.ruleNodeType.INPUT.value,
  279 + nodeClass: types.ruleNodeType.INPUT.nodeClass,
  280 + icon: types.ruleNodeType.INPUT.icon,
  281 + readonly: true,
  282 + x: 50,
  283 + y: 150,
  284 + connectors: [
  285 + {
  286 + type: flowchartConstants.rightConnectorType,
  287 + id: vm.inputConnectorId
  288 + },
  289 + ]
  290 +
  291 + }
  292 + );
  293 + ruleChainService.resolveTargetRuleChains(vm.ruleChainMetaData.ruleChainConnections)
  294 + .then((ruleChainsMap) => {
  295 + createRuleChainModel(ruleChainsMap);
  296 + }
  297 + );
133 } 298 }
134 299
135 - function deleteRuleChainsText() {  
136 - return $translate.instant('rulechain.delete-rulechains-text'); 300 + function createRuleChainModel(ruleChainsMap) {
  301 + var nodes = [];
  302 + for (var i=0;i<vm.ruleChainMetaData.nodes.length;i++) {
  303 + var ruleNode = vm.ruleChainMetaData.nodes[i];
  304 + var nodeType = ruleChainService.getRuleNodeComponentType(ruleNode.type);
  305 + if (nodeType) {
  306 + var node = {
  307 + id: vm.nextNodeID++,
  308 + ruleNodeId: ruleNode.id,
  309 + additionalInfo: ruleNode.additionalInfo,
  310 + configuration: ruleNode.configuration,
  311 + x: ruleNode.additionalInfo.layoutX,
  312 + y: ruleNode.additionalInfo.layoutY,
  313 + type: ruleNode.type,
  314 + name: ruleNode.name,
  315 + nodeType: nodeType,
  316 + nodeClass: vm.types.ruleNodeType[nodeType].nodeClass,
  317 + icon: vm.types.ruleNodeType[nodeType].icon,
  318 + connectors: [
  319 + {
  320 + type: flowchartConstants.leftConnectorType,
  321 + id: vm.nextConnectorID++
  322 + },
  323 + {
  324 + type: flowchartConstants.rightConnectorType,
  325 + id: vm.nextConnectorID++
  326 + }
  327 + ]
  328 + };
  329 + nodes.push(node);
  330 + vm.ruleChainModel.nodes.push(node);
  331 + }
  332 + }
  333 +
  334 + if (vm.ruleChainMetaData.firstNodeIndex > -1) {
  335 + var destNode = nodes[vm.ruleChainMetaData.firstNodeIndex];
  336 + if (destNode) {
  337 + var connectors = vm.modelservice.nodes.getConnectorsByType(destNode, flowchartConstants.leftConnectorType);
  338 + if (connectors && connectors.length) {
  339 + var edge = {
  340 + source: vm.inputConnectorId,
  341 + destination: connectors[0].id
  342 + };
  343 + vm.ruleChainModel.edges.push(edge);
  344 + }
  345 + }
  346 + }
  347 +
  348 + if (vm.ruleChainMetaData.connections) {
  349 + for (i = 0; i < vm.ruleChainMetaData.connections.length; i++) {
  350 + var connection = vm.ruleChainMetaData.connections[0];
  351 + var sourceNode = nodes[connection.fromIndex];
  352 + destNode = nodes[connection.toIndex];
  353 + if (sourceNode && destNode) {
  354 + var sourceConnectors = vm.modelservice.nodes.getConnectorsByType(sourceNode, flowchartConstants.rightConnectorType);
  355 + var destConnectors = vm.modelservice.nodes.getConnectorsByType(destNode, flowchartConstants.leftConnectorType);
  356 + if (sourceConnectors && sourceConnectors.length && destConnectors && destConnectors.length) {
  357 + edge = {
  358 + source: sourceConnectors[0].id,
  359 + destination: destConnectors[0].id,
  360 + label: connection.type
  361 + };
  362 + vm.ruleChainModel.edges.push(edge);
  363 + }
  364 + }
  365 + }
  366 + }
  367 +
  368 + if (vm.ruleChainMetaData.ruleChainConnections) {
  369 + var ruleChainNodesMap = {};
  370 + for (i = 0; i < vm.ruleChainMetaData.ruleChainConnections.length; i++) {
  371 + var ruleChainConnection = vm.ruleChainMetaData.ruleChainConnections[i];
  372 + var ruleChain = ruleChainsMap[ruleChainConnection.targetRuleChainId.id];
  373 + if (ruleChainConnection.additionalInfo && ruleChainConnection.additionalInfo.ruleChainNodeId) {
  374 + var ruleChainNode = ruleChainNodesMap[ruleChainConnection.additionalInfo.ruleChainNodeId];
  375 + if (!ruleChainNode) {
  376 + ruleChainNode = {
  377 + id: vm.nextNodeID++,
  378 + additionalInfo: ruleChainConnection.additionalInfo,
  379 + targetRuleChainId: ruleChainConnection.targetRuleChainId.id,
  380 + x: ruleChainConnection.additionalInfo.layoutX,
  381 + y: ruleChainConnection.additionalInfo.layoutY,
  382 + type: 'Rule chain',
  383 + name: ruleChain.name,
  384 + nodeType: vm.types.ruleNodeType.RULE_CHAIN.value,
  385 + nodeClass: vm.types.ruleNodeType.RULE_CHAIN.nodeClass,
  386 + icon: vm.types.ruleNodeType.RULE_CHAIN.icon,
  387 + connectors: [
  388 + {
  389 + type: flowchartConstants.leftConnectorType,
  390 + id: vm.nextConnectorID++
  391 + }
  392 + ]
  393 + };
  394 + ruleChainNodesMap[ruleChainConnection.additionalInfo.ruleChainNodeId] = ruleChainNode;
  395 + vm.ruleChainModel.nodes.push(ruleChainNode);
  396 + }
  397 + sourceNode = nodes[ruleChainConnection.fromIndex];
  398 + if (sourceNode) {
  399 + connectors = vm.modelservice.nodes.getConnectorsByType(sourceNode, flowchartConstants.rightConnectorType);
  400 + if (connectors && connectors.length) {
  401 + var ruleChainEdge = {
  402 + source: connectors[0].id,
  403 + destination: ruleChainNode.connectors[0].id,
  404 + label: ruleChainConnection.type
  405 + };
  406 + vm.ruleChainModel.edges.push(ruleChainEdge);
  407 + }
  408 + }
  409 + }
  410 + }
  411 + }
  412 +
  413 + vm.canvasControl.adjustCanvasSize();
  414 +
  415 + vm.isDirty = false;
  416 +
  417 + $mdUtil.nextTick(() => {
  418 + vm.ruleChainWatch = $scope.$watch('vm.ruleChainModel',
  419 + function (newVal, oldVal) {
  420 + if (!vm.isDirty && !angular.equals(newVal, oldVal)) {
  421 + vm.isDirty = true;
  422 + }
  423 + }, true
  424 + );
  425 + });
137 } 426 }
138 427
139 - function gridInited(grid) {  
140 - vm.grid = grid; 428 + function saveRuleChain() {
  429 + var ruleChainMetaData = {
  430 + ruleChainId: vm.ruleChain.id,
  431 + nodes: [],
  432 + connections: [],
  433 + ruleChainConnections: []
  434 + };
  435 +
  436 + var nodes = [];
  437 +
  438 + for (var i=0;i<vm.ruleChainModel.nodes.length;i++) {
  439 + var node = vm.ruleChainModel.nodes[i];
  440 + if (node.nodeType != types.ruleNodeType.INPUT.value && node.nodeType != types.ruleNodeType.RULE_CHAIN.value) {
  441 + var ruleNode = {};
  442 + if (node.ruleNodeId) {
  443 + ruleNode.id = node.ruleNodeId;
  444 + }
  445 + ruleNode.type = node.type;
  446 + ruleNode.name = node.name;
  447 + ruleNode.configuration = node.configuration;
  448 + ruleNode.additionalInfo = node.additionalInfo;
  449 + if (!ruleNode.additionalInfo) {
  450 + ruleNode.additionalInfo = {};
  451 + }
  452 + ruleNode.additionalInfo.layoutX = node.x;
  453 + ruleNode.additionalInfo.layoutY = node.y;
  454 + ruleChainMetaData.nodes.push(ruleNode);
  455 + nodes.push(node);
  456 + }
  457 + }
  458 + var res = $filter('filter')(vm.ruleChainModel.edges, {source: vm.inputConnectorId});
  459 + if (res && res.length) {
  460 + var firstNodeEdge = res[0];
  461 + var firstNode = vm.modelservice.nodes.getNodeByConnectorId(firstNodeEdge.destination);
  462 + ruleChainMetaData.firstNodeIndex = nodes.indexOf(firstNode);
  463 + }
  464 + for (i=0;i<vm.ruleChainModel.edges.length;i++) {
  465 + var edge = vm.ruleChainModel.edges[i];
  466 + var sourceNode = vm.modelservice.nodes.getNodeByConnectorId(edge.source);
  467 + var destNode = vm.modelservice.nodes.getNodeByConnectorId(edge.destination);
  468 + if (sourceNode.nodeType != types.ruleNodeType.INPUT.value) {
  469 + var fromIndex = nodes.indexOf(sourceNode);
  470 + if (destNode.nodeType == types.ruleNodeType.RULE_CHAIN.value) {
  471 + var ruleChainConnection = {
  472 + fromIndex: fromIndex,
  473 + targetRuleChainId: {entityType: vm.types.entityType.rulechain, id: destNode.targetRuleChainId},
  474 + additionalInfo: destNode.additionalInfo,
  475 + type: edge.label
  476 + };
  477 + if (!ruleChainConnection.additionalInfo) {
  478 + ruleChainConnection.additionalInfo = {};
  479 + }
  480 + ruleChainConnection.additionalInfo.layoutX = destNode.x;
  481 + ruleChainConnection.additionalInfo.layoutY = destNode.y;
  482 + ruleChainConnection.additionalInfo.ruleChainNodeId = destNode.id;
  483 + ruleChainMetaData.ruleChainConnections.push(ruleChainConnection);
  484 + } else {
  485 + var toIndex = nodes.indexOf(destNode);
  486 + var nodeConnection = {
  487 + fromIndex: fromIndex,
  488 + toIndex: toIndex,
  489 + type: edge.label
  490 + };
  491 + ruleChainMetaData.connections.push(nodeConnection);
  492 + }
  493 + }
  494 + }
  495 + ruleChainService.saveRuleChainMetaData(ruleChainMetaData).then(
  496 + (ruleChainMetaData) => {
  497 + vm.ruleChainMetaData = ruleChainMetaData;
  498 + prepareRuleChain();
  499 + }
  500 + );
141 } 501 }
142 502
143 - function fetchRuleChains(pageLink) {  
144 - return ruleChainService.getRuleChains(pageLink); 503 + function revertRuleChain() {
  504 + prepareRuleChain();
145 } 505 }
146 506
147 - function saveRuleChain(ruleChain) {  
148 - return ruleChainService.saveRuleChain(ruleChain); 507 + function addRuleNode($event, ruleNode) {
  508 + $mdDialog.show({
  509 + controller: 'AddRuleNodeController',
  510 + controllerAs: 'vm',
  511 + templateUrl: addRuleNodeTemplate,
  512 + parent: angular.element($document[0].body),
  513 + locals: {ruleNode: ruleNode, ruleChainId: vm.ruleChain.id.id},
  514 + fullscreen: true,
  515 + targetEvent: $event
  516 + }).then(function (ruleNode) {
  517 + ruleNode.id = vm.nextNodeID++;
  518 + ruleNode.connectors = [];
  519 + ruleNode.connectors.push(
  520 + {
  521 + id: vm.nextConnectorID++,
  522 + type: flowchartConstants.leftConnectorType
  523 + }
  524 + );
  525 + if (ruleNode.nodeType != types.ruleNodeType.RULE_CHAIN.value) {
  526 + ruleNode.connectors.push(
  527 + {
  528 + id: vm.nextConnectorID++,
  529 + type: flowchartConstants.rightConnectorType
  530 + }
  531 + );
  532 + }
  533 + vm.ruleChainModel.nodes.push(ruleNode);
  534 + }, function () {
  535 + });
149 } 536 }
150 537
151 - function deleteRuleChain(ruleChainId) {  
152 - return ruleChainService.deleteRuleChain(ruleChainId); 538 + function addRuleNodeLink($event, link, labels) {
  539 + return $mdDialog.show({
  540 + controller: 'AddRuleNodeLinkController',
  541 + controllerAs: 'vm',
  542 + templateUrl: addRuleNodeLinkTemplate,
  543 + parent: angular.element($document[0].body),
  544 + locals: {link: link, labels: labels},
  545 + fullscreen: true,
  546 + targetEvent: $event
  547 + });
153 } 548 }
154 549
155 - function getRuleChainTitle(ruleChain) {  
156 - return ruleChain ? ruleChain.name : ''; 550 +}
  551 +
  552 +/*@ngInject*/
  553 +export function AddRuleNodeController($scope, $mdDialog, ruleNode, ruleChainId, helpLinks) {
  554 +
  555 + var vm = this;
  556 +
  557 + vm.helpLinks = helpLinks;
  558 + vm.ruleNode = ruleNode;
  559 + vm.ruleChainId = ruleChainId;
  560 +
  561 + vm.add = add;
  562 + vm.cancel = cancel;
  563 +
  564 + function cancel() {
  565 + $mdDialog.cancel();
157 } 566 }
158 567
159 - function isRuleChainEditable(ruleChain) {  
160 - if (userService.getAuthority() === 'TENANT_ADMIN') {  
161 - return ruleChain && ruleChain.tenantId.id != types.id.nullUid;  
162 - } else {  
163 - return userService.getAuthority() === 'SYS_ADMIN';  
164 - } 568 + function add() {
  569 + $scope.theForm.$setPristine();
  570 + $mdDialog.hide(vm.ruleNode);
165 } 571 }
  572 +}
  573 +
  574 +/*@ngInject*/
  575 +export function AddRuleNodeLinkController($scope, $mdDialog, link, labels, helpLinks) {
166 576
167 - function exportRuleChain($event, ruleChain) {  
168 - $event.stopPropagation();  
169 - importExport.exportRuleChain(ruleChain.id.id); 577 + var vm = this;
  578 +
  579 + vm.helpLinks = helpLinks;
  580 + vm.link = link;
  581 + vm.labels = labels;
  582 +
  583 + vm.add = add;
  584 + vm.cancel = cancel;
  585 +
  586 + function cancel() {
  587 + $mdDialog.cancel();
170 } 588 }
171 589
  590 + function add() {
  591 + $scope.theForm.$setPristine();
  592 + $mdDialog.hide(vm.link);
  593 + }
172 } 594 }
@@ -15,12 +15,16 @@ @@ -15,12 +15,16 @@
15 */ 15 */
16 /* eslint-disable import/no-unresolved, import/default */ 16 /* eslint-disable import/no-unresolved, import/default */
17 17
  18 +import ruleNodeTemplate from './rulenode.tpl.html';
18 import ruleChainsTemplate from './rulechains.tpl.html'; 19 import ruleChainsTemplate from './rulechains.tpl.html';
  20 +import ruleChainTemplate from './rulechain.tpl.html';
19 21
20 /* eslint-enable import/no-unresolved, import/default */ 22 /* eslint-enable import/no-unresolved, import/default */
21 23
22 /*@ngInject*/ 24 /*@ngInject*/
23 -export default function RuleChainRoutes($stateProvider) { 25 +export default function RuleChainRoutes($stateProvider, NodeTemplatePathProvider) {
  26 +
  27 + NodeTemplatePathProvider.setTemplatePath(ruleNodeTemplate);
24 28
25 $stateProvider 29 $stateProvider
26 .state('home.ruleChains', { 30 .state('home.ruleChains', {
@@ -32,7 +36,7 @@ export default function RuleChainRoutes($stateProvider) { @@ -32,7 +36,7 @@ export default function RuleChainRoutes($stateProvider) {
32 "content@home": { 36 "content@home": {
33 templateUrl: ruleChainsTemplate, 37 templateUrl: ruleChainsTemplate,
34 controllerAs: 'vm', 38 controllerAs: 'vm',
35 - controller: 'RuleChainController' 39 + controller: 'RuleChainsController'
36 } 40 }
37 }, 41 },
38 data: { 42 data: {
@@ -42,5 +46,36 @@ export default function RuleChainRoutes($stateProvider) { @@ -42,5 +46,36 @@ export default function RuleChainRoutes($stateProvider) {
42 ncyBreadcrumb: { 46 ncyBreadcrumb: {
43 label: '{"icon": "settings_ethernet", "label": "rulechain.rulechains"}' 47 label: '{"icon": "settings_ethernet", "label": "rulechain.rulechains"}'
44 } 48 }
45 - }); 49 + }).state('home.ruleChains.ruleChain', {
  50 + url: '/:ruleChainId',
  51 + reloadOnSearch: false,
  52 + module: 'private',
  53 + auth: ['SYS_ADMIN', 'TENANT_ADMIN'],
  54 + views: {
  55 + "content@home": {
  56 + templateUrl: ruleChainTemplate,
  57 + controller: 'RuleChainController',
  58 + controllerAs: 'vm'
  59 + }
  60 + },
  61 + resolve: {
  62 + ruleChain:
  63 + /*@ngInject*/
  64 + function($stateParams, ruleChainService) {
  65 + return ruleChainService.getRuleChain($stateParams.ruleChainId);
  66 + },
  67 + ruleChainMetaData:
  68 + /*@ngInject*/
  69 + function($stateParams, ruleChainService) {
  70 + return ruleChainService.getRuleChainMetaData($stateParams.ruleChainId);
  71 + }
  72 + },
  73 + data: {
  74 + searchEnabled: false,
  75 + pageTitle: 'rulechain.rulechain'
  76 + },
  77 + ncyBreadcrumb: {
  78 + label: '{"icon": "settings_ethernet", "label": "{{ vm.ruleChain.name }}", "translate": "false"}'
  79 + }
  80 + });
46 } 81 }
  1 +/**
  2 + * Copyright © 2016-2018 The Thingsboard Authors
  3 + *
  4 + * Licensed under the Apache License, Version 2.0 (the "License");
  5 + * you may not use this file except in compliance with the License.
  6 + * You may obtain a copy of the License at
  7 + *
  8 + * http://www.apache.org/licenses/LICENSE-2.0
  9 + *
  10 + * Unless required by applicable law or agreed to in writing, software
  11 + * distributed under the License is distributed on an "AS IS" BASIS,
  12 + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  13 + * See the License for the specific language governing permissions and
  14 + * limitations under the License.
  15 + */
  16 +
  17 +.tb-rulechain {
  18 + .tb-fullscreen-button-style {
  19 + z-index: 1;
  20 + }
  21 + .tb-rulechain-library {
  22 + width: 250px;
  23 + min-width: 250px;
  24 + overflow-y: auto;
  25 + overflow-x: hidden;
  26 +
  27 + .tb-rulechain-library-panel-group {
  28 + .tb-panel-title {
  29 + -webkit-user-select: none;
  30 + -moz-user-select: none;
  31 + -ms-user-select: none;
  32 + user-select: none;
  33 + min-width: 180px;
  34 + }
  35 + .fc-canvas {
  36 + background: none;
  37 + }
  38 + md-icon.md-expansion-panel-icon {
  39 + margin-right: 0px;
  40 + }
  41 + md-expansion-panel-collapsed, .md-expansion-panel-header-container {
  42 + background: #e6e6e6;
  43 + border-color: #909090;
  44 + position: static;
  45 + }
  46 + md-expansion-panel {
  47 + &.md-open {
  48 + margin-top: 0;
  49 + margin-bottom: 0;
  50 + }
  51 + }
  52 + md-expansion-panel-content {
  53 + padding: 0px;
  54 + }
  55 + }
  56 + }
  57 + .tb-rulechain-graph {
  58 + overflow: auto;
  59 + }
  60 +}
  61 +
  62 +.fc-canvas {
  63 + min-width: 100%;
  64 + min-height: 100%;
  65 + outline: none;
  66 +}
  67 +
  68 +.tb-rule-node {
  69 + display: flex;
  70 + flex-direction: row;
  71 + min-width: 150px;
  72 + max-width: 150px;
  73 + min-height: 28px;
  74 + max-height: 28px;
  75 + padding: 5px 10px;
  76 + border-radius: 5px;
  77 + background-color: #F15B26;
  78 + color: #333;
  79 + border: solid 1px #777;
  80 + font-size: 12px;
  81 + &.tb-input-type {
  82 + background-color: #a3eaa9;
  83 + user-select: none;
  84 + }
  85 + &.tb-filter-type {
  86 + background-color: #f1e861;
  87 + }
  88 + &.tb-enrichment-type {
  89 + background-color: #cdf14e;
  90 + }
  91 + &.tb-transformation-type {
  92 + background-color: #79cef1;
  93 + }
  94 + &.tb-action-type {
  95 + background-color: #f1928f;
  96 + }
  97 + &.tb-rule-chain-type {
  98 + background-color: #d6c4f1;
  99 + }
  100 + md-icon {
  101 + font-size: 20px;
  102 + width: 20px;
  103 + height: 20px;
  104 + min-height: 20px;
  105 + min-width: 20px;
  106 + padding-right: 4px;
  107 + }
  108 + .tb-node-type {
  109 +
  110 + }
  111 + .tb-node-title {
  112 + font-weight: 600;
  113 + }
  114 + .tb-node-type, .tb-node-title {
  115 + overflow: hidden;
  116 + white-space: nowrap;
  117 + text-overflow: ellipsis;
  118 + }
  119 +}
  120 +
  121 +.fc-node {
  122 + z-index: 1;
  123 + outline: none;
  124 + &.fc-hover, &.fc-selected {
  125 + -webkit-filter: brightness(70%);
  126 + filter: brightness(70%);
  127 + }
  128 + &.fc-dragging {
  129 + z-index: 10;
  130 + }
  131 + p {
  132 + padding: 0 15px;
  133 + text-align: center;
  134 + }
  135 +}
  136 +
  137 +.fc-leftConnectors, .fc-rightConnectors {
  138 + position: absolute;
  139 + top: 0;
  140 + height: 100%;
  141 +
  142 + display: flex;
  143 + flex-direction: column;
  144 +
  145 + z-index: 0;
  146 + .fc-magnet {
  147 + align-items: center;
  148 + }
  149 +}
  150 +
  151 +.fc-leftConnectors {
  152 + left: -20px;
  153 +}
  154 +
  155 +.fc-rightConnectors {
  156 + right: -20px;
  157 +}
  158 +
  159 +.fc-magnet {
  160 + display: flex;
  161 + flex-grow: 1;
  162 + height: 60px;
  163 + justify-content: center;
  164 +}
  165 +
  166 +.fc-connector {
  167 + width: 14px;
  168 + height: 14px;
  169 + border: 1px solid #333;
  170 + margin: 10px;
  171 + border-radius: 5px;
  172 + background-color: #ccc;
  173 +}
  174 +
  175 +.fc-connector.fc-hover {
  176 + background-color: #000;
  177 +}
  178 +
  179 +.fc-edge {
  180 + outline: none;
  181 + stroke: gray;
  182 + stroke-width: 4;
  183 + fill: transparent;
  184 + &.fc-selected {
  185 + stroke: red;
  186 + stroke-width: 4;
  187 + fill: transparent;
  188 + }
  189 + &.fc-active {
  190 + animation: dash 3s linear infinite;
  191 + stroke-dasharray: 20;
  192 + }
  193 + &.fc-hover {
  194 + stroke: gray;
  195 + stroke-width: 6;
  196 + fill: transparent;
  197 + }
  198 + &.fc-dragging {
  199 + pointer-events: none;
  200 + }
  201 +}
  202 +
  203 +.edge-endpoint {
  204 + fill: gray;
  205 +}
  206 +
  207 +.fc-nodedelete {
  208 + display: none;
  209 +}
  210 +
  211 +.fc-selected .fc-nodedelete {
  212 + outline: none;
  213 + display: block;
  214 + position: absolute;
  215 + right: -13px;
  216 + top: -16px;
  217 + border: solid 2px white;
  218 + border-radius: 50%;
  219 + font-weight: 600;
  220 + font-size: 18px;
  221 + line-height: 18px;
  222 + height: 20px;
  223 + padding-top: 2px;
  224 + width: 22px;
  225 + background: #494949;
  226 + color: #fff;
  227 + text-align: center;
  228 + vertical-align: bottom;
  229 + cursor: pointer;
  230 +}
  231 +
  232 +.fc-edge-label {
  233 + position: absolute;
  234 + user-select: none;
  235 + pointer-events: none;
  236 + opacity: 0.8;
  237 +}
  238 +
  239 +.fc-edge-label-text {
  240 + position: absolute;
  241 + left: 50%;
  242 + -webkit-transform: translateX(-50%);
  243 + transform: translateX(-50%);
  244 + white-space: nowrap;
  245 + text-align: center;
  246 + font-size: 14px;
  247 + font-weight: 600;
  248 + top: 5px;
  249 + span {
  250 + border: solid 2px #003a79;
  251 + border-radius: 10px;
  252 + color: #003a79;
  253 + background-color: #fff;
  254 + padding: 3px 5px;
  255 + }
  256 +}
  257 +
  258 +@keyframes dash {
  259 + from {
  260 + stroke-dashoffset: 500;
  261 + }
  262 +}
  1 +<!--
  2 +
  3 + Copyright © 2016-2018 The Thingsboard Authors
  4 +
  5 + Licensed under the Apache License, Version 2.0 (the "License");
  6 + you may not use this file except in compliance with the License.
  7 + You may obtain a copy of the License at
  8 +
  9 + http://www.apache.org/licenses/LICENSE-2.0
  10 +
  11 + Unless required by applicable law or agreed to in writing, software
  12 + distributed under the License is distributed on an "AS IS" BASIS,
  13 + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  14 + See the License for the specific language governing permissions and
  15 + limitations under the License.
  16 +
  17 +-->
  18 +
  19 +<md-content flex tb-expand-fullscreen
  20 + expand-tooltip-direction="bottom" layout="column" class="tb-rulechain">
  21 + <section class="tb-rulechain-container" flex layout="column">
  22 + <div class="tb-rulechain-layout" flex layout="row">
  23 + <div class="tb-rulechain-library">
  24 + <md-expansion-panel-group class="tb-rulechain-library-panel-group" md-component-id="libraryPanelGroup" auto-expand="true" multiple>
  25 + <md-expansion-panel md-component-id="{{typeId}}" id="{{typeId}}" ng-repeat="(typeId, typeModel) in vm.ruleNodeTypesModel">
  26 + <md-expansion-panel-collapsed>
  27 + <div class="tb-panel-title" translate>{{vm.types.ruleNodeType[typeId].name}}</div>
  28 + <md-expansion-panel-icon></md-expansion-panel-icon>
  29 + </md-expansion-panel-collapsed>
  30 + <md-expansion-panel-expanded>
  31 + <md-expansion-panel-header ng-click="vm.$mdExpansionPanel(typeId).collapse()">
  32 + <div class="tb-panel-title" translate>{{vm.types.ruleNodeType[typeId].name}}</div>
  33 + <md-expansion-panel-icon></md-expansion-panel-icon>
  34 + </md-expansion-panel-header>
  35 + <md-expansion-panel-content>
  36 + <fc-canvas id="tb-rulechain-{{typeId}}"
  37 + model="vm.ruleNodeTypesModel[typeId].model" selected-objects="vm.ruleNodeTypesModel[typeId].selectedObjects"
  38 + automatic-resize="false"
  39 + node-width="170"
  40 + node-height="50"
  41 + drop-target-id="'tb-rulchain-canvas'"></fc-canvas>
  42 + </md-expansion-panel-content>
  43 + </md-expansion-panel-expanded>
  44 + </md-expansion-panel>
  45 + </md-expansion-panel-group>
  46 + </div>
  47 + <div flex class="tb-rulechain-graph">
  48 + <fc-canvas id="tb-rulchain-canvas"
  49 + ng-keydown="vm.keyDown($event)"
  50 + ng-keyup="vm.keyUp($event)"
  51 + model="vm.ruleChainModel"
  52 + selected-objects="vm.selectedObjects"
  53 + edge-style="curved"
  54 + node-width="170"
  55 + node-height="50"
  56 + automatic-resize="true"
  57 + control="vm.canvasControl"
  58 + callbacks="vm.editCallbacks">
  59 + </fc-canvas>
  60 + </div>
  61 + </div>
  62 + <tb-details-sidenav class="tb-rulenode-details-sidenav"
  63 + header-title="{{vm.editingRuleNode.name}}"
  64 + header-subtitle="{{'rulenode.rulenode-details' | translate}}"
  65 + is-read-only="false"
  66 + is-open="vm.isEditingRuleNode"
  67 + is-always-edit="true"
  68 + on-close-details="vm.onEditRuleNodeClosed()"
  69 + on-toggle-details-edit-mode="vm.onRevertRuleNodeEdit(vm.ruleNodeForm)"
  70 + on-apply-details="vm.saveRuleNode(vm.ruleNodeForm)"
  71 + the-form="vm.ruleNodeForm">
  72 + <details-buttons tb-help="vm.helpLinkIdForRuleNodeType()" help-container-id="help-container">
  73 + <div id="help-container"></div>
  74 + </details-buttons>
  75 + <form name="vm.ruleNodeForm" ng-if="vm.isEditingRuleNode">
  76 + <tb-rule-node
  77 + rule-node="vm.editingRuleNode"
  78 + rule-chain-id="vm.ruleChain.id.id"
  79 + is-edit="true"
  80 + is-read-only="false"
  81 + on-delete-rule-node="vm.deleteRuleNode(event, vm.editingRuleNode)"
  82 + the-form="vm.ruleNodeForm">
  83 + </tb-rule-node>
  84 + </form>
  85 + </tb-details-sidenav>
  86 + <tb-details-sidenav class="tb-rulenode-link-details-sidenav"
  87 + header-title="{{vm.editingRuleNodeLink.label}}"
  88 + header-subtitle="{{'rulenode.link-details' | translate}}"
  89 + is-read-only="false"
  90 + is-open="vm.isEditingRuleNodeLink"
  91 + is-always-edit="true"
  92 + on-close-details="vm.onEditRuleNodeLinkClosed()"
  93 + on-toggle-details-edit-mode="vm.onRevertRuleNodeLinkEdit(vm.ruleNodeLinkForm)"
  94 + on-apply-details="vm.saveRuleNodeLink(vm.ruleNodeLinkForm)"
  95 + the-form="vm.ruleNodeLinkForm">
  96 + <details-buttons tb-help="vm.helpLinkIdForRuleNodeLink()" help-container-id="link-help-container">
  97 + <div id="link-help-container"></div>
  98 + </details-buttons>
  99 + <form name="vm.ruleNodeLinkForm" ng-if="vm.isEditingRuleNodeLink">
  100 + <tb-rule-node-link
  101 + link="vm.editingRuleNodeLink"
  102 + labels="vm.editingRuleNodeLinkLabels"
  103 + is-edit="true"
  104 + is-read-only="false"
  105 + the-form="vm.ruleNodeLinkForm">
  106 + </tb-rule-node-link>
  107 + </form>
  108 + </tb-details-sidenav>
  109 + </section>
  110 + <section layout="row" layout-wrap class="tb-footer-buttons md-fab" layout-align="start end">
  111 + <md-button ng-disabled="$root.loading || !vm.isDirty"
  112 + class="tb-btn-footer md-accent md-hue-2 md-fab"
  113 + aria-label="{{ 'action.apply' | translate }}"
  114 + ng-click="vm.saveRuleChain()">
  115 + <md-tooltip md-direction="top">
  116 + {{ 'action.apply-changes' | translate }}
  117 + </md-tooltip>
  118 + <ng-md-icon icon="done"></ng-md-icon>
  119 + </md-button>
  120 + <md-button ng-disabled="$root.loading || !vm.isDirty"
  121 + class="tb-btn-footer md-accent md-hue-2 md-fab"
  122 + aria-label="{{ 'action.decline-changes' | translate }}"
  123 + ng-click="vm.revertRuleChain()">
  124 + <md-tooltip md-direction="top">
  125 + {{ 'action.decline-changes' | translate }}
  126 + </md-tooltip>
  127 + <ng-md-icon icon="close"></ng-md-icon>
  128 + </md-button>
  129 + </section>
  130 +</md-content>
  1 +/*
  2 + * Copyright © 2016-2018 The Thingsboard Authors
  3 + *
  4 + * Licensed under the Apache License, Version 2.0 (the "License");
  5 + * you may not use this file except in compliance with the License.
  6 + * You may obtain a copy of the License at
  7 + *
  8 + * http://www.apache.org/licenses/LICENSE-2.0
  9 + *
  10 + * Unless required by applicable law or agreed to in writing, software
  11 + * distributed under the License is distributed on an "AS IS" BASIS,
  12 + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  13 + * See the License for the specific language governing permissions and
  14 + * limitations under the License.
  15 + */
  16 +/* eslint-disable import/no-unresolved, import/default */
  17 +
  18 +import addRuleChainTemplate from './add-rulechain.tpl.html';
  19 +import ruleChainCard from './rulechain-card.tpl.html';
  20 +
  21 +/* eslint-enable import/no-unresolved, import/default */
  22 +
  23 +/*@ngInject*/
  24 +export default function RuleChainsController(ruleChainService, userService, importExport, $state, $stateParams, $filter, $translate, types) {
  25 +
  26 + var ruleChainActionsList = [
  27 + {
  28 + onAction: function ($event, item) {
  29 + vm.grid.openItem($event, item);
  30 + },
  31 + name: function() { return $translate.instant('rulechain.details') },
  32 + details: function() { return $translate.instant('rulechain.rulechain-details') },
  33 + icon: "edit"
  34 + },
  35 + {
  36 + onAction: function ($event, item) {
  37 + exportRuleChain($event, item);
  38 + },
  39 + name: function() { $translate.instant('action.export') },
  40 + details: function() { return $translate.instant('rulechain.export') },
  41 + icon: "file_download"
  42 + },
  43 + {
  44 + onAction: function ($event, item) {
  45 + vm.grid.deleteItem($event, item);
  46 + },
  47 + name: function() { return $translate.instant('action.delete') },
  48 + details: function() { return $translate.instant('rulechain.delete') },
  49 + icon: "delete",
  50 + isEnabled: isRuleChainEditable
  51 + }
  52 + ];
  53 +
  54 + var ruleChainAddItemActionsList = [
  55 + {
  56 + onAction: function ($event) {
  57 + vm.grid.addItem($event);
  58 + },
  59 + name: function() { return $translate.instant('action.create') },
  60 + details: function() { return $translate.instant('rulechain.create-new-rulechain') },
  61 + icon: "insert_drive_file"
  62 + },
  63 + {
  64 + onAction: function ($event) {
  65 + importExport.importRuleChain($event).then(
  66 + function() {
  67 + vm.grid.refreshList();
  68 + }
  69 + );
  70 + },
  71 + name: function() { return $translate.instant('action.import') },
  72 + details: function() { return $translate.instant('rulechain.import') },
  73 + icon: "file_upload"
  74 + }
  75 + ];
  76 +
  77 + var vm = this;
  78 +
  79 + vm.types = types;
  80 +
  81 + vm.ruleChainGridConfig = {
  82 +
  83 + refreshParamsFunc: null,
  84 +
  85 + deleteItemTitleFunc: deleteRuleChainTitle,
  86 + deleteItemContentFunc: deleteRuleChainText,
  87 + deleteItemsTitleFunc: deleteRuleChainsTitle,
  88 + deleteItemsActionTitleFunc: deleteRuleChainsActionTitle,
  89 + deleteItemsContentFunc: deleteRuleChainsText,
  90 +
  91 + fetchItemsFunc: fetchRuleChains,
  92 + saveItemFunc: saveRuleChain,
  93 + clickItemFunc: openRuleChain,
  94 + deleteItemFunc: deleteRuleChain,
  95 +
  96 + getItemTitleFunc: getRuleChainTitle,
  97 + itemCardTemplateUrl: ruleChainCard,
  98 + parentCtl: vm,
  99 +
  100 + actionsList: ruleChainActionsList,
  101 + addItemActions: ruleChainAddItemActionsList,
  102 +
  103 + onGridInited: gridInited,
  104 +
  105 + addItemTemplateUrl: addRuleChainTemplate,
  106 +
  107 + addItemText: function() { return $translate.instant('rulechain.add-rulechain-text') },
  108 + noItemsText: function() { return $translate.instant('rulechain.no-rulechains-text') },
  109 + itemDetailsText: function() { return $translate.instant('rulechain.rulechain-details') },
  110 + isSelectionEnabled: isRuleChainEditable,
  111 + isDetailsReadOnly: function(ruleChain) {
  112 + return !isRuleChainEditable(ruleChain);
  113 + }
  114 + };
  115 +
  116 + if (angular.isDefined($stateParams.items) && $stateParams.items !== null) {
  117 + vm.ruleChainGridConfig.items = $stateParams.items;
  118 + }
  119 +
  120 + if (angular.isDefined($stateParams.topIndex) && $stateParams.topIndex > 0) {
  121 + vm.ruleChainGridConfig.topIndex = $stateParams.topIndex;
  122 + }
  123 +
  124 + vm.isRuleChainEditable = isRuleChainEditable;
  125 +
  126 + vm.exportRuleChain = exportRuleChain;
  127 +
  128 + function deleteRuleChainTitle(ruleChain) {
  129 + return $translate.instant('rulechain.delete-rulechain-title', {ruleChainName: ruleChain.name});
  130 + }
  131 +
  132 + function deleteRuleChainText() {
  133 + return $translate.instant('rulechain.delete-rulechain-text');
  134 + }
  135 +
  136 + function deleteRuleChainsTitle(selectedCount) {
  137 + return $translate.instant('rulechain.delete-rulechains-title', {count: selectedCount}, 'messageformat');
  138 + }
  139 +
  140 + function deleteRuleChainsActionTitle(selectedCount) {
  141 + return $translate.instant('rulechain.delete-rulechains-action-title', {count: selectedCount}, 'messageformat');
  142 + }
  143 +
  144 + function deleteRuleChainsText() {
  145 + return $translate.instant('rulechain.delete-rulechains-text');
  146 + }
  147 +
  148 + function gridInited(grid) {
  149 + vm.grid = grid;
  150 + }
  151 +
  152 + function fetchRuleChains(pageLink) {
  153 + return ruleChainService.getRuleChains(pageLink);
  154 + }
  155 +
  156 + function saveRuleChain(ruleChain) {
  157 + return ruleChainService.saveRuleChain(ruleChain);
  158 + }
  159 +
  160 + function openRuleChain($event, ruleChain) {
  161 + if ($event) {
  162 + $event.stopPropagation();
  163 + }
  164 + $state.go('home.ruleChains.ruleChain', {ruleChainId: ruleChain.id.id});
  165 + }
  166 +
  167 + function deleteRuleChain(ruleChainId) {
  168 + return ruleChainService.deleteRuleChain(ruleChainId);
  169 + }
  170 +
  171 + function getRuleChainTitle(ruleChain) {
  172 + return ruleChain ? ruleChain.name : '';
  173 + }
  174 +
  175 + function isRuleChainEditable(ruleChain) {
  176 + if (userService.getAuthority() === 'TENANT_ADMIN') {
  177 + return ruleChain && ruleChain.tenantId.id != types.id.nullUid;
  178 + } else {
  179 + return userService.getAuthority() === 'SYS_ADMIN';
  180 + }
  181 + }
  182 +
  183 + function exportRuleChain($event, ruleChain) {
  184 + $event.stopPropagation();
  185 + importExport.exportRuleChain(ruleChain.id.id);
  186 + }
  187 +
  188 +}
  1 +<!--
  2 +
  3 + Copyright © 2016-2018 The Thingsboard Authors
  4 +
  5 + Licensed under the Apache License, Version 2.0 (the "License");
  6 + you may not use this file except in compliance with the License.
  7 + You may obtain a copy of the License at
  8 +
  9 + http://www.apache.org/licenses/LICENSE-2.0
  10 +
  11 + Unless required by applicable law or agreed to in writing, software
  12 + distributed under the License is distributed on an "AS IS" BASIS,
  13 + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  14 + See the License for the specific language governing permissions and
  15 + limitations under the License.
  16 +
  17 +-->
  18 +<md-button ng-click="onDeleteRuleNode({event: $event})"
  19 + ng-show="!isEdit && !isReadOnly"
  20 + class="md-raised md-primary">{{ 'rulenode.delete' | translate }}</md-button>
  21 +
  22 +<md-content class="md-padding tb-rulenode" layout="column">
  23 + <fieldset ng-disabled="$root.loading || !isEdit || isReadOnly">
  24 + <md-input-container class="md-block">
  25 + <label translate>rulenode.type</label>
  26 + <input readonly name="type" ng-model="ruleNode.type">
  27 + </md-input-container>
  28 + <section ng-if="ruleNode.nodeType != types.ruleNodeType.RULE_CHAIN.value">
  29 + <md-input-container class="md-block">
  30 + <label translate>rulenode.name</label>
  31 + <input required name="name" ng-model="ruleNode.name">
  32 + <div ng-messages="theForm.name.$error">
  33 + <div translate ng-message="required">rulenode.name-required</div>
  34 + </div>
  35 + </md-input-container>
  36 + <md-input-container class="md-block">
  37 + <label translate>rulenode.description</label>
  38 + <textarea ng-model="ruleNode.additionalInfo.description" rows="2"></textarea>
  39 + </md-input-container>
  40 + </section>
  41 + <section ng-if="ruleNode.nodeType == types.ruleNodeType.RULE_CHAIN.value">
  42 + <tb-entity-autocomplete the-form="theForm"
  43 + ng-disabled="$root.loading || !isEdit || isReadOnly"
  44 + tb-required="true"
  45 + exclude-entity-ids="[ruleChainId]"
  46 + entity-type="types.entityType.rulechain"
  47 + ng-model="params.targetRuleChainId">
  48 + </tb-entity-autocomplete>
  49 + <md-input-container class="md-block">
  50 + <label translate>rulenode.description</label>
  51 + <textarea ng-model="ruleNode.additionalInfo.description" rows="2"></textarea>
  52 + </md-input-container>
  53 + </section>
  54 + </fieldset>
  55 +</md-content>
  1 +/*
  2 + * Copyright © 2016-2018 The Thingsboard Authors
  3 + *
  4 + * Licensed under the Apache License, Version 2.0 (the "License");
  5 + * you may not use this file except in compliance with the License.
  6 + * You may obtain a copy of the License at
  7 + *
  8 + * http://www.apache.org/licenses/LICENSE-2.0
  9 + *
  10 + * Unless required by applicable law or agreed to in writing, software
  11 + * distributed under the License is distributed on an "AS IS" BASIS,
  12 + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  13 + * See the License for the specific language governing permissions and
  14 + * limitations under the License.
  15 + */
  16 +
  17 +/* eslint-disable import/no-unresolved, import/default */
  18 +
  19 +import ruleNodeFieldsetTemplate from './rulenode-fieldset.tpl.html';
  20 +
  21 +/* eslint-enable import/no-unresolved, import/default */
  22 +
  23 +/*@ngInject*/
  24 +export default function RuleNodeDirective($compile, $templateCache, ruleChainService, types) {
  25 + var linker = function (scope, element) {
  26 + var template = $templateCache.get(ruleNodeFieldsetTemplate);
  27 + element.html(template);
  28 +
  29 + scope.types = types;
  30 +
  31 + scope.params = {
  32 + targetRuleChainId: null
  33 + };
  34 +
  35 + scope.$watch('ruleNode', function() {
  36 + if (scope.ruleNode && scope.ruleNode.nodeType == types.ruleNodeType.RULE_CHAIN.value) {
  37 + scope.params.targetRuleChainId = scope.ruleNode.targetRuleChainId;
  38 + watchTargetRuleChain();
  39 + } else {
  40 + if (scope.targetRuleChainWatch) {
  41 + scope.targetRuleChainWatch();
  42 + scope.targetRuleChainWatch = null;
  43 + }
  44 + }
  45 + });
  46 +
  47 + function watchTargetRuleChain() {
  48 + scope.targetRuleChainWatch = scope.$watch('params.targetRuleChainId',
  49 + function(targetRuleChainId) {
  50 + if (scope.ruleNode.targetRuleChainId != targetRuleChainId) {
  51 + scope.ruleNode.targetRuleChainId = targetRuleChainId;
  52 + if (targetRuleChainId) {
  53 + ruleChainService.getRuleChain(targetRuleChainId).then(
  54 + (ruleChain) => {
  55 + scope.ruleNode.name = ruleChain.name;
  56 + }
  57 + );
  58 + } else {
  59 + scope.ruleNode.name = "";
  60 + }
  61 + }
  62 + }
  63 + );
  64 + }
  65 + $compile(element.contents())(scope);
  66 + }
  67 + return {
  68 + restrict: "E",
  69 + link: linker,
  70 + scope: {
  71 + ruleChainId: '=',
  72 + ruleNode: '=',
  73 + isEdit: '=',
  74 + isReadOnly: '=',
  75 + theForm: '=',
  76 + onDeleteRuleNode: '&'
  77 + }
  78 + };
  79 +}
  1 +<!--
  2 +
  3 + Copyright © 2016-2018 The Thingsboard Authors
  4 +
  5 + Licensed under the Apache License, Version 2.0 (the "License");
  6 + you may not use this file except in compliance with the License.
  7 + You may obtain a copy of the License at
  8 +
  9 + http://www.apache.org/licenses/LICENSE-2.0
  10 +
  11 + Unless required by applicable law or agreed to in writing, software
  12 + distributed under the License is distributed on an "AS IS" BASIS,
  13 + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  14 + See the License for the specific language governing permissions and
  15 + limitations under the License.
  16 +
  17 +-->
  18 +<div
  19 + id="{{node.id}}"
  20 + ng-attr-style="position: absolute; top: {{ node.y }}px; left: {{ node.x }}px;"
  21 + ng-dblclick="callbacks.doubleClick($event, node)">
  22 + <div class="tb-rule-node {{node.nodeClass}}">
  23 + <md-icon aria-label="node-type-icon" flex="15"
  24 + class="material-icons">{{node.icon}}</md-icon>
  25 + <div layout="column" flex="85" layout-align="center">
  26 + <span class="tb-node-type">{{ node.type }}</span>
  27 + <span class="tb-node-title" ng-if="node.name">{{ node.name }}</span>
  28 + </div>
  29 + <div class="{{flowchartConstants.leftConnectorClass}}">
  30 + <div fc-magnet
  31 + ng-repeat="connector in modelservice.nodes.getConnectorsByType(node, flowchartConstants.leftConnectorType)">
  32 + <div fc-connector></div>
  33 + </div>
  34 + </div>
  35 + <div class="{{flowchartConstants.rightConnectorClass}}">
  36 + <div fc-magnet
  37 + ng-repeat="connector in modelservice.nodes.getConnectorsByType(node, flowchartConstants.rightConnectorType)">
  38 + <div fc-connector></div>
  39 + </div>
  40 + </div>
  41 + </div>
  42 + <div ng-if="modelservice.isEditable() && !node.readonly" class="fc-nodedelete" ng-click="modelservice.nodes.delete(node)">
  43 + &times;
  44 + </div>
  45 +</div>