thingsboard-memoizeit

RuleNode Config UI

3/28/2018 5:03:11 AM

Details

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 479f424..9377756 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;
     }
 

pom.xml 1(+1 -0)

diff --git a/pom.xml b/pom.xml
index f0c915a..a90a8aa 100755
--- a/pom.xml
+++ b/pom.xml
@@ -284,6 +284,7 @@
                             <exclude>src/sh/**</exclude>
                             <exclude>src/main/scripts/control/**</exclude>
                             <exclude>src/main/scripts/windows/**</exclude>
+                            <exclude>src/main/resources/public/static/rulenode/**</exclude>
                         </excludes>
                         <mapping>
                             <proto>JAVADOC_STYLE</proto>
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 6c57d92..18b2b94 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 1617034..eea92ed 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 07b166d..c684b20 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 <b>True</b> - send Message via <b>True</b> chain, otherwise <b>False</b> chain is used." +
                 "Message payload can be accessed via <code>msg</code> property. For example <code>msg.temperature < 10;</code>" +
-                "Message metadata can be accessed via <code>meta</code> property. For example <code>meta.customerName === 'John';</code>")
+                "Message metadata can be accessed via <code>meta</code> property. For example <code>meta.customerName === 'John';</code>",
+        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 0000000..f254cf5
--- /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=' <section layout=column> <label translate class="tb-title no-padding">tb.rulenode.filter</label> <tb-js-func ng-model=configuration.jsScript function-name=Filter function-args="{{ [\'msg\'] }}" no-validate=true> </tb-js-func> </section> '},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

ui/server.js 21(+21 -0)

diff --git a/ui/server.js b/ui/server.js
index fae132f..0513600 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 ebc48fa..af14a3f 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<components.length;i++) {
+            var component = components[i];
+            tasks.push(resolveRuleNodeComponentUiResources(component));
+        }
+        $q.all(tasks).then(
+            (components) => {
+                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<uiResources.length;i++) {
+                var uiResource = uiResources[i];
+                tasks.push($ocLazyLoad.load(uiResource));
+            }
+            $q.all(tasks).then(
+                () => {
+                    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 33cebde..deb5626 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<scope.validationArgs.length;i++) {
@@ -200,9 +210,19 @@ function JsFunc($compile, $templateCache, toast, utils, $translate) {
             }
         };
 
-        scope.$on('form-submit', function () {
-            scope.functionValid = scope.validate();
-            scope.updateValidity();
+        scope.$on('form-submit', function (event, args) {
+            if (!args || scope.validationTriggerArg && scope.validationTriggerArg == args) {
+                scope.validationArgs = scope.$eval(attrs.validationArgs);
+                scope.cleanupJsErrors();
+                scope.functionValid = true;
+                scope.updateValidity();
+                scope.functionValid = scope.validate();
+                scope.updateValidity();
+            }
+        });
+
+        scope.$on('update-ace-editor-size', function () {
+            updateEditorSize();
         });
 
         $compile(element.contents())(scope);
@@ -211,7 +231,11 @@ function JsFunc($compile, $templateCache, toast, utils, $translate) {
     return {
         restrict: "E",
         require: "^ngModel",
-        scope: {},
+        scope: {
+            disabled:'=ngDisabled',
+            noValidate: '=?',
+            fillHeight:'=?'
+        },
         link: linker
     };
 }
diff --git a/ui/src/app/components/js-func.scss b/ui/src/app/components/js-func.scss
index 2bd5df1..e1072be 100644
--- a/ui/src/app/components/js-func.scss
+++ b/ui/src/app/components/js-func.scss
@@ -15,6 +15,12 @@
  */
 tb-js-func {
   position: relative;
+  .tb-disabled {
+    color: rgba(0,0,0,0.38);
+  }
+  .fill-height {
+    height: 100%;
+  }
 }
 
 .tb-js-func-panel {
@@ -23,8 +29,10 @@ tb-js-func {
   height: 100%;
   #tb-javascript-input {
     min-width: 200px;
-    min-height: 200px;
     width: 100%;
     height: 100%;
+    &:not(.fill-height) {
+      min-height: 200px;
+    }
   }
 }
diff --git a/ui/src/app/components/js-func.tpl.html b/ui/src/app/components/js-func.tpl.html
index 806de4a..93043d4 100644
--- a/ui/src/app/components/js-func.tpl.html
+++ b/ui/src/app/components/js-func.tpl.html
@@ -15,19 +15,20 @@
     limitations under the License.
 
 -->
-<div style="background: #fff;" tb-expand-fullscreen fullscreen-zindex="100" expand-button-id="expand-button" on-fullscreen-changed="onFullscreenChanged()" layout="column">
+<div style="background: #fff;" ng-class="{'tb-disabled': disabled, 'fill-height': fillHeight}" tb-expand-fullscreen fullscreen-zindex="100" expand-button-id="expand-button" on-fullscreen-changed="onFullscreenChanged()" layout="column">
 	<div layout="row" layout-align="start center" style="height: 40px;">
-		<span style="font-style: italic;">function({{ functionArgsString }}) {</span>
+		<label class="tb-title no-padding">function {{ functionName }}({{ functionArgsString }}) {</label>
 		<span flex></span>
 		<div id="expand-button" layout="column" aria-label="Fullscreen" class="md-button md-icon-button tb-md-32 tb-fullscreen-button-style"></div>
 	</div>
 	<div flex id="tb-javascript-panel" class="tb-js-func-panel" layout="column">
-		<div flex id="tb-javascript-input"
-			 ui-ace="jsEditorOptions" 
+		<div flex id="tb-javascript-input" ng-class="{'fill-height': fillHeight}"
+			 ui-ace="jsEditorOptions"
+			 ng-readonly="disabled"
 			 ng-model="functionBody">
 		</div>
 	</div>
 	<div layout="row" layout-align="start center"  style="height: 40px;">
-		<span style="font-style: italic;">}</span>
-	</div>	   
-</div>
\ No newline at end of file
+		<label class="tb-title no-padding">}</label>
+	</div>
+</div>
diff --git a/ui/src/app/locale/locale.constant.js b/ui/src/app/locale/locale.constant.js
index 5dce787..b511b56 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 7306762..c674467 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 4eba5b2..dd48bb0 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.scss b/ui/src/app/rulechain/rulenode.scss
index febc637..0466673 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
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 0000000..4b75c79
--- /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 0000000..32d5347
--- /dev/null
+++ b/ui/src/app/rulechain/rulenode-config.tpl.html
@@ -0,0 +1,32 @@
+<!--
+
+    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-rule-node-defined-config ng-if="useDefinedDirective()"
+                             ng-model="configuration"
+                             rule-node-directive="{{nodeDefinition.configDirective}}"
+                             ng-required="required"
+                             ng-readonly="readonly">
+</tb-rule-node-defined-config>
+<div class="tb-rulenode-directive-error" ng-if="definedDirectiveError">{{definedDirectiveError}}</div>
+<tb-json-object-edit ng-if="!useDefinedDirective()"
+                     class="tb-rule-node-configuration-json"
+                     ng-model="configuration"
+                     label="{{ 'rulenode.configuration' | translate }}"
+                     ng-required="required"
+                     fill-height="true">
+</tb-json-object-edit>
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 0000000..5ec2620
--- /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"></${directive}>`;
+            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 30cf075..ad109ef 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 }}
                 </md-checkbox>
             </md-input-container>
-            <tb-json-object-edit class="tb-rule-node-configuration-json" ng-model="ruleNode.configuration"
+            <tb-rule-node-config ng-model="ruleNode.configuration"
+                                 ng-required="true"
+                                 node-definition="ruleNode.component.configurationDescriptor.nodeDefinition"
+                                 ng-readonly="$root.loading || !isEdit || isReadOnly">
+            </tb-rule-node-config>
+            <!--tb-json-object-edit class="tb-rule-node-configuration-json" ng-model="ruleNode.configuration"
                                  label="{{ 'rulenode.configuration' | translate }}"
                                  ng-required="true"
                                  fill-height="true">
-            </tb-json-object-edit>
+            </tb-json-object-edit-->
             <md-input-container class="md-block">
                 <label translate>rulenode.description</label>
                 <textarea ng-model="ruleNode.additionalInfo.description" rows="2"></textarea>