diff --git a/application/src/main/java/org/thingsboard/server/service/component/AnnotationComponentDiscoveryService.java b/application/src/main/java/org/thingsboard/server/service/component/AnnotationComponentDiscoveryService.java index 479f424e93..9377756b86 100644 --- a/application/src/main/java/org/thingsboard/server/service/component/AnnotationComponentDiscoveryService.java +++ b/application/src/main/java/org/thingsboard/server/service/component/AnnotationComponentDiscoveryService.java @@ -192,6 +192,8 @@ public class AnnotationComponentDiscoveryService implements ComponentDiscoverySe NodeConfiguration config = configClazz.newInstance(); NodeConfiguration defaultConfiguration = config.defaultConfiguration(); nodeDefinition.setDefaultConfiguration(mapper.valueToTree(defaultConfiguration)); + nodeDefinition.setUiResources(nodeAnnotation.uiResources()); + nodeDefinition.setConfigDirective(nodeAnnotation.configDirective()); return nodeDefinition; } diff --git a/pom.xml b/pom.xml index f0c915a5b0..a90a8aa857 100755 --- a/pom.xml +++ b/pom.xml @@ -284,6 +284,7 @@ src/sh/** src/main/scripts/control/** src/main/scripts/windows/** + src/main/resources/public/static/rulenode/** JAVADOC_STYLE diff --git a/rule-engine/rule-engine-api/src/main/java/org/thingsboard/rule/engine/api/NodeDefinition.java b/rule-engine/rule-engine-api/src/main/java/org/thingsboard/rule/engine/api/NodeDefinition.java index 6c57d92a54..18b2b941ad 100644 --- a/rule-engine/rule-engine-api/src/main/java/org/thingsboard/rule/engine/api/NodeDefinition.java +++ b/rule-engine/rule-engine-api/src/main/java/org/thingsboard/rule/engine/api/NodeDefinition.java @@ -29,5 +29,7 @@ public class NodeDefinition { String[] relationTypes; boolean customRelations; JsonNode defaultConfiguration; + String[] uiResources; + String configDirective; } diff --git a/rule-engine/rule-engine-api/src/main/java/org/thingsboard/rule/engine/api/RuleNode.java b/rule-engine/rule-engine-api/src/main/java/org/thingsboard/rule/engine/api/RuleNode.java index 16170347a7..eea92ed00a 100644 --- a/rule-engine/rule-engine-api/src/main/java/org/thingsboard/rule/engine/api/RuleNode.java +++ b/rule-engine/rule-engine-api/src/main/java/org/thingsboard/rule/engine/api/RuleNode.java @@ -45,6 +45,10 @@ public @interface RuleNode { String[] relationTypes() default {"Success", "Failure"}; + String[] uiResources() default {}; + + String configDirective() default ""; + boolean customRelations() default false; } diff --git a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/filter/TbJsFilterNode.java b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/filter/TbJsFilterNode.java index 07b166db68..c684b200c5 100644 --- a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/filter/TbJsFilterNode.java +++ b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/filter/TbJsFilterNode.java @@ -35,7 +35,10 @@ import static org.thingsboard.rule.engine.DonAsynchron.withCallback; nodeDetails = "Evaluate incoming Message with configured JS condition. " + "If True - send Message via True chain, otherwise False chain is used." + "Message payload can be accessed via msg property. For example msg.temperature < 10;" + - "Message metadata can be accessed via meta property. For example meta.customerName === 'John';") + "Message metadata can be accessed via meta property. For example meta.customerName === 'John';", + uiResources = {"static/rulenode/rulenode-core-config.js"}, + configDirective = "tbFilterNodeScriptConfig") + public class TbJsFilterNode implements TbNode { private TbJsFilterNodeConfiguration config; diff --git a/rule-engine/rule-engine-components/src/main/resources/public/static/rulenode/rulenode-core-config.js b/rule-engine/rule-engine-components/src/main/resources/public/static/rulenode/rulenode-core-config.js new file mode 100644 index 0000000000..f254cf599d --- /dev/null +++ b/rule-engine/rule-engine-components/src/main/resources/public/static/rulenode/rulenode-core-config.js @@ -0,0 +1,2 @@ +!function(e){function t(r){if(n[r])return n[r].exports;var u=n[r]={exports:{},id:r,loaded:!1};return e[r].call(u.exports,u,u.exports,t),u.loaded=!0,u.exports}var n={};return t.m=e,t.c=n,t.p="/static/",t(0)}([function(e,t,n){e.exports=n(3)},function(e,t){e.exports='
'},function(e,t,n){"use strict";function r(e){return e&&e.__esModule?e:{default:e}}function u(e){var t=function(t,n,r,u){var o=i.default;n.html(o),t.$watch("configuration",function(e,n){angular.equals(e,n)||u.$setViewValue(t.configuration)}),u.$render=function(){t.configuration=u.$viewValue},e(n.contents())(t)};return{restrict:"E",require:"^ngModel",scope:{},link:t}}u.$inject=["$compile"],Object.defineProperty(t,"__esModule",{value:!0}),t.default=u;var o=n(1),i=r(o)},function(e,t,n){"use strict";function r(e){return e&&e.__esModule?e:{default:e}}Object.defineProperty(t,"__esModule",{value:!0});var u=n(2),o=r(u),i=n(5),a=r(i);t.default=angular.module("thingsboard.ruleChain.config",[]).directive("tbFilterNodeScriptConfig",o.default).config(a.default).name},function(e,t){"use strict";function n(e){var t={tb:{rulenode:{filter:"Filter"}}};angular.merge(e.en_US,t)}Object.defineProperty(t,"__esModule",{value:!0}),t.default=n},function(e,t,n){"use strict";function r(e){return e&&e.__esModule?e:{default:e}}function u(e,t){(0,i.default)(t);for(var n in t){var r=t[n];e.translations(n,r)}}u.$inject=["$translateProvider","locales"],Object.defineProperty(t,"__esModule",{value:!0}),t.default=u;var o=n(4),i=r(o)}]); +//# sourceMappingURL=rulenode-core-config.js.map \ No newline at end of file diff --git a/ui/server.js b/ui/server.js index fae132f02f..0513600afd 100644 --- a/ui/server.js +++ b/ui/server.js @@ -30,6 +30,10 @@ const httpProxy = require('http-proxy'); const forwardHost = 'localhost'; const forwardPort = 8080; +const ruleNodeUiforwardHost = 'localhost'; +const ruleNodeUiforwardPort = 8080; +//const ruleNodeUiforwardPort = 5000; + const app = express(); const server = http.createServer(app); @@ -52,17 +56,34 @@ const apiProxy = httpProxy.createProxyServer({ } }); +const ruleNodeUiApiProxy = httpProxy.createProxyServer({ + target: { + host: ruleNodeUiforwardHost, + port: ruleNodeUiforwardPort + } +}); + apiProxy.on('error', function (err, req, res) { console.warn('API proxy error: ' + err); res.end('Error.'); }); +ruleNodeUiApiProxy.on('error', function (err, req, res) { + console.warn('RuleNode UI API proxy error: ' + err); + res.end('Error.'); +}); + console.info(`Forwarding API requests to http://${forwardHost}:${forwardPort}`); +console.info(`Forwarding Rule Node UI requests to http://${ruleNodeUiforwardHost}:${ruleNodeUiforwardPort}`); app.all('/api/*', (req, res) => { apiProxy.web(req, res); }); +app.all('/static/rulenode/*', (req, res) => { + ruleNodeUiApiProxy.web(req, res); +}); + app.get('*', function(req, res) { res.sendFile(path.join(__dirname, 'src/index.html')); }); diff --git a/ui/src/app/api/rule-chain.service.js b/ui/src/app/api/rule-chain.service.js index ebc48fad61..af14a3f582 100644 --- a/ui/src/app/api/rule-chain.service.js +++ b/ui/src/app/api/rule-chain.service.js @@ -17,7 +17,7 @@ export default angular.module('thingsboard.api.ruleChain', []) .factory('ruleChainService', RuleChainService).name; /*@ngInject*/ -function RuleChainService($http, $q, $filter, types, componentDescriptorService) { +function RuleChainService($http, $q, $filter, $ocLazyLoad, $translate, types, componentDescriptorService) { var ruleNodeComponents = null; @@ -177,11 +177,18 @@ function RuleChainService($http, $q, $filter, types, componentDescriptorService) } else { loadRuleNodeComponents().then( (components) => { - ruleNodeComponents = components; - ruleNodeComponents.push( - types.ruleChainNodeComponent + resolveRuleNodeComponentsUiResources(components).then( + (components) => { + ruleNodeComponents = components; + ruleNodeComponents.push( + types.ruleChainNodeComponent + ); + deferred.resolve(ruleNodeComponents); + }, + () => { + deferred.reject(); + } ); - deferred.resolve(ruleNodeComponents); }, () => { deferred.reject(); @@ -191,6 +198,48 @@ function RuleChainService($http, $q, $filter, types, componentDescriptorService) return deferred.promise; } + function resolveRuleNodeComponentsUiResources(components) { + var deferred = $q.defer(); + var tasks = []; + for (var i=0;i { + deferred.resolve(components); + }, + () => { + deferred.resolve(components); + } + ); + return deferred.promise; + } + + function resolveRuleNodeComponentUiResources(component) { + var deferred = $q.defer(); + var uiResources = component.configurationDescriptor.nodeDefinition.uiResources; + if (uiResources && uiResources.length) { + var tasks = []; + for (var i=0;i { + deferred.resolve(component); + }, + () => { + component.configurationDescriptor.nodeDefinition.uiResourceLoadError = $translate.instant('rulenode.ui-resources-load-error'); + deferred.resolve(component); + } + ) + } else { + deferred.resolve(component); + } + return deferred.promise; + } + function getRuleNodeComponentByClazz(clazz) { var res = $filter('filter')(ruleNodeComponents, {clazz: clazz}, true); if (res && res.length) { diff --git a/ui/src/app/components/js-func.directive.js b/ui/src/app/components/js-func.directive.js index 33cebdebf0..deb5626826 100644 --- a/ui/src/app/components/js-func.directive.js +++ b/ui/src/app/components/js-func.directive.js @@ -43,6 +43,7 @@ function JsFunc($compile, $templateCache, toast, utils, $translate) { var template = $templateCache.get(jsFuncTemplate); element.html(template); + scope.functionName = attrs.functionName; scope.functionArgs = scope.$eval(attrs.functionArgs); scope.validationArgs = scope.$eval(attrs.validationArgs); scope.resultType = attrs.resultType; @@ -50,6 +51,8 @@ function JsFunc($compile, $templateCache, toast, utils, $translate) { scope.resultType = "nocheck"; } + scope.validationTriggerArg = attrs.validationTriggerArg; + scope.functionValid = true; var Range = ace.acequire("ace/range").Range; @@ -66,11 +69,15 @@ function JsFunc($compile, $templateCache, toast, utils, $translate) { } scope.onFullscreenChanged = function () { + updateEditorSize(); + }; + + function updateEditorSize() { if (scope.js_editor) { scope.js_editor.resize(); scope.js_editor.renderer.updateFull(); } - }; + } scope.jsEditorOptions = { useWrapMode: true, @@ -131,6 +138,9 @@ function JsFunc($compile, $templateCache, toast, utils, $translate) { scope.validate = function () { try { var toValidate = new Function(scope.functionArgsString, scope.functionBody); + if (scope.noValidate) { + return true; + } var res; var validationError; for (var i=0;i -
+
- function({{ functionArgsString }}) { +
-
- } -
-
\ No newline at end of file + +
+ diff --git a/ui/src/app/locale/locale.constant.js b/ui/src/app/locale/locale.constant.js index 5dce7872e8..b511b56c33 100644 --- a/ui/src/app/locale/locale.constant.js +++ b/ui/src/app/locale/locale.constant.js @@ -1198,7 +1198,9 @@ export default angular.module('thingsboard.locale', []) "type-action": "Action", "type-action-details": "Perform special action", "type-rule-chain": "Rule Chain", - "type-rule-chain-details": "Forwards incoming messages to specified Rule Chain" + "type-rule-chain-details": "Forwards incoming messages to specified Rule Chain", + "directive-is-not-loaded": "Defined configuration directive '{{directiveName}}' is not available.", + "ui-resources-load-error": "Failed to load configuration ui resources." }, "rule-plugin": { "management": "Rules and plugins management" diff --git a/ui/src/app/rulechain/index.js b/ui/src/app/rulechain/index.js index 7306762572..c674467d35 100644 --- a/ui/src/app/rulechain/index.js +++ b/ui/src/app/rulechain/index.js @@ -18,6 +18,8 @@ import RuleChainRoutes from './rulechain.routes'; import RuleChainsController from './rulechains.controller'; import {RuleChainController, AddRuleNodeController, AddRuleNodeLinkController} from './rulechain.controller'; import RuleChainDirective from './rulechain.directive'; +import RuleNodeDefinedConfigDirective from './rulenode-defined-config.directive'; +import RuleNodeConfigDirective from './rulenode-config.directive'; import RuleNodeDirective from './rulenode.directive'; import LinkDirective from './link.directive'; @@ -28,6 +30,8 @@ export default angular.module('thingsboard.ruleChain', []) .controller('AddRuleNodeController', AddRuleNodeController) .controller('AddRuleNodeLinkController', AddRuleNodeLinkController) .directive('tbRuleChain', RuleChainDirective) + .directive('tbRuleNodeDefinedConfig', RuleNodeDefinedConfigDirective) + .directive('tbRuleNodeConfig', RuleNodeConfigDirective) .directive('tbRuleNode', RuleNodeDirective) .directive('tbRuleNodeLink', LinkDirective) .name; diff --git a/ui/src/app/rulechain/rulechain.controller.js b/ui/src/app/rulechain/rulechain.controller.js index 4eba5b2c52..dd48bb007a 100644 --- a/ui/src/app/rulechain/rulechain.controller.js +++ b/ui/src/app/rulechain/rulechain.controller.js @@ -137,10 +137,13 @@ export function RuleChainController($stateParams, $scope, $compile, $q, $mdUtil, }; vm.saveRuleNode = function(theForm) { - theForm.$setPristine(); - vm.isEditingRuleNode = false; - vm.ruleChainModel.nodes[vm.editingRuleNodeIndex] = vm.editingRuleNode; - vm.editingRuleNode = angular.copy(vm.editingRuleNode); + $scope.$broadcast('form-submit'); + if (theForm.$valid) { + theForm.$setPristine(); + vm.isEditingRuleNode = false; + vm.ruleChainModel.nodes[vm.editingRuleNodeIndex] = vm.editingRuleNode; + vm.editingRuleNode = angular.copy(vm.editingRuleNode); + } }; vm.saveRuleNodeLink = function(theForm) { @@ -309,7 +312,7 @@ export function RuleChainController($stateParams, $scope, $compile, $q, $mdUtil, var componentType = ruleNodeComponent.type; var model = vm.ruleNodeTypesModel[componentType].model; var node = { - id: model.nodes.length, + id: 'node-lib-' + componentType + '-' + model.nodes.length, component: ruleNodeComponent, name: '', nodeClass: vm.types.ruleNodeType[componentType].nodeClass, @@ -358,7 +361,7 @@ export function RuleChainController($stateParams, $scope, $compile, $q, $mdUtil, vm.ruleChainModel.nodes.push( { - id: vm.nextNodeID++, + id: 'rule-chain-node-' + vm.nextNodeID++, component: types.inputNodeComponent, name: "", nodeClass: types.ruleNodeType.INPUT.nodeClass, @@ -389,7 +392,7 @@ export function RuleChainController($stateParams, $scope, $compile, $q, $mdUtil, var component = ruleChainService.getRuleNodeComponentByClazz(ruleNode.type); if (component) { var node = { - id: vm.nextNodeID++, + id: 'rule-chain-node-' + vm.nextNodeID++, ruleNodeId: ruleNode.id, additionalInfo: ruleNode.additionalInfo, configuration: ruleNode.configuration, @@ -466,7 +469,7 @@ export function RuleChainController($stateParams, $scope, $compile, $q, $mdUtil, var ruleChainNode = ruleChainNodesMap[ruleChainConnection.additionalInfo.ruleChainNodeId]; if (!ruleChainNode) { ruleChainNode = { - id: vm.nextNodeID++, + id: 'rule-chain-node-' + vm.nextNodeID++, additionalInfo: ruleChainConnection.additionalInfo, targetRuleChainId: ruleChainConnection.targetRuleChainId.id, x: ruleChainConnection.additionalInfo.layoutX, @@ -611,7 +614,7 @@ export function RuleChainController($stateParams, $scope, $compile, $q, $mdUtil, fullscreen: true, targetEvent: $event }).then(function (ruleNode) { - ruleNode.id = vm.nextNodeID++; + ruleNode.id = 'rule-chain-node-' + vm.nextNodeID++; ruleNode.connectors = []; if (ruleNode.component.configurationDescriptor.nodeDefinition.inEnabled) { ruleNode.connectors.push( diff --git a/ui/src/app/rulechain/rulenode-config.directive.js b/ui/src/app/rulechain/rulenode-config.directive.js new file mode 100644 index 0000000000..4b75c793f1 --- /dev/null +++ b/ui/src/app/rulechain/rulenode-config.directive.js @@ -0,0 +1,73 @@ +/* + * 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 ruleNodeConfigTemplate from './rulenode-config.tpl.html'; + +/* eslint-enable import/no-unresolved, import/default */ + +/*@ngInject*/ +export default function RuleNodeConfigDirective($compile, $templateCache, $injector, $translate) { + + var linker = function (scope, element, attrs, ngModelCtrl) { + var template = $templateCache.get(ruleNodeConfigTemplate); + element.html(template); + + scope.$watch('configuration', function (newVal, prevVal) { + if (!angular.equals(newVal, prevVal)) { + ngModelCtrl.$setViewValue(scope.configuration); + } + }); + + ngModelCtrl.$render = function () { + scope.configuration = ngModelCtrl.$viewValue; + }; + + scope.useDefinedDirective = function() { + return scope.nodeDefinition.configDirective && !scope.definedDirectiveError; + }; + + validateDefinedDirective(); + + function validateDefinedDirective() { + if (scope.nodeDefinition.uiResourceLoadError && scope.nodeDefinition.uiResourceLoadError.length) { + scope.definedDirectiveError = scope.nodeDefinition.uiResourceLoadError; + } else { + var definedDirective = scope.nodeDefinition.configDirective; + if (definedDirective && definedDirective.length) { + if (!$injector.has(definedDirective + 'Directive')) { + scope.definedDirectiveError = $translate.instant('rulenode.directive-is-not-loaded', {directiveName: definedDirective}); + } + } + } + } + + $compile(element.contents())(scope); + }; + + return { + restrict: "E", + require: "^ngModel", + scope: { + nodeDefinition:'=', + required:'=ngRequired', + readonly:'=ngReadonly' + }, + link: linker + }; + +} diff --git a/ui/src/app/rulechain/rulenode-config.tpl.html b/ui/src/app/rulechain/rulenode-config.tpl.html new file mode 100644 index 0000000000..32d5347b43 --- /dev/null +++ b/ui/src/app/rulechain/rulenode-config.tpl.html @@ -0,0 +1,32 @@ + + + + +
{{definedDirectiveError}}
+ + diff --git a/ui/src/app/rulechain/rulenode-defined-config.directive.js b/ui/src/app/rulechain/rulenode-defined-config.directive.js new file mode 100644 index 0000000000..5ec2620aaf --- /dev/null +++ b/ui/src/app/rulechain/rulenode-defined-config.directive.js @@ -0,0 +1,63 @@ +/* + * 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. + */ + +const SNAKE_CASE_REGEXP = /[A-Z]/g; + +/*@ngInject*/ +export default function RuleNodeDefinedConfigDirective($compile) { + + var linker = function (scope, element, attrs, ngModelCtrl) { + + attrs.$observe('ruleNodeDirective', function() { + loadTemplate(); + }); + + scope.$watch('configuration', function (newVal, prevVal) { + if (!angular.equals(newVal, prevVal)) { + ngModelCtrl.$setViewValue(scope.configuration); + } + }); + + ngModelCtrl.$render = function () { + scope.configuration = ngModelCtrl.$viewValue; + }; + + function loadTemplate() { + var directive = snake_case(attrs.ruleNodeDirective, '-'); + var template = `<${directive} ng-model="configuration" ng-required="required" ng-readonly="readonly">`; + element.html(template); + $compile(element.contents())(scope); + } + + function snake_case(name, separator) { + separator = separator || '_'; + return name.replace(SNAKE_CASE_REGEXP, function(letter, pos) { + return (pos ? separator : '') + letter.toLowerCase(); + }); + } + }; + + return { + restrict: "E", + require: "^ngModel", + scope: { + required:'=ngRequired', + readonly:'=ngReadonly' + }, + link: linker + }; + +} diff --git a/ui/src/app/rulechain/rulenode-fieldset.tpl.html b/ui/src/app/rulechain/rulenode-fieldset.tpl.html index 30cf0752cd..ad109ef6cb 100644 --- a/ui/src/app/rulechain/rulenode-fieldset.tpl.html +++ b/ui/src/app/rulechain/rulenode-fieldset.tpl.html @@ -38,11 +38,16 @@ ng-model="ruleNode.debugMode">{{ 'rulenode.debug-mode' | translate }} - + + diff --git a/ui/src/app/rulechain/rulenode.scss b/ui/src/app/rulechain/rulenode.scss index febc637a37..0466673337 100644 --- a/ui/src/app/rulechain/rulenode.scss +++ b/ui/src/app/rulechain/rulenode.scss @@ -19,4 +19,10 @@ height: 300px; display: block; } +} + +.tb-rulenode-directive-error { + color: rgb(221,44,0); + font-size: 13px; + font-weight: 400; } \ No newline at end of file