Showing
23 changed files
with
1761 additions
and
121 deletions
... | ... | @@ -15,6 +15,7 @@ |
15 | 15 | */ |
16 | 16 | package org.thingsboard.server.common.data.rule; |
17 | 17 | |
18 | +import com.fasterxml.jackson.databind.JsonNode; | |
18 | 19 | import lombok.Data; |
19 | 20 | import org.thingsboard.server.common.data.id.RuleChainId; |
20 | 21 | |
... | ... | @@ -47,11 +48,12 @@ public class RuleChainMetaData { |
47 | 48 | } |
48 | 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 | 52 | RuleChainConnectionInfo connectionInfo = new RuleChainConnectionInfo(); |
52 | 53 | connectionInfo.setFromIndex(fromIndex); |
53 | 54 | connectionInfo.setTargetRuleChainId(targetRuleChainId); |
54 | 55 | connectionInfo.setType(type); |
56 | + connectionInfo.setAdditionalInfo(additionalInfo); | |
55 | 57 | if (ruleChainConnections == null) { |
56 | 58 | ruleChainConnections = new ArrayList<>(); |
57 | 59 | } |
... | ... | @@ -59,16 +61,17 @@ public class RuleChainMetaData { |
59 | 61 | } |
60 | 62 | |
61 | 63 | @Data |
62 | - public class NodeConnectionInfo { | |
64 | + public static class NodeConnectionInfo { | |
63 | 65 | private int fromIndex; |
64 | 66 | private int toIndex; |
65 | 67 | private String type; |
66 | 68 | } |
67 | 69 | |
68 | 70 | @Data |
69 | - public class RuleChainConnectionInfo { | |
71 | + public static class RuleChainConnectionInfo { | |
70 | 72 | private int fromIndex; |
71 | 73 | private RuleChainId targetRuleChainId; |
74 | + private JsonNode additionalInfo; | |
72 | 75 | private String type; |
73 | 76 | } |
74 | 77 | ... | ... |
... | ... | @@ -166,7 +166,7 @@ public class BaseRuleChainService extends AbstractEntityService implements RuleC |
166 | 166 | EntityId to = nodeToRuleChainConnection.getTargetRuleChainId(); |
167 | 167 | String type = nodeToRuleChainConnection.getType(); |
168 | 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 | 170 | } catch (ExecutionException | InterruptedException e) { |
171 | 171 | log.warn("[{}] Failed to create rule node to rule chain relation. from: [{}], to: [{}]", from, to); |
172 | 172 | throw new RuntimeException(e); |
... | ... | @@ -206,7 +206,7 @@ public class BaseRuleChainService extends AbstractEntityService implements RuleC |
206 | 206 | ruleChainMetaData.addConnectionInfo(fromIndex, toIndex, type); |
207 | 207 | } else if (nodeRelation.getTo().getEntityType() == EntityType.RULE_CHAIN) { |
208 | 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 | } | ... | ... |
... | ... | @@ -22,7 +22,7 @@ export default angular.module('thingsboard.api.entity', [thingsboardTypes]) |
22 | 22 | /*@ngInject*/ |
23 | 23 | function EntityService($http, $q, $filter, $translate, $log, userService, deviceService, |
24 | 24 | assetService, tenantService, customerService, |
25 | - ruleService, pluginService, dashboardService, entityRelationService, attributeService, types, utils) { | |
25 | + ruleService, pluginService, ruleChainService, dashboardService, entityRelationService, attributeService, types, utils) { | |
26 | 26 | var service = { |
27 | 27 | getEntity: getEntity, |
28 | 28 | getEntities: getEntities, |
... | ... | @@ -73,6 +73,9 @@ function EntityService($http, $q, $filter, $translate, $log, userService, device |
73 | 73 | case types.entityType.user: |
74 | 74 | promise = userService.getUser(entityId, true, config); |
75 | 75 | break; |
76 | + case types.entityType.rulechain: | |
77 | + promise = ruleChainService.getRuleChain(entityId, config); | |
78 | + break; | |
76 | 79 | case types.entityType.alarm: |
77 | 80 | $log.error('Get Alarm Entity is not implemented!'); |
78 | 81 | break; |
... | ... | @@ -271,6 +274,9 @@ function EntityService($http, $q, $filter, $translate, $log, userService, device |
271 | 274 | case types.entityType.plugin: |
272 | 275 | promise = pluginService.getAllPlugins(pageLink, config); |
273 | 276 | break; |
277 | + case types.entityType.rulechain: | |
278 | + promise = ruleChainService.getRuleChains(pageLink, config); | |
279 | + break; | |
274 | 280 | case types.entityType.dashboard: |
275 | 281 | if (user.authority === 'CUSTOMER_USER') { |
276 | 282 | promise = dashboardService.getCustomerDashboards(customerId, pageLink, config); | ... | ... |
... | ... | @@ -17,7 +17,9 @@ export default angular.module('thingsboard.api.ruleChain', []) |
17 | 17 | .factory('ruleChainService', RuleChainService).name; |
18 | 18 | |
19 | 19 | /*@ngInject*/ |
20 | -function RuleChainService($http, $q) { | |
20 | +function RuleChainService($http, $q, $filter, types) { | |
21 | + | |
22 | + var ruleNodeTypes = null; | |
21 | 23 | |
22 | 24 | var service = { |
23 | 25 | getSystemRuleChains: getSystemRuleChains, |
... | ... | @@ -27,7 +29,11 @@ function RuleChainService($http, $q) { |
27 | 29 | saveRuleChain: saveRuleChain, |
28 | 30 | deleteRuleChain: deleteRuleChain, |
29 | 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 | 39 | return service; |
... | ... | @@ -147,4 +153,131 @@ function RuleChainService($http, $q) { |
147 | 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 | 49 | import 'react-schema-form'; |
50 | 50 | import react from 'ngreact'; |
51 | 51 | import '@flowjs/ng-flow/dist/ng-flow-standalone.min'; |
52 | +import 'ngFlowchart/dist/ngFlowchart'; | |
52 | 53 | |
53 | 54 | import thingsboardLocales from './locale/locale.constant'; |
54 | 55 | import thingsboardLogin from './login'; |
... | ... | @@ -86,6 +87,7 @@ import 'mdPickers/dist/mdPickers.min.css'; |
86 | 87 | import 'angular-hotkeys/build/hotkeys.min.css'; |
87 | 88 | import 'angular-carousel/dist/angular-carousel.min.css'; |
88 | 89 | import 'angular-material-expansion-panel/dist/md-expansion-panel.min.css'; |
90 | +import 'ngFlowchart/dist/flowchart.css'; | |
89 | 91 | import '../scss/main.scss'; |
90 | 92 | |
91 | 93 | import AppConfig from './app.config'; |
... | ... | @@ -113,6 +115,7 @@ angular.module('thingsboard', [ |
113 | 115 | 'ngclipboard', |
114 | 116 | react.name, |
115 | 117 | 'flow', |
118 | + 'flowchart', | |
116 | 119 | thingsboardLocales, |
117 | 120 | thingsboardLogin, |
118 | 121 | thingsboardDialogs, | ... | ... |
... | ... | @@ -457,6 +457,44 @@ export default angular.module('thingsboard.types', []) |
457 | 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 | 498 | valueType: { |
461 | 499 | string: { |
462 | 500 | value: "string", | ... | ... |
... | ... | @@ -143,6 +143,12 @@ export default function EntityAutocomplete($compile, $templateCache, $q, $filter |
143 | 143 | scope.noEntitiesMatchingText = 'plugin.no-plugins-matching'; |
144 | 144 | scope.entityRequiredText = 'plugin.plugin-required'; |
145 | 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 | 152 | case types.entityType.tenant: |
147 | 153 | scope.selectEntityText = 'tenant.select-tenant'; |
148 | 154 | scope.entityText = 'tenant.tenant'; | ... | ... |
... | ... | @@ -1169,6 +1169,26 @@ export default angular.module('thingsboard.locale', []) |
1169 | 1169 | "rulechain-required": "Rule chain is required", |
1170 | 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 | 1192 | "rule-plugin": { |
1173 | 1193 | "management": "Rules and plugins management" |
1174 | 1194 | }, | ... | ... |
ui/src/app/rulechain/add-link.tpl.html
0 → 100644
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> | ... | ... |
ui/src/app/rulechain/add-rulenode.tpl.html
0 → 100644
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 | 15 | */ |
16 | 16 | |
17 | 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 | 20 | import RuleChainDirective from './rulechain.directive'; |
21 | +import RuleNodeDirective from './rulenode.directive'; | |
22 | +import LinkDirective from './link.directive'; | |
20 | 23 | |
21 | 24 | export default angular.module('thingsboard.ruleChain', []) |
22 | 25 | .config(RuleChainRoutes) |
26 | + .controller('RuleChainsController', RuleChainsController) | |
23 | 27 | .controller('RuleChainController', RuleChainController) |
28 | + .controller('AddRuleNodeController', AddRuleNodeController) | |
29 | + .controller('AddRuleNodeLinkController', AddRuleNodeLinkController) | |
24 | 30 | .directive('tbRuleChain', RuleChainDirective) |
31 | + .directive('tbRuleNode', RuleNodeDirective) | |
32 | + .directive('tbRuleNodeLink', LinkDirective) | |
25 | 33 | .name; | ... | ... |
ui/src/app/rulechain/link-fieldset.tpl.html
0 → 100644
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> | ... | ... |
ui/src/app/rulechain/link.directive.js
0 → 100644
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 | 32 | </md-button> |
33 | 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 | 36 | <fieldset ng-disabled="$root.loading || !isEdit || isReadOnly"> |
37 | 37 | <md-input-container class="md-block"> |
38 | 38 | <label translate>rulechain.name</label> | ... | ... |
... | ... | @@ -13,160 +13,582 @@ |
13 | 13 | * See the License for the specific language governing permissions and |
14 | 14 | * limitations under the License. |
15 | 15 | */ |
16 | + | |
17 | +import './rulechain.scss'; | |
18 | + | |
16 | 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 | 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 | 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 | 35 | var vm = this; |
70 | 36 | |
37 | + vm.$mdExpansionPanel = $mdExpansionPanel; | |
71 | 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 | 15 | */ |
16 | 16 | /* eslint-disable import/no-unresolved, import/default */ |
17 | 17 | |
18 | +import ruleNodeTemplate from './rulenode.tpl.html'; | |
18 | 19 | import ruleChainsTemplate from './rulechains.tpl.html'; |
20 | +import ruleChainTemplate from './rulechain.tpl.html'; | |
19 | 21 | |
20 | 22 | /* eslint-enable import/no-unresolved, import/default */ |
21 | 23 | |
22 | 24 | /*@ngInject*/ |
23 | -export default function RuleChainRoutes($stateProvider) { | |
25 | +export default function RuleChainRoutes($stateProvider, NodeTemplatePathProvider) { | |
26 | + | |
27 | + NodeTemplatePathProvider.setTemplatePath(ruleNodeTemplate); | |
24 | 28 | |
25 | 29 | $stateProvider |
26 | 30 | .state('home.ruleChains', { |
... | ... | @@ -32,7 +36,7 @@ export default function RuleChainRoutes($stateProvider) { |
32 | 36 | "content@home": { |
33 | 37 | templateUrl: ruleChainsTemplate, |
34 | 38 | controllerAs: 'vm', |
35 | - controller: 'RuleChainController' | |
39 | + controller: 'RuleChainsController' | |
36 | 40 | } |
37 | 41 | }, |
38 | 42 | data: { |
... | ... | @@ -42,5 +46,36 @@ export default function RuleChainRoutes($stateProvider) { |
42 | 46 | ncyBreadcrumb: { |
43 | 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 | } | ... | ... |
ui/src/app/rulechain/rulechain.scss
0 → 100644
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 | +} | ... | ... |
ui/src/app/rulechain/rulechain.tpl.html
0 → 100644
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> | ... | ... |
ui/src/app/rulechain/rulenode.directive.js
0 → 100644
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 | +} | ... | ... |
ui/src/app/rulechain/rulenode.tpl.html
0 → 100644
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 | + × | |
44 | + </div> | |
45 | +</div> | ... | ... |