thingsboard-memoizeit
Changes
ui/package.json 1(+1 -0)
ui/src/app/api/entity.service.js 8(+7 -1)
ui/src/app/api/rule-chain.service.js 137(+135 -2)
ui/src/app/app.js 3(+3 -0)
ui/src/app/common/types.constant.js 38(+38 -0)
ui/src/app/locale/locale.constant.js 20(+20 -0)
ui/src/app/rulechain/add-link.tpl.html 48(+48 -0)
ui/src/app/rulechain/add-rulenode.tpl.html 48(+48 -0)
ui/src/app/rulechain/index.js 10(+9 -1)
ui/src/app/rulechain/link.directive.js 71(+71 -0)
ui/src/app/rulechain/link-fieldset.tpl.html 39(+39 -0)
ui/src/app/rulechain/rulechain.controller.js 638(+530 -108)
ui/src/app/rulechain/rulechain.routes.js 41(+38 -3)
ui/src/app/rulechain/rulechain.scss 262(+262 -0)
ui/src/app/rulechain/rulechain.tpl.html 130(+130 -0)
ui/src/app/rulechain/rulechains.controller.js 188(+188 -0)
ui/src/app/rulechain/rulenode.directive.js 79(+79 -0)
ui/src/app/rulechain/rulenode.tpl.html 45(+45 -0)
Details
diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/rule/RuleChainMetaData.java b/common/data/src/main/java/org/thingsboard/server/common/data/rule/RuleChainMetaData.java
index af141d6..8deece2 100644
--- a/common/data/src/main/java/org/thingsboard/server/common/data/rule/RuleChainMetaData.java
+++ b/common/data/src/main/java/org/thingsboard/server/common/data/rule/RuleChainMetaData.java
@@ -15,6 +15,7 @@
*/
package org.thingsboard.server.common.data.rule;
+import com.fasterxml.jackson.databind.JsonNode;
import lombok.Data;
import org.thingsboard.server.common.data.id.RuleChainId;
@@ -47,11 +48,12 @@ public class RuleChainMetaData {
}
connections.add(connectionInfo);
}
- public void addRuleChainConnectionInfo(int fromIndex, RuleChainId targetRuleChainId, String type) {
+ public void addRuleChainConnectionInfo(int fromIndex, RuleChainId targetRuleChainId, String type, JsonNode additionalInfo) {
RuleChainConnectionInfo connectionInfo = new RuleChainConnectionInfo();
connectionInfo.setFromIndex(fromIndex);
connectionInfo.setTargetRuleChainId(targetRuleChainId);
connectionInfo.setType(type);
+ connectionInfo.setAdditionalInfo(additionalInfo);
if (ruleChainConnections == null) {
ruleChainConnections = new ArrayList<>();
}
@@ -59,16 +61,17 @@ public class RuleChainMetaData {
}
@Data
- public class NodeConnectionInfo {
+ public static class NodeConnectionInfo {
private int fromIndex;
private int toIndex;
private String type;
}
@Data
- public class RuleChainConnectionInfo {
+ public static class RuleChainConnectionInfo {
private int fromIndex;
private RuleChainId targetRuleChainId;
+ private JsonNode additionalInfo;
private String type;
}
diff --git a/dao/src/main/java/org/thingsboard/server/dao/rule/BaseRuleChainService.java b/dao/src/main/java/org/thingsboard/server/dao/rule/BaseRuleChainService.java
index 04207ec..c8c8df7 100644
--- a/dao/src/main/java/org/thingsboard/server/dao/rule/BaseRuleChainService.java
+++ b/dao/src/main/java/org/thingsboard/server/dao/rule/BaseRuleChainService.java
@@ -166,7 +166,7 @@ public class BaseRuleChainService extends AbstractEntityService implements RuleC
EntityId to = nodeToRuleChainConnection.getTargetRuleChainId();
String type = nodeToRuleChainConnection.getType();
try {
- createRelation(new EntityRelation(from, to, type, RelationTypeGroup.RULE_NODE));
+ createRelation(new EntityRelation(from, to, type, RelationTypeGroup.RULE_NODE, nodeToRuleChainConnection.getAdditionalInfo()));
} catch (ExecutionException | InterruptedException e) {
log.warn("[{}] Failed to create rule node to rule chain relation. from: [{}], to: [{}]", from, to);
throw new RuntimeException(e);
@@ -206,7 +206,7 @@ public class BaseRuleChainService extends AbstractEntityService implements RuleC
ruleChainMetaData.addConnectionInfo(fromIndex, toIndex, type);
} else if (nodeRelation.getTo().getEntityType() == EntityType.RULE_CHAIN) {
RuleChainId targetRuleChainId = new RuleChainId(nodeRelation.getTo().getId());
- ruleChainMetaData.addRuleChainConnectionInfo(fromIndex, targetRuleChainId, type);
+ ruleChainMetaData.addRuleChainConnectionInfo(fromIndex, targetRuleChainId, type, nodeRelation.getAdditionalInfo());
}
}
}
ui/package.json 1(+1 -0)
diff --git a/ui/package.json b/ui/package.json
index ad95ef4..b602720 100644
--- a/ui/package.json
+++ b/ui/package.json
@@ -69,6 +69,7 @@
"moment": "^2.15.0",
"ngclipboard": "^1.1.1",
"ngreact": "^0.3.0",
+ "ngFlowchart": "git://github.com/ikulikov/ngFlowchart.git#master",
"objectpath": "^1.2.1",
"oclazyload": "^1.0.9",
"raphael": "^2.2.7",
ui/src/app/api/entity.service.js 8(+7 -1)
diff --git a/ui/src/app/api/entity.service.js b/ui/src/app/api/entity.service.js
index e4c51a2..ba1265f 100644
--- a/ui/src/app/api/entity.service.js
+++ b/ui/src/app/api/entity.service.js
@@ -22,7 +22,7 @@ export default angular.module('thingsboard.api.entity', [thingsboardTypes])
/*@ngInject*/
function EntityService($http, $q, $filter, $translate, $log, userService, deviceService,
assetService, tenantService, customerService,
- ruleService, pluginService, dashboardService, entityRelationService, attributeService, types, utils) {
+ ruleService, pluginService, ruleChainService, dashboardService, entityRelationService, attributeService, types, utils) {
var service = {
getEntity: getEntity,
getEntities: getEntities,
@@ -73,6 +73,9 @@ function EntityService($http, $q, $filter, $translate, $log, userService, device
case types.entityType.user:
promise = userService.getUser(entityId, true, config);
break;
+ case types.entityType.rulechain:
+ promise = ruleChainService.getRuleChain(entityId, config);
+ break;
case types.entityType.alarm:
$log.error('Get Alarm Entity is not implemented!');
break;
@@ -271,6 +274,9 @@ function EntityService($http, $q, $filter, $translate, $log, userService, device
case types.entityType.plugin:
promise = pluginService.getAllPlugins(pageLink, config);
break;
+ case types.entityType.rulechain:
+ promise = ruleChainService.getRuleChains(pageLink, config);
+ break;
case types.entityType.dashboard:
if (user.authority === 'CUSTOMER_USER') {
promise = dashboardService.getCustomerDashboards(customerId, pageLink, config);
ui/src/app/api/rule-chain.service.js 137(+135 -2)
diff --git a/ui/src/app/api/rule-chain.service.js b/ui/src/app/api/rule-chain.service.js
index 16afa74..be8e99d 100644
--- a/ui/src/app/api/rule-chain.service.js
+++ b/ui/src/app/api/rule-chain.service.js
@@ -17,7 +17,9 @@ export default angular.module('thingsboard.api.ruleChain', [])
.factory('ruleChainService', RuleChainService).name;
/*@ngInject*/
-function RuleChainService($http, $q) {
+function RuleChainService($http, $q, $filter, types) {
+
+ var ruleNodeTypes = null;
var service = {
getSystemRuleChains: getSystemRuleChains,
@@ -27,7 +29,11 @@ function RuleChainService($http, $q) {
saveRuleChain: saveRuleChain,
deleteRuleChain: deleteRuleChain,
getRuleChainMetaData: getRuleChainMetaData,
- saveRuleChainMetaData: saveRuleChainMetaData
+ saveRuleChainMetaData: saveRuleChainMetaData,
+ getRuleNodeTypes: getRuleNodeTypes,
+ getRuleNodeComponentType: getRuleNodeComponentType,
+ getRuleNodeSupportedLinks: getRuleNodeSupportedLinks,
+ resolveTargetRuleChains: resolveTargetRuleChains
};
return service;
@@ -147,4 +153,131 @@ function RuleChainService($http, $q) {
return deferred.promise;
}
+ function getRuleNodeSupportedLinks(nodeType) { //eslint-disable-line
+ //TODO:
+ var deferred = $q.defer();
+ var linkLabels = [
+ { name: 'Success', custom: false },
+ { name: 'Fail', custom: false },
+ { name: 'Custom', custom: true },
+ ];
+ deferred.resolve(linkLabels);
+ return deferred.promise;
+ }
+
+ function getRuleNodeTypes() {
+ var deferred = $q.defer();
+ if (ruleNodeTypes) {
+ deferred.resolve(ruleNodeTypes);
+ } else {
+ loadRuleNodeTypes().then(
+ (nodeTypes) => {
+ ruleNodeTypes = nodeTypes;
+ ruleNodeTypes.push(
+ {
+ nodeType: types.ruleNodeType.RULE_CHAIN.value,
+ type: 'Rule chain'
+ }
+ );
+ deferred.resolve(ruleNodeTypes);
+ },
+ () => {
+ deferred.reject();
+ }
+ );
+ }
+ return deferred.promise;
+ }
+
+ function getRuleNodeComponentType(type) {
+ var res = $filter('filter')(ruleNodeTypes, {type: type}, true);
+ if (res && res.length) {
+ return res[0].nodeType;
+ }
+ return null;
+ }
+
+ function resolveTargetRuleChains(ruleChainConnections) {
+ var deferred = $q.defer();
+ if (ruleChainConnections && ruleChainConnections.length) {
+ var tasks = [];
+ for (var i = 0; i < ruleChainConnections.length; i++) {
+ tasks.push(getRuleChain(ruleChainConnections[i].targetRuleChainId.id));
+ }
+ $q.all(tasks).then(
+ (ruleChains) => {
+ var ruleChainsMap = {};
+ for (var i = 0; i < ruleChains.length; i++) {
+ ruleChainsMap[ruleChains[i].id.id] = ruleChains[i];
+ }
+ deferred.resolve(ruleChainsMap);
+ },
+ () => {
+ deferred.reject();
+ }
+ );
+ } else {
+ deferred.resolve({});
+ }
+ return deferred.promise;
+ }
+
+ function loadRuleNodeTypes() {
+ var deferred = $q.defer();
+ deferred.resolve(
+ [
+ {
+ nodeType: types.ruleNodeType.FILTER.value,
+ type: 'Filter'
+ },
+ {
+ nodeType: types.ruleNodeType.FILTER.value,
+ type: 'Switch'
+ },
+ {
+ nodeType: types.ruleNodeType.ENRICHMENT.value,
+ type: 'Self'
+ },
+ {
+ nodeType: types.ruleNodeType.ENRICHMENT.value,
+ type: 'Tenant/Customer'
+ },
+ {
+ nodeType: types.ruleNodeType.ENRICHMENT.value,
+ type: 'Related Entity'
+ },
+ {
+ nodeType: types.ruleNodeType.ENRICHMENT.value,
+ type: 'Last Telemetry'
+ },
+ {
+ nodeType: types.ruleNodeType.TRANSFORMATION.value,
+ type: 'Modify'
+ },
+ {
+ nodeType: types.ruleNodeType.TRANSFORMATION.value,
+ type: 'New/Update'
+ },
+ {
+ nodeType: types.ruleNodeType.ACTION.value,
+ type: 'Telemetry'
+ },
+ {
+ nodeType: types.ruleNodeType.ACTION.value,
+ type: 'RPC call'
+ },
+ {
+ nodeType: types.ruleNodeType.ACTION.value,
+ type: 'Send email'
+ },
+ {
+ nodeType: types.ruleNodeType.ACTION.value,
+ type: 'Alarm'
+ }
+ ]
+ );
+ return deferred.promise;
+ }
+
+
}
ui/src/app/app.js 3(+3 -0)
diff --git a/ui/src/app/app.js b/ui/src/app/app.js
index 138b230..3131013 100644
--- a/ui/src/app/app.js
+++ b/ui/src/app/app.js
@@ -49,6 +49,7 @@ import 'material-ui';
import 'react-schema-form';
import react from 'ngreact';
import '@flowjs/ng-flow/dist/ng-flow-standalone.min';
+import 'ngFlowchart/dist/ngFlowchart';
import thingsboardLocales from './locale/locale.constant';
import thingsboardLogin from './login';
@@ -86,6 +87,7 @@ import 'mdPickers/dist/mdPickers.min.css';
import 'angular-hotkeys/build/hotkeys.min.css';
import 'angular-carousel/dist/angular-carousel.min.css';
import 'angular-material-expansion-panel/dist/md-expansion-panel.min.css';
+import 'ngFlowchart/dist/flowchart.css';
import '../scss/main.scss';
import AppConfig from './app.config';
@@ -113,6 +115,7 @@ angular.module('thingsboard', [
'ngclipboard',
react.name,
'flow',
+ 'flowchart',
thingsboardLocales,
thingsboardLogin,
thingsboardDialogs,
ui/src/app/common/types.constant.js 38(+38 -0)
diff --git a/ui/src/app/common/types.constant.js b/ui/src/app/common/types.constant.js
index 77c21e1..b134bf0 100644
--- a/ui/src/app/common/types.constant.js
+++ b/ui/src/app/common/types.constant.js
@@ -457,6 +457,44 @@ export default angular.module('thingsboard.types', [])
clientSide: false
}
},
+ ruleNodeType: {
+ FILTER: {
+ value: "FILTER",
+ name: "rulenode.type-filter",
+ nodeClass: "tb-filter-type",
+ icon: "filter_list"
+ },
+ ENRICHMENT: {
+ value: "ENRICHMENT",
+ name: "rulenode.type-enrichment",
+ nodeClass: "tb-enrichment-type",
+ icon: "playlist_add"
+ },
+ TRANSFORMATION: {
+ value: "TRANSFORMATION",
+ name: "rulenode.type-transformation",
+ nodeClass: "tb-transformation-type",
+ icon: "transform"
+ },
+ ACTION: {
+ value: "ACTION",
+ name: "rulenode.type-action",
+ nodeClass: "tb-action-type",
+ icon: "flash_on"
+ },
+ RULE_CHAIN: {
+ value: "RULE_CHAIN",
+ name: "rulenode.type-rule-chain",
+ nodeClass: "tb-rule-chain-type",
+ icon: "settings_ethernet"
+ },
+ INPUT: {
+ value: "INPUT",
+ nodeClass: "tb-input-type",
+ icon: "input",
+ special: true
+ }
+ },
valueType: {
string: {
value: "string",
diff --git a/ui/src/app/entity/entity-autocomplete.directive.js b/ui/src/app/entity/entity-autocomplete.directive.js
index c2053c0..2dfc3be 100644
--- a/ui/src/app/entity/entity-autocomplete.directive.js
+++ b/ui/src/app/entity/entity-autocomplete.directive.js
@@ -143,6 +143,12 @@ export default function EntityAutocomplete($compile, $templateCache, $q, $filter
scope.noEntitiesMatchingText = 'plugin.no-plugins-matching';
scope.entityRequiredText = 'plugin.plugin-required';
break;
+ case types.entityType.rulechain:
+ scope.selectEntityText = 'rulechain.select-rulechain';
+ scope.entityText = 'rulechain.rulechain';
+ scope.noEntitiesMatchingText = 'rulechain.no-rulechains-matching';
+ scope.entityRequiredText = 'rulechain.rulechain-required';
+ break;
case types.entityType.tenant:
scope.selectEntityText = 'tenant.select-tenant';
scope.entityText = 'tenant.tenant';
ui/src/app/locale/locale.constant.js 20(+20 -0)
diff --git a/ui/src/app/locale/locale.constant.js b/ui/src/app/locale/locale.constant.js
index ea576ec..4c392ca 100644
--- a/ui/src/app/locale/locale.constant.js
+++ b/ui/src/app/locale/locale.constant.js
@@ -1169,6 +1169,26 @@ export default angular.module('thingsboard.locale', [])
"rulechain-required": "Rule chain is required",
"management": "Rules management"
},
+ "rulenode": {
+ "add": "Add rule node",
+ "name": "Name",
+ "name-required": "Name is required.",
+ "type": "Type",
+ "description": "Description",
+ "delete": "Delete rule node",
+ "rulenode-details": "Rule node details",
+ "link-details": "Rule node link details",
+ "add-link": "Add link",
+ "link-label": "Link label",
+ "link-label-required": "Link label is required.",
+ "custom-link-label": "Custom link label",
+ "custom-link-label-required": "Custom link label is required.",
+ "type-filter": "Filter",
+ "type-enrichment": "Enrichment",
+ "type-transformation": "Transformation",
+ "type-action": "Action",
+ "type-rule-chain": "Rule Chain"
+ },
"rule-plugin": {
"management": "Rules and plugins management"
},
ui/src/app/rulechain/add-link.tpl.html 48(+48 -0)
diff --git a/ui/src/app/rulechain/add-link.tpl.html b/ui/src/app/rulechain/add-link.tpl.html
new file mode 100644
index 0000000..42c0777
--- /dev/null
+++ b/ui/src/app/rulechain/add-link.tpl.html
@@ -0,0 +1,48 @@
+<!--
+
+ Copyright © 2016-2018 The Thingsboard Authors
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+
+-->
+<md-dialog aria-label="{{ 'rulenode.add-link' | translate }}" tb-help="'rulechains'" help-container-id="help-container">
+ <form name="theForm" ng-submit="vm.add()">
+ <md-toolbar>
+ <div class="md-toolbar-tools">
+ <h2 translate>rulenode.add-link</h2>
+ <span flex></span>
+ <div id="help-container"></div>
+ <md-button class="md-icon-button" ng-click="vm.cancel()">
+ <ng-md-icon icon="close" aria-label="{{ 'dialog.close' | translate }}"></ng-md-icon>
+ </md-button>
+ </div>
+ </md-toolbar>
+ <md-progress-linear class="md-warn" md-mode="indeterminate" ng-disabled="!$root.loading" ng-show="$root.loading"></md-progress-linear>
+ <span style="min-height: 5px;" flex="" ng-show="!$root.loading"></span>
+ <md-dialog-content>
+ <div class="md-dialog-content">
+ <tb-rule-node-link link="vm.link" labels="vm.labels" is-edit="true" the-form="theForm"></tb-rule-node-link>
+ </div>
+ </md-dialog-content>
+ <md-dialog-actions layout="row">
+ <span flex></span>
+ <md-button ng-disabled="$root.loading || theForm.$invalid || !theForm.$dirty" type="submit"
+ class="md-raised md-primary">
+ {{ 'action.add' | translate }}
+ </md-button>
+ <md-button ng-disabled="$root.loading" ng-click="vm.cancel()" style="margin-right:20px;">{{ 'action.cancel' |
+ translate }}
+ </md-button>
+ </md-dialog-actions>
+ </form>
+</md-dialog>
ui/src/app/rulechain/add-rulenode.tpl.html 48(+48 -0)
diff --git a/ui/src/app/rulechain/add-rulenode.tpl.html b/ui/src/app/rulechain/add-rulenode.tpl.html
new file mode 100644
index 0000000..c36b43b
--- /dev/null
+++ b/ui/src/app/rulechain/add-rulenode.tpl.html
@@ -0,0 +1,48 @@
+<!--
+
+ Copyright © 2016-2018 The Thingsboard Authors
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+
+-->
+<md-dialog aria-label="{{ 'rulenode.add' | translate }}" tb-help="'rulechains'" help-container-id="help-container" style="min-width: 650px;">
+ <form name="theForm" ng-submit="vm.add()">
+ <md-toolbar>
+ <div class="md-toolbar-tools">
+ <h2 translate>rulenode.add</h2>
+ <span flex></span>
+ <div id="help-container"></div>
+ <md-button class="md-icon-button" ng-click="vm.cancel()">
+ <ng-md-icon icon="close" aria-label="{{ 'dialog.close' | translate }}"></ng-md-icon>
+ </md-button>
+ </div>
+ </md-toolbar>
+ <md-progress-linear class="md-warn" md-mode="indeterminate" ng-disabled="!$root.loading" ng-show="$root.loading"></md-progress-linear>
+ <span style="min-height: 5px;" flex="" ng-show="!$root.loading"></span>
+ <md-dialog-content>
+ <div class="md-dialog-content">
+ <tb-rule-node rule-node="vm.ruleNode" rule-chain-id="vm.ruleChainId" is-edit="true" the-form="theForm"></tb-rule-node>
+ </div>
+ </md-dialog-content>
+ <md-dialog-actions layout="row">
+ <span flex></span>
+ <md-button ng-disabled="$root.loading || theForm.$invalid || !theForm.$dirty" type="submit"
+ class="md-raised md-primary">
+ {{ 'action.add' | translate }}
+ </md-button>
+ <md-button ng-disabled="$root.loading" ng-click="vm.cancel()" style="margin-right:20px;">{{ 'action.cancel' |
+ translate }}
+ </md-button>
+ </md-dialog-actions>
+ </form>
+</md-dialog>
ui/src/app/rulechain/index.js 10(+9 -1)
diff --git a/ui/src/app/rulechain/index.js b/ui/src/app/rulechain/index.js
index faecc03..7306762 100644
--- a/ui/src/app/rulechain/index.js
+++ b/ui/src/app/rulechain/index.js
@@ -15,11 +15,19 @@
*/
import RuleChainRoutes from './rulechain.routes';
-import RuleChainController from './rulechain.controller';
+import RuleChainsController from './rulechains.controller';
+import {RuleChainController, AddRuleNodeController, AddRuleNodeLinkController} from './rulechain.controller';
import RuleChainDirective from './rulechain.directive';
+import RuleNodeDirective from './rulenode.directive';
+import LinkDirective from './link.directive';
export default angular.module('thingsboard.ruleChain', [])
.config(RuleChainRoutes)
+ .controller('RuleChainsController', RuleChainsController)
.controller('RuleChainController', RuleChainController)
+ .controller('AddRuleNodeController', AddRuleNodeController)
+ .controller('AddRuleNodeLinkController', AddRuleNodeLinkController)
.directive('tbRuleChain', RuleChainDirective)
+ .directive('tbRuleNode', RuleNodeDirective)
+ .directive('tbRuleNodeLink', LinkDirective)
.name;
ui/src/app/rulechain/link.directive.js 71(+71 -0)
diff --git a/ui/src/app/rulechain/link.directive.js b/ui/src/app/rulechain/link.directive.js
new file mode 100644
index 0000000..b3565a3
--- /dev/null
+++ b/ui/src/app/rulechain/link.directive.js
@@ -0,0 +1,71 @@
+/*
+ * Copyright © 2016-2018 The Thingsboard Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+/* eslint-disable import/no-unresolved, import/default */
+
+import linkFieldsetTemplate from './link-fieldset.tpl.html';
+
+/* eslint-enable import/no-unresolved, import/default */
+
+/*@ngInject*/
+export default function LinkDirective($compile, $templateCache, $filter) {
+ var linker = function (scope, element) {
+ var template = $templateCache.get(linkFieldsetTemplate);
+ element.html(template);
+
+ scope.selectedLabel = null;
+
+ scope.$watch('link', function() {
+ scope.selectedLabel = null;
+ if (scope.link && scope.labels) {
+ if (scope.link.label) {
+ var result = $filter('filter')(scope.labels, {name: scope.link.label});
+ if (result && result.length) {
+ scope.selectedLabel = result[0];
+ } else {
+ result = $filter('filter')(scope.labels, {custom: true});
+ if (result && result.length && result[0].custom) {
+ scope.selectedLabel = result[0];
+ }
+ }
+ }
+ }
+ });
+
+ scope.selectedLabelChanged = function() {
+ if (scope.link && scope.selectedLabel) {
+ if (!scope.selectedLabel.custom) {
+ scope.link.label = scope.selectedLabel.name;
+ } else {
+ scope.link.label = "";
+ }
+ }
+ };
+
+ $compile(element.contents())(scope);
+ }
+ return {
+ restrict: "E",
+ link: linker,
+ scope: {
+ link: '=',
+ labels: '=',
+ isEdit: '=',
+ isReadOnly: '=',
+ theForm: '='
+ }
+ };
+}
ui/src/app/rulechain/link-fieldset.tpl.html 39(+39 -0)
diff --git a/ui/src/app/rulechain/link-fieldset.tpl.html b/ui/src/app/rulechain/link-fieldset.tpl.html
new file mode 100644
index 0000000..13ec6c3
--- /dev/null
+++ b/ui/src/app/rulechain/link-fieldset.tpl.html
@@ -0,0 +1,39 @@
+<!--
+
+ Copyright © 2016-2018 The Thingsboard Authors
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+
+-->
+<md-content class="md-padding tb-link" layout="column">
+ <fieldset ng-disabled="$root.loading || !isEdit || isReadOnly">
+ <md-input-container class="md-block">
+ <label translate>rulenode.link-label</label>
+ <md-select ng-model="selectedLabel" ng-change="selectedLabelChanged()">
+ <md-option ng-repeat="label in labels" ng-value="label">
+ {{label.name}}
+ </md-option>
+ </md-select>
+ <div ng-messages="theForm.linkLabel.$error">
+ <div translate ng-message="required">rulenode.link-label-required</div>
+ </div>
+ </md-input-container>
+ <md-input-container ng-if="selectedLabel.custom" class="md-block">
+ <label translate>rulenode.link-label</label>
+ <input required name="customLinkLabel" ng-model="link.label">
+ <div ng-messages="theForm.customLinkLabel.$error">
+ <div translate ng-message="required">rulenode.custom-link-label-required</div>
+ </div>
+ </md-input-container>
+ </fieldset>
+</md-content>
ui/src/app/rulechain/rulechain.controller.js 638(+530 -108)
diff --git a/ui/src/app/rulechain/rulechain.controller.js b/ui/src/app/rulechain/rulechain.controller.js
index 475ef4e..c854cfa 100644
--- a/ui/src/app/rulechain/rulechain.controller.js
+++ b/ui/src/app/rulechain/rulechain.controller.js
@@ -13,160 +13,582 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
+
+import './rulechain.scss';
+
/* eslint-disable import/no-unresolved, import/default */
-import addRuleChainTemplate from './add-rulechain.tpl.html';
-import ruleChainCard from './rulechain-card.tpl.html';
+import addRuleNodeTemplate from './add-rulenode.tpl.html';
+import addRuleNodeLinkTemplate from './add-link.tpl.html';
/* eslint-enable import/no-unresolved, import/default */
+
+const deleteKeyCode = 46;
+const ctrlKeyCode = 17;
+const aKeyCode = 65;
+const escKeyCode = 27;
+
/*@ngInject*/
-export default function RuleChainController(ruleChainService, userService, importExport, $state, $stateParams, $filter, $translate, types) {
-
- var ruleChainActionsList = [
- {
- onAction: function ($event, item) {
- exportRuleChain($event, item);
- },
- name: function() { $translate.instant('action.export') },
- details: function() { return $translate.instant('rulechain.export') },
- icon: "file_download"
- },
- {
- onAction: function ($event, item) {
- vm.grid.deleteItem($event, item);
- },
- name: function() { return $translate.instant('action.delete') },
- details: function() { return $translate.instant('rulechain.delete') },
- icon: "delete",
- isEnabled: isRuleChainEditable
- }
- ];
-
- var ruleChainAddItemActionsList = [
- {
- onAction: function ($event) {
- vm.grid.addItem($event);
- },
- name: function() { return $translate.instant('action.create') },
- details: function() { return $translate.instant('rulechain.create-new-rulechain') },
- icon: "insert_drive_file"
- },
- {
- onAction: function ($event) {
- importExport.importRuleChain($event).then(
- function() {
- vm.grid.refreshList();
- }
- );
- },
- name: function() { return $translate.instant('action.import') },
- details: function() { return $translate.instant('rulechain.import') },
- icon: "file_upload"
- }
- ];
+export function RuleChainController($stateParams, $scope, $q, $mdUtil, $mdExpansionPanel, $document, $mdDialog, $filter, types, ruleChainService, Modelfactory, flowchartConstants, ruleChain, ruleChainMetaData) {
var vm = this;
+ vm.$mdExpansionPanel = $mdExpansionPanel;
vm.types = types;
- vm.ruleChainGridConfig = {
+ vm.editingRuleNode = null;
+ vm.isEditingRuleNode = false;
+
+ vm.editingRuleNodeLink = null;
+ vm.isEditingRuleNodeLink = false;
- refreshParamsFunc: null,
+ vm.ruleChain = ruleChain;
+ vm.ruleChainMetaData = ruleChainMetaData;
- deleteItemTitleFunc: deleteRuleChainTitle,
- deleteItemContentFunc: deleteRuleChainText,
- deleteItemsTitleFunc: deleteRuleChainsTitle,
- deleteItemsActionTitleFunc: deleteRuleChainsActionTitle,
- deleteItemsContentFunc: deleteRuleChainsText,
+ vm.canvasControl = {};
- fetchItemsFunc: fetchRuleChains,
- saveItemFunc: saveRuleChain,
- deleteItemFunc: deleteRuleChain,
+ vm.ruleChainModel = {
+ nodes: [],
+ edges: []
+ };
+
+ vm.ruleNodeTypesModel = {};
+ for (var type in types.ruleNodeType) {
+ if (!types.ruleNodeType[type].special) {
+ vm.ruleNodeTypesModel[type] = {
+ model: {
+ nodes: [],
+ edges: []
+ },
+ selectedObjects: []
+ };
+ }
+ }
- getItemTitleFunc: getRuleChainTitle,
- itemCardTemplateUrl: ruleChainCard,
- parentCtl: vm,
+ vm.selectedObjects = [];
- actionsList: ruleChainActionsList,
- addItemActions: ruleChainAddItemActionsList,
+ vm.modelservice = Modelfactory(vm.ruleChainModel, vm.selectedObjects);
- onGridInited: gridInited,
+ vm.ctrlDown = false;
- addItemTemplateUrl: addRuleChainTemplate,
+ vm.saveRuleChain = saveRuleChain;
+ vm.revertRuleChain = revertRuleChain;
- addItemText: function() { return $translate.instant('rulechain.add-rulechain-text') },
- noItemsText: function() { return $translate.instant('rulechain.no-rulechains-text') },
- itemDetailsText: function() { return $translate.instant('rulechain.rulechain-details') },
- isSelectionEnabled: isRuleChainEditable,
- isDetailsReadOnly: function(ruleChain) {
- return !isRuleChainEditable(ruleChain);
+ vm.keyDown = function (evt) {
+ if (evt.keyCode === ctrlKeyCode) {
+ vm.ctrlDown = true;
+ evt.stopPropagation();
+ evt.preventDefault();
}
};
- if (angular.isDefined($stateParams.items) && $stateParams.items !== null) {
- vm.ruleChainGridConfig.items = $stateParams.items;
- }
+ vm.keyUp = function (evt) {
- if (angular.isDefined($stateParams.topIndex) && $stateParams.topIndex > 0) {
- vm.ruleChainGridConfig.topIndex = $stateParams.topIndex;
- }
+ if (evt.keyCode === deleteKeyCode) {
+ vm.modelservice.deleteSelected();
+ }
- vm.isRuleChainEditable = isRuleChainEditable;
+ if (evt.keyCode == aKeyCode && vm.ctrlDown) {
+ vm.modelservice.selectAll();
+ }
- vm.exportRuleChain = exportRuleChain;
+ if (evt.keyCode == escKeyCode) {
+ vm.modelservice.deselectAll();
+ }
- function deleteRuleChainTitle(ruleChain) {
- return $translate.instant('rulechain.delete-rulechain-title', {ruleChainName: ruleChain.name});
- }
+ if (evt.keyCode === ctrlKeyCode) {
+ vm.ctrlDown = false;
+ evt.stopPropagation();
+ evt.preventDefault();
+ }
+ };
- function deleteRuleChainText() {
- return $translate.instant('rulechain.delete-rulechain-text');
- }
+ vm.onEditRuleNodeClosed = function() {
+ vm.editingRuleNode = null;
+ };
+
+ vm.onEditRuleNodeLinkClosed = function() {
+ vm.editingRuleNodeLink = null;
+ };
+
+ vm.saveRuleNode = function(theForm) {
+ theForm.$setPristine();
+ vm.isEditingRuleNode = false;
+ vm.ruleChainModel.nodes[vm.editingRuleNodeIndex] = vm.editingRuleNode;
+ vm.editingRuleNode = angular.copy(vm.editingRuleNode);
+ };
+
+ vm.saveRuleNodeLink = function(theForm) {
+ theForm.$setPristine();
+ vm.isEditingRuleNodeLink = false;
+ vm.ruleChainModel.edges[vm.editingRuleNodeLinkIndex] = vm.editingRuleNodeLink;
+ vm.editingRuleNodeLink = angular.copy(vm.editingRuleNodeLink);
+ };
+
+ vm.onRevertRuleNodeEdit = function(theForm) {
+ theForm.$setPristine();
+ var node = vm.ruleChainModel.nodes[vm.editingRuleNodeIndex];
+ vm.editingRuleNode = angular.copy(node);
+ };
+
+ vm.onRevertRuleNodeLinkEdit = function(theForm) {
+ theForm.$setPristine();
+ var edge = vm.ruleChainModel.edges[vm.editingRuleNodeLinkIndex];
+ vm.editingRuleNodeLink = angular.copy(edge);
+ };
+
+ vm.editCallbacks = {
+ edgeDoubleClick: function (event, edge) {
+ var sourceNode = vm.modelservice.nodes.getNodeByConnectorId(edge.source);
+ if (sourceNode.nodeType != types.ruleNodeType.INPUT.value) {
+ ruleChainService.getRuleNodeSupportedLinks(sourceNode.type).then(
+ (labels) => {
+ vm.isEditingRuleNode = false;
+ vm.editingRuleNode = null;
+ vm.editingRuleNodeLinkLabels = labels;
+ vm.isEditingRuleNodeLink = true;
+ vm.editingRuleNodeLinkIndex = vm.ruleChainModel.edges.indexOf(edge);
+ vm.editingRuleNodeLink = angular.copy(edge);
+ }
+ );
+ }
+ },
+ nodeCallbacks: {
+ 'doubleClick': function (event, node) {
+ if (node.nodeType != types.ruleNodeType.INPUT.value) {
+ vm.isEditingRuleNodeLink = false;
+ vm.editingRuleNodeLink = null;
+ vm.isEditingRuleNode = true;
+ vm.editingRuleNodeIndex = vm.ruleChainModel.nodes.indexOf(node);
+ vm.editingRuleNode = angular.copy(node);
+ }
+ }
+ },
+ isValidEdge: function (source, destination) {
+ return source.type === flowchartConstants.rightConnectorType && destination.type === flowchartConstants.leftConnectorType;
+ },
+ createEdge: function (event, edge) {
+ var deferred = $q.defer();
+ var sourceNode = vm.modelservice.nodes.getNodeByConnectorId(edge.source);
+ if (sourceNode.nodeType == types.ruleNodeType.INPUT.value) {
+ var destNode = vm.modelservice.nodes.getNodeByConnectorId(edge.destination);
+ if (destNode.nodeType == types.ruleNodeType.RULE_CHAIN.value) {
+ deferred.reject();
+ } else {
+ var res = $filter('filter')(vm.ruleChainModel.edges, {source: vm.inputConnectorId});
+ if (res && res.length) {
+ vm.modelservice.edges.delete(res[0]);
+ }
+ deferred.resolve(edge);
+ }
+ } else {
+ ruleChainService.getRuleNodeSupportedLinks(sourceNode.type).then(
+ (labels) => {
+ addRuleNodeLink(event, edge, labels).then(
+ (link) => {
+ deferred.resolve(link);
+ },
+ () => {
+ deferred.reject();
+ }
+ );
+ },
+ () => {
+ deferred.reject();
+ }
+ );
+ }
+ return deferred.promise;
+ },
+ dropNode: function (event, node) {
+ addRuleNode(event, node);
+ }
+ };
- function deleteRuleChainsTitle(selectedCount) {
- return $translate.instant('rulechain.delete-rulechains-title', {count: selectedCount}, 'messageformat');
+ loadRuleChainLibrary();
+
+ function loadRuleChainLibrary() {
+ ruleChainService.getRuleNodeTypes().then(
+ (ruleNodeTypes) => {
+ for (var i=0;i<ruleNodeTypes.length;i++) {
+ var ruleNodeType = ruleNodeTypes[i];
+ var nodeType = ruleNodeType.nodeType;
+ var model = vm.ruleNodeTypesModel[nodeType].model;
+ var node = {
+ id: model.nodes.length,
+ nodeType: nodeType,
+ type: ruleNodeType.type,
+ name: '',
+ nodeClass: vm.types.ruleNodeType[nodeType].nodeClass,
+ icon: vm.types.ruleNodeType[nodeType].icon,
+ x: 30,
+ y: 10+50*model.nodes.length,
+ connectors: []
+ };
+ if (nodeType == types.ruleNodeType.RULE_CHAIN.value) {
+ node.connectors.push(
+ {
+ type: flowchartConstants.leftConnectorType,
+ id: model.nodes.length
+ }
+ );
+ } else {
+ node.connectors.push(
+ {
+ type: flowchartConstants.leftConnectorType,
+ id: model.nodes.length*2
+ }
+ );
+ node.connectors.push(
+ {
+ type: flowchartConstants.rightConnectorType,
+ id: model.nodes.length*2+1
+ }
+ );
+ }
+ model.nodes.push(node);
+ }
+ prepareRuleChain();
+ }
+ );
}
- function deleteRuleChainsActionTitle(selectedCount) {
- return $translate.instant('rulechain.delete-rulechains-action-title', {count: selectedCount}, 'messageformat');
+ function prepareRuleChain() {
+
+ if (vm.ruleChainWatch) {
+ vm.ruleChainWatch();
+ vm.ruleChainWatch = null;
+ }
+
+ vm.nextNodeID = 1;
+ vm.nextConnectorID = 1;
+
+ vm.selectedObjects.length = 0;
+ vm.ruleChainModel.nodes.length = 0;
+ vm.ruleChainModel.edges.length = 0;
+
+ vm.inputConnectorId = vm.nextConnectorID++;
+
+ vm.ruleChainModel.nodes.push(
+ {
+ id: vm.nextNodeID++,
+ type: "Input",
+ name: "",
+ nodeType: types.ruleNodeType.INPUT.value,
+ nodeClass: types.ruleNodeType.INPUT.nodeClass,
+ icon: types.ruleNodeType.INPUT.icon,
+ readonly: true,
+ x: 50,
+ y: 150,
+ connectors: [
+ {
+ type: flowchartConstants.rightConnectorType,
+ id: vm.inputConnectorId
+ },
+ ]
+
+ }
+ );
+ ruleChainService.resolveTargetRuleChains(vm.ruleChainMetaData.ruleChainConnections)
+ .then((ruleChainsMap) => {
+ createRuleChainModel(ruleChainsMap);
+ }
+ );
}
- function deleteRuleChainsText() {
- return $translate.instant('rulechain.delete-rulechains-text');
+ function createRuleChainModel(ruleChainsMap) {
+ var nodes = [];
+ for (var i=0;i<vm.ruleChainMetaData.nodes.length;i++) {
+ var ruleNode = vm.ruleChainMetaData.nodes[i];
+ var nodeType = ruleChainService.getRuleNodeComponentType(ruleNode.type);
+ if (nodeType) {
+ var node = {
+ id: vm.nextNodeID++,
+ ruleNodeId: ruleNode.id,
+ additionalInfo: ruleNode.additionalInfo,
+ configuration: ruleNode.configuration,
+ x: ruleNode.additionalInfo.layoutX,
+ y: ruleNode.additionalInfo.layoutY,
+ type: ruleNode.type,
+ name: ruleNode.name,
+ nodeType: nodeType,
+ nodeClass: vm.types.ruleNodeType[nodeType].nodeClass,
+ icon: vm.types.ruleNodeType[nodeType].icon,
+ connectors: [
+ {
+ type: flowchartConstants.leftConnectorType,
+ id: vm.nextConnectorID++
+ },
+ {
+ type: flowchartConstants.rightConnectorType,
+ id: vm.nextConnectorID++
+ }
+ ]
+ };
+ nodes.push(node);
+ vm.ruleChainModel.nodes.push(node);
+ }
+ }
+
+ if (vm.ruleChainMetaData.firstNodeIndex > -1) {
+ var destNode = nodes[vm.ruleChainMetaData.firstNodeIndex];
+ if (destNode) {
+ var connectors = vm.modelservice.nodes.getConnectorsByType(destNode, flowchartConstants.leftConnectorType);
+ if (connectors && connectors.length) {
+ var edge = {
+ source: vm.inputConnectorId,
+ destination: connectors[0].id
+ };
+ vm.ruleChainModel.edges.push(edge);
+ }
+ }
+ }
+
+ if (vm.ruleChainMetaData.connections) {
+ for (i = 0; i < vm.ruleChainMetaData.connections.length; i++) {
+ var connection = vm.ruleChainMetaData.connections[0];
+ var sourceNode = nodes[connection.fromIndex];
+ destNode = nodes[connection.toIndex];
+ if (sourceNode && destNode) {
+ var sourceConnectors = vm.modelservice.nodes.getConnectorsByType(sourceNode, flowchartConstants.rightConnectorType);
+ var destConnectors = vm.modelservice.nodes.getConnectorsByType(destNode, flowchartConstants.leftConnectorType);
+ if (sourceConnectors && sourceConnectors.length && destConnectors && destConnectors.length) {
+ edge = {
+ source: sourceConnectors[0].id,
+ destination: destConnectors[0].id,
+ label: connection.type
+ };
+ vm.ruleChainModel.edges.push(edge);
+ }
+ }
+ }
+ }
+
+ if (vm.ruleChainMetaData.ruleChainConnections) {
+ var ruleChainNodesMap = {};
+ for (i = 0; i < vm.ruleChainMetaData.ruleChainConnections.length; i++) {
+ var ruleChainConnection = vm.ruleChainMetaData.ruleChainConnections[i];
+ var ruleChain = ruleChainsMap[ruleChainConnection.targetRuleChainId.id];
+ if (ruleChainConnection.additionalInfo && ruleChainConnection.additionalInfo.ruleChainNodeId) {
+ var ruleChainNode = ruleChainNodesMap[ruleChainConnection.additionalInfo.ruleChainNodeId];
+ if (!ruleChainNode) {
+ ruleChainNode = {
+ id: vm.nextNodeID++,
+ additionalInfo: ruleChainConnection.additionalInfo,
+ targetRuleChainId: ruleChainConnection.targetRuleChainId.id,
+ x: ruleChainConnection.additionalInfo.layoutX,
+ y: ruleChainConnection.additionalInfo.layoutY,
+ type: 'Rule chain',
+ name: ruleChain.name,
+ nodeType: vm.types.ruleNodeType.RULE_CHAIN.value,
+ nodeClass: vm.types.ruleNodeType.RULE_CHAIN.nodeClass,
+ icon: vm.types.ruleNodeType.RULE_CHAIN.icon,
+ connectors: [
+ {
+ type: flowchartConstants.leftConnectorType,
+ id: vm.nextConnectorID++
+ }
+ ]
+ };
+ ruleChainNodesMap[ruleChainConnection.additionalInfo.ruleChainNodeId] = ruleChainNode;
+ vm.ruleChainModel.nodes.push(ruleChainNode);
+ }
+ sourceNode = nodes[ruleChainConnection.fromIndex];
+ if (sourceNode) {
+ connectors = vm.modelservice.nodes.getConnectorsByType(sourceNode, flowchartConstants.rightConnectorType);
+ if (connectors && connectors.length) {
+ var ruleChainEdge = {
+ source: connectors[0].id,
+ destination: ruleChainNode.connectors[0].id,
+ label: ruleChainConnection.type
+ };
+ vm.ruleChainModel.edges.push(ruleChainEdge);
+ }
+ }
+ }
+ }
+ }
+
+ vm.canvasControl.adjustCanvasSize();
+
+ vm.isDirty = false;
+
+ $mdUtil.nextTick(() => {
+ vm.ruleChainWatch = $scope.$watch('vm.ruleChainModel',
+ function (newVal, oldVal) {
+ if (!vm.isDirty && !angular.equals(newVal, oldVal)) {
+ vm.isDirty = true;
+ }
+ }, true
+ );
+ });
}
- function gridInited(grid) {
- vm.grid = grid;
+ function saveRuleChain() {
+ var ruleChainMetaData = {
+ ruleChainId: vm.ruleChain.id,
+ nodes: [],
+ connections: [],
+ ruleChainConnections: []
+ };
+
+ var nodes = [];
+
+ for (var i=0;i<vm.ruleChainModel.nodes.length;i++) {
+ var node = vm.ruleChainModel.nodes[i];
+ if (node.nodeType != types.ruleNodeType.INPUT.value && node.nodeType != types.ruleNodeType.RULE_CHAIN.value) {
+ var ruleNode = {};
+ if (node.ruleNodeId) {
+ ruleNode.id = node.ruleNodeId;
+ }
+ ruleNode.type = node.type;
+ ruleNode.name = node.name;
+ ruleNode.configuration = node.configuration;
+ ruleNode.additionalInfo = node.additionalInfo;
+ if (!ruleNode.additionalInfo) {
+ ruleNode.additionalInfo = {};
+ }
+ ruleNode.additionalInfo.layoutX = node.x;
+ ruleNode.additionalInfo.layoutY = node.y;
+ ruleChainMetaData.nodes.push(ruleNode);
+ nodes.push(node);
+ }
+ }
+ var res = $filter('filter')(vm.ruleChainModel.edges, {source: vm.inputConnectorId});
+ if (res && res.length) {
+ var firstNodeEdge = res[0];
+ var firstNode = vm.modelservice.nodes.getNodeByConnectorId(firstNodeEdge.destination);
+ ruleChainMetaData.firstNodeIndex = nodes.indexOf(firstNode);
+ }
+ for (i=0;i<vm.ruleChainModel.edges.length;i++) {
+ var edge = vm.ruleChainModel.edges[i];
+ var sourceNode = vm.modelservice.nodes.getNodeByConnectorId(edge.source);
+ var destNode = vm.modelservice.nodes.getNodeByConnectorId(edge.destination);
+ if (sourceNode.nodeType != types.ruleNodeType.INPUT.value) {
+ var fromIndex = nodes.indexOf(sourceNode);
+ if (destNode.nodeType == types.ruleNodeType.RULE_CHAIN.value) {
+ var ruleChainConnection = {
+ fromIndex: fromIndex,
+ targetRuleChainId: {entityType: vm.types.entityType.rulechain, id: destNode.targetRuleChainId},
+ additionalInfo: destNode.additionalInfo,
+ type: edge.label
+ };
+ if (!ruleChainConnection.additionalInfo) {
+ ruleChainConnection.additionalInfo = {};
+ }
+ ruleChainConnection.additionalInfo.layoutX = destNode.x;
+ ruleChainConnection.additionalInfo.layoutY = destNode.y;
+ ruleChainConnection.additionalInfo.ruleChainNodeId = destNode.id;
+ ruleChainMetaData.ruleChainConnections.push(ruleChainConnection);
+ } else {
+ var toIndex = nodes.indexOf(destNode);
+ var nodeConnection = {
+ fromIndex: fromIndex,
+ toIndex: toIndex,
+ type: edge.label
+ };
+ ruleChainMetaData.connections.push(nodeConnection);
+ }
+ }
+ }
+ ruleChainService.saveRuleChainMetaData(ruleChainMetaData).then(
+ (ruleChainMetaData) => {
+ vm.ruleChainMetaData = ruleChainMetaData;
+ prepareRuleChain();
+ }
+ );
}
- function fetchRuleChains(pageLink) {
- return ruleChainService.getRuleChains(pageLink);
+ function revertRuleChain() {
+ prepareRuleChain();
}
- function saveRuleChain(ruleChain) {
- return ruleChainService.saveRuleChain(ruleChain);
+ function addRuleNode($event, ruleNode) {
+ $mdDialog.show({
+ controller: 'AddRuleNodeController',
+ controllerAs: 'vm',
+ templateUrl: addRuleNodeTemplate,
+ parent: angular.element($document[0].body),
+ locals: {ruleNode: ruleNode, ruleChainId: vm.ruleChain.id.id},
+ fullscreen: true,
+ targetEvent: $event
+ }).then(function (ruleNode) {
+ ruleNode.id = vm.nextNodeID++;
+ ruleNode.connectors = [];
+ ruleNode.connectors.push(
+ {
+ id: vm.nextConnectorID++,
+ type: flowchartConstants.leftConnectorType
+ }
+ );
+ if (ruleNode.nodeType != types.ruleNodeType.RULE_CHAIN.value) {
+ ruleNode.connectors.push(
+ {
+ id: vm.nextConnectorID++,
+ type: flowchartConstants.rightConnectorType
+ }
+ );
+ }
+ vm.ruleChainModel.nodes.push(ruleNode);
+ }, function () {
+ });
}
- function deleteRuleChain(ruleChainId) {
- return ruleChainService.deleteRuleChain(ruleChainId);
+ function addRuleNodeLink($event, link, labels) {
+ return $mdDialog.show({
+ controller: 'AddRuleNodeLinkController',
+ controllerAs: 'vm',
+ templateUrl: addRuleNodeLinkTemplate,
+ parent: angular.element($document[0].body),
+ locals: {link: link, labels: labels},
+ fullscreen: true,
+ targetEvent: $event
+ });
}
- function getRuleChainTitle(ruleChain) {
- return ruleChain ? ruleChain.name : '';
+}
+
+/*@ngInject*/
+export function AddRuleNodeController($scope, $mdDialog, ruleNode, ruleChainId, helpLinks) {
+
+ var vm = this;
+
+ vm.helpLinks = helpLinks;
+ vm.ruleNode = ruleNode;
+ vm.ruleChainId = ruleChainId;
+
+ vm.add = add;
+ vm.cancel = cancel;
+
+ function cancel() {
+ $mdDialog.cancel();
}
- function isRuleChainEditable(ruleChain) {
- if (userService.getAuthority() === 'TENANT_ADMIN') {
- return ruleChain && ruleChain.tenantId.id != types.id.nullUid;
- } else {
- return userService.getAuthority() === 'SYS_ADMIN';
- }
+ function add() {
+ $scope.theForm.$setPristine();
+ $mdDialog.hide(vm.ruleNode);
}
+}
+
+/*@ngInject*/
+export function AddRuleNodeLinkController($scope, $mdDialog, link, labels, helpLinks) {
- function exportRuleChain($event, ruleChain) {
- $event.stopPropagation();
- importExport.exportRuleChain(ruleChain.id.id);
+ var vm = this;
+
+ vm.helpLinks = helpLinks;
+ vm.link = link;
+ vm.labels = labels;
+
+ vm.add = add;
+ vm.cancel = cancel;
+
+ function cancel() {
+ $mdDialog.cancel();
}
+ function add() {
+ $scope.theForm.$setPristine();
+ $mdDialog.hide(vm.link);
+ }
}
ui/src/app/rulechain/rulechain.routes.js 41(+38 -3)
diff --git a/ui/src/app/rulechain/rulechain.routes.js b/ui/src/app/rulechain/rulechain.routes.js
index e8d6be4..808661a 100644
--- a/ui/src/app/rulechain/rulechain.routes.js
+++ b/ui/src/app/rulechain/rulechain.routes.js
@@ -15,12 +15,16 @@
*/
/* eslint-disable import/no-unresolved, import/default */
+import ruleNodeTemplate from './rulenode.tpl.html';
import ruleChainsTemplate from './rulechains.tpl.html';
+import ruleChainTemplate from './rulechain.tpl.html';
/* eslint-enable import/no-unresolved, import/default */
/*@ngInject*/
-export default function RuleChainRoutes($stateProvider) {
+export default function RuleChainRoutes($stateProvider, NodeTemplatePathProvider) {
+
+ NodeTemplatePathProvider.setTemplatePath(ruleNodeTemplate);
$stateProvider
.state('home.ruleChains', {
@@ -32,7 +36,7 @@ export default function RuleChainRoutes($stateProvider) {
"content@home": {
templateUrl: ruleChainsTemplate,
controllerAs: 'vm',
- controller: 'RuleChainController'
+ controller: 'RuleChainsController'
}
},
data: {
@@ -42,5 +46,36 @@ export default function RuleChainRoutes($stateProvider) {
ncyBreadcrumb: {
label: '{"icon": "settings_ethernet", "label": "rulechain.rulechains"}'
}
- });
+ }).state('home.ruleChains.ruleChain', {
+ url: '/:ruleChainId',
+ reloadOnSearch: false,
+ module: 'private',
+ auth: ['SYS_ADMIN', 'TENANT_ADMIN'],
+ views: {
+ "content@home": {
+ templateUrl: ruleChainTemplate,
+ controller: 'RuleChainController',
+ controllerAs: 'vm'
+ }
+ },
+ resolve: {
+ ruleChain:
+ /*@ngInject*/
+ function($stateParams, ruleChainService) {
+ return ruleChainService.getRuleChain($stateParams.ruleChainId);
+ },
+ ruleChainMetaData:
+ /*@ngInject*/
+ function($stateParams, ruleChainService) {
+ return ruleChainService.getRuleChainMetaData($stateParams.ruleChainId);
+ }
+ },
+ data: {
+ searchEnabled: false,
+ pageTitle: 'rulechain.rulechain'
+ },
+ ncyBreadcrumb: {
+ label: '{"icon": "settings_ethernet", "label": "{{ vm.ruleChain.name }}", "translate": "false"}'
+ }
+ });
}
ui/src/app/rulechain/rulechain.scss 262(+262 -0)
diff --git a/ui/src/app/rulechain/rulechain.scss b/ui/src/app/rulechain/rulechain.scss
new file mode 100644
index 0000000..258e232
--- /dev/null
+++ b/ui/src/app/rulechain/rulechain.scss
@@ -0,0 +1,262 @@
+/**
+ * Copyright © 2016-2018 The Thingsboard Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+.tb-rulechain {
+ .tb-fullscreen-button-style {
+ z-index: 1;
+ }
+ .tb-rulechain-library {
+ width: 250px;
+ min-width: 250px;
+ overflow-y: auto;
+ overflow-x: hidden;
+
+ .tb-rulechain-library-panel-group {
+ .tb-panel-title {
+ -webkit-user-select: none;
+ -moz-user-select: none;
+ -ms-user-select: none;
+ user-select: none;
+ min-width: 180px;
+ }
+ .fc-canvas {
+ background: none;
+ }
+ md-icon.md-expansion-panel-icon {
+ margin-right: 0px;
+ }
+ md-expansion-panel-collapsed, .md-expansion-panel-header-container {
+ background: #e6e6e6;
+ border-color: #909090;
+ position: static;
+ }
+ md-expansion-panel {
+ &.md-open {
+ margin-top: 0;
+ margin-bottom: 0;
+ }
+ }
+ md-expansion-panel-content {
+ padding: 0px;
+ }
+ }
+ }
+ .tb-rulechain-graph {
+ overflow: auto;
+ }
+}
+
+.fc-canvas {
+ min-width: 100%;
+ min-height: 100%;
+ outline: none;
+}
+
+.tb-rule-node {
+ display: flex;
+ flex-direction: row;
+ min-width: 150px;
+ max-width: 150px;
+ min-height: 28px;
+ max-height: 28px;
+ padding: 5px 10px;
+ border-radius: 5px;
+ background-color: #F15B26;
+ color: #333;
+ border: solid 1px #777;
+ font-size: 12px;
+ &.tb-input-type {
+ background-color: #a3eaa9;
+ user-select: none;
+ }
+ &.tb-filter-type {
+ background-color: #f1e861;
+ }
+ &.tb-enrichment-type {
+ background-color: #cdf14e;
+ }
+ &.tb-transformation-type {
+ background-color: #79cef1;
+ }
+ &.tb-action-type {
+ background-color: #f1928f;
+ }
+ &.tb-rule-chain-type {
+ background-color: #d6c4f1;
+ }
+ md-icon {
+ font-size: 20px;
+ width: 20px;
+ height: 20px;
+ min-height: 20px;
+ min-width: 20px;
+ padding-right: 4px;
+ }
+ .tb-node-type {
+
+ }
+ .tb-node-title {
+ font-weight: 600;
+ }
+ .tb-node-type, .tb-node-title {
+ overflow: hidden;
+ white-space: nowrap;
+ text-overflow: ellipsis;
+ }
+}
+
+.fc-node {
+ z-index: 1;
+ outline: none;
+ &.fc-hover, &.fc-selected {
+ -webkit-filter: brightness(70%);
+ filter: brightness(70%);
+ }
+ &.fc-dragging {
+ z-index: 10;
+ }
+ p {
+ padding: 0 15px;
+ text-align: center;
+ }
+}
+
+.fc-leftConnectors, .fc-rightConnectors {
+ position: absolute;
+ top: 0;
+ height: 100%;
+
+ display: flex;
+ flex-direction: column;
+
+ z-index: 0;
+ .fc-magnet {
+ align-items: center;
+ }
+}
+
+.fc-leftConnectors {
+ left: -20px;
+}
+
+.fc-rightConnectors {
+ right: -20px;
+}
+
+.fc-magnet {
+ display: flex;
+ flex-grow: 1;
+ height: 60px;
+ justify-content: center;
+}
+
+.fc-connector {
+ width: 14px;
+ height: 14px;
+ border: 1px solid #333;
+ margin: 10px;
+ border-radius: 5px;
+ background-color: #ccc;
+}
+
+.fc-connector.fc-hover {
+ background-color: #000;
+}
+
+.fc-edge {
+ outline: none;
+ stroke: gray;
+ stroke-width: 4;
+ fill: transparent;
+ &.fc-selected {
+ stroke: red;
+ stroke-width: 4;
+ fill: transparent;
+ }
+ &.fc-active {
+ animation: dash 3s linear infinite;
+ stroke-dasharray: 20;
+ }
+ &.fc-hover {
+ stroke: gray;
+ stroke-width: 6;
+ fill: transparent;
+ }
+ &.fc-dragging {
+ pointer-events: none;
+ }
+}
+
+.edge-endpoint {
+ fill: gray;
+}
+
+.fc-nodedelete {
+ display: none;
+}
+
+.fc-selected .fc-nodedelete {
+ outline: none;
+ display: block;
+ position: absolute;
+ right: -13px;
+ top: -16px;
+ border: solid 2px white;
+ border-radius: 50%;
+ font-weight: 600;
+ font-size: 18px;
+ line-height: 18px;
+ height: 20px;
+ padding-top: 2px;
+ width: 22px;
+ background: #494949;
+ color: #fff;
+ text-align: center;
+ vertical-align: bottom;
+ cursor: pointer;
+}
+
+.fc-edge-label {
+ position: absolute;
+ user-select: none;
+ pointer-events: none;
+ opacity: 0.8;
+}
+
+.fc-edge-label-text {
+ position: absolute;
+ left: 50%;
+ -webkit-transform: translateX(-50%);
+ transform: translateX(-50%);
+ white-space: nowrap;
+ text-align: center;
+ font-size: 14px;
+ font-weight: 600;
+ top: 5px;
+ span {
+ border: solid 2px #003a79;
+ border-radius: 10px;
+ color: #003a79;
+ background-color: #fff;
+ padding: 3px 5px;
+ }
+}
+
+@keyframes dash {
+ from {
+ stroke-dashoffset: 500;
+ }
+}
ui/src/app/rulechain/rulechain.tpl.html 130(+130 -0)
diff --git a/ui/src/app/rulechain/rulechain.tpl.html b/ui/src/app/rulechain/rulechain.tpl.html
new file mode 100644
index 0000000..9f22ebc
--- /dev/null
+++ b/ui/src/app/rulechain/rulechain.tpl.html
@@ -0,0 +1,130 @@
+<!--
+
+ Copyright © 2016-2018 The Thingsboard Authors
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+
+-->
+
+<md-content flex tb-expand-fullscreen
+ expand-tooltip-direction="bottom" layout="column" class="tb-rulechain">
+ <section class="tb-rulechain-container" flex layout="column">
+ <div class="tb-rulechain-layout" flex layout="row">
+ <div class="tb-rulechain-library">
+ <md-expansion-panel-group class="tb-rulechain-library-panel-group" md-component-id="libraryPanelGroup" auto-expand="true" multiple>
+ <md-expansion-panel md-component-id="{{typeId}}" id="{{typeId}}" ng-repeat="(typeId, typeModel) in vm.ruleNodeTypesModel">
+ <md-expansion-panel-collapsed>
+ <div class="tb-panel-title" translate>{{vm.types.ruleNodeType[typeId].name}}</div>
+ <md-expansion-panel-icon></md-expansion-panel-icon>
+ </md-expansion-panel-collapsed>
+ <md-expansion-panel-expanded>
+ <md-expansion-panel-header ng-click="vm.$mdExpansionPanel(typeId).collapse()">
+ <div class="tb-panel-title" translate>{{vm.types.ruleNodeType[typeId].name}}</div>
+ <md-expansion-panel-icon></md-expansion-panel-icon>
+ </md-expansion-panel-header>
+ <md-expansion-panel-content>
+ <fc-canvas id="tb-rulechain-{{typeId}}"
+ model="vm.ruleNodeTypesModel[typeId].model" selected-objects="vm.ruleNodeTypesModel[typeId].selectedObjects"
+ automatic-resize="false"
+ node-width="170"
+ node-height="50"
+ drop-target-id="'tb-rulchain-canvas'"></fc-canvas>
+ </md-expansion-panel-content>
+ </md-expansion-panel-expanded>
+ </md-expansion-panel>
+ </md-expansion-panel-group>
+ </div>
+ <div flex class="tb-rulechain-graph">
+ <fc-canvas id="tb-rulchain-canvas"
+ ng-keydown="vm.keyDown($event)"
+ ng-keyup="vm.keyUp($event)"
+ model="vm.ruleChainModel"
+ selected-objects="vm.selectedObjects"
+ edge-style="curved"
+ node-width="170"
+ node-height="50"
+ automatic-resize="true"
+ control="vm.canvasControl"
+ callbacks="vm.editCallbacks">
+ </fc-canvas>
+ </div>
+ </div>
+ <tb-details-sidenav class="tb-rulenode-details-sidenav"
+ header-title="{{vm.editingRuleNode.name}}"
+ header-subtitle="{{'rulenode.rulenode-details' | translate}}"
+ is-read-only="false"
+ is-open="vm.isEditingRuleNode"
+ is-always-edit="true"
+ on-close-details="vm.onEditRuleNodeClosed()"
+ on-toggle-details-edit-mode="vm.onRevertRuleNodeEdit(vm.ruleNodeForm)"
+ on-apply-details="vm.saveRuleNode(vm.ruleNodeForm)"
+ the-form="vm.ruleNodeForm">
+ <details-buttons tb-help="vm.helpLinkIdForRuleNodeType()" help-container-id="help-container">
+ <div id="help-container"></div>
+ </details-buttons>
+ <form name="vm.ruleNodeForm" ng-if="vm.isEditingRuleNode">
+ <tb-rule-node
+ rule-node="vm.editingRuleNode"
+ rule-chain-id="vm.ruleChain.id.id"
+ is-edit="true"
+ is-read-only="false"
+ on-delete-rule-node="vm.deleteRuleNode(event, vm.editingRuleNode)"
+ the-form="vm.ruleNodeForm">
+ </tb-rule-node>
+ </form>
+ </tb-details-sidenav>
+ <tb-details-sidenav class="tb-rulenode-link-details-sidenav"
+ header-title="{{vm.editingRuleNodeLink.label}}"
+ header-subtitle="{{'rulenode.link-details' | translate}}"
+ is-read-only="false"
+ is-open="vm.isEditingRuleNodeLink"
+ is-always-edit="true"
+ on-close-details="vm.onEditRuleNodeLinkClosed()"
+ on-toggle-details-edit-mode="vm.onRevertRuleNodeLinkEdit(vm.ruleNodeLinkForm)"
+ on-apply-details="vm.saveRuleNodeLink(vm.ruleNodeLinkForm)"
+ the-form="vm.ruleNodeLinkForm">
+ <details-buttons tb-help="vm.helpLinkIdForRuleNodeLink()" help-container-id="link-help-container">
+ <div id="link-help-container"></div>
+ </details-buttons>
+ <form name="vm.ruleNodeLinkForm" ng-if="vm.isEditingRuleNodeLink">
+ <tb-rule-node-link
+ link="vm.editingRuleNodeLink"
+ labels="vm.editingRuleNodeLinkLabels"
+ is-edit="true"
+ is-read-only="false"
+ the-form="vm.ruleNodeLinkForm">
+ </tb-rule-node-link>
+ </form>
+ </tb-details-sidenav>
+ </section>
+ <section layout="row" layout-wrap class="tb-footer-buttons md-fab" layout-align="start end">
+ <md-button ng-disabled="$root.loading || !vm.isDirty"
+ class="tb-btn-footer md-accent md-hue-2 md-fab"
+ aria-label="{{ 'action.apply' | translate }}"
+ ng-click="vm.saveRuleChain()">
+ <md-tooltip md-direction="top">
+ {{ 'action.apply-changes' | translate }}
+ </md-tooltip>
+ <ng-md-icon icon="done"></ng-md-icon>
+ </md-button>
+ <md-button ng-disabled="$root.loading || !vm.isDirty"
+ class="tb-btn-footer md-accent md-hue-2 md-fab"
+ aria-label="{{ 'action.decline-changes' | translate }}"
+ ng-click="vm.revertRuleChain()">
+ <md-tooltip md-direction="top">
+ {{ 'action.decline-changes' | translate }}
+ </md-tooltip>
+ <ng-md-icon icon="close"></ng-md-icon>
+ </md-button>
+ </section>
+</md-content>
diff --git a/ui/src/app/rulechain/rulechain-fieldset.tpl.html b/ui/src/app/rulechain/rulechain-fieldset.tpl.html
index ec68c57..a79ba35 100644
--- a/ui/src/app/rulechain/rulechain-fieldset.tpl.html
+++ b/ui/src/app/rulechain/rulechain-fieldset.tpl.html
@@ -32,7 +32,7 @@
</md-button>
</div>
-<md-content class="md-padding tb-rulechain" layout="column">
+<md-content class="md-padding tb-rulechain-fieldset" layout="column">
<fieldset ng-disabled="$root.loading || !isEdit || isReadOnly">
<md-input-container class="md-block">
<label translate>rulechain.name</label>
ui/src/app/rulechain/rulechains.controller.js 188(+188 -0)
diff --git a/ui/src/app/rulechain/rulechains.controller.js b/ui/src/app/rulechain/rulechains.controller.js
new file mode 100644
index 0000000..2a30b0c
--- /dev/null
+++ b/ui/src/app/rulechain/rulechains.controller.js
@@ -0,0 +1,188 @@
+/*
+ * Copyright © 2016-2018 The Thingsboard Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+/* eslint-disable import/no-unresolved, import/default */
+
+import addRuleChainTemplate from './add-rulechain.tpl.html';
+import ruleChainCard from './rulechain-card.tpl.html';
+
+/* eslint-enable import/no-unresolved, import/default */
+
+/*@ngInject*/
+export default function RuleChainsController(ruleChainService, userService, importExport, $state, $stateParams, $filter, $translate, types) {
+
+ var ruleChainActionsList = [
+ {
+ onAction: function ($event, item) {
+ vm.grid.openItem($event, item);
+ },
+ name: function() { return $translate.instant('rulechain.details') },
+ details: function() { return $translate.instant('rulechain.rulechain-details') },
+ icon: "edit"
+ },
+ {
+ onAction: function ($event, item) {
+ exportRuleChain($event, item);
+ },
+ name: function() { $translate.instant('action.export') },
+ details: function() { return $translate.instant('rulechain.export') },
+ icon: "file_download"
+ },
+ {
+ onAction: function ($event, item) {
+ vm.grid.deleteItem($event, item);
+ },
+ name: function() { return $translate.instant('action.delete') },
+ details: function() { return $translate.instant('rulechain.delete') },
+ icon: "delete",
+ isEnabled: isRuleChainEditable
+ }
+ ];
+
+ var ruleChainAddItemActionsList = [
+ {
+ onAction: function ($event) {
+ vm.grid.addItem($event);
+ },
+ name: function() { return $translate.instant('action.create') },
+ details: function() { return $translate.instant('rulechain.create-new-rulechain') },
+ icon: "insert_drive_file"
+ },
+ {
+ onAction: function ($event) {
+ importExport.importRuleChain($event).then(
+ function() {
+ vm.grid.refreshList();
+ }
+ );
+ },
+ name: function() { return $translate.instant('action.import') },
+ details: function() { return $translate.instant('rulechain.import') },
+ icon: "file_upload"
+ }
+ ];
+
+ var vm = this;
+
+ vm.types = types;
+
+ vm.ruleChainGridConfig = {
+
+ refreshParamsFunc: null,
+
+ deleteItemTitleFunc: deleteRuleChainTitle,
+ deleteItemContentFunc: deleteRuleChainText,
+ deleteItemsTitleFunc: deleteRuleChainsTitle,
+ deleteItemsActionTitleFunc: deleteRuleChainsActionTitle,
+ deleteItemsContentFunc: deleteRuleChainsText,
+
+ fetchItemsFunc: fetchRuleChains,
+ saveItemFunc: saveRuleChain,
+ clickItemFunc: openRuleChain,
+ deleteItemFunc: deleteRuleChain,
+
+ getItemTitleFunc: getRuleChainTitle,
+ itemCardTemplateUrl: ruleChainCard,
+ parentCtl: vm,
+
+ actionsList: ruleChainActionsList,
+ addItemActions: ruleChainAddItemActionsList,
+
+ onGridInited: gridInited,
+
+ addItemTemplateUrl: addRuleChainTemplate,
+
+ addItemText: function() { return $translate.instant('rulechain.add-rulechain-text') },
+ noItemsText: function() { return $translate.instant('rulechain.no-rulechains-text') },
+ itemDetailsText: function() { return $translate.instant('rulechain.rulechain-details') },
+ isSelectionEnabled: isRuleChainEditable,
+ isDetailsReadOnly: function(ruleChain) {
+ return !isRuleChainEditable(ruleChain);
+ }
+ };
+
+ if (angular.isDefined($stateParams.items) && $stateParams.items !== null) {
+ vm.ruleChainGridConfig.items = $stateParams.items;
+ }
+
+ if (angular.isDefined($stateParams.topIndex) && $stateParams.topIndex > 0) {
+ vm.ruleChainGridConfig.topIndex = $stateParams.topIndex;
+ }
+
+ vm.isRuleChainEditable = isRuleChainEditable;
+
+ vm.exportRuleChain = exportRuleChain;
+
+ function deleteRuleChainTitle(ruleChain) {
+ return $translate.instant('rulechain.delete-rulechain-title', {ruleChainName: ruleChain.name});
+ }
+
+ function deleteRuleChainText() {
+ return $translate.instant('rulechain.delete-rulechain-text');
+ }
+
+ function deleteRuleChainsTitle(selectedCount) {
+ return $translate.instant('rulechain.delete-rulechains-title', {count: selectedCount}, 'messageformat');
+ }
+
+ function deleteRuleChainsActionTitle(selectedCount) {
+ return $translate.instant('rulechain.delete-rulechains-action-title', {count: selectedCount}, 'messageformat');
+ }
+
+ function deleteRuleChainsText() {
+ return $translate.instant('rulechain.delete-rulechains-text');
+ }
+
+ function gridInited(grid) {
+ vm.grid = grid;
+ }
+
+ function fetchRuleChains(pageLink) {
+ return ruleChainService.getRuleChains(pageLink);
+ }
+
+ function saveRuleChain(ruleChain) {
+ return ruleChainService.saveRuleChain(ruleChain);
+ }
+
+ function openRuleChain($event, ruleChain) {
+ if ($event) {
+ $event.stopPropagation();
+ }
+ $state.go('home.ruleChains.ruleChain', {ruleChainId: ruleChain.id.id});
+ }
+
+ function deleteRuleChain(ruleChainId) {
+ return ruleChainService.deleteRuleChain(ruleChainId);
+ }
+
+ function getRuleChainTitle(ruleChain) {
+ return ruleChain ? ruleChain.name : '';
+ }
+
+ function isRuleChainEditable(ruleChain) {
+ if (userService.getAuthority() === 'TENANT_ADMIN') {
+ return ruleChain && ruleChain.tenantId.id != types.id.nullUid;
+ } else {
+ return userService.getAuthority() === 'SYS_ADMIN';
+ }
+ }
+
+ function exportRuleChain($event, ruleChain) {
+ $event.stopPropagation();
+ importExport.exportRuleChain(ruleChain.id.id);
+ }
+
+}
ui/src/app/rulechain/rulenode.directive.js 79(+79 -0)
diff --git a/ui/src/app/rulechain/rulenode.directive.js b/ui/src/app/rulechain/rulenode.directive.js
new file mode 100644
index 0000000..7fa4a18
--- /dev/null
+++ b/ui/src/app/rulechain/rulenode.directive.js
@@ -0,0 +1,79 @@
+/*
+ * Copyright © 2016-2018 The Thingsboard Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+/* eslint-disable import/no-unresolved, import/default */
+
+import ruleNodeFieldsetTemplate from './rulenode-fieldset.tpl.html';
+
+/* eslint-enable import/no-unresolved, import/default */
+
+/*@ngInject*/
+export default function RuleNodeDirective($compile, $templateCache, ruleChainService, types) {
+ var linker = function (scope, element) {
+ var template = $templateCache.get(ruleNodeFieldsetTemplate);
+ element.html(template);
+
+ scope.types = types;
+
+ scope.params = {
+ targetRuleChainId: null
+ };
+
+ scope.$watch('ruleNode', function() {
+ if (scope.ruleNode && scope.ruleNode.nodeType == types.ruleNodeType.RULE_CHAIN.value) {
+ scope.params.targetRuleChainId = scope.ruleNode.targetRuleChainId;
+ watchTargetRuleChain();
+ } else {
+ if (scope.targetRuleChainWatch) {
+ scope.targetRuleChainWatch();
+ scope.targetRuleChainWatch = null;
+ }
+ }
+ });
+
+ function watchTargetRuleChain() {
+ scope.targetRuleChainWatch = scope.$watch('params.targetRuleChainId',
+ function(targetRuleChainId) {
+ if (scope.ruleNode.targetRuleChainId != targetRuleChainId) {
+ scope.ruleNode.targetRuleChainId = targetRuleChainId;
+ if (targetRuleChainId) {
+ ruleChainService.getRuleChain(targetRuleChainId).then(
+ (ruleChain) => {
+ scope.ruleNode.name = ruleChain.name;
+ }
+ );
+ } else {
+ scope.ruleNode.name = "";
+ }
+ }
+ }
+ );
+ }
+ $compile(element.contents())(scope);
+ }
+ return {
+ restrict: "E",
+ link: linker,
+ scope: {
+ ruleChainId: '=',
+ ruleNode: '=',
+ isEdit: '=',
+ isReadOnly: '=',
+ theForm: '=',
+ onDeleteRuleNode: '&'
+ }
+ };
+}
ui/src/app/rulechain/rulenode.tpl.html 45(+45 -0)
diff --git a/ui/src/app/rulechain/rulenode.tpl.html b/ui/src/app/rulechain/rulenode.tpl.html
new file mode 100644
index 0000000..f1d3619
--- /dev/null
+++ b/ui/src/app/rulechain/rulenode.tpl.html
@@ -0,0 +1,45 @@
+<!--
+
+ Copyright © 2016-2018 The Thingsboard Authors
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+
+-->
+<div
+ id="{{node.id}}"
+ ng-attr-style="position: absolute; top: {{ node.y }}px; left: {{ node.x }}px;"
+ ng-dblclick="callbacks.doubleClick($event, node)">
+ <div class="tb-rule-node {{node.nodeClass}}">
+ <md-icon aria-label="node-type-icon" flex="15"
+ class="material-icons">{{node.icon}}</md-icon>
+ <div layout="column" flex="85" layout-align="center">
+ <span class="tb-node-type">{{ node.type }}</span>
+ <span class="tb-node-title" ng-if="node.name">{{ node.name }}</span>
+ </div>
+ <div class="{{flowchartConstants.leftConnectorClass}}">
+ <div fc-magnet
+ ng-repeat="connector in modelservice.nodes.getConnectorsByType(node, flowchartConstants.leftConnectorType)">
+ <div fc-connector></div>
+ </div>
+ </div>
+ <div class="{{flowchartConstants.rightConnectorClass}}">
+ <div fc-magnet
+ ng-repeat="connector in modelservice.nodes.getConnectorsByType(node, flowchartConstants.rightConnectorType)">
+ <div fc-connector></div>
+ </div>
+ </div>
+ </div>
+ <div ng-if="modelservice.isEditable() && !node.readonly" class="fc-nodedelete" ng-click="modelservice.nodes.delete(node)">
+ ×
+ </div>
+</div>
diff --git a/ui/src/app/rulechain/rulenode-fieldset.tpl.html b/ui/src/app/rulechain/rulenode-fieldset.tpl.html
new file mode 100644
index 0000000..2b2faf8
--- /dev/null
+++ b/ui/src/app/rulechain/rulenode-fieldset.tpl.html
@@ -0,0 +1,55 @@
+<!--
+
+ Copyright © 2016-2018 The Thingsboard Authors
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+
+-->
+<md-button ng-click="onDeleteRuleNode({event: $event})"
+ ng-show="!isEdit && !isReadOnly"
+ class="md-raised md-primary">{{ 'rulenode.delete' | translate }}</md-button>
+
+<md-content class="md-padding tb-rulenode" layout="column">
+ <fieldset ng-disabled="$root.loading || !isEdit || isReadOnly">
+ <md-input-container class="md-block">
+ <label translate>rulenode.type</label>
+ <input readonly name="type" ng-model="ruleNode.type">
+ </md-input-container>
+ <section ng-if="ruleNode.nodeType != types.ruleNodeType.RULE_CHAIN.value">
+ <md-input-container class="md-block">
+ <label translate>rulenode.name</label>
+ <input required name="name" ng-model="ruleNode.name">
+ <div ng-messages="theForm.name.$error">
+ <div translate ng-message="required">rulenode.name-required</div>
+ </div>
+ </md-input-container>
+ <md-input-container class="md-block">
+ <label translate>rulenode.description</label>
+ <textarea ng-model="ruleNode.additionalInfo.description" rows="2"></textarea>
+ </md-input-container>
+ </section>
+ <section ng-if="ruleNode.nodeType == types.ruleNodeType.RULE_CHAIN.value">
+ <tb-entity-autocomplete the-form="theForm"
+ ng-disabled="$root.loading || !isEdit || isReadOnly"
+ tb-required="true"
+ exclude-entity-ids="[ruleChainId]"
+ entity-type="types.entityType.rulechain"
+ ng-model="params.targetRuleChainId">
+ </tb-entity-autocomplete>
+ <md-input-container class="md-block">
+ <label translate>rulenode.description</label>
+ <textarea ng-model="ruleNode.additionalInfo.description" rows="2"></textarea>
+ </md-input-container>
+ </section>
+ </fieldset>
+</md-content>