thingsboard-developers

Rule Node Debug UI

3/29/2018 2:25:47 PM

Details

diff --git a/application/src/main/java/org/thingsboard/server/actors/ActorSystemContext.java b/application/src/main/java/org/thingsboard/server/actors/ActorSystemContext.java
index a5a20b8..9e02946 100644
--- a/application/src/main/java/org/thingsboard/server/actors/ActorSystemContext.java
+++ b/application/src/main/java/org/thingsboard/server/actors/ActorSystemContext.java
@@ -25,6 +25,7 @@ import com.typesafe.config.Config;
 import com.typesafe.config.ConfigFactory;
 import lombok.Getter;
 import lombok.Setter;
+import lombok.extern.slf4j.Slf4j;
 import org.springframework.beans.factory.annotation.Autowired;
 import org.springframework.beans.factory.annotation.Value;
 import org.springframework.stereotype.Component;
@@ -38,6 +39,7 @@ import org.thingsboard.server.common.data.id.EntityId;
 import org.thingsboard.server.common.data.id.TenantId;
 import org.thingsboard.server.common.data.plugin.ComponentLifecycleEvent;
 import org.thingsboard.server.common.msg.TbMsg;
+import org.thingsboard.server.common.msg.TbMsgDataType;
 import org.thingsboard.server.common.msg.cluster.ServerAddress;
 import org.thingsboard.server.common.transport.auth.DeviceAuthService;
 import org.thingsboard.server.controller.plugin.PluginWebSocketMsgEndpoint;
@@ -60,11 +62,13 @@ import org.thingsboard.server.service.cluster.routing.ClusterRoutingService;
 import org.thingsboard.server.service.cluster.rpc.ClusterRpcService;
 import org.thingsboard.server.service.component.ComponentDiscoveryService;
 
+import java.io.IOException;
 import java.io.PrintWriter;
 import java.io.StringWriter;
 import java.nio.charset.StandardCharsets;
 import java.util.Optional;
 
+@Slf4j
 @Component
 public class ActorSystemContext {
     private static final String AKKA_CONF_FILE_NAME = "actor-system.conf";
@@ -292,38 +296,49 @@ public class ActorSystemContext {
     }
 
     private void persistDebug(TenantId tenantId, EntityId entityId, String type, TbMsg tbMsg, Throwable error) {
-        Event event = new Event();
-        event.setTenantId(tenantId);
-        event.setEntityId(entityId);
-        event.setType(DataConstants.DEBUG);
-
-        ObjectNode node = mapper.createObjectNode()
-                .put("type", type)
-                .put("server", getServerAddress())
-                .put("entityId", tbMsg.getOriginator().getId().toString())
-                .put("entityName", tbMsg.getOriginator().getEntityType().name())
-                .put("msgId", tbMsg.getId().toString())
-                .put("msgType", tbMsg.getType())
-                .put("dataType", tbMsg.getDataType().name());
-
-        ObjectNode mdNode = node.putObject("metadata");
-        tbMsg.getMetaData().getData().forEach(mdNode::put);
+        try {
+            Event event = new Event();
+            event.setTenantId(tenantId);
+            event.setEntityId(entityId);
+            event.setType(DataConstants.DEBUG_RULE_NODE);
+
+            String metadata = mapper.writeValueAsString(tbMsg.getMetaData().getData());
+
+            ObjectNode node = mapper.createObjectNode()
+                    .put("type", type)
+                    .put("server", getServerAddress())
+                    .put("entityId", tbMsg.getOriginator().getId().toString())
+                    .put("entityName", tbMsg.getOriginator().getEntityType().name())
+                    .put("msgId", tbMsg.getId().toString())
+                    .put("msgType", tbMsg.getType())
+                    .put("dataType", tbMsg.getDataType().name())
+                    .put("data", convertToString(tbMsg.getDataType(), tbMsg.getData()))
+                    .put("metadata", metadata);
+
+            if (error != null) {
+                node = node.put("error", toString(error));
+            }
+
+            event.setBody(node);
+            eventService.save(event);
+        } catch (IOException ex) {
+            log.warn("Failed to persist rule node debug message", ex);
+        }
+    }
 
-        switch (tbMsg.getDataType()) {
+    private String convertToString(TbMsgDataType messageType, byte[] data) {
+        if (data == null) {
+            return null;
+        }
+        switch (messageType) {
+            case JSON:
+            case TEXT:
+                return new String(data, StandardCharsets.UTF_8);
             case BINARY:
-                node.put("data", Base64Utils.encodeUrlSafe(tbMsg.getData()));
-                break;
+                return Base64Utils.encodeToString(data);
             default:
-                node.put("data", new String(tbMsg.getData(), StandardCharsets.UTF_8));
-                break;
-        }
-
-        if (error != null) {
-            node = node.put("error", toString(error));
+                throw new RuntimeException("Message type: " + messageType + " is not supported!");
         }
-
-        event.setBody(node);
-        eventService.save(event);
     }
 
     public static Exception toException(Throwable error) {
diff --git a/application/src/test/java/org/thingsboard/server/controller/AbstractRuleEngineControllerTest.java b/application/src/test/java/org/thingsboard/server/controller/AbstractRuleEngineControllerTest.java
index bbcb98f..93fe767 100644
--- a/application/src/test/java/org/thingsboard/server/controller/AbstractRuleEngineControllerTest.java
+++ b/application/src/test/java/org/thingsboard/server/controller/AbstractRuleEngineControllerTest.java
@@ -51,6 +51,6 @@ public class AbstractRuleEngineControllerTest extends AbstractControllerTest {
         TimePageLink pageLink = new TimePageLink(limit);
         return doGetTypedWithTimePageLink("/api/events/{entityType}/{entityId}/{eventType}?tenantId={tenantId}&",
                 new TypeReference<TimePageData<Event>>() {
-                }, pageLink, entityId.getEntityType(), entityId.getId(), DataConstants.DEBUG, tenantId.getId());
+                }, pageLink, entityId.getEntityType(), entityId.getId(), DataConstants.DEBUG_RULE_NODE, tenantId.getId());
     }
 }
diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/DataConstants.java b/common/data/src/main/java/org/thingsboard/server/common/data/DataConstants.java
index 659a242..7d4e480 100644
--- a/common/data/src/main/java/org/thingsboard/server/common/data/DataConstants.java
+++ b/common/data/src/main/java/org/thingsboard/server/common/data/DataConstants.java
@@ -37,7 +37,7 @@ public class DataConstants {
     public static final String ERROR = "ERROR";
     public static final String LC_EVENT = "LC_EVENT";
     public static final String STATS = "STATS";
-    public static final String DEBUG = "DEBUG";
+    public static final String DEBUG_RULE_NODE = "DEBUG_RULE_NODE";
 
     public static final String ONEWAY = "ONEWAY";
     public static final String TWOWAY = "TWOWAY";
diff --git a/ui/src/app/common/types.constant.js b/ui/src/app/common/types.constant.js
index 8026115..2186508 100644
--- a/ui/src/app/common/types.constant.js
+++ b/ui/src/app/common/types.constant.js
@@ -279,6 +279,23 @@ export default angular.module('thingsboard.types', [])
                 function: "function",
                 alarm: "alarm"
             },
+            contentType: {
+                "JSON": {
+                    value: "JSON",
+                    name: "content-type.json",
+                    code: "json"
+                },
+                "TEXT": {
+                    value: "TEXT",
+                    name: "content-type.text",
+                    code: "text"
+                },
+                "BINARY": {
+                    value: "BINARY",
+                    name: "content-type.binary",
+                    code: "text"
+                }
+            },
             componentType: {
                 filter: "FILTER",
                 processor: "PROCESSOR",
@@ -295,7 +312,8 @@ export default angular.module('thingsboard.types', [])
                 user: "USER",
                 dashboard: "DASHBOARD",
                 alarm: "ALARM",
-                rulechain: "RULE_CHAIN"
+                rulechain: "RULE_CHAIN",
+                rulenode: "RULE_NODE"
             },
             aliasEntityType: {
                 current_customer: "CURRENT_CUSTOMER"
@@ -388,6 +406,16 @@ export default angular.module('thingsboard.types', [])
                     name: "event.type-stats"
                 }
             },
+            debugEventType: {
+                debugRuleNode: {
+                    value: "DEBUG_RULE_NODE",
+                    name: "event.type-debug-rule-node"
+                },
+                debugRuleChain: {
+                    value: "DEBUG_RULE_CHAIN",
+                    name: "event.type-debug-rule-chain"
+                }
+            },
             extensionType: {
                 http: "HTTP",
                 mqtt: "MQTT",
diff --git a/ui/src/app/components/details-sidenav.directive.js b/ui/src/app/components/details-sidenav.directive.js
index e455a80..a25374b 100644
--- a/ui/src/app/components/details-sidenav.directive.js
+++ b/ui/src/app/components/details-sidenav.directive.js
@@ -26,7 +26,7 @@ export default angular.module('thingsboard.directives.detailsSidenav', [])
     .name;
 
 /*@ngInject*/
-function DetailsSidenav($timeout) {
+function DetailsSidenav($timeout, $window) {
 
     var linker = function (scope, element, attrs) {
 
@@ -42,6 +42,23 @@ function DetailsSidenav($timeout) {
             scope.isEdit = true;
         }
 
+        if (angular.isDefined(attrs.closeOnClickOutside && attrs.closeOnClickOutside)) {
+            scope.closeOnClickOutside = true;
+            var clickOutsideHandler = function() {
+                scope.closeDetails();
+            };
+            angular.element($window).click(clickOutsideHandler);
+            scope.$on("$destroy", function () {
+                angular.element($window).unbind('click', clickOutsideHandler);
+            });
+        }
+
+        scope.onClick = function($event) {
+            if (scope.closeOnClickOutside) {
+                $event.stopPropagation();
+            }
+        };
+
         scope.toggleDetailsEditMode = function () {
             if (!scope.isAlwaysEdit) {
                 if (!scope.isEdit) {
diff --git a/ui/src/app/components/details-sidenav.tpl.html b/ui/src/app/components/details-sidenav.tpl.html
index c504a24..a0032ff 100644
--- a/ui/src/app/components/details-sidenav.tpl.html
+++ b/ui/src/app/components/details-sidenav.tpl.html
@@ -19,6 +19,7 @@
       md-disable-backdrop="true"
       md-is-open="isOpen"
       md-component-id="right"
+      ng-click="onClick($event)"
       layout="column">
       <header>
 	      <md-toolbar class="md-theme-light" ng-style="{'height':headerHeightPx+'px'}">
diff --git a/ui/src/app/event/event-content-dialog.controller.js b/ui/src/app/event/event-content-dialog.controller.js
index 108f95e..8d13f96 100644
--- a/ui/src/app/event/event-content-dialog.controller.js
+++ b/ui/src/app/event/event-content-dialog.controller.js
@@ -17,11 +17,14 @@ import $ from 'jquery';
 import 'brace/ext/language_tools';
 import 'brace/mode/java';
 import 'brace/theme/github';
+import beautify from 'js-beautify';
 
 /* eslint-disable angular/angularelement */
 
+const js_beautify = beautify.js;
+
 /*@ngInject*/
-export default function EventContentDialogController($mdDialog, content, title, showingCallback) {
+export default function EventContentDialogController($mdDialog, types, content, contentType, title, showingCallback) {
 
     var vm = this;
 
@@ -32,9 +35,19 @@ export default function EventContentDialogController($mdDialog, content, title, 
     vm.content = content;
     vm.title = title;
 
+    var mode;
+    if (contentType) {
+        mode = types.contentType[contentType].code;
+        if (contentType == types.contentType.JSON.value && vm.content) {
+            vm.content = js_beautify(vm.content, {indent_size: 4});
+        }
+    } else {
+        mode = 'java';
+    }
+
     vm.contentOptions = {
         useWrapMode: false,
-        mode: 'java',
+        mode: mode,
         showGutter: false,
         showPrintMargin: false,
         theme: 'github',
diff --git a/ui/src/app/event/event-header.directive.js b/ui/src/app/event/event-header.directive.js
index afac804..bc4cdbe 100644
--- a/ui/src/app/event/event-header.directive.js
+++ b/ui/src/app/event/event-header.directive.js
@@ -18,6 +18,7 @@
 import eventHeaderLcEventTemplate from './event-header-lc-event.tpl.html';
 import eventHeaderStatsTemplate from './event-header-stats.tpl.html';
 import eventHeaderErrorTemplate from './event-header-error.tpl.html';
+import eventHeaderDebugRuleNodeTemplate from './event-header-debug-rulenode.tpl.html';
 
 /* eslint-enable import/no-unresolved, import/default */
 
@@ -38,6 +39,12 @@ export default function EventHeaderDirective($compile, $templateCache, types) {
                 case types.eventType.error.value:
                     template = eventHeaderErrorTemplate;
                     break;
+                case types.debugEventType.debugRuleNode.value:
+                    template = eventHeaderDebugRuleNodeTemplate;
+                    break;
+                case types.debugEventType.debugRuleChain.value:
+                    template = eventHeaderDebugRuleNodeTemplate;
+                    break;
             }
             return $templateCache.get(template);
         }
diff --git a/ui/src/app/event/event-header-debug-rulenode.tpl.html b/ui/src/app/event/event-header-debug-rulenode.tpl.html
new file mode 100644
index 0000000..b412a0c
--- /dev/null
+++ b/ui/src/app/event/event-header-debug-rulenode.tpl.html
@@ -0,0 +1,27 @@
+<!--
+
+    Copyright © 2016-2018 The Thingsboard Authors
+
+    Licensed under the Apache License, Version 2.0 (the "License");
+    you may not use this file except in compliance with the License.
+    You may obtain a copy of the License at
+
+        http://www.apache.org/licenses/LICENSE-2.0
+
+    Unless required by applicable law or agreed to in writing, software
+    distributed under the License is distributed on an "AS IS" BASIS,
+    WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+    See the License for the specific language governing permissions and
+    limitations under the License.
+
+-->
+<div hide-xs hide-sm translate class="tb-cell" flex="30">event.event-time</div>
+<div translate class="tb-cell" flex="20">event.server</div>
+<div translate class="tb-cell" flex="20">event.type</div>
+<div translate class="tb-cell" flex="20">event.entity</div>
+<div translate class="tb-cell" flex="20">event.message-id</div>
+<div translate class="tb-cell" flex="20">event.message-type</div>
+<div translate class="tb-cell" flex="20">event.data-type</div>
+<div translate class="tb-cell" flex="20">event.data</div>
+<div translate class="tb-cell" flex="20">event.metadata</div>
+<div translate class="tb-cell" flex="20">event.error</div>
diff --git a/ui/src/app/event/event-row.directive.js b/ui/src/app/event/event-row.directive.js
index f005542..4643761 100644
--- a/ui/src/app/event/event-row.directive.js
+++ b/ui/src/app/event/event-row.directive.js
@@ -20,6 +20,7 @@ import eventErrorDialogTemplate from './event-content-dialog.tpl.html';
 import eventRowLcEventTemplate from './event-row-lc-event.tpl.html';
 import eventRowStatsTemplate from './event-row-stats.tpl.html';
 import eventRowErrorTemplate from './event-row-error.tpl.html';
+import eventRowDebugRuleNodeTemplate from './event-row-debug-rulenode.tpl.html';
 
 /* eslint-enable import/no-unresolved, import/default */
 
@@ -40,6 +41,12 @@ export default function EventRowDirective($compile, $templateCache, $mdDialog, $
                 case types.eventType.error.value:
                     template = eventRowErrorTemplate;
                     break;
+                case types.debugEventType.debugRuleNode.value:
+                    template = eventRowDebugRuleNodeTemplate;
+                    break;
+                case types.debugEventType.debugRuleChain.value:
+                    template = eventRowDebugRuleNodeTemplate;
+                    break;
             }
             return $templateCache.get(template);
         }
@@ -53,17 +60,22 @@ export default function EventRowDirective($compile, $templateCache, $mdDialog, $
             scope.loadTemplate();
         });
 
+        scope.types = types;
+
         scope.event = attrs.event;
 
-        scope.showContent = function($event, content, title) {
+        scope.showContent = function($event, content, title, contentType) {
             var onShowingCallback = {
                 onShowing: function(){}
             }
+            if (!contentType) {
+                contentType = null;
+            }
             $mdDialog.show({
                 controller: 'EventContentDialogController',
                 controllerAs: 'vm',
                 templateUrl: eventErrorDialogTemplate,
-                locals: {content: content, title: title, showingCallback: onShowingCallback},
+                locals: {content: content, title: title, contentType: contentType, showingCallback: onShowingCallback},
                 parent: angular.element($document[0].body),
                 fullscreen: true,
                 targetEvent: $event,
diff --git a/ui/src/app/event/event-row-debug-rulenode.tpl.html b/ui/src/app/event/event-row-debug-rulenode.tpl.html
new file mode 100644
index 0000000..ec00b39
--- /dev/null
+++ b/ui/src/app/event/event-row-debug-rulenode.tpl.html
@@ -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.
+
+-->
+<div hide-xs hide-sm class="tb-cell" flex="30">{{event.createdTime | date :  'yyyy-MM-dd HH:mm:ss'}}</div>
+<div class="tb-cell" flex="20">{{event.body.server}}</div>
+<div class="tb-cell" flex="20">{{event.body.type}}</div>
+<div class="tb-cell" flex="20">{{event.body.entityName}}</div>
+<div class="tb-cell" flex="20">{{event.body.msgId}}</div>
+<div class="tb-cell" flex="20">{{event.body.msgType}}</div>
+<div class="tb-cell" flex="20">{{event.body.dataType}}</div>
+<div class="tb-cell" flex="20">
+    <md-button ng-if="event.body.data" class="md-icon-button md-primary"
+               ng-click="showContent($event, event.body.data, 'event.data', event.body.msgType)"
+               aria-label="{{ 'action.view' | translate }}">
+        <md-tooltip md-direction="top">
+            {{ 'action.view' | translate }}
+        </md-tooltip>
+        <md-icon aria-label="{{ 'action.view' | translate }}"
+                 class="material-icons">
+            more_horiz
+        </md-icon>
+    </md-button>
+</div>
+<div class="tb-cell" flex="20">
+    <md-button ng-if="event.body.metadata" class="md-icon-button md-primary"
+               ng-click="showContent($event, event.body.metadata, 'event.metadata', 'JSON')"
+               aria-label="{{ 'action.view' | translate }}">
+        <md-tooltip md-direction="top">
+            {{ 'action.view' | translate }}
+        </md-tooltip>
+        <md-icon aria-label="{{ 'action.view' | translate }}"
+                 class="material-icons">
+            more_horiz
+        </md-icon>
+    </md-button>
+</div>
+<div class="tb-cell" flex="20">
+    <md-button ng-if="event.body.error" class="md-icon-button md-primary"
+               ng-click="showContent($event, event.body.error, 'event.error')"
+               aria-label="{{ 'action.view' | translate }}">
+        <md-tooltip md-direction="top">
+            {{ 'action.view' | translate }}
+        </md-tooltip>
+        <md-icon aria-label="{{ 'action.view' | translate }}"
+                 class="material-icons">
+            more_horiz
+        </md-icon>
+    </md-button>
+</div>
diff --git a/ui/src/app/event/event-table.directive.js b/ui/src/app/event/event-table.directive.js
index 4291014..c61078d 100644
--- a/ui/src/app/event/event-table.directive.js
+++ b/ui/src/app/event/event-table.directive.js
@@ -36,8 +36,8 @@ export default function EventTableDirective($compile, $templateCache, $rootScope
             for (var type in types.eventType) {
                 var eventType = types.eventType[type];
                 var enabled = true;
-                for (var disabledType in disabledEventTypes) {
-                    if (eventType.value === disabledEventTypes[disabledType]) {
+                for (var i=0;i<disabledEventTypes.length;i++) {
+                    if (eventType.value === disabledEventTypes[i]) {
                         enabled = false;
                         break;
                     }
@@ -47,7 +47,19 @@ export default function EventTableDirective($compile, $templateCache, $rootScope
                 }
             }
         } else {
-            scope.eventTypes = types.eventType;
+            scope.eventTypes = angular.copy(types.eventType);
+        }
+
+        if (attrs.debugEventTypes) {
+            var debugEventTypes = attrs.debugEventTypes.split(',');
+            for (i=0;i<debugEventTypes.length;i++) {
+                for (type in types.debugEventType) {
+                    eventType = types.debugEventType[type];
+                    if (eventType.value === debugEventTypes[i]) {
+                        scope.eventTypes[type] = eventType;
+                    }
+                }
+            }
         }
 
         scope.eventType = attrs.defaultEventType;
diff --git a/ui/src/app/locale/locale.constant.js b/ui/src/app/locale/locale.constant.js
index a70ca14..0a17e39 100644
--- a/ui/src/app/locale/locale.constant.js
+++ b/ui/src/app/locale/locale.constant.js
@@ -341,6 +341,11 @@ export default angular.module('thingsboard.locale', [])
                     "enter-password": "Enter password",
                     "enter-search": "Enter search"
                 },
+                "content-type": {
+                    "json": "Json",
+                    "text": "Text",
+                    "binary": "Binary (Base64)"
+                },
                 "customer": {
                     "customer": "Customer",
                     "customers": "Customers",
@@ -762,6 +767,8 @@ export default angular.module('thingsboard.locale', [])
                     "type-error": "Error",
                     "type-lc-event": "Lifecycle event",
                     "type-stats": "Statistics",
+                    "type-debug-rule-node": "Debug",
+                    "type-debug-rule-chain": "Debug",
                     "no-events-prompt": "No events found",
                     "error": "Error",
                     "alarm": "Alarm",
@@ -769,6 +776,13 @@ export default angular.module('thingsboard.locale', [])
                     "server": "Server",
                     "body": "Body",
                     "method": "Method",
+                    "type": "Type",
+                    "entity": "Entity",
+                    "message-id": "Message Id",
+                    "message-type": "Message Type",
+                    "data-type": "Data Type",
+                    "metadata": "Metadata",
+                    "data": "Data",
                     "event": "Event",
                     "status": "Status",
                     "success": "Success",
@@ -1172,6 +1186,7 @@ export default angular.module('thingsboard.locale', [])
                 },
                 "rulenode": {
                     "details": "Details",
+                    "events": "Events",
                     "add": "Add rule node",
                     "name": "Name",
                     "name-required": "Name is required.",
diff --git a/ui/src/app/rulechain/rulechain.controller.js b/ui/src/app/rulechain/rulechain.controller.js
index fbdda22..83bca32 100644
--- a/ui/src/app/rulechain/rulechain.controller.js
+++ b/ui/src/app/rulechain/rulechain.controller.js
@@ -28,7 +28,7 @@ import addRuleNodeLinkTemplate from './add-link.tpl.html';
 /* eslint-enable import/no-unresolved, import/default */
 
 /*@ngInject*/
-export function RuleChainController($stateParams, $scope, $compile, $q, $mdUtil, $timeout, $mdExpansionPanel, $document, $mdDialog,
+export function RuleChainController($stateParams, $scope, $compile, $q, $mdUtil, $timeout, $mdExpansionPanel, $window, $document, $mdDialog,
                                     $filter, $translate, hotkeys, types, ruleChainService, Modelfactory, flowchartConstants,
                                     ruleChain, ruleChainMetaData, ruleNodeComponents) {
 
@@ -77,6 +77,8 @@ export function RuleChainController($stateParams, $scope, $compile, $q, $mdUtil,
     vm.objectsSelected = objectsSelected;
     vm.deleteSelected = deleteSelected;
 
+    vm.triggerResize = triggerResize;
+
     initHotKeys();
 
     function initHotKeys() {
@@ -129,18 +131,17 @@ export function RuleChainController($stateParams, $scope, $compile, $q, $mdUtil,
     }
 
     vm.onEditRuleNodeClosed = function() {
-        vm.editingRuleNode = null;
+        //vm.editingRuleNode = null;
     };
 
     vm.onEditRuleNodeLinkClosed = function() {
-        vm.editingRuleNodeLink = null;
+        //vm.editingRuleNodeLink = null;
     };
 
     vm.saveRuleNode = function(theForm) {
         $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);
         }
@@ -148,7 +149,6 @@ export function RuleChainController($stateParams, $scope, $compile, $q, $mdUtil,
 
     vm.saveRuleNodeLink = function(theForm) {
         theForm.$setPristine();
-        vm.isEditingRuleNodeLink = false;
         vm.ruleChainModel.edges[vm.editingRuleNodeLinkIndex] = vm.editingRuleNodeLink;
         vm.editingRuleNodeLink = angular.copy(vm.editingRuleNodeLink);
     };
@@ -663,6 +663,11 @@ export function RuleChainController($stateParams, $scope, $compile, $q, $mdUtil,
     function deleteSelected() {
         vm.modelservice.deleteSelected();
     }
+
+    function triggerResize() {
+        var w = angular.element($window);
+        w.triggerHandler('resize');
+    }
 }
 
 /*@ngInject*/
diff --git a/ui/src/app/rulechain/rulechain.tpl.html b/ui/src/app/rulechain/rulechain.tpl.html
index 3083f83..0d55771 100644
--- a/ui/src/app/rulechain/rulechain.tpl.html
+++ b/ui/src/app/rulechain/rulechain.tpl.html
@@ -67,8 +67,9 @@
                             header-title="{{vm.editingRuleNode.name}}"
                             header-subtitle="{{(vm.types.ruleNodeType[vm.editingRuleNode.component.type].name | translate)
                             + ' - ' + vm.editingRuleNode.component.name}}"
-                            is-read-only="false"
+                            is-read-only="vm.selectedRuleNodeTabIndex > 0"
                             is-open="vm.isEditingRuleNode"
+                            close-on-click-outside="true"
                             is-always-edit="true"
                             on-close-details="vm.onEditRuleNodeClosed()"
                             on-toggle-details-edit-mode="vm.onRevertRuleNodeEdit(vm.ruleNodeForm)"
@@ -77,9 +78,10 @@
             <details-buttons tb-help="vm.helpLinkIdForRuleNodeType()" help-container-id="help-container">
                 <div id="help-container"></div>
             </details-buttons>
-            <md-tabs id="ruleNodeTabs" md-border-bottom flex class="tb-absolute-fill">
+            <md-tabs md-selected="vm.selectedRuleNodeTabIndex"
+                     id="ruleNodeTabs" md-border-bottom flex class="tb-absolute-fill" ng-if="vm.isEditingRuleNode">
                 <md-tab label="{{ 'rulenode.details' | translate }}">
-                    <form name="vm.ruleNodeForm" ng-if="vm.isEditingRuleNode">
+                    <form name="vm.ruleNodeForm">
                         <tb-rule-node
                                 rule-node="vm.editingRuleNode"
                                 rule-chain-id="vm.ruleChain.id.id"
@@ -90,6 +92,15 @@
                         </tb-rule-node>
                     </form>
                 </md-tab>
+                <md-tab ng-if="vm.isEditingRuleNode && vm.editingRuleNode.ruleNodeId"
+                        md-on-select="vm.triggerResize()" label="{{ 'rulenode.events' | translate }}">
+                    <tb-event-table flex entity-type="vm.types.entityType.rulenode"
+                                    entity-id="vm.editingRuleNode.ruleNodeId.id"
+                                    tenant-id="vm.ruleChain.tenantId.id"
+                                    debug-event-types="{{vm.types.debugEventType.debugRuleNode.value}}"
+                                    default-event-type="{{vm.types.debugEventType.debugRuleNode.value}}">
+                    </tb-event-table>
+                </md-tab>
             </md-tabs>
         </tb-details-sidenav>
         <tb-details-sidenav class="tb-rulenode-link-details-sidenav"
@@ -97,6 +108,7 @@
                             header-subtitle="{{'rulenode.link-details' | translate}}"
                             is-read-only="false"
                             is-open="vm.isEditingRuleNodeLink"
+                            close-on-click-outside="true"
                             is-always-edit="true"
                             on-close-details="vm.onEditRuleNodeLinkClosed()"
                             on-toggle-details-edit-mode="vm.onRevertRuleNodeLinkEdit(vm.ruleNodeLinkForm)"
diff --git a/ui/src/app/rulechain/rulechains.tpl.html b/ui/src/app/rulechain/rulechains.tpl.html
index a4fbd79..cf9d256 100644
--- a/ui/src/app/rulechain/rulechains.tpl.html
+++ b/ui/src/app/rulechain/rulechains.tpl.html
@@ -55,7 +55,8 @@
             <tb-event-table flex entity-type="vm.types.entityType.rulechain"
                             entity-id="vm.grid.operatingItem().id.id"
                             tenant-id="vm.grid.operatingItem().tenantId.id"
-                            default-event-type="{{vm.types.eventType.lcEvent.value}}">
+                            debug-event-types="{{vm.types.debugEventType.debugRuleChain.value}}"
+                            default-event-type="{{vm.types.debugEventType.debugRuleChain.value}}">
             </tb-event-table>
         </md-tab>
         <md-tab ng-if="!vm.grid.detailsConfig.isDetailsEditMode && vm.isRuleChainEditable(vm.grid.operatingItem())" md-on-select="vm.grid.triggerResize()" label="{{ 'relation.relations' | translate }}">