From 6ece02f88f864f5a9551b870b3f40e8a33eb20c8 Mon Sep 17 00:00:00 2001 From: Igor Kulikov Date: Wed, 21 Mar 2018 14:56:16 +0200 Subject: [PATCH] Rule chains UI --- .../common/data/rule/RuleChainMetaData.java | 9 +- .../server/dao/rule/BaseRuleChainService.java | 4 +- ui/package.json | 1 + ui/src/app/api/entity.service.js | 8 +- ui/src/app/api/rule-chain.service.js | 137 +++- ui/src/app/app.js | 3 + ui/src/app/common/types.constant.js | 38 ++ .../entity/entity-autocomplete.directive.js | 6 + ui/src/app/locale/locale.constant.js | 20 + ui/src/app/rulechain/add-link.tpl.html | 48 ++ ui/src/app/rulechain/add-rulenode.tpl.html | 48 ++ ui/src/app/rulechain/index.js | 10 +- ui/src/app/rulechain/link-fieldset.tpl.html | 39 ++ ui/src/app/rulechain/link.directive.js | 71 ++ .../app/rulechain/rulechain-fieldset.tpl.html | 2 +- ui/src/app/rulechain/rulechain.controller.js | 638 +++++++++++++++--- ui/src/app/rulechain/rulechain.routes.js | 41 +- ui/src/app/rulechain/rulechain.scss | 262 +++++++ ui/src/app/rulechain/rulechain.tpl.html | 130 ++++ ui/src/app/rulechain/rulechains.controller.js | 188 ++++++ .../app/rulechain/rulenode-fieldset.tpl.html | 55 ++ ui/src/app/rulechain/rulenode.directive.js | 79 +++ ui/src/app/rulechain/rulenode.tpl.html | 45 ++ 23 files changed, 1761 insertions(+), 121 deletions(-) create mode 100644 ui/src/app/rulechain/add-link.tpl.html create mode 100644 ui/src/app/rulechain/add-rulenode.tpl.html create mode 100644 ui/src/app/rulechain/link-fieldset.tpl.html create mode 100644 ui/src/app/rulechain/link.directive.js create mode 100644 ui/src/app/rulechain/rulechain.scss create mode 100644 ui/src/app/rulechain/rulechain.tpl.html create mode 100644 ui/src/app/rulechain/rulechains.controller.js create mode 100644 ui/src/app/rulechain/rulenode-fieldset.tpl.html create mode 100644 ui/src/app/rulechain/rulenode.directive.js create mode 100644 ui/src/app/rulechain/rulenode.tpl.html 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 af141d6142..8deece2237 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 04207ec2f5..c8c8df7641 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()); } } } diff --git a/ui/package.json b/ui/package.json index ad95ef4a7a..b602720a48 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", diff --git a/ui/src/app/api/entity.service.js b/ui/src/app/api/entity.service.js index e4c51a2ea9..ba1265f100 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); diff --git a/ui/src/app/api/rule-chain.service.js b/ui/src/app/api/rule-chain.service.js index 16afa747b8..be8e99db11 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; + } + + } diff --git a/ui/src/app/app.js b/ui/src/app/app.js index 138b2308f3..31310135cd 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, diff --git a/ui/src/app/common/types.constant.js b/ui/src/app/common/types.constant.js index 77c21e1fd0..b134bf0575 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 c2053c0bcc..2dfc3be28f 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'; diff --git a/ui/src/app/locale/locale.constant.js b/ui/src/app/locale/locale.constant.js index ea576ecf51..4c392ca84d 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" }, 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 0000000000..42c07773bb --- /dev/null +++ b/ui/src/app/rulechain/add-link.tpl.html @@ -0,0 +1,48 @@ + + +
+ +
+

rulenode.add-link

+ +
+ + + +
+
+ + + +
+ +
+
+ + + + {{ 'action.add' | translate }} + + {{ 'action.cancel' | + translate }} + + +
+
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 0000000000..c36b43bc0f --- /dev/null +++ b/ui/src/app/rulechain/add-rulenode.tpl.html @@ -0,0 +1,48 @@ + + +
+ +
+

rulenode.add

+ +
+ + + +
+
+ + + +
+ +
+
+ + + + {{ 'action.add' | translate }} + + {{ 'action.cancel' | + translate }} + + +
+
diff --git a/ui/src/app/rulechain/index.js b/ui/src/app/rulechain/index.js index faecc03e03..7306762572 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; 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 0000000000..13ec6c3ce8 --- /dev/null +++ b/ui/src/app/rulechain/link-fieldset.tpl.html @@ -0,0 +1,39 @@ + + +
+ + + + + {{label.name}} + + +
+
rulenode.link-label-required
+
+
+ + + +
+
rulenode.custom-link-label-required
+
+
+
+
diff --git a/ui/src/app/rulechain/link.directive.js b/ui/src/app/rulechain/link.directive.js new file mode 100644 index 0000000000..b3565a3954 --- /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: '=' + } + }; +} diff --git a/ui/src/app/rulechain/rulechain-fieldset.tpl.html b/ui/src/app/rulechain/rulechain-fieldset.tpl.html index ec68c574bf..a79ba35cf0 100644 --- a/ui/src/app/rulechain/rulechain-fieldset.tpl.html +++ b/ui/src/app/rulechain/rulechain-fieldset.tpl.html @@ -32,7 +32,7 @@ - +
diff --git a/ui/src/app/rulechain/rulechain.controller.js b/ui/src/app/rulechain/rulechain.controller.js index 475ef4e09a..c854cfa36d 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 { + createRuleChainModel(ruleChainsMap); + } + ); } - function deleteRuleChainsText() { - return $translate.instant('rulechain.delete-rulechains-text'); + function createRuleChainModel(ruleChainsMap) { + var nodes = []; + for (var i=0;i -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.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); + } } diff --git a/ui/src/app/rulechain/rulechain.routes.js b/ui/src/app/rulechain/rulechain.routes.js index e8d6be49d2..808661a65a 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"}' + } + }); } diff --git a/ui/src/app/rulechain/rulechain.scss b/ui/src/app/rulechain/rulechain.scss new file mode 100644 index 0000000000..258e232ea4 --- /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; + } +} diff --git a/ui/src/app/rulechain/rulechain.tpl.html b/ui/src/app/rulechain/rulechain.tpl.html new file mode 100644 index 0000000000..9f22ebceb9 --- /dev/null +++ b/ui/src/app/rulechain/rulechain.tpl.html @@ -0,0 +1,130 @@ + + + +
+
+
+ + + +
{{vm.types.ruleNodeType[typeId].name}}
+ +
+ + +
{{vm.types.ruleNodeType[typeId].name}}
+ +
+ + + +
+
+
+
+
+ + +
+
+ + +
+
+
+ + +
+
+ + + + +
+ + +
+
+
+ +
diff --git a/ui/src/app/rulechain/rulechains.controller.js b/ui/src/app/rulechain/rulechains.controller.js new file mode 100644 index 0000000000..2a30b0c98f --- /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); + } + +} 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 0000000000..2b2faf8257 --- /dev/null +++ b/ui/src/app/rulechain/rulenode-fieldset.tpl.html @@ -0,0 +1,55 @@ + +{{ 'rulenode.delete' | translate }} + + +
+ + + + +
+ + + +
+
rulenode.name-required
+
+
+ + + + +
+
+ + + + + + +
+
+
diff --git a/ui/src/app/rulechain/rulenode.directive.js b/ui/src/app/rulechain/rulenode.directive.js new file mode 100644 index 0000000000..7fa4a18ca3 --- /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: '&' + } + }; +} diff --git a/ui/src/app/rulechain/rulenode.tpl.html b/ui/src/app/rulechain/rulenode.tpl.html new file mode 100644 index 0000000000..f1d3619ebc --- /dev/null +++ b/ui/src/app/rulechain/rulenode.tpl.html @@ -0,0 +1,45 @@ + +
+
+ {{node.icon}} +
+ {{ node.type }} + {{ node.name }} +
+
+
+
+
+
+
+
+
+
+
+
+
+ × +
+